mirror of
https://github.com/jsdoc/jsdoc.git
synced 2025-12-08 19:46:11 +00:00
feat: add isGlobal() and isVisible() methods to Doclet
This commit is contained in:
parent
e926a3f300
commit
f5865d3e59
@ -35,7 +35,10 @@ const {
|
||||
} = jsdocName;
|
||||
const { isFunction } = astNode;
|
||||
|
||||
const ACCESS_LEVELS = ['package', 'private', 'protected', 'public'];
|
||||
const DEFAULT_SCOPE = SCOPE.NAMES.STATIC;
|
||||
// TODO: `class` should be on this list, right? What are the implications of adding it?
|
||||
const GLOBAL_KINDS = ['constant', 'function', 'member', 'typedef'];
|
||||
|
||||
function fakeMeta(node) {
|
||||
return {
|
||||
@ -370,6 +373,9 @@ function copySpecificProperties(primary, secondary, target, include) {
|
||||
* @alias module:@jsdoc/doclet.Doclet
|
||||
*/
|
||||
export class Doclet {
|
||||
#accessConfig;
|
||||
#dictionary;
|
||||
|
||||
/**
|
||||
* Create a doclet.
|
||||
*
|
||||
@ -381,6 +387,8 @@ export class Doclet {
|
||||
let newTags = [];
|
||||
|
||||
meta = meta || {};
|
||||
this.#accessConfig = dependencies.get('config')?.opts?.access ?? [];
|
||||
this.#dictionary = dependencies.get('tags');
|
||||
/** The original text of the comment from the source code. */
|
||||
this.comment = docletSrc;
|
||||
Object.defineProperty(this, 'dependencies', {
|
||||
@ -441,8 +449,7 @@ export class Doclet {
|
||||
* @param {string} [text] - The text of the tag being added.
|
||||
*/
|
||||
addTag(title, text) {
|
||||
const dictionary = this.dependencies.get('tags');
|
||||
const tagDef = dictionary.lookUp(title);
|
||||
const tagDef = this.#dictionary.lookUp(title);
|
||||
const newTag = new Tag(title, text, this.meta, this.dependencies);
|
||||
|
||||
if (tagDef && tagDef.onTagged) {
|
||||
@ -455,6 +462,75 @@ export class Doclet {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the doclet represents a globally available symbol.
|
||||
*
|
||||
* @returns {boolean} `true` if the doclet represents a global; `false` otherwise.
|
||||
*/
|
||||
isGlobal() {
|
||||
return this.scope === 'global' && GLOBAL_KINDS.includes(this.kind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the doclet should be used to generate output.
|
||||
*
|
||||
* @returns {boolean} `true` if the doclet should be used to generate output; `false` otherwise.
|
||||
*/
|
||||
isVisible() {
|
||||
const accessConfig = this.#accessConfig;
|
||||
|
||||
// By default, we don't use:
|
||||
//
|
||||
// + Doclets that explicitly declare that they should be ignored
|
||||
// + Doclets that claim to belong to an anonymous scope
|
||||
// + "Undocumented" doclets (usually code with no JSDoc comment; might also include some odd
|
||||
// artifacts of the parsing process)
|
||||
if (this.ignore === true || this.memberof === '<anonymous>' || this.undocumented === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We also don't use private doclets by default, unless the user told us to use them.
|
||||
if (
|
||||
(!accessConfig.length ||
|
||||
(!accessConfig.includes('all') && !accessConfig.includes('private'))) &&
|
||||
this.access === 'private'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (accessConfig.length && !accessConfig.includes('all')) {
|
||||
// The string `undefined` needs special treatment.
|
||||
if (
|
||||
!accessConfig.includes('undefined') &&
|
||||
(this.access === null || this.access === undefined)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// For other access levels, we can just check whether the user asked us to use that level.
|
||||
if (ACCESS_LEVELS.some((level) => !accessConfig.includes(level) && this.access === level)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the doclet's `longname` property.
|
||||
*
|
||||
* @param {string} longname - The longname for the doclet.
|
||||
*/
|
||||
setLongname(longname) {
|
||||
/**
|
||||
* The fully resolved symbol name.
|
||||
* @type {string}
|
||||
*/
|
||||
this.longname = removeGlobal(longname);
|
||||
if (this.#dictionary.isNamespace(this.kind)) {
|
||||
this.longname = applyNamespace(this.longname, this.kind);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the doclet's `memberof` property.
|
||||
*
|
||||
@ -470,24 +546,6 @@ export class Doclet {
|
||||
.replace(/\.prototype/g, SCOPE.PUNC.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the doclet's `longname` property.
|
||||
*
|
||||
* @param {string} longname - The longname for the doclet.
|
||||
*/
|
||||
setLongname(longname) {
|
||||
const dictionary = this.dependencies.get('tags');
|
||||
|
||||
/**
|
||||
* The fully resolved symbol name.
|
||||
* @type {string}
|
||||
*/
|
||||
this.longname = removeGlobal(longname);
|
||||
if (dictionary.isNamespace(this.kind)) {
|
||||
this.longname = applyNamespace(this.longname, this.kind);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the doclet's `scope` property. Must correspond to a scope name that is defined in
|
||||
* {@link module:@jsdoc/core.name.SCOPE.NAMES}.
|
||||
|
||||
@ -18,7 +18,9 @@ import { name } from '@jsdoc/core';
|
||||
import _ from 'lodash';
|
||||
|
||||
import * as doclet from '../../../lib/doclet.js';
|
||||
import { DOCLET_SCHEMA } from '../../../lib/schema.js';
|
||||
|
||||
const ACCESS_VALUES = DOCLET_SCHEMA.properties.access.enum.concat([undefined]);
|
||||
const { Doclet } = doclet;
|
||||
const { SCOPE } = name;
|
||||
|
||||
@ -36,7 +38,79 @@ describe('@jsdoc/doclet/lib/doclet', () => {
|
||||
expect(doclet.Doclet).toBeFunction();
|
||||
});
|
||||
|
||||
describe('combineDoclets', () => {
|
||||
it('overrides most properties of the secondary doclet', () => {
|
||||
const primaryDoclet = new Doclet(
|
||||
'/** New and improved!\n@version 2.0.0 */',
|
||||
null,
|
||||
jsdoc.deps
|
||||
);
|
||||
const secondaryDoclet = new Doclet('/** Hello!\n@version 1.0.0 */', null, jsdoc.deps);
|
||||
const newDoclet = doclet.combineDoclets(primaryDoclet, secondaryDoclet);
|
||||
|
||||
Object.getOwnPropertyNames(newDoclet).forEach((property) => {
|
||||
expect(newDoclet[property]).toEqual(primaryDoclet[property]);
|
||||
});
|
||||
});
|
||||
|
||||
it('adds properties from the secondary doclet that are missing', () => {
|
||||
const primaryDoclet = new Doclet('/** Hello!\n@version 2.0.0 */', null, jsdoc.deps);
|
||||
const secondaryDoclet = new Doclet('/** Hello! */', null, jsdoc.deps);
|
||||
const newDoclet = doclet.combineDoclets(primaryDoclet, secondaryDoclet);
|
||||
|
||||
expect(newDoclet.version).toBe('2.0.0');
|
||||
});
|
||||
|
||||
describe('params and properties', () => {
|
||||
const properties = ['params', 'properties'];
|
||||
|
||||
it('uses params and properties from the secondary doclet if the primary lacks them', () => {
|
||||
const primaryDoclet = new Doclet('/** Hello! */', null, jsdoc.deps);
|
||||
const secondaryComment = [
|
||||
'/**',
|
||||
' * @param {string} foo - The foo.',
|
||||
' * @property {number} bar - The bar.',
|
||||
' */',
|
||||
].join('\n');
|
||||
const secondaryDoclet = new Doclet(secondaryComment, null, jsdoc.deps);
|
||||
const newDoclet = doclet.combineDoclets(primaryDoclet, secondaryDoclet);
|
||||
|
||||
properties.forEach((property) => {
|
||||
expect(newDoclet[property]).toEqual(secondaryDoclet[property]);
|
||||
});
|
||||
});
|
||||
|
||||
it('uses params and properties from the primary doclet, if present', () => {
|
||||
const primaryComment = [
|
||||
'/**',
|
||||
' * @param {number} baz - The baz.',
|
||||
' * @property {string} qux - The qux.',
|
||||
' */',
|
||||
].join('\n');
|
||||
const primaryDoclet = new Doclet(primaryComment, null, jsdoc.deps);
|
||||
const secondaryComment = [
|
||||
'/**',
|
||||
' * @param {string} foo - The foo.',
|
||||
' * @property {number} bar - The bar.',
|
||||
' */',
|
||||
].join('\n');
|
||||
const secondaryDoclet = new Doclet(secondaryComment, null, jsdoc.deps);
|
||||
const newDoclet = doclet.combineDoclets(primaryDoclet, secondaryDoclet);
|
||||
|
||||
properties.forEach((property) => {
|
||||
expect(newDoclet[property]).toEqual(primaryDoclet[property]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Doclet', () => {
|
||||
function makeDoclet(tagStrings, deps) {
|
||||
const comment = `/**\n${tagStrings.join('\n')}\n*/`;
|
||||
|
||||
return new Doclet(comment, {}, deps || jsdoc.deps);
|
||||
}
|
||||
|
||||
const docSet = jsdoc.getDocSetFromFile('test/fixtures/doclet.js');
|
||||
const testDoclet = docSet.getByLongname('test2')[0];
|
||||
|
||||
@ -51,15 +125,9 @@ describe('@jsdoc/doclet/lib/doclet', () => {
|
||||
expect(descriptor.enumerable).toBeFalse();
|
||||
});
|
||||
|
||||
// TODO: more tests (namespaces, modules, etc.)
|
||||
// TODO: more tests (namespaces, modules, etc.); fold into `postProcess()` tests if that's
|
||||
// really what we're testing here
|
||||
describe('name resolution', () => {
|
||||
// TODO: Load fixtures instead of creating doclets manually
|
||||
function makeDoclet(tagStrings) {
|
||||
const comment = `/**\n${tagStrings.join('\n')}\n*/`;
|
||||
|
||||
return new Doclet(comment, {}, jsdoc.deps);
|
||||
}
|
||||
|
||||
describe('aliases', () => {
|
||||
// TODO: This comment implies that we _don't_ need to set doclet.name...
|
||||
// If `doclet.alias` is defined, `doclet.name` will be set to the same value by the
|
||||
@ -219,93 +287,205 @@ describe('@jsdoc/doclet/lib/doclet', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setScope', () => {
|
||||
it('accepts the correct scope names', () => {
|
||||
function setScope(scopeName) {
|
||||
const newDoclet = new Doclet('/** Huzzah, a doclet! */', null, jsdoc.deps);
|
||||
xdescribe('addTag', () => {
|
||||
xit('TODO: write tests');
|
||||
});
|
||||
|
||||
newDoclet.setScope(scopeName);
|
||||
}
|
||||
xdescribe('augment', () => {
|
||||
xit('TODO: write tests');
|
||||
});
|
||||
|
||||
_.values(SCOPE.NAMES).forEach((scopeName) => {
|
||||
expect(setScope.bind(null, scopeName)).not.toThrow();
|
||||
xdescribe('borrow', () => {
|
||||
xit('TODO: write tests');
|
||||
});
|
||||
|
||||
describe('isGlobal', () => {
|
||||
it('identifies global constants', () => {
|
||||
const newDoclet = makeDoclet(['@constant', '@global', '@name foo']);
|
||||
|
||||
expect(newDoclet.isGlobal()).toBeTrue();
|
||||
});
|
||||
|
||||
it('identifies global functions', () => {
|
||||
const newDoclet = makeDoclet(['@function', '@global', '@name foo']);
|
||||
|
||||
expect(newDoclet.isGlobal()).toBeTrue();
|
||||
});
|
||||
|
||||
it('identifies global members', () => {
|
||||
const newDoclet = makeDoclet(['@global', '@member', '@name foo']);
|
||||
|
||||
expect(newDoclet.isGlobal()).toBeTrue();
|
||||
});
|
||||
|
||||
it('identifies global typedefs', () => {
|
||||
const newDoclet = makeDoclet(['@global', '@name foo', '@typedef']);
|
||||
|
||||
expect(newDoclet.isGlobal()).toBeTrue();
|
||||
});
|
||||
|
||||
it('does not say a doclet is global if its scope is not `global`', () => {
|
||||
const newDoclet = makeDoclet(['@name foo', '@static', '@typedef']);
|
||||
|
||||
expect(newDoclet.isGlobal()).toBeFalse();
|
||||
});
|
||||
|
||||
it('does not say a doclet is global if it has the wrong `kind`', () => {
|
||||
const newDoclet = makeDoclet(['@name foo', '@param', '@static']);
|
||||
|
||||
expect(newDoclet.isGlobal()).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error for invalid scope names', () => {
|
||||
function setScope() {
|
||||
const newDoclet = new Doclet('/** Woe betide this doclet. */', null, jsdoc.deps);
|
||||
describe('isVisible', () => {
|
||||
function makeDeps(access) {
|
||||
const config = _.cloneDeep(jsdoc.deps.get('config'));
|
||||
const map = new Map();
|
||||
|
||||
newDoclet.setScope('fiddlesticks');
|
||||
if (access) {
|
||||
config.opts.access = access.slice();
|
||||
}
|
||||
map.set('config', config);
|
||||
map.set('tags', jsdoc.deps.get('tags'));
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
expect(setScope).toThrow();
|
||||
});
|
||||
});
|
||||
it('returns `false` for ignored doclets', () => {
|
||||
const newDoclet = makeDoclet(['@ignore', '@name foo', '@function']);
|
||||
|
||||
describe('combineDoclets', () => {
|
||||
it('overrides most properties of the secondary doclet', () => {
|
||||
const primaryDoclet = new Doclet(
|
||||
'/** New and improved!\n@version 2.0.0 */',
|
||||
null,
|
||||
jsdoc.deps
|
||||
);
|
||||
const secondaryDoclet = new Doclet('/** Hello!\n@version 1.0.0 */', null, jsdoc.deps);
|
||||
const newDoclet = doclet.combineDoclets(primaryDoclet, secondaryDoclet);
|
||||
|
||||
Object.getOwnPropertyNames(newDoclet).forEach((property) => {
|
||||
expect(newDoclet[property]).toEqual(primaryDoclet[property]);
|
||||
expect(newDoclet.isVisible()).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
it('adds properties from the secondary doclet that are missing', () => {
|
||||
const primaryDoclet = new Doclet('/** Hello!\n@version 2.0.0 */', null, jsdoc.deps);
|
||||
const secondaryDoclet = new Doclet('/** Hello! */', null, jsdoc.deps);
|
||||
const newDoclet = doclet.combineDoclets(primaryDoclet, secondaryDoclet);
|
||||
it('returns `false` for undocumented doclets', () => {
|
||||
const newDoclet = makeDoclet(['@name foo', '@function']);
|
||||
|
||||
expect(newDoclet.version).toBe('2.0.0');
|
||||
});
|
||||
newDoclet.undocumented = true;
|
||||
|
||||
describe('params and properties', () => {
|
||||
const properties = ['params', 'properties'];
|
||||
expect(newDoclet.isVisible()).toBeFalse();
|
||||
});
|
||||
|
||||
it('uses params and properties from the secondary doclet if the primary lacks them', () => {
|
||||
const primaryDoclet = new Doclet('/** Hello! */', null, jsdoc.deps);
|
||||
const secondaryComment = [
|
||||
'/**',
|
||||
' * @param {string} foo - The foo.',
|
||||
' * @property {number} bar - The bar.',
|
||||
' */',
|
||||
].join('\n');
|
||||
const secondaryDoclet = new Doclet(secondaryComment, null, jsdoc.deps);
|
||||
const newDoclet = doclet.combineDoclets(primaryDoclet, secondaryDoclet);
|
||||
it('returns `false` for members of anonymous scopes', () => {
|
||||
const newDoclet = makeDoclet(['@name foo', '@function']);
|
||||
|
||||
properties.forEach((property) => {
|
||||
expect(newDoclet[property]).toEqual(secondaryDoclet[property]);
|
||||
newDoclet.memberof = '<anonymous>';
|
||||
|
||||
expect(newDoclet.isVisible()).toBeFalse();
|
||||
});
|
||||
|
||||
describe('access', () => {
|
||||
it('returns `false` for `private` doclets by default', () => {
|
||||
const newDoclet = makeDoclet(['@name foo', '@function', '@private']);
|
||||
|
||||
expect(newDoclet.isVisible()).toBeFalse();
|
||||
});
|
||||
|
||||
it('returns `true` with `access === undefined` by default', () => {
|
||||
const newDoclet = makeDoclet(['@name foo', '@function']);
|
||||
|
||||
// Just to be sure.
|
||||
delete newDoclet.access;
|
||||
|
||||
expect(newDoclet.isVisible()).toBeTrue();
|
||||
});
|
||||
|
||||
it('always returns `true` based on `doclet.access` when `access` config includes `all`', () => {
|
||||
const fakeDeps = makeDeps(['all']);
|
||||
const doclets = ACCESS_VALUES.map((value) => {
|
||||
let newDoclet;
|
||||
const tags = ['@function', '@name foo'];
|
||||
|
||||
if (value) {
|
||||
tags.push('@' + value);
|
||||
}
|
||||
newDoclet = makeDoclet(tags, fakeDeps);
|
||||
// Just to be sure.
|
||||
if (!value) {
|
||||
delete newDoclet.access;
|
||||
}
|
||||
|
||||
return newDoclet;
|
||||
});
|
||||
|
||||
doclets.forEach((d) => {
|
||||
expect(d.isVisible()).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns `false` for `package` doclets when config omits `package`', () => {
|
||||
const fakeDeps = makeDeps(['public']);
|
||||
const newDoclet = makeDoclet(['@function', '@name foo', '@package'], fakeDeps);
|
||||
|
||||
expect(newDoclet.isVisible()).toBeFalse();
|
||||
});
|
||||
|
||||
it('returns `false` for `protected` doclets when config omits `protected`', () => {
|
||||
const fakeDeps = makeDeps(['public']);
|
||||
const newDoclet = makeDoclet(['@function', '@name foo', '@protected'], fakeDeps);
|
||||
|
||||
expect(newDoclet.isVisible()).toBeFalse();
|
||||
});
|
||||
|
||||
it('returns `false` for `public` doclets when config omits `public`', () => {
|
||||
const fakeDeps = makeDeps(['private']);
|
||||
const newDoclet = makeDoclet(['@function', '@name foo', '@public'], fakeDeps);
|
||||
|
||||
expect(newDoclet.isVisible()).toBeFalse();
|
||||
});
|
||||
|
||||
it('returns `false` for undefined-access doclets when config omits `undefined`', () => {
|
||||
const fakeDeps = makeDeps(['public']);
|
||||
const newDoclet = makeDoclet(['@function', '@name foo'], fakeDeps);
|
||||
|
||||
// Just to be sure.
|
||||
delete newDoclet.access;
|
||||
|
||||
expect(newDoclet.isVisible()).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
it('uses params and properties from the primary doclet, if present', () => {
|
||||
const primaryComment = [
|
||||
'/**',
|
||||
' * @param {number} baz - The baz.',
|
||||
' * @property {string} qux - The qux.',
|
||||
' */',
|
||||
].join('\n');
|
||||
const primaryDoclet = new Doclet(primaryComment, null, jsdoc.deps);
|
||||
const secondaryComment = [
|
||||
'/**',
|
||||
' * @param {string} foo - The foo.',
|
||||
' * @property {number} bar - The bar.',
|
||||
' */',
|
||||
].join('\n');
|
||||
const secondaryDoclet = new Doclet(secondaryComment, null, jsdoc.deps);
|
||||
const newDoclet = doclet.combineDoclets(primaryDoclet, secondaryDoclet);
|
||||
xdescribe('mix', () => {
|
||||
xit('TODO: write tests');
|
||||
});
|
||||
|
||||
properties.forEach((property) => {
|
||||
expect(newDoclet[property]).toEqual(primaryDoclet[property]);
|
||||
xdescribe('postProcess', () => {
|
||||
xit('TODO: write tests');
|
||||
});
|
||||
|
||||
xdescribe('setLongname', () => {
|
||||
xit('TODO: write tests');
|
||||
});
|
||||
|
||||
xdescribe('setMemberof', () => {
|
||||
xit('TODO: write tests');
|
||||
});
|
||||
|
||||
xdescribe('setMeta', () => {
|
||||
xit('TODO: write tests');
|
||||
});
|
||||
|
||||
describe('setScope', () => {
|
||||
it('accepts the correct scope names', () => {
|
||||
function setScope(scopeName) {
|
||||
const newDoclet = new Doclet('/** Huzzah, a doclet! */', null, jsdoc.deps);
|
||||
|
||||
newDoclet.setScope(scopeName);
|
||||
}
|
||||
|
||||
_.values(SCOPE.NAMES).forEach((scopeName) => {
|
||||
expect(setScope.bind(null, scopeName)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error for invalid scope names', () => {
|
||||
function setScope() {
|
||||
const newDoclet = new Doclet('/** Woe betide this doclet. */', null, jsdoc.deps);
|
||||
|
||||
newDoclet.setScope('fiddlesticks');
|
||||
}
|
||||
|
||||
expect(setScope).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user