From b2df642b3192ebd1bb743cf6c5ca000adaf83a4c Mon Sep 17 00:00:00 2001 From: Jeff Williams Date: Sun, 17 Sep 2023 17:48:23 -0700 Subject: [PATCH] feat(jsdoc-doclet): add DocletStore, a reactive tracker for doclets --- packages/jsdoc-doclet/index.js | 5 +- packages/jsdoc-doclet/lib/augment.js | 2 +- packages/jsdoc-doclet/lib/doclet-store.js | 304 ++++++ packages/jsdoc-doclet/lib/doclet.js | 121 ++- packages/jsdoc-doclet/package-lock.json | 453 +++++++++ packages/jsdoc-doclet/package.json | 1 + packages/jsdoc-doclet/test/specs/index.js | 7 + .../test/specs/lib/doclet-store.js | 875 ++++++++++++++++++ .../jsdoc-doclet/test/specs/lib/doclet.js | 20 +- packages/jsdoc-parse/lib/parser.js | 7 + packages/jsdoc-parse/test/specs/lib/parser.js | 4 +- 11 files changed, 1740 insertions(+), 59 deletions(-) create mode 100644 packages/jsdoc-doclet/lib/doclet-store.js create mode 100644 packages/jsdoc-doclet/test/specs/lib/doclet-store.js diff --git a/packages/jsdoc-doclet/index.js b/packages/jsdoc-doclet/index.js index f9a82636..279cfeee 100644 --- a/packages/jsdoc-doclet/index.js +++ b/packages/jsdoc-doclet/index.js @@ -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 }; diff --git a/packages/jsdoc-doclet/lib/augment.js b/packages/jsdoc-doclet/lib/augment.js index d8f0a8db..27b9a672 100644 --- a/packages/jsdoc-doclet/lib/augment.js +++ b/packages/jsdoc-doclet/lib/augment.js @@ -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); } }); diff --git a/packages/jsdoc-doclet/lib/doclet-store.js b/packages/jsdoc-doclet/lib/doclet-store.js new file mode 100644 index 00000000..508fd72d --- /dev/null +++ b/packages/jsdoc-doclet/lib/doclet-store.js @@ -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> */ + this.docletsByKind = new Map(); + /** @type Map> */ + this.docletsByLongname = new Map(); + /** @type Map> */ + this.docletsByMemberof = new Map(); + /** @type Map> */ + 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> */ + 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()); + } +} diff --git a/packages/jsdoc-doclet/lib/doclet.js b/packages/jsdoc-doclet/lib/doclet.js index 563730ee..23b6bf2a 100644 --- a/packages/jsdoc-doclet/lib/doclet.js +++ b/packages/jsdoc-doclet/lib/doclet.js @@ -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})`; } diff --git a/packages/jsdoc-doclet/package-lock.json b/packages/jsdoc-doclet/package-lock.json index 3d4f5b6a..bfc2c7d3 100644 --- a/packages/jsdoc-doclet/package-lock.json +++ b/packages/jsdoc-doclet/package-lock.json @@ -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" + } } } } diff --git a/packages/jsdoc-doclet/package.json b/packages/jsdoc-doclet/package.json index aac301f9..233b3ff5 100644 --- a/packages/jsdoc-doclet/package.json +++ b/packages/jsdoc-doclet/package.json @@ -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" } } diff --git a/packages/jsdoc-doclet/test/specs/index.js b/packages/jsdoc-doclet/test/specs/index.js index b12ebc69..20002f4d 100644 --- a/packages/jsdoc-doclet/test/specs/index.js +++ b/packages/jsdoc-doclet/test/specs/index.js @@ -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); diff --git a/packages/jsdoc-doclet/test/specs/lib/doclet-store.js b/packages/jsdoc-doclet/test/specs/lib/doclet-store.js new file mode 100644 index 00000000..e8803359 --- /dev/null +++ b/packages/jsdoc-doclet/test/specs/lib/doclet-store.js @@ -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', + ]); + }); + }); + }); +}); diff --git a/packages/jsdoc-doclet/test/specs/lib/doclet.js b/packages/jsdoc-doclet/test/specs/lib/doclet.js index c5a10f84..6c0bd39c 100644 --- a/packages/jsdoc-doclet/test/specs/lib/doclet.js +++ b/packages/jsdoc-doclet/test/specs/lib/doclet.js @@ -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(); }); }); diff --git a/packages/jsdoc-parse/lib/parser.js b/packages/jsdoc-parse/lib/parser.js index e1d5b6dc..ea86b95c 100644 --- a/packages/jsdoc-parse/lib/parser.js +++ b/packages/jsdoc-parse/lib/parser.js @@ -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. diff --git a/packages/jsdoc-parse/test/specs/lib/parser.js b/packages/jsdoc-parse/test/specs/lib/parser.js index f414fb96..7ea0c3fb 100644 --- a/packages/jsdoc-parse/test/specs/lib/parser.js +++ b/packages/jsdoc-parse/test/specs/lib/parser.js @@ -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'; }