feat(jsdoc-doclet): add DocletStore, a reactive tracker for doclets

This commit is contained in:
Jeff Williams 2023-09-17 17:48:23 -07:00
parent 76ac64eaf2
commit b2df642b31
No known key found for this signature in database
11 changed files with 1740 additions and 59 deletions

View File

@ -16,8 +16,9 @@
import * as augment from './lib/augment.js';
import { resolveBorrows } from './lib/borrow.js';
import { combineDoclets, Doclet } from './lib/doclet.js';
import { DocletStore } from './lib/doclet-store.js';
import { Package } from './lib/package.js';
import * as schema from './lib/schema.js';
export { augment, combineDoclets, Doclet, Package, resolveBorrows, schema };
export default { augment, combineDoclets, Doclet, Package, resolveBorrows, schema };
export { augment, combineDoclets, Doclet, DocletStore, Package, resolveBorrows, schema };
export default { augment, combineDoclets, Doclet, DocletStore, Package, resolveBorrows, schema };

View File

@ -413,7 +413,7 @@ function updateImplements(implDoclets, implementedLongname) {
implDoclets.forEach((implDoclet) => {
implDoclet.implements ??= [];
if (!implDoclet.implements.includes(implementedLongname)) {
if (!implDoclet.implements?.includes(implementedLongname)) {
implDoclet.implements.push(implementedLongname);
}
});

View File

@ -0,0 +1,304 @@
/*
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.
*/
import { dirname, join } from 'node:path';
import commonPathPrefix from 'common-path-prefix';
import _ from 'lodash';
function addToSet(targetMap, key, value) {
if (!targetMap.has(key)) {
targetMap.set(key, new Set());
}
targetMap.get(key).add(value);
}
function diffArrays(value, previousValue = []) {
return {
added: _.difference(value, previousValue),
removed: _.difference(previousValue, value),
};
}
function getSourcePath({ meta }) {
return meta.path ? join(meta.path, meta.filename) : meta.filename;
}
function removeFromSet(targetMap, key, value) {
const set = targetMap.get(key);
if (set) {
set.delete(value);
// If the set is now empty, delete it from the map.
if (set.size === 0) {
targetMap.delete(key);
}
}
}
export class DocletStore {
#commonPathPrefix;
#docletChangedHandler;
#eventBus;
#newDocletHandler;
#sourcePaths;
static #propertiesWithMaps = ['kind', 'longname', 'memberof'];
static #propertyToMapName = new Map(
DocletStore.#propertiesWithMaps.map((prop) => {
return [prop, 'docletsBy' + _.capitalize(prop)];
})
);
static #propertiesWithSets = ['augments', 'borrowed', 'implements', 'mixes'];
static #propertyToSetName = new Map(
DocletStore.#propertiesWithSets.map((prop) => {
return [prop, 'docletsWith' + _.capitalize(prop)];
})
);
constructor(dependencies) {
this.#commonPathPrefix = null;
this.#eventBus = dependencies.get('eventBus');
this.#sourcePaths = new Map();
/** Doclets that are used to generate output. */
this.doclets = new Set();
/** @type Map<string, Set<Doclet>> */
this.docletsByKind = new Map();
/** @type Map<string, Set<Doclet>> */
this.docletsByLongname = new Map();
/** @type Map<string, Set<Doclet>> */
this.docletsByMemberof = new Map();
/** @type Map<string, Set<Doclet>> */
this.docletsByNodeId = new Map();
this.docletsWithAugments = new Set();
this.docletsWithBorrowed = new Set();
this.docletsWithImplements = new Set();
this.docletsWithMixes = new Set();
this.globals = new Set();
/** @type Map<string, Set<Doclet>> */
this.listenersByListensTo = new Map();
/** Doclets that aren't used to generate output. */
this.unusedDoclets = new Set();
this.#docletChangedHandler = (e) => this.#handleDocletChanged(e, {});
this.#newDocletHandler = (e) => this.#handleDocletChanged(e, { newDoclet: true });
this.#eventBus.on('docletChanged', this.#docletChangedHandler);
this.#eventBus.on('newDoclet', this.#newDocletHandler);
}
#handleDocletChanged({ doclet, property, oldValue, newValue }, opts) {
const isVisible = doclet.isVisible();
const newDoclet = opts.newDoclet ?? false;
const wasVisible = newDoclet ? false : this.doclets.has(doclet);
const visibilityChanged = (() => {
return newDoclet || (!wasVisible && isVisible) || (wasVisible && !isVisible);
})();
const docletInfo = {
isGlobal: doclet.isGlobal(),
isVisible,
newDoclet,
newValue,
oldValue,
setFnName: isVisible ? 'add' : 'delete',
visibilityChanged,
wasVisible,
};
if (newDoclet) {
this.#trackDocletByNodeId(doclet);
}
if (visibilityChanged) {
this.#toggleVisibility(doclet, docletInfo);
}
// In the following cases, there's nothing more to do:
//
// + The doclet isn't visible, and we're seeing it for the first time.
// + The doclet isn't visible, and its visibility didn't change.
if (!isVisible && (newDoclet || !visibilityChanged)) {
return;
}
// Update all watchable properties.
this.#updateWatchableProperties(doclet, property, docletInfo);
// Update list of source paths for visible doclets.
if (visibilityChanged) {
this.#updateSourcePaths(doclet, docletInfo);
}
}
#toggleGlobal(doclet, { isGlobal, isVisible }) {
if (isGlobal && isVisible) {
this.globals.add(doclet);
} else {
this.globals.delete(doclet);
}
}
#toggleVisibility(doclet, { isVisible, setFnName }) {
this.doclets[setFnName](doclet);
this.unusedDoclets[isVisible ? 'delete' : 'add'](doclet);
}
#trackDocletByNodeId(doclet) {
const nodeId = doclet.meta?.code?.node?.nodeId;
if (nodeId) {
addToSet(this.docletsByNodeId, nodeId, doclet);
}
}
#updateMapProperty(prop, oldKey, newKey, doclet, { isVisible, newDoclet, wasVisible }) {
const map = this[DocletStore.#propertyToMapName.get(prop)];
// For `newDoclet` events, there's no "new key"; just use the one from the doclet.
if (newDoclet) {
newKey = doclet[prop];
}
if (wasVisible && oldKey) {
removeFromSet(map, oldKey, doclet);
}
if (isVisible && newKey) {
addToSet(map, newKey, doclet);
}
}
#updateSetProperty(prop, value, setFnName) {
const set = this[DocletStore.#propertyToSetName.get(prop)];
if (Object.hasOwn(value, prop) && value[prop]?.length) {
set[setFnName](value);
} else {
set.delete(value);
}
}
#updateSourcePaths(doclet, { isVisible }) {
const sourcePath = getSourcePath(doclet);
if (!sourcePath || !isVisible) {
this.#sourcePaths.delete(doclet);
} else if (sourcePath) {
this.#sourcePaths.set(doclet, sourcePath);
}
// Invalidate the cached common prefix for source paths.
this.#commonPathPrefix = null;
}
#updateWatchableProperties(doclet, property, docletInfo) {
const {
isGlobal,
isVisible,
newDoclet,
newValue,
oldValue,
setFnName,
visibilityChanged,
wasVisible,
} = docletInfo;
// `access` only affects visibility, which is handled above, so we ignore it here.
if (visibilityChanged || property === 'augments') {
this.#updateSetProperty('augments', doclet, setFnName);
}
if (visibilityChanged || property === 'borrowed') {
this.#updateSetProperty('borrowed', doclet, setFnName);
}
// `ignore` only affects visibility, which is handled above, so we ignore it here.
if (visibilityChanged || property === 'implements') {
this.#updateSetProperty('implements', doclet, setFnName);
}
if (visibilityChanged || property === 'kind') {
this.#toggleGlobal(doclet, { isGlobal, isVisible });
this.#updateMapProperty('kind', oldValue, newValue, doclet, docletInfo);
}
if (visibilityChanged || property === 'listens') {
let added;
let diff;
let removed;
if (newDoclet) {
added = doclet.listens;
removed = [];
} else {
diff = diffArrays(newValue, oldValue);
added = diff.added;
removed = diff.removed;
}
if (added && isVisible) {
added.forEach((listensTo) => addToSet(this.listenersByListensTo, listensTo, doclet));
}
if (removed && wasVisible) {
removed.forEach((listensTo) => removeFromSet(this.listenersByListensTo, listensTo, doclet));
}
}
if (visibilityChanged || property === 'longname') {
this.#updateMapProperty('longname', oldValue, newValue, doclet, docletInfo);
}
if (visibilityChanged || property === 'memberof') {
this.#updateMapProperty('memberof', oldValue, newValue, doclet, docletInfo);
}
if (visibilityChanged || property === 'mixes') {
this.#updateSetProperty('mixes', doclet, setFnName);
}
if (visibilityChanged || property === 'scope') {
this.#toggleGlobal(doclet, { isGlobal, isVisible });
}
// `undocumented` only affects visibility, which is handled above, so we ignore it here.
}
_removeListeners() {
this.#eventBus.removeListener('docletChanged', this.#docletChangedHandler);
this.#eventBus.removeListener('newDoclet', this.#newDocletHandler);
}
get commonPathPrefix() {
let commonPrefix = '';
const sourcePaths = this.sourcePaths;
if (this.#commonPathPrefix !== null) {
return this.#commonPathPrefix;
}
if (sourcePaths.length === 1) {
// If there's only one filepath, then the common prefix is just its dirname.
commonPrefix = dirname(sourcePaths[0]);
} else if (sourcePaths.length > 1) {
// Remove the trailing slash if present.
commonPrefix = commonPathPrefix(sourcePaths).replace(/[\\/]$/, '');
}
this.#commonPathPrefix = commonPrefix;
return commonPrefix;
}
get longnames() {
return Array.from(this.docletsByLongname.keys());
}
get sourcePaths() {
return Array.from(this.#sourcePaths.values());
}
}

