refactor: provide logging functions in dependencies; stop using EventBus

These changes enable templates to use the logging functions even if they're not installed in the same `node_modules` directory as JSDoc.

Includes API changes to various modules and functions that didn't have access to the dependency object. Most notably, you now call a function to retrieve tag definitions, rather than just using an exported object as-is.
This commit is contained in:
Jeff Williams 2023-11-15 17:43:21 -08:00
parent 4bcf76c830
commit 89f2c72da4
No known key found for this signature in database
34 changed files with 975 additions and 931 deletions

View File

@ -13,8 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import babelParser from '@babel/parser';
import { log } from '@jsdoc/util';
import _ from 'lodash';
// Exported so we can use them in tests.
@ -62,23 +62,24 @@ export const parserOptions = {
ranges: true,
};
function parse(source, filename, sourceType) {
let ast;
const options = _.defaults({}, parserOptions, { sourceType });
try {
ast = babelParser.parse(source, options);
} catch (e) {
log.error(`Unable to parse ${filename}: ${e.message}`);
}
return ast;
}
// TODO: docs
export class AstBuilder {
// TODO: docs
static build(source, filename, sourceType) {
return parse(source, filename, sourceType);
#log;
constructor(deps) {
this.#log = deps.get('log');
}
build(source, filename, sourceType) {
let ast;
const options = _.defaults({}, parserOptions, { sourceType });
try {
ast = babelParser.parse(source, options);
} catch (e) {
this.#log.error(`Unable to parse ${filename}: ${e.message}`);
}
return ast;
}
}

View File

@ -13,10 +13,10 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Traversal utilities for ASTs that are compatible with the ESTree API.
*/
import { log } from '@jsdoc/util';
import * as astNode from './ast-node.js';
import { Syntax } from './syntax.js';
@ -645,7 +645,8 @@ walkers[Syntax.YieldExpression] = (node, parent, state, cb) => {
*/
export class Walker {
// TODO: docs
constructor(walkerFuncs = walkers) {
constructor(deps, walkerFuncs = walkers) {
this._log = deps.get('log');
this._walkers = walkerFuncs;
}
@ -659,7 +660,7 @@ export class Walker {
};
function logUnknownNodeType({ type }) {
log.debug(
self._log.debug(
`Found a node with unrecognized type ${type}. Ignoring the node and its descendants.`
);
}

View File

@ -13,8 +13,13 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
/* global jsdoc */
import * as astBuilder from '../../../lib/ast-builder.js';
const { AstBuilder } = astBuilder;
describe('@jsdoc/ast/lib/ast-builder', () => {
it('is an object', () => {
expect(astBuilder).toBeObject();
@ -29,23 +34,26 @@ describe('@jsdoc/ast/lib/ast-builder', () => {
});
describe('AstBuilder', () => {
const { AstBuilder } = astBuilder;
let instance;
beforeEach(() => {
instance = new AstBuilder(jsdoc.deps);
});
// TODO: more tests
it('has a "build" static method', () => {
expect(AstBuilder.build).toBeFunction();
it('has a `build` method', () => {
expect(instance.build).toBeFunction();
});
describe('build', () => {
// TODO: more tests
it('logs (not throws) an error when a file cannot be parsed', () => {
function parse() {
AstBuilder.build('qwerty!!!!!', 'bad.js');
instance.build('qwerty!!!!!', 'bad.js');
}
expect(parse).not.toThrow();
// TODO: figure out why this stopped working
// expect(jsdoc.didLog(parse, 'error')).toBeTrue();
expect(jsdoc.didLog(parse, 'error')).toBeTrue();
});
});
});

View File

@ -13,7 +13,10 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventBus } from '@jsdoc/util';
import EventEmitter from 'node:events';
import { getLogFunctions } from '@jsdoc/util';
import _ from 'lodash';
import ow from 'ow';
import yargs from 'yargs-parser';
@ -116,14 +119,13 @@ export default class Engine {
ow(opts.revision, ow.optional.date);
ow(opts.version, ow.optional.string);
this._bus = new EventBus('jsdoc', {
cache: _.isBoolean(opts._cacheEventBus) ? opts._cacheEventBus : true,
});
this.emitter = new EventEmitter();
this.flags = [];
this._logger = new Logger({
emitter: this._bus,
emitter: this.emitter,
level: opts.logLevel,
});
this.flags = [];
this.log = getLogFunctions(this.emitter);
this.revision = opts.revision;
this.version = opts.version;
}

View File

@ -13,22 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import RealEngine from '../../../lib/engine.js';
import Engine from '../../../lib/engine.js';
import flags from '../../../lib/flags.js';
import { LEVELS } from '../../../lib/logger.js';
const TYPE_ERROR = 'TypeError';
// Wrapper to prevent reuse of the event bus, which leads to `MaxListenersExceededWarning` messages.
class Engine extends RealEngine {
constructor(opts) {
opts = opts || {};
opts._cacheEventBus = false;
super(opts);
}
}
describe('@jsdoc/cli/lib/engine', () => {
it('exists', () => {
expect(Engine).toBeFunction();

View File

@ -13,7 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventBus } from '@jsdoc/util';
import EventEmitter from 'node:events';
import { LEVELS, Logger } from '../../../lib/logger.js';
@ -22,21 +23,18 @@ const TYPE_ERROR = 'TypeError';
describe('@jsdoc/cli/lib/logger', () => {
describe('Logger', () => {
let bus;
let emitter;
let logger;
beforeEach(() => {
bus = new EventBus('loggerTest', {
_console: console,
cache: false,
});
logger = new Logger({ emitter: bus });
emitter = new EventEmitter();
logger = new Logger({ emitter });
['debug', 'error', 'info', 'warn'].forEach((func) => spyOn(console, func));
});
it('exports a Logger constructor', () => {
expect(() => new Logger({ emitter: bus })).not.toThrow();
expect(() => new Logger({ emitter })).not.toThrow();
});
it('exports a LEVELS enum', () => {
@ -49,7 +47,7 @@ describe('@jsdoc/cli/lib/logger', () => {
});
it('accepts a valid emitter', () => {
expect(() => new Logger({ emitter: bus })).not.toThrow();
expect(() => new Logger({ emitter })).not.toThrow();
});
it('throws on an invalid emitter', () => {
@ -60,7 +58,7 @@ describe('@jsdoc/cli/lib/logger', () => {
expect(
() =>
new Logger({
emitter: bus,
emitter,
level: LEVELS.VERBOSE,
})
).not.toThrow();
@ -70,7 +68,7 @@ describe('@jsdoc/cli/lib/logger', () => {
expect(
() =>
new Logger({
emitter: bus,
emitter,
level: LEVELS.VERBOSE + 1,
})
).toThrowErrorOfType(TYPE_ERROR);
@ -83,85 +81,85 @@ describe('@jsdoc/cli/lib/logger', () => {
const eventType = 'logger:info';
logger.level = LEVELS.VERBOSE;
bus.emit(eventType, ...args);
emitter.emit(eventType, ...args);
expect(console.info).toHaveBeenCalledWith(...args);
});
it('logs logger:fatal events by default', () => {
bus.emit('logger:fatal');
emitter.emit('logger:fatal');
expect(console.error).toHaveBeenCalled();
});
it('does not log logger:fatal events when level is SILENT', () => {
logger.level = LEVELS.SILENT;
bus.emit('logger:fatal');
emitter.emit('logger:fatal');
expect(console.error).not.toHaveBeenCalled();
});
it('logs logger:error events by default', () => {
bus.emit('logger:error');
emitter.emit('logger:error');
expect(console.error).toHaveBeenCalled();
});
it('does not log logger:error events when level is FATAL', () => {
logger.level = LEVELS.FATAL;
bus.emit('logger:error');
emitter.emit('logger:error');
expect(console.error).not.toHaveBeenCalled();
});
it('logs logger:warn events by default', () => {
bus.emit('logger:warn');
emitter.emit('logger:warn');
expect(console.warn).toHaveBeenCalled();
});
it('does not log logger:warn events when level is ERROR', () => {
logger.level = LEVELS.ERROR;
bus.emit('logger:warn');
emitter.emit('logger:warn');
expect(console.warn).not.toHaveBeenCalled();
});
it('does not log logger:info events by default', () => {
bus.emit('logger:info');
emitter.emit('logger:info');
expect(console.info).not.toHaveBeenCalled();
});
it('logs logger:info events when level is INFO', () => {
logger.level = LEVELS.INFO;
bus.emit('logger:info');
emitter.emit('logger:info');
expect(console.info).toHaveBeenCalled();
});
it('does not log logger:debug events by default', () => {
bus.emit('logger:debug');
emitter.emit('logger:debug');
expect(console.debug).not.toHaveBeenCalled();
});
it('logs logger:debug events when level is DEBUG', () => {
logger.level = LEVELS.DEBUG;
bus.emit('logger:debug');
emitter.emit('logger:debug');
expect(console.debug).toHaveBeenCalled();
});
it('does not log logger:verbose events by default', () => {
bus.emit('logger:verbose');
emitter.emit('logger:verbose');
expect(console.debug).not.toHaveBeenCalled();
});
it('logs logger:verbose events when level is VERBOSE', () => {
logger.level = LEVELS.VERBOSE;
bus.emit('logger:verbose');
emitter.emit('logger:verbose');
expect(console.debug).toHaveBeenCalled();
});

View File

@ -55,7 +55,7 @@ function removeFromSet(targetMap, key, value) {
export class DocletStore {
#commonPathPrefix;
#docletChangedHandler;
#eventBus;
#emitter;
#isListening;
#newDocletHandler;
#sourcePaths;
@ -76,7 +76,7 @@ export class DocletStore {
constructor(dependencies) {
this.#commonPathPrefix = null;
this.#eventBus = dependencies.get('eventBus');
this.#emitter = dependencies.get('emitter');
this.#isListening = false;
this.#sourcePaths = new Map();
@ -357,8 +357,8 @@ export class DocletStore {
startListening() {
if (!this.#isListening) {
this.#eventBus.on('docletChanged', this.#docletChangedHandler);
this.#eventBus.on('newDoclet', this.#newDocletHandler);
this.#emitter.on('docletChanged', this.#docletChangedHandler);
this.#emitter.on('newDoclet', this.#newDocletHandler);
this.#isListening = true;
}
@ -366,8 +366,8 @@ export class DocletStore {
stopListening() {
if (this.#isListening) {
this.#eventBus.removeListener('docletChanged', this.#docletChangedHandler);
this.#eventBus.removeListener('newDoclet', this.#newDocletHandler);
this.#emitter.removeListener('docletChanged', this.#docletChangedHandler);
this.#emitter.removeListener('newDoclet', this.#newDocletHandler);
this.#isListening = false;
}

View File

@ -327,8 +327,8 @@ 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 emitDocletChanged(emitter, doclet, property, oldValue, newValue) {
emitter.emit('docletChanged', { doclet, property, oldValue, newValue });
}
function clone(source, target, properties) {
@ -453,9 +453,9 @@ Doclet = class {
*/
constructor(docletSrc, meta, dependencies) {
const accessConfig = dependencies.get('config')?.opts?.access?.slice() ?? [];
const eventBus = dependencies.get('eventBus');
const emitter = dependencies.get('emitter');
const boundDefineWatchableProp = defineWatchableProp.bind(null, this);
const boundEmitDocletChanged = emitDocletChanged.bind(null, eventBus, this);
const boundEmitDocletChanged = emitDocletChanged.bind(null, emitter, this);
let newTags = [];
this.#dictionary = dependencies.get('tags');

View File

@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import { log } from '@jsdoc/util';
import stripBom from 'strip-bom';
/**
@ -79,8 +79,10 @@ function getLicenses(packageInfo) {
export class Package {
/**
* @param {string} json - The contents of the `package.json` file.
* @param {Object} deps - The JSDoc dependencies.
*/
constructor(json) {
constructor(json, deps) {
const log = deps.get('log');
let packageInfo;
/**

View File

@ -29,7 +29,7 @@ function makeDoclet(comment, meta, deps) {
deps ??= jsdoc.deps;
doclet = new Doclet(`/**\n${comment.join('\n')}\n*/`, meta, deps);
if (meta?._emitEvent !== false) {
deps.get('eventBus').emit('newDoclet', { doclet });
deps.get('emitter').emit('newDoclet', { doclet });
}
return doclet;

View File

@ -357,7 +357,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
config.opts.access = access.slice();
}
map.set('config', config);
map.set('eventBus', jsdoc.deps.get('eventBus'));
map.set('emitter', jsdoc.deps.get('emitter'));
map.set('tags', jsdoc.deps.get('tags'));
return map;
@ -501,7 +501,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
});
describe('watchable properties', () => {
const eventBus = jsdoc.deps.get('eventBus');
const emitter = jsdoc.deps.get('emitter');
let events;
function listener(e) {
@ -509,12 +509,12 @@ describe('@jsdoc/doclet/lib/doclet', () => {
}
beforeEach(() => {
eventBus.on('docletChanged', listener);
emitter.on('docletChanged', listener);
events = [];
});
afterEach(() => {
eventBus.removeListener('docletChanged', listener);
emitter.removeListener('docletChanged', listener);
});
it('sends events to the event bus when watchable properties change', () => {

View File

@ -13,7 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
/* global jsdoc */
import * as jsdocPackage from '../../../lib/package.js';
const { Package } = jsdocPackage;
@ -26,7 +28,7 @@ describe('@jsdoc/doclet/lib/package', () => {
const obj = {};
obj[name] = value;
myPackage = new Package(JSON.stringify(obj));
myPackage = new Package(JSON.stringify(obj), jsdoc.deps);
// Add the package object to the cached parse results, so we can validate it against the
// doclet schema.
jsdoc.addParseResults(`package-property-${name}.js`, [myPackage]);
@ -44,12 +46,12 @@ describe('@jsdoc/doclet/lib/package', () => {
describe('Package', () => {
beforeEach(() => {
emptyPackage = new Package();
emptyPackage = new Package(null, jsdoc.deps);
});
it('accepts a JSON-format string', () => {
function newPackage() {
return new Package('{"foo": "bar"}');
return new Package('{"foo": "bar"}', jsdoc.deps);
}
expect(newPackage).not.toThrow();
@ -57,15 +59,7 @@ describe('@jsdoc/doclet/lib/package', () => {
it('accepts a JSON-format string with a leading BOM', () => {
function newPackage() {
return new Package('\uFEFF{}');
}
expect(newPackage).not.toThrow();
});
it('works with no arguments', () => {
function newPackage() {
return new Package();
return new Package('\uFEFF{}', jsdoc.deps);
}
expect(newPackage).not.toThrow();
@ -73,7 +67,7 @@ describe('@jsdoc/doclet/lib/package', () => {
it('logs an error when called with bad input', () => {
function newPackage() {
return new Package('abcdefg');
return new Package('abcdefg', jsdoc.deps);
}
expect(newPackage).not.toThrow();
@ -164,7 +158,7 @@ describe('@jsdoc/doclet/lib/package', () => {
});
it('ignores the value from the package file', () => {
const myPackage = new Package('{"files": ["foo", "bar"]}');
const myPackage = new Package('{"files": ["foo", "bar"]}', jsdoc.deps);
expect(myPackage.files).toBeEmptyArray();
});
@ -205,7 +199,7 @@ describe('@jsdoc/doclet/lib/package', () => {
});
it('contains the value of `license` from the package file', () => {
const myPackage = new Package('{"license": "My-OSS-License"}');
const myPackage = new Package('{"license": "My-OSS-License"}', jsdoc.deps);
expect(myPackage.license).toBeUndefined();
expect(myPackage.licenses).toBeArrayOfSize(1);
@ -222,7 +216,7 @@ describe('@jsdoc/doclet/lib/package', () => {
},
],
};
const myPackage = new Package(JSON.stringify(packageInfo));
const myPackage = new Package(JSON.stringify(packageInfo), jsdoc.deps);
expect(myPackage.licenses).toBeArrayOfSize(2);
});
@ -234,7 +228,7 @@ describe('@jsdoc/doclet/lib/package', () => {
});
it('reflects the value of the `name` property', () => {
const myPackage = new Package('{"name": "foo"}');
const myPackage = new Package('{"name": "foo"}', jsdoc.deps);
expect(myPackage.longname).toBe('package:foo');
});

View File

@ -16,7 +16,6 @@
import { Syntax } from '@jsdoc/ast';
import { name } from '@jsdoc/core';
import { Doclet } from '@jsdoc/doclet';
import { log } from '@jsdoc/util';
import escape from 'escape-string-regexp';
const PROTOTYPE_OWNER_REGEXP = /^(.+?)(\.prototype|#)$/;
@ -43,6 +42,7 @@ function filterByLongname({ longname }) {
function createDoclet(comment, e, deps) {
let doclet;
let flatComment;
let log;
let msg;
try {
@ -50,6 +50,7 @@ function createDoclet(comment, e, deps) {
} catch (error) {
flatComment = comment.replace(/[\r\n]/g, '');
msg = `cannot create a doclet for the comment "${flatComment}": ${error.message}`;
log = deps.get('log');
log.error(msg);
doclet = new Doclet('', e, deps);
}

View File

@ -19,7 +19,6 @@ import fs from 'node:fs';
import { AstBuilder, astNode, Syntax, Walker } from '@jsdoc/ast';
import { name } from '@jsdoc/core';
import { Doclet, DocletStore } from '@jsdoc/doclet';
import { log } from '@jsdoc/util';
import _ from 'lodash';
import { Visitor } from './visitor.js';
@ -73,9 +72,10 @@ export class Parser extends EventEmitter {
this._conf = dependencies.get('config');
this._dependencies = dependencies;
this._docletStore = new DocletStore(dependencies);
this._eventBus = dependencies.get('eventBus');
this._emitter = dependencies.get('emitter');
this._log = dependencies.get('log');
this._visitor = new Visitor();
this._walker = new Walker();
this._walker = new Walker(dependencies);
this._visitor.setParser(this);
@ -107,10 +107,10 @@ export class Parser extends EventEmitter {
this._docletStore.stopListening();
}
// TODO: Always emit events from the event bus, never from the parser.
// TODO: Always emit events from the dependencies' emitter, never from the parser.
emit(eventName, event, ...args) {
super.emit(eventName, event, ...args);
this._eventBus.emit(eventName, event, ...args);
this._emitter.emit(eventName, event, ...args);
}
// TODO: update docs
@ -145,7 +145,7 @@ export class Parser extends EventEmitter {
}
e.sourcefiles = sourceFiles;
log.debug('Parsing source files: %j', sourceFiles);
this._log.debug('Parsing source files: %j', sourceFiles);
this.emit('parseBegin', e);
@ -161,7 +161,7 @@ export class Parser extends EventEmitter {
try {
sourceCode = fs.readFileSync(filename, encoding);
} catch (err) {
log.error(`Unable to read and parse the source file ${filename}: ${err}`);
this._log.error(`Unable to read and parse the source file ${filename}: ${err}`);
}
}
@ -177,7 +177,7 @@ export class Parser extends EventEmitter {
doclets: this.results(),
});
}
log.debug('Finished parsing source files.');
this._log.debug('Finished parsing source files.');
return this._docletStore;
}
@ -209,13 +209,14 @@ export class Parser extends EventEmitter {
/** @private */
_parseSourceCode(sourceCode, sourceName) {
let ast;
const builder = new AstBuilder(this._dependencies);
let e = {
filename: sourceName,
};
let sourceType;
this.emit('fileBegin', e);
log.info(`Parsing ${sourceName} ...`);
this._log.info(`Parsing ${sourceName} ...`);
if (!e.defaultPrevented) {
e = {
@ -229,7 +230,7 @@ export class Parser extends EventEmitter {
sourceCode = pretreat(e.source);
sourceType = this._conf.source ? this._conf.source.type : undefined;
ast = AstBuilder.build(sourceCode, sourceName, sourceType);
ast = builder.build(sourceCode, sourceName, sourceType);
if (ast) {
this._walkAst(ast, this._visitor, sourceName);
}

View File

@ -13,7 +13,6 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import { log } from '@jsdoc/util';
export const handlers = {
/**
@ -46,12 +45,10 @@ export const handlers = {
try {
value = JSON.parse(tag.value);
} catch (ex) {
log.error(
throw new Error(
'@source tag expects a valid JSON value, like ' +
'{ "filename": "myfile.js", "lineno": 123 }.'
);
return;
}
doclet.meta = doclet.meta || {};

View File

@ -15,7 +15,7 @@
*/
// Tag dictionary for Google Closure Compiler.
import { tags as core } from './core.js';
import { getTags as getCoreTags } from './core.js';
import * as util from './util.js';
const NOOP_TAG = {
@ -24,139 +24,143 @@ const NOOP_TAG = {
},
};
export const tags = {
const: {
canHaveType: true,
onTagged(doclet, tag) {
doclet.kind = 'constant';
util.setDocletTypeToValueType(doclet, tag);
export const getTags = (deps) => {
const coreTags = getCoreTags(deps);
return {
const: {
canHaveType: true,
onTagged(doclet, tag) {
doclet.kind = 'constant';
util.setDocletTypeToValueType(doclet, tag);
},
// Closure Compiler only
synonyms: ['define'],
},
constructor: util.cloneTagDef(coreTags.class),
deprecated: util.cloneTagDef(coreTags.deprecated),
// Closure Compiler only
dict: NOOP_TAG,
enum: util.cloneTagDef(coreTags.enum),
// Closure Compiler only
export: NOOP_TAG,
extends: util.cloneTagDef(coreTags.augments),
// Closure Compiler only
externs: NOOP_TAG,
fileoverview: {
onTagged(doclet, tag) {
util.setNameToFile(doclet);
doclet.kind = 'file';
util.setDocletDescriptionToValue(doclet, tag);
doclet.preserveName = true;
},
},
final: util.cloneTagDef(coreTags.readonly),
implements: util.cloneTagDef(coreTags.implements),
// Closure Compiler only
implicitcast: NOOP_TAG,
inheritdoc: util.cloneTagDef(coreTags.inheritdoc),
interface: util.cloneTagDef(coreTags.interface, {
canHaveName: false,
mustNotHaveValue: true,
// Closure Compiler only
synonyms: ['record'],
}),
lends: util.cloneTagDef(coreTags.lends),
license: util.cloneTagDef(coreTags.license),
modifies: util.cloneTagDef(coreTags.modifies),
// Closure Compiler only
noalias: NOOP_TAG,
// Closure Compiler only
nocollapse: NOOP_TAG,
// Closure Compiler only
nocompile: NOOP_TAG,
// Closure Compiler only
nosideeffects: {
onTagged(doclet) {
doclet.modifies = [];
},
},
// Closure Compiler only
synonyms: ['define'],
},
constructor: util.cloneTagDef(core.class),
deprecated: util.cloneTagDef(core.deprecated),
// Closure Compiler only
dict: NOOP_TAG,
enum: util.cloneTagDef(core.enum),
// Closure Compiler only
export: NOOP_TAG,
extends: util.cloneTagDef(core.augments),
// Closure Compiler only
externs: NOOP_TAG,
fileoverview: {
onTagged(doclet, tag) {
util.setNameToFile(doclet);
doclet.kind = 'file';
util.setDocletDescriptionToValue(doclet, tag);
doclet.preserveName = true;
override: {
mustNotHaveValue: true,
onTagged(doclet) {
doclet.override = true;
},
},
},
final: util.cloneTagDef(core.readonly),
implements: util.cloneTagDef(core.implements),
// Closure Compiler only
implicitcast: NOOP_TAG,
inheritdoc: util.cloneTagDef(core.inheritdoc),
interface: util.cloneTagDef(core.interface, {
canHaveName: false,
mustNotHaveValue: true,
package: {
canHaveType: true,
onTagged(doclet, tag) {
doclet.access = 'package';
if (tag.value && tag.value.type) {
util.setDocletTypeToValueType(doclet, tag);
}
},
},
param: util.cloneTagDef(coreTags.param),
// Closure Compiler only
synonyms: ['record'],
}),
lends: util.cloneTagDef(core.lends),
license: util.cloneTagDef(core.license),
modifies: util.cloneTagDef(core.modifies),
// Closure Compiler only
noalias: NOOP_TAG,
// Closure Compiler only
nocollapse: NOOP_TAG,
// Closure Compiler only
nocompile: NOOP_TAG,
// Closure Compiler only
nosideeffects: {
onTagged(doclet) {
doclet.modifies = [];
},
},
// Closure Compiler only
override: {
mustNotHaveValue: true,
onTagged(doclet) {
doclet.override = true;
},
},
package: {
canHaveType: true,
onTagged(doclet, tag) {
doclet.access = 'package';
polymer: NOOP_TAG,
// Closure Compiler only
polymerBehavior: NOOP_TAG,
// Closure Compiler only
preserve: util.cloneTagDef(coreTags.license),
private: {
canHaveType: true,
onTagged(doclet, tag) {
doclet.access = 'private';
if (tag.value && tag.value.type) {
util.setDocletTypeToValueType(doclet, tag);
}
if (tag.value && tag.value.type) {
util.setDocletTypeToValueType(doclet, tag);
}
},
},
},
param: util.cloneTagDef(core.param),
// Closure Compiler only
polymer: NOOP_TAG,
// Closure Compiler only
polymerBehavior: NOOP_TAG,
// Closure Compiler only
preserve: util.cloneTagDef(core.license),
private: {
canHaveType: true,
onTagged(doclet, tag) {
doclet.access = 'private';
protected: {
canHaveType: true,
onTagged(doclet, tag) {
doclet.access = 'protected';
if (tag.value && tag.value.type) {
util.setDocletTypeToValueType(doclet, tag);
}
if (tag.value && tag.value.type) {
util.setDocletTypeToValueType(doclet, tag);
}
},
},
},
protected: {
canHaveType: true,
onTagged(doclet, tag) {
doclet.access = 'protected';
public: {
canHaveType: true,
onTagged(doclet, tag) {
doclet.access = 'public';
if (tag.value && tag.value.type) {
if (tag.value && tag.value.type) {
util.setDocletTypeToValueType(doclet, tag);
}
},
},
return: util.cloneTagDef(coreTags.returns),
// Closure Compiler only
struct: NOOP_TAG,
// Closure Compiler only
suppress: NOOP_TAG,
// Closure Compiler only
template: NOOP_TAG,
this: {
canHaveType: true,
onTagged(doclet, tag) {
doclet.this = util.combineTypes(tag);
},
},
throws: util.cloneTagDef(coreTags.throws),
type: util.cloneTagDef(coreTags.type, {
mustNotHaveDescription: false,
}),
typedef: {
canHaveType: true,
onTagged(doclet, tag) {
util.setDocletKindToTitle(doclet, tag);
util.setDocletTypeToValueType(doclet, tag);
}
},
},
},
public: {
canHaveType: true,
onTagged(doclet, tag) {
doclet.access = 'public';
if (tag.value && tag.value.type) {
util.setDocletTypeToValueType(doclet, tag);
}
},
},
return: util.cloneTagDef(core.returns),
// Closure Compiler only
struct: NOOP_TAG,
// Closure Compiler only
suppress: NOOP_TAG,
// Closure Compiler only
template: NOOP_TAG,
this: {
canHaveType: true,
onTagged(doclet, tag) {
doclet.this = util.combineTypes(tag);
},
},
throws: util.cloneTagDef(core.throws),
type: util.cloneTagDef(core.type, {
mustNotHaveDescription: false,
}),
typedef: {
canHaveType: true,
onTagged(doclet, tag) {
util.setDocletKindToTitle(doclet, tag);
util.setDocletTypeToValueType(doclet, tag);
},
},
// Closure Compiler only
unrestricted: NOOP_TAG,
// Closure Compiler only
unrestricted: NOOP_TAG,
};
};

File diff suppressed because it is too large Load Diff

View File

@ -13,10 +13,10 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import { tags as closure } from './closure.js';
import { tags as core } from './core.js';
import { tags as internal } from './internal.js';
import { tags as jsdoc } from './jsdoc.js';
import { getTags as getClosureTags } from './closure.js';
import { getTags as getCoreTags } from './core.js';
import { getTags as getInternalTags } from './internal.js';
import { getTags as getJsdocTags } from './jsdoc.js';
export { closure, core, internal, jsdoc };
export default { closure, core, internal, jsdoc };
export { getClosureTags, getCoreTags, getInternalTags, getJsdocTags };
export default { getClosureTags, getCoreTags, getInternalTags, getJsdocTags };

View File

@ -13,48 +13,51 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
// Tags that JSDoc uses internally, and that must always be defined.
export const tags = {
// Special separator tag indicating that multiple doclets should be generated for the same
// comment. Used internally (and by some JSDoc users, although it's not officially supported).
//
// In the following example, the parser will replace `//**` with an `@also` tag:
// /**
// * Foo.
// *//**
// * Foo with a param.
// * @param {string} bar
// */
// function foo(bar) {}
also: {
onTagged() {
// Let the parser handle it. We define the tag here to avoid "not a known tag" errors.
export const getTags = () => {
return {
// Special separator tag indicating that multiple doclets should be generated for the same
// comment. Used internally (and by some JSDoc users, although it's not officially supported).
//
// In the following example, the parser will replace `//**` with an `@also` tag:
// /**
// * Foo.
// *//**
// * Foo with a param.
// * @param {string} bar
// */
// function foo(bar) {}
also: {
onTagged() {
// Let the parser handle it. We define the tag here to avoid "not a known tag" errors.
},
},
},
description: {
mustHaveValue: true,
onTagged: (doclet, { value }) => {
doclet.description = value;
description: {
mustHaveValue: true,
onTagged: (doclet, { value }) => {
doclet.description = value;
},
synonyms: ['desc'],
},
synonyms: ['desc'],
},
kind: {
mustHaveValue: true,
onTagged: (doclet, { value }) => {
doclet.kind = value;
kind: {
mustHaveValue: true,
onTagged: (doclet, { value }) => {
doclet.kind = value;
},
},
},
name: {
mustHaveValue: true,
onTagged: (doclet, { value }) => {
doclet.name = value;
name: {
mustHaveValue: true,
onTagged: (doclet, { value }) => {
doclet.name = value;
},
},
},
undocumented: {
mustNotHaveValue: true,
onTagged(doclet) {
doclet.undocumented = true;
doclet.comment = '';
undocumented: {
mustNotHaveValue: true,
onTagged(doclet) {
doclet.undocumented = true;
doclet.comment = '';
},
},
},
};
};

View File

@ -13,4 +13,4 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
export { tags } from './core.js';
export { getTags } from './core.js';

View File

@ -13,10 +13,10 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import path from 'node:path';
import { name } from '@jsdoc/core';
import { log } from '@jsdoc/util';
import commonPathPrefix from 'common-path-prefix';
import _ from 'lodash';
@ -74,10 +74,13 @@ export function setDocletKindToTitle(doclet, { title }) {
doclet.addTag('kind', title);
}
export function setDocletScopeToTitle(doclet, { title }) {
export function setDocletScopeToTitle(doclet, { title }, deps) {
let log;
try {
doclet.setScope(title);
} catch (e) {
log = deps.get('log');
log.error(e.message);
}
}

View File

@ -13,14 +13,14 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
/** @module @jsdoc/tag/lib/dictionary */
import { log } from '@jsdoc/util';
import definitions from './definitions/index.js';
const DEFINITIONS = {
closure: 'closure',
jsdoc: 'jsdoc',
closure: 'getClosureTags',
jsdoc: 'getJsdocTags',
};
/** @private */
@ -103,9 +103,12 @@ export class Dictionary {
this._tagSynonyms[synonym.toLowerCase()] = this.normalize(title);
}
static fromConfig(env) {
let dictionaries = env.conf.tags.dictionaries;
static fromConfig(deps) {
const dict = new Dictionary();
const env = deps.get('env');
const log = deps.get('log');
let dictionaries = env.conf.tags.dictionaries;
let tagDefs;
if (!dictionaries) {
log.error(
@ -117,9 +120,9 @@ export class Dictionary {
.slice()
.reverse()
.forEach((dictName) => {
const tagDefs = definitions[DEFINITIONS[dictName]];
if (!tagDefs) {
try {
tagDefs = definitions[DEFINITIONS[dictName]](deps);
} catch (e) {
log.error(
'The configuration setting "tags.dictionaries" contains ' +
`the unknown dictionary name ${dictName}. Ignoring the dictionary.`
@ -131,7 +134,7 @@ export class Dictionary {
dict.defineTags(tagDefs);
});
dict.defineTags(definitions.internal);
dict.defineTags(definitions.getInternalTags(deps));
}
return dict;

View File

@ -13,12 +13,13 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Functionality related to JSDoc tags.
*/
import path from 'node:path';
import { log } from '@jsdoc/util';
import _ from 'lodash';
import * as type from './type.js';
@ -66,10 +67,13 @@ function addHiddenProperty(obj, propName, propValue, dependencies) {
});
}
function parseType({ text, originalTitle }, { canHaveName, canHaveType }, meta) {
function parseType({ dependencies, text, originalTitle }, { canHaveName, canHaveType }, meta) {
let log;
try {
return type.parse(text, canHaveName, canHaveType);
} catch (e) {
log = dependencies.get('log');
log.error(
'Unable to parse a tag\'s type expression%s with tag title "%s" and text "%s": %s',
meta.path && meta.filename

View File

@ -13,7 +13,6 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import { log } from '@jsdoc/util';
function buildMessage(tagName, { filename, lineno, comment }, desc) {
let result = `The @${tagName} tag ${desc}. File: ${filename}, line: ${lineno}`;
@ -30,6 +29,7 @@ function buildMessage(tagName, { filename, lineno, comment }, desc) {
*/
export function validate({ dependencies, title, text, value }, tagDef, meta) {
const config = dependencies.get('config');
const log = dependencies.get('log');
const allowUnknownTags = config.tags.allowUnknownTags;
// handle cases where the tag definition does not exist

View File

@ -16,20 +16,20 @@
import * as definitions from '../../../../lib/definitions/index.js';
describe('@jsdoc/tag/lib/definitions', () => {
it('has a `closure` object', () => {
expect(definitions.closure).toBeObject();
it('has a `getClosureTags` function', () => {
expect(definitions.getClosureTags).toBeFunction();
});
it('has a `core` object', () => {
expect(definitions.core).toBeObject();
it('has a `getCoreTags` function', () => {
expect(definitions.getCoreTags).toBeFunction();
});
it('has an `internal` object', () => {
expect(definitions.internal).toBeObject();
it('has a `getInternalTags` function', () => {
expect(definitions.getInternalTags).toBeFunction();
});
it('has a `jsdoc` object', () => {
expect(definitions.jsdoc).toBeObject();
it('has a `getJsdocTags` function', () => {
expect(definitions.getJsdocTags).toBeFunction();
});
// For additional tests, see packages/jsdoc/test/specs/tags/.

View File

@ -119,16 +119,19 @@ describe('@jsdoc/tag/lib/dictionary', () => {
beforeEach(() => {
env.conf.tags.dictionaries = [];
jsdoc.deps.registerValue('env', env);
});
afterEach(() => {
env.conf.tags.dictionaries = dictionaryConfig.slice();
jsdoc.deps.registerValue('env', env);
});
it('logs an error if `env.conf.tags.dictionaries` is undefined', () => {
function defineTags() {
env.conf.tags.dictionaries = undefined;
Dictionary.fromConfig(env);
jsdoc.deps.registerValue('env', env);
Dictionary.fromConfig(jsdoc.deps);
}
expect(jsdoc.didLog(defineTags, 'error')).toBeTrue();
@ -137,7 +140,8 @@ describe('@jsdoc/tag/lib/dictionary', () => {
it('logs an error if an unknown dictionary is requested', () => {
function defineTags() {
env.conf.tags.dictionaries = ['jsmarmoset'];
Dictionary.fromConfig(env);
jsdoc.deps.registerValue('env', env);
Dictionary.fromConfig(jsdoc.deps);
}
expect(jsdoc.didLog(defineTags, 'error')).toBeTrue();
@ -145,7 +149,8 @@ describe('@jsdoc/tag/lib/dictionary', () => {
it('adds both JSDoc and Closure tags by default', () => {
env.conf.tags.dictionaries = dictionaryConfig.slice();
testDictionary = Dictionary.fromConfig(env);
jsdoc.deps.registerValue('env', env);
testDictionary = Dictionary.fromConfig(jsdoc.deps);
expect(testDictionary.lookup(JSDOC_TAGNAME)).toBeObject();
expect(testDictionary.lookup(CLOSURE_TAGNAME)).toBeObject();
@ -153,7 +158,8 @@ describe('@jsdoc/tag/lib/dictionary', () => {
it('adds only the JSDoc tags if requested', () => {
env.conf.tags.dictionaries = ['jsdoc'];
testDictionary = Dictionary.fromConfig(env);
jsdoc.deps.registerValue('env', env);
testDictionary = Dictionary.fromConfig(jsdoc.deps);
expect(testDictionary.lookup(JSDOC_TAGNAME)).toBeObject();
expect(testDictionary.lookup(CLOSURE_TAGNAME)).toBeFalse();
@ -161,7 +167,8 @@ describe('@jsdoc/tag/lib/dictionary', () => {
it('adds only the Closure tags if requested', () => {
env.conf.tags.dictionaries = ['closure'];
testDictionary = Dictionary.fromConfig(env);
jsdoc.deps.registerValue('env', env);
testDictionary = Dictionary.fromConfig(jsdoc.deps);
expect(testDictionary.lookup(JSDOC_TAGNAME)).toBeFalse();
expect(testDictionary.lookup(CLOSURE_TAGNAME)).toBeObject();
@ -169,14 +176,16 @@ describe('@jsdoc/tag/lib/dictionary', () => {
it('prefers tagdefs from the first dictionary on the list', () => {
env.conf.tags.dictionaries = ['closure', 'jsdoc'];
testDictionary = Dictionary.fromConfig(env);
jsdoc.deps.registerValue('env', env);
testDictionary = Dictionary.fromConfig(jsdoc.deps);
expect(testDictionary.lookup('deprecated').synonyms).not.toBeDefined();
});
it('adds tag synonyms', () => {
env.conf.tags.dictionaries = ['jsdoc'];
testDictionary = Dictionary.fromConfig(env);
jsdoc.deps.registerValue('env', env);
testDictionary = Dictionary.fromConfig(jsdoc.deps);
expect(testDictionary.lookup('extends')).toBeObject();
expect(testDictionary.normalize('extends')).toBe('augments');

View File

@ -13,8 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
/* global jsdoc */
import { EventBus } from '@jsdoc/util';
import { Tag } from '../../../lib/tag.js';
import * as validator from '../../../lib/validator.js';
@ -129,10 +129,10 @@ describe('@jsdoc/tag/lib/validator', () => {
});
it('logs the offending comment for validation errors', () => {
const bus = new EventBus('jsdoc');
const emitter = jsdoc.deps.get('emitter');
const events = [];
bus.once('logger:error', (e) => events.push(e));
emitter.once('logger:error', (e) => events.push(e));
config.tags.allowUnknownTags = false;
validateTag(badTag);

View File

@ -15,7 +15,6 @@
*/
import { name } from '@jsdoc/core';
import { inline } from '@jsdoc/tag';
import { log } from '@jsdoc/util';
import catharsis from 'catharsis';
const { longnamesToTree, SCOPE, SCOPE_TO_PUNC, toParts } = name;
@ -249,7 +248,7 @@ function parseType(longname) {
return catharsis.parse(longname, { jsdoc: true });
} catch (e) {
err = new Error(`unable to parse ${longname}: ${e.message}`);
log.error(err);
console.error(err);
return longname;
}

View File

@ -13,7 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
/* global jsdoc */
import { Dependencies } from '@jsdoc/core';
import { Doclet } from '@jsdoc/doclet';
import salty from '@jsdoc/salty';
@ -398,11 +400,19 @@ describe('@jsdoc/template-legacy/lib/templateHelper', () => {
});
it('does not try to parse a longname starting with <anonymous> as a type application', () => {
function linkto() {
helper.linkto('<anonymous>~foo');
const emitter = jsdoc.deps.get('emitter');
const events = [];
function storeEvent(e) {
events.push(e);
}
expect(jsdoc.didLog(linkto, 'error')).toBeFalse();
emitter.on('logger:error', storeEvent);
helper.linkto('<anonymous>~foo');
expect(events).toBeEmptyArray();
emitter.off('logger:error', storeEvent);
});
it('does not treat a longname with a variation as a type application', () => {

View File

@ -13,6 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Utility modules for JSDoc.
*
@ -20,7 +21,7 @@
*/
import EventBus from './lib/bus.js';
import cast from './lib/cast.js';
import log from './lib/log.js';
import getLogFunctions from './lib/log.js';
export { cast, EventBus, log };
export default { cast, EventBus, log };
export { cast, EventBus, getLogFunctions };
export default { cast, EventBus, getLogFunctions };

View File

@ -13,13 +13,15 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import EventBus from './bus.js';
const bus = new EventBus('jsdoc');
const loggerFuncs = {};
export const LOG_TYPES = ['debug', 'error', 'info', 'fatal', 'verbose', 'warn'];
['debug', 'error', 'info', 'fatal', 'verbose', 'warn'].forEach((fn) => {
loggerFuncs[fn] = (...args) => bus.emit(`logger:${fn}`, ...args);
});
export default function getLogFunctions(emitter) {
const logFunctions = {};
export default loggerFuncs;
LOG_TYPES.forEach((type) => {
logFunctions[type] = (...args) => emitter.emit(`logger:${type}`, ...args);
});
return logFunctions;
}

View File

@ -13,14 +13,23 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import EventBus from '../../../lib/bus.js';
import log from '../../../lib/log.js';
/* global jsdoc */
import getLogFunctions from '../../../lib/log.js';
describe('@jsdoc/util/lib/log', () => {
let emitter;
const fns = ['debug', 'error', 'info', 'fatal', 'verbose', 'warn'];
let log;
it('is an object', () => {
expect(log).toBeObject();
beforeEach(() => {
emitter = jsdoc.deps.get('emitter');
log = getLogFunctions(emitter);
});
it('is a function', () => {
expect(getLogFunctions).toBeFunction();
});
it('provides the expected functions', () => {
@ -30,13 +39,11 @@ describe('@jsdoc/util/lib/log', () => {
});
describe('functions', () => {
const bus = new EventBus('jsdoc');
it('sends events to the event bus', () => {
it('sends events to the emitter', () => {
fns.forEach((fn) => {
let event;
bus.once(`logger:${fn}`, (e) => {
emitter.once(`logger:${fn}`, (e) => {
event = e;
});
log[fn]('testing');

View File

@ -23,7 +23,6 @@ import { config, Dependencies, plugins } from '@jsdoc/core';
import { augment, Package, resolveBorrows } from '@jsdoc/doclet';
import { createParser, handlers } from '@jsdoc/parse';
import { Dictionary } from '@jsdoc/tag';
import { EventBus, log } from '@jsdoc/util';
import fastGlob from 'fast-glob';
import _ from 'lodash';
import stripBom from 'strip-bom';
@ -47,14 +46,18 @@ export default (() => {
tmpdir: null,
};
const bus = new EventBus('jsdoc');
const cli = {};
const dependencies = new Dependencies();
const engine = new Engine();
const emitter = engine.emitter;
const log = engine.log;
const FATAL_ERROR_MESSAGE =
'Exiting JSDoc because an error occurred. See the previous log messages for details.';
const LOG_LEVELS = Engine.LOG_LEVELS;
dependencies.registerValue('emitter', emitter);
dependencies.registerValue('log', engine.log);
cli.setEnv = (env) => {
dependencies.registerValue('env', env);
@ -107,9 +110,7 @@ export default (() => {
// Now that we're done loading and merging things, register dependencies.
dependencies.registerValue('config', env.conf);
dependencies.registerValue('options', env.opts);
dependencies.registerSingletonFactory('tags', () =>
Dictionary.fromConfig(dependencies.get('env'))
);
dependencies.registerSingletonFactory('tags', () => Dictionary.fromConfig(dependencies));
return cli;
};
@ -136,13 +137,13 @@ export default (() => {
}
if (options.pedantic) {
bus.once('logger:warn', recoverableError);
bus.once('logger:error', fatalError);
emitter.once('logger:warn', recoverableError);
emitter.once('logger:error', fatalError);
} else {
bus.once('logger:error', recoverableError);
emitter.once('logger:error', recoverableError);
}
bus.once('logger:fatal', fatalError);
emitter.once('logger:fatal', fatalError);
}
return cli;
@ -177,8 +178,6 @@ 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);
@ -321,7 +320,7 @@ export default (() => {
docletStore = props.parser.parse(env.sourceFiles, options.encoding);
// If there is no package.json, just create an empty package
packageDocs = new Package(props.packageJson);
packageDocs = new Package(props.packageJson, dependencies);
packageDocs.files = env.sourceFiles || [];
docletStore.add(packageDocs);

View File

@ -19,10 +19,8 @@ import { fileURLToPath } from 'node:url';
import { augment } from '@jsdoc/doclet';
import { createParser, handlers } from '@jsdoc/parse';
import { EventBus } from '@jsdoc/util';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const bus = new EventBus('jsdoc');
const originalDictionaries = ['jsdoc', 'closure'];
const packagePath = path.resolve(__dirname, '../..');
const parseResults = [];
@ -36,15 +34,16 @@ const helpers = {
},
createParser: () => createParser(jsdoc.deps),
didLog: (fn, level) => {
const emitter = jsdoc.deps.get('emitter');
const events = [];
function listener(e) {
events.push(e);
}
bus.on(`logger:${level}`, listener);
emitter.on(`logger:${level}`, listener);
fn();
bus.off(`logger:${level}`, listener);
emitter.off(`logger:${level}`, listener);
return events.length !== 0;
},