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

View File

@ -22,7 +22,7 @@ import _ from 'lodash';
const { SCOPE } = name;
function cloneBorrowedDoclets({ borrowed, longname }, doclets) {
borrowed.forEach(({ from, as }) => {
borrowed?.forEach(({ from, as }) => {
const borrowedDoclets = doclets.index.longname[from];
let borrowedAs = as || from;
let parts;
@ -60,7 +60,7 @@ function cloneBorrowedDoclets({ borrowed, longname }, doclets) {
export function resolveBorrows(doclets) {
for (let doclet of doclets.index.borrowed) {
cloneBorrowedDoclets(doclet, doclets);
delete doclet.borrowed;
doclet.borrowed = undefined;
}
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?
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) {
return {
type: node ? node.type : null,
@ -242,7 +257,7 @@ function resolve(doclet) {
if (doclet.scope === SCOPE.NAMES.GLOBAL) {
// via @global tag?
doclet.setLongname(doclet.name);
delete doclet.memberof;
doclet.memberof = undefined;
} else if (about.scope) {
if (about.memberof === LONGNAMES.GLOBAL) {
// via @memberof <global> ?
@ -375,6 +390,7 @@ function copySpecificProperties(primary, secondary, target, include) {
export class Doclet {
#accessConfig;
#dictionary;
#eventBus;
/**
* Create a doclet.
@ -389,12 +405,31 @@ export class Doclet {
meta = meta || {};
this.#accessConfig = dependencies.get('config')?.opts?.access ?? [];
this.#dictionary = dependencies.get('tags');
/** The original text of the comment from the source code. */
this.comment = docletSrc;
this.#eventBus = dependencies.get('eventBus');
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);
},
});
}
/** The original text of the comment from the source code. */
this.comment = docletSrc;
this.setMeta(meta);
docletSrc = unwrap(docletSrc);
@ -409,6 +444,15 @@ export class Doclet {
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
// if we don't call the method twice, various doclet properties can be incorrect, including name
// and memberof.
@ -421,7 +465,7 @@ export class Doclet {
this.setLongname(this.name);
}
if (this.memberof === '') {
delete this.memberof;
this.memberof = undefined;
}
if (!this.kind && this.meta && this.meta.code) {

View File

@ -38,6 +38,10 @@ describe('@jsdoc/doclet/lib/doclet', () => {
expect(doclet.Doclet).toBeFunction();
});
it('has a WATCHABLE_PROPS array', () => {
expect(doclet.WATCHABLE_PROPS).toBeArrayOfStrings();
});
describe('combineDoclets', () => {
it('overrides most properties of the secondary doclet', () => {
const primaryDoclet = new Doclet(
@ -347,6 +351,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
config.opts.access = access.slice();
}
map.set('config', config);
map.set('eventBus', jsdoc.deps.get('eventBus'));
map.set('tags', jsdoc.deps.get('tags'));
return map;
@ -385,7 +390,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
const newDoclet = makeDoclet(['@name foo', '@function']);
// Just to be sure.
delete newDoclet.access;
newDoclet.access = undefined;
expect(newDoclet.isVisible()).toBeTrue();
});
@ -402,7 +407,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
newDoclet = makeDoclet(tags, fakeDeps);
// Just to be sure.
if (!value) {
delete newDoclet.access;
newDoclet.access = undefined;
}
return newDoclet;
@ -439,7 +444,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
const newDoclet = makeDoclet(['@function', '@name foo'], fakeDeps);
// Just to be sure.
delete newDoclet.access;
newDoclet.access = undefined;
expect(newDoclet.isVisible()).toBeFalse();
});
@ -488,6 +493,76 @@ describe('@jsdoc/doclet/lib/doclet', () => {
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);
}
delete e.doclet.undocumented;
e.doclet.undocumented = undefined;
e.doclet.defaultvalue = e.doclet.meta.code.value;
// 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)) {
doclet.access = value.toLowerCase();
} else {
delete doclet.access;
doclet.access = undefined;
}
},
},
@ -263,7 +263,7 @@ export const tags = {
mustNotHaveValue: true,
onTagged(doclet) {
doclet.scope = SCOPE.NAMES.GLOBAL;
delete doclet.memberof;
doclet.memberof = undefined;
},
},
hideconstructor: {
@ -349,7 +349,7 @@ export const tags = {
doclet.forceMemberof = true;
if (tag.value === LONGNAMES.GLOBAL) {
doclet.addTag('global');
delete doclet.memberof;
doclet.memberof = undefined;
}
}
util.setDocletMemberof(doclet, tag);

View File

@ -179,6 +179,8 @@ export default (() => {
let cmd;
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 (props.shouldExitWithError) {
cmd = () => Promise.resolve(0);