/* Copyright 2011 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. */ /** * Provides methods for augmenting the parse results based on their content. */ import { name } from '@jsdoc/core'; import { combineDoclets, Doclet } from './doclet.js'; const { fromParts, SCOPE, toParts } = name; function mapDependencies(index, propertyName) { const dependencies = {}; let doc; let doclets; const kinds = ['class', 'external', 'interface', 'mixin']; let len = 0; Object.keys(index).forEach((indexName) => { doclets = index[indexName]; for (let i = 0, ii = doclets.length; i < ii; i++) { doc = doclets[i]; if (kinds.includes(doc.kind)) { dependencies[indexName] = {}; if (Object.hasOwn(doc, propertyName)) { len = doc[propertyName]?.length; for (let j = 0; j < len; j++) { dependencies[indexName][doc[propertyName][j]] = true; } } } } }); return dependencies; } class Sorter { constructor(dependencies) { this.dependencies = dependencies; this.visited = {}; this.sorted = []; } visit(key) { if (!(key in this.visited)) { this.visited[key] = true; if (this.dependencies[key]) { Object.keys(this.dependencies[key]).forEach((path) => { this.visit(path); }); } this.sorted.push(key); } } sort() { Object.keys(this.dependencies).forEach((key) => { this.visit(key); }); return this.sorted; } } function sort(dependencies) { const sorter = new Sorter(dependencies); return sorter.sort(); } function getMembers(longname, { index }, scopes) { const memberof = index.memberof[longname] || []; const members = []; memberof.forEach((candidate) => { if (scopes.includes(candidate.scope)) { members.push(candidate); } }); return members; } function getDocumentedLongname(longname, { index }) { const doclets = index.documented[longname] || []; return doclets[doclets.length - 1]; } function addDocletProperty(doclets, propName, value) { for (let i = 0, l = doclets.length; i < l; i++) { doclets[i][propName] = value; } } function reparentDoclet({ longname }, child) { const parts = toParts(child.longname); parts.memberof = longname; child.memberof = longname; child.longname = fromParts(parts); } function parentIsClass({ kind }) { return kind === 'class'; } function staticToInstance(doclet) { const parts = toParts(doclet.longname); parts.scope = SCOPE.PUNC.INSTANCE; doclet.longname = fromParts(parts); doclet.scope = SCOPE.NAMES.INSTANCE; } /** * Update the list of doclets to be added to another symbol. * * We add only one doclet per longname. For example: If `ClassA` inherits from two classes that both * use the same method name, `ClassA` gets docs for one method rather than two. * * Also, the last symbol wins for any given longname. For example: If you write `@extends Class1 * @extends Class2`, and both classes have an instance method called `myMethod`, you get the docs * from `Class2#myMethod`. * * @private * @param {module:@jsdoc/doclet.Doclet} doclet - The doclet to be added. * @param {Array.} additions - An array of doclets that will be added to * another symbol. * @param {Object.} indexes - A dictionary of indexes into the `additions` array. * Each key is a longname, and each value is the index of the longname's doclet. * @return {void} */ function updateAddedDoclets(doclet, additions, indexes) { if (typeof indexes[doclet.longname] !== 'undefined') { // replace the existing doclet additions[indexes[doclet.longname]] = doclet; } else { // add the doclet to the array, and track its index additions.push(doclet); indexes[doclet.longname] = additions.length - 1; } } /** * Update the index of doclets whose `undocumented` property is not `true`. * * @private * @param {module:@jsdoc/doclet.Doclet} doclet - The doclet to be added to the index. * @param {Object.>} documented - The index of doclets * whose `undocumented` property is not `true`. * @return {void} */ function updateDocumentedDoclets(doclet, documented) { if (!Object.hasOwn(documented, doclet.longname)) { documented[doclet.longname] = []; } documented[doclet.longname].push(doclet); } /** * Update the index of doclets with a `memberof` value. * * @private * @param {module:@jsdoc/doclet.Doclet} doclet - The doclet to be added to the index. * @param {Object.>} memberof - The index of doclets * with a `memberof` value. * @return {void} */ function updateMemberofDoclets(doclet, memberof) { if (doclet.memberof) { if (!Object.hasOwn(memberof, doclet.memberof)) { memberof[doclet.memberof] = []; } memberof[doclet.memberof].push(doclet); } } function explicitlyInherits(doclets) { let doclet; let inherits = false; for (let i = 0, l = doclets.length; i < l; i++) { doclet = doclets[i]; if (typeof doclet.inheritdoc !== 'undefined' || typeof doclet.override !== 'undefined') { inherits = true; break; } } return inherits; } function changeMemberof(longname, newMemberof) { const atoms = toParts(longname); atoms.memberof = newMemberof; return fromParts(atoms); } // TODO: try to reduce overlap with similar methods function getInheritedAdditions(doclets, docs, { documented, memberof }) { let additionIndexes; const additions = []; let childDoclet; let childLongname; let doc; let parentDoclet; let parentMembers; let parents; let member; let parts; // doclets will be undefined if the inherited symbol isn't documented doclets = doclets || []; for (let i = 0, ii = doclets.length; i < ii; i++) { doc = doclets[i]; parents = doc.augments; if (parents && (doc.kind === 'class' || doc.kind === 'interface')) { // reset the lookup table of added doclet indexes by longname additionIndexes = {}; for (let j = 0, jj = parents.length; j < jj; j++) { parentMembers = getMembers(parents[j], docs, ['instance']); for (let k = 0, kk = parentMembers.length; k < kk; k++) { parentDoclet = parentMembers[k]; // We only care about symbols that are documented. if (parentDoclet.undocumented) { continue; } childLongname = changeMemberof(parentDoclet.longname, doc.longname); childDoclet = getDocumentedLongname(childLongname, docs) || {}; // We don't want to fold in properties from the child doclet if it had an // `@inheritdoc` tag. if (Object.hasOwn(childDoclet, 'inheritdoc')) { childDoclet = {}; } member = combineDoclets(childDoclet, parentDoclet); if (!member.inherited) { member.inherits = member.longname; } member.inherited = true; member.memberof = doc.longname; parts = toParts(member.longname); parts.memberof = doc.longname; member.longname = fromParts(parts); // Indicate what the descendant is overriding. (We only care about the closest // ancestor. For classes A > B > C, if B#a overrides A#a, and C#a inherits B#a, // we don't want the doclet for C#a to say that it overrides A#a.) if (Object.hasOwn(docs.index.longname, member.longname)) { member.overrides = parentDoclet.longname; } else { delete member.overrides; } // Add the ancestor's docs unless the descendant overrides the ancestor AND // documents the override. if (!Object.hasOwn(documented, member.longname)) { updateAddedDoclets(member, additions, additionIndexes); updateDocumentedDoclets(member, documented); updateMemberofDoclets(member, memberof); } // If the descendant used an @inheritdoc or @override tag, add the ancestor's // docs, and ignore the existing doclets. else if (explicitlyInherits(documented[member.longname])) { // Ignore any existing doclets. (This is safe because we only get here if // `member.longname` is an own property of `documented`.) addDocletProperty(documented[member.longname], 'ignore', true); updateAddedDoclets(member, additions, additionIndexes); updateDocumentedDoclets(member, documented); updateMemberofDoclets(member, memberof); // Remove property that's no longer accurate. if (member.virtual) { delete member.virtual; } // Remove properties that we no longer need. if (member.inheritdoc) { delete member.inheritdoc; } if (member.override) { delete member.override; } } // If the descendant overrides the ancestor and documents the override, // update the doclets to indicate what the descendant is overriding. else { addDocletProperty(documented[member.longname], 'overrides', parentDoclet.longname); } } } } } return additions; } function updateMixes(mixedDoclet, mixedLongname) { let idx; let mixedName; let names; // take the fast path if there's no array of mixed-in longnames if (!mixedDoclet.mixes) { mixedDoclet.mixes = [mixedLongname]; } else { // find the short name of the longname we're mixing in mixedName = toParts(mixedLongname).name; // find the short name of each previously mixed-in symbol // TODO: why do we run a map if we always shorten the same value? this looks like a bug... names = mixedDoclet.mixes.map(() => toParts(mixedDoclet.longname).name); // if we're mixing `myMethod` into `MixinC` from `MixinB`, and `MixinB` had the method mixed // in from `MixinA`, don't show `MixinA.myMethod` in the `mixes` list idx = names.indexOf(mixedName); if (idx !== -1) { mixedDoclet.mixes.splice(idx, 1); } mixedDoclet.mixes.push(mixedLongname); } } // TODO: try to reduce overlap with similar methods function getMixedInAdditions(mixinDoclets, allDoclets, { documented, memberof }) { let additionIndexes; const additions = []; const commentedDoclets = documented; let doclet; let mixedDoclet; let mixedDoclets; let mixes; // mixinDoclets will be undefined if the mixed-in symbol isn't documented mixinDoclets = mixinDoclets || []; for (let i = 0, ii = mixinDoclets.length; i < ii; i++) { doclet = mixinDoclets[i]; mixes = doclet.mixes; if (mixes) { // reset the lookup table of added doclet indexes by longname additionIndexes = {}; for (let j = 0, jj = mixes.length; j < jj; j++) { mixedDoclets = getMembers(mixes[j], allDoclets, ['static']); for (let k = 0, kk = mixedDoclets.length; k < kk; k++) { // We only care about symbols that are documented. if (mixedDoclets[k].undocumented) { continue; } mixedDoclet = new Doclet('', null, mixedDoclets[k].dependencies); mixedDoclet = combineDoclets(mixedDoclets[k], mixedDoclet); updateMixes(mixedDoclet, mixedDoclet.longname); mixedDoclet.mixed = true; reparentDoclet(doclet, mixedDoclet); // if we're mixing into a class, treat the mixed-in symbol as an instance member if (parentIsClass(doclet)) { staticToInstance(mixedDoclet); } updateAddedDoclets(mixedDoclet, additions, additionIndexes); updateDocumentedDoclets(mixedDoclet, commentedDoclets); updateMemberofDoclets(mixedDoclet, memberof); } } } } return additions; } function updateImplements(implDoclets, implementedLongname) { if (!Array.isArray(implDoclets)) { implDoclets = [implDoclets]; } implDoclets.forEach((implDoclet) => { implDoclet.implements ??= []; if (!implDoclet.implements.includes(implementedLongname)) { implDoclet.implements.push(implementedLongname); } }); } // TODO: try to reduce overlap with similar methods function getImplementedAdditions(implDoclets, allDoclets, { documented, memberof }) { let additionIndexes; const additions = []; let childDoclet; let childLongname; const commentedDoclets = documented; let doclet; let implementations; let implExists; let implementationDoclet; let interfaceDoclets; let parentDoclet; // interfaceDoclets will be undefined if the implemented symbol isn't documented implDoclets = implDoclets || []; for (let i = 0, ii = implDoclets.length; i < ii; i++) { doclet = implDoclets[i]; implementations = doclet.implements; if (implementations) { // reset the lookup table of added doclet indexes by longname additionIndexes = {}; for (let j = 0, jj = implementations.length; j < jj; j++) { interfaceDoclets = getMembers(implementations[j], allDoclets, ['instance']); for (let k = 0, kk = interfaceDoclets.length; k < kk; k++) { parentDoclet = interfaceDoclets[k]; // We only care about symbols that are documented. if (parentDoclet.undocumented) { continue; } childLongname = changeMemberof(parentDoclet.longname, doclet.longname); childDoclet = getDocumentedLongname(childLongname, allDoclets) || {}; // We don't want to fold in properties from the child doclet if it had an // `@inheritdoc` tag. if (Object.hasOwn(childDoclet, 'inheritdoc')) { childDoclet = {}; } implementationDoclet = combineDoclets(childDoclet, parentDoclet); reparentDoclet(doclet, implementationDoclet); updateImplements(implementationDoclet, parentDoclet.longname); // If there's no implementation, move along. implExists = Object.hasOwn(allDoclets.index.longname, implementationDoclet.longname); if (!implExists) { continue; } // Add the interface's docs unless the implementation is already documented. if (!Object.hasOwn(commentedDoclets, implementationDoclet.longname)) { updateAddedDoclets(implementationDoclet, additions, additionIndexes); updateDocumentedDoclets(implementationDoclet, commentedDoclets); updateMemberofDoclets(implementationDoclet, memberof); } // If the implementation used an @inheritdoc or @override tag, add the // interface's docs, and ignore the existing doclets. else if (explicitlyInherits(commentedDoclets[implementationDoclet.longname])) { // Ignore any existing doclets. (This is safe because we only get here if // `implementationDoclet.longname` is an own property of // `commentedDoclets`.) addDocletProperty(commentedDoclets[implementationDoclet.longname], 'ignore', true); updateAddedDoclets(implementationDoclet, additions, additionIndexes); updateDocumentedDoclets(implementationDoclet, commentedDoclets); updateMemberofDoclets(implementationDoclet, memberof); // Remove property that's no longer accurate. if (implementationDoclet.virtual) { delete implementationDoclet.virtual; } // Remove properties that we no longer need. if (implementationDoclet.inheritdoc) { delete implementationDoclet.inheritdoc; } if (implementationDoclet.override) { delete implementationDoclet.override; } } // If there's an implementation, and it's documented, update the doclets to // indicate what the implementation is implementing. else { updateImplements( commentedDoclets[implementationDoclet.longname], parentDoclet.longname ); } } } } } return additions; } function augment(doclets, propertyName, docletFinder, jsdocDeps) { const index = doclets.index.longname; const dependencies = sort(mapDependencies(index, propertyName)); dependencies.forEach((depName) => { const additions = docletFinder(index[depName], doclets, doclets.index, jsdocDeps); additions.forEach((addition) => { const longname = addition.longname; if (!Object.hasOwn(index, longname)) { index[longname] = []; } index[longname].push(addition); doclets.push(addition); }); }); } /** * Add doclets to reflect class inheritance. * * For example, if `ClassA` has the instance method `myMethod`, and `ClassB` inherits from `ClassA`, * calling this method creates a new doclet for `ClassB#myMethod`. * * @param {!Array.} doclets - The doclets generated by JSDoc. * @param {!Object} doclets.index - The doclet index. * @return {void} */ export function addInherited(doclets) { augment(doclets, 'augments', getInheritedAdditions); } /** * Add doclets to reflect mixins. When a symbol is mixed into a class, the class' version of the * mixed-in symbol is treated as an instance member. * * For example: * * + If `MixinA` has the static method `myMethod`, and `MixinB` mixes `MixinA`, calling this method * creates a new doclet for the static method `MixinB.myMethod`. * + If `MixinA` has the static method `myMethod`, and `ClassA` mixes `MixinA`, calling this method * creates a new doclet for the instance method `ClassA#myMethod`. * * @param {!Array.} doclets - The doclets generated by JSDoc. * @param {!Object} doclets.index - The doclet index. * @return {void} */ export function addMixedIn(doclets) { augment(doclets, 'mixes', getMixedInAdditions); } /** * Add and update doclets to reflect implementations of interfaces. * * For example, if `InterfaceA` has the instance method `myMethod`, and `ClassA` implements * `InterfaceA`, calling this method does the following: * * + Updates `InterfaceA` to indicate that it is implemented by `ClassA` * + Updates `InterfaceA#myMethod` to indicate that it is implemented by `ClassA#myMethod` * + Updates `ClassA#myMethod` to indicate that it implements `InterfaceA#myMethod` * * If `ClassA#myMethod` used the `@override` or `@inheritdoc` tag, calling this method would also * generate a new doclet that reflects the interface's documentation for `InterfaceA#myMethod`. * * @param {!Array.} docs - The doclets generated by JSDoc. * @param {!Object} doclets.index - The doclet index. * @return {void} */ export function addImplemented(doclets) { augment(doclets, 'implements', getImplementedAdditions); } /** * Add and update doclets to reflect all of the following: * * + Inherited classes * + Mixins * + Interface implementations * * Calling this method is equivalent to calling all other methods exported by this module. * * @return {void} */ export function augmentAll(doclets) { addMixedIn(doclets); addImplemented(doclets); addInherited(doclets); // look for implemented doclets again, in case we inherited an interface addImplemented(doclets); }