View File

@ -19,6 +19,7 @@ import { astNode, Syntax } from '@jsdoc/ast';
import { name as jsdocName } from '@jsdoc/core';
import { Tag } from '@jsdoc/tag';
import _ from 'lodash';
import onChange from 'on-change';
const {
applyNamespace,
@ -36,6 +37,7 @@ const {
const { isFunction } = astNode;
const ACCESS_LEVELS = ['package', 'private', 'protected', 'public'];
const ALL_SCOPE_NAMES = _.values(SCOPE.NAMES);
const DEFAULT_SCOPE = SCOPE.NAMES.STATIC;
// TODO: `class` should be on this list, right? What are the implications of adding it?
const GLOBAL_KINDS = ['constant', 'function', 'member', 'typedef'];
@ -55,6 +57,8 @@ export const WATCHABLE_PROPS = [
'undocumented',
];
WATCHABLE_PROPS.sort();
function fakeMeta(node) {
return {
type: node ? node.type : null,
@ -318,20 +322,22 @@ function getFilepath(doclet) {
return path.join(doclet.meta.path || '', doclet.meta.filename);
}
function emitDocletChanged(eventBus, doclet, property, oldValue, newValue) {
eventBus.emit('docletChanged', { doclet, property, oldValue, newValue });
}
function clone(source, target, properties) {
properties.forEach((property) => {
switch (typeof source[property]) {
case 'function':
// do nothing
break;
const sourceProperty = source[property];
case 'object':
target[property] = _.cloneDeep(source[property]);
break;
default:
target[property] = source[property];
if (_.isFunction(sourceProperty)) {
// Do nothing.
} else if (_.isArray(sourceProperty)) {
target[property] = sourceProperty.slice();
} else if (_.isObject(sourceProperty)) {
target[property] = _.cloneDeep(sourceProperty);
} else {
target[property] = sourceProperty;
}
});
}
@ -382,15 +388,26 @@ function copySpecificProperties(primary, secondary, target, include) {
});
}
function defineWatchableProp(doclet, prop) {
Object.defineProperty(doclet, prop, {
configurable: false,
enumerable: true,
get() {
return doclet.watchableProps[prop];
},
set(newValue) {
doclet.watchableProps[prop] = newValue;
},
});
}
/**
* Represents a single JSDoc comment.
*
* @alias module:@jsdoc/doclet.Doclet
*/
export class Doclet {
#accessConfig;
#dictionary;
#eventBus;
/**
* Create a doclet.
@ -400,57 +417,66 @@ export class Doclet {
* @param {object} dependencies - JSDoc dependencies.
*/
constructor(docletSrc, meta, dependencies) {
const accessConfig = dependencies.get('config')?.opts?.access?.slice() ?? [];
const eventBus = dependencies.get('eventBus');
const boundDefineWatchableProp = defineWatchableProp.bind(null, this);
const boundEmitDocletChanged = emitDocletChanged.bind(null, eventBus, this);
let newTags = [];
meta = meta || {};
this.#accessConfig = dependencies.get('config')?.opts?.access ?? [];
this.#dictionary = dependencies.get('tags');
this.#eventBus = dependencies.get('eventBus');
Object.defineProperty(this, 'accessConfig', {
value: accessConfig,
writable: true,
});
Object.defineProperty(this, 'dependencies', {
enumerable: false,
value: dependencies,
});
Object.defineProperty(this, 'watchableProps', {
enumerable: false,
value: {},
writable: true,
});
for (const prop of WATCHABLE_PROPS) {
Object.defineProperty(this, prop, {
enumerable: true,
get() {
return this.watchableProps[prop];
},
set(newValue) {
this.#setWatchableProperty(prop, newValue);
},
});
}
WATCHABLE_PROPS.forEach(boundDefineWatchableProp);
/** The original text of the comment from the source code. */
this.comment = docletSrc;
meta ??= {};
this.setMeta(meta);
docletSrc = unwrap(docletSrc);
docletSrc = fixDescription(docletSrc, meta);
docletSrc = fixDescription(unwrap(docletSrc), meta);
newTags = toTags.call(this, docletSrc);
for (let i = 0, l = newTags.length; i < l; i++) {
this.addTag(newTags[i].title, newTags[i].text);
}
this.postProcess();
}
#setWatchableProperty(name, newValue) {
const oldValue = this.watchableProps[name];
// Now that we've set the doclet's initial properties, listen for changes to those properties.
this.watchableProps = onChange(
this.watchableProps,
(propertyPath, newValue, oldValue) => {
let index;
let newArray;
let oldArray;
const property = propertyPath[0];
if (newValue !== oldValue) {
this.watchableProps[name] = newValue;
this.#eventBus.emit('docletChanged', { doclet: this, property: name, oldValue, newValue });
}
// Handle changes to arrays, like: `doclet.listens[0] = 'event:foo';`
if (propertyPath.length > 1) {
newArray = this.watchableProps[property].slice();
oldArray = newArray.slice();
// Update `oldArray` to contain the original value.
index = propertyPath[propertyPath.length - 1];
oldArray[index] = oldValue;
boundEmitDocletChanged(property, oldArray, newArray);
}
// Handle changes to primitive values.
else if (newValue !== oldValue) {
boundEmitDocletChanged(property, oldValue, newValue);
}
},
{ ignoreDetached: true, pathAsArray: true }
);
}
// TODO: We call this method in the constructor _and_ in `jsdoc/src/handlers`. It appears that
@ -521,7 +547,7 @@ export class Doclet {
* @returns {boolean} `true` if the doclet should be used to generate output; `false` otherwise.
*/
isVisible() {
const accessConfig = this.#accessConfig;
const accessConfig = this.accessConfig;
// By default, we don't use:
//
@ -569,10 +595,12 @@ export class Doclet {
* The fully resolved symbol name.
* @type {string}
*/
this.longname = removeGlobal(longname);
longname = removeGlobal(longname);
if (this.#dictionary.isNamespace(this.kind)) {
this.longname = applyNamespace(this.longname, this.kind);
longname = applyNamespace(longname, this.kind);
}
this.longname = longname;
}
/**
@ -600,14 +628,13 @@ export class Doclet {
setScope(scope) {
let errorMessage;
let filepath;
const scopeNames = _.values(SCOPE.NAMES);
if (!scopeNames.includes(scope)) {
if (!ALL_SCOPE_NAMES.includes(scope)) {
filepath = getFilepath(this);
errorMessage =
`The scope name "${scope}" is not recognized. Use one of the ` +
`following values: ${scopeNames}`;
`following values: ${ALL_SCOPE_NAMES}`;
if (filepath) {
errorMessage += ` (Source file: ${filepath})`;
}

View File

@ -9,7 +9,12 @@
"version": "0.2.4",
"license": "Apache-2.0",
"dependencies": {
"@jsdoc/ast": "^0.2.4",
"@jsdoc/core": "^0.5.4",
"@jsdoc/tag": "^0.2.4",
"@jsdoc/util": "^0.3.0",
"lodash": "^4.17.21",
"on-change": "^4.0.2",
"strip-bom": "^5.0.0"
}
},
@ -76,10 +81,417 @@
"node": ">=v18.12.0"
}
},
"node_modules/@babel/code-frame": {
"version": "7.22.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
"integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
"dependencies": {
"@babel/highlight": "^7.22.13",
"chalk": "^2.4.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
"integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.22.16",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz",
"integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==",
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jsdoc/ast": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@jsdoc/ast/-/ast-0.2.4.tgz",
"integrity": "sha512-PZSf4ivb7SWEaPgJwXYHjYlH/UxbgMJ5inZ1VHIDS+/w7nDKocnqoknhK7oliupWqIoBkT+KkxA2jHwl/43TRQ==",
"dependencies": {
"@babel/parser": "^7.22.16",
"@jsdoc/core": "^0.5.4",
"@jsdoc/util": "^0.3.0",
"lodash": "^4.17.21"
},
"engines": {
"node": ">=v18.12.0"
}
},
"node_modules/@jsdoc/core": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/@jsdoc/core/-/core-0.5.4.tgz",
"integrity": "sha512-bLqJP6fKGSvgqE41QSdShIe00oaSgkHRGStDjOBXQhWTTqVspX8tp6RERqcixwEesW147gRHFEMfJK/zyGd+bA==",
"dependencies": {
"bottlejs": "^2.0.1",
"cosmiconfig": "^8.3.6",
"escape-string-regexp": "^5.0.0",
"lodash": "^4.17.21",
"strip-bom": "^5.0.0",
"strip-json-comments": "^5.0.1"
},
"engines": {
"node": ">=v18.12.0"
}
},
"node_modules/@jsdoc/tag": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@jsdoc/tag/-/tag-0.2.4.tgz",
"integrity": "sha512-OEwYdcD+FSKln+XWX6JAGtrQWSrbkAvyKOCknXXorBzNwC/LPmJSYpXYDPIwy80RmMxnsq/jXmfscMeoKrPimA==",
"dependencies": {
"@jsdoc/ast": "^0.2.4",
"@jsdoc/core": "^0.5.4",
"@jsdoc/util": "^0.3.0",
"catharsis": "^0.9.0",
"common-path-prefix": "^3.0.0",
"lodash": "^4.17.21"
},
"engines": {
"node": ">=v18.12.0"
}
},
"node_modules/@jsdoc/util": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@jsdoc/util/-/util-0.3.0.tgz",
"integrity": "sha512-l9uSPSKGvul8TAEvahtLHW5cY9VF9ocygEc7cfWHEkopO0QPlgcDagqY8jm93pzsDSM6ITdfJ9OfkfvLlts7PQ==",
"dependencies": {
"klaw-sync": "^6.0.0",
"lodash": "^4.17.21",
"ow": "^1.1.1"
},
"engines": {
"node": ">=v18.12.0"
}
},
"node_modules/@sindresorhus/is": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz",
"integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/bottlejs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/bottlejs/-/bottlejs-2.0.1.tgz",
"integrity": "sha512-50T0bzqeAqZ+//kgjdDxNu7UP8Je04isNPyHPwwOOPoeZmtVESkuF9nwkWEqSEd9Sw1yJ1oaoHBAMxe/wG4Zzg=="
},
"node_modules/callsites": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-4.1.0.tgz",
"integrity": "sha512-aBMbD1Xxay75ViYezwT40aQONfr+pSXTHwNKvIXhXD6+LY3F1dLIcceoC5OZKBVHbXcysz1hL9D2w0JJIMXpUw==",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/catharsis": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz",
"integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==",
"dependencies": {
"lodash": "^4.17.15"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/chalk/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/common-path-prefix": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
"integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w=="
},
"node_modules/cosmiconfig": {
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
"dependencies": {
"import-fresh": "^3.3.0",
"js-yaml": "^4.1.0",
"parse-json": "^5.2.0",
"path-type": "^4.0.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/d-fischer"
},
"peerDependencies": {
"typescript": ">=4.9.5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/dot-prop": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-7.2.0.tgz",
"integrity": "sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==",
"dependencies": {
"type-fest": "^2.11.2"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"engines": {
"node": ">=4"
}
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/klaw-sync": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
"dependencies": {
"graceful-fs": "^4.1.11"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/lodash": {
"version": "4.17.21",
"license": "MIT"
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"node_modules/on-change": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/on-change/-/on-change-4.0.2.tgz",
"integrity": "sha512-cMtCyuJmTx/bg2HCpHo3ZLeF7FZnBOapLqZHr2AlLeJ5Ul0Zu2mUJJz051Fdwu/Et2YW04ZD+TtU+gVy0ACNCA==",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sindresorhus/on-change?sponsor=1"
}
},
"node_modules/ow": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ow/-/ow-1.1.1.tgz",
"integrity": "sha512-sJBRCbS5vh1Jp9EOgwp1Ws3c16lJrUkJYlvWTYC03oyiYVwS/ns7lKRWow4w4XjDyTrA2pplQv4B2naWSR6yDA==",
"dependencies": {
"@sindresorhus/is": "^5.3.0",
"callsites": "^4.0.0",
"dot-prop": "^7.2.0",
"lodash.isequal": "^4.5.0",
"vali-date": "^1.0.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dependencies": {
"callsites": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/parent-module/node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"engines": {
"node": ">=8"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"engines": {
"node": ">=4"
}
},
"node_modules/strip-bom": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-5.0.0.tgz",
@ -90,6 +502,47 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-json-comments": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.1.tgz",
"integrity": "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/vali-date": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
"integrity": "sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==",
"engines": {
"node": ">=0.10.0"
}
}
}
}

View File

@ -36,6 +36,7 @@
"@jsdoc/tag": "^0.2.4",
"@jsdoc/util": "^0.3.0",
"lodash": "^4.17.21",
"on-change": "^4.0.2",
"strip-bom": "^5.0.0"
}
}

View File

@ -17,6 +17,7 @@ import doclet from '../../index.js';
import * as augment from '../../lib/augment.js';
import { resolveBorrows } from '../../lib/borrow.js';
import { combineDoclets, Doclet } from '../../lib/doclet.js';
import { DocletStore } from '../../lib/doclet-store.js';
import { Package } from '../../lib/package.js';
import * as schema from '../../lib/schema.js';
@ -43,6 +44,12 @@ describe('@jsdoc/doclet', () => {
});
});
describe('DocletStore', () => {
it('is lib/doclet.DocletStore', () => {
expect(doclet.DocletStore).toEqual(DocletStore);
});
});
describe('Package', () => {
it('is lib/package.Package', () => {
expect(doclet.Package).toEqual(Package);

View File

@ -0,0 +1,875 @@
/*
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.
*/
/* global jsdoc */
import { Doclet } from '../../../lib/doclet.js';
import * as docletStore from '../../../lib/doclet-store.js';
const { DocletStore } = docletStore;
function makeDoclet(comment, meta, deps) {
let doclet;
deps ??= jsdoc.deps;
doclet = new Doclet(`/**\n${comment.join('\n')}\n*/`, meta, deps);
deps.get('eventBus').emit('newDoclet', { doclet });
return doclet;
}
describe('@jsdoc/doclet/lib/doclet-store', () => {
it('exists', () => {
expect(docletStore).toBeObject();
});
it('has a DocletStore class', () => {
expect(docletStore.DocletStore).toBeFunction();
});
describe('DocletStore', () => {
let store;
beforeEach(() => {
store = new DocletStore(jsdoc.deps);
});
afterEach(() => {
store._removeListeners();
});
it('is not constructable with no arguments', () => {
expect(() => new DocletStore()).toThrow();
});
it('is constructable when dependencies are passed in', () => {
expect(() => new DocletStore(jsdoc.deps)).not.toThrow();
});
it('has a `commonPathPrefix` property', () => {
expect(store.commonPathPrefix).toBeEmptyString();
});
it('has a `doclets` property', () => {
expect(store.doclets).toBeEmptySet();
});
it('has a `docletsByKind` property', () => {
expect(store.docletsByKind).toBeEmptyMap();
});
it('has a `docletsByLongname` property', () => {
expect(store.docletsByLongname).toBeEmptyMap();
});
it('has a `docletsByMemberof` property', () => {
expect(store.docletsByMemberof).toBeEmptyMap();
});
it('has a `docletsWithBorrowed` property', () => {
expect(store.docletsWithBorrowed).toBeEmptySet();
});
it('has a `globals` property', () => {
expect(store.globals).toBeEmptySet();
});
it('has a `listenersByListensTo` property', () => {
expect(store.listenersByListensTo).toBeEmptyMap();
});
it('has a `longnames` property', () => {
expect(store.longnames).toBeEmptyArray();
});
it('has a `sourcePaths` property', () => {
expect(store.sourcePaths).toBeEmptyArray();
});
it('has an `unusedDoclets` property', () => {
expect(store.unusedDoclets).toBeEmptySet();
});
describe('add', () => {
describe('visible doclets', () => {
let doclet;
it('adds the doclet to the list of visible doclets when appropriate', () => {
doclet = makeDoclet(['@class', '@memberof foo', '@name Bar']);
expect(doclet.isVisible()).toBeTrue();
expect(store.doclets).toHave(doclet);
});
it('never adds visible doclets to the list of unused doclets', () => {
doclet = makeDoclet(['@class', '@memberof foo', '@name Bar']);
expect(doclet.isVisible()).toBeTrue();
expect(store.unusedDoclets).toBeEmptySet();
});
it('adds visible doclets to the map of doclets by kind', () => {
doclet = makeDoclet(['@class', '@memberof foo', '@name Bar']);
expect(doclet.isVisible()).toBeTrue();
expect(store.docletsByKind.get('class')).toHave(doclet);
});
it('adds visible doclets to the map of doclets by longname', () => {
doclet = makeDoclet(['@class', '@memberof foo', '@name Bar']);
expect(doclet.isVisible()).toBeTrue();
expect(store.docletsByLongname.get('foo.Bar')).toHave(doclet);
});
it('adds visible doclets to the map of doclets by memberof when appropriate', () => {
doclet = makeDoclet(['@class', '@memberof foo', '@name Bar']);
expect(doclet.isVisible()).toBeTrue();
expect(store.docletsByMemberof.get('foo')).toHave(doclet);
});
it('adds doclets that augment others to the list of doclets that augment', () => {
doclet = makeDoclet(['@class', '@name Qux', '@extends foo.Bar']);
expect(store.docletsWithAugments).toHave(doclet);
});
it('does not add doclets that do not augment others to the list of doclets that augment', () => {
doclet = makeDoclet(['@class', '@name Qux']);
expect(store.docletsWithAugments).not.toHave(doclet);
});
it('adds doclets that borrow others to the list of doclets that borrow', () => {
doclet = makeDoclet(['@class', '@name Qux', '@borrows foo.Bar#baz']);
expect(store.docletsWithBorrowed).toHave(doclet);
});
it('does not add doclets that do not borrow others to the list of doclets that borrow', () => {
doclet = makeDoclet(['@class', '@name Qux']);
expect(store.docletsWithBorrowed).not.toHave(doclet);
});
it('adds doclets that implement others to the list of doclets that implement', () => {
doclet = makeDoclet(['@class', '@name Qux', '@implements IQux']);
expect(store.docletsWithImplements).toHave(doclet);
});
it('does not add doclets that do not implement others to the list of doclets that implement', () => {
doclet = makeDoclet(['@class', '@name Qux']);
expect(store.docletsWithImplements).not.toHave(doclet);
});
it('adds doclets that mix others to the list of doclets that mix', () => {
doclet = makeDoclet(['@class', '@name Qux', '@mixes foo']);
expect(store.docletsWithMixes).toHave(doclet);
});
it('does not add doclets that do not mix others to the list of doclets that mix', () => {
doclet = makeDoclet(['@class', '@name Qux']);
expect(store.docletsWithMixes).not.toHave(doclet);
});
it('adds visible doclets to the list of globals when appropriate', () => {
doclet = makeDoclet(['@function', '@global', '@name baz']);
expect(doclet.isVisible()).toBeTrue();
expect(store.globals).toHave(doclet);
});
it('does not add doclets that are global, but not visible, to the list of globals', () => {
doclet = makeDoclet(['@function', '@global', '@name baz', '@ignore']);
expect(doclet.isVisible()).toBeFalse();
expect(store.globals).toBeEmptySet();
});
it('adds visible doclets to the map of listeners by listens to when appropriate', () => {
doclet = makeDoclet(['@function', '@listens event:bar', '@name foo']);
expect(doclet.isVisible()).toBeTrue();
expect(store.listenersByListensTo.get('event:bar')).toHave(doclet);
});
it('adds the source paths for visible doclets to the list of source paths', () => {
const meta = {
filename: '/Users/carolr/code/foo.js',
lineno: 1,
};
doclet = makeDoclet(['@function', '@global', '@name baz'], meta);
expect(doclet.isVisible()).toBeTrue();
expect(store.sourcePaths).toContain('/Users/carolr/code/foo.js');
});
});
describe('unused doclets', () => {
let doclet;
it('adds the doclet to the list of unused doclets when appropriate', () => {
doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@ignore']);
expect(doclet.isVisible()).toBeFalse();
expect(store.unusedDoclets).toHave(doclet);
});
it('does not add unused doclets to the list of visible doclets', () => {
doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@ignore']);
expect(doclet.isVisible()).toBeFalse();
expect(store.doclets).toBeEmptySet();
});
it('does not add unused doclets to the map of doclets by kind', () => {
doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@ignore']);
expect(doclet.isVisible()).toBeFalse();
expect(store.docletsByKind).toBeEmptyMap();
});
it('does not add unused doclets to the map of doclets by longname', () => {
doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@ignore']);
expect(doclet.isVisible()).toBeFalse();
expect(store.docletsByLongname).toBeEmptyMap();
});
it('does not add unused doclets to the map of doclets by memberof', () => {
doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@ignore']);
expect(doclet.isVisible()).toBeFalse();
expect(store.docletsByMemberof).toBeEmptyMap();
});
it('does not add doclets that augment others to the list of doclets that augment', () => {
doclet = makeDoclet(['@class', '@name Qux', '@extends foo.Bar', '@ignore']);
expect(doclet.isVisible()).toBeFalse();
expect(store.docletsWithAugments).toBeEmptySet();
});
it('does not add doclets that borrow others to the list of doclets that borrow', () => {
doclet = makeDoclet(['@class', '@name Qux', '@borrows foo.Bar#baz', '@ignore']);
expect(doclet.isVisible()).toBeFalse();
expect(store.docletsWithBorrowed).toBeEmptySet();
});
it('does not add doclets that implement others to the list of doclets that implement', () => {
doclet = makeDoclet(['@class', '@name Qux', '@implements IQux', '@ignore']);
expect(doclet.isVisible()).toBeFalse();
expect(store.docletsWithImplements).toBeEmptySet();
});
it('does not add doclets that mix others to the list of doclets that mix', () => {
doclet = makeDoclet(['@class', '@name Qux', '@mixes foo', '@ignore']);
expect(doclet.isVisible()).toBeFalse();
expect(store.docletsWithMixes).toBeEmptySet();
});
it('does not add unused doclets to the list of globals', () => {
doclet = makeDoclet(['@function', '@global', '@name baz', '@ignore']);
expect(doclet.isVisible()).toBeFalse();
expect(store.globals).toBeEmptySet();
});
it('does not add unused doclets to the map of listeners by listens to', () => {
doclet = makeDoclet(['@function', '@listens event:bar', '@name foo', '@ignore']);
expect(doclet.isVisible()).toBeFalse();
expect(store.listenersByListensTo).toBeEmptyMap();
});
it('does not add longnames for unused doclets to the list of longnames', () => {
expect(store.longnames).toBeEmptyArray();
});
it('does not add source paths for unused doclets to the list of source paths', () => {
const meta = {
filename: '/Users/carolr/code/foo.js',
lineno: 1,
};
doclet = makeDoclet(['@function', '@global', '@name baz', '@ignore'], meta);
expect(doclet.isVisible()).toBeFalse();
expect(store.sourcePaths).toBeEmptyArray();
});
});
});
describe('commonPathPrefix', () => {
it('returns an empty string if there are no paths', () => {
expect(store.commonPathPrefix).toBeEmptyString();
});
it('returns the dirname if there is only one path', () => {
const meta = {
filename: '/Users/carolr/code/foo.js',
lineno: 1,
};
makeDoclet(['@function', '@global', '@name baz'], meta);
expect(store.commonPathPrefix).toBe('/Users/carolr/code');
});
// Here for the sake of code coverage; it tests the path where the value is already cached.
it('works twice in a row', () => {
const meta = {
filename: '/Users/carolr/code/foo.js',
lineno: 1,
};
makeDoclet(['@function', '@global', '@name baz'], meta);
expect(store.commonPathPrefix).toBe('/Users/carolr/code');
expect(store.commonPathPrefix).toBe('/Users/carolr/code');
});
it('returns the correct common path if one exists', () => {
const metaA = {
filename: '/Users/carolr/code/foo.js',
lineno: 1,
};
const metaB = {
filename: '/Users/carolr/assets/bar.js',
lineno: 1,
};
makeDoclet(['@function', '@global', '@name foo'], metaA);
makeDoclet(['@function', '@global', '@name bar'], metaB);
expect(store.commonPathPrefix).toBe('/Users/carolr');
});
it('returns an empty string if there is no common path', () => {
const metaA = {
filename: 'C:\\carolr\\code\\foo.js',
lineno: 1,
};
const metaB = {
filename: 'D:\\code\\bar.js',
lineno: 1,
};
makeDoclet(['@function', '@global', '@name foo'], metaA);
makeDoclet(['@function', '@global', '@name bar'], metaB);
expect(store.commonPathPrefix).toBeEmptyString();
});
it('returns the correct value if you get the property, then add a path that does not change the result', () => {
const metaA = {
filename: '/Users/carolr/code/foo.js',
lineno: 1,
};
const metaB = {
filename: '/Users/carolr/code/bar.js',
lineno: 1,
};
let prefix1;
let prefix2;
makeDoclet(['@function', '@global', '@name foo'], metaA);
prefix1 = store.commonPathPrefix;
makeDoclet(['@function', '@global', '@name bar'], metaB);
prefix2 = store.commonPathPrefix;
expect(prefix1).toBe('/Users/carolr/code');
expect(prefix2).toBe('/Users/carolr/code');
});
it('returns the correct value if you get the property, then add a path that changes the result', () => {
const metaA = {
filename: '/Users/carolr/code/foo.js',
lineno: 1,
};
const metaB = {
filename: '/Users/carolr/bar.js',
lineno: 1,
};
let prefix1;
let prefix2;
makeDoclet(['@function', '@global', '@name foo'], metaA);
prefix1 = store.commonPathPrefix;
makeDoclet(['@function', '@global', '@name bar'], metaB);
prefix2 = store.commonPathPrefix;
expect(prefix1).toBe('/Users/carolr/code');
expect(prefix2).toBe('/Users/carolr');
});
});
describe('longnames', () => {
it('is an empty array if there are no doclets', () => {
expect(store.longnames).toBeEmptyArray();
});
it('is a list of longnames if there are doclets', () => {
makeDoclet(['@class', '@memberof foo', '@name Bar']);
expect(store.longnames).toEqual(['foo.Bar']);
});
});
describe('reactivity', () => {
describe('`access`', () => {
it('marks a public doclet as unused if it becomes private', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar']);
expect(store.unusedDoclets).not.toHave(doclet);
doclet.access = 'private';
expect(store.unusedDoclets).toHave(doclet);
});
it('marks a private doclet as visible if it becomes public', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@private']);
expect(store.unusedDoclets).toHave(doclet);
doclet.access = 'public';
expect(store.unusedDoclets).not.toHave(doclet);
});
});
describe('`augments`', () => {
it('adds a doclet to the list of doclets that augment others when the doclet gains an `augments` value', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar']);
expect(store.docletsWithAugments).not.toHave(doclet);
doclet.augments = ['Baz'];
expect(store.docletsWithAugments).toHave(doclet);
});
it('removes a doclet from the list of doclets that augment others when the doclet loses all `augments` values', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@extends Baz']);
expect(store.docletsWithAugments).toHave(doclet);
doclet.augments.pop();
expect(store.docletsWithAugments).not.toHave(doclet);
});
it('removes a doclet from the list of doclets that augment others when the doclet loses its `augments` property', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@extends Baz']);
expect(store.docletsWithAugments).toHave(doclet);
doclet.augments = undefined;
expect(store.docletsWithAugments).not.toHave(doclet);
});
it('does nothing when a doclet augmented A, but it now augments B instead', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@extends Baz']);
expect(store.docletsWithAugments).toHave(doclet);
doclet.augments[0] = 'Qux';
expect(store.docletsWithAugments).toHave(doclet);
});
});
describe('`borrowed`', () => {
it('adds a doclet to the list of doclets that borrow others when the doclet gains a `borrowed` value', () => {
const doclet = makeDoclet(['@function', '@memberof Foo', '@name bar', '@instance']);
expect(store.docletsWithBorrowed).not.toHave(doclet);
doclet.borrowed = [{ from: 'Baz#bar' }];
expect(store.docletsWithBorrowed).toHave(doclet);
});
it('removes a doclet from the list of doclets that borrow others when the doclet loses all `borrowed` values', () => {
const doclet = makeDoclet([
'@function',
'@memberof Foo',
'@name bar',
'@instance',
'@borrows Baz#bar',
]);
expect(store.docletsWithBorrowed).toHave(doclet);
doclet.borrowed.pop();
expect(store.docletsWithBorrowed).not.toHave(doclet);
});
it('removes a doclet from the list of doclets that borrow others when the doclet loses its `borrowed` property', () => {
const doclet = makeDoclet([
'@function',
'@memberof Foo',
'@name bar',
'@instance',
'@borrows Baz#bar',
]);
expect(store.docletsWithBorrowed).toHave(doclet);
doclet.borrowed = undefined;
expect(store.docletsWithBorrowed).not.toHave(doclet);
});
it('does nothing when a doclet borrowed A, but it now borrows B instead', () => {
const doclet = makeDoclet([
'@function',
'@memberof Foo',
'@name bar',
'@instance',
'@borrows Baz#bar',
]);
expect(store.docletsWithBorrowed).toHave(doclet);
doclet.borrowed[0] = { from: 'Qux#bar' };
expect(store.docletsWithBorrowed).toContain(doclet);
});
});
describe('`ignore`', () => {
it('marks the doclet as unused when added', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar']);
expect(store.doclets).toHave(doclet);
expect(store.unusedDoclets).not.toHave(doclet);
doclet.ignore = true;
expect(store.doclets).not.toHave(doclet);
expect(store.unusedDoclets).toHave(doclet);
});
it('marks the doclet as visible when removed', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@ignore']);
expect(store.doclets).not.toHave(doclet);
expect(store.unusedDoclets).toHave(doclet);
doclet.ignore = undefined;
expect(store.doclets).toHave(doclet);
expect(store.unusedDoclets).not.toHave(doclet);
});
});
describe('`implements`', () => {
it('adds a doclet to the list of doclets that implement others when the doclet gains an `implements` value', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar']);
expect(store.docletsWithImplements).not.toHave(doclet);
doclet.implements = ['IBar'];
expect(store.docletsWithImplements).toHave(doclet);
});
it('removes a doclet from the list of doclets that implement others when the doclet loses all `implements` values', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@implements IBar']);
expect(store.docletsWithImplements).toHave(doclet);
doclet.implements.pop();
expect(store.docletsWithImplements).not.toHave(doclet);
});
it('removes a doclet from the list of doclets that implement others when the doclet loses its `implements` property', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@implements IBar']);
expect(store.docletsWithImplements).toHave(doclet);
doclet.implements = undefined;
expect(store.docletsWithImplements).not.toHave(doclet);
});
it('does nothing when a doclet implemented A, but it now implements B instead', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@implements IBar']);
expect(store.docletsWithImplements).toHave(doclet);
doclet.implements[0] = 'IBaz';
expect(store.docletsWithImplements).toHave(doclet);
});
});
describe('`kind`', () => {
it('adds a doclet to the list of doclets with that `kind`', () => {
const doclet = makeDoclet(['@function', '@name foo']);
expect(store.docletsByKind.get('function')).toHave(doclet);
});
it('removes a doclet from the list of doclets for a kind when the kind changes', () => {
const doclet = makeDoclet(['@function', '@name foo']);
expect(store.docletsByKind.get('constant')).not.toHave(doclet);
expect(store.docletsByKind.get('function')).toHave(doclet);
doclet.kind = 'constant';
expect(store.docletsByKind.get('constant')).toHave(doclet);
expect(store.docletsByKind.get('function')).not.toHave(doclet);
});
// Doclets should always have a `kind`, so this test is just for completeness.
it('removes a doclet from the list of doclets for a kind when the doclet loses its kind', () => {
const doclet = makeDoclet(['@function', '@name foo']);
expect(store.docletsByKind.get('function')).toHave(doclet);
doclet.kind = 'undefined';
expect(store.docletsByKind.get('function')).not.toHave(doclet);
});
it('adds a doclet to the list of global doclets when it gains a `kind` that can be global', () => {
const doclet = makeDoclet(['@class', '@name foo', '@global']);
expect(store.globals).toBeEmptySet();
doclet.kind = 'function';
expect(store.globals).toHave(doclet);
});
it('removes a doclet from the list of global doclets when its new `kind` cannot be global', () => {
const doclet = makeDoclet(['@function', '@name foo', '@global']);
expect(store.globals).toHave(doclet);
doclet.kind = 'class';
expect(store.globals).toBeEmptySet();
});
});
describe('`listens`', () => {
it('adds a doclet to the list of listeners when the doclet gains a `listens` value', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar']);
expect(store.listenersByListensTo.get('event:baz')).toBeUndefined();
doclet.listens = ['event:baz'];
expect(store.listenersByListensTo.get('event:baz')).toHave(doclet);
});
it('removes a doclet from the list of listeners when the doclet loses all `listens` values', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@listens event:baz']);
expect(store.listenersByListensTo.get('event:baz')).toHave(doclet);
doclet.listens.pop();
expect(store.listenersByListensTo.get('event:baz')).toBeUndefined();
});
it('removes a doclet from the list of listeners when the doclet loses its `listens` property', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@listens event:baz']);
expect(store.listenersByListensTo.get('event:baz')).toHave(doclet);
doclet.listens = undefined;
expect(store.listenersByListensTo.get('event:baz')).toBeUndefined();
});
it('changes the longname that a doclet listens to when needed', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@listens event:baz']);
expect(store.listenersByListensTo.get('event:baz')).toHave(doclet);
expect(store.listenersByListensTo.get('event:qux')).toBeUndefined();
global.lorgg = true;
doclet.listens[0] = 'event:qux';
global.lorgg = false;
expect(store.listenersByListensTo.get('event:baz')).toBeUndefined();
expect(store.listenersByListensTo.get('event:qux')).toHave(doclet);
});
});
describe('`longname`', () => {
it('tracks a doclet by its new longname after it changes', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar']);
expect(store.docletsByLongname.get('foo.Bar')).toHave(doclet);
expect(store.docletsByLongname.get('zoo.Bar')).toBeUndefined();
doclet.memberof = 'zoo';
doclet.longname = 'zoo.Bar';
expect(store.docletsByLongname.get('foo.Bar')).toBeUndefined();
expect(store.docletsByLongname.get('zoo.Bar')).toHave(doclet);
});
});
describe('`memberof`', () => {
it('tracks a doclet by its new memberof after it changes', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar']);
expect(store.docletsByMemberof.get('foo')).toHave(doclet);
expect(store.docletsByMemberof.get('zoo')).toBeUndefined();
doclet.memberof = 'zoo';
doclet.longname = 'zoo.Bar';
expect(store.docletsByMemberof.get('foo')).toBeUndefined();
expect(store.docletsByMemberof.get('zoo')).toHave(doclet);
});
});
describe('`mixes`', () => {
it('adds a doclet to the list of doclets that mix in others when the doclet gains a `mixes` value', () => {
const doclet = makeDoclet(['@class', '@name Foo']);
expect(store.docletsWithMixes).not.toHave(doclet);
doclet.mixes = ['bar'];
expect(store.docletsWithMixes).toHave(doclet);
});
it('removes a doclet from the list of doclets that mix in others when the doclet loses all `mixes` values', () => {
const doclet = makeDoclet(['@class', '@name Foo', '@mixes bar']);
expect(store.docletsWithMixes).toHave(doclet);
doclet.mixes.pop();
expect(store.docletsWithMixes).not.toHave(doclet);
});
it('removes a doclet from the list of doclets that mix in others when the doclet loses its `mixes` property', () => {
const doclet = makeDoclet(['@class', '@name Foo', '@mixes bar']);
expect(store.docletsWithMixes).toHave(doclet);
doclet.mixes = undefined;
expect(store.docletsWithMixes).not.toHave(doclet);
});
it('does nothing when a doclet mixed in A, but it now mixes in B instead', () => {
const doclet = makeDoclet(['@class', '@name Foo', '@mixes bar']);
expect(store.docletsWithMixes).toHave(doclet);
doclet.mixes[0] = 'baz';
expect(store.docletsWithMixes).toContain(doclet);
});
});
describe('`scope`', () => {
it('adds a doclet to the list of global doclets when it gains global scope', () => {
const doclet = makeDoclet(['@function', '@name foo']);
expect(store.globals).toBeEmptySet();
doclet.scope = 'global';
expect(store.globals).toHave(doclet);
});
it('removes a doclet from the list of global doclets when it loses global scope', () => {
const doclet = makeDoclet(['@function', '@name foo', '@global']);
expect(store.globals).toHave(doclet);
doclet.scope = 'static';
expect(store.globals).toBeEmptySet();
});
});
describe('`undocumented`', () => {
it('marks the doclet as unused when added', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar']);
expect(store.doclets).toHave(doclet);
expect(store.unusedDoclets).not.toHave(doclet);
doclet.undocumented = true;
expect(store.doclets).not.toHave(doclet);
expect(store.unusedDoclets).toHave(doclet);
});
it('marks the doclet as visible when removed', () => {
const doclet = makeDoclet(['@class', '@memberof foo', '@name Bar', '@undocumented']);
expect(store.doclets).not.toHave(doclet);
expect(store.unusedDoclets).toHave(doclet);
doclet.undocumented = undefined;
expect(store.doclets).toHave(doclet);
expect(store.unusedDoclets).not.toHave(doclet);
});
});
});
describe('sourcePaths', () => {
it('is an empty array if there are no visible doclets', () => {
expect(store.sourcePaths).toBeEmptyArray();
});
it('is a list of source paths if there are visible doclets', () => {
const metaA = {
filename: '/Users/carolr/code/foo.js',
lineno: 1,
};
const metaB = {
filename: '/Users/carolr/code/bar.js',
lineno: 1,
};
makeDoclet(['@function', '@name baz'], metaA);
makeDoclet(['@function', '@name qux'], metaB);
expect(store.sourcePaths).toEqual([
'/Users/carolr/code/foo.js',
'/Users/carolr/code/bar.js',
]);
});
});
});
});

