UNPKG

8.6 kBJavaScriptView Raw
1// Copyright 2020 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4'use strict';
5
6const fs = require('fs');
7const path = require('path');
8
9const SRC_PATH = path.resolve(__dirname, '..');
10const NODE_MODULES_PATH = path.resolve(SRC_PATH, 'node_modules');
11const espree = require(path.resolve(NODE_MODULES_PATH, '@typescript-eslint', 'parser'));
12const parseOptions = {
13 ecmaVersion: 11,
14 sourceType: 'module',
15 range: true,
16};
17
18const USER_METRICS_ENUM_ENDPOINT = '__lastValidEnumPosition';
19
20/**
21 * Determines if a node is a class declaration.
22 * If className is provided, node must also match class name.
23 */
24function isClassNameDeclaration(node, className) {
25 const isClassDeclaration = node.type === 'ExportNamedDeclaration' && node.declaration.type === 'ClassDeclaration';
26 if (className) {
27 return isClassDeclaration && node.declaration.id.name === className;
28 }
29 return isClassDeclaration;
30}
31
32/**
33 * Determines if a node is an typescript enum declaration.
34 * If enumName is provided, node must also match enum name.
35 */
36function isEnumDeclaration(node, enumName) {
37 const isEnumDeclaration = node.type === 'ExportNamedDeclaration' && node.declaration.type === 'TSEnumDeclaration';
38 if (enumName) {
39 return isEnumDeclaration && node.declaration.id.name === enumName;
40 }
41 return isEnumDeclaration;
42}
43
44/**
45 * Finds a function declaration node inside a class declaration node
46 */
47function findFunctionInClass(classNode, functionName) {
48 for (const node of classNode.declaration.body.body) {
49 if (node.key.name === functionName) {
50 return node;
51 }
52 }
53 return null;
54}
55
56/**
57 * Determines if AST Node is a call to register a DevtoolsExperiment
58 */
59function isExperimentRegistrationCall(node) {
60 return node.expression && node.expression.type === 'CallExpression' &&
61 node.expression.callee.property.name === 'register';
62}
63
64/**
65 * Extract the enum Root.Runtime.ExperimentName to a map
66 */
67function getExperimentNameEnum(mainImplFile) {
68 const mainAST = espree.parse(mainImplFile, parseOptions);
69
70 let experimentNameEnum;
71 for (const node of mainAST.body) {
72 if (isEnumDeclaration(node, 'ExperimentName')) {
73 experimentNameEnum = node;
74 break;
75 }
76 }
77
78 const map = new Map();
79 if (!experimentNameEnum) {
80 return map;
81 }
82 for (const member of experimentNameEnum.declaration.members) {
83 map.set(member.id.name, member.initializer.value);
84 }
85 return map;
86}
87
88/**
89 * Determine if node is of the form Root.Runtime.ExperimentName.NAME, and if so
90 * return NAME as string.
91 */
92function isExperimentNameReference(node) {
93 if (node.type !== 'MemberExpression') {
94 return false;
95 }
96 if (node.object.type !== 'MemberExpression' || node.object.property?.name !== 'ExperimentName') {
97 return false;
98 }
99 if (node.object.object.type !== 'MemberExpression' || node.object.object.property?.name !== 'Runtime') {
100 return false;
101 }
102 if (node.object.object.object.type !== 'Identifier' || node.object.object.object.name !== 'Root') {
103 return false;
104 }
105 return node.property.name;
106}
107
108/**
109 * Gets list of experiments registered in MainImpl.js.
110 */
111function getMainImplExperimentList(mainImplFile, experimentNames) {
112 const mainAST = espree.parse(mainImplFile, parseOptions);
113
114 // Find MainImpl Class node
115 let mainImplClassNode;
116 for (const node of mainAST.body) {
117 if (isClassNameDeclaration(node, 'MainImpl')) {
118 mainImplClassNode = node;
119 break;
120 }
121 }
122 if (!mainImplClassNode) {
123 return null;
124 }
125
126 // Find function in MainImpl Class
127 const initializeExperimentNode = findFunctionInClass(mainImplClassNode, 'initializeExperiments');
128 if (!initializeExperimentNode) {
129 return null;
130 }
131
132 // Get list of experiments
133 const experiments = [];
134 for (const statement of initializeExperimentNode.value.body.body) {
135 if (isExperimentRegistrationCall(statement)) {
136 // Experiment name is first argument of registration call
137 const experimentNameArg = statement.expression.arguments[0];
138 // The experiment name can either be a literal, e.g. 'fooExperiment'..
139 if (experimentNameArg.type === 'Literal') {
140 experiments.push(experimentNameArg.value);
141 } else {
142 // .. or a member of Root.Runtime.ExperimentName.
143 const experimentName = isExperimentNameReference(experimentNameArg);
144 if (experimentName) {
145 const translatedName = experimentNames.get(experimentName);
146 if (!translatedName) {
147 console.log('Failed to resolve Root.Runtime.ExperimentName.${experimentName} to a string');
148 process.exit(1);
149 }
150 experiments.push(translatedName);
151 } else {
152 console.log('Unexpected argument to Root.Runtime.experiments.register: ', experimentNameArg);
153 process.exit(1);
154 }
155 }
156 }
157 }
158 return experiments.length ? experiments : null;
159}
160
161/**
162 * Determines if AST Node is the DevtoolsExperiments Enum declaration
163 */
164function isExperimentEnumDeclaration(node) {
165 return node.type === 'ExportNamedDeclaration' && node.declaration.declarations &&
166 node.declaration.declarations[0].id.name === 'DevtoolsExperiments';
167}
168
169/**
170 * Gets list of experiments registered in UserMetrics.ts
171 */
172function getUserMetricExperimentList(userMetricsFile) {
173 const userMetricsAST = espree.parse(userMetricsFile, {ecmaVersion: 11, sourceType: 'module', range: true});
174 for (const node of userMetricsAST.body) {
175 if (isExperimentEnumDeclaration(node)) {
176 return node.declaration.declarations[0].init.properties.map(property => {
177 return property.key.value;
178 });
179 }
180 }
181 return null;
182}
183
184/**
185 * Compares list of experiments, fires error if an experiment is registered without telemetry entry.
186 */
187function compareExperimentLists(mainImplList, userMetricsList) {
188 // Ensure both lists are valid
189 let errorFound = false;
190 if (!mainImplList) {
191 console.log(
192 'Changes to Devtools Experiment registration have prevented this check from finding registered experiments.');
193 console.log('Please update scripts/check_experiments.js to account for the new experiment registration.');
194 errorFound = true;
195 }
196 if (!userMetricsList) {
197 console.log(
198 'Changes to Devtools Experiment UserMetrics enum have prevented this check from finding experiments registered for telemetry.');
199 console.log('Please update scripts/check_experiments.js to account for the new experiment telemetry format.');
200 errorFound = true;
201 }
202 if (errorFound) {
203 process.exit(1);
204 }
205
206 // Ensure both lists match
207 const missingTelemetry = mainImplList.filter(experiment => !userMetricsList.includes(experiment));
208 const staleTelemetry = userMetricsList.filter(
209 experiment => !mainImplList.includes(experiment) && experiment !== USER_METRICS_ENUM_ENDPOINT);
210 if (missingTelemetry.length) {
211 console.log('Devtools Experiments have been added without corresponding histogram update!');
212 console.log(missingTelemetry.join('\n'));
213 console.log(
214 'Please ensure that the DevtoolsExperiments enum in UserMetrics.ts is updated with the new experiment.');
215 console.log(
216 'Please ensure that a corresponding CL is openend against chromium.src/tools/metrics/histograms/enums.xml to update the DevtoolsExperiments enum');
217 errorFound = true;
218 }
219 if (staleTelemetry.length) {
220 console.log('Devtools Experiments that are no longer registered are still listed in the telemetry enum!');
221 console.log(staleTelemetry.join('\n'));
222 console.log(
223 'Please ensure that the DevtoolsExperiments enum in UserMetrics.ts is updated to remove these stale experiments.');
224 errorFound = true;
225 }
226 if (errorFound) {
227 process.exit(1);
228 }
229 console.log('DevTools Experiment Telemetry checker passed.');
230}
231
232function main() {
233 const mainImplPath = path.resolve(__dirname, '..', 'front_end', 'entrypoints', 'main', 'MainImpl.ts');
234 const mainImplFile = fs.readFileSync(mainImplPath, 'utf-8');
235
236 const userMetricsPath = path.resolve(__dirname, '..', 'front_end', 'core', 'host', 'UserMetrics.ts');
237 const userMetricsFile = fs.readFileSync(userMetricsPath, 'utf-8');
238
239 const runtimePath = path.resolve(__dirname, '..', 'front_end', 'core', 'root', 'Runtime.ts');
240 const runtimeFile = fs.readFileSync(runtimePath, 'utf-8');
241 const experimentNames = getExperimentNameEnum(runtimeFile);
242
243 compareExperimentLists(
244 getMainImplExperimentList(mainImplFile, experimentNames), getUserMetricExperimentList(userMetricsFile));
245}
246
247main();