fix(jsdoc-parse): infer the existence of a module from @alias tags

If a class has a tag like `@alias module:foo.Bar`, then we can infer that the module for the current file is `module:foo`, even if there's no `/** @module foo */` comment in the file.
This commit is contained in:
Jeff Williams 2023-12-30 19:58:50 -08:00
parent aa49b841bb
commit 65da78e6bb
No known key found for this signature in database
4 changed files with 157 additions and 22 deletions

View File

@ -22,14 +22,16 @@ const PROTOTYPE_OWNER_REGEXP = /^(.+?)(\.prototype|#)$/;
const { SCOPE } = name;
let currentModule = null;
// Modules inferred from the value of an `@alias` tag, like `@alias module:foo.bar`.
let inferredModules = [];
class CurrentModule {
class ModuleInfo {
constructor(doclet) {
this.doclet = doclet;
this.longname = doclet.longname;
this.originalName = doclet.meta.code.name || '';
this.originalName = doclet.meta?.code?.name ?? '';
}
}
function filterByLongname({ longname }) {
// you can't document prototypes
if (/#$/.test(longname)) {
@ -89,37 +91,46 @@ function createSymbolDoclet(comment, e, deps) {
return doclet;
}
function setCurrentModule(doclet) {
function getModule() {
return inferredModules.length ? inferredModules[inferredModules.length - 1] : currentModule;
}
function setModule(doclet) {
if (doclet.kind === 'module') {
currentModule = new CurrentModule(doclet);
currentModule = new ModuleInfo(doclet);
} else if (doclet.longname.startsWith('module:')) {
inferredModules.push(
new ModuleInfo({
longname: name.getBasename(doclet.longname),
})
);
}
}
function setModuleScopeMemberOf(parser, doclet) {
const moduleInfo = getModule();
let parentDoclet;
let skipMemberof;
// handle module symbols that are _not_ assigned to module.exports
if (currentModule && currentModule.longname !== doclet.name) {
if (moduleInfo && moduleInfo.longname !== doclet.name) {
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) {
parentDoclet = parser._getDocletById(doclet.meta.code.node.parent.parent.nodeId);
// special case for constructors of classes that have @alias tags
if (doclet.meta.code.node.kind === 'constructor') {
parentDoclet = parser._getDocletById(doclet.meta.code.node.parent.parent.nodeId);
if (parentDoclet?.alias) {
// the constructor should use the same name as the class
doclet.addTag('alias', parentDoclet.alias);
doclet.addTag('name', parentDoclet.alias);
// and we shouldn't try to set a memberof value
skipMemberof = true;
}
} else if (doclet.meta.code.node.static) {
doclet.addTag('static');
if (doclet.meta.code.node.kind === 'constructor' && parentDoclet?.alias) {
// the constructor should use the same name as the class
doclet.addTag('alias', parentDoclet.alias);
doclet.addTag('name', parentDoclet.alias);
// and we shouldn't try to set a memberof value
skipMemberof = true;
} else {
doclet.addTag('instance');
doclet.addTag(doclet.meta.code.node.static ? 'static' : 'instance');
// The doclet should be a member of the parent doclet's alias.
if (parentDoclet?.alias) {
doclet.memberof = parentDoclet.alias;
}
}
}
// is this something that the module exports? if so, it's a static member
@ -135,7 +146,7 @@ function setModuleScopeMemberOf(parser, doclet) {
// if the doclet isn't a memberof anything yet, and it's not a global, it must be a memberof
// the current module (unless we were told to skip adding memberof)
if (!doclet.memberof && doclet.scope !== SCOPE.NAMES.GLOBAL && !skipMemberof) {
doclet.addTag('memberof', currentModule.longname);
doclet.addTag('memberof', moduleInfo.longname);
}
}
}
@ -151,7 +162,7 @@ function addDoclet(parser, newDoclet) {
let e;
if (newDoclet) {
setCurrentModule(newDoclet);
setModule(newDoclet);
e = { doclet: newDoclet };
parser.emit('newDoclet', e);
@ -365,5 +376,6 @@ export function attachTo(parser) {
parser.on('fileComplete', () => {
currentModule = null;
inferredModules = [];
});
}

View File

@ -0,0 +1,18 @@
/**
* A basket of delicious fruits.
*
* @alias module:fruits.Basket
*/
export default class Basket {
constructor() {
/**
* The material that the basket is made from.
*/
this.material = 'canvas';
}
/**
* Empties the basket.
*/
empty() {}
}

View File

@ -0,0 +1,32 @@
/** @module vegetables */
/**
* A basket of delicious fruits.
*
* @alias module:fruits.Basket
*/
export default class Basket {
constructor() {
/**
* The material that the basket is made from.
*/
this.material = 'canvas';
}
/**
* Empties the basket.
*/
empty() {}
/**
* Adds a sock to the basket.
*
* @alias module:laundry.Basket#addSock
*/
addSock() {}
/**
* Fills the basket.
*/
fill() {}
}

View File

@ -0,0 +1,73 @@
/*
Copyright 2023 the JSDoc Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
describe('aliased default export for ES6 module', () => {
describe('no `@module` tag', () => {
const docSet = jsdoc.getDocSetFromFile('test/fixtures/aliasfores6export.js');
it('uses the alias for the class to create longnames for its methods', () => {
const empty = docSet.getByLongname('module:fruits.Basket#empty')[0];
expect(empty).toBeObject();
expect(empty.memberof).toBe('module:fruits.Basket');
expect(empty.name).toBe('empty');
});
it('uses the alias for the class to create longnames for its other members', () => {
const material = docSet.getByLongname('module:fruits.Basket#material')[0];
expect(material).toBeObject();
expect(material.memberof).toBe('module:fruits.Basket');
expect(material.name).toBe('material');
});
});
describe('`@module` tag', () => {
const docSet = jsdoc.getDocSetFromFile('test/fixtures/aliasfores6export2.js');
it('uses the alias for the class to create longnames for its methods', () => {
const empty = docSet.getByLongname('module:fruits.Basket#empty')[0];
expect(empty).toBeObject();
expect(empty.memberof).toBe('module:fruits.Basket');
expect(empty.name).toBe('empty');
});
it('uses the alias for the class to create longnames for its other members', () => {
const material = docSet.getByLongname('module:fruits.Basket#material')[0];
expect(material).toBeObject();
expect(material.memberof).toBe('module:fruits.Basket');
expect(material.name).toBe('material');
});
it('uses the correct longname if there is a nested alias', () => {
const addSock = docSet.getByLongname('module:laundry.Basket#addSock')[0];
expect(addSock).toBeObject();
expect(addSock.memberof).toBe('module:laundry.Basket');
expect(addSock.name).toBe('addSock');
});
it('uses the correct longname if a method with no alias follows a method with an alias', () => {
const fill = docSet.getByLongname('module:fruits.Basket#fill')[0];
expect(fill).toBeObject();
expect(fill.memberof).toBe('module:fruits.Basket');
expect(fill.name).toBe('fill');
});
});
});