feat(jsdoc-doclet): emit events when certain doclet properties change

This commit is contained in:
Jeff Williams 2023-09-10 14:56:27 -07:00
parent 6ececc4680
commit 954d17f87c
No known key found for this signature in database
7 changed files with 139 additions and 20 deletions

View File

@ -17,9 +17,8 @@
* Provides methods for augmenting the parse results based on their content. * Provides methods for augmenting the parse results based on their content.
*/ */
import { name } from '@jsdoc/core'; import { name } from '@jsdoc/core';
import _ from 'lodash';
import { combineDoclets } from './doclet.js'; import { combineDoclets, Doclet } from './doclet.js';
const { fromParts, SCOPE, toParts } = name; const { fromParts, SCOPE, toParts } = name;
@ -37,7 +36,7 @@ function mapDependencies(index, propertyName) {
if (kinds.includes(doc.kind)) { if (kinds.includes(doc.kind)) {
dependencies[indexName] = {}; dependencies[indexName] = {};
if (Object.hasOwn(doc, propertyName)) { if (Object.hasOwn(doc, propertyName)) {
len = doc[propertyName].length; len = doc[propertyName]?.length;
for (let j = 0; j < len; j++) { for (let j = 0; j < len; j++) {
dependencies[indexName][doc[propertyName][j]] = true; dependencies[indexName][doc[propertyName][j]] = true;
} }
@ -382,7 +381,8 @@ function getMixedInAdditions(mixinDoclets, allDoclets, { documented, memberof })
continue; continue;
} }
mixedDoclet = _.cloneDeep(mixedDoclets[k]); mixedDoclet = new Doclet('', null, mixedDoclets[k].dependencies);
mixedDoclet = combineDoclets(mixedDoclets[k], mixedDoclet);
updateMixes(mixedDoclet, mixedDoclet.longname); updateMixes(mixedDoclet, mixedDoclet.longname);
mixedDoclet.mixed = true; mixedDoclet.mixed = true;
@ -411,9 +411,7 @@ function updateImplements(implDoclets, implementedLongname) {
} }
implDoclets.forEach((implDoclet) => { implDoclets.forEach((implDoclet) => {
if (!Object.hasOwn(implDoclet, 'implements')) { implDoclet.implements ??= [];
implDoclet.implements = [];
}
if (!implDoclet.implements.includes(implementedLongname)) { if (!implDoclet.implements.includes(implementedLongname)) {
implDoclet.implements.push(implementedLongname); implDoclet.implements.push(implementedLongname);

View File

@ -22,7 +22,7 @@ import _ from 'lodash';
const { SCOPE } = name; const { SCOPE } = name;
function cloneBorrowedDoclets({ borrowed, longname }, doclets) { function cloneBorrowedDoclets({ borrowed, longname }, doclets) {
borrowed.forEach(({ from, as }) => { borrowed?.forEach(({ from, as }) => {
const borrowedDoclets = doclets.index.longname[from]; const borrowedDoclets = doclets.index.longname[from];
let borrowedAs = as || from; let borrowedAs = as || from;
let parts; let parts;
@ -60,7 +60,7 @@ function cloneBorrowedDoclets({ borrowed, longname }, doclets) {
export function resolveBorrows(doclets) { export function resolveBorrows(doclets) {
for (let doclet of doclets.index.borrowed) { for (let doclet of doclets.index.borrowed) {
cloneBorrowedDoclets(doclet, doclets); cloneBorrowedDoclets(doclet, doclets);
delete doclet.borrowed; doclet.borrowed = undefined;
} }
doclets.index.borrowed = []; doclets.index.borrowed = [];

View File

@ -40,6 +40,21 @@ const DEFAULT_SCOPE = SCOPE.NAMES.STATIC;
// TODO: `class` should be on this list, right? What are the implications of adding it? // TODO: `class` should be on this list, right? What are the implications of adding it?
const GLOBAL_KINDS = ['constant', 'function', 'member', 'typedef']; const GLOBAL_KINDS = ['constant', 'function', 'member', 'typedef'];
export const WATCHABLE_PROPS = [
'access',
'augments',
'borrowed',
'ignore',
'implements',
'kind',
'listens',
'longname',
'memberof',
'mixes',
'scope',
'undocumented',
];
function fakeMeta(node) { function fakeMeta(node) {
return { return {
type: node ? node.type : null, type: node ? node.type : null,
@ -242,7 +257,7 @@ function resolve(doclet) {
if (doclet.scope === SCOPE.NAMES.GLOBAL) { if (doclet.scope === SCOPE.NAMES.GLOBAL) {
// via @global tag? // via @global tag?
doclet.setLongname(doclet.name); doclet.setLongname(doclet.name);
delete doclet.memberof; doclet.memberof = undefined;
} else if (about.scope) { } else if (about.scope) {
if (about.memberof === LONGNAMES.GLOBAL) { if (about.memberof === LONGNAMES.GLOBAL) {
// via @memberof <global> ? // via @memberof <global> ?
@ -375,6 +390,7 @@ function copySpecificProperties(primary, secondary, target, include) {
export class Doclet { export class Doclet {
#accessConfig; #accessConfig;
#dictionary; #dictionary;
#eventBus;
/** /**
* Create a doclet. * Create a doclet.
@ -389,12 +405,31 @@ export class Doclet {
meta = meta || {}; meta = meta || {};
this.#accessConfig = dependencies.get('config')?.opts?.access ?? []; this.#accessConfig = dependencies.get('config')?.opts?.access ?? [];
this.#dictionary = dependencies.get('tags'); this.#dictionary = dependencies.get('tags');
/** The original text of the comment from the source code. */ this.#eventBus = dependencies.get('eventBus');
this.comment = docletSrc;
Object.defineProperty(this, 'dependencies', { Object.defineProperty(this, 'dependencies', {
enumerable: false, enumerable: false,
value: dependencies, 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);
},
});
}
/** The original text of the comment from the source code. */
this.comment = docletSrc;
this.setMeta(meta); this.setMeta(meta);
docletSrc = unwrap(docletSrc); docletSrc = unwrap(docletSrc);
@ -409,6 +444,15 @@ export class Doclet {
this.postProcess(); this.postProcess();
} }
#setWatchableProperty(name, newValue) {
const oldValue = this.watchableProps[name];
if (newValue !== oldValue) {
this.watchableProps[name] = newValue;
this.#eventBus.emit('docletChanged', { doclet: this, property: name, oldValue, newValue });
}
}
// TODO: We call this method in the constructor _and_ in `jsdoc/src/handlers`. It appears that // TODO: We call this method in the constructor _and_ in `jsdoc/src/handlers`. It appears that
// if we don't call the method twice, various doclet properties can be incorrect, including name // if we don't call the method twice, various doclet properties can be incorrect, including name
// and memberof. // and memberof.
@ -421,7 +465,7 @@ export class Doclet {
this.setLongname(this.name); this.setLongname(this.name);
} }
if (this.memberof === '') { if (this.memberof === '') {
delete this.memberof; this.memberof = undefined;
} }
if (!this.kind && this.meta && this.meta.code) { if (!this.kind && this.meta && this.meta.code) {

View File

@ -38,6 +38,10 @@ describe('@jsdoc/doclet/lib/doclet', () => {
expect(doclet.Doclet).toBeFunction(); expect(doclet.Doclet).toBeFunction();
}); });
it('has a WATCHABLE_PROPS array', () => {
expect(doclet.WATCHABLE_PROPS).toBeArrayOfStrings();
});
describe('combineDoclets', () => { describe('combineDoclets', () => {
it('overrides most properties of the secondary doclet', () => { it('overrides most properties of the secondary doclet', () => {
const primaryDoclet = new Doclet( const primaryDoclet = new Doclet(
@ -347,6 +351,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
config.opts.access = access.slice(); config.opts.access = access.slice();
} }
map.set('config', config); map.set('config', config);
map.set('eventBus', jsdoc.deps.get('eventBus'));
map.set('tags', jsdoc.deps.get('tags')); map.set('tags', jsdoc.deps.get('tags'));
return map; return map;
@ -385,7 +390,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
const newDoclet = makeDoclet(['@name foo', '@function']); const newDoclet = makeDoclet(['@name foo', '@function']);
// Just to be sure. // Just to be sure.
delete newDoclet.access; newDoclet.access = undefined;
expect(newDoclet.isVisible()).toBeTrue(); expect(newDoclet.isVisible()).toBeTrue();
}); });
@ -402,7 +407,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
newDoclet = makeDoclet(tags, fakeDeps); newDoclet = makeDoclet(tags, fakeDeps);
// Just to be sure. // Just to be sure.
if (!value) { if (!value) {
delete newDoclet.access; newDoclet.access = undefined;
} }
return newDoclet; return newDoclet;
@ -439,7 +444,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
const newDoclet = makeDoclet(['@function', '@name foo'], fakeDeps); const newDoclet = makeDoclet(['@function', '@name foo'], fakeDeps);
// Just to be sure. // Just to be sure.
delete newDoclet.access; newDoclet.access = undefined;
expect(newDoclet.isVisible()).toBeFalse(); expect(newDoclet.isVisible()).toBeFalse();
}); });
@ -488,6 +493,76 @@ describe('@jsdoc/doclet/lib/doclet', () => {
expect(setScope).toThrow(); expect(setScope).toThrow();
}); });
}); });
describe('watchable properties', () => {
const eventBus = jsdoc.deps.get('eventBus');
let events;
function listener(e) {
events.push(e);
}
beforeEach(() => {
eventBus.on('docletChanged', listener);
events = [];
});
afterEach(() => {
eventBus.removeListener('docletChanged', listener);
});
it('sends events to the event bus when watchable properties change', () => {
const propValues = {
access: 'private',
augments: 'Foo',
borrowed: true,
ignore: true,
implements: 'Foo',
kind: 'class',
listens: 'event:foo',
longname: 'foo',
memberof: 'foo',
mixes: 'foo',
scope: 'static',
undocumented: true,
};
const keys = Object.keys(propValues);
// Make sure this test covers all watchable properties.
expect(keys).toEqual(doclet.WATCHABLE_PROPS);
keys.forEach((key) => {
const newDoclet = new Doclet('/** Huzzah, a doclet! */', null, jsdoc.deps);
events = [];
// Generates first event.
newDoclet[key] = propValues[key];
// Generates second event.
newDoclet[key] = undefined;
expect(events.length).toBe(2);
expect(events[0]).toBeObject();
expect(events[0].doclet).toBe(newDoclet);
expect(events[0].property).toBe(key);
if (key === 'kind') {
expect(events[0].oldValue).toBe('member');
} else if (key === 'longname') {
expect(events[0].oldValue).toBeEmptyString();
} else {
expect(events[0].oldValue).toBeUndefined();
}
expect(events[0].newValue).toBe(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].newValue).toBeUndefined();
});
});
});
}); });
}); });
}); });

View File

@ -631,7 +631,7 @@ export class Parser extends EventEmitter {
e.doclet.type = _.cloneDeep(doclet.type); e.doclet.type = _.cloneDeep(doclet.type);
} }
delete e.doclet.undocumented; e.doclet.undocumented = undefined;
e.doclet.defaultvalue = e.doclet.meta.code.value; e.doclet.defaultvalue = e.doclet.meta.code.value;
// add the doclet to the parent's properties // add the doclet to the parent's properties

View File

@ -44,7 +44,7 @@ export const tags = {
if (/^(package|private|protected|public)$/i.test(value)) { if (/^(package|private|protected|public)$/i.test(value)) {
doclet.access = value.toLowerCase(); doclet.access = value.toLowerCase();
} else { } else {
delete doclet.access; doclet.access = undefined;
} }
}, },
}, },
@ -263,7 +263,7 @@ export const tags = {
mustNotHaveValue: true, mustNotHaveValue: true,
onTagged(doclet) { onTagged(doclet) {
doclet.scope = SCOPE.NAMES.GLOBAL; doclet.scope = SCOPE.NAMES.GLOBAL;
delete doclet.memberof; doclet.memberof = undefined;
}, },
}, },
hideconstructor: { hideconstructor: {
@ -349,7 +349,7 @@ export const tags = {
doclet.forceMemberof = true; doclet.forceMemberof = true;
if (tag.value === LONGNAMES.GLOBAL) { if (tag.value === LONGNAMES.GLOBAL) {
doclet.addTag('global'); doclet.addTag('global');
delete doclet.memberof; doclet.memberof = undefined;
} }
} }
util.setDocletMemberof(doclet, tag); util.setDocletMemberof(doclet, tag);

View File

@ -179,6 +179,8 @@ export default (() => {
let cmd; let cmd;
const options = dependencies.get('options'); const options = dependencies.get('options');
dependencies.registerValue('eventBus', bus);
// If we already need to exit with an error, don't do any more work. // If we already need to exit with an error, don't do any more work.
if (props.shouldExitWithError) { if (props.shouldExitWithError) {
cmd = () => Promise.resolve(0); cmd = () => Promise.resolve(0);