| 'use strict'; |
| |
| // Scan API sources for definitions. |
| // |
| // Note the output is produced based on a world class parser, adherence to |
| // conventions, and a bit of guess work. Examples: |
| // |
| // * We scan for top level module.exports statements, and determine what |
| // is exported by looking at the source code only (i.e., we don't do |
| // an eval). If exports include `Foo`, it probably is a class, whereas |
| // if what is exported is `constants` it probably is prefixed by the |
| // basename of the source file (e.g., `zlib`), unless that source file is |
| // `buffer.js`, in which case the name is just `buf`. unless the constant |
| // is `kMaxLength`, in which case it is `buffer`. |
| // |
| // * We scan for top level definitions for those exports, handling |
| // most common cases (e.g., `X.prototype.foo =`, `X.foo =`, |
| // `function X(...) {...}`). Over time, we expect to handle more |
| // cases (example: ES2015 class definitions). |
| |
| const acorn = require('../../deps/acorn/acorn'); |
| const fs = require('fs'); |
| const path = require('path'); |
| const child_process = require('child_process'); |
| |
| // Run a command, capturing stdout, ignoring errors. |
| function execSync(command) { |
| try { |
| return child_process.execSync( |
| command, |
| { stdio: ['ignore', null, 'ignore'] } |
| ).toString().trim(); |
| } catch { |
| return ''; |
| } |
| } |
| |
| // Determine origin repo and tag (or hash) of the most recent commit. |
| const localBranch = execSync('git name-rev --name-only HEAD'); |
| const trackingRemote = execSync(`git config branch.${localBranch}.remote`); |
| const remoteUrl = execSync(`git config remote.${trackingRemote}.url`); |
| const repo = (remoteUrl.match(/(\w+\/\w+)\.git\r?\n?$/) || |
| ['', 'nodejs/node'])[1]; |
| |
| const hash = execSync('git log -1 --pretty=%H') || 'master'; |
| const tag = execSync(`git describe --contains ${hash}`).split('\n')[0] || hash; |
| |
| // Extract definitions from each file specified. |
| const definition = {}; |
| const output = process.argv[2]; |
| const inputs = process.argv.slice(3); |
| inputs.forEach((file) => { |
| const basename = path.basename(file, '.js'); |
| |
| // Parse source. |
| const source = fs.readFileSync(file, 'utf8'); |
| const ast = acorn.parse( |
| source, |
| { allowReturnOutsideFunction: true, ecmaVersion: 10, locations: true }); |
| const program = ast.body; |
| |
| // Build link |
| const link = `https://github.com/${repo}/blob/${tag}/` + |
| path.relative('.', file).replace(/\\/g, '/'); |
| |
| // Scan for exports. |
| const exported = { constructors: [], identifiers: [] }; |
| const indirect = {}; |
| program.forEach((statement) => { |
| if (statement.type === 'ExpressionStatement') { |
| const expr = statement.expression; |
| if (expr.type !== 'AssignmentExpression') return; |
| |
| let lhs = expr.left; |
| if (lhs.type !== 'MemberExpression') return; |
| if (lhs.object.type === 'MemberExpression') lhs = lhs.object; |
| if (lhs.object.name === 'exports') { |
| const name = lhs.property.name; |
| if (expr.right.type === 'FunctionExpression') { |
| definition[`${basename}.${name}`] = |
| `${link}#L${statement.loc.start.line}`; |
| } else if (expr.right.type === 'Identifier') { |
| if (expr.right.name === name) { |
| indirect[name] = `${basename}.${name}`; |
| } |
| } else { |
| exported.identifiers.push(name); |
| } |
| } else if (lhs.object.name === 'module') { |
| if (lhs.property.name !== 'exports') return; |
| |
| let rhs = expr.right; |
| while (rhs.type === 'AssignmentExpression') rhs = rhs.right; |
| |
| if (rhs.type === 'NewExpression') { |
| exported.constructors.push(rhs.callee.name); |
| } else if (rhs.type === 'ObjectExpression') { |
| rhs.properties.forEach((property) => { |
| if (property.value.type === 'Identifier') { |
| exported.identifiers.push(property.value.name); |
| if (/^[A-Z]/.test(property.value.name[0])) { |
| exported.constructors.push(property.value.name); |
| } |
| } |
| }); |
| } else if (rhs.type === 'Identifier') { |
| exported.identifiers.push(rhs.name); |
| } |
| } |
| } else if (statement.type === 'VariableDeclaration') { |
| for (const decl of statement.declarations) { |
| let init = decl.init; |
| while (init && init.type === 'AssignmentExpression') init = init.left; |
| if (!init || init.type !== 'MemberExpression') continue; |
| if (init.object.name === 'exports') { |
| definition[`${basename}.${init.property.name}`] = |
| `${link}#L${statement.loc.start.line}`; |
| } else if (init.object.name === 'module') { |
| if (init.property.name !== 'exports') continue; |
| exported.constructors.push(decl.id.name); |
| definition[decl.id.name] = `${link}#L${statement.loc.start.line}`; |
| } |
| } |
| } |
| }); |
| |
| // Scan for definitions matching those exports; currently supports: |
| // |
| // ClassName.foo = ...; |
| // ClassName.prototype.foo = ...; |
| // function Identifier(...) {...}; |
| // class Foo {...}; |
| // |
| program.forEach((statement) => { |
| if (statement.type === 'ExpressionStatement') { |
| const expr = statement.expression; |
| if (expr.type !== 'AssignmentExpression') return; |
| if (expr.left.type !== 'MemberExpression') return; |
| |
| let object; |
| if (expr.left.object.type === 'MemberExpression') { |
| if (expr.left.object.property.name !== 'prototype') return; |
| object = expr.left.object.object; |
| } else if (expr.left.object.type === 'Identifier') { |
| object = expr.left.object; |
| } else { |
| return; |
| } |
| |
| if (!exported.constructors.includes(object.name)) return; |
| |
| let objectName = object.name; |
| if (expr.left.object.type === 'MemberExpression') { |
| objectName = objectName.toLowerCase(); |
| if (objectName === 'buffer') objectName = 'buf'; |
| } |
| |
| let name = expr.left.property.name; |
| if (expr.left.computed) { |
| name = `${objectName}[${name}]`; |
| } else { |
| name = `${objectName}.${name}`; |
| } |
| |
| definition[name] = `${link}#L${statement.loc.start.line}`; |
| |
| if (expr.left.property.name === expr.right.name) { |
| indirect[expr.right.name] = name; |
| } |
| |
| } else if (statement.type === 'FunctionDeclaration') { |
| const name = statement.id.name; |
| if (!exported.identifiers.includes(name)) return; |
| if (basename.startsWith('_')) return; |
| definition[`${basename}.${name}`] = |
| `${link}#L${statement.loc.start.line}`; |
| |
| } else if (statement.type === 'ClassDeclaration') { |
| if (!exported.constructors.includes(statement.id.name)) return; |
| definition[statement.id.name] = `${link}#L${statement.loc.start.line}`; |
| |
| const name = statement.id.name.slice(0, 1).toLowerCase() + |
| statement.id.name.slice(1); |
| |
| statement.body.body.forEach((defn) => { |
| if (defn.type !== 'MethodDefinition') return; |
| if (defn.kind === 'method') { |
| definition[`${name}.${defn.key.name}`] = |
| `${link}#L${defn.loc.start.line}`; |
| } else if (defn.kind === 'constructor') { |
| definition[`new ${statement.id.name}`] = |
| `${link}#L${defn.loc.start.line}`; |
| } |
| }); |
| } |
| }); |
| |
| // Search for indirect references of the form ClassName.foo = foo; |
| if (Object.keys(indirect).length > 0) { |
| program.forEach((statement) => { |
| if (statement.type === 'FunctionDeclaration') { |
| const name = statement.id.name; |
| if (indirect[name]) { |
| definition[indirect[name]] = `${link}#L${statement.loc.start.line}`; |
| } |
| } |
| }); |
| } |
| }); |
| |
| fs.writeFileSync(output, JSON.stringify(definition, null, 2), 'utf8'); |