tools: overhaul tools/doc/json.js

Modernize:
* Replace `var` with `const` / `let`.
* Wrap `switch` cases with `const`/`let` in blocks.
* Replace common functions with arrow functions.
* Replace string concatenation with template literals.
* Shorthand object literals.
* Use destructuring and spread.

Optimize:
* Move RegExp declaration out of loops.
* Replace `.match()` with `.test()` in boolean context.
* Replace RegExp with string when string suffices.
* Make RegExp more strict to reject unrelated cases.
* Make RegExp do the trimming to eliminate many `.trim()` calls.
* Cache retrieved object properties.
* Remove conditions that cannot be false.
* Remove code that seems obsolete
  (it means a state that cannot happen or is not typical).

Clarify:
* Sync code examples in comments with the actual source state.
* Expand some one-letter variable names.
* Rename confusingly similar variables.
* Move variable declarations closer to their context.
* Remove non-actual commented out code.
* Unify blank lines between top-level blocks.

Fix:
* Fix conditions that cannot be true.

Guard:
* Throw on unexpected state more often.

PR-URL: https://github.com/nodejs/node/pull/19832
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
This commit is contained in:
Vse Mozhet Byt 2018-03-29 15:56:32 +03:00
parent 0a679327be
commit 9a6dd07e8d

View File

