Teach check_experiments.js to recognize Root.Runtime.ExperimentName

This modifies check_experiments.js such that it recognizes the use
of Root.Runtime.ExperimentName enum members, e.g. it allows to
write

Root.Runtime.experiments.register(
  Root.Runtime.ExperimentName.LOCALIZED_DEVTOOLS,
  'Enable localized DevTools');

Bug: chromium:1226082
Change-Id: Iac0bd394a739562df7e7ce0b06f0a8ede3fdfbd7
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3003255
Commit-Queue: Sigurd Schneider <sigurds@chromium.org>
Reviewed-by: Mathias Bynens <mathias@chromium.org>
diff --git a/scripts/check_experiments.js b/scripts/check_experiments.js
index 82bbb3a..06a2bb1 100644
--- a/scripts/check_experiments.js
+++ b/scripts/check_experiments.js
@@ -9,6 +9,11 @@
 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 @@
  */
 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;
 }
 
 /**
@@ -42,10 +63,54 @@
 }
 
 /**
+ * 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 @@
   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 @@
   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();