diff --git a/lib/jsdoc/src/astbuilder.js b/lib/jsdoc/src/astbuilder.js index 83670e93..ab627b2d 100644 --- a/lib/jsdoc/src/astbuilder.js +++ b/lib/jsdoc/src/astbuilder.js @@ -8,6 +8,8 @@ var parserOptions = exports.parserOptions = { ranges: true, sourceType: 'module', plugins: [ + 'classPrivateProperties', + 'classProperties', 'decorators2', 'doExpressions', 'estree', diff --git a/lib/jsdoc/src/astnode.js b/lib/jsdoc/src/astnode.js index 626796e8..c6212bbd 100644 --- a/lib/jsdoc/src/astnode.js +++ b/lib/jsdoc/src/astnode.js @@ -46,6 +46,7 @@ var isFunction = exports.isFunction = function(node) { var isScope = exports.isScope = function(node) { // TODO: handle blocks with "let" declarations return !!node && typeof node === 'object' && ( node.type === Syntax.CatchClause || + node.type === Syntax.ClassDeclaration || node.type === Syntax.ClassExpression || isFunction(node) ); }; @@ -138,6 +139,20 @@ var nodeToValue = exports.nodeToValue = function(node) { str = nodeToValue(node.id); break; + case Syntax.ClassPrivateProperty: + // TODO: Strictly speaking, the name should be '#' plus node.key, but because we + // already use '#' as scope punctuation, that causes JSDoc to get extremely confused. + // The solution probably involves quoting part or all of the name, but JSDoc doesn't + // deal with quoted names very nicely right now, and most people probably won't want to + // document class private properties anyhow. So for now, we'll just cheat and omit the + // leading '#'. + str = nodeToValue(node.key); + break; + + case Syntax.ClassProperty: + str = nodeToValue(node.key); + break; + case Syntax.ExportAllDeclaration: // falls through @@ -382,6 +397,20 @@ var getInfo = exports.getInfo = function(node) { break; + // like "#b = 1;" in: "class A { #b = 1; }" + case Syntax.ClassPrivateProperty: + info.node = node; + info.name = nodeToValue(info.node); + info.type = info.node.type; + break; + + // like "b = 1;" in: "class A { b = 1; }" + case Syntax.ClassProperty: + info.node = node; + info.name = nodeToValue(info.node); + info.type = info.node.type; + break; + // like: "export * from 'foo'" case Syntax.ExportAllDeclaration: info.node = node; diff --git a/lib/jsdoc/src/parser.js b/lib/jsdoc/src/parser.js index cd27f203..9bff5e02 100644 --- a/lib/jsdoc/src/parser.js +++ b/lib/jsdoc/src/parser.js @@ -354,6 +354,11 @@ Parser.prototype.astnodeToMemberof = function(node) { result.memberof = doclet.longname + jsdoc.name.SCOPE.PUNC.INNER; } } + else if (type === Syntax.ClassPrivateProperty || type === Syntax.ClassProperty) { + doclet = this._getDocletById(node.enclosingScope.nodeId); + + result.memberof = doclet.longname + jsdoc.name.SCOPE.PUNC.INSTANCE; + } else { // check local references for aliases scope = node; diff --git a/lib/jsdoc/src/syntax.js b/lib/jsdoc/src/syntax.js index bbe27629..c2908230 100644 --- a/lib/jsdoc/src/syntax.js +++ b/lib/jsdoc/src/syntax.js @@ -16,6 +16,8 @@ exports.Syntax = { ClassBody: 'ClassBody', ClassDeclaration: 'ClassDeclaration', ClassExpression: 'ClassExpression', + ClassPrivateProperty: 'ClassPrivateProperty', + ClassProperty: 'ClassProperty', ComprehensionBlock: 'ComprehensionBlock', ComprehensionExpression: 'ComprehensionExpression', ConditionalExpression: 'ConditionalExpression', @@ -65,6 +67,7 @@ exports.Syntax = { NewExpression: 'NewExpression', ObjectExpression: 'ObjectExpression', ObjectPattern: 'ObjectPattern', + PrivateName: 'PrivateName', Program: 'Program', Property: 'Property', RestElement: 'RestElement', diff --git a/lib/jsdoc/src/visitor.js b/lib/jsdoc/src/visitor.js index c1bef156..68862dc7 100644 --- a/lib/jsdoc/src/visitor.js +++ b/lib/jsdoc/src/visitor.js @@ -346,6 +346,19 @@ function makeAsyncFunctionFinisher(parser) { }; } +/** + * Create a function that will mark a doclet as private. + * + * @private + * @param {module:jsdoc/src/parser.Parser} parser - The JSDoc parser. + * @return {function} A function that marks a doclet as private. + */ +function makePrivatePropertyFinisher(parser) { + return function(e) { + e.doclet.access = 'private'; + }; +} + // TODO: docs function SymbolFound(node, filename, extras) { var self = this; @@ -433,7 +446,6 @@ Visitor.prototype.visit = function(node, filename) { return true; }; -// TODO: docs /** * Verify that a block comment exists and that its leading delimiter does not contain three or more * asterisks. @@ -613,6 +625,25 @@ Visitor.prototype.makeSymbolFoundEvent = function(node, parser, filename) { break; + // like `#b = 1` in: class A { #b = 1; } + case Syntax.ClassPrivateProperty: + extras.finishers = [ + parser.resolveEnum, + makePrivatePropertyFinisher(parser) + ]; + + e = new SymbolFound(node, filename, extras); + + break; + + // like `b = 1` in: class A { b = 1; } + case Syntax.ClassProperty: + extras.finishers = [parser.resolveEnum]; + + e = new SymbolFound(node, filename, extras); + + break; + // like: export * from 'foo' case Syntax.ExportAllDeclaration: e = new SymbolFound(node, filename, extras); diff --git a/lib/jsdoc/src/walker.js b/lib/jsdoc/src/walker.js index 12c4abb4..65642f01 100644 --- a/lib/jsdoc/src/walker.js +++ b/lib/jsdoc/src/walker.js @@ -12,20 +12,6 @@ var doclet = require('jsdoc/doclet'); var logger = require('jsdoc/util/logger'); var Syntax = require('jsdoc/src/syntax').Syntax; -/** - * Check whether an AST node creates a new scope. - * - * @private - * @param {Object} node - The AST node to check. - * @return {Boolean} Set to `true` if the node creates a new scope, or `false` in all other cases. - */ -function isScopeNode(node) { - // TODO: handle blocks with "let" declarations - return node && typeof node === 'object' && (node.type === Syntax.CatchClause || - node.type === Syntax.FunctionDeclaration || node.type === Syntax.FunctionExpression || - node.type === Syntax.ArrowFunctionExpression); -} - // TODO: docs function getCurrentScope(scopes) { return scopes[scopes.length - 1] || null; @@ -159,6 +145,10 @@ walkers[Syntax.ClassDeclaration] = function(node, parent, state, cb) { walkers[Syntax.ClassExpression] = walkers[Syntax.ClassDeclaration]; +// walkers[Syntax.ClassPrivateProperty] is defined later + +// walkers[Syntax.ClassProperty] is defined later + // TODO: verify correctness walkers[Syntax.ComprehensionBlock] = walkers[Syntax.AssignmentExpression]; @@ -448,6 +438,10 @@ walkers[Syntax.ObjectExpression] = function(node, parent, state, cb) { walkers[Syntax.ObjectPattern] = walkers[Syntax.ObjectExpression]; +walkers[Syntax.PrivateName] = function(node, parent, state, cb) { + cb(node.name, node, state); +}; + walkers[Syntax.Program] = function(node, parent, state, cb) { // if the first item in the body has multiple leading comments, move all but the last one to // this node. this happens, for example, when a file has a /** @module */ standalone comment @@ -479,6 +473,10 @@ walkers[Syntax.Property] = function(node, parent, state, cb) { } }; +walkers[Syntax.ClassPrivateProperty] = walkers[Syntax.Property]; + +walkers[Syntax.ClassProperty] = walkers[Syntax.Property]; + walkers[Syntax.RestElement] = function(node, parent, state, cb) { if (node.argument) { cb(node.argument, node, state); diff --git a/test/fixtures/classproperties.js b/test/fixtures/classproperties.js new file mode 100644 index 00000000..ba46450b --- /dev/null +++ b/test/fixtures/classproperties.js @@ -0,0 +1,8 @@ +/** Sample class. */ +class A { + /** Public property. */ + b = 1; + + /** Private property. */ + #c = 2; +} diff --git a/test/specs/documentation/classproperties.js b/test/specs/documentation/classproperties.js new file mode 100644 index 00000000..6a822ccb --- /dev/null +++ b/test/specs/documentation/classproperties.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('class properties', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/classproperties.js'); + var b = docSet.getByLongname('A#b')[0]; + var c = docSet.getByLongname('A#c')[0]; + + it('should assign the correct name, memberof, and scope to class properties', function() { + expect(b.name).toBe('b'); + expect(b.memberof).toBe('A'); + expect(b.scope).toBe('instance'); + }); + + it('should assign the correct name, memberof, scope, and access type to class private ' + + 'properties', function() { + expect(c.name).toBe('c'); + expect(c.memberof).toBe('A'); + expect(c.scope).toBe('instance'); + expect(c.access).toBe('private'); + }); +});