DevTools: Argument hints for native functions
Change-Id: Ia0e9f3c98735d64a1188b4373c90b7ef2684ea48
Reviewed-on: https://chromium-review.googlesource.com/1054582
Commit-Queue: Joel Einbinder <einbinder@chromium.org>
Reviewed-by: Andrey Lushnikov <lushnikov@chromium.org>
Reviewed-by: Erik Luo <luoe@chromium.org>
Cr-Original-Commit-Position: refs/heads/master@{#558527}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: a5be1a79f122d4496aa4a09ddfad0b475d878248
diff --git a/scripts/javascript_natives/index.js b/scripts/javascript_natives/index.js
new file mode 100644
index 0000000..871e586
--- /dev/null
+++ b/scripts/javascript_natives/index.js
@@ -0,0 +1,190 @@
+const WebIDL2 = require('webidl2');
+const fs = require('fs');
+const path = require('path');
+const ts = require('typescript');
+const glob = require('glob');
+const methods = {
+ __proto__: null
+};
+const methodsByName = {
+ __proto__: null
+};
+const program =
+ ts.createProgram([path.join(__dirname, 'node_modules', 'typescript', 'lib', 'lib.esnext.d.ts')], {noLib: true});
+for (const file of program.getSourceFiles()) {
+ ts.forEachChild(file, node => {
+ if (node.kind === ts.SyntaxKind.InterfaceDeclaration) {
+ for (const member of node.members) {
+ if (member.kind === ts.SyntaxKind.MethodSignature)
+ parseTSFunction(member, node);
+ }
+ }
+ if (node.kind === ts.SyntaxKind.FunctionDeclaration)
+ parseTSFunction(node, {name: {text: 'Window'}});
+
+ });
+}
+
+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);
+}
+
+const files = glob('../../../+(core|modules)/**/*.idl', {cwd: __dirname}, function(er, files) {
+ for (const file of files) {
+ if (file.includes('testing'))
+ continue;
+ const data = fs.readFileSync(path.join(__dirname, file), 'utf8');
+ const lines = data.split('\n');
+ const newLines = [];
+ for (line of lines) {
+ if (!line.includes(' attribute '))
+ newLines.push(line);
+ }
+
+ try {
+ WebIDL2.parse(newLines.join('\n')).forEach(walk);
+ } catch (e) {
+ // console.error(file);
+ }
+ }
+ WebIDL2
+ .parse(`
+ namespace console {
+ void assert(optional boolean condition = false, any... data);
+ void clear();
+ void count(optional DOMString label = "default");
+ void debug(any... data);
+ void dir(any item, optional object? options);
+ void dirxml(any... data);
+ void error(any... data);
+ void group(any... data);
+ void groupCollapsed(any... data);
+ void groupEnd();
+ void info(any... data);
+ void log(any... data);
+ void profile(optional DOMString title);
+ void profileEnd(optional DOMString title);
+ void table(any... tabularData);
+ void time(optional DOMString label);
+ void timeEnd(optional DOMString label);
+ void timeStamp(optional DOMString name);
+ void trace(any... data);
+ void warn(any... data);
+ };
+`).forEach(walk);
+ postProcess();
+});
+
+function walk(thing, parent) {
+ if (thing.type === 'interface') {
+ const constructor = thing.extAttrs.find(extAttr => extAttr.name === 'Constructor');
+ if (constructor && constructor.arguments && thing.extAttrs.find(extAttr => extAttr.name === 'Exposed'))
+ storeMethod('Window', thing.name, constructor.arguments.map(argName));
+
+ const namedConstructor = thing.extAttrs.find(extAttr => extAttr.name === 'NamedConstructor');
+ if (namedConstructor && namedConstructor.arguments)
+ storeMethod('Window', namedConstructor.rhs.value, namedConstructor.arguments.map(argName));
+ }
+ if (thing.type.includes('operation')) {
+ storeMethod(thing.static ? (parent.name + 'Constructor') : parent.name, thing.name, thing.arguments.map(argName));
+ return;
+ }
+ if (thing.members) {
+ for (const member of thing.members)
+ walk(member, thing);
+ }
+}
+
+function argName(a) {
+ let name = a.name;
+ if (a.optional)
+ name = '?' + name;
+ if (a.variadic)
+ name = '...' + name;
+ return name;
+}
+
+function storeMethod(parent, name, args) {
+ if (!methods[name])
+ methods[name] = {__proto__: null};
+ if (!methods[name][parent])
+ methods[name][parent] = [];
+ methods[name][parent].push(args);
+}
+
+function postProcess() {
+ for (const name in methods) {
+ const jsonParents = new Set();
+ for (const parent in methods[name]) {
+ const signatures = methods[name][parent];
+ 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) => {
+ if (index < filteredSignatures[smallerIndex].length)
+ return 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++) {
+ if (smaller[i] !== bigger[i])
+ return false;
+ }
+ return true;
+ }
+
+ methods[name][parent] = filteredSignatures;
+ jsonParents.add(JSON.stringify(filteredSignatures));
+ }
+ if (jsonParents.size === 1) {
+ methods[name] = {'*': JSON.parse(jsonParents.values().next().value)};
+ }
+ for (const parent in methods[name]) {
+ const signatures = methods[name][parent];
+ if (signatures.length === 1 && !signatures[0].length)
+ delete methods[name][parent];
+ }
+ if (!Object.keys(methods[name]).length)
+ delete methods[name];
+ }
+ const functions = [];
+ for (const name in methods) {
+ if (methods[name]['*']) {
+ functions.push({name, signatures: methods[name]['*']});
+ } else {
+ for (const parent in methods[name]) {
+ if (parent.endsWith('Constructor'))
+ functions.push({name, signatures: methods[name][parent], static: true, receiver: constructor});
+ else
+ functions.push({name, signatures: methods[name][parent], receiver: parent});
+ }
+ }
+ }
+
+ fs.writeFileSync(
+ path.join(__dirname, '..', '..', 'front_end', 'javascript_metadata', 'NativeFunctions.js'),
+ `// Generated from ${path.relative(path.join(__dirname, '..', '..'), __filename)}
+JavaScriptMetadata.NativeFunctions = ${JSON.stringify(functions)};`);
+}
diff --git a/scripts/javascript_natives/package.json b/scripts/javascript_natives/package.json
new file mode 100644
index 0000000..4e9e907
--- /dev/null
+++ b/scripts/javascript_natives/package.json
@@ -0,0 +1,11 @@
+{
+ "main": "index.js",
+ "scripts": {
+ "start": "node index.js"
+ },
+ "dependencies": {
+ "glob": "^7.1.2",
+ "typescript": "^2.8.3",
+ "webidl2": "^10.3.3"
+ }
+}
diff --git a/scripts/special_case_namespaces.json b/scripts/special_case_namespaces.json
index d0d204d..914005e 100644
--- a/scripts/special_case_namespaces.json
+++ b/scripts/special_case_namespaces.json
@@ -4,6 +4,7 @@
"browser_sdk": "BrowserSDK",
"ui": "UI",
"object_ui": "ObjectUI",
+ "javascript_metadata": "JavaScriptMetadata",
"perf_ui": "PerfUI",
"har_importer": "HARImporter",
"sdk_test_runner": "SDKTestRunner",