From 89f2c72da4e5b23f2d2abf12e4cc675cbabedd60 Mon Sep 17 00:00:00 2001 From: Jeff Williams Date: Wed, 15 Nov 2023 17:43:21 -0800 Subject: [PATCH] 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. --- packages/jsdoc-ast/lib/ast-builder.js | 35 +- packages/jsdoc-ast/lib/walker.js | 7 +- .../jsdoc-ast/test/specs/lib/ast-builder.js | 20 +- packages/jsdoc-cli/lib/engine.js | 14 +- packages/jsdoc-cli/test/specs/lib/engine.js | 12 +- packages/jsdoc-cli/test/specs/lib/logger.js | 46 +- packages/jsdoc-doclet/lib/doclet-store.js | 12 +- packages/jsdoc-doclet/lib/doclet.js | 8 +- packages/jsdoc-doclet/lib/package.js | 6 +- .../test/specs/lib/doclet-store.js | 2 +- .../jsdoc-doclet/test/specs/lib/doclet.js | 8 +- .../jsdoc-doclet/test/specs/lib/package.js | 28 +- packages/jsdoc-parse/lib/handlers.js | 3 +- packages/jsdoc-parse/lib/parser.js | 21 +- packages/jsdoc-plugins/source-tag.js | 5 +- packages/jsdoc-tag/lib/definitions/closure.js | 254 ++-- packages/jsdoc-tag/lib/definitions/core.js | 1154 +++++++++-------- packages/jsdoc-tag/lib/definitions/index.js | 12 +- .../jsdoc-tag/lib/definitions/internal.js | 79 +- packages/jsdoc-tag/lib/definitions/jsdoc.js | 2 +- packages/jsdoc-tag/lib/definitions/util.js | 7 +- packages/jsdoc-tag/lib/dictionary.js | 21 +- packages/jsdoc-tag/lib/tag.js | 8 +- packages/jsdoc-tag/lib/validator.js | 2 +- .../test/specs/lib/definitions/index.js | 16 +- .../jsdoc-tag/test/specs/lib/dictionary.js | 23 +- .../jsdoc-tag/test/specs/lib/validator.js | 6 +- .../lib/templateHelper.js | 3 +- .../test/specs/lib/templateHelper.js | 16 +- packages/jsdoc-util/index.js | 7 +- packages/jsdoc-util/lib/log.js | 16 +- packages/jsdoc-util/test/specs/lib/log.js | 23 +- packages/jsdoc/cli.js | 23 +- packages/jsdoc/test/helpers/jsdoc.js | 7 +- 34 files changed, 975 insertions(+), 931 deletions(-) diff --git a/packages/jsdoc-ast/lib/ast-builder.js b/packages/jsdoc-ast/lib/ast-builder.js index eefbac1b..6fc633b2 100644 --- a/packages/jsdoc-ast/lib/ast-builder.js +++ b/packages/jsdoc-ast/lib/ast-builder.js @@ -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; } } diff --git a/packages/jsdoc-ast/lib/walker.js b/packages/jsdoc-ast/lib/walker.js index 59fd4ef3..3cd9342c 100644 --- a/packages/jsdoc-ast/lib/walker.js +++ b/packages/jsdoc-ast/lib/walker.js @@ -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.` ); } diff --git a/packages/jsdoc-ast/test/specs/lib/ast-builder.js b/packages/jsdoc-ast/test/specs/lib/ast-builder.js index 23580578..207fe611 100644 --- a/packages/jsdoc-ast/test/specs/lib/ast-builder.js +++ b/packages/jsdoc-ast/test/specs/lib/ast-builder.js @@ -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(); }); }); }); diff --git a/packages/jsdoc-cli/lib/engine.js b/packages/jsdoc-cli/lib/engine.js index 9c00cba3..3b0631d4 100644 --- a/packages/jsdoc-cli/lib/engine.js +++ b/packages/jsdoc-cli/lib/engine.js @@ -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; } diff --git a/packages/jsdoc-cli/test/specs/lib/engine.js b/packages/jsdoc-cli/test/specs/lib/engine.js index 217f7264..5b079b2d 100644 --- a/packages/jsdoc-cli/test/specs/lib/engine.js +++ b/packages/jsdoc-cli/test/specs/lib/engine.js @@ -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(); diff --git a/packages/jsdoc-cli/test/specs/lib/logger.js b/packages/jsdoc-cli/test/specs/lib/logger.js index 4d9cfd3c..0b433bd6 100644 --- a/packages/jsdoc-cli/test/specs/lib/logger.js +++ b/packages/jsdoc-cli/test/specs/lib/logger.js @@ -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(); }); diff --git a/packages/jsdoc-doclet/lib/doclet-store.js b/packages/jsdoc-doclet/lib/doclet-store.js index 062dc4f7..4e5e29a6 100644 --- a/packages/jsdoc-doclet/lib/doclet-store.js +++ b/packages/jsdoc-doclet/lib/doclet-store.js @@ -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; } diff --git a/packages/jsdoc-doclet/lib/doclet.js b/packages/jsdoc-doclet/lib/doclet.js index 6e231b5d..81573afe 100644 --- a/packages/jsdoc-doclet/lib/doclet.js +++ b/packages/jsdoc-doclet/lib/doclet.js @@ -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'); diff --git a/packages/jsdoc-doclet/lib/package.js b/packages/jsdoc-doclet/lib/package.js index 91e80a4b..89e20fee 100644 --- a/packages/jsdoc-doclet/lib/package.js +++ b/packages/jsdoc-doclet/lib/package.js @@ -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; /** diff --git a/packages/jsdoc-doclet/test/specs/lib/doclet-store.js b/packages/jsdoc-doclet/test/specs/lib/doclet-store.js index 39feb185..3462be0b 100644 --- a/packages/jsdoc-doclet/test/specs/lib/doclet-store.js +++ b/packages/jsdoc-doclet/test/specs/lib/doclet-store.js @@ -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; diff --git a/packages/jsdoc-doclet/test/specs/lib/doclet.js b/packages/jsdoc-doclet/test/specs/lib/doclet.js index 6c0bd39c..efd4df9f 100644 --- a/packages/jsdoc-doclet/test/specs/lib/doclet.js +++ b/packages/jsdoc-doclet/test/specs/lib/doclet.js @@ -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', () => { diff --git a/packages/jsdoc-doclet/test/specs/lib/package.js b/packages/jsdoc-doclet/test/specs/lib/package.js index d793f308..7d5fa4d8 100644 --- a/packages/jsdoc-doclet/test/specs/lib/package.js +++ b/packages/jsdoc-doclet/test/specs/lib/package.js @@ -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'); }); diff --git a/packages/jsdoc-parse/lib/handlers.js b/packages/jsdoc-parse/lib/handlers.js index efc7669e..b836c62e 100644 --- a/packages/jsdoc-parse/lib/handlers.js +++ b/packages/jsdoc-parse/lib/handlers.js @@ -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); } diff --git a/packages/jsdoc-parse/lib/parser.js b/packages/jsdoc-parse/lib/parser.js index 21f9af7b..f8b2ae32 100644 --- a/packages/jsdoc-parse/lib/parser.js +++ b/packages/jsdoc-parse/lib/parser.js @@ -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); } diff --git a/packages/jsdoc-plugins/source-tag.js b/packages/jsdoc-plugins/source-tag.js index cad98a9a..3a4c908c 100644 --- a/packages/jsdoc-plugins/source-tag.js +++ b/packages/jsdoc-plugins/source-tag.js @@ -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 || {}; diff --git a/packages/jsdoc-tag/lib/definitions/closure.js b/packages/jsdoc-tag/lib/definitions/closure.js index bcb2481e..e13e8f98 100644 --- a/packages/jsdoc-tag/lib/definitions/closure.js +++ b/packages/jsdoc-tag/lib/definitions/closure.js @@ -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, + }; }; diff --git a/packages/jsdoc-tag/lib/definitions/core.js b/packages/jsdoc-tag/lib/definitions/core.js index 0ed56d52..39b1e080 100644 --- a/packages/jsdoc-tag/lib/definitions/core.js +++ b/packages/jsdoc-tag/lib/definitions/core.js @@ -29,587 +29,593 @@ function stripModuleNamespace(docletName) { } // Core JSDoc tags that are shared with other tag dictionaries. -export const tags = { - abstract: { - mustNotHaveValue: true, - onTagged(doclet) { - // we call this `virtual` because `abstract` is a reserved word - doclet.virtual = true; +export const getTags = (deps) => { + return { + abstract: { + mustNotHaveValue: true, + onTagged(doclet) { + // we call this `virtual` because `abstract` is a reserved word + doclet.virtual = true; + }, + synonyms: ['virtual'], }, - synonyms: ['virtual'], - }, - access: { - mustHaveValue: true, - onTagged(doclet, { value }) { - // only valid values are package, private, protected and public - if (/^(package|private|protected|public)$/i.test(value)) { - doclet.access = value.toLowerCase(); - } else { - doclet.access = undefined; - } - }, - }, - alias: { - mustHaveValue: true, - onTagged(doclet, { value }) { - doclet.alias = value; - }, - }, - async: { - mustNotHaveValue: true, - onTagged(doclet) { - doclet.async = true; - }, - }, - augments: { - mustHaveValue: true, - // Allow augments value to be specified as a normal type, e.g. {Type} - onTagText: util.parseTypeText, - onTagged(doclet, { value }) { - doclet.augment(util.firstWordOf(value)); - }, - synonyms: ['extends'], - }, - author: { - mustHaveValue: true, - onTagged(doclet, { value }) { - doclet.author ??= []; - doclet.author.push(value); - }, - }, - // This symbol has a member that should use the same docs as another symbol. - borrows: { - mustHaveValue: true, - onTagged(doclet, tag) { - const parsed = util.parseBorrows(doclet, tag); - - doclet.borrow(parsed.target, parsed.source); - }, - }, - class: { - onTagged(doclet, tag) { - let looksLikeDesc; - - doclet.addTag('kind', 'class'); - - // handle special case where both @class and @constructor tags exist in same doclet - if (tag.originalTitle === 'class') { - // multiple words after @class? - looksLikeDesc = (tag.value || '').match(/\S+\s+\S+/); - if ( - (looksLikeDesc || /@construct(s|or)\b/i.test(doclet.comment)) && - !/@classdesc\b/i.test(doclet.comment) - ) { - // treat the @class tag as a @classdesc tag instead - doclet.classdesc = tag.value; - - return; - } - } - - util.setDocletNameToValue(doclet, tag); - }, - synonyms: ['constructor'], - }, - classdesc: { - onTagged(doclet, { value }) { - doclet.classdesc = value; - }, - }, - constant: { - canHaveType: true, - canHaveName: true, - onTagged(doclet, tag) { - util.setDocletKindToTitle(doclet, tag); - util.setDocletNameToValueName(doclet, tag); - util.setDocletTypeToValueType(doclet, tag); - }, - synonyms: ['const'], - }, - constructs: { - onTagged(doclet, { value }) { - let ownerClassName; - - if (!value) { - // this can be resolved later in the handlers - ownerClassName = '{@thisClass}'; - } else { - ownerClassName = util.firstWordOf(value); - } - doclet.addTag('alias', ownerClassName); - doclet.addTag('kind', 'class'); - }, - }, - copyright: { - mustHaveValue: true, - onTagged(doclet, { value }) { - doclet.copyright = value; - }, - }, - default: { - onTagged(doclet, { value }) { - if (value) { - doclet.defaultvalue = value; - } else if (doclet.meta && doclet.meta.code && typeof doclet.meta.code.value !== 'undefined') { - switch (doclet.meta.code.type) { - case Syntax.ArrayExpression: - doclet.defaultvalue = astNode.nodeToValue(doclet.meta.code.node); - doclet.defaultvaluetype = 'array'; - break; - - case Syntax.Literal: - doclet.defaultvalue = doclet.meta.code.value; - break; - - case Syntax.ObjectExpression: - doclet.defaultvalue = astNode.nodeToValue(doclet.meta.code.node); - doclet.defaultvaluetype = 'object'; - break; - - default: - // do nothing - break; - } - } - }, - synonyms: ['defaultvalue'], - }, - deprecated: { - // value is optional - onTagged(doclet, { value }) { - doclet.deprecated = value ?? true; - }, - }, - enum: { - canHaveType: true, - onTagged(doclet, tag) { - doclet.kind ??= 'member'; - doclet.isEnum = true; - util.setDocletTypeToValueType(doclet, tag); - }, - }, - event: { - isNamespace: true, - onTagged(doclet, tag) { - util.setDocletKindToTitle(doclet, tag); - util.setDocletNameToValue(doclet, tag); - }, - }, - example: { - keepsWhitespace: true, - removesIndent: true, - mustHaveValue: true, - onTagged(doclet, { value }) { - doclet.examples ??= []; - doclet.examples.push(value); - }, - }, - exports: { - mustHaveValue: true, - onTagged(doclet, { value }) { - const modName = util.firstWordOf(value); - - // in case the user wrote something like `/** @exports module:foo */`: - doclet.addTag('alias', stripModuleNamespace(modName)); - doclet.addTag('kind', 'module'); - }, - }, - external: { - canHaveType: true, - isNamespace: true, - onTagged(doclet, tag) { - util.setDocletKindToTitle(doclet, tag); - if (tag.value && tag.value.type) { - util.setDocletTypeToValueType(doclet, tag); - doclet.addTag('name', doclet.type.names[0]); - } else { - util.setDocletNameToValue(doclet, tag); - } - }, - synonyms: ['host'], - }, - file: { - onTagged(doclet, tag) { - util.setNameToFile(doclet); - util.setDocletKindToTitle(doclet, tag); - util.setDocletDescriptionToValue(doclet, tag); - - doclet.preserveName = true; - }, - synonyms: ['fileoverview', 'overview'], - }, - fires: { - mustHaveValue: true, - onTagged(doclet, tag) { - doclet.fires ??= []; - util.applyNamespaceToTag('event', tag); - doclet.fires.push(tag.value); - }, - synonyms: ['emits'], - }, - function: { - onTagged(doclet, tag) { - util.setDocletKindToTitle(doclet, tag); - util.setDocletNameToValue(doclet, tag); - }, - synonyms: ['func', 'method'], - }, - generator: { - mustNotHaveValue: true, - onTagged(doclet) { - doclet.generator = true; - }, - }, - global: { - mustNotHaveValue: true, - onTagged(doclet) { - doclet.scope = SCOPE.NAMES.GLOBAL; - doclet.memberof = undefined; - }, - }, - hideconstructor: { - mustNotHaveValue: true, - onTagged(doclet) { - doclet.hideconstructor = true; - }, - }, - ignore: { - mustNotHaveValue: true, - onTagged(doclet) { - doclet.ignore = true; - }, - }, - implements: { - mustHaveValue: true, - onTagText: util.parseTypeText, - onTagged(doclet, { value }) { - doclet.implements ??= []; - doclet.implements.push(value); - }, - }, - inheritdoc: { - mustNotHaveValue: true, - onTagged(doclet) { - // Use an empty string so JSDoc can support `@inheritdoc Foo#bar` in the future. - doclet.inheritdoc = ''; - }, - }, - inner: { - onTagged(doclet, tag) { - util.setDocletScopeToTitle(doclet, tag); - }, - }, - instance: { - onTagged(doclet, tag) { - util.setDocletScopeToTitle(doclet, tag); - }, - }, - interface: { - canHaveName: true, - onTagged(doclet, tag) { - doclet.addTag('kind', 'interface'); - if (tag.value) { - util.setDocletNameToValueName(doclet, tag); - } - }, - }, - lends: { - onTagged(doclet, { value }) { - doclet.alias = value ?? LONGNAMES.GLOBAL; - doclet.addTag('undocumented'); - }, - }, - license: { - mustHaveValue: true, - onTagged(doclet, { value }) { - doclet.license = value; - }, - }, - listens: { - mustHaveValue: true, - onTagged(doclet, tag) { - doclet.listens ??= []; - util.applyNamespaceToTag('event', tag); - doclet.listens.push(tag.value); - }, - }, - member: { - canHaveType: true, - canHaveName: true, - onTagged(doclet, tag) { - util.setDocletKindToTitle(doclet, tag); - util.setDocletNameToValueName(doclet, tag); - util.setDocletTypeToValueType(doclet, tag); - }, - synonyms: ['var'], - }, - memberof: { - mustHaveValue: true, - onTagged(doclet, tag) { - if (tag.originalTitle === 'memberof!') { - doclet.forceMemberof = true; - if (tag.value === LONGNAMES.GLOBAL) { - doclet.addTag('global'); - doclet.memberof = undefined; - } - } - util.setDocletMemberof(doclet, tag); - }, - synonyms: ['memberof!'], - }, - mixes: { - mustHaveValue: true, - onTagged(doclet, { value }) { - const source = util.firstWordOf(value); - - doclet.mix(source); - }, - }, - mixin: { - onTagged(doclet, tag) { - util.setDocletKindToTitle(doclet, tag); - util.setDocletNameToValue(doclet, tag); - }, - }, - modifies: { - canHaveType: true, - onTagged(doclet, { value }) { - doclet.modifies ??= []; - doclet.modifies.push(value); - }, - }, - module: { - canHaveType: true, - isNamespace: true, - onTagged(doclet, tag) { - util.setDocletKindToTitle(doclet, tag); - util.setDocletNameToValue(doclet, tag); - if (!doclet.name) { - util.setDocletNameToFilename(doclet); - } - // in case the user wrote something like `/** @module module:foo */`: - doclet.name = stripModuleNamespace(doclet.name); - - util.setDocletTypeToValueType(doclet, tag); - }, - }, - namespace: { - canHaveType: true, - onTagged(doclet, tag) { - util.setDocletKindToTitle(doclet, tag); - util.setDocletNameToValue(doclet, tag); - util.setDocletTypeToValueType(doclet, tag); - }, - }, - package: { - mustNotHaveValue: true, - onTagged(doclet) { - doclet.access = 'package'; - }, - }, - param: { - canHaveType: true, - canHaveName: true, - onTagged(doclet, { value }) { - doclet.params ??= []; - doclet.params.push(value ?? {}); - }, - synonyms: ['arg', 'argument'], - }, - private: { - mustNotHaveValue: true, - onTagged(doclet) { - doclet.access = 'private'; - }, - }, - property: { - mustHaveValue: true, - canHaveType: true, - canHaveName: true, - onTagged(doclet, { value }) { - doclet.properties ??= []; - doclet.properties.push(value); - }, - synonyms: ['prop'], - }, - protected: { - mustNotHaveValue: true, - onTagged(doclet) { - doclet.access = 'protected'; - }, - }, - public: { - mustNotHaveValue: true, - onTagged(doclet) { - doclet.access = 'public'; - }, - }, - readonly: { - mustNotHaveValue: true, - onTagged(doclet) { - doclet.readonly = true; - }, - }, - requires: { - mustHaveValue: true, - onTagged(doclet, { value }) { - let requiresName; - - // Inline link tags are passed through as-is so that `@requires {@link foo}` works. - if (isInlineTag(value, 'link\\S*')) { - requiresName = value; - } - // Otherwise, assume it's a module. - else { - requiresName = util.firstWordOf(value); - if (!requiresName.match(MODULE_NAMESPACE_REGEXP)) { - requiresName = MODULE_NAMESPACE + requiresName; - } - } - - doclet.requires ??= []; - doclet.requires.push(requiresName); - }, - }, - returns: { - mustHaveValue: true, - canHaveType: true, - onTagged(doclet, { value }) { - doclet.returns ??= []; - doclet.returns.push(value); - }, - synonyms: ['return'], - }, - see: { - mustHaveValue: true, - onTagged(doclet, { value }) { - doclet.see ??= []; - doclet.see.push(value); - }, - }, - since: { - mustHaveValue: true, - onTagged(doclet, { value }) { - doclet.since = value; - }, - }, - static: { - onTagged(doclet, tag) { - util.setDocletScopeToTitle(doclet, tag); - }, - }, - summary: { - mustHaveValue: true, - onTagged(doclet, { value }) { - doclet.summary = value; - }, - }, - this: { - mustHaveValue: true, - onTagged(doclet, { value }) { - doclet.this = util.firstWordOf(value); - }, - }, - throws: { - mustHaveValue: true, - canHaveType: true, - onTagged(doclet, { value }) { - doclet.exceptions ??= []; - doclet.exceptions.push(value); - }, - synonyms: ['exception'], - }, - todo: { - mustHaveValue: true, - onTagged(doclet, { value }) { - doclet.todo ??= []; - doclet.todo.push(value); - }, - }, - type: { - mustHaveValue: true, - mustNotHaveDescription: true, - canHaveType: true, - onTagText(text) { - let closeIdx; - let openIdx; - - const OPEN_BRACE = '{'; - const CLOSE_BRACE = '}'; - - // Remove line breaks. - text = text.replace(/[\f\n\r]/g, ''); - - // Text must be a type expression; for backwards compatibility, we add braces if they're - // missing. But do NOT add braces to things like `@type {string} some pointless text`. - openIdx = text.indexOf(OPEN_BRACE); - closeIdx = text.indexOf(CLOSE_BRACE); - - // A type expression is at least one character long. - if (openIdx !== 0 || closeIdx <= openIdx + 1) { - text = OPEN_BRACE + text + CLOSE_BRACE; - } - - return text; - }, - onTagged(doclet, tag) { - if (tag.value && tag.value.type) { - util.setDocletTypeToValueType(doclet, tag); - - // For backwards compatibility, we interpret `@type` for functions as the return type. - if (doclet.kind === 'function') { - doclet.addTag('returns', tag.text); - } - } - }, - }, - typedef: { - canHaveType: true, - canHaveName: true, - onTagged(doclet, tag) { - util.setDocletKindToTitle(doclet, tag); - - if (tag.value) { - util.setDocletNameToValueName(doclet, tag); - - // Callbacks are always type {function}. - if (tag.originalTitle === 'callback') { - doclet.type = { - names: ['function'], - }; + access: { + mustHaveValue: true, + onTagged(doclet, { value }) { + // only valid values are package, private, protected and public + if (/^(package|private|protected|public)$/i.test(value)) { + doclet.access = value.toLowerCase(); } else { - util.setDocletTypeToValueType(doclet, tag); + doclet.access = undefined; } - } + }, }, - synonyms: ['callback'], - }, - variation: { - mustHaveValue: true, - onTagged(doclet, tag) { - let value = tag.value; - const match = value.match(VARIATION_VALUE_REGEXP); + alias: { + mustHaveValue: true, + onTagged(doclet, { value }) { + doclet.alias = value; + }, + }, + async: { + mustNotHaveValue: true, + onTagged(doclet) { + doclet.async = true; + }, + }, + augments: { + mustHaveValue: true, + // Allow augments value to be specified as a normal type, e.g. {Type} + onTagText: util.parseTypeText, + onTagged(doclet, { value }) { + doclet.augment(util.firstWordOf(value)); + }, + synonyms: ['extends'], + }, + author: { + mustHaveValue: true, + onTagged(doclet, { value }) { + doclet.author ??= []; + doclet.author.push(value); + }, + }, + // This symbol has a member that should use the same docs as another symbol. + borrows: { + mustHaveValue: true, + onTagged(doclet, tag) { + const parsed = util.parseBorrows(doclet, tag); - if (match) { - value = match[1]; - } + doclet.borrow(parsed.target, parsed.source); + }, + }, + class: { + onTagged(doclet, tag) { + let looksLikeDesc; - doclet.variation = value; + doclet.addTag('kind', 'class'); + + // handle special case where both @class and @constructor tags exist in same doclet + if (tag.originalTitle === 'class') { + // multiple words after @class? + looksLikeDesc = (tag.value || '').match(/\S+\s+\S+/); + if ( + (looksLikeDesc || /@construct(s|or)\b/i.test(doclet.comment)) && + !/@classdesc\b/i.test(doclet.comment) + ) { + // treat the @class tag as a @classdesc tag instead + doclet.classdesc = tag.value; + + return; + } + } + + util.setDocletNameToValue(doclet, tag); + }, + synonyms: ['constructor'], }, - }, - version: { - mustHaveValue: true, - onTagged(doclet, { value }) { - doclet.version = value; + classdesc: { + onTagged(doclet, { value }) { + doclet.classdesc = value; + }, }, - }, - yields: { - mustHaveValue: true, - canHaveType: true, - onTagged(doclet, { value }) { - doclet.yields ??= []; - doclet.yields.push(value); + constant: { + canHaveType: true, + canHaveName: true, + onTagged(doclet, tag) { + util.setDocletKindToTitle(doclet, tag); + util.setDocletNameToValueName(doclet, tag); + util.setDocletTypeToValueType(doclet, tag); + }, + synonyms: ['const'], }, - synonyms: ['yield'], - }, + constructs: { + onTagged(doclet, { value }) { + let ownerClassName; + + if (!value) { + // this can be resolved later in the handlers + ownerClassName = '{@thisClass}'; + } else { + ownerClassName = util.firstWordOf(value); + } + doclet.addTag('alias', ownerClassName); + doclet.addTag('kind', 'class'); + }, + }, + copyright: { + mustHaveValue: true, + onTagged(doclet, { value }) { + doclet.copyright = value; + }, + }, + default: { + onTagged(doclet, { value }) { + if (value) { + doclet.defaultvalue = value; + } else if ( + doclet.meta && + doclet.meta.code && + typeof doclet.meta.code.value !== 'undefined' + ) { + switch (doclet.meta.code.type) { + case Syntax.ArrayExpression: + doclet.defaultvalue = astNode.nodeToValue(doclet.meta.code.node); + doclet.defaultvaluetype = 'array'; + break; + + case Syntax.Literal: + doclet.defaultvalue = doclet.meta.code.value; + break; + + case Syntax.ObjectExpression: + doclet.defaultvalue = astNode.nodeToValue(doclet.meta.code.node); + doclet.defaultvaluetype = 'object'; + break; + + default: + // do nothing + break; + } + } + }, + synonyms: ['defaultvalue'], + }, + deprecated: { + // value is optional + onTagged(doclet, { value }) { + doclet.deprecated = value ?? true; + }, + }, + enum: { + canHaveType: true, + onTagged(doclet, tag) { + doclet.kind ??= 'member'; + doclet.isEnum = true; + util.setDocletTypeToValueType(doclet, tag); + }, + }, + event: { + isNamespace: true, + onTagged(doclet, tag) { + util.setDocletKindToTitle(doclet, tag); + util.setDocletNameToValue(doclet, tag); + }, + }, + example: { + keepsWhitespace: true, + removesIndent: true, + mustHaveValue: true, + onTagged(doclet, { value }) { + doclet.examples ??= []; + doclet.examples.push(value); + }, + }, + exports: { + mustHaveValue: true, + onTagged(doclet, { value }) { + const modName = util.firstWordOf(value); + + // in case the user wrote something like `/** @exports module:foo */`: + doclet.addTag('alias', stripModuleNamespace(modName)); + doclet.addTag('kind', 'module'); + }, + }, + external: { + canHaveType: true, + isNamespace: true, + onTagged(doclet, tag) { + util.setDocletKindToTitle(doclet, tag); + if (tag.value && tag.value.type) { + util.setDocletTypeToValueType(doclet, tag); + doclet.addTag('name', doclet.type.names[0]); + } else { + util.setDocletNameToValue(doclet, tag); + } + }, + synonyms: ['host'], + }, + file: { + onTagged(doclet, tag) { + util.setNameToFile(doclet); + util.setDocletKindToTitle(doclet, tag); + util.setDocletDescriptionToValue(doclet, tag); + + doclet.preserveName = true; + }, + synonyms: ['fileoverview', 'overview'], + }, + fires: { + mustHaveValue: true, + onTagged(doclet, tag) { + doclet.fires ??= []; + util.applyNamespaceToTag('event', tag); + doclet.fires.push(tag.value); + }, + synonyms: ['emits'], + }, + function: { + onTagged(doclet, tag) { + util.setDocletKindToTitle(doclet, tag); + util.setDocletNameToValue(doclet, tag); + }, + synonyms: ['func', 'method'], + }, + generator: { + mustNotHaveValue: true, + onTagged(doclet) { + doclet.generator = true; + }, + }, + global: { + mustNotHaveValue: true, + onTagged(doclet) { + doclet.scope = SCOPE.NAMES.GLOBAL; + doclet.memberof = undefined; + }, + }, + hideconstructor: { + mustNotHaveValue: true, + onTagged(doclet) { + doclet.hideconstructor = true; + }, + }, + ignore: { + mustNotHaveValue: true, + onTagged(doclet) { + doclet.ignore = true; + }, + }, + implements: { + mustHaveValue: true, + onTagText: util.parseTypeText, + onTagged(doclet, { value }) { + doclet.implements ??= []; + doclet.implements.push(value); + }, + }, + inheritdoc: { + mustNotHaveValue: true, + onTagged(doclet) { + // Use an empty string so JSDoc can support `@inheritdoc Foo#bar` in the future. + doclet.inheritdoc = ''; + }, + }, + inner: { + onTagged(doclet, tag) { + util.setDocletScopeToTitle(doclet, tag, deps); + }, + }, + instance: { + onTagged(doclet, tag) { + util.setDocletScopeToTitle(doclet, tag, deps); + }, + }, + interface: { + canHaveName: true, + onTagged(doclet, tag) { + doclet.addTag('kind', 'interface'); + if (tag.value) { + util.setDocletNameToValueName(doclet, tag); + } + }, + }, + lends: { + onTagged(doclet, { value }) { + doclet.alias = value ?? LONGNAMES.GLOBAL; + doclet.addTag('undocumented'); + }, + }, + license: { + mustHaveValue: true, + onTagged(doclet, { value }) { + doclet.license = value; + }, + }, + listens: { + mustHaveValue: true, + onTagged(doclet, tag) { + doclet.listens ??= []; + util.applyNamespaceToTag('event', tag); + doclet.listens.push(tag.value); + }, + }, + member: { + canHaveType: true, + canHaveName: true, + onTagged(doclet, tag) { + util.setDocletKindToTitle(doclet, tag); + util.setDocletNameToValueName(doclet, tag); + util.setDocletTypeToValueType(doclet, tag); + }, + synonyms: ['var'], + }, + memberof: { + mustHaveValue: true, + onTagged(doclet, tag) { + if (tag.originalTitle === 'memberof!') { + doclet.forceMemberof = true; + if (tag.value === LONGNAMES.GLOBAL) { + doclet.addTag('global'); + doclet.memberof = undefined; + } + } + util.setDocletMemberof(doclet, tag); + }, + synonyms: ['memberof!'], + }, + mixes: { + mustHaveValue: true, + onTagged(doclet, { value }) { + const source = util.firstWordOf(value); + + doclet.mix(source); + }, + }, + mixin: { + onTagged(doclet, tag) { + util.setDocletKindToTitle(doclet, tag); + util.setDocletNameToValue(doclet, tag); + }, + }, + modifies: { + canHaveType: true, + onTagged(doclet, { value }) { + doclet.modifies ??= []; + doclet.modifies.push(value); + }, + }, + module: { + canHaveType: true, + isNamespace: true, + onTagged(doclet, tag) { + util.setDocletKindToTitle(doclet, tag); + util.setDocletNameToValue(doclet, tag); + if (!doclet.name) { + util.setDocletNameToFilename(doclet); + } + // in case the user wrote something like `/** @module module:foo */`: + doclet.name = stripModuleNamespace(doclet.name); + + util.setDocletTypeToValueType(doclet, tag); + }, + }, + namespace: { + canHaveType: true, + onTagged(doclet, tag) { + util.setDocletKindToTitle(doclet, tag); + util.setDocletNameToValue(doclet, tag); + util.setDocletTypeToValueType(doclet, tag); + }, + }, + package: { + mustNotHaveValue: true, + onTagged(doclet) { + doclet.access = 'package'; + }, + }, + param: { + canHaveType: true, + canHaveName: true, + onTagged(doclet, { value }) { + doclet.params ??= []; + doclet.params.push(value ?? {}); + }, + synonyms: ['arg', 'argument'], + }, + private: { + mustNotHaveValue: true, + onTagged(doclet) { + doclet.access = 'private'; + }, + }, + property: { + mustHaveValue: true, + canHaveType: true, + canHaveName: true, + onTagged(doclet, { value }) { + doclet.properties ??= []; + doclet.properties.push(value); + }, + synonyms: ['prop'], + }, + protected: { + mustNotHaveValue: true, + onTagged(doclet) { + doclet.access = 'protected'; + }, + }, + public: { + mustNotHaveValue: true, + onTagged(doclet) { + doclet.access = 'public'; + }, + }, + readonly: { + mustNotHaveValue: true, + onTagged(doclet) { + doclet.readonly = true; + }, + }, + requires: { + mustHaveValue: true, + onTagged(doclet, { value }) { + let requiresName; + + // Inline link tags are passed through as-is so that `@requires {@link foo}` works. + if (isInlineTag(value, 'link\\S*')) { + requiresName = value; + } + // Otherwise, assume it's a module. + else { + requiresName = util.firstWordOf(value); + if (!requiresName.match(MODULE_NAMESPACE_REGEXP)) { + requiresName = MODULE_NAMESPACE + requiresName; + } + } + + doclet.requires ??= []; + doclet.requires.push(requiresName); + }, + }, + returns: { + mustHaveValue: true, + canHaveType: true, + onTagged(doclet, { value }) { + doclet.returns ??= []; + doclet.returns.push(value); + }, + synonyms: ['return'], + }, + see: { + mustHaveValue: true, + onTagged(doclet, { value }) { + doclet.see ??= []; + doclet.see.push(value); + }, + }, + since: { + mustHaveValue: true, + onTagged(doclet, { value }) { + doclet.since = value; + }, + }, + static: { + onTagged(doclet, tag) { + util.setDocletScopeToTitle(doclet, tag, deps); + }, + }, + summary: { + mustHaveValue: true, + onTagged(doclet, { value }) { + doclet.summary = value; + }, + }, + this: { + mustHaveValue: true, + onTagged(doclet, { value }) { + doclet.this = util.firstWordOf(value); + }, + }, + throws: { + mustHaveValue: true, + canHaveType: true, + onTagged(doclet, { value }) { + doclet.exceptions ??= []; + doclet.exceptions.push(value); + }, + synonyms: ['exception'], + }, + todo: { + mustHaveValue: true, + onTagged(doclet, { value }) { + doclet.todo ??= []; + doclet.todo.push(value); + }, + }, + type: { + mustHaveValue: true, + mustNotHaveDescription: true, + canHaveType: true, + onTagText(text) { + let closeIdx; + let openIdx; + + const OPEN_BRACE = '{'; + const CLOSE_BRACE = '}'; + + // Remove line breaks. + text = text.replace(/[\f\n\r]/g, ''); + + // Text must be a type expression; for backwards compatibility, we add braces if they're + // missing. But do NOT add braces to things like `@type {string} some pointless text`. + openIdx = text.indexOf(OPEN_BRACE); + closeIdx = text.indexOf(CLOSE_BRACE); + + // A type expression is at least one character long. + if (openIdx !== 0 || closeIdx <= openIdx + 1) { + text = OPEN_BRACE + text + CLOSE_BRACE; + } + + return text; + }, + onTagged(doclet, tag) { + if (tag.value && tag.value.type) { + util.setDocletTypeToValueType(doclet, tag); + + // For backwards compatibility, we interpret `@type` for functions as the return type. + if (doclet.kind === 'function') { + doclet.addTag('returns', tag.text); + } + } + }, + }, + typedef: { + canHaveType: true, + canHaveName: true, + onTagged(doclet, tag) { + util.setDocletKindToTitle(doclet, tag); + + if (tag.value) { + util.setDocletNameToValueName(doclet, tag); + + // Callbacks are always type {function}. + if (tag.originalTitle === 'callback') { + doclet.type = { + names: ['function'], + }; + } else { + util.setDocletTypeToValueType(doclet, tag); + } + } + }, + synonyms: ['callback'], + }, + variation: { + mustHaveValue: true, + onTagged(doclet, tag) { + let value = tag.value; + const match = value.match(VARIATION_VALUE_REGEXP); + + if (match) { + value = match[1]; + } + + doclet.variation = value; + }, + }, + version: { + mustHaveValue: true, + onTagged(doclet, { value }) { + doclet.version = value; + }, + }, + yields: { + mustHaveValue: true, + canHaveType: true, + onTagged(doclet, { value }) { + doclet.yields ??= []; + doclet.yields.push(value); + }, + synonyms: ['yield'], + }, + }; }; diff --git a/packages/jsdoc-tag/lib/definitions/index.js b/packages/jsdoc-tag/lib/definitions/index.js index 9773b12f..90503ee2 100644 --- a/packages/jsdoc-tag/lib/definitions/index.js +++ b/packages/jsdoc-tag/lib/definitions/index.js @@ -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 }; diff --git a/packages/jsdoc-tag/lib/definitions/internal.js b/packages/jsdoc-tag/lib/definitions/internal.js index 4b91ad60..8806919e 100644 --- a/packages/jsdoc-tag/lib/definitions/internal.js +++ b/packages/jsdoc-tag/lib/definitions/internal.js @@ -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 = ''; + }, }, - }, + }; }; diff --git a/packages/jsdoc-tag/lib/definitions/jsdoc.js b/packages/jsdoc-tag/lib/definitions/jsdoc.js index 246e1a43..026e31d2 100644 --- a/packages/jsdoc-tag/lib/definitions/jsdoc.js +++ b/packages/jsdoc-tag/lib/definitions/jsdoc.js @@ -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'; diff --git a/packages/jsdoc-tag/lib/definitions/util.js b/packages/jsdoc-tag/lib/definitions/util.js index ba8689f6..e2bf5e4a 100644 --- a/packages/jsdoc-tag/lib/definitions/util.js +++ b/packages/jsdoc-tag/lib/definitions/util.js @@ -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); } } diff --git a/packages/jsdoc-tag/lib/dictionary.js b/packages/jsdoc-tag/lib/dictionary.js index b767cc09..7119e735 100644 --- a/packages/jsdoc-tag/lib/dictionary.js +++ b/packages/jsdoc-tag/lib/dictionary.js @@ -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; diff --git a/packages/jsdoc-tag/lib/tag.js b/packages/jsdoc-tag/lib/tag.js index 4ac085a6..f2698870 100644 --- a/packages/jsdoc-tag/lib/tag.js +++ b/packages/jsdoc-tag/lib/tag.js @@ -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 diff --git a/packages/jsdoc-tag/lib/validator.js b/packages/jsdoc-tag/lib/validator.js index cef0d0d0..edd10b87 100644 --- a/packages/jsdoc-tag/lib/validator.js +++ b/packages/jsdoc-tag/lib/validator.js @@ -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 diff --git a/packages/jsdoc-tag/test/specs/lib/definitions/index.js b/packages/jsdoc-tag/test/specs/lib/definitions/index.js index 4ac6f077..7c6edf6c 100644 --- a/packages/jsdoc-tag/test/specs/lib/definitions/index.js +++ b/packages/jsdoc-tag/test/specs/lib/definitions/index.js @@ -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/. diff --git a/packages/jsdoc-tag/test/specs/lib/dictionary.js b/packages/jsdoc-tag/test/specs/lib/dictionary.js index 6387a79c..9bb3565b 100644 --- a/packages/jsdoc-tag/test/specs/lib/dictionary.js +++ b/packages/jsdoc-tag/test/specs/lib/dictionary.js @@ -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'); diff --git a/packages/jsdoc-tag/test/specs/lib/validator.js b/packages/jsdoc-tag/test/specs/lib/validator.js index b43e628f..506c865c 100644 --- a/packages/jsdoc-tag/test/specs/lib/validator.js +++ b/packages/jsdoc-tag/test/specs/lib/validator.js @@ -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); diff --git a/packages/jsdoc-template-legacy/lib/templateHelper.js b/packages/jsdoc-template-legacy/lib/templateHelper.js index 1e0b9ecb..b66e84a3 100644 --- a/packages/jsdoc-template-legacy/lib/templateHelper.js +++ b/packages/jsdoc-template-legacy/lib/templateHelper.js @@ -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; } diff --git a/packages/jsdoc-template-legacy/test/specs/lib/templateHelper.js b/packages/jsdoc-template-legacy/test/specs/lib/templateHelper.js index 94205421..db227858 100644 --- a/packages/jsdoc-template-legacy/test/specs/lib/templateHelper.js +++ b/packages/jsdoc-template-legacy/test/specs/lib/templateHelper.js @@ -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 as a type application', () => { - function linkto() { - helper.linkto('~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('~foo'); + + expect(events).toBeEmptyArray(); + + emitter.off('logger:error', storeEvent); }); it('does not treat a longname with a variation as a type application', () => { diff --git a/packages/jsdoc-util/index.js b/packages/jsdoc-util/index.js index 8cdcf04d..2fcee2f9 100644 --- a/packages/jsdoc-util/index.js +++ b/packages/jsdoc-util/index.js @@ -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 }; diff --git a/packages/jsdoc-util/lib/log.js b/packages/jsdoc-util/lib/log.js index 7352fbdf..1b15d1dc 100644 --- a/packages/jsdoc-util/lib/log.js +++ b/packages/jsdoc-util/lib/log.js @@ -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; +} diff --git a/packages/jsdoc-util/test/specs/lib/log.js b/packages/jsdoc-util/test/specs/lib/log.js index c2dc3873..40876db4 100644 --- a/packages/jsdoc-util/test/specs/lib/log.js +++ b/packages/jsdoc-util/test/specs/lib/log.js @@ -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'); diff --git a/packages/jsdoc/cli.js b/packages/jsdoc/cli.js index 7d22eda4..3ff627f5 100644 --- a/packages/jsdoc/cli.js +++ b/packages/jsdoc/cli.js @@ -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); diff --git a/packages/jsdoc/test/helpers/jsdoc.js b/packages/jsdoc/test/helpers/jsdoc.js index 32a179de..09101a3a 100644 --- a/packages/jsdoc/test/helpers/jsdoc.js +++ b/packages/jsdoc/test/helpers/jsdoc.js @@ -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; },