UNPKG

documentation

Version:
497 lines (444 loc) 15 kB
import n from '@babel/types'; import pathParse from 'parse-filepath'; import isJSDocComment from '../is_jsdoc_comment.js'; import parse from '../parse.js'; function inferModuleName(comment) { return ( (comment.kind === 'module' && comment.name) || pathParse(comment.context.file).name ); } /** * Given an AST node, try to find a comment in front of it that * has a `lends` tag, and if it has that, return the tag, split by * .s. * * @private * @param {Object} path AST node * @returns {string|undefined} lends identifier, if any */ function findLendsIdentifiers(path) { if (!path || !path.get('leadingComments')) { return; } const leadingComments = path.get('leadingComments'); for (let i = 0; i < leadingComments.length; i++) { const comment = leadingComments[i]; if (isJSDocComment(comment.node)) { const lends = parse(comment.node.value).lends; if (lends) { return lends.split('.'); } } } } /** * Given an AST node, try to find a comment before the class declaration that * has a `memberof` tag, and if it has that, return the tag, split by * .s with the name of the class. * * @private * @param {Object} path AST node * @returns {Array<string>} class membership */ function inferClassMembership(path) { if (path.get('leadingComments')) { const leadingComments = path.get('leadingComments'); for (let i = leadingComments.length - 1; i >= 0; i--) { const comment = leadingComments[i]; if (isJSDocComment(comment.node)) { const memberof = parse(comment.node.value).memberof; if (memberof) { return [...memberof.split('.'), path.node.id.name]; } } } } return [path.node.id.name]; } /** * Extract and return the identifiers for expressions of * type this.foo * * @param {NodePath} path AssignmentExpression, MemberExpression, * or Identifier * @param {Comment} comment * @returns {Array<string>} identifiers * @private */ function extractThis(path, comment) { let identifiers = []; path.traverse({ /** * Add the resolved identifier of this in a path to the identifiers array * @param {Object} path ast path * @returns {undefined} has side-effects * @private */ ThisExpression(path) { let scope = path.scope; while (n.isBlockStatement(scope.block)) { scope = scope.parent; } if ( n.isClassMethod(scope.block) && scope.path.parentPath.parentPath.node.id !== null ) { identifiers.push( scope.path.parentPath.parentPath.node.id.name, 'prototype' ); } // function OldClass() { this.foo = 1 } if (n.isFunctionDeclaration(scope.block)) { // named function like // function OldClass() { ... } if (scope.block.id) { identifiers.push(scope.block.id.name, 'prototype'); } else if (n.isExportDefaultDeclaration(path.scope.parentBlock)) { identifiers.push(inferModuleName(comment)); } // var Binding = function OldClass() { this.foo = 1 } } else if (n.isFunctionExpression(scope.block)) { if (scope.path.parentPath.isVariableDeclarator()) { /** var Bar = function(foo) { this.foo = foo; }; */ identifiers = identifiers .concat(scope.path.parentPath.get('id').node.name) .concat('prototype'); } else if (scope.path.parentPath.isAssignmentExpression()) { /** this.Bar = function(foo) { this.foo = foo; }; */ identifiers = identifiers .concat(extractIdentifiers(scope.path.parentPath.get('left'))) .concat('prototype'); } } } }); return identifiers; } /** * Extract and return the chain of identifiers from the left hand side of expressions * of the forms `Foo = ...`, `Foo.bar = ...`, `Foo.bar.baz = ...`, etc. * * @param {NodePath} path AssignmentExpression, MemberExpression, or Identifier * @returns {Array<string>} identifiers * @private */ function extractIdentifiers(path) { const identifiers = []; path.traverse({ /** * Add an identifier in a path to the identifiers array * @param {Object} path ast path * @returns {undefined} has side-effects * @private */ Identifier(path) { identifiers.push(path.node.name); } }); return identifiers; } /** * Count leading identifiers that refer to a module export (`exports` or `module.exports`). * @private * @param {Object} comment parsed comment * @param {Array<string>} identifiers array of identifier names * @returns {number} number of identifiers referring to a module export (0, 1 or 2) */ function countModuleIdentifiers(comment, identifiers) { if (identifiers.length >= 1 && identifiers[0] === 'exports') { return 1; } if ( identifiers.length >= 2 && identifiers[0] === 'module' && identifiers[1] === 'exports' ) { return 2; } return 0; } /** * Returns the comment object after normalizing Foo.prototype and Foo# expressions * @param comment parsed comment * @returns the normalized comment */ function normalizeMemberof(comment) { if (typeof comment.memberof != 'string') { return comment; } const memberof = comment.memberof; const isPrototype = /.prototype$/; if (memberof.match(isPrototype) !== null) { comment.memberof = memberof.replace(isPrototype, ''); comment.scope = 'instance'; return comment; } const isInstanceMember = /#$/; if (memberof.match(isInstanceMember) !== null) { comment.memberof = memberof.replace(isInstanceMember, ''); comment.scope = 'instance'; } return comment; } /** * Uses code structure to infer `memberof`, `instance`, and `static` * tags from the placement of JSDoc * annotations within a file * * @private * @returns {Object} comment with membership inferred */ export default function () { let currentModule; /** * Set `memberof` and `instance`/`static` tags on `comment` based on the * array of `identifiers`. If the last element of the `identifiers` is * `"prototype"`, it is assumed to be an instance member; otherwise static. * If the `identifiers` start with `exports` or `module.exports`, assign * membership based on the last seen @module tag or name of the current file. * * @param {Object} comment comment for which to infer memberships * @param {Array<string>} identifiers array of identifier names * @param {string} explicitScope if derived from an es6 class, whether or * not this method had the static keyword * @returns {Comment} returns mutated `comment` * @private */ function inferMembershipFromIdentifiers(comment, identifiers, explicitScope) { if ( identifiers.length === 1 && identifiers[0] === 'module' && comment.name === 'exports' ) { comment.name = inferModuleName(currentModule || comment); return comment; } /* * Test whether identifiers start with a module export (`exports` or `module.exports`), * and if so replace those identifiers with the name of the current module. */ const moduleIdentifierCount = countModuleIdentifiers(comment, identifiers); if (moduleIdentifierCount) { identifiers = identifiers.slice(moduleIdentifierCount); identifiers.unshift(inferModuleName(currentModule || comment)); } if (identifiers[identifiers.length - 1] === 'prototype') { comment.memberof = identifiers.slice(0, -1).join('.'); comment.scope = 'instance'; } else { comment.memberof = identifiers.join('.'); if (explicitScope !== undefined) { comment.scope = explicitScope; } else { comment.scope = 'static'; } } return comment; } return function inferMembership(comment) { // Lends tags are go-betweens that let people reassign membership // in bulk: they themselves don't get an inference step if (comment.lends) { return comment; } if (comment.kind === 'module') { currentModule = comment; } // If someone explicitly specifies the parent of this chunk, don't // try to infer it, just return what they specified. if (comment.memberof) { return normalizeMemberof(comment); } let path = comment.context.ast; // If this chunk doesn't have code attached, like if it was the result // of a polyglot parse, don't try to infer anything. if (!path) { return comment; } // INFERENCE =============================================================== // Deal with an oddity of espree: the jsdoc comment is attached to a different // node in the two expressions `a.b = c` vs `a.b = function () {}`. if ( path.isExpressionStatement() && path.get('expression').isAssignmentExpression() && path.get('expression').get('left').isMemberExpression() ) { path = path.get('expression').get('left'); } // Same as above but for `b: c` vs `b: function () {}`. if ( path.isObjectProperty() && (path.get('key').isIdentifier() || path.get('key').isLiteral()) ) { path = path.get('key'); } // Forms: // // Foo.bar = ...; // Foo.prototype.bar = ...; // Foo.bar.baz = ...; // // Lends is not supported in this codepath. if (path.isMemberExpression()) { const memberIdentifiers = [].concat( extractThis(path, comment), extractIdentifiers(path) ); if (memberIdentifiers.length >= 2) { return inferMembershipFromIdentifiers( comment, memberIdentifiers.slice(0, -1) ); } return comment; } // Like straight membership, classes don't need // to support lends. // // class Foo { bar() { } } // var Foo = class { bar() { } } // class Foo { prop: T } // var Foo = class { prop: T } if ( (path.isClassMethod() || path.isClassProperty() || path.isTSDeclareMethod()) && path.parentPath.isClassBody() && path.parentPath.parentPath.isClass() ) { let scope = 'instance'; if (path.node.static == true) { scope = 'static'; } if (path.parentPath.parentPath.isExpression()) { return inferMembershipFromIdentifiers( comment, extractIdentifiers(path.parentPath.parentPath.parentPath.get('left')), scope ); } const declarationNode = path.parentPath.parentPath.node; if (!declarationNode.id && !declarationNode.key) { // export default function () {} // export default class {} // Use module name instead. return inferMembershipFromIdentifiers( comment, [pathParse(comment.context.file).name], scope ); } return inferMembershipFromIdentifiers( comment, inferClassMembership(path.parentPath.parentPath), scope ); } // Whether something is an ObjectMethod (shorthand like foo() {} ) // or ObjectProperty (old fashioned like foo: function() {} ) // doesn't matter for the membership phase, as long as we end up knowing // that it belongs to an object. So we first establish objectParent, // and then have the logic for the numerous ways an object can be named. let objectParent; if ( (path.isIdentifier() || path.isLiteral()) && path.parentPath.isObjectProperty() && path.parentPath.parentPath.isObjectExpression() ) { objectParent = path.parentPath.parentPath; } else if (path.isObjectMethod() && path.parentPath.isObjectExpression()) { objectParent = path.parentPath; } else if (path.isObjectTypeProperty() || path.isTSTypeElement()) { objectParent = path.parentPath; } // Confirm that the thing being documented is a property of an object. if (objectParent) { // Collect all keys of parent nested object keys, e.g. {foo: bar: {baz: 1}} const objectKeys = []; while (!objectParent.isStatement()) { if ( objectParent.isObjectProperty() || objectParent.isObjectTypeProperty() || objectParent.isTSPropertySignature() ) { objectKeys.unshift(objectParent.node.key.name); } // The @lends comment is sometimes attached to the first property rather than // the object expression itself. const lendsIdentifiers = findLendsIdentifiers(objectParent) || findLendsIdentifiers(objectParent.get('properties')[0]); if (lendsIdentifiers) { return inferMembershipFromIdentifiers(comment, [ ...lendsIdentifiers, ...objectKeys ]); } else if (objectParent.parentPath.isAssignmentExpression()) { // Foo = { ... }; // Foo.prototype = { ... }; // Foo.bar = { ... }; return inferMembershipFromIdentifiers(comment, [ ...extractIdentifiers(objectParent.parentPath.get('left')), ...objectKeys ]); } else if (objectParent.parentPath.isVariableDeclarator()) { // var Foo = { ... }; return inferMembershipFromIdentifiers(comment, [ objectParent.parentPath.get('id').node.name, ...objectKeys ]); } else if (objectParent.parentPath.isExportDefaultDeclaration()) { // export default { ... }; return inferMembershipFromIdentifiers(comment, [ inferModuleName(currentModule || comment), ...objectKeys ]); } else if ( objectParent.parentPath.isTypeAlias() || objectParent.parentPath.isTSTypeAliasDeclaration() ) { // type X = { ... } return inferMembershipFromIdentifiers(comment, [ objectParent.parentPath.node.id.name, ...objectKeys ]); } else if ( objectParent.parentPath.isInterfaceDeclaration() || objectParent.parentPath.isTSInterfaceDeclaration() ) { // interface Foo { ... } return inferMembershipFromIdentifiers( comment, [...inferClassMembership(objectParent.parentPath), ...objectKeys], 'instance' ); } objectParent = objectParent.parentPath; } } // TypeScript enums // enum Foo { A } if (path.isTSEnumMember()) { const enumPath = path.parentPath; return inferMembershipFromIdentifiers( comment, [enumPath.node.id.name], 'static' ); } // var function Foo() { // function bar() {} // return { bar: bar }; // } /* if (n.isFunctionDeclaration(path) && n.isBlockStatement(path.parentPath) && n.isFunction(path.parentPath.parentPath)) { inferMembershipFromIdentifiers(comment, [path.parentPath.parentPath.node.id.name]); } */ return comment; }; }