mirror of
https://github.com/nodejs/node.git
synced 2024-11-28 14:33:11 +01:00
3ce4cef4e6
PR-URL: https://github.com/nodejs/node/pull/45889 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
213 lines
7.5 KiB
JavaScript
213 lines
7.5 KiB
JavaScript
// 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).
|
|
|
|
import child_process from 'child_process';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import * as acorn from '../../deps/acorn/acorn/dist/acorn.mjs';
|
|
|
|
// 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') || 'main';
|
|
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: 'latest',
|
|
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');
|