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