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