View File

@ -44,6 +44,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
describe('combineDoclets', () => {
it('overrides most properties of the secondary doclet', () => {
let descriptors;
const primaryDoclet = new Doclet(
'/** New and improved!\n@version 2.0.0 */',
null,
@ -52,7 +53,12 @@ describe('@jsdoc/doclet/lib/doclet', () => {
const secondaryDoclet = new Doclet('/** Hello!\n@version 1.0.0 */', null, jsdoc.deps);
const newDoclet = doclet.combineDoclets(primaryDoclet, secondaryDoclet);
Object.getOwnPropertyNames(newDoclet).forEach((property) => {
descriptors = Object.getOwnPropertyDescriptors(newDoclet);
Object.keys(descriptors).forEach((property) => {
if (!descriptors[property].enumerable) {
return;
}
expect(newDoclet[property]).toEqual(primaryDoclet[property]);
});
});
@ -514,15 +520,15 @@ describe('@jsdoc/doclet/lib/doclet', () => {
it('sends events to the event bus when watchable properties change', () => {
const propValues = {
access: 'private',
augments: 'Foo',
augments: ['Foo'],
borrowed: true,
ignore: true,
implements: 'Foo',
implements: ['Foo'],
kind: 'class',
listens: 'event:foo',
listens: ['event:foo'],
longname: 'foo',
memberof: 'foo',
mixes: 'foo',
mixes: ['foo'],
scope: 'static',
undocumented: true,
};
@ -553,12 +559,12 @@ describe('@jsdoc/doclet/lib/doclet', () => {
} else {
expect(events[0].oldValue).toBeUndefined();
}
expect(events[0].newValue).toBe(propValues[key]);
expect(events[0].newValue).toEqual(propValues[key]);
expect(events[1]).toBeObject();
expect(events[1].doclet).toBe(newDoclet);
expect(events[1].property).toBe(key);
expect(events[1].oldValue).toBe(propValues[key]);
expect(events[1].oldValue).toEqual(propValues[key]);
expect(events[1].newValue).toBeUndefined();
});
});

View File

@ -90,6 +90,7 @@ export class Parser extends EventEmitter {
this._conf = dependencies.get('config');
this._dependencies = dependencies;
this._eventBus = dependencies.get('eventBus');
this._visitor = new Visitor();
this._walker = new Walker();
@ -130,6 +131,12 @@ export class Parser extends EventEmitter {
});
}
// TODO: update other code to always emit parser events directly to the event bus, then remove
emit(...args) {
super.emit(...args);
this._eventBus.emit(...args);
}
// TODO: update docs
/**
* Parse the given source files for JSDoc comments.

View File

@ -19,7 +19,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { Syntax, Walker } from '@jsdoc/ast';
import _ from 'lodash';
import { combineDoclets, Doclet } from '@jsdoc/doclet';
import { attachTo } from '../../../lib/handlers.js';
import * as jsdocParser from '../../../lib/parser.js';
@ -146,7 +146,7 @@ describe('@jsdoc/parse/lib/parser', () => {
const sourceCode = 'javascript:/** @class */function Foo() {}';
function handler(e) {
e.doclet = _.cloneDeep(e.doclet);
e.doclet = combineDoclets(e.doclet, new Doclet('', {}, jsdoc.deps));
e.doclet.foo = 'bar';
}