mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
[doclint] Start linting method arguments
This patch introduces a general Documentation.diff method, which produces a diff of two documentations. With this, the patch teaches documentation linter to lint method arguments. References #14.
This commit is contained in:
parent
f05cc3168c
commit
7765760186
28
docs/api.md
28
docs/api.md
@ -14,8 +14,8 @@
|
|||||||
* [page.addScriptTag(url)](#pageaddscripttagurl)
|
* [page.addScriptTag(url)](#pageaddscripttagurl)
|
||||||
* [page.click()](#pageclick)
|
* [page.click()](#pageclick)
|
||||||
* [page.close()](#pageclose)
|
* [page.close()](#pageclose)
|
||||||
* [page.evaluate(fun, args)](#pageevaluatefun-args)
|
* [page.evaluate(fun, ...args)](#pageevaluatefun-args)
|
||||||
* [page.evaluateOnInitialized(fun, args)](#pageevaluateoninitializedfun-args)
|
* [page.evaluateOnInitialized(fun, ...args)](#pageevaluateoninitializedfun-args)
|
||||||
* [page.focus()](#pagefocus)
|
* [page.focus()](#pagefocus)
|
||||||
* [page.frames()](#pageframes)
|
* [page.frames()](#pageframes)
|
||||||
* [page.httpHeaders()](#pagehttpheaders)
|
* [page.httpHeaders()](#pagehttpheaders)
|
||||||
@ -44,7 +44,7 @@
|
|||||||
* [dialog.message()](#dialogmessage)
|
* [dialog.message()](#dialogmessage)
|
||||||
- [class: Frame](#class-frame)
|
- [class: Frame](#class-frame)
|
||||||
* [frame.childFrames()](#framechildframes)
|
* [frame.childFrames()](#framechildframes)
|
||||||
* [frame.evaluate(fun, args)](#frameevaluatefun-args)
|
* [frame.evaluate(fun, ...args)](#frameevaluatefun-args)
|
||||||
* [frame.isDetached()](#frameisdetached)
|
* [frame.isDetached()](#frameisdetached)
|
||||||
* [frame.isMainFrame()](#frameismainframe)
|
* [frame.isMainFrame()](#frameismainframe)
|
||||||
* [frame.name()](#framename)
|
* [frame.name()](#framename)
|
||||||
@ -133,16 +133,16 @@ Pages could be closed by `page.close()` method.
|
|||||||
|
|
||||||
- returns: <[Promise]> Returns promise which resolves when page gets closed.
|
- returns: <[Promise]> Returns promise which resolves when page gets closed.
|
||||||
|
|
||||||
#### page.evaluate(fun, args)
|
#### page.evaluate(fun, ...args)
|
||||||
|
|
||||||
- `fun` <[function]> Function to be evaluated in browser context
|
- `fun` <[function]> Function to be evaluated in browser context
|
||||||
- `args` <[Array]<[string]>> Arguments to pass to `fun`
|
- `...args` <...[string]> Arguments to pass to `fun`
|
||||||
- returns: <[Promise]<[Object]>> Promise which resolves to function return value
|
- returns: <[Promise]<[Object]>> Promise which resolves to function return value
|
||||||
|
|
||||||
#### page.evaluateOnInitialized(fun, args)
|
#### page.evaluateOnInitialized(fun, ...args)
|
||||||
|
|
||||||
- `fun` <[function]> Function to be evaluated in browser context
|
- `fun` <[function]> Function to be evaluated in browser context
|
||||||
- `args` <[Array]<[string]>> Arguments to pass to `fun`
|
- `...args` <...[string]> Arguments to pass to `fun`
|
||||||
- returns: <[Promise]<[Object]>> Promise which resolves to function
|
- returns: <[Promise]<[Object]>> Promise which resolves to function
|
||||||
|
|
||||||
#### page.focus()
|
#### page.focus()
|
||||||
@ -156,7 +156,7 @@ Pages could be closed by `page.close()` method.
|
|||||||
|
|
||||||
#### page.injectFile(filePath)
|
#### page.injectFile(filePath)
|
||||||
|
|
||||||
- `url` <[string]> Path to the javascript file to be injected into page.
|
- `filePath` <[string]> Path to the javascript file to be injected into page.
|
||||||
- returns: <[Promise]> Promise which resolves when file gets successfully evaluated in page.
|
- returns: <[Promise]> Promise which resolves when file gets successfully evaluated in page.
|
||||||
|
|
||||||
#### page.mainFrame()
|
#### page.mainFrame()
|
||||||
@ -222,7 +222,7 @@ The `page.navigate` will throw an error if:
|
|||||||
|
|
||||||
#### page.setInPageCallback(name, callback)
|
#### page.setInPageCallback(name, callback)
|
||||||
|
|
||||||
- `url` <[string]> Name of the callback to be assigned on window object
|
- `name` <[string]> Name of the callback to be assigned on window object
|
||||||
- `callback` <[function]> Callback function which will be called in node.js
|
- `callback` <[function]> Callback function which will be called in node.js
|
||||||
- returns: <[Promise]> Promise which resolves when callback is successfully initialized
|
- returns: <[Promise]> Promise which resolves when callback is successfully initialized
|
||||||
|
|
||||||
@ -267,6 +267,12 @@ The `page.navigate` will throw an error if:
|
|||||||
|
|
||||||
#### page.waitFor(selector)
|
#### page.waitFor(selector)
|
||||||
|
|
||||||
|
- `selector` <[string]> A query selector to wait for on the page.
|
||||||
|
|
||||||
|
Wait for the `selector` to appear in page. If at the moment of calling
|
||||||
|
the method the `selector` already exists, the method will return
|
||||||
|
immediately.
|
||||||
|
|
||||||
Shortcut for [page.mainFrame().waitFor(selector)](#framewaitforselector).
|
Shortcut for [page.mainFrame().waitFor(selector)](#framewaitforselector).
|
||||||
|
|
||||||
### class: Dialog
|
### class: Dialog
|
||||||
@ -276,10 +282,10 @@ Shortcut for [page.mainFrame().waitFor(selector)](#framewaitforselector).
|
|||||||
|
|
||||||
### class: Frame
|
### class: Frame
|
||||||
#### frame.childFrames()
|
#### frame.childFrames()
|
||||||
#### frame.evaluate(fun, args)
|
#### frame.evaluate(fun, ...args)
|
||||||
|
|
||||||
- `fun` <[function]> Function to be evaluated in browser context
|
- `fun` <[function]> Function to be evaluated in browser context
|
||||||
- `args` <[Array]<[string]>> Arguments to pass to `fun`
|
- `...args` <[Array]<[string]>> Arguments to pass to `fun`
|
||||||
- returns: <[Promise]<[Object]>> Promise which resolves to function return value
|
- returns: <[Promise]<[Object]>> Promise which resolves to function return value
|
||||||
|
|
||||||
#### frame.isDetached()
|
#### frame.isDetached()
|
||||||
|
@ -583,13 +583,13 @@ class Page extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} selector
|
* @param {string} selector
|
||||||
* @param {!Array<string>} files
|
* @param {!Array<string>} filePaths
|
||||||
* @return {!Promise}
|
* @return {!Promise}
|
||||||
*/
|
*/
|
||||||
async uploadFile(selector, ...files) {
|
async uploadFile(selector, ...filePaths) {
|
||||||
try {
|
try {
|
||||||
const nodeId = await this._querySelector(selector);
|
const nodeId = await this._querySelector(selector);
|
||||||
await this._client.send('DOM.setFileInputFiles', { nodeId, files });
|
await this._client.send('DOM.setFileInputFiles', { nodeId, files: filePaths });
|
||||||
} finally {
|
} finally {
|
||||||
await this._client.send('DOM.disable');
|
await this._client.send('DOM.disable');
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,56 @@
|
|||||||
let Documentation = {};
|
class Documentation {
|
||||||
|
/**
|
||||||
|
* @param {!Array<!Documentation.Class>} clasesArray
|
||||||
|
*/
|
||||||
|
constructor(classesArray) {
|
||||||
|
this.classesArray = classesArray;
|
||||||
|
this.classes = new Map();
|
||||||
|
for (let cls of classesArray)
|
||||||
|
this.classes.set(cls.name, cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!Documentation} actual
|
||||||
|
* @param {!Documentation} expected
|
||||||
|
*/
|
||||||
|
static diff(actual, expected) {
|
||||||
|
const result = {};
|
||||||
|
// Diff classes.
|
||||||
|
const actualClasses = Array.from(actual.classes.keys()).sort();
|
||||||
|
const expectedClasses = Array.from(expected.classes.keys()).sort();
|
||||||
|
let classesDiff = diff(actualClasses, expectedClasses);
|
||||||
|
result.extraClasses = classesDiff.extra;
|
||||||
|
result.missingClasses = classesDiff.missing;
|
||||||
|
|
||||||
|
result.extraMethods = [];
|
||||||
|
result.missingMethods = [];
|
||||||
|
result.badArguments = [];
|
||||||
|
for (let className of classesDiff.equal) {
|
||||||
|
const actualClass = actual.classes.get(className);
|
||||||
|
const expectedClass = expected.classes.get(className);
|
||||||
|
const actualMethods = Array.from(actualClass.methods.keys()).sort();
|
||||||
|
const expectedMethods = Array.from(expectedClass.methods.keys()).sort();
|
||||||
|
const methodDiff = diff(actualMethods, expectedMethods);
|
||||||
|
result.extraMethods.push(...(methodDiff.extra.map(methodName => className + '.' + methodName)));
|
||||||
|
result.missingMethods.push(...(methodDiff.missing.map(methodName => className + '.' + methodName)));
|
||||||
|
for (let methodName of methodDiff.equal) {
|
||||||
|
const actualMethod = actualClass.methods.get(methodName);
|
||||||
|
const expectedMethod = expectedClass.methods.get(methodName);
|
||||||
|
const actualArgs = actualMethod.args.map(arg => arg.name);
|
||||||
|
const expectedArgs = expectedMethod.args.map(arg => arg.name);
|
||||||
|
const argDiff = diff(actualArgs, expectedArgs);
|
||||||
|
if (argDiff.extra.length || argDiff.missing.diff) {
|
||||||
|
result.badArguments.push({
|
||||||
|
method: `${className}.${methodName}`,
|
||||||
|
missingArgs: argDiff.missing,
|
||||||
|
extraArgs: argDiff.extra
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Documentation.Class = class {
|
Documentation.Class = class {
|
||||||
/**
|
/**
|
||||||
@ -15,6 +67,17 @@ Documentation.Class = class {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Documentation.Method = class {
|
Documentation.Method = class {
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @param {!Array<!Documentation.Argument>} args
|
||||||
|
*/
|
||||||
|
constructor(name, args) {
|
||||||
|
this.name = name;
|
||||||
|
this.args = args;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Documentation.Argument = class {
|
||||||
/**
|
/**
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
*/
|
*/
|
||||||
@ -23,4 +86,78 @@ Documentation.Method = class {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!Array<string>} actual
|
||||||
|
* @param {!Array<string>} expected
|
||||||
|
* @return {{extra: !Array<string>, missing: !Array<string>, equal: !Array<string>}}
|
||||||
|
*/
|
||||||
|
function diff(actual, expected) {
|
||||||
|
const N = actual.length;
|
||||||
|
const M = expected.length;
|
||||||
|
if (N === 0 && M === 0)
|
||||||
|
return { extra: [], missing: [], equal: []};
|
||||||
|
if (N === 0)
|
||||||
|
return {extra: [], missing: expected.slice(), equal: []};
|
||||||
|
if (M === 0)
|
||||||
|
return {extra: actual.slice(), missing: [], equal: []};
|
||||||
|
let d = new Array(N);
|
||||||
|
let bt = new Array(N);
|
||||||
|
for (let i = 0; i < N; ++i) {
|
||||||
|
d[i] = new Array(M);
|
||||||
|
bt[i] = new Array(M);
|
||||||
|
for (let j = 0; j < M; ++j) {
|
||||||
|
const top = val(i - 1, j);
|
||||||
|
const left = val(i, j - 1);
|
||||||
|
if (top > left) {
|
||||||
|
d[i][j] = top;
|
||||||
|
bt[i][j] = 'extra';
|
||||||
|
} else {
|
||||||
|
d[i][j] = left;
|
||||||
|
bt[i][j] = 'missing';
|
||||||
|
}
|
||||||
|
let diag = val(i - 1, j - 1);
|
||||||
|
if (actual[i] === expected[j] && d[i][j] < diag + 1) {
|
||||||
|
d[i][j] = diag + 1;
|
||||||
|
bt[i][j] = 'eq';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Backtrack results.
|
||||||
|
let i = N - 1;
|
||||||
|
let j = M - 1;
|
||||||
|
let missing = [];
|
||||||
|
let extra = [];
|
||||||
|
let equal = [];
|
||||||
|
while (i >= 0 && j >= 0) {
|
||||||
|
switch (bt[i][j]) {
|
||||||
|
case 'extra':
|
||||||
|
extra.push(actual[i]);
|
||||||
|
i -= 1;
|
||||||
|
break;
|
||||||
|
case 'missing':
|
||||||
|
missing.push(expected[j]);
|
||||||
|
j -= 1;
|
||||||
|
break;
|
||||||
|
case 'eq':
|
||||||
|
equal.push(actual[i]);
|
||||||
|
i -= 1;
|
||||||
|
j -= 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (i >= 0)
|
||||||
|
extra.push(actual[i--]);
|
||||||
|
while (j >= 0)
|
||||||
|
missing.push(expected[j--]);
|
||||||
|
extra.reverse();
|
||||||
|
missing.reverse();
|
||||||
|
equal.reverse();
|
||||||
|
return {extra, missing, equal};
|
||||||
|
|
||||||
|
function val(i, j) {
|
||||||
|
return i < 0 || j < 0 ? 0 : d[i][j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = Documentation;
|
module.exports = Documentation;
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
const esprima = require('esprima');
|
const esprima = require('esprima');
|
||||||
const ESTreeWalker = require('../../third_party/chromium/ESTreeWalker');
|
const ESTreeWalker = require('../../third_party/chromium/ESTreeWalker');
|
||||||
const Documentation = require('./Documentation');
|
const Documentation = require('./Documentation');
|
||||||
@ -22,13 +24,17 @@ class JSOutline {
|
|||||||
|
|
||||||
_onClassDeclaration(node) {
|
_onClassDeclaration(node) {
|
||||||
this._flushClassIfNeeded();
|
this._flushClassIfNeeded();
|
||||||
this._currentClassName = this._getIdentifier(node.id);
|
this._currentClassName = this._extractText(node.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMethodDefinition(node) {
|
_onMethodDefinition(node) {
|
||||||
console.assert(this._currentClassName !== null);
|
console.assert(this._currentClassName !== null);
|
||||||
let methodName = this._getIdentifier(node.key);
|
console.assert(node.value.type === 'FunctionExpression');
|
||||||
let method = new Documentation.Method(methodName);
|
const args = [];
|
||||||
|
for (let param of node.value.params)
|
||||||
|
args.push(new Documentation.Argument(this._extractText(param)));
|
||||||
|
let methodName = this._extractText(node.key);
|
||||||
|
let method = new Documentation.Method(methodName, args);
|
||||||
this._currentClassMethods.push(method);
|
this._currentClassMethods.push(method);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,12 +47,27 @@ class JSOutline {
|
|||||||
this._currentClassMethods = [];
|
this._currentClassMethods = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
_getIdentifier(node) {
|
_extractText(node) {
|
||||||
if (!node)
|
if (!node)
|
||||||
return null;
|
return null;
|
||||||
let text = this._text.substring(node.range[0], node.range[1]).trim();
|
let text = this._text.substring(node.range[0], node.range[1]).trim();
|
||||||
return /^[$A-Z_][0-9A-Z_$]*$/i.test(text) ? text : null;
|
return text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = JSOutline;
|
/**
|
||||||
|
* @param {!Array<string>} dirPath
|
||||||
|
* @return {!Promise<!Documentation>}
|
||||||
|
*/
|
||||||
|
module.exports = async function(dirPath) {
|
||||||
|
let filePaths = fs.readdirSync(dirPath)
|
||||||
|
.filter(fileName => fileName.endsWith('.js'))
|
||||||
|
.map(fileName => path.join(dirPath, fileName));
|
||||||
|
let classes = [];
|
||||||
|
for (let filePath of filePaths) {
|
||||||
|
let outline = new JSOutline(fs.readFileSync(filePath, 'utf8'));
|
||||||
|
classes.push(...outline.classes);
|
||||||
|
}
|
||||||
|
return new Documentation(classes);
|
||||||
|
};
|
||||||
|
|
126
test/doclint/MDBuilder.js
Normal file
126
test/doclint/MDBuilder.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const markdownToc = require('markdown-toc');
|
||||||
|
const path = require('path');
|
||||||
|
const Documentation = require('./Documentation');
|
||||||
|
const commonmark = require('commonmark');
|
||||||
|
const Browser = require('../../lib/Browser');
|
||||||
|
|
||||||
|
class MDOutline {
|
||||||
|
/**
|
||||||
|
* @param {!Browser} browser
|
||||||
|
* @param {string} text
|
||||||
|
* @return {!MDOutline}
|
||||||
|
*/
|
||||||
|
static async create(browser, text) {
|
||||||
|
// Render markdown as HTML.
|
||||||
|
const reader = new commonmark.Parser();
|
||||||
|
const parsed = reader.parse(text);
|
||||||
|
const writer = new commonmark.HtmlRenderer();
|
||||||
|
const html = writer.render(parsed);
|
||||||
|
|
||||||
|
// Extract headings.
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setContent(html);
|
||||||
|
const classes = await page.evaluate(() => {
|
||||||
|
let classes = [];
|
||||||
|
let currentClass = {};
|
||||||
|
let method = {};
|
||||||
|
for (let element of document.body.querySelectorAll('h3, h4, h4 + ul > li')) {
|
||||||
|
if (element.matches('h3')) {
|
||||||
|
currentClass = {
|
||||||
|
name: element.textContent,
|
||||||
|
methods: [],
|
||||||
|
};
|
||||||
|
classes.push(currentClass);
|
||||||
|
} else if (element.matches('h4')) {
|
||||||
|
method = {
|
||||||
|
name: element.textContent,
|
||||||
|
args: []
|
||||||
|
};
|
||||||
|
currentClass.methods.push(method);
|
||||||
|
} else if (element.matches('li') && element.firstChild.matches && element.firstChild.matches('code')) {
|
||||||
|
method.args.push(element.firstChild.textContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return classes;
|
||||||
|
});
|
||||||
|
return new MDOutline(classes);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(classes) {
|
||||||
|
this.classes = [];
|
||||||
|
this.errors = [];
|
||||||
|
const classHeading = /^class: (\w+)$/;
|
||||||
|
const constructorRegex = /^new (\w+)\((.*)\)$/;
|
||||||
|
const methodRegex = /^(\w+)\.(\w+)\((.*)\)$/;
|
||||||
|
let currentClassName = null;
|
||||||
|
let currentClassMethods = [];
|
||||||
|
for (const cls of classes) {
|
||||||
|
let match = cls.name.match(classHeading);
|
||||||
|
currentClassName = match[1];
|
||||||
|
for (let mdMethod of cls.methods) {
|
||||||
|
let className = null;
|
||||||
|
let methodName = null;
|
||||||
|
let parameters = null;
|
||||||
|
let title = mdMethod.name;
|
||||||
|
if (constructorRegex.test(title)) {
|
||||||
|
let match = title.match(constructorRegex);
|
||||||
|
className = match[1];
|
||||||
|
parameters = match[2];
|
||||||
|
methodName = 'constructor';
|
||||||
|
} else if (methodRegex.test(title)) {
|
||||||
|
let match = title.match(methodRegex);
|
||||||
|
className = match[1];
|
||||||
|
methodName = match[2];
|
||||||
|
parameters = match[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentClassName || !className || !methodName || className.toLowerCase() !== currentClassName.toLowerCase()) {
|
||||||
|
console.warn('failed to process header as method: ' + mdMethod.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parameters = parameters.trim().replace(/[\[\]]/g, '');
|
||||||
|
if (parameters !== mdMethod.args.join(', '))
|
||||||
|
this.errors.push(`Heading arguments for "${mdMethod.name}" do not match described ones, i.e. "${parameters}" != "${mdMethod.args.join(', ')}"`);
|
||||||
|
let args = mdMethod.args.map(arg => new Documentation.Argument(arg));
|
||||||
|
let method = new Documentation.Method(methodName, args);
|
||||||
|
currentClassMethods.push(method);
|
||||||
|
}
|
||||||
|
flushClassIfNeeded.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushClassIfNeeded() {
|
||||||
|
if (currentClassName === null)
|
||||||
|
return;
|
||||||
|
this.classes.push(new Documentation.Class(currentClassName, currentClassMethods));
|
||||||
|
currentClassName = null;
|
||||||
|
currentClassMethods = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {!Array<string>} dirPath
|
||||||
|
* @return {!Promise<{documentation: !Documentation, errors: !Array<string>}>}
|
||||||
|
*/
|
||||||
|
module.exports = async function(dirPath) {
|
||||||
|
let filePaths = fs.readdirSync(dirPath)
|
||||||
|
.filter(fileName => fileName.endsWith('.md'))
|
||||||
|
.map(fileName => path.join(dirPath, fileName));
|
||||||
|
let classes = [];
|
||||||
|
let errors = [];
|
||||||
|
const browser = new Browser({args: ['--no-sandbox']});
|
||||||
|
for (let filePath of filePaths) {
|
||||||
|
const markdownText = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const newMarkdownText = markdownToc.insert(markdownText);
|
||||||
|
if (markdownText !== newMarkdownText)
|
||||||
|
errors.push('Markdown TOC is outdated, run `yarn generate-toc`');
|
||||||
|
let outline = await MDOutline.create(browser, markdownText);
|
||||||
|
classes.push(...outline.classes);
|
||||||
|
errors.push(...outline.errors);
|
||||||
|
}
|
||||||
|
await browser.close();
|
||||||
|
const documentation = new Documentation(classes);
|
||||||
|
return { documentation, errors };
|
||||||
|
};
|
||||||
|
|
@ -1,81 +0,0 @@
|
|||||||
const Documentation = require('./Documentation');
|
|
||||||
const commonmark = require('commonmark');
|
|
||||||
const Browser = require('../../lib/Browser');
|
|
||||||
|
|
||||||
class MDOutline {
|
|
||||||
/**
|
|
||||||
* @return {!MDOutline}
|
|
||||||
*/
|
|
||||||
static async create(text) {
|
|
||||||
// Render markdown as HTML.
|
|
||||||
const reader = new commonmark.Parser();
|
|
||||||
const parsed = reader.parse(text);
|
|
||||||
const writer = new commonmark.HtmlRenderer();
|
|
||||||
const html = writer.render(parsed);
|
|
||||||
|
|
||||||
// Extract headings.
|
|
||||||
const browser = new Browser({args: ['--no-sandbox']});
|
|
||||||
const page = await browser.newPage();
|
|
||||||
await page.setContent(html);
|
|
||||||
const headings = await page.evaluate(() => {
|
|
||||||
let headings = {};
|
|
||||||
let methods = [];
|
|
||||||
for (let header of document.body.querySelectorAll('h3,h4')) {
|
|
||||||
if (header.matches('h3')) {
|
|
||||||
methods = [];
|
|
||||||
headings[header.textContent] = methods;
|
|
||||||
} else {
|
|
||||||
methods.push(header.textContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return headings;
|
|
||||||
});
|
|
||||||
await browser.close();
|
|
||||||
return new MDOutline(headings);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(headings) {
|
|
||||||
this.classes = [];
|
|
||||||
const classHeading = /^class: (\w+)$/;
|
|
||||||
const constructorRegex = /^new (\w+)\((.*)\)$/;
|
|
||||||
const methodRegex = /^(\w+)\.(\w+)\((.*)\)$/;
|
|
||||||
let currentClassName = null;
|
|
||||||
let currentClassMethods = [];
|
|
||||||
for (const heading of Object.keys(headings)) {
|
|
||||||
|
|
||||||
let match = heading.match(classHeading);
|
|
||||||
currentClassName = match[1];
|
|
||||||
for (const title of headings[heading]) {
|
|
||||||
let className = null;
|
|
||||||
let methodName = null;
|
|
||||||
if (constructorRegex.test(title)) {
|
|
||||||
let match = title.match(constructorRegex);
|
|
||||||
className = match[1];
|
|
||||||
methodName = 'constructor';
|
|
||||||
} else if (methodRegex.test(title)) {
|
|
||||||
let match = title.match(methodRegex);
|
|
||||||
className = match[1];
|
|
||||||
methodName = match[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentClassName || !className || !methodName || className.toLowerCase() !== currentClassName.toLowerCase()) {
|
|
||||||
console.warn('failed to process header as method: ' + heading);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let method = new Documentation.Method(methodName);
|
|
||||||
currentClassMethods.push(method);
|
|
||||||
}
|
|
||||||
flushClassIfNeeded.call(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
function flushClassIfNeeded() {
|
|
||||||
if (currentClassName === null)
|
|
||||||
return;
|
|
||||||
this.classes.push(new Documentation.Class(currentClassName, currentClassMethods));
|
|
||||||
currentClassName = null;
|
|
||||||
currentClassMethods = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = MDOutline;
|
|
@ -1,12 +1,9 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const JSOutline = require('./JSOutline');
|
const jsBuilder = require('./JSBuilder');
|
||||||
const MDOutline = require('./MDOutline');
|
const mdBuilder = require('./MDBuilder');
|
||||||
const Documentation = require('./Documentation');
|
const Documentation = require('./Documentation');
|
||||||
const markdownToc = require('markdown-toc');
|
|
||||||
|
|
||||||
const PROJECT_DIR = path.join(__dirname, '..', '..');
|
const PROJECT_DIR = path.join(__dirname, '..', '..');
|
||||||
const apiMdText = fs.readFileSync(path.join(PROJECT_DIR, 'docs', 'api.md'), 'utf8');
|
|
||||||
|
|
||||||
let EXCLUDE_CLASSES = new Set([
|
let EXCLUDE_CLASSES = new Set([
|
||||||
'Connection',
|
'Connection',
|
||||||
@ -30,64 +27,65 @@ let EXCLUDE_METHODS = new Set([
|
|||||||
'Response.constructor',
|
'Response.constructor',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Build up documentation from JS sources.
|
/**
|
||||||
let jsClassesArray = [];
|
* @param {!Documentation} jsDocumentation
|
||||||
let files = fs.readdirSync(path.join(PROJECT_DIR, 'lib'));
|
* @return {!Documentation}
|
||||||
for (let file of files) {
|
*/
|
||||||
if (!file.endsWith('.js'))
|
function filterJSDocumentation(jsDocumentation) {
|
||||||
continue;
|
// Filter classes and methods.
|
||||||
let filePath = path.join(PROJECT_DIR, 'lib', file);
|
let classes = [];
|
||||||
let outline = new JSOutline(fs.readFileSync(filePath, 'utf8'));
|
for (let cls of jsDocumentation.classesArray) {
|
||||||
// Filter out private classes and methods.
|
|
||||||
for (let cls of outline.classes) {
|
|
||||||
if (EXCLUDE_CLASSES.has(cls.name))
|
if (EXCLUDE_CLASSES.has(cls.name))
|
||||||
continue;
|
continue;
|
||||||
let methodsArray = cls.methodsArray.filter(method => {
|
let methods = cls.methodsArray.filter(method => {
|
||||||
if (method.name.startsWith('_'))
|
if (method.name.startsWith('_'))
|
||||||
return false;
|
return false;
|
||||||
let shorthand = `${cls.name}.${method.name}`;
|
return !EXCLUDE_METHODS.has(`${cls.name}.${method.name}`);
|
||||||
return !EXCLUDE_METHODS.has(shorthand);
|
|
||||||
});
|
});
|
||||||
jsClassesArray.push(new Documentation.Class(cls.name, methodsArray));
|
classes.push(new Documentation.Class(cls.name, methods));
|
||||||
}
|
}
|
||||||
|
return new Documentation(classes);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mdClassesArray;
|
let jsDocumentation;
|
||||||
|
let mdDocumentation;
|
||||||
|
let mdParseErrors;
|
||||||
|
let diff;
|
||||||
|
|
||||||
beforeAll(SX(async function() {
|
beforeAll(SX(async function() {
|
||||||
// Build up documentation from MD sources.
|
jsDocumentation = filterJSDocumentation(await jsBuilder(path.join(PROJECT_DIR, 'lib')));
|
||||||
let mdOutline = await MDOutline.create(apiMdText);
|
let mdDoc = await mdBuilder(path.join(PROJECT_DIR, 'docs'));
|
||||||
mdClassesArray = mdOutline.classes;
|
mdDocumentation = mdDoc.documentation;
|
||||||
|
mdParseErrors = mdDoc.errors;
|
||||||
|
diff = Documentation.diff(mdDocumentation, jsDocumentation);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('table of contents', function() {
|
describe('JavaScript documentation parser', function() {
|
||||||
it('should match markdown-toc\'s output', () => {
|
it('should not contain any duplicate classes (probably error in parsing!)', () => {
|
||||||
const newApiMdText = markdownToc.insert(apiMdText);
|
let jsClasses = new Map();
|
||||||
if (apiMdText !== newApiMdText)
|
for (let jsClass of jsDocumentation.classesArray) {
|
||||||
fail('markdown TOC is outdated, run `yarn generate-toc`');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Compare to codebase.
|
|
||||||
describe('api.md', function() {
|
|
||||||
let mdClasses = new Map();
|
|
||||||
let jsClasses = new Map();
|
|
||||||
it('MarkDown should not contain any duplicate classes', () => {
|
|
||||||
for (let mdClass of mdClassesArray) {
|
|
||||||
if (mdClasses.has(mdClass.name))
|
|
||||||
fail(`Documentation has duplicate declaration of ${mdClass.name}`);
|
|
||||||
mdClasses.set(mdClass.name, mdClass);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
it('JavaScript should not contain any duplicate classes (probably error in parsing!)', () => {
|
|
||||||
for (let jsClass of jsClassesArray) {
|
|
||||||
if (jsClasses.has(jsClass.name))
|
if (jsClasses.has(jsClass.name))
|
||||||
fail(`JavaScript has duplicate declaration of ${jsClass.name}. (This probably means that this linter has an error)`);
|
fail(`JavaScript has duplicate declaration of ${jsClass.name}. (This probably means that this linter has an error)`);
|
||||||
jsClasses.set(jsClass.name, jsClass);
|
jsClasses.set(jsClass.name, jsClass);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Markdown Documentation', function() {
|
||||||
|
it('should not have any parse errors', () => {
|
||||||
|
for (let error of mdParseErrors)
|
||||||
|
fail(error);
|
||||||
|
});
|
||||||
|
it('should not contain any duplicate classes', () => {
|
||||||
|
let mdClasses = new Map();
|
||||||
|
for (let mdClass of mdDocumentation.classesArray) {
|
||||||
|
if (mdClasses.has(mdClass.name))
|
||||||
|
fail(`Documentation has duplicate declaration of class ${mdClass.name}`);
|
||||||
|
mdClasses.set(mdClass.name, mdClass);
|
||||||
|
}
|
||||||
|
});
|
||||||
it('class constructors should be defined before other methods', () => {
|
it('class constructors should be defined before other methods', () => {
|
||||||
for (let mdClass of mdClasses.values()) {
|
for (let mdClass of mdDocumentation.classesArray) {
|
||||||
let constructorMethod = mdClass.methods.get('constructor');
|
let constructorMethod = mdClass.methods.get('constructor');
|
||||||
if (!constructorMethod)
|
if (!constructorMethod)
|
||||||
continue;
|
continue;
|
||||||
@ -96,7 +94,7 @@ describe('api.md', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('methods should be sorted alphabetically', () => {
|
it('methods should be sorted alphabetically', () => {
|
||||||
for (let mdClass of mdClasses.values()) {
|
for (let mdClass of mdDocumentation.classesArray) {
|
||||||
for (let i = 0; i < mdClass.methodsArray.length - 1; ++i) {
|
for (let i = 0; i < mdClass.methodsArray.length - 1; ++i) {
|
||||||
// Constructor should always go first.
|
// Constructor should always go first.
|
||||||
if (mdClass.methodsArray[i].name === 'constructor')
|
if (mdClass.methodsArray[i].name === 'constructor')
|
||||||
@ -109,37 +107,29 @@ describe('api.md', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('should not contain any non-existing class', () => {
|
it('should not contain any non-existing class', () => {
|
||||||
for (let mdClass of mdClasses.values()) {
|
for (let className of diff.extraClasses)
|
||||||
if (!jsClasses.has(mdClass.name))
|
fail(`Documentation describes non-existing class ${className}`);
|
||||||
fail(`Documentation describes non-existing class ${mdClass.name}`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
it('should describe all existing classes', () => {
|
it('should describe all existing classes', () => {
|
||||||
for (let jsClass of jsClasses.values()) {
|
for (let className of diff.missingClasses)
|
||||||
if (!mdClasses.has(jsClass.name))
|
fail(`Documentation lacks description of class ${className}`);
|
||||||
fail(`Documentation lacks description of class ${jsClass.name}`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
it('should not contain any non-existing methods', () => {
|
it('should not contain any non-existing methods', () => {
|
||||||
for (let mdClass of mdClasses.values()) {
|
for (let methodName of diff.extraMethods)
|
||||||
let jsClass = jsClasses.get(mdClass.name);
|
fail(`Documentation describes non-existing method: ${methodName}`);
|
||||||
if (!jsClass)
|
|
||||||
continue;
|
|
||||||
for (let method of mdClass.methods.values()) {
|
|
||||||
if (!jsClass.methods.has(method.name))
|
|
||||||
fail(`Documentation describes non-existing method: ${jsClass.name}.${method.name}()`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
it('should describe all existing methods', () => {
|
it('should describe all existing methods', () => {
|
||||||
for (let jsClass of jsClasses.values()) {
|
for (let methodName of diff.missingMethods)
|
||||||
let mdClass = mdClasses.get(jsClass.name);
|
fail(`Documentation lacks method ${methodName}`);
|
||||||
if (!mdClass)
|
});
|
||||||
continue;
|
it('should describe all arguments propertly', () => {
|
||||||
for (let method of jsClass.methods.values()) {
|
for (let badArgument of diff.badArguments) {
|
||||||
if (!mdClass.methods.has(method.name))
|
let text = [`Method ${badArgument.method} fails to describe its parameters:`];
|
||||||
fail(`Documentation lacks ${jsClass.name}.${method.name}()`);
|
for (let missing of badArgument.missingArgs)
|
||||||
}
|
text.push(`- Missing description for "${missing}"`);
|
||||||
|
for (let extra of badArgument.extraArgs)
|
||||||
|
text.push(`- Described non-existing parameter "${extra}"`);
|
||||||
|
fail(text.join('\n'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user