summaryrefslogtreecommitdiff
path: root/tools/doc/apilinks.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/doc/apilinks.js')
-rw-r--r--tools/doc/apilinks.js141
1 files changed, 141 insertions, 0 deletions
diff --git a/tools/doc/apilinks.js b/tools/doc/apilinks.js
new file mode 100644
index 0000000000..98dd7827d6
--- /dev/null
+++ b/tools/doc/apilinks.js
@@ -0,0 +1,141 @@
+'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');
+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 orign repo and tag (or hash) of the most recent commit.
+const local_branch = execSync('git name-rev --name-only HEAD');
+const tracking_remote = execSync(`git config branch.${local_branch}.remote`);
+const remote_url = execSync(`git config remote.${tracking_remote}.url`);
+const repo = (remote_url.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 = {};
+process.argv.slice(2).forEach((file) => {
+ const basename = path.basename(file, '.js');
+
+ // Parse source.
+ const source = fs.readFileSync(file, 'utf8');
+ const ast = acorn.parse(source, { ecmaVersion: 10, locations: true });
+ const program = ast.body;
+
+ // Scan for exports.
+ const exported = { constructors: [], identifiers: [] };
+ program.forEach((statement) => {
+ if (statement.type !== 'ExpressionStatement') return;
+ const expr = statement.expression;
+ if (expr.type !== 'AssignmentExpression') return;
+
+ let lhs = expr.left;
+ if (expr.left.object.type === 'MemberExpression') lhs = lhs.object;
+ if (lhs.type !== 'MemberExpression') return;
+ if (lhs.object.name !== 'module') return;
+ 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);
+ }
+ });
+
+ // Scan for definitions matching those exports; currently supports:
+ //
+ // ClassName.foo = ...;
+ // ClassName.prototype.foo = ...;
+ // function Identifier(...) {...};
+ //
+ const link = `https://github.com/${repo}/blob/${tag}/` +
+ path.relative('.', file).replace(/\\/g, '/');
+
+ 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}`;
+ } 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}`;
+ }
+ });
+});
+
+console.log(JSON.stringify(definition, null, 2));