[doclint] introduce class members

This patch unifies Documentation.Method with Documentation.Property,
making it possible to verify sorting order across methods and
properties.

References #14.
This commit is contained in:
Andrey Lushnikov 2017-07-13 22:52:02 -07:00
parent 880575c71d
commit 45c20c7dad
8 changed files with 97 additions and 66 deletions

View File

@ -9,9 +9,9 @@
* [browser.close()](#browserclose) * [browser.close()](#browserclose)
* [browser.closePage(page)](#browserclosepagepage) * [browser.closePage(page)](#browserclosepagepage)
* [browser.newPage()](#browsernewpage) * [browser.newPage()](#browsernewpage)
* [browser.version()](#browserversion)
* [browser.stderr](#browserstderr) * [browser.stderr](#browserstderr)
* [browser.stdout](#browserstdout) * [browser.stdout](#browserstdout)
* [browser.version()](#browserversion)
- [class: Page](#class-page) - [class: Page](#class-page)
* [page.addScriptTag(url)](#pageaddscripttagurl) * [page.addScriptTag(url)](#pageaddscripttagurl)
* [page.click(selector)](#pageclickselector) * [page.click(selector)](#pageclickselector)
@ -56,25 +56,25 @@
* [frame.url()](#frameurl) * [frame.url()](#frameurl)
* [frame.waitFor(selector)](#framewaitforselector) * [frame.waitFor(selector)](#framewaitforselector)
- [class: Request](#class-request) - [class: Request](#class-request)
* [request.response()](#requestresponse)
* [request.headers](#requestheaders) * [request.headers](#requestheaders)
* [request.method](#requestmethod) * [request.method](#requestmethod)
* [request.response()](#requestresponse)
* [request.url](#requesturl) * [request.url](#requesturl)
- [class: Response](#class-response) - [class: Response](#class-response)
* [response.headers](#responseheaders) * [response.headers](#responseheaders)
* [response.ok](#responseok) * [response.ok](#responseok)
* [response.request()](#responserequest)
* [response.status](#responsestatus) * [response.status](#responsestatus)
* [response.statusText](#responsestatustext) * [response.statusText](#responsestatustext)
* [response.url](#responseurl) * [response.url](#responseurl)
* [response.request()](#responserequest)
- [class: InterceptedRequest](#class-interceptedrequest) - [class: InterceptedRequest](#class-interceptedrequest)
* [interceptedRequest.headers](#interceptedrequestheaders)
* [interceptedRequest.method](#interceptedrequestmethod)
* [interceptedRequest.url](#interceptedrequesturl)
* [interceptedRequest.postData](#interceptedrequestpostdata)
* [interceptedRequest.abort()](#interceptedrequestabort) * [interceptedRequest.abort()](#interceptedrequestabort)
* [interceptedRequest.continue()](#interceptedrequestcontinue) * [interceptedRequest.continue()](#interceptedrequestcontinue)
* [interceptedRequest.headers](#interceptedrequestheaders)
* [interceptedRequest.isHandled()](#interceptedrequestishandled) * [interceptedRequest.isHandled()](#interceptedrequestishandled)
* [interceptedRequest.method](#interceptedrequestmethod)
* [interceptedRequest.postData](#interceptedrequestpostdata)
* [interceptedRequest.url](#interceptedrequesturl)
- [class: Headers](#class-headers) - [class: Headers](#class-headers)
* [headers.append(name, value)](#headersappendname-value) * [headers.append(name, value)](#headersappendname-value)
* [headers.delete(name)](#headersdeletename) * [headers.delete(name)](#headersdeletename)
@ -122,10 +122,6 @@ Closes chromium application with all the pages (if any were opened). The browser
Create a new page in browser and returns a promise which gets resolved with a Page object. Create a new page in browser and returns a promise which gets resolved with a Page object.
#### browser.version()
- returns: <[Promise]<[string]>>
#### browser.stderr #### browser.stderr
- <[stream.Readable]> - <[stream.Readable]>
@ -136,6 +132,9 @@ A Readable Stream that represents the browser process's stderr.
A Readable Stream that represents the browser process's stdout. A Readable Stream that represents the browser process's stdout.
#### browser.version()
- returns: <[Promise]<[string]>>
### class: Page ### class: Page
Page provides interface to interact with a tab in a browser. Pages are created by browser: Page provides interface to interact with a tab in a browser. Pages are created by browser:
@ -339,7 +338,6 @@ Dialog's type, could be one of the `alert`, `beforeunload`, `confirm` and `promp
### class: Request ### class: Request
#### request.response()
#### request.headers #### request.headers
- <[Headers]> - <[Headers]>
@ -350,6 +348,9 @@ Contains the associated [Headers] object of the request.
Contains the request's method (GET, POST, etc.) Contains the request's method (GET, POST, etc.)
#### request.response()
#### request.url #### request.url
- <[string]> - <[string]>
@ -367,6 +368,7 @@ Contains the [Headers] object associated with the response.
Contains a boolean stating whether the response was successful (status in the range 200-299) or not. Contains a boolean stating whether the response was successful (status in the range 200-299) or not.
#### response.request()
#### response.status #### response.status
- <[number]> - <[number]>
@ -378,39 +380,44 @@ Contains the status code of the response (e.g., 200 for a success).
Contains the status message corresponding to the status code (e.g., OK for 200). Contains the status message corresponding to the status code (e.g., OK for 200).
#### response.url #### response.url
- <[string]> - <[string]>
Contains the URL of the response. Contains the URL of the response.
#### response.request()
### class: InterceptedRequest ### class: InterceptedRequest
#### interceptedRequest.abort()
#### interceptedRequest.continue()
#### interceptedRequest.headers #### interceptedRequest.headers
- <[Headers]> - <[Headers]>
Contains the [Headers] object associated with the request. Contains the [Headers] object associated with the request.
#### interceptedRequest.isHandled()
#### interceptedRequest.method #### interceptedRequest.method
- <[string]> - <[string]>
Contains the request's method (GET, POST, etc.) Contains the request's method (GET, POST, etc.)
#### interceptedRequest.url
- <[string]>
Contains the URL of the request.
#### interceptedRequest.postData #### interceptedRequest.postData
- <[string]> - <[string]>
In case of a `POST` request, contains `POST` data. In case of a `POST` request, contains `POST` data.
#### interceptedRequest.abort() #### interceptedRequest.url
#### interceptedRequest.continue() - <[string]>
#### interceptedRequest.isHandled()
Contains the URL of the request.
### class: Headers ### class: Headers
#### headers.append(name, value) #### headers.append(name, value)

View File

@ -74,15 +74,15 @@ class Documentation {
if (classes.has(cls.name)) if (classes.has(cls.name))
errors.push(`Duplicate declaration of class ${cls.name}`); errors.push(`Duplicate declaration of class ${cls.name}`);
classes.add(cls.name); classes.add(cls.name);
let methods = new Set(); let members = new Set();
for (let method of cls.methodsArray) { for (let member of cls.membersArray) {
if (methods.has(method.name)) if (members.has(member.name))
errors.push(`Duplicate declaration of method ${cls.name}.${method.name}()`); errors.push(`Duplicate declaration of method ${cls.name}.${member.name}()`);
methods.add(method.name); members.add(member.name);
let args = new Set(); let args = new Set();
for (let arg of method.argsArray) { for (let arg of member.argsArray) {
if (args.has(arg.name)) if (args.has(arg.name))
errors.push(`Duplicate declaration of argument ${cls.name}.${method.name} "${arg.name}"`); errors.push(`Duplicate declaration of argument ${cls.name}.${member.name} "${arg.name}"`);
args.add(arg.name); args.add(arg.name);
} }
} }
@ -94,32 +94,55 @@ class Documentation {
Documentation.Class = class { Documentation.Class = class {
/** /**
* @param {string} name * @param {string} name
* @param {!Array<!Documentation.Method>} methodsArray * @param {!Array<!Documentation.Member>} membersArray
* @param {!Array<string>} propertiesArray
*/ */
constructor(name, methodsArray, propertiesArray) { constructor(name, membersArray) {
this.name = name; this.name = name;
this.methodsArray = methodsArray; this.membersArray = membersArray;
this.members = new Map();
this.properties = new Map();
this.methods = new Map(); this.methods = new Map();
for (let method of methodsArray) for (let member of membersArray) {
this.methods.set(method.name, method); this.members.set(member.name, member);
this.propertiesArray = propertiesArray; if (member.type === 'method')
this.properties = new Set(propertiesArray); this.methods.set(member.name, member);
else if (member.type === 'property')
this.properties.set(member.name, member);
}
} }
}; };
Documentation.Method = class { Documentation.Member = class {
/** /**
* @param {string} type
* @param {string} name * @param {string} name
* @param {!Array<!Documentation.Argument>} argsArray * @param {!Array<!Documentation.Argument>} argsArray
*/ */
constructor(name, argsArray) { constructor(type, name, argsArray) {
this.type = type;
this.name = name; this.name = name;
this.argsArray = argsArray; this.argsArray = argsArray;
this.args = new Map(); this.args = new Map();
for (let arg of argsArray) for (let arg of argsArray)
this.args.set(arg.name, arg); this.args.set(arg.name, arg);
} }
/**
* @param {string} name
* @param {!Array<!Documentation.Argument>} argsArray
* @return {!Documentation.Member}
*/
static createMethod(name, argsArray) {
return new Documentation.Member('method', name, argsArray);
}
/**
* @param {string} name
* @return {!Documentation.Member}
*/
static createProperty(name) {
return new Documentation.Member('property', name, []);
}
}; };
Documentation.Argument = class { Documentation.Argument = class {

View File

@ -8,8 +8,7 @@ class JSOutline {
constructor(text) { constructor(text) {
this.classes = []; this.classes = [];
this._currentClassName = null; this._currentClassName = null;
this._currentClassMethods = []; this._currentClassMembers = [];
this._currentClassProperties = [];
this._text = text; this._text = text;
let ast = esprima.parseScript(this._text, {loc: true, range: true}); let ast = esprima.parseScript(this._text, {loc: true, range: true});
@ -35,8 +34,8 @@ class JSOutline {
for (let param of node.value.params) for (let param of node.value.params)
args.push(new Documentation.Argument(this._extractText(param))); args.push(new Documentation.Argument(this._extractText(param)));
let methodName = this._extractText(node.key); let methodName = this._extractText(node.key);
let method = new Documentation.Method(methodName, args); let method = Documentation.Member.createMethod(methodName, args);
this._currentClassMethods.push(method); this._currentClassMembers.push(method);
// Extract properties from constructor. // Extract properties from constructor.
if (node.kind === 'constructor') { if (node.kind === 'constructor') {
let walker = new ESTreeWalker(node => { let walker = new ESTreeWalker(node => {
@ -46,7 +45,7 @@ class JSOutline {
if (node.type === 'MemberExpression' && node.object && if (node.type === 'MemberExpression' && node.object &&
node.object.type === 'ThisExpression' && node.property && node.object.type === 'ThisExpression' && node.property &&
node.property.type === 'Identifier') node.property.type === 'Identifier')
this._currentClassProperties.push(node.property.name); this._currentClassMembers.push(Documentation.Member.createProperty(node.property.name));
}); });
walker.walk(node); walker.walk(node);
} }
@ -60,11 +59,10 @@ class JSOutline {
_flushClassIfNeeded() { _flushClassIfNeeded() {
if (this._currentClassName === null) if (this._currentClassName === null)
return; return;
let jsClass = new Documentation.Class(this._currentClassName, this._currentClassMethods, this._currentClassProperties); let jsClass = new Documentation.Class(this._currentClassName, this._currentClassMembers);
this.classes.push(jsClass); this.classes.push(jsClass);
this._currentClassName = null; this._currentClassName = null;
this._currentClassMethods = []; this._currentClassMembers = [];
this._currentClassProperties = [];
} }
_extractText(node) { _extractText(node) {

View File

@ -53,8 +53,7 @@ class MDOutline {
const methodRegex = /^(\w+)\.(\w+)\((.*)\)$/; const methodRegex = /^(\w+)\.(\w+)\((.*)\)$/;
const propertyRegex = /^(\w+)\.(\w+)$/; const propertyRegex = /^(\w+)\.(\w+)$/;
let currentClassName = null; let currentClassName = null;
let currentClassMethods = []; let currentClassMembers = [];
let currentClassProperties = [];
for (const cls of classes) { for (const cls of classes) {
let match = cls.name.match(classHeading); let match = cls.name.match(classHeading);
if (!match) if (!match)
@ -84,8 +83,8 @@ class MDOutline {
if (parameters !== member.args.join(', ')) if (parameters !== member.args.join(', '))
this.errors.push(`Heading arguments for "${member.name}" do not match described ones, i.e. "${parameters}" != "${member.args.join(', ')}"`); this.errors.push(`Heading arguments for "${member.name}" do not match described ones, i.e. "${parameters}" != "${member.args.join(', ')}"`);
let args = member.args.map(arg => new Documentation.Argument(arg)); let args = member.args.map(arg => new Documentation.Argument(arg));
let method = new Documentation.Method(methodName, args); let method = Documentation.Member.createMethod(methodName, args);
currentClassMethods.push(method); currentClassMembers.push(method);
} }
function handleProperty(member, className, propertyName) { function handleProperty(member, className, propertyName) {
@ -93,16 +92,15 @@ class MDOutline {
this.errors.push(`Failed to process header as property: ${member.name}`); this.errors.push(`Failed to process header as property: ${member.name}`);
return; return;
} }
currentClassProperties.push(propertyName); currentClassMembers.push(Documentation.Member.createProperty(propertyName));
} }
function flushClassIfNeeded() { function flushClassIfNeeded() {
if (currentClassName === null) if (currentClassName === null)
return; return;
this.classes.push(new Documentation.Class(currentClassName, currentClassMethods, currentClassProperties)); this.classes.push(new Documentation.Class(currentClassName, currentClassMembers));
currentClassName = null; currentClassName = null;
currentClassMethods = []; currentClassMembers = [];
currentClassProperties = [];
} }
} }
} }

View File

@ -63,17 +63,19 @@ function lintMarkdown(doc) {
const errors = []; const errors = [];
// Methods should be sorted alphabetically. // Methods should be sorted alphabetically.
for (let cls of doc.classesArray) { for (let cls of doc.classesArray) {
for (let i = 0; i < cls.methodsArray.length - 1; ++i) { for (let i = 0; i < cls.membersArray.length - 1; ++i) {
// Constructor always goes first. // Constructor always goes first.
if (cls.methodsArray[i].name === 'constructor') { if (cls.membersArray[i].name === 'constructor') {
if (i > 0) if (i > 0)
errors.push(`Constructor of ${cls.name} should go before other methods`); errors.push(`Constructor of ${cls.name} should go before other methods`);
continue; continue;
} }
let method1 = cls.methodsArray[i]; let member1 = cls.membersArray[i];
let method2 = cls.methodsArray[i + 1]; let member2 = cls.membersArray[i + 1];
if (method1.name > method2.name) if (member1.name > member2.name) {
errors.push(`${cls.name}.${method1.name} breaks alphabetic methods sorting inside class ${cls.name}`); let memberName = `${cls.name}.${member1.name}` + (member1.type === 'method' ? '()' : '');
errors.push(`${memberName} breaks alphabetic member sorting inside class ${cls.name}`);
}
} }
} }
return errors; return errors;
@ -89,13 +91,12 @@ function filterJSDocumentation(jsDocumentation) {
for (let cls of jsDocumentation.classesArray) { for (let cls of jsDocumentation.classesArray) {
if (EXCLUDE_CLASSES.has(cls.name)) if (EXCLUDE_CLASSES.has(cls.name))
continue; continue;
let methods = cls.methodsArray.filter(method => { let members = cls.membersArray.filter(member => {
if (method.name.startsWith('_')) if (member.name.startsWith('_'))
return false; return false;
return !EXCLUDE_METHODS.has(`${cls.name}.${method.name}`); return !EXCLUDE_METHODS.has(`${cls.name}.${member.name}`);
}); });
let properties = cls.propertiesArray.filter(property => !property.startsWith('_')); classes.push(new Documentation.Class(cls.name, members));
classes.push(new Documentation.Class(cls.name, methods, properties));
} }
return new Documentation(classes); return new Documentation(classes);
} }

View File

@ -4,6 +4,8 @@
#### new Foo() #### new Foo()
#### foo.ddd
#### foo.ccc() #### foo.ccc()
#### foo.bbb() #### foo.bbb()

View File

@ -1,5 +1,6 @@
class Foo { class Foo {
constructor() { constructor() {
this.ddd = 10;
} }
aaa() {} aaa() {}

View File

@ -1,2 +1,3 @@
[MarkDown] Constructor of Foo should go before other methods [MarkDown] Constructor of Foo should go before other methods
[MarkDown] Foo.ccc breaks alphabetic methods sorting inside class Foo [MarkDown] Foo.ddd breaks alphabetic member sorting inside class Foo
[MarkDown] Foo.ccc() breaks alphabetic member sorting inside class Foo