mirror of
https://github.com/jsdoc/jsdoc.git
synced 2025-12-08 19:46:11 +00:00
feat(jsdoc-doclet): emit events when certain doclet properties change
This commit is contained in:
parent
6ececc4680
commit
954d17f87c
@ -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);
|
||||
|
||||
@ -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 = [];
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user