| // Copyright 2021 The Chromium Authors | |
| // 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 'node:fs'; | |
| import * as path from 'node:path'; | |
| import {fileURLToPath} from 'node:url'; | |
| /** @type {Map<string, Map<string, string[][]>>} */ | |
| const methods = new Map(); | |
| /** @type {Map<string, string[]>} */ | |
| const includes = new Map(); | |
| export function clearState() { | |
| methods.clear(); | |
| includes.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 thing | |
| * */ | |
| export function walkRoot(thing) { | |
| switch (thing.type) { | |
| case 'interface': | |
| walkInterface(thing); | |
| break; | |
| case 'interface mixin': | |
| case 'namespace': | |
| walkMembers(thing); | |
| break; | |
| case 'includes': | |
| walkIncludes(thing); | |
| break; | |
| } | |
| } | |
| /** | |
| * @param thing | |
| * */ | |
| function walkIncludes(thing) { | |
| if (includes.has(thing.includes)) { | |
| includes.get(thing.includes).push(thing.target); | |
| } else { | |
| includes.set(thing.includes, [thing.target]); | |
| } | |
| } | |
| /** | |
| * @param 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 thing | |
| * */ | |
| function walkMembers(thing) { | |
| thing.members.forEach(member => { | |
| if (member.type === 'operation') { | |
| handleOperation(member); | |
| } | |
| }); | |
| } | |
| /** | |
| * @param member | |
| * */ | |
| function handleOperation(member) { | |
| storeMethod(member.parent.name, member.name, member.arguments.map(argName)); | |
| } | |
| /** | |
| * @param a | |
| * */ | |
| function argName(a) { | |
| let name = a.name; | |
| if (a.optional) { | |
| name = '?' + name; | |
| } | |
| if (a.variadic) { | |
| name = '...' + name; | |
| } | |
| return name; | |
| } | |
| /** | |
| * @param parent | |
| * @param name | |
| * @param 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 { | |
| const receiversMap = new Map(); | |
| for (const [parent, signatures] of method) { | |
| const receivers = receiversMap.get(JSON.stringify(signatures)) || new Set(); | |
| if (includes.has(parent)) { | |
| includes.get(parent).forEach(receiver => receivers.add(receiver)); | |
| } else { | |
| receivers.add(parent); | |
| } | |
| receiversMap.set(JSON.stringify(signatures), receivers); | |
| } | |
| for (const [signatures, receivers] of receiversMap) { | |
| functions.push({name, signatures: JSON.parse(signatures), receivers: Array.from(receivers)}); | |
| } | |
| } | |
| } | |
| const output = `export interface NativeFunctionValue { | |
| name: string; | |
| signatures: string[][]; | |
| receivers?: string[]; | |
| } | |
| export const NativeFunctions: readonly NativeFunctionValue[] = [\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.ts', import.meta.url)).pathname, | |
| `// Copyright 2020 The Chromium Authors | |
| // 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))} | |
| // clang-format off | |
| ${output} | |
| `); | |
| } |