diff --git a/docs/USAGE.md b/docs/USAGE.md index ac8e5a6f9..035902e5a 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -19,37 +19,41 @@ Commands: readme inject documentation into your README.md Options: - --help Show help [boolean] - --version Show version number [boolean] - --shallow shallow mode turns off dependency resolution, only - processing the specified files (or the main script - specified in package.json) [boolean] [default: false] - --config, -c configuration file. an array defining explicit sort order - --external a string / glob match pattern that defines which external - modules will be whitelisted and included in the generated - documentation. [default: null] - --extension, -e only input source files matching this extension will be - parsed, this option can be used multiple times. - --polyglot polyglot mode turns off dependency resolution and enables - multi-language support. use this to document c++ [boolean] - --private, -p generate documentation tagged as private + --help Show help [boolean] + --version Show version number [boolean] + --shallow shallow mode turns off dependency resolution, only + processing the specified files (or the main script + specified in package.json) [boolean] [default: false] + --config, -c configuration file. an array defining explicit sort order + --external a string / glob match pattern that defines which external + modules will be whitelisted and included in the generated + documentation. [default: null] + --extension, -e only input source files matching this extension will be + parsed, this option can be used multiple times. + --polyglot polyglot mode turns off dependency resolution and enables + multi-language support. use this to document c++[boolean] + --private, -p generate documentation tagged as private [boolean] [default: false] - --access, -a Include only comments with a given access level, out of - private, protected, public, undefined. By default, public, - protected, and undefined access levels are included + --access, -a Include only comments with a given access level, out of + private, protected, public, undefined. By default, + public, protected, and undefined access levels are + included [choices: "public", "private", "protected", "undefined"] - --github, -g infer links to github in documentation [boolean] - --infer-private Infer private access based on the name. This is a regular - expression that is used to match the name [string] - --theme, -t specify a theme: this must be a valid theme module - --name project name. by default, inferred from package.json - --watch, -w watch input files and rebuild documentation when they - change [boolean] - --project-version project version. by default, inferred from package.json - --output, -o output location. omit for stdout, otherwise is a filename - for single-file outputs and a directory name for multi-file - outputs like html [default: "stdout"] - --format, -f [choices: "json", "md", "html"] [default: "json"] + --github, -g infer links to github in documentation [boolean] + --infer-private Infer private access based on the name. This is a regular + expression that is used to match the name [string] + --document-exported Generate documentation for all exported bindings and + members even if there is no JSDoc for them + [boolean] [default: false] + --theme, -t specify a theme: this must be a valid theme module + --name project name. by default, inferred from package.json + --watch, -w watch input files and rebuild documentation when they + change [boolean] + --project-version project version. by default, inferred from package.json + --output, -o output location. omit for stdout, otherwise is a filename + for single-file outputs and a directory name for + multi-file outputs like html [default: "stdout"] + --format, -f [choices: "json", "md", "remark", "html"] [default: "json"] Examples: documentation build foo.js -f md > parse documentation in a file and diff --git a/index.js b/index.js index 35e85de64..ff60399b5 100644 --- a/index.js +++ b/index.js @@ -194,7 +194,7 @@ function buildSync(indexes, options) { return []; } - return parseFn(indexObject).map(buildPipeline); + return parseFn(indexObject, options).map(buildPipeline); }) .filter(Boolean), options))); } @@ -249,7 +249,7 @@ function lint(indexes, options, callback) { inputs .filter(filterJS(options.extension, options.polyglot)) .reduce(function (memo, file) { - return memo.concat(parseFn(file).map(lintPipeline)); + return memo.concat(parseFn(file, options).map(lintPipeline)); }, []) .filter(Boolean)))); }); diff --git a/lib/commands/shared_options.js b/lib/commands/shared_options.js index 870ff5b51..585066877 100644 --- a/lib/commands/shared_options.js +++ b/lib/commands/shared_options.js @@ -53,6 +53,12 @@ function sharedInputOptions(parser) { type: 'string', describe: 'Infer private access based on the name. This is a regular expression that ' + 'is used to match the name' + }) + .option('document-exported', { + type: 'boolean', + describe: 'Generate documentation for all exported bindings and members ' + + 'even if there is no JSDoc for them', + default: false }); } diff --git a/lib/parsers/javascript.js b/lib/parsers/javascript.js index 2e8daa56f..e86388370 100644 --- a/lib/parsers/javascript.js +++ b/lib/parsers/javascript.js @@ -27,9 +27,11 @@ function leftPad(str, width) { * reads the file, parses the JavaScript, and parses the JSDoc. * * @param {Object} data a chunk of data provided by module-deps + * @param {Object} options options * @return {Array} an array of parsed comments */ -function parseJavaScript(data) { +function parseJavaScript(data, options) { + options = options || {}; var results = []; var ast = babylon.parse(data.source, { allowImportExportEverywhere: true, @@ -80,31 +82,7 @@ function parseJavaScript(data) { * @return {undefined} this emits data */ function parseComment(comment) { - var context = { - loc: extend({}, JSON.parse(JSON.stringify(path.node.loc))), - file: data.file, - sortKey: data.sortKey + ' ' + leftPad(path.node.loc.start.line, 8) - }; - // Avoid visiting the same comment twice as a leading - // and trailing node - var key = JSON.stringify(comment.loc); - if (!visited[key]) { - visited[key] = true; - if (includeContext) { - // This is non-enumerable so that it doesn't get stringified in - // output; e.g. by the documentation binary. - Object.defineProperty(context, 'ast', { - enumerable: false, - value: path - }); - - if (path.parentPath && path.parentPath.node) { - context.code = data.source.substring - .apply(data.source, path.parentPath.node.range); - } - } - results.push(parse(comment.value, comment.loc, context)); - } + addComment(comment.value, comment.loc, path, path.node.loc, includeContext); } (path.node[type] || []) @@ -116,10 +94,86 @@ function parseJavaScript(data) { traverse.clearCache(); } + function addComment(commentValue, commentLoc, path, nodeLoc, includeContext) { + var context = { + loc: extend({}, JSON.parse(JSON.stringify(nodeLoc))), + file: data.file, + sortKey: data.sortKey + ' ' + leftPad(nodeLoc.start.line, 8) + }; + // Avoid visiting the same comment twice as a leading + // and trailing node + var key = JSON.stringify(commentLoc); + if (!visited[key]) { + visited[key] = true; + if (includeContext) { + // This is non-enumerable so that it doesn't get stringified in + // output; e.g. by the documentation binary. + Object.defineProperty(context, 'ast', { + enumerable: false, + value: path + }); + + if (path.parentPath && path.parentPath.node) { + context.code = data.source.substring + .apply(data.source, path.parentPath.node.range); + } + } + results.push(parse(commentValue, commentLoc, context)); + } + } + + function addBlankComment(path, node) { + addComment('', node.loc, path, node.loc, true); + } + + function walkExported(ast) { + traverse(ast, { + enter: function (path) { + if (path.isExportDeclaration()) { + if (!hasJSDocComment(path)) { + if (!path.node.declaration) { + return; + } + const node = path.node.declaration; + addBlankComment(path, node); + } + } else if ((path.isClassProperty() || path.isClassMethod()) && + !hasJSDocComment(path) && inExportedClass(path)) { + addBlankComment(path, path.node); + } else if ((path.isObjectProperty() || path.isObjectMethod()) && + !hasJSDocComment(path) && inExportedObject(path)) { + addBlankComment(path, path.node); + } + } + }); + } + + function hasJSDocComment(path) { + return path.node.leadingComments && path.node.leadingComments.some(isJSDocComment); + } + + function inExportedClass(path) { + var c = path.parentPath.parentPath; + return c.isClass() && c.parentPath.isExportDeclaration(); + } + + function inExportedObject(path) { + // ObjectExpression -> VariableDeclarator -> VariableDeclaration -> ExportNamedDeclaration + var p = path.parentPath.parentPath; + if (!p.isVariableDeclarator()) { + return false; + } + return p.parentPath.parentPath.isExportDeclaration(); + } + walkComments(ast, 'leadingComments', true); walkComments(ast, 'innerComments', false); walkComments(ast, 'trailingComments', false); + if (options.documentExported) { + walkExported(ast); + } + return results; } diff --git a/test/bin.js b/test/bin.js index 3c7627a62..a6934a8c0 100644 --- a/test/bin.js +++ b/test/bin.js @@ -341,3 +341,19 @@ test('fatal error', options, function (t) { t.end(); }, false); }); + +test('build --document-exported', function (t) { + + documentation(['build fixture/document-exported.input.js --document-exported -f md'], {}, function (err, data) { + t.error(err); + + var outputfile = path.join(__dirname, 'fixture', 'document-exported.output.md'); + if (process.env.UPDATE) { + fs.writeFileSync(outputfile, data, 'utf8'); + } + + var expect = fs.readFileSync(outputfile, 'utf-8'); + t.equal(data, expect); + t.end(); + }, false); +}, options); diff --git a/test/fixture/document-exported.input.js b/test/fixture/document-exported.input.js new file mode 100644 index 000000000..67cf1db5a --- /dev/null +++ b/test/fixture/document-exported.input.js @@ -0,0 +1,35 @@ +// Options: {"documentExported": true} + +export class Class { + classMethod() {} + get classGetter() {} + set classSetter(v) {} + static staticMethod() {} + static get staticGetter() {} + static set staticSetter(v) {} +} + +export var object = { + method() {}, + get getter() {}, + set setter(v) {}, + prop: 42, + func: function() {}, +}; + +class NotExportedClass { + classMethod() {} + get classGetter() {} + set classSetter(v) {} + static staticMethod() {} + static get staticGetter() {} + static set staticSetter(v) {} +} + +var notExportedObject = { + method() {}, + get getter() {}, + set setter(v) {}, + prop: 42, + func: function() {}, +}; diff --git a/test/fixture/document-exported.output.json b/test/fixture/document-exported.output.json new file mode 100644 index 000000000..dafd32387 --- /dev/null +++ b/test/fixture/document-exported.output.json @@ -0,0 +1,589 @@ +[ + { + "description": "", + "tags": [], + "loc": { + "start": { + "line": 3, + "column": 7 + }, + "end": { + "line": 10, + "column": 1 + } + }, + "context": { + "loc": { + "start": { + "line": 3, + "column": 7 + }, + "end": { + "line": 10, + "column": 1 + } + } + }, + "errors": [], + "name": "Class", + "members": { + "instance": [ + { + "description": "", + "tags": [], + "loc": { + "start": { + "line": 4, + "column": 2 + }, + "end": { + "line": 4, + "column": 18 + } + }, + "context": { + "loc": { + "start": { + "line": 4, + "column": 2 + }, + "end": { + "line": 4, + "column": 18 + } + } + }, + "errors": [], + "name": "classMethod", + "kind": "function", + "memberof": "Class", + "scope": "instance", + "members": { + "instance": [], + "static": [] + }, + "path": [ + { + "name": "Class" + }, + { + "name": "classMethod", + "kind": "function", + "scope": "instance" + } + ], + "namespace": "Class#classMethod" + }, + { + "description": "", + "tags": [], + "loc": { + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 22 + } + }, + "context": { + "loc": { + "start": { + "line": 5, + "column": 2 + }, + "end": { + "line": 5, + "column": 22 + } + } + }, + "errors": [], + "name": "classGetter", + "kind": "member", + "memberof": "Class", + "scope": "instance", + "members": { + "instance": [], + "static": [] + }, + "path": [ + { + "name": "Class" + }, + { + "name": "classGetter", + "kind": "member", + "scope": "instance" + } + ], + "namespace": "Class#classGetter" + }, + { + "description": "", + "tags": [], + "loc": { + "start": { + "line": 6, + "column": 2 + }, + "end": { + "line": 6, + "column": 23 + } + }, + "context": { + "loc": { + "start": { + "line": 6, + "column": 2 + }, + "end": { + "line": 6, + "column": 23 + } + } + }, + "errors": [], + "name": "classSetter", + "kind": "member", + "params": [ + { + "title": "param", + "name": "v", + "lineNumber": 6 + } + ], + "memberof": "Class", + "scope": "instance", + "members": { + "instance": [], + "static": [] + }, + "path": [ + { + "name": "Class" + }, + { + "name": "classSetter", + "kind": "member", + "scope": "instance" + } + ], + "namespace": "Class#classSetter" + } + ], + "static": [ + { + "description": "", + "tags": [], + "loc": { + "start": { + "line": 7, + "column": 2 + }, + "end": { + "line": 7, + "column": 26 + } + }, + "context": { + "loc": { + "start": { + "line": 7, + "column": 2 + }, + "end": { + "line": 7, + "column": 26 + } + } + }, + "errors": [], + "name": "staticMethod", + "kind": "function", + "memberof": "Class", + "scope": "static", + "members": { + "instance": [], + "static": [] + }, + "path": [ + { + "name": "Class" + }, + { + "name": "staticMethod", + "kind": "function", + "scope": "static" + } + ], + "namespace": "Class.staticMethod" + }, + { + "description": "", + "tags": [], + "loc": { + "start": { + "line": 8, + "column": 2 + }, + "end": { + "line": 8, + "column": 30 + } + }, + "context": { + "loc": { + "start": { + "line": 8, + "column": 2 + }, + "end": { + "line": 8, + "column": 30 + } + } + }, + "errors": [], + "name": "staticGetter", + "kind": "member", + "memberof": "Class", + "scope": "static", + "members": { + "instance": [], + "static": [] + }, + "path": [ + { + "name": "Class" + }, + { + "name": "staticGetter", + "kind": "member", + "scope": "static" + } + ], + "namespace": "Class.staticGetter" + }, + { + "description": "", + "tags": [], + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 31 + } + }, + "context": { + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 31 + } + } + }, + "errors": [], + "name": "staticSetter", + "kind": "member", + "params": [ + { + "title": "param", + "name": "v", + "lineNumber": 9 + } + ], + "memberof": "Class", + "scope": "static", + "members": { + "instance": [], + "static": [] + }, + "path": [ + { + "name": "Class" + }, + { + "name": "staticSetter", + "kind": "member", + "scope": "static" + } + ], + "namespace": "Class.staticSetter" + } + ], + "events": [] + }, + "path": [ + { + "name": "Class" + } + ], + "namespace": "Class" + }, + { + "description": "", + "tags": [], + "loc": { + "start": { + "line": 12, + "column": 7 + }, + "end": { + "line": 18, + "column": 2 + } + }, + "context": { + "loc": { + "start": { + "line": 12, + "column": 7 + }, + "end": { + "line": 18, + "column": 2 + } + } + }, + "errors": [], + "name": "object", + "members": { + "instance": [], + "static": [ + { + "description": "", + "tags": [], + "loc": { + "start": { + "line": 16, + "column": 2 + }, + "end": { + "line": 16, + "column": 10 + } + }, + "context": { + "loc": { + "start": { + "line": 16, + "column": 2 + }, + "end": { + "line": 16, + "column": 10 + } + } + }, + "errors": [], + "name": "prop", + "memberof": "object", + "scope": "static", + "members": { + "instance": [], + "static": [] + }, + "path": [ + { + "name": "object" + }, + { + "name": "prop", + "scope": "static" + } + ], + "namespace": "object.prop" + }, + { + "description": "", + "tags": [], + "loc": { + "start": { + "line": 17, + "column": 2 + }, + "end": { + "line": 17, + "column": 21 + } + }, + "context": { + "loc": { + "start": { + "line": 17, + "column": 2 + }, + "end": { + "line": 17, + "column": 21 + } + } + }, + "errors": [], + "name": "func", + "kind": "function", + "memberof": "object", + "scope": "static", + "members": { + "instance": [], + "static": [] + }, + "path": [ + { + "name": "object" + }, + { + "name": "func", + "kind": "function", + "scope": "static" + } + ], + "namespace": "object.func" + } + ] + }, + "path": [ + { + "name": "object" + } + ], + "namespace": "object" + }, + { + "description": "", + "tags": [], + "loc": { + "start": { + "line": 13, + "column": 2 + }, + "end": { + "line": 13, + "column": 13 + } + }, + "context": { + "loc": { + "start": { + "line": 13, + "column": 2 + }, + "end": { + "line": 13, + "column": 13 + } + } + }, + "errors": [], + "name": "method", + "kind": "function", + "members": { + "instance": [], + "static": [] + }, + "path": [ + { + "name": "method", + "kind": "function" + } + ], + "namespace": "method" + }, + { + "description": "", + "tags": [], + "loc": { + "start": { + "line": 14, + "column": 2 + }, + "end": { + "line": 14, + "column": 17 + } + }, + "context": { + "loc": { + "start": { + "line": 14, + "column": 2 + }, + "end": { + "line": 14, + "column": 17 + } + } + }, + "errors": [], + "name": "getter", + "kind": "member", + "members": { + "instance": [], + "static": [] + }, + "path": [ + { + "name": "getter", + "kind": "member" + } + ], + "namespace": "getter" + }, + { + "description": "", + "tags": [], + "loc": { + "start": { + "line": 15, + "column": 2 + }, + "end": { + "line": 15, + "column": 18 + } + }, + "context": { + "loc": { + "start": { + "line": 15, + "column": 2 + }, + "end": { + "line": 15, + "column": 18 + } + } + }, + "errors": [], + "name": "setter", + "kind": "member", + "params": [ + { + "title": "param", + "name": "v", + "lineNumber": 15 + } + ], + "members": { + "instance": [], + "static": [] + }, + "path": [ + { + "name": "setter", + "kind": "member" + } + ], + "namespace": "setter" + } +] \ No newline at end of file diff --git a/test/fixture/document-exported.output.md b/test/fixture/document-exported.output.md new file mode 100644 index 000000000..a515467d0 --- /dev/null +++ b/test/fixture/document-exported.output.md @@ -0,0 +1,39 @@ + + +# Class + +## classMethod + +## classGetter + +## classSetter + +**Parameters** + +- `v` + +## staticMethod + +## staticGetter + +## staticSetter + +**Parameters** + +- `v` + +# object + +## prop + +## func + +# method + +# getter + +# setter + +**Parameters** + +- `v` diff --git a/test/fixture/document-exported.output.md.json b/test/fixture/document-exported.output.md.json new file mode 100644 index 000000000..77eaa00f0 --- /dev/null +++ b/test/fixture/document-exported.output.md.json @@ -0,0 +1,250 @@ +{ + "type": "root", + "children": [ + { + "type": "html", + "value": "" + }, + { + "depth": 1, + "type": "heading", + "children": [ + { + "type": "text", + "value": "Class" + } + ] + }, + { + "depth": 2, + "type": "heading", + "children": [ + { + "type": "text", + "value": "classMethod" + } + ] + }, + { + "depth": 2, + "type": "heading", + "children": [ + { + "type": "text", + "value": "classGetter" + } + ] + }, + { + "depth": 2, + "type": "heading", + "children": [ + { + "type": "text", + "value": "classSetter" + } + ] + }, + { + "type": "strong", + "children": [ + { + "type": "text", + "value": "Parameters" + } + ] + }, + { + "ordered": false, + "type": "list", + "children": [ + { + "type": "listItem", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "inlineCode", + "value": "v" + }, + { + "type": "text", + "value": " " + }, + { + "type": "text", + "value": " " + } + ] + } + ] + } + ] + }, + { + "depth": 2, + "type": "heading", + "children": [ + { + "type": "text", + "value": "staticMethod" + } + ] + }, + { + "depth": 2, + "type": "heading", + "children": [ + { + "type": "text", + "value": "staticGetter" + } + ] + }, + { + "depth": 2, + "type": "heading", + "children": [ + { + "type": "text", + "value": "staticSetter" + } + ] + }, + { + "type": "strong", + "children": [ + { + "type": "text", + "value": "Parameters" + } + ] + }, + { + "ordered": false, + "type": "list", + "children": [ + { + "type": "listItem", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "inlineCode", + "value": "v" + }, + { + "type": "text", + "value": " " + }, + { + "type": "text", + "value": " " + } + ] + } + ] + } + ] + }, + { + "depth": 1, + "type": "heading", + "children": [ + { + "type": "text", + "value": "object" + } + ] + }, + { + "depth": 2, + "type": "heading", + "children": [ + { + "type": "text", + "value": "prop" + } + ] + }, + { + "depth": 2, + "type": "heading", + "children": [ + { + "type": "text", + "value": "func" + } + ] + }, + { + "depth": 1, + "type": "heading", + "children": [ + { + "type": "text", + "value": "method" + } + ] + }, + { + "depth": 1, + "type": "heading", + "children": [ + { + "type": "text", + "value": "getter" + } + ] + }, + { + "depth": 1, + "type": "heading", + "children": [ + { + "type": "text", + "value": "setter" + } + ] + }, + { + "type": "strong", + "children": [ + { + "type": "text", + "value": "Parameters" + } + ] + }, + { + "ordered": false, + "type": "list", + "children": [ + { + "type": "listItem", + "children": [ + { + "type": "paragraph", + "children": [ + { + "type": "inlineCode", + "value": "v" + }, + { + "type": "text", + "value": " " + }, + { + "type": "text", + "value": " " + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/lib/parsers/javascript.js b/test/lib/parsers/javascript.js index 15f7105f6..85faa7ba0 100644 --- a/test/lib/parsers/javascript.js +++ b/test/lib/parsers/javascript.js @@ -4,11 +4,12 @@ var test = require('tap').test, remark = require('remark'), parse = require('../../../lib/parsers/javascript'); -function toComments(fn, filename) { +function toComments(source, filename, opts) { + source = typeof source === 'string' ? source : '(' + source.toString() + ')'; return parse({ file: filename || 'test.js', - source: '(' + fn.toString() + ')' - }); + source: source + }, opts); } test('parse - leading comment', function (t) { @@ -48,3 +49,18 @@ test('parse - error', function (t) { { message: 'Missing or invalid tag name' }]); t.end(); }); + +test('parse - document exported', function (t) { + t.equal(toComments(` + export class C {} + `).length, 0); + t.equal(toComments(` + export class C {} + `, 'test.js', {documentExported: true}).length, 1); + t.equal(toComments(` + export class C { + method() {} + } + `, 'test.js', {documentExported: true}).length, 2); + t.end(); +}); diff --git a/test/normalize.js b/test/normalize.js index 579b28150..e977f8d36 100644 --- a/test/normalize.js +++ b/test/normalize.js @@ -1,5 +1,4 @@ -var walk = require('../lib/walk'), - traverse = require('babel-traverse').default; +var walk = require('../lib/walk'); module.exports = function (comments) { return walk(comments, function (comment) {