Update javascript_natives/index.js script

This change also includes a manual run of the script to update
NativeFunctions.js.

Change-Id: I9ce1c4979674d9513cb7259e3e311e30b13de9ed
Bug: 1255619
Doc: https://goo.gle/devtools-js-argument-hints
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3399837
Reviewed-by: Mathias Bynens <mathias@chromium.org>
Commit-Queue: Johan Bay <jobay@chromium.org>
diff --git a/scripts/javascript_natives/helpers.js b/scripts/javascript_natives/helpers.js
new file mode 100644
index 0000000..ac0b71e
--- /dev/null
+++ b/scripts/javascript_natives/helpers.js
@@ -0,0 +1,211 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// @ts-check
+
+import * as fs from 'fs';
+import * as path from 'path';
+import {fileURLToPath} from 'url';
+
+/** @type {Map<string, Map<string, string[][]>>} */
+const methods = new Map();
+
+export function clearState() {
+  methods.clear();
+}
+
+export function parseTSFunction(func, node) {
+  if (!func.name.escapedText) {
+    return;
+  }
+
+  const args = func.parameters
+                   .map(p => {
+                     let text = p.name.escapedText;
+                     if (p.questionToken) {
+                       text = '?' + text;
+                     }
+                     if (p.dotDotDotToken) {
+                       text = '...' + text;
+                     }
+                     return text;
+                   })
+                   .filter(x => x !== 'this');
+  storeMethod(node.name.text, func.name.escapedText, args);
+}
+
+/**
+ * @param {WebIDL2.IDLRootType} thing
+ * */
+export function walkRoot(thing) {
+  switch (thing.type) {
+    case 'interface':
+      walkInterface(thing);
+      break;
+    case 'interface mixin':
+    case 'namespace':
+      walkMembers(thing);
+      break;
+  }
+}
+
+/**
+ * @param {WebIDL2.InterfaceType} thing
+ * */
+function walkInterface(thing) {
+  thing.members.forEach(member => {
+    switch (member.type) {
+      case 'constructor':
+        storeMethod('Window', thing.name, member.arguments.map(argName));
+        break;
+      case 'operation':
+        handleOperation(member);
+    }
+  });
+  const namedConstructor = thing.extAttrs.find(extAttr => extAttr.name === 'NamedConstructor');
+  if (namedConstructor && namedConstructor.arguments) {
+    storeMethod('Window', namedConstructor.rhs.value, namedConstructor.arguments.map(argName));
+  }
+}
+
+/**
+ * @param {WebIDL2.NamespaceType | WebIDL2.InterfaceMixinType} thing
+ * */
+function walkMembers(thing) {
+  thing.members.forEach(member => {
+    if (member.type === 'operation') {
+      handleOperation(member);
+    }
+  });
+}
+
+/**
+ * @param {WebIDL2.OperationMemberType} member
+ * */
+function handleOperation(member) {
+  storeMethod(
+      member.special === 'static' ? (parent.name + 'Constructor') : member.parent.name, member.name,
+      member.arguments.map(argName));
+}
+
+/**
+ * @param {WebIDL2.Argument} a
+ * */
+function argName(a) {
+  let name = a.name;
+  if (a.optional) {
+    name = '?' + name;
+  }
+  if (a.variadic) {
+    name = '...' + name;
+  }
+  return name;
+}
+
+/**
+ * @param {string} parent
+ * @param {string} name
+ * @param {Array<string>} args
+ * */
+function storeMethod(parent, name, args) {
+  if (!methods.has(name)) {
+    methods.set(name, new Map());
+  }
+  const method = methods.get(name);
+  if (!method.has(parent)) {
+    method.set(parent, []);
+  }
+  method.get(parent).push(args);
+}
+
+export function postProcess(dryRun = false) {
+  for (const name of methods.keys()) {
+    // We use the set jsonParents to track the set of different signatures across parent for this function name.
+    // If all signatures are identical, we leave out the parent and emit a single NativeFunction entry without receiver.
+    const jsonParents = new Set();
+    for (const [parent, signatures] of methods.get(name)) {
+      signatures.sort((a, b) => a.length - b.length);
+      const filteredSignatures = [];
+      for (const signature of signatures) {
+        const smallerIndex = filteredSignatures.findIndex(smaller => startsTheSame(smaller, signature));
+        if (smallerIndex !== -1) {
+          filteredSignatures[smallerIndex] = (signature.map((arg, index) => {
+            const otherArg = filteredSignatures[smallerIndex][index];
+            if (otherArg) {
+              return otherArg.length > arg.length ? otherArg : arg;
+            }
+            if (arg.startsWith('?') || arg.startsWith('...')) {
+              return arg;
+            }
+            return '?' + arg;
+          }));
+        } else {
+          filteredSignatures.push(signature);
+        }
+      }
+
+      function startsTheSame(smaller, bigger) {
+        for (let i = 0; i < smaller.length; i++) {
+          const withoutQuestion = str => /[\?]?(.*)/.exec(str)[1];
+          if (withoutQuestion(smaller[i]) !== withoutQuestion(bigger[i])) {
+            return false;
+          }
+        }
+        return true;
+      }
+
+      methods.get(name).set(parent, filteredSignatures);
+      jsonParents.add(JSON.stringify(filteredSignatures));
+    }
+    // If all parents had the same signature for this name, we put a `*` as parent for this entry.
+    if (jsonParents.size === 1) {
+      methods.set(name, new Map([['*', JSON.parse(jsonParents.values().next().value)]]));
+    }
+    for (const [parent, signatures] of methods.get(name)) {
+      if (signatures.length === 1 && !signatures[0].length) {
+        methods.get(name).delete(parent);
+      }
+    }
+    if (methods.get(name).size === 0) {
+      methods.delete(name);
+    }
+  }
+  const functions = [];
+  for (const [name, method] of methods) {
+    if (method.has('*')) {
+      // All parents had the same signature so we emit an entry without receiver.
+      functions.push({name, signatures: method.get('*')});
+    } else {
+      for (const [parent, signatures] of method) {
+        if (parent.endsWith('Constructor')) {
+          functions.push(
+              {name, signatures, static: true, receiver: parent.substring(0, parent.length - 'Constructor'.length)});
+        } else {
+          functions.push({name, signatures, receiver: parent});
+        }
+      }
+    }
+  }
+  const output = `export const NativeFunctions = [\n${
+      functions
+          .map(
+              entry =>
+                  `  {\n${Object.entries(entry).map(kv => `    ${kv[0]}: ${JSON.stringify(kv[1])}`).join(',\n')}\n  }`)
+          .join(',\n')}\n];`;
+
+  if (dryRun) {
+    return output;
+  }
+
+  fs.writeFileSync(
+      (new URL('../../front_end/models/javascript_metadata/NativeFunctions.js', import.meta.url)).pathname,
+      `// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+// Generated from ${
+          path.relative(path.join(fileURLToPath(import.meta.url), '..', '..'), fileURLToPath(import.meta.url))}
+
+${output}
+`);
+}