blob: 06a2bb19a82378fab585800790a3db493e86ff1c [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
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';
Sigurd Schneider6fdf2da2021-07-05 13:11:12 +020026 if (className) {
27 return isClassDeclaration && node.declaration.id.name === className;
28 }
29 return isClassDeclaration;
30}
31
32
33/**
34 * Determines if a node is an typescript enum declaration.
35 * If enumName is provided, node must also match enum name.
36 */
37function isEnumDeclaration(node, enumName) {
38 const isEnumDeclaration = node.type === 'ExportNamedDeclaration' && node.declaration.type === 'TSEnumDeclaration';
39 if (enumName) {
40 return isEnumDeclaration && node.declaration.id.name === enumName;
41 }
42 return isEnumDeclaration;
Brandon Goddard33104372020-08-13 08:49:23 -070043}
44
45/**
46 * Finds a function declaration node inside a class declaration node
47 */
48function findFunctionInClass(classNode, functionName) {
49 for (const node of classNode.declaration.body.body) {
50 if (node.key.name === functionName) {
51 return node;
52 }
53 }
54 return null;
55}
56
57/**
58 * Determines if AST Node is a call to register a DevtoolsExperiment
59 */
60function isExperimentRegistrationCall(node) {
61 return node.expression && node.expression.type === 'CallExpression' &&
62 node.expression.callee.property.name === 'register';
63}
64
65/**
Sigurd Schneider6fdf2da2021-07-05 13:11:12 +020066 * Extract the enum Root.Runtime.ExperimentName to a map
67 */
68function getExperimentNameEnum(mainImplFile) {
69 const mainAST = espree.parse(mainImplFile, parseOptions);
70
71 let experimentNameEnum;
72 for (const node of mainAST.body) {
73 if (isEnumDeclaration(node, 'ExperimentName')) {
74 experimentNameEnum = node;
75 break;
76 }
77 }
78
79 const map = new Map();
80 if (!experimentNameEnum) {
81 return map;
82 }
83 for (const member of experimentNameEnum.declaration.members) {
84 map.set(member.id.name, member.initializer.value);
85 }
86 return map;
87}
88
89/**
90 * Determine if node is of the form Root.Runtime.ExperimentName.NAME, and if so
91 * return NAME as string.
92 */
93function isExperimentNameReference(node) {
94 if (node.type !== 'MemberExpression') {
95 return false;
96 }
97 if (node.object.type !== 'MemberExpression' || node.object.property?.name !== 'ExperimentName') {
98 return false;
99 }
100 if (node.object.object.type !== 'MemberExpression' || node.object.object.property?.name !== 'Runtime') {
101 return false;
102 }
103 if (node.object.object.object.type !== 'Identifier' || node.object.object.object.name !== 'Root') {
104 return false;
105 }
106 return node.property.name;
107}
108
109/**
Brandon Goddard33104372020-08-13 08:49:23 -0700110 * Gets list of experiments registered in MainImpl.js.
111 */
Sigurd Schneider6fdf2da2021-07-05 13:11:12 +0200112function getMainImplExperimentList(mainImplFile, experimentNames) {
113 const mainAST = espree.parse(mainImplFile, parseOptions);
Brandon Goddard33104372020-08-13 08:49:23 -0700114
115 // Find MainImpl Class node
116 let mainImplClassNode;
117 for (const node of mainAST.body) {
118 if (isClassNameDeclaration(node, 'MainImpl')) {
119 mainImplClassNode = node;
120 break;
121 }
122 }
123 if (!mainImplClassNode) {
124 return null;
125 }
126
127 // Find function in MainImpl Class
128 const initializeExperimentNode = findFunctionInClass(mainImplClassNode, '_initializeExperiments');
129 if (!initializeExperimentNode) {
130 return null;
131 }
132
133 // Get list of experiments
134 const experiments = [];
135 for (const statement of initializeExperimentNode.value.body.body) {
136 if (isExperimentRegistrationCall(statement)) {
137 // Experiment name is first argument of registration call
Sigurd Schneider6fdf2da2021-07-05 13:11:12 +0200138 const experimentNameArg = statement.expression.arguments[0];
139 // The experiment name can either be a literal, e.g. 'fooExperiment'..
140 if (experimentNameArg.type === 'Literal') {
141 experiments.push(experimentNameArg.value);
142 } else {
143 // .. or a member of Root.Runtime.ExperimentName.
144 const experimentName = isExperimentNameReference(experimentNameArg);
145 if (experimentName) {
146 const translatedName = experimentNames.get(experimentName);
147 if (!translatedName) {
148 console.log('Failed to resolve Root.Runtime.ExperimentName.${experimentName} to a string');
149 process.exit(1);
150 }
151 experiments.push(translatedName);
152 } else {
153 console.log('Unexpected argument to Root.Runtime.experiments.register: ', experimentNameArg);
154 process.exit(1);
155 }
156 }
Brandon Goddard33104372020-08-13 08:49:23 -0700157 }
158 }
159 return experiments.length ? experiments : null;
160}
161
162/**
163 * Determines if AST Node is the DevtoolsExperiments Enum declaration
164 */
165function isExperimentEnumDeclaration(node) {
166 return node.type === 'ExportNamedDeclaration' && node.declaration.declarations &&
167 node.declaration.declarations[0].id.name === 'DevtoolsExperiments';
168}
169
170/**
Jan Scheffler9aca8c72021-02-25 10:03:24 +0100171 * Gets list of experiments registered in UserMetrics.ts
Brandon Goddard33104372020-08-13 08:49:23 -0700172 */
173function getUserMetricExperimentList(userMetricsFile) {
174 const userMetricsAST = espree.parse(userMetricsFile, {ecmaVersion: 11, sourceType: 'module', range: true});
175 for (const node of userMetricsAST.body) {
176 if (isExperimentEnumDeclaration(node)) {
177 return node.declaration.declarations[0].init.properties.map(property => {
178 return property.key.value;
179 });
180 }
181 }
182 return null;
183}
184
185/**
186 * Compares list of experiments, fires error if an experiment is registered without telemetry entry.
187 */
188function compareExperimentLists(mainImplList, userMetricsList) {
189 // Ensure both lists are valid
190 let errorFound = false;
191 if (!mainImplList) {
192 console.log(
193 'Changes to Devtools Experiment registration have prevented this check from finding registered experiments.');
194 console.log('Please update scripts/check_experiments.js to account for the new experiment registration.');
195 errorFound = true;
196 }
197 if (!userMetricsList) {
198 console.log(
199 'Changes to Devtools Experiment UserMetrics enum have prevented this check from finding experiments registered for telemetry.');
200 console.log('Please update scripts/check_experiments.js to account for the new experiment telemetry format.');
201 errorFound = true;
202 }
203 if (errorFound) {
204 process.exit(1);
205 }
206
207 // Ensure both lists match
208 const missingTelemetry = mainImplList.filter(experiment => !userMetricsList.includes(experiment));
209 const staleTelemetry = userMetricsList.filter(
210 experiment => !mainImplList.includes(experiment) && experiment !== USER_METRICS_ENUM_ENDPOINT);
211 if (missingTelemetry.length) {
212 console.log('Devtools Experiments have been added without corresponding histogram update!');
213 console.log(missingTelemetry.join('\n'));
214 console.log(
Jan Scheffler9aca8c72021-02-25 10:03:24 +0100215 'Please ensure that the DevtoolsExperiments enum in UserMetrics.ts is updated with the new experiment.');
Brandon Goddard33104372020-08-13 08:49:23 -0700216 console.log(
217 'Please ensure that a corresponding CL is openend against chromium.src/tools/metrics/histograms/enums.xml to update the DevtoolsExperiments enum');
218 errorFound = true;
219 }
220 if (staleTelemetry.length) {
221 console.log('Devtools Experiments that are no longer registered are still listed in the telemetry enum!');
222 console.log(staleTelemetry.join('\n'));
223 console.log(
Jan Scheffler9aca8c72021-02-25 10:03:24 +0100224 'Please ensure that the DevtoolsExperiments enum in UserMetrics.ts is updated to remove these stale experiments.');
Brandon Goddard33104372020-08-13 08:49:23 -0700225 errorFound = true;
226 }
227 if (errorFound) {
228 process.exit(1);
229 }
230 console.log('DevTools Experiment Telemetry checker passed.');
231}
232
233function main() {
Christy Chenab9a44d2021-07-02 12:54:30 -0700234 const mainImplPath = path.resolve(__dirname, '..', 'front_end', 'entrypoints', 'main', 'MainImpl.ts');
Brandon Goddard33104372020-08-13 08:49:23 -0700235 const mainImplFile = fs.readFileSync(mainImplPath, 'utf-8');
236
Tim van der Lippee0247312021-04-01 15:25:30 +0100237 const userMetricsPath = path.resolve(__dirname, '..', 'front_end', 'core', 'host', 'UserMetrics.ts');
Brandon Goddard33104372020-08-13 08:49:23 -0700238 const userMetricsFile = fs.readFileSync(userMetricsPath, 'utf-8');
239
Sigurd Schneider6fdf2da2021-07-05 13:11:12 +0200240 const runtimePath = path.resolve(__dirname, '..', 'front_end', 'core', 'root', 'Runtime.ts');
241 const runtimeFile = fs.readFileSync(runtimePath, 'utf-8');
242 const experimentNames = getExperimentNameEnum(runtimeFile);
243
244 compareExperimentLists(
245 getMainImplExperimentList(mainImplFile, experimentNames), getUserMetricExperimentList(userMetricsFile));
Brandon Goddard33104372020-08-13 08:49:23 -0700246}
247
248main();