blob: 7ae635bd6a818c001fd896453649ee472e6c8c4b [file] [log] [blame]
Brandon Goddard33104372020-08-13 08:49:23 -07001// 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'));
Sigurd Schneider6fdf2da2021-07-05 13:11:12 +020012const parseOptions = {
13 ecmaVersion: 11,
14 sourceType: 'module',
15 range: true,
16};
Brandon Goddard33104372020-08-13 08:49:23 -070017
Wolfgang Beyerb2da5bb2021-12-20 16:03:30 +010018const USER_METRICS_ENUM_ENDPOINT = 'MaxValue';
Brandon Goddard33104372020-08-13 08:49:23 -070019
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';
Sigurd Schneider6fdf2da2021-07-05 13:11:12 +020026 if (className) {
27 return isClassDeclaration && node.declaration.id.name === className;
28 }
29 return isClassDeclaration;
30}
31
Sigurd Schneider6fdf2da2021-07-05 13:11:12 +020032/**
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;
Brandon Goddard33104372020-08-13 08:49:23 -070042}
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/**
Sigurd Schneider6fdf2da2021-07-05 13:11:12 +020065 * 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/**
Brandon Goddard33104372020-08-13 08:49:23 -0700109 * Gets list of experiments registered in MainImpl.js.
110 */
Sigurd Schneider6fdf2da2021-07-05 13:11:12 +0200111function getMainImplExperimentList(mainImplFile, experimentNames) {
112 const mainAST = espree.parse(mainImplFile, parseOptions);
Brandon Goddard33104372020-08-13 08:49:23 -0700113
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
Jan Schefflerd5bf8792021-08-07 17:09:58 +0200127 const initializeExperimentNode = findFunctionInClass(mainImplClassNode, 'initializeExperiments');
Brandon Goddard33104372020-08-13 08:49:23 -0700128 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
Sigurd Schneider6fdf2da2021-07-05 13:11:12 +0200137 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 }
Brandon Goddard33104372020-08-13 08:49:23 -0700156 }
157 }
158 return experiments.length ? experiments : null;
159}
160
161/**
162 * Determines if AST Node is the DevtoolsExperiments Enum declaration
163 */
164function isExperimentEnumDeclaration(node) {
Wolfgang Beyerb2da5bb2021-12-20 16:03:30 +0100165 return node.type === 'ExportNamedDeclaration' && node?.declaration?.id?.name === 'DevtoolsExperiments';
Brandon Goddard33104372020-08-13 08:49:23 -0700166}
167
168/**
Jan Scheffler9aca8c72021-02-25 10:03:24 +0100169 * Gets list of experiments registered in UserMetrics.ts
Brandon Goddard33104372020-08-13 08:49:23 -0700170 */
171function getUserMetricExperimentList(userMetricsFile) {
172 const userMetricsAST = espree.parse(userMetricsFile, {ecmaVersion: 11, sourceType: 'module', range: true});
173 for (const node of userMetricsAST.body) {
174 if (isExperimentEnumDeclaration(node)) {
Wolfgang Beyerb2da5bb2021-12-20 16:03:30 +0100175 return node.declaration.members.map(member => member.id.value);
Brandon Goddard33104372020-08-13 08:49:23 -0700176 }
177 }
178 return null;
179}
180
181/**
182 * Compares list of experiments, fires error if an experiment is registered without telemetry entry.
183 */
184function compareExperimentLists(mainImplList, userMetricsList) {
185 // Ensure both lists are valid
186 let errorFound = false;
187 if (!mainImplList) {
188 console.log(
189 'Changes to Devtools Experiment registration have prevented this check from finding registered experiments.');
190 console.log('Please update scripts/check_experiments.js to account for the new experiment registration.');
191 errorFound = true;
192 }
193 if (!userMetricsList) {
194 console.log(
195 'Changes to Devtools Experiment UserMetrics enum have prevented this check from finding experiments registered for telemetry.');
196 console.log('Please update scripts/check_experiments.js to account for the new experiment telemetry format.');
197 errorFound = true;
198 }
199 if (errorFound) {
200 process.exit(1);
201 }
202
203 // Ensure both lists match
204 const missingTelemetry = mainImplList.filter(experiment => !userMetricsList.includes(experiment));
205 const staleTelemetry = userMetricsList.filter(
206 experiment => !mainImplList.includes(experiment) && experiment !== USER_METRICS_ENUM_ENDPOINT);
207 if (missingTelemetry.length) {
208 console.log('Devtools Experiments have been added without corresponding histogram update!');
209 console.log(missingTelemetry.join('\n'));
210 console.log(
Jan Scheffler9aca8c72021-02-25 10:03:24 +0100211 'Please ensure that the DevtoolsExperiments enum in UserMetrics.ts is updated with the new experiment.');
Brandon Goddard33104372020-08-13 08:49:23 -0700212 console.log(
213 'Please ensure that a corresponding CL is openend against chromium.src/tools/metrics/histograms/enums.xml to update the DevtoolsExperiments enum');
214 errorFound = true;
215 }
216 if (staleTelemetry.length) {
217 console.log('Devtools Experiments that are no longer registered are still listed in the telemetry enum!');
218 console.log(staleTelemetry.join('\n'));
219 console.log(
Jan Scheffler9aca8c72021-02-25 10:03:24 +0100220 'Please ensure that the DevtoolsExperiments enum in UserMetrics.ts is updated to remove these stale experiments.');
Brandon Goddard33104372020-08-13 08:49:23 -0700221 errorFound = true;
222 }
223 if (errorFound) {
224 process.exit(1);
225 }
226 console.log('DevTools Experiment Telemetry checker passed.');
227}
228
229function main() {
Christy Chenab9a44d2021-07-02 12:54:30 -0700230 const mainImplPath = path.resolve(__dirname, '..', 'front_end', 'entrypoints', 'main', 'MainImpl.ts');
Brandon Goddard33104372020-08-13 08:49:23 -0700231 const mainImplFile = fs.readFileSync(mainImplPath, 'utf-8');
232
Tim van der Lippee0247312021-04-01 15:25:30 +0100233 const userMetricsPath = path.resolve(__dirname, '..', 'front_end', 'core', 'host', 'UserMetrics.ts');
Brandon Goddard33104372020-08-13 08:49:23 -0700234 const userMetricsFile = fs.readFileSync(userMetricsPath, 'utf-8');
235
Sigurd Schneider6fdf2da2021-07-05 13:11:12 +0200236 const runtimePath = path.resolve(__dirname, '..', 'front_end', 'core', 'root', 'Runtime.ts');
237 const runtimeFile = fs.readFileSync(runtimePath, 'utf-8');
238 const experimentNames = getExperimentNameEnum(runtimeFile);
239
240 compareExperimentLists(
241 getMainImplExperimentList(mainImplFile, experimentNames), getUserMetricExperimentList(userMetricsFile));
Brandon Goddard33104372020-08-13 08:49:23 -0700242}
243
244main();