• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3// Scan API sources for definitions.
4//
5// Note the output is produced based on a world class parser, adherence to
6// conventions, and a bit of guess work. Examples:
7//
8//  * We scan for top level module.exports statements, and determine what
9//    is exported by looking at the source code only (i.e., we don't do
10//    an eval). If exports include `Foo`, it probably is a class, whereas
11//    if what is exported is `constants` it probably is prefixed by the
12//    basename of the source file (e.g., `zlib`), unless that source file is
13//    `buffer.js`, in which case the name is just `buf`.  unless the constant
14//    is `kMaxLength`, in which case it is `buffer`.
15//
16//  * We scan for top level definitions for those exports, handling
17//    most common cases (e.g., `X.prototype.foo =`, `X.foo =`,
18//    `function X(...) {...}`). Over time, we expect to handle more
19//    cases (example: ES2015 class definitions).
20
21const acorn = require('../../deps/acorn/acorn');
22const fs = require('fs');
23const path = require('path');
24const child_process = require('child_process');
25
26// Run a command, capturing stdout, ignoring errors.
27function execSync(command) {
28  try {
29    return child_process.execSync(
30      command,
31      { stdio: ['ignore', null, 'ignore'] }
32    ).toString().trim();
33  } catch {
34    return '';
35  }
36}
37
38// Determine origin repo and tag (or hash) of the most recent commit.
39const localBranch = execSync('git name-rev --name-only HEAD');
40const trackingRemote = execSync(`git config branch.${localBranch}.remote`);
41const remoteUrl = execSync(`git config remote.${trackingRemote}.url`);
42const repo = (remoteUrl.match(/(\w+\/\w+)\.git\r?\n?$/) ||
43             ['', 'nodejs/node'])[1];
44
45const hash = execSync('git log -1 --pretty=%H') || 'master';
46const tag = execSync(`git describe --contains ${hash}`).split('\n')[0] || hash;
47
48// Extract definitions from each file specified.
49const definition = {};
50const output = process.argv[2];
51const inputs = process.argv.slice(3);
52inputs.forEach((file) => {
53  const basename = path.basename(file, '.js');
54
55  // Parse source.
56  const source = fs.readFileSync(file, 'utf8');
57  const ast = acorn.parse(
58    source,
59    { allowReturnOutsideFunction: true, ecmaVersion: 10, locations: true });
60  const program = ast.body;
61
62  // Build link
63  const link = `https://github.com/${repo}/blob/${tag}/` +
64    path.relative('.', file).replace(/\\/g, '/');
65
66  // Scan for exports.
67  const exported = { constructors: [], identifiers: [] };
68  const indirect = {};
69  program.forEach((statement) => {
70    if (statement.type === 'ExpressionStatement') {
71      const expr = statement.expression;
72      if (expr.type !== 'AssignmentExpression') return;
73
74      let lhs = expr.left;
75      if (lhs.type !== 'MemberExpression') return;
76      if (lhs.object.type === 'MemberExpression') lhs = lhs.object;
77      if (lhs.object.name === 'exports') {
78        const name = lhs.property.name;
79        if (expr.right.type === 'FunctionExpression') {
80          definition[`${basename}.${name}`] =
81            `${link}#L${statement.loc.start.line}`;
82        } else if (expr.right.type === 'Identifier') {
83          if (expr.right.name === name) {
84            indirect[name] = `${basename}.${name}`;
85          }
86        } else {
87          exported.identifiers.push(name);
88        }
89      } else if (lhs.object.name === 'module') {
90        if (lhs.property.name !== 'exports') return;
91
92        let rhs = expr.right;
93        while (rhs.type === 'AssignmentExpression') rhs = rhs.right;
94
95        if (rhs.type === 'NewExpression') {
96          exported.constructors.push(rhs.callee.name);
97        } else if (rhs.type === 'ObjectExpression') {
98          rhs.properties.forEach((property) => {
99            if (property.value.type === 'Identifier') {
100              exported.identifiers.push(property.value.name);
101              if (/^[A-Z]/.test(property.value.name[0])) {
102                exported.constructors.push(property.value.name);
103              }
104            }
105          });
106        } else if (rhs.type === 'Identifier') {
107          exported.identifiers.push(rhs.name);
108        }
109      }
110    } else if (statement.type === 'VariableDeclaration') {
111      for (const decl of statement.declarations) {
112        let init = decl.init;
113        while (init && init.type === 'AssignmentExpression') init = init.left;
114        if (!init || init.type !== 'MemberExpression') continue;
115        if (init.object.name === 'exports') {
116          definition[`${basename}.${init.property.name}`] =
117            `${link}#L${statement.loc.start.line}`;
118        } else if (init.object.name === 'module') {
119          if (init.property.name !== 'exports') continue;
120          exported.constructors.push(decl.id.name);
121          definition[decl.id.name] = `${link}#L${statement.loc.start.line}`;
122        }
123      }
124    }
125  });
126
127  // Scan for definitions matching those exports; currently supports:
128  //
129  //   ClassName.foo = ...;
130  //   ClassName.prototype.foo = ...;
131  //   function Identifier(...) {...};
132  //   class Foo {...};
133  //
134  program.forEach((statement) => {
135    if (statement.type === 'ExpressionStatement') {
136      const expr = statement.expression;
137      if (expr.type !== 'AssignmentExpression') return;
138      if (expr.left.type !== 'MemberExpression') return;
139
140      let object;
141      if (expr.left.object.type === 'MemberExpression') {
142        if (expr.left.object.property.name !== 'prototype') return;
143        object = expr.left.object.object;
144      } else if (expr.left.object.type === 'Identifier') {
145        object = expr.left.object;
146      } else {
147        return;
148      }
149
150      if (!exported.constructors.includes(object.name)) return;
151
152      let objectName = object.name;
153      if (expr.left.object.type === 'MemberExpression') {
154        objectName = objectName.toLowerCase();
155        if (objectName === 'buffer') objectName = 'buf';
156      }
157
158      let name = expr.left.property.name;
159      if (expr.left.computed) {
160        name = `${objectName}[${name}]`;
161      } else {
162        name = `${objectName}.${name}`;
163      }
164
165      definition[name] = `${link}#L${statement.loc.start.line}`;
166
167      if (expr.left.property.name === expr.right.name) {
168        indirect[expr.right.name] = name;
169      }
170
171    } else if (statement.type === 'FunctionDeclaration') {
172      const name = statement.id.name;
173      if (!exported.identifiers.includes(name)) return;
174      if (basename.startsWith('_')) return;
175      definition[`${basename}.${name}`] =
176        `${link}#L${statement.loc.start.line}`;
177
178    } else if (statement.type === 'ClassDeclaration') {
179      if (!exported.constructors.includes(statement.id.name)) return;
180      definition[statement.id.name] = `${link}#L${statement.loc.start.line}`;
181
182      const name = statement.id.name.slice(0, 1).toLowerCase() +
183                  statement.id.name.slice(1);
184
185      statement.body.body.forEach((defn) => {
186        if (defn.type !== 'MethodDefinition') return;
187        if (defn.kind === 'method') {
188          definition[`${name}.${defn.key.name}`] =
189            `${link}#L${defn.loc.start.line}`;
190        } else if (defn.kind === 'constructor') {
191          definition[`new ${statement.id.name}`] =
192            `${link}#L${defn.loc.start.line}`;
193        }
194      });
195    }
196  });
197
198  // Search for indirect references of the form ClassName.foo = foo;
199  if (Object.keys(indirect).length > 0) {
200    program.forEach((statement) => {
201      if (statement.type === 'FunctionDeclaration') {
202        const name = statement.id.name;
203        if (indirect[name]) {
204          definition[indirect[name]] = `${link}#L${statement.loc.start.line}`;
205        }
206      }
207    });
208  }
209});
210
211fs.writeFileSync(output, JSON.stringify(definition, null, 2), 'utf8');
212