diff --git a/utils/doclint/.gitignore b/utils/doclint/.gitignore new file mode 100644 index 00000000..ea1472ec --- /dev/null +++ b/utils/doclint/.gitignore @@ -0,0 +1 @@ +output/ diff --git a/utils/doclint/lint.js b/utils/doclint/lint.js index cb922240..66108994 100644 --- a/utils/doclint/lint.js +++ b/utils/doclint/lint.js @@ -1,4 +1,3 @@ -const {describe, it, fail, runSpecs} = require('./specRunner'); const path = require('path'); const jsBuilder = require('./JSBuilder'); const mdBuilder = require('./MDBuilder'); @@ -29,101 +28,88 @@ let EXCLUDE_METHODS = new Set([ 'Response.constructor', ]); -const browser = new Browser({args: ['--no-sandbox']}); -browser.newPage() - .then(initializeSpecs) - .then(runSpecs) - .catch(console.error) - .then(() => browser.close()); - -async function initializeSpecs(page) { - let jsResult = await jsBuilder(path.join(PROJECT_DIR, 'lib')); - let mdResult = await mdBuilder(page, path.join(PROJECT_DIR, 'docs')); +/** + * @param {!Page} page + * @param {string} docsFolderPath + * @param {string} jsFolderPath + * @return {!Promise>} + */ +async function lint(page, docsFolderPath, jsFolderPath) { + let mdResult = await mdBuilder(page, docsFolderPath); + let jsResult = await jsBuilder(jsFolderPath); let jsDocumentation = filterJSDocumentation(jsResult); let mdDocumentation = mdResult.documentation; let diff = Documentation.diff(mdDocumentation, jsDocumentation); - describe('JavaScript documentation parser', function() { - it('should not contain any duplicate classes (probably error in parsing!)', () => { - let jsClasses = new Map(); - for (let jsClass of jsDocumentation.classesArray) { - if (jsClasses.has(jsClass.name)) - fail(`JavaScript has duplicate declaration of ${jsClass.name}. (This probably means that this linter has an error)`); - jsClasses.set(jsClass.name, jsClass); - } - }); - }); + let jsErrors = []; + let mdErrors = []; - describe('Markdown Documentation', function() { - it('should not have any parse errors', () => { - for (let error of mdResult.errors) - 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', () => { - for (let mdClass of mdDocumentation.classesArray) { - let constructorMethod = mdClass.methods.get('constructor'); - if (!constructorMethod) + // Report all markdown parse errors. + mdErrors.push(...mdResult.errors); + { + // Report duplicate JavaScript classes. + let jsClasses = new Map(); + for (let jsClass of jsDocumentation.classesArray) { + if (jsClasses.has(jsClass.name)) + jsErrors.push(`Duplicate declaration of class ${jsClass.name}`); + jsClasses.set(jsClass.name, jsClass); + } + } + { + // Report duplicate MarkDown classes. + let mdClasses = new Map(); + for (let mdClass of mdDocumentation.classesArray) { + if (mdClasses.has(mdClass.name)) + mdErrors.push(`Duplicate declaration of class ${mdClass.name}`); + mdClasses.set(mdClass.name, mdClass); + } + } + { + // Make sure class constructors are defined before other methods. + for (let mdClass of mdDocumentation.classesArray) { + let constructorMethod = mdClass.methods.get('constructor'); + if (!constructorMethod) + continue; + if (mdClass.methodsArray[0] !== constructorMethod) + mdErrors.push(`Constructor of ${mdClass.name} should go before other methods`); + } + } + { + // Methods should be sorted alphabetically. + for (let mdClass of mdDocumentation.classesArray) { + for (let i = 0; i < mdClass.methodsArray.length - 1; ++i) { + // Constructor should always go first. + if (mdClass.methodsArray[i].name === 'constructor') continue; - if (mdClass.methodsArray[0] !== constructorMethod) - fail(`Method 'new ${mdClass.name}' should go before other methods of class ${mdClass.name}`); + let method1 = mdClass.methodsArray[i]; + let method2 = mdClass.methodsArray[i + 1]; + if (method1.name > method2.name) + mdErrors.push(`${mdClass.name}.${method1.name} breaks alphabetic sorting inside class ${mdClass.name}`); } - }); - it('methods should be sorted alphabetically', () => { - for (let mdClass of mdDocumentation.classesArray) { - for (let i = 0; i < mdClass.methodsArray.length - 1; ++i) { - // Constructor should always go first. - if (mdClass.methodsArray[i].name === 'constructor') - continue; - let method1 = mdClass.methodsArray[i]; - let method2 = mdClass.methodsArray[i + 1]; - if (method1.name > method2.name) - fail(`${mdClass.name}.${method1.name} breaks alphabetic sorting inside class ${mdClass.name}`); - } - } - }); - it('should not contain any non-existing class', () => { - for (let className of diff.extraClasses) - fail(`Documentation describes non-existing class ${className}`); - }); - it('should describe all existing classes', () => { - for (let className of diff.missingClasses) - fail(`Documentation lacks description of class ${className}`); - }); - it('should not contain any non-existing methods', () => { - for (let methodName of diff.extraMethods) - fail(`Documentation describes non-existing method: ${methodName}`); - }); - it('should describe all existing methods', () => { - for (let methodName of diff.missingMethods) - fail(`Documentation lacks method ${methodName}`); - }); - it('should describe all arguments propertly', () => { - for (let badArgument of diff.badArguments) { - let text = [`Method ${badArgument.method} fails to describe its parameters:`]; - 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')); - } - }); - it('should not contain any non-existing properties', () => { - for (let propertyName of diff.extraProperties) - fail(`Documentation describes non-existing property: ${propertyName}`); - }); - it('should describe all existing properties', () => { - for (let propertyName of diff.missingProperties) - fail(`Documentation lacks property ${propertyName}`); - }); - }); + } + } + // Report non-existing and missing classes. + mdErrors.push(...diff.extraClasses.map(className => `Non-existing class found: ${className}`)); + mdErrors.push(...diff.missingClasses.map(className => `Class not found: ${className}`)); + mdErrors.push(...diff.extraMethods.map(methodName => `Non-existing method found: ${methodName}`)); + mdErrors.push(...diff.missingMethods.map(methodName => `Method not found: ${methodName}`)); + mdErrors.push(...diff.extraProperties.map(propertyName => `Non-existing property found: ${propertyName}`)); + mdErrors.push(...diff.missingProperties.map(propertyName => `Property not found: ${propertyName}`)); + { + // Report badly described arguments. + for (let badArgument of diff.badArguments) { + let text = [`Method ${badArgument.method} fails to describe its parameters:`]; + 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}"`); + mdErrors.push(text.join('\n')); + } + } + // Push all errors with proper prefixes + let errors = jsErrors.map(error => '[JavaScript] ' + error); + errors.push(...mdErrors.map(error => '[MarkDown] ' + error)); + return errors; } /** @@ -146,3 +132,30 @@ function filterJSDocumentation(jsDocumentation) { } return new Documentation(classes); } + +module.exports = lint; + +const RED_COLOR = '\x1b[31m'; +const RESET_COLOR = '\x1b[0m'; + +// Handle CLI invocation. +if (!module.parent) { + const startTime = Date.now(); + const browser = new Browser({args: ['--no-sandbox']}); + browser.newPage().then(async page => { + const errors = await lint(page, path.join(PROJECT_DIR, 'docs'), path.join(PROJECT_DIR, 'lib')); + await browser.close(); + if (errors.length) { + console.log('Documentation Failures:'); + for (let i = 0; i < errors.length; ++i) { + let error = errors[i]; + error = error.split('\n').join('\n '); + console.log(`${i + 1}) ${RED_COLOR}${error}${RESET_COLOR}`); + } + } + console.log(`${errors.length} failures`); + const runningTime = Date.now() - startTime; + console.log(`Finished in ${runningTime / 1000} seconds`); + process.exit(errors.length > 0 ? 1 : 0); + }); +} diff --git a/utils/doclint/specRunner.js b/utils/doclint/specRunner.js deleted file mode 100644 index 39fec0b3..00000000 --- a/utils/doclint/specRunner.js +++ /dev/null @@ -1,85 +0,0 @@ -const startTime = Date.now(); -let allTests = []; -let titles = []; -let currentTest = null; - -const colors = { - reset: '\x1b[0m', - red: '\x1b[31m', - green: '\x1b[32m', -}; - -/** - * @param {string} title - * @param {function()} fun - */ -function describe(title, fun) { - titles.push(title); - fun(); - titles.pop(); -} - -/** - * @param {string} title - * @param {function()} fun - */ -function it(title, fun) { - titles.push(title); - allTests.push({ - errors: [], - title: titles.join(' '), - fun, - }); - titles.pop(); -} - -/** - * @param {string} msg - */ -function fail(msg) { - currentTest.errors.push(msg); -} - -function runSpecs() { - console.log('Started\n'); - for (currentTest of allTests) { - currentTest.fun(); - if (currentTest.errors.length) - process.stdout.write(`${colors.red}F${colors.reset}`); - else - process.stdout.write(`${colors.green}.${colors.reset}`); - } - console.log('\n'); - reportErrors(); -} - -function reportErrors() { - let failedTests = allTests.filter(test => !!test.errors.length); - if (failedTests.length) { - console.log('Failures:'); - for (let i = 0; i < failedTests.length; ++i) { - let test = failedTests[i]; - console.log(`${i + 1}) ${test.title}`); - console.log(` Messages:`); - for (let error of test.errors) { - error = error.split('\n').join('\n '); - console.log(' * ' + colors.red + error + colors.reset); - } - } - console.log(''); - } - - console.log(`Ran ${allTests.length} specs`); - console.log(`${allTests.length} specs, ${failedTests.length} failures`); - const runningTime = Date.now() - startTime; - console.log(`Finished in ${runningTime / 1000} seconds`); - process.exit(failedTests.length > 0 ? 1 : 0); -} - -module.exports = { - describe, - it, - fail, - runSpecs, -}; - diff --git a/utils/doclint/test/01-class-errors/01-class-errors.txt b/utils/doclint/test/01-class-errors/01-class-errors.txt new file mode 100644 index 00000000..2852a437 --- /dev/null +++ b/utils/doclint/test/01-class-errors/01-class-errors.txt @@ -0,0 +1,3 @@ +[MarkDown] Non-existing class found: Bar +[MarkDown] Non-existing class found: Baz +[MarkDown] Class not found: Other \ No newline at end of file diff --git a/utils/doclint/test/02-extra-class/doc.md b/utils/doclint/test/01-class-errors/doc.md similarity index 65% rename from utils/doclint/test/02-extra-class/doc.md rename to utils/doclint/test/01-class-errors/doc.md index 17d6a88e..7e51ca47 100644 --- a/utils/doclint/test/02-extra-class/doc.md +++ b/utils/doclint/test/01-class-errors/doc.md @@ -1,3 +1,5 @@ ### class: Foo ### class: Bar + +### class: Baz diff --git a/utils/doclint/test/01-missing-class/foo.js b/utils/doclint/test/01-class-errors/foo.js similarity index 100% rename from utils/doclint/test/01-missing-class/foo.js rename to utils/doclint/test/01-class-errors/foo.js diff --git a/utils/doclint/test/01-class-errors/other.js b/utils/doclint/test/01-class-errors/other.js new file mode 100644 index 00000000..1b94418e --- /dev/null +++ b/utils/doclint/test/01-class-errors/other.js @@ -0,0 +1,2 @@ +class Other { +} diff --git a/utils/doclint/test/01-missing-class/doc.md b/utils/doclint/test/01-missing-class/doc.md deleted file mode 100644 index e69de29b..00000000 diff --git a/utils/doclint/test/02-method-errors/doc.md b/utils/doclint/test/02-method-errors/doc.md new file mode 100644 index 00000000..3b479423 --- /dev/null +++ b/utils/doclint/test/02-method-errors/doc.md @@ -0,0 +1,6 @@ +### class: Foo + +#### foo.proceed() + +#### foo.start() + diff --git a/utils/doclint/test/02-method-errors/foo.js b/utils/doclint/test/02-method-errors/foo.js new file mode 100644 index 00000000..fe8b2039 --- /dev/null +++ b/utils/doclint/test/02-method-errors/foo.js @@ -0,0 +1,7 @@ +class Foo { + start() { + } + + stop() { + } +} diff --git a/utils/doclint/test/03-missing-method/doc.md b/utils/doclint/test/03-missing-method/doc.md deleted file mode 100644 index 58af6ebb..00000000 --- a/utils/doclint/test/03-missing-method/doc.md +++ /dev/null @@ -1,2 +0,0 @@ -### class: Foo - diff --git a/utils/doclint/test/03-missing-method/foo.js b/utils/doclint/test/03-missing-method/foo.js deleted file mode 100644 index 638a741f..00000000 --- a/utils/doclint/test/03-missing-method/foo.js +++ /dev/null @@ -1,4 +0,0 @@ -class Foo { - bar() { - } -} diff --git a/utils/doclint/test/03-property-errors/doc.md b/utils/doclint/test/03-property-errors/doc.md new file mode 100644 index 00000000..75d4fd10 --- /dev/null +++ b/utils/doclint/test/03-property-errors/doc.md @@ -0,0 +1,5 @@ +### class: Foo + +#### foo.a + +#### foo.c diff --git a/utils/doclint/test/03-property-errors/foo.js b/utils/doclint/test/03-property-errors/foo.js new file mode 100644 index 00000000..a978379f --- /dev/null +++ b/utils/doclint/test/03-property-errors/foo.js @@ -0,0 +1,7 @@ +class Foo { + constructor() { + this.a = 42; + this.b = 'hello'; + this.emit('done'); + } +} diff --git a/utils/doclint/test/04-bad-arguments/doc.md b/utils/doclint/test/04-bad-arguments/doc.md new file mode 100644 index 00000000..2e4e7c64 --- /dev/null +++ b/utils/doclint/test/04-bad-arguments/doc.md @@ -0,0 +1,7 @@ +### class: Foo +#### new Foo(arg1, arg2) +- `arg1` <[string]> +- `arg2` <[string]> + +#### foo.test(fileNames) +- `filePaths` <[Array]> diff --git a/utils/doclint/test/04-bad-arguments/foo.js b/utils/doclint/test/04-bad-arguments/foo.js new file mode 100644 index 00000000..6538ac8e --- /dev/null +++ b/utils/doclint/test/04-bad-arguments/foo.js @@ -0,0 +1,7 @@ +class Foo { + constructor(arg1, arg3) { + } + + test(filePaths) { + } +} diff --git a/utils/doclint/test/04-extra-method/doc.md b/utils/doclint/test/04-extra-method/doc.md deleted file mode 100644 index 9538f966..00000000 --- a/utils/doclint/test/04-extra-method/doc.md +++ /dev/null @@ -1,4 +0,0 @@ -### class: Foo - -#### foo.bar() - diff --git a/utils/doclint/test/04-extra-method/foo.js b/utils/doclint/test/04-extra-method/foo.js deleted file mode 100644 index f230fa0f..00000000 --- a/utils/doclint/test/04-extra-method/foo.js +++ /dev/null @@ -1,2 +0,0 @@ -class Foo { -} diff --git a/utils/doclint/test/05-missing-property/doc.md b/utils/doclint/test/05-missing-property/doc.md deleted file mode 100644 index e47eb5ec..00000000 --- a/utils/doclint/test/05-missing-property/doc.md +++ /dev/null @@ -1,3 +0,0 @@ -### class: Foo - -#### new Foo() diff --git a/utils/doclint/test/05-missing-property/foo.js b/utils/doclint/test/05-missing-property/foo.js deleted file mode 100644 index b87cc3af..00000000 --- a/utils/doclint/test/05-missing-property/foo.js +++ /dev/null @@ -1,5 +0,0 @@ -class Foo { - constructor() { - this.barProperty = 42; - } -} diff --git a/utils/doclint/test/08-outdated-table-of-contents/doc.md b/utils/doclint/test/05-outdated-toc/doc.md similarity index 100% rename from utils/doclint/test/08-outdated-table-of-contents/doc.md rename to utils/doclint/test/05-outdated-toc/doc.md diff --git a/utils/doclint/test/02-extra-class/foo.js b/utils/doclint/test/05-outdated-toc/foo.js similarity index 100% rename from utils/doclint/test/02-extra-class/foo.js rename to utils/doclint/test/05-outdated-toc/foo.js diff --git a/utils/doclint/test/06-extra-property/doc.md b/utils/doclint/test/06-extra-property/doc.md deleted file mode 100644 index e211bb97..00000000 --- a/utils/doclint/test/06-extra-property/doc.md +++ /dev/null @@ -1,3 +0,0 @@ -### class: Foo - -#### foo.bazProperty diff --git a/utils/doclint/test/06-extra-property/foo.js b/utils/doclint/test/06-extra-property/foo.js deleted file mode 100644 index 89fad81c..00000000 --- a/utils/doclint/test/06-extra-property/foo.js +++ /dev/null @@ -1,4 +0,0 @@ -class Foo { - constructor() { - } -} diff --git a/utils/doclint/test/07-bad-arguments/doc.md b/utils/doclint/test/07-bad-arguments/doc.md deleted file mode 100644 index 67798f19..00000000 --- a/utils/doclint/test/07-bad-arguments/doc.md +++ /dev/null @@ -1,4 +0,0 @@ -### class: Foo - -#### new Foo(arg2) -- `arg2` <[string]> Some arg. diff --git a/utils/doclint/test/07-bad-arguments/foo.js b/utils/doclint/test/07-bad-arguments/foo.js deleted file mode 100644 index dcfa5e9d..00000000 --- a/utils/doclint/test/07-bad-arguments/foo.js +++ /dev/null @@ -1,4 +0,0 @@ -class Foo { - constructor(arg1) { - } -} diff --git a/utils/doclint/test/08-outdated-table-of-contents/foo.js b/utils/doclint/test/08-outdated-table-of-contents/foo.js deleted file mode 100644 index f230fa0f..00000000 --- a/utils/doclint/test/08-outdated-table-of-contents/foo.js +++ /dev/null @@ -1,2 +0,0 @@ -class Foo { -} diff --git a/utils/doclint/test/golden/01-class-errors.txt b/utils/doclint/test/golden/01-class-errors.txt new file mode 100644 index 00000000..2852a437 --- /dev/null +++ b/utils/doclint/test/golden/01-class-errors.txt @@ -0,0 +1,3 @@ +[MarkDown] Non-existing class found: Bar +[MarkDown] Non-existing class found: Baz +[MarkDown] Class not found: Other \ No newline at end of file diff --git a/utils/doclint/test/golden/02-method-errors.txt b/utils/doclint/test/golden/02-method-errors.txt new file mode 100644 index 00000000..0c13eb51 --- /dev/null +++ b/utils/doclint/test/golden/02-method-errors.txt @@ -0,0 +1,2 @@ +[MarkDown] Non-existing method found: Foo.proceed +[MarkDown] Method not found: Foo.stop \ No newline at end of file diff --git a/utils/doclint/test/golden/03-property-errors.txt b/utils/doclint/test/golden/03-property-errors.txt new file mode 100644 index 00000000..aa50ead7 --- /dev/null +++ b/utils/doclint/test/golden/03-property-errors.txt @@ -0,0 +1,3 @@ +[MarkDown] Method not found: Foo.constructor +[MarkDown] Non-existing property found: Foo.c +[MarkDown] Property not found: Foo.b \ No newline at end of file diff --git a/utils/doclint/test/golden/04-bad-arguments.txt b/utils/doclint/test/golden/04-bad-arguments.txt new file mode 100644 index 00000000..813256c1 --- /dev/null +++ b/utils/doclint/test/golden/04-bad-arguments.txt @@ -0,0 +1,4 @@ +[MarkDown] Heading arguments for "foo.test(fileNames)" do not match described ones, i.e. "fileNames" != "filePaths" +[MarkDown] Method Foo.constructor fails to describe its parameters: +- Missing description for "arg3" +- Described non-existing parameter "arg2" \ No newline at end of file diff --git a/utils/doclint/test/golden/05-outdated-toc.txt b/utils/doclint/test/golden/05-outdated-toc.txt new file mode 100644 index 00000000..7c756aa3 --- /dev/null +++ b/utils/doclint/test/golden/05-outdated-toc.txt @@ -0,0 +1 @@ +[MarkDown] Markdown TOC is outdated, run `yarn generate-toc` \ No newline at end of file diff --git a/utils/doclint/test/test.js b/utils/doclint/test/test.js index f6cd8bc8..035bd3c1 100644 --- a/utils/doclint/test/test.js +++ b/utils/doclint/test/test.js @@ -1,69 +1,47 @@ +const fs = require('fs'); +const rm = require('rimraf').sync; const path = require('path'); -const jsBuilder = require('../JSBuilder'); -const mdBuilder = require('../MDBuilder'); -const Documentation = require('../Documentation'); const Browser = require('../../../lib/Browser'); +const doclint = require('../lint'); +const GoldenUtils = require('../../../test/golden-utils'); + +const OUTPUT_DIR = path.join(__dirname, 'output'); +const GOLDEN_DIR = path.join(__dirname, 'golden'); const browser = new Browser({args: ['--no-sandbox']}); let page; +let specName; + +jasmine.getEnv().addReporter({ + specStarted: result => specName = result.description +}); beforeAll(SX(async function() { page = await browser.newPage(); + if (fs.existsSync(OUTPUT_DIR)) + rm(OUTPUT_DIR); })); afterAll(SX(async function() { await browser.close(); })); -describe('doclint', function() { - test('01-missing-class', diff => { - expect(diff.missingClasses.length).toBe(1); - expect(diff.missingClasses[0]).toBe('Foo'); - }); - test('02-extra-class', diff => { - expect(diff.extraClasses.length).toBe(1); - expect(diff.extraClasses[0]).toBe('Bar'); - }); - test('03-missing-method', diff => { - expect(diff.missingMethods.length).toBe(1); - expect(diff.missingMethods[0]).toBe('Foo.bar'); - }); - test('04-extra-method', diff => { - expect(diff.extraMethods.length).toBe(1); - expect(diff.extraMethods[0]).toBe('Foo.bar'); - }); - test('05-missing-property', diff => { - expect(diff.missingProperties.length).toBe(1); - expect(diff.missingProperties[0]).toBe('Foo.barProperty'); - }); - test('06-extra-property', diff => { - expect(diff.extraProperties.length).toBe(1); - expect(diff.extraProperties[0]).toBe('Foo.bazProperty'); - }); - test('07-bad-arguments', diff => { - expect(diff.badArguments.length).toBe(1); - expect(diff.badArguments[0]).toEqual({ - method: 'Foo.constructor', - missingArgs: ['arg1'], - extraArgs: ['arg2'] - }); - }); - test('08-outdated-table-of-contents', (diff, mdErrors) => { - expect(mdErrors.length).toBe(1); - expect(mdErrors[0]).toBe('Markdown TOC is outdated, run `yarn generate-toc`'); - }); +beforeEach(function() { + GoldenUtils.addMatchers(jasmine, GOLDEN_DIR, OUTPUT_DIR); }); -async function test(folderName, func) { - it(folderName, SX(async () => { - const [jsResult, mdResult] = await Promise.all([ - jsBuilder(path.join(__dirname, folderName)), - mdBuilder(page, path.join(__dirname, folderName)) - ]); - const jsDocumentation = jsResult; - const mdDocumentation = mdResult.documentation; - func(Documentation.diff(mdDocumentation, jsDocumentation), mdResult.errors); - })); +describe('doclint', function() { + it('01-class-errors', SX(test)); + it('02-method-errors', SX(test)); + it('03-property-errors', SX(test)); + it('04-bad-arguments', SX(test)); + it('05-outdated-toc', SX(test)); +}); + +async function test() { + const filePath = path.join(__dirname, specName); + const errors = await doclint(page, filePath, filePath); + expect(errors.join('\n')).toBeGolden(specName + '.txt'); } // Since Jasmine doesn't like async functions, they should be wrapped