@ -31,49 +31,47 @@ const marked = require('marked');
// Customized heading without id attribute. // Customized heading without id attribute.
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
renderer.heading = function(text, level) { renderer.heading = (text, level) => `<h${level}>${text}</h${level}>\n`;
return `<h${level}>${text}</h${level}>\n`; marked.setOptions({ renderer });
};
marked.setOptions({
renderer: renderer
});
function doJSON(input, filename, cb) { function doJSON(input, filename, cb) {
const root = { source: filename }; const root = { source: filename };
const stack = [root]; const stack = [root];
var depth = 0; let depth = 0;
var current = root; let current = root;
var state = null; let state = null;
const lexed = marked.lexer(input);
lexed.forEach(function(tok) {
const type = tok.type;
var text = tok.text;
// <!-- type = module --> const exampleHeading = /^example/i;
const metaExpr = /<!--([^=]+)=([^-]+)-->\n*/g;
const stabilityExpr = /^Stability: ([0-5])(?:\s*-\s*)?(.*)$/;
const lexed = marked.lexer(input);
lexed.forEach((tok) => {
const { type } = tok;
let { text } = tok;
// <!-- name=module -->
// This is for cases where the markdown semantic structure is lacking. // This is for cases where the markdown semantic structure is lacking.
if (type === 'paragraph' || type === 'html') { if (type === 'paragraph' || type === 'html') {
var metaExpr = /<!--([^=]+)=([^-]+)-->\n*/g; text = text.replace(metaExpr, (_0, key, value) => {
text = text.replace(metaExpr, function(_0, k, v) { current[key.trim()] = value.trim();
current[k.trim()] = v.trim();
return ''; return '';
}); });
text = text.trim(); text = text.trim();
if (!text) return; if (!text) return;
} }
if (type === 'heading' && if (type === 'heading' && !exampleHeading.test(text.trim())) {
!text.trim().match(/^example/i)) {
if (tok.depth - depth > 1) { if (tok.depth - depth > 1) {
return cb(new Error('Inappropriate heading level\n' + return cb(
JSON.stringify(tok))); new Error(`Inappropriate heading level\n${JSON.stringify(tok)}`));
} }
// Sometimes we have two headings with a single blob of description. // Sometimes we have two headings with a single blob of description.
// Treat as a clone. // Treat as a clone.
if (current && if (state === 'AFTERHEADING' && depth === tok.depth) {
state === 'AFTERHEADING' && const clone = current;
depth === tok.depth) {
var clone = current;
current = newSection(tok); current = newSection(tok);
current.clone = clone; current.clone = clone;
// Don't keep it around on the stack. // Don't keep it around on the stack.
@ -86,15 +84,15 @@ function doJSON(input, filename, cb) {
// root is always considered the level=0 section, // root is always considered the level=0 section,
// and the lowest heading is 1, so this should always // and the lowest heading is 1, so this should always
// result in having a valid parent node. // result in having a valid parent node.
var d = tok.depth; let closingDepth = tok.depth;
while (d <= depth) { while (closingDepth <= depth) {
finishSection(stack.pop(), stack[stack.length - 1]); finishSection(stack.pop(), stack[stack.length - 1]);
d++; closingDepth++;
} }
current = newSection(tok); current = newSection(tok);
} }
depth = tok.depth; ({ depth } = tok);
stack.push(current); stack.push(current);
state = 'AFTERHEADING'; state = 'AFTERHEADING';
return; return;
@ -109,9 +107,13 @@ function doJSON(input, filename, cb) {
// A list: starting with list_start, ending with list_end, // A list: starting with list_start, ending with list_end,
// maybe containing other nested lists in each item. // maybe containing other nested lists in each item.
// //
// A metadata:
// <!-- YAML
// added: v1.0.0
// -->
//
// If one of these isn't found, then anything that comes // If one of these isn't found, then anything that comes
// between here and the next heading should be parsed as the desc. // between here and the next heading should be parsed as the desc.
var stability;
if (state === 'AFTERHEADING') { if (state === 'AFTERHEADING') {
if (type === 'blockquote_start') { if (type === 'blockquote_start') {
state = 'AFTERHEADING_BLOCKQUOTE'; state = 'AFTERHEADING_BLOCKQUOTE';
@ -156,8 +158,8 @@ function doJSON(input, filename, cb) {
return; return;
} }
if (type === 'paragraph' && let stability;
(stability = text.match(/^Stability: ([0-5])(?:\s*-\s*)?(.*)$/))) { if (type === 'paragraph' && (stability = text.match(stabilityExpr))) {
current.stability = parseInt(stability[1], 10); current.stability = parseInt(stability[1], 10);
current.stabilityText = stability[2].trim(); current.stabilityText = stability[2].trim();
return; return;
@ -167,7 +169,6 @@ function doJSON(input, filename, cb) {
current.desc = current.desc || []; current.desc = current.desc || [];
current.desc.links = lexed.links; current.desc.links = lexed.links;
current.desc.push(tok); current.desc.push(tok);
}); });
// Finish any sections left open. // Finish any sections left open.
@ -181,69 +182,67 @@ function doJSON(input, filename, cb) {
// Go from something like this: // Go from something like this:
// //
// [ { type: 'list_item_start' }, // [ { type: "list_item_start" },
// { type: 'text', // { type: "text",
// text: '`settings` Object, Optional' }, // text: "`options` {Object|string}" },
// { type: 'list_start', ordered: false }, // { type: "list_start",
// { type: 'list_item_start' }, // ordered: false },
// { type: 'text', // { type: "list_item_start" },
// text: 'exec: String, file path to worker file. Default: `__filename`' }, // { type: "text",
// { type: 'list_item_end' }, // text: "`encoding` {string|null} **Default:** `'utf8'`" },
// { type: 'list_item_start' }, // { type: "list_item_end" },
// { type: 'text', // { type: "list_item_start" },
// text: 'args: Array, string arguments passed to worker.' }, // { type: "text",
// { type: 'text', // text: "`mode` {integer} **Default:** `0o666`" },
// text: 'Default: `process.argv.slice(2)`' }, // { type: "list_item_end" },
// { type: 'list_item_end' }, // { type: "list_item_start" },
// { type: 'list_item_start' }, // { type: "text",
// { type: 'text', // text: "`flag` {string} **Default:** `'a'`" },
// text: 'silent: Boolean, whether to send output to parent\'s stdio.' }, // { type: "space" },
// { type: 'text', text: 'Default: `false`' }, // { type: "list_item_end" },
// { type: 'space' }, // { type: "list_end" },
// { type: 'list_item_end' }, // { type: "list_item_end" } ]
// { type: 'list_end' },
// { type: 'list_item_end' },
// { type: 'list_end' } ]
// //
// to something like: // to something like:
// //
// [ { name: 'settings', // [ { textRaw: "`options` {Object|string} ",
// type: 'object', // options: [
// optional: true, // { textRaw: "`encoding` {string|null} **Default:** `'utf8'` ",
// settings: // name: "encoding",
// [ { name: 'exec', // type: "string|null",
// type: 'string', // default: "`'utf8'`" },
// desc: 'file path to worker file', // { textRaw: "`mode` {integer} **Default:** `0o666` ",
// default: '__filename' }, // name: "mode",
// { name: 'args', // type: "integer",
// type: 'array', // default: "`0o666`" },
// default: 'process.argv.slice(2)', // { textRaw: "`flag` {string} **Default:** `'a'` ",
// desc: 'string arguments passed to worker.' }, // name: "flag",
// { name: 'silent', // type: "string",
// type: 'boolean', // default: "`'a'`" } ],
// desc: 'whether to send output to parent\'s stdio.', // name: "options",
// default: 'false' } ] } ] // type: "Object|string",
// optional: true } ]
function processList(section) { function processList(section) {
const list = section.list; const { list } = section;
const values = []; const values = [];
var current;
const stack = []; const stack = [];
let current;
// For now, *just* build the hierarchical list. // For now, *just* build the hierarchical list.
list.forEach(function(tok) { list.forEach((tok) => {
const type = tok.type; const { type } = tok;
if (type === 'space') return; if (type === 'space') return;
if (type === 'list_item_start' || type === 'loose_item_start') { if (type === 'list_item_start' || type === 'loose_item_start') {
var n = {}; const item = {};
if (!current) { if (!current) {
values.push(n); values.push(item);
current = n; current = item;
} else { } else {
current.options = current.options || []; current.options = current.options || [];
stack.push(current); stack.push(current);
current.options.push(n); current.options.push(item);
current = n; current = item;
} }
} else if (type === 'list_item_end') { } else if (type === 'list_item_end') {
if (!current) { if (!current) {
@ -277,33 +276,35 @@ function processList(section) {
switch (section.type) { switch (section.type) {
case 'ctor': case 'ctor':
case 'classMethod': case 'classMethod':
case 'method': case 'method': {
// Each item is an argument, unless the name is 'return', // Each item is an argument, unless the name is 'return',
// in which case it's the return value. // in which case it's the return value.
section.signatures = section.signatures || []; section.signatures = section.signatures || [];
var sig = {}; const sig = {};
section.signatures.push(sig); section.signatures.push(sig);
sig.params = values.filter(function(v) { sig.params = values.filter((value) => {
if (v.name === 'return') { if (value.name === 'return') {
sig.return = v; sig.return = value;
return false; return false;
} }
return true; return true;
}); });
parseSignature(section.textRaw, sig); parseSignature(section.textRaw, sig);
break; break;
}
case 'property': case 'property': {
// There should be only one item, which is the value. // There should be only one item, which is the value.
// Copy the data up to the section. // Copy the data up to the section.
var value = values[0] || {}; const value = values[0] || {};
delete value.name; delete value.name;
section.typeof = value.type || section.typeof; section.typeof = value.type || section.typeof;
delete value.type; delete value.type;
Object.keys(value).forEach(function(k) { Object.keys(value).forEach((key) => {
section[k] = value[k]; section[key] = value[key];
}); });
break; break;
}
case 'event': case 'event':
// Event: each item is an argument. // Event: each item is an argument.
@ -313,117 +314,111 @@ function processList(section) {
default: default:
if (section.list.length > 0) { if (section.list.length > 0) {
section.desc = section.desc || []; section.desc = section.desc || [];
for (var i = 0; i < section.list.length; i++) { section.desc.push(...section.list);
section.desc.push(section.list[i]);
}
} }
} }
// section.listParsed = values;
delete section.list; delete section.list;
} }
const paramExpr = /\((.*)\);?$/;
// textRaw = "someobject.someMethod(a[, b=100][, c])" const paramExpr = /\((.+)\);?$/;
// text: "someobject.someMethod(a[, b=100][, c])"
function parseSignature(text, sig) { function parseSignature(text, sig) {
var params = text.match(paramExpr); let [, sigParams] = text.match(paramExpr) || [];
if (!params) return; if (!sigParams) return;
params = params[1]; sigParams = sigParams.split(',');
params = params.split(/,/); let optionalLevel = 0;
var optionalLevel = 0;
const optionalCharDict = { '[': 1, ' ': 0, ']': -1 }; const optionalCharDict = { '[': 1, ' ': 0, ']': -1 };
params.forEach(function(p, i) { sigParams.forEach((sigParam, i) => {
p = p.trim(); sigParam = sigParam.trim();
if (!p) return; if (!sigParam) {
var param = sig.params[i]; throw new Error(`Empty parameter slot: ${text}`);
var optional = false; }
var def; let listParam = sig.params[i];
let optional = false;
let defaultValue;
// For grouped optional params such as someMethod(a[, b[, c]]). // For grouped optional params such as someMethod(a[, b[, c]]).
var pos; let pos;
for (pos = 0; pos < p.length; pos++) { for (pos = 0; pos < sigParam.length; pos++) {
if (optionalCharDict[p[pos]] === undefined) { break; } const levelChange = optionalCharDict[sigParam[pos]];
optionalLevel += optionalCharDict[p[pos]]; if (levelChange === undefined) break;
optionalLevel += levelChange;
} }
p = p.substring(pos); sigParam = sigParam.substring(pos);
optional = (optionalLevel > 0); optional = (optionalLevel > 0);
for (pos = p.length - 1; pos >= 0; pos--) { for (pos = sigParam.length - 1; pos >= 0; pos--) {
if (optionalCharDict[p[pos]] === undefined) { break; } const levelChange = optionalCharDict[sigParam[pos]];
optionalLevel += optionalCharDict[p[pos]]; if (levelChange === undefined) break;
optionalLevel += levelChange;
} }
p = p.substring(0, pos + 1); sigParam = sigParam.substring(0, pos + 1);
const eq = p.indexOf('='); const eq = sigParam.indexOf('=');
if (eq !== -1) { if (eq !== -1) {
def = p.substr(eq + 1); defaultValue = sigParam.substr(eq + 1);
p = p.substr(0, eq); sigParam = sigParam.substr(0, eq);
} }
if (!param) { if (!listParam) {
param = sig.params[i] = { name: p }; listParam = sig.params[i] = { name: sigParam };
} }
// At this point, the name should match. // At this point, the name should match.
if (p !== param.name) { if (sigParam !== listParam.name) {
console.error('Warning: invalid param "%s"', p); throw new Error(
console.error(` > ${JSON.stringify(param)}`); `Warning: invalid param "${sigParam}"\n` +
console.error(` > ${text}`); ` > ${JSON.stringify(listParam)}\n` +
` > ${text}`
);
} }
if (optional) param.optional = true; if (optional) listParam.optional = true;
if (def !== undefined) param.default = def; if (defaultValue !== undefined) listParam.default = defaultValue.trim();
}); });
} }
const returnExpr = /^returns?\s*:?\s*/i;
const nameExpr = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/;
const typeExpr = /^\{([^}]+)\}\s*/;
const leadingHyphen = /^-\s*/;
const defaultExpr = /\s*\*\*Default:\*\*\s*([^]+)$/i;
function parseListItem(item) { function parseListItem(item) {
if (item.options) item.options.forEach(parseListItem); if (item.options) item.options.forEach(parseListItem);
if (!item.textRaw) return; if (!item.textRaw) {
throw new Error(`Empty list item: ${JSON.stringify(item)}`);
}
// The goal here is to find the name, type, default, and optional. // The goal here is to find the name, type, default, and optional.
// Anything left over is 'desc'. // Anything left over is 'desc'.
var text = item.textRaw.trim(); let text = item.textRaw.trim();
// text = text.replace(/^(Argument|Param)s?\s*:?\s*/i, '');
text = text.replace(/^, /, '').trim(); if (returnExpr.test(text)) {
const retExpr = /^returns?\s*:?\s*/i;
const ret = text.match(retExpr);
if (ret) {
item.name = 'return'; item.name = 'return';
text = text.replace(retExpr, ''); text = text.replace(returnExpr, '');
} else { } else {
var nameExpr = /^['`"]?([^'`": {]+)['`"]?\s*:?\s*/; const [, name] = text.match(nameExpr) || [];
var name = text.match(nameExpr);
if (name) { if (name) {
item.name = name[1]; item.name = name;
text = text.replace(nameExpr, ''); text = text.replace(nameExpr, '');
} }
} }
text = text.trim(); const [, type] = text.match(typeExpr) || [];
const defaultExpr = /\s*\*\*Default:\*\*\s*([^]+)$/i;
const def = text.match(defaultExpr);
if (def) {
item.default = def[1].replace(/\.$/, '');
text = text.replace(defaultExpr, '');
}
text = text.trim();
const typeExpr = /^\{([^}]+)\}/;
const type = text.match(typeExpr);
if (type) { if (type) {
item.type = type[1]; item.type = type;
text = text.replace(typeExpr, ''); text = text.replace(typeExpr, '');
} }
text = text.trim(); text = text.replace(leadingHyphen, '');
const optExpr = /^Optional\.|(?:, )?Optional$/;
const optional = text.match(optExpr); const [, defaultValue] = text.match(defaultExpr) || [];
if (optional) { if (defaultValue) {
item.optional = true; item.default = defaultValue.replace(/\.$/, '');
text = text.replace(optExpr, ''); text = text.replace(defaultExpr, '');
} }
text = text.replace(/^\s*-\s*/, '');
text = text.trim();
if (text) item.desc = text; if (text) item.desc = text;
} }
@ -437,7 +432,7 @@ function finishSection(section, parent) {
if (!section.type) { if (!section.type) {
section.type = 'module'; section.type = 'module';
if (parent && (parent.type === 'misc')) { if (parent.type === 'misc') {
section.type = 'misc'; section.type = 'misc';
} }
section.displayName = section.name; section.displayName = section.name;
@ -458,13 +453,13 @@ function finishSection(section, parent) {
// Merge them into the parent. // Merge them into the parent.
if (section.type === 'class' && section.ctors) { if (section.type === 'class' && section.ctors) {
section.signatures = section.signatures || []; section.signatures = section.signatures || [];
var sigs = section.signatures; const sigs = section.signatures;
section.ctors.forEach(function(ctor) { section.ctors.forEach((ctor) => {
ctor.signatures = ctor.signatures || [{}]; ctor.signatures = ctor.signatures || [{}];
ctor.signatures.forEach(function(sig) { ctor.signatures.forEach((sig) => {
sig.desc = ctor.desc; sig.desc = ctor.desc;
}); });
sigs.push.apply(sigs, ctor.signatures); sigs.push(...ctor.signatures);
}); });
delete section.ctors; delete section.ctors;
} }
@ -472,23 +467,26 @@ function finishSection(section, parent) {
// Properties are a bit special. // Properties are a bit special.
// Their "type" is the type of object, not "property". // Their "type" is the type of object, not "property".
if (section.properties) { if (section.properties) {
section.properties.forEach(function(p) { section.properties.forEach((prop) => {
if (p.typeof) p.type = p.typeof; if (prop.typeof) {
else delete p.type; prop.type = prop.typeof;
delete p.typeof; delete prop.typeof;
} else {
delete prop.type;
}
}); });
} }
// Handle clones. // Handle clones.
if (section.clone) { if (section.clone) {
var clone = section.clone; const { clone } = section;
delete section.clone; delete section.clone;
delete clone.clone; delete clone.clone;
deepCopy(section, clone); deepCopy(section, clone);
finishSection(clone, parent); finishSection(clone, parent);
} }
var plur; let plur;
if (section.type.slice(-1) === 's') { if (section.type.slice(-1) === 's') {
plur = `${section.type}es`; plur = `${section.type}es`;
} else if (section.type.slice(-1) === 'y') { } else if (section.type.slice(-1) === 'y') {
@ -501,8 +499,8 @@ function finishSection(section, parent) {
// collection of stuff, like the "globals" section. // collection of stuff, like the "globals" section.
// Make the children top-level items. // Make the children top-level items.
if (section.type === 'misc') { if (section.type === 'misc') {
Object.keys(section).forEach(function(k) { Object.keys(section).forEach((key) => {
switch (k) { switch (key) {
case 'textRaw': case 'textRaw':
case 'name': case 'name':
case 'type': case 'type':
@ -513,10 +511,10 @@ function finishSection(section, parent) {
if (parent.type === 'misc') { if (parent.type === 'misc') {
return; return;
} }
if (Array.isArray(k) && parent[k]) { if (parent[key] && Array.isArray(parent[key])) {
parent[k] = parent[k].concat(section[k]); parent[key] = parent[key].concat(section[key]);
} else if (!parent[k]) { } else if (!parent[key]) {
parent[k] = section[k]; parent[key] = section[key];
} }
} }
}); });
@ -530,28 +528,26 @@ function finishSection(section, parent) {
// Not a general purpose deep copy. // Not a general purpose deep copy.
// But sufficient for these basic things. // But sufficient for these basic things.
function deepCopy(src, dest) { function deepCopy(src, dest) {
Object.keys(src).filter(function(k) { Object.keys(src)
return !dest.hasOwnProperty(k); .filter((key) => !dest.hasOwnProperty(key))
}).forEach(function(k) { .forEach((key) => { dest[key] = cloneValue(src[key]); });
dest[k] = deepCopy_(src[k]);
});
} }
function deepCopy_(src) { function cloneValue(src) {
if (!src) return src; if (!src) return src;
if (Array.isArray(src)) { if (Array.isArray(src)) {
const c = new Array(src.length); const clone = new Array(src.length);
src.forEach(function(v, i) { src.forEach((value, i) => {
c[i] = deepCopy_(v); clone[i] = cloneValue(value);
}); });
return c; return clone;
} }
if (typeof src === 'object') { if (typeof src === 'object') {
const c = {}; const clone = {};
Object.keys(src).forEach(function(k) { Object.keys(src).forEach((key) => {
c[k] = deepCopy_(src[k]); clone[key] = cloneValue(src[key]);
}); });
return c; return clone;
} }
return src; return src;
} }