Skip to content

Commit

Permalink
fix: use a special longname for an ES2015 module's default export
Browse files Browse the repository at this point in the history
Previously, we used `module.exports`, which is both incorrect and confusing.
  • Loading branch information
hegemonic committed May 11, 2024
1 parent 14cfaa9 commit 575f0dc
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 18 deletions.
8 changes: 4 additions & 4 deletions packages/jsdoc-ast/lib/ast-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import moduleTypes from 'ast-module-types';

import { Syntax } from './syntax.js';

const { SCOPE } = name;
const { LONGNAMES, SCOPE } = name;

// Counter for generating unique node IDs.
let uid = 100000000;
Expand Down Expand Up @@ -166,7 +166,7 @@ export function nodeToValue(node) {
// falls through

case Syntax.ExportDefaultDeclaration:
str = 'module.exports';
str = LONGNAMES.MODULE_DEFAULT_EXPORT;
break;

case Syntax.ExportNamedDeclaration:
Expand Down Expand Up @@ -232,7 +232,7 @@ export function nodeToValue(node) {
parent.parent &&
parent.parent.type === Syntax.ExportDefaultDeclaration
) {
str = 'module.exports';
str = LONGNAMES.MODULE_DEFAULT_EXPORT;
}
// for the constructor of a module's named export, use the name of the export
// declaration
Expand Down Expand Up @@ -399,7 +399,7 @@ export function getInfo(node) {
info.node = node;
// if this class is the default export, we need to use a special name
if (node.parent && node.parent.type === Syntax.ExportDefaultDeclaration) {
info.name = 'module.exports';
info.name = LONGNAMES.MODULE_DEFAULT_EXPORT;
} else {
info.name = node.id ? nodeToValue(node.id) : '';
}
Expand Down
2 changes: 2 additions & 0 deletions packages/jsdoc-core/lib/name.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export const LONGNAMES = {
ANONYMOUS: '<anonymous>',
/** Longname that represents global scope. */
GLOBAL: '<global>',
/** Longname for the default export in an ES2015 module. */
MODULE_DEFAULT_EXPORT: '<moduleDefaultExport>',
};

// Module namespace prefix.
Expand Down
4 changes: 4 additions & 0 deletions packages/jsdoc-core/test/specs/lib/name.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ describe('@jsdoc/core.name', () => {
it('has a GLOBAL property', () => {
expect(name.LONGNAMES.GLOBAL).toBeString();
});

it('has a MODULE_DEFAULT_EXPORT property', () => {
expect(name.LONGNAMES.MODULE_DEFAULT_EXPORT).toBeString();
});
});

// TODO: longnamesToTree tests
Expand Down
42 changes: 28 additions & 14 deletions packages/jsdoc-parse/lib/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import { Doclet } from '@jsdoc/doclet';
import escape from 'escape-string-regexp';

const PROTOTYPE_OWNER_REGEXP = /^(.+?)(\.prototype|#)$/;
const { SCOPE } = name;
const { LONGNAMES, SCOPE } = name;
const ESCAPED_MODULE_LONGNAMES =
escape(LONGNAMES.MODULE_DEFAULT_EXPORT) + '|' + escape('module.exports');

let currentModule = null;
// Modules inferred from the value of an `@alias` tag, like `@alias module:foo.bar`.
Expand Down Expand Up @@ -107,13 +109,17 @@ function setModule(doclet) {
}
}

function isModuleExports(module, doclet) {
return module.longname === doclet.name;
}

function setModuleScopeMemberOf(parser, doclet) {
const moduleInfo = getModule();
let parentDoclet;
let skipMemberof;

// handle module symbols that are _not_ assigned to module.exports
if (moduleInfo && moduleInfo.longname !== doclet.name) {
// Handle CommonJS module symbols that are _not_ assigned to `module.exports`.
if (moduleInfo && !isModuleExports(moduleInfo, doclet)) {
if (!doclet.scope) {
// is this a method definition? if so, we usually get the scope from the node directly
if (doclet.meta?.code?.node?.type === Syntax.MethodDefinition) {
Expand Down Expand Up @@ -191,8 +197,13 @@ function processAlias(parser, doclet, astNode) {
doclet.postProcess();
}

function isModuleObject(doclet) {
return doclet.name === LONGNAMES.MODULE_DEFAULT_EXPORT || doclet.name === 'module.exports';
}

// TODO: separate code that resolves `this` from code that resolves the module object
function findSymbolMemberof(parser, doclet, astNode, nameStartsWith, trailingPunc) {
const docletIsModuleObject = isModuleObject(doclet);
let memberof = '';
let nameAndPunc;
let scopePunc = '';
Expand All @@ -205,22 +216,22 @@ function findSymbolMemberof(parser, doclet, astNode, nameStartsWith, trailingPun

nameAndPunc = nameStartsWith + (trailingPunc || '');

// remove stuff that indicates module membership (but don't touch the name `module.exports`,
// which identifies the module object itself)
if (doclet.name !== 'module.exports') {
// Remove parts of the name that indicate module membership. Don't touch the name if it identifies
// the module object itself.
if (!docletIsModuleObject) {
doclet.name = doclet.name.replace(nameAndPunc, '');
}

// like `bar` in:
// exports.bar = 1;
// module.exports.bar = 1;
// module.exports = MyModuleObject; MyModuleObject.bar = 1;
if (nameStartsWith !== 'this' && currentModule && doclet.name !== 'module.exports') {
if (nameStartsWith !== 'this' && currentModule && !docletIsModuleObject) {
memberof = currentModule.longname;
scopePunc = SCOPE.PUNC.STATIC;
}
// like: module.exports = 1;
else if (doclet.name === 'module.exports' && currentModule) {
else if (docletIsModuleObject && currentModule) {
doclet.addTag('name', currentModule.longname);
doclet.postProcess();
} else {
Expand Down Expand Up @@ -260,7 +271,9 @@ function addSymbolMemberof(parser, doclet, astNode) {
if (currentModule) {
moduleOriginalName = `|${currentModule.originalName}`;
}
resolveTargetRegExp = new RegExp(`^((?:module.)?exports|this${moduleOriginalName})(\\.|\\[|$)`);
resolveTargetRegExp = new RegExp(
`^((?:module\\.)?exports|${ESCAPED_MODULE_LONGNAMES}|this${moduleOriginalName})(\\.|\\[|$)`
);
unresolved = resolveTargetRegExp.exec(doclet.name);

if (unresolved) {
Expand Down Expand Up @@ -309,14 +322,15 @@ function newSymbolDoclet(parser, docletSrc, e) {
return false;
}

// set the scope to global unless any of the following are true:
// a) the doclet is a memberof something
// b) the doclet represents a module
// c) we're in a module that exports only this symbol
// Set the scope to `global` unless any of the following are true:
//
// + The doclet is a `memberof` something.
// + The doclet represents a module.
// + We're in a CommonJS module that exports only this symbol.
if (
!newDoclet.memberof &&
newDoclet.kind !== 'module' &&
(!currentModule || currentModule.longname !== newDoclet.name)
(!currentModule || !isModuleExports(currentModule, newDoclet))
) {
newDoclet.scope = SCOPE.NAMES.GLOBAL;
}
Expand Down

0 comments on commit 575f0dc

Please sign in to comment.