diff --git a/front_end/entrypoints/main/MainImpl.ts b/front_end/entrypoints/main/MainImpl.ts index 8aeb581c31..39f54eb4f0 100644 --- a/front_end/entrypoints/main/MainImpl.ts +++ b/front_end/entrypoints/main/MainImpl.ts @@ -316,7 +316,7 @@ export class MainImpl { ]); // Localized DevTools, hide "locale selector" setting behind an experiment. - Root.Runtime.experiments.register('localizedDevTools', 'Enable localized DevTools'); + Root.Runtime.experiments.register(Root.Runtime.ExperimentName.LOCALIZED_DEVTOOLS, 'Enable localized DevTools'); Root.Runtime.experiments.cleanUpStaleExperiments(); const enabledExperiments = Root.Runtime.Runtime.queryParam('enabledExperiments'); diff --git a/scripts/check_experiments.js b/scripts/check_experiments.js index 82bbb3ab90..06a2bb19a8 100644 --- a/scripts/check_experiments.js +++ b/scripts/check_experiments.js @@ -9,6 +9,11 @@ const path = require('path'); const SRC_PATH = path.resolve(__dirname, '..'); const NODE_MODULES_PATH = path.resolve(SRC_PATH, 'node_modules'); const espree = require(path.resolve(NODE_MODULES_PATH, '@typescript-eslint', 'parser')); +const parseOptions = { + ecmaVersion: 11, + sourceType: 'module', + range: true, +}; const USER_METRICS_ENUM_ENDPOINT = '__lastValidEnumPosition'; @@ -18,7 +23,23 @@ const USER_METRICS_ENUM_ENDPOINT = '__lastValidEnumPosition'; */ function isClassNameDeclaration(node, className) { const isClassDeclaration = node.type === 'ExportNamedDeclaration' && node.declaration.type === 'ClassDeclaration'; - return className ? (isClassDeclaration && node.declaration.id.name === className) : isClassDeclaration; + if (className) { + return isClassDeclaration && node.declaration.id.name === className; + } + return isClassDeclaration; +} + + +/** + * Determines if a node is an typescript enum declaration. + * If enumName is provided, node must also match enum name. + */ +function isEnumDeclaration(node, enumName) { + const isEnumDeclaration = node.type === 'ExportNamedDeclaration' && node.declaration.type === 'TSEnumDeclaration'; + if (enumName) { + return isEnumDeclaration && node.declaration.id.name === enumName; + } + return isEnumDeclaration; } /** @@ -41,11 +62,55 @@ function isExperimentRegistrationCall(node) { node.expression.callee.property.name === 'register'; } +/** + * Extract the enum Root.Runtime.ExperimentName to a map + */ +function getExperimentNameEnum(mainImplFile) { + const mainAST = espree.parse(mainImplFile, parseOptions); + + let experimentNameEnum; + for (const node of mainAST.body) { + if (isEnumDeclaration(node, 'ExperimentName')) { + experimentNameEnum = node; + break; + } + } + + const map = new Map(); + if (!experimentNameEnum) { + return map; + } + for (const member of experimentNameEnum.declaration.members) { + map.set(member.id.name, member.initializer.value); + } + return map; +} + +/** + * Determine if node is of the form Root.Runtime.ExperimentName.NAME, and if so + * return NAME as string. + */ +function isExperimentNameReference(node) { + if (node.type !== 'MemberExpression') { + return false; + } + if (node.object.type !== 'MemberExpression' || node.object.property?.name !== 'ExperimentName') { + return false; + } + if (node.object.object.type !== 'MemberExpression' || node.object.object.property?.name !== 'Runtime') { + return false; + } + if (node.object.object.object.type !== 'Identifier' || node.object.object.object.name !== 'Root') { + return false; + } + return node.property.name; +} + /** * Gets list of experiments registered in MainImpl.js. */ -function getMainImplExperimentList(mainImplFile) { - const mainAST = espree.parse(mainImplFile, {ecmaVersion: 11, sourceType: 'module', range: true}); +function getMainImplExperimentList(mainImplFile, experimentNames) { + const mainAST = espree.parse(mainImplFile, parseOptions); // Find MainImpl Class node let mainImplClassNode; @@ -70,7 +135,25 @@ function getMainImplExperimentList(mainImplFile) { for (const statement of initializeExperimentNode.value.body.body) { if (isExperimentRegistrationCall(statement)) { // Experiment name is first argument of registration call - experiments.push(statement.expression.arguments[0].value); + const experimentNameArg = statement.expression.arguments[0]; + // The experiment name can either be a literal, e.g. 'fooExperiment'.. + if (experimentNameArg.type === 'Literal') { + experiments.push(experimentNameArg.value); + } else { + // .. or a member of Root.Runtime.ExperimentName. + const experimentName = isExperimentNameReference(experimentNameArg); + if (experimentName) { + const translatedName = experimentNames.get(experimentName); + if (!translatedName) { + console.log('Failed to resolve Root.Runtime.ExperimentName.${experimentName} to a string'); + process.exit(1); + } + experiments.push(translatedName); + } else { + console.log('Unexpected argument to Root.Runtime.experiments.register: ', experimentNameArg); + process.exit(1); + } + } } } return experiments.length ? experiments : null; @@ -154,7 +237,12 @@ function main() { const userMetricsPath = path.resolve(__dirname, '..', 'front_end', 'core', 'host', 'UserMetrics.ts'); const userMetricsFile = fs.readFileSync(userMetricsPath, 'utf-8'); - compareExperimentLists(getMainImplExperimentList(mainImplFile), getUserMetricExperimentList(userMetricsFile)); + const runtimePath = path.resolve(__dirname, '..', 'front_end', 'core', 'root', 'Runtime.ts'); + const runtimeFile = fs.readFileSync(runtimePath, 'utf-8'); + const experimentNames = getExperimentNameEnum(runtimeFile); + + compareExperimentLists( + getMainImplExperimentList(mainImplFile, experimentNames), getUserMetricExperimentList(userMetricsFile)); } main();