UNPKG

jsdoc-75lb

Version:

An API documentation generator for JavaScript.

445 lines (367 loc) 11.9 kB
'use strict'; var espree = require('espree'); var jsdoc = { src: { syntax: require('jsdoc/src/syntax'), Walker: require('jsdoc/src/walker').Walker }, util: { logger: require('jsdoc/util/logger') } }; var Syntax = jsdoc.src.syntax.Syntax; var VISITOR_CONTINUE = true; var VISITOR_STOP = false; // TODO: docs; empty array means any node type, otherwise only the node types in the array var acceptsLeadingComments = (function() { var accepts = {}; // these nodes always accept leading comments var commentable = [ Syntax.ArrowFunctionExpression, Syntax.AssignmentExpression, Syntax.CallExpression, Syntax.ClassDeclaration, Syntax.ExportAllDeclaration, Syntax.ExportDefaultDeclaration, Syntax.ExportNamedDeclaration, Syntax.ExportSpecifier, Syntax.FunctionDeclaration, Syntax.FunctionExpression, Syntax.MemberExpression, Syntax.MethodDefinition, Syntax.Property, Syntax.TryStatement, Syntax.VariableDeclaration, Syntax.VariableDeclarator, Syntax.WithStatement ]; for (var i = 0, l = commentable.length; i < l; i++) { accepts[commentable[i]] = []; } // these nodes accept leading comments if they have specific types of parent nodes // like: function foo(/** @type {string} */ bar) {} accepts[Syntax.Identifier] = [ Syntax.ArrowFunctionExpression, Syntax.CatchClause, Syntax.FunctionDeclaration, Syntax.FunctionExpression ]; // like: function foo(/** @type {string} */ bar='baz') {} accepts[Syntax.AssignmentPattern] = [ Syntax.ArrowFunctionExpression, Syntax.FunctionDeclaration, Syntax.FunctionExpression ]; // like: function foo(/** @type {string} */ ...bar) {} accepts[Syntax.RestElement] = [ Syntax.ArrowFunctionExpression, Syntax.FunctionDeclaration, Syntax.FunctionExpression ]; // like: var Foo = Class.create(/** @lends Foo */{ // ... }) accepts[Syntax.ObjectExpression] = [ Syntax.CallExpression, Syntax.Property, Syntax.ReturnStatement ]; return accepts; })(); // exported so we can use them in tests var parserOptions = exports.parserOptions = { comment: true, ecmaFeatures: { experimentalObjectRestSpread: true, globalReturn: true, impliedStrict: true, jsx: true }, ecmaVersion: 7, loc: true, range: true, sourceType: 'module', tokens: true }; // TODO: docs function canAcceptComment(node) { var canAccept = false; var spec = acceptsLeadingComments[node.type]; if (spec) { // empty array means we don't care about the parent type if (spec.length === 0) { canAccept = true; } // we can accept the comment if the spec contains the type of the node's parent else if (node.parent) { canAccept = spec.indexOf(node.parent.type) !== -1; } } return canAccept; } // TODO: docs // check whether node1 is before node2 function isBefore(beforeRange, afterRange) { return beforeRange[1] <= afterRange[0]; } // TODO: docs function isWithin(innerRange, outerRange) { return innerRange[0] >= outerRange[0] && innerRange[1] <= outerRange[1]; } // TODO: docs function isJsdocComment(comment) { return comment && (comment.type === 'Block') && (comment.value[0] === '*'); } /** * Add the raw comment string to a block comment node. * * @private * @param {!Object} comment - A comment node with `type` and `value` properties. */ function addRawComment(comment) { comment.raw = comment.raw || ('/*' + comment.value + '*/'); return comment; } // TODO: docs function scrubComments(comments) { var comment; var scrubbed = []; for (var i = 0, l = comments.length; i < l; i++) { comment = comments[i]; if ( isJsdocComment(comment) ) { scrubbed.push( addRawComment(comment) ); } } return scrubbed; } // TODO: docs var AstBuilder = exports.AstBuilder = function() {}; function parse(source, filename) { var ast; try { ast = espree.parse(source, parserOptions); } catch (e) { jsdoc.util.logger.error('Unable to parse %s: %s', filename, e.message); } return ast; } // TODO: docs AstBuilder.prototype.build = function(source, filename) { var ast = parse(source, filename); if (ast) { this._postProcess(filename, ast); } return ast; }; // TODO: docs function atomSorter(a, b) { var aRange = a.range; var bRange = b.range; var result = 0; // does a end before b starts? if ( isBefore(aRange, bRange) ) { result = -1; } // does a enclose b? else if ( isWithin(bRange, aRange) ) { result = -1; } // does a start before b? else if (aRange[0] < bRange[0]) { result = -1; } // are the ranges non-identical? if so, b must be first else if ( aRange[0] !== bRange[0] || aRange[1] !== bRange[1] ) { result = 1; } return result; } // TODO: docs // TODO: export? function CommentAttacher(comments, tokens) { this._comments = comments || []; this._tokens = tokens || []; this._tokenIndex = 0; this._previousNode = null; this._astRoot = null; this._resetPendingComments() ._resetCandidates(); } // TODO: docs CommentAttacher.prototype._resetPendingComments = function() { this._pendingComments = []; this._pendingCommentRange = null; return this; }; // TODO: docs CommentAttacher.prototype._resetCandidates = function() { this._candidates = []; return this; }; // TODO: docs CommentAttacher.prototype._nextComment = function() { return this._comments[0] || null; }; // TODO: docs CommentAttacher.prototype._nextToken = function() { return this._tokens[this._tokenIndex] || null; }; // TODO: docs // find the index of the atom whose end position is closest to (but not after) the specified // position CommentAttacher.prototype._nextIndexBefore = function(atoms, startIndex, position) { var atom; var newIndex = startIndex; for (var i = newIndex, l = atoms.length; i < l; i++) { atom = atoms[i]; if (atom.range[1] > position) { break; } else { newIndex = i; } } return newIndex; }; // TODO: docs CommentAttacher.prototype._advanceTokenIndex = function(node) { var position = node.range[0]; this._tokenIndex = this._nextIndexBefore(this._tokens, this._tokenIndex, position); return this; }; // TODO: docs CommentAttacher.prototype._fastForwardComments = function(node) { var position = node.range[0]; var commentIndex = this._nextIndexBefore(this._comments, 0, position); // all comments before the node (except the last one) are pended if (commentIndex > 0) { this._pendingComments = this._pendingComments.concat( this._comments.splice(0, commentIndex) ); } }; CommentAttacher.prototype._attachPendingCommentsAsLeading = function(target) { target.leadingComments = (target.leadingComments || []).concat(this._pendingComments); }; CommentAttacher.prototype._attachPendingCommentsAsTrailing = function(target) { target.trailingComments = (target.trailingComments || []).concat(this._pendingComments); }; // TODO: docs CommentAttacher.prototype._attachPendingComments = function(currentNode) { var target; if (!this._pendingComments.length) { return this; } // if there are one or more candidate nodes, attach the pending comments before the last // candidate node if (this._candidates.length > 0) { target = this._candidates[this._candidates.length - 1]; this._attachPendingCommentsAsLeading(target); } // if we don't have a previous node, attach pending comments before the AST root; this should // mean that we haven't encountered any other nodes yet, or that the source file contains // JSDoc comments but not code else if (!this._previousNode) { target = this._astRoot; this._attachPendingCommentsAsLeading(target); } // otherwise, the comments must come after the current node (or the last node of the AST, if // we've run out of nodes) else { this._attachPendingCommentsAsTrailing(currentNode || this._previousNode); } // update the previous node this._previousNode = currentNode; this._resetPendingComments() ._resetCandidates(); return this; }; // TODO: docs CommentAttacher.prototype._isEligible = function(node) { var atoms; var token; var isEligible = false; var comment = this._nextComment(); if (comment) { atoms = [node, comment]; token = this._nextToken(); if (token) { atoms.push(token); } atoms.sort(atomSorter); // a candidate node must immediately follow the comment if (atoms.indexOf(node) === atoms.indexOf(comment) + 1) { isEligible = true; } } return isEligible; }; // TODO: docs // TODO: do we ever get multiple candidate nodes? CommentAttacher.prototype.visit = function(node) { var isEligible; // bail if we're out of comments if ( !this._nextComment() ) { return VISITOR_STOP; } // set the AST root if necessary this._astRoot = this._astRoot || node; // move to the next token, and fast-forward past comments that can no longer be attached this._advanceTokenIndex(node); this._fastForwardComments(node); // now we can check whether the current node is in the right position to accept the next comment isEligible = this._isEligible(node); // attach the pending comments, if any this._attachPendingComments(node); // okay, now that we've done all that bookkeeping, we can check whether the current node accepts // leading comments and add it to the candidate list if needed if ( isEligible && canAcceptComment(node) ) { // make sure we don't go past the end of the outermost target node if (!this._pendingCommentRange) { this._pendingCommentRange = node.range.slice(0); } this._candidates.push(node); // we have a candidate node, so pend the current comment this._pendingComments.push(this._comments.splice(0, 1)[0]); } return VISITOR_CONTINUE; }; // TODO: docs CommentAttacher.prototype.finish = function() { var length = this._comments.length; // any leftover comments are pended if (length) { this._pendingComments = this._pendingComments.concat( this._comments.splice(0, length) ); } // attach the pending comments, if any this._attachPendingComments(); }; // TODO: docs // TODO: refactor to make this extensible /** * @private * @param {string} filename - The full path to the source file. * @param {Object} ast - An abstract syntax tree that conforms to the Mozilla Parser API. */ AstBuilder.prototype._postProcess = function(filename, ast) { var attachComments = !!ast.comments && !!ast.comments.length; var commentAttacher; var scrubbed; var visitor; var walker; if (!attachComments) { return; } scrubbed = scrubComments(ast.comments.slice(0)); commentAttacher = new CommentAttacher(scrubbed.slice(0), ast.tokens); visitor = { visit: function(node) { return commentAttacher.visit(node); } }; walker = new jsdoc.src.Walker(); walker.recurse(ast, visitor, filename); commentAttacher.finish(); // replace the comments with the filtered comments ast.comments = scrubbed; // we no longer need the tokens ast.tokens = []; };