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