chore: use Prettier to format source files

This commit is contained in:
Jeff Williams 2021-09-19 13:20:31 -07:00
parent 3025520e15
commit 1305499207
277 changed files with 38661 additions and 21761 deletions

View File

@ -9,4 +9,4 @@ indent_size = 2
[{**/*.js,**/*.css,**/*.json}] [{**/*.js,**/*.css,**/*.json}]
indent_style = space indent_style = space
indent_size = 4 indent_size = 2

View File

@ -1,3 +1,3 @@
module.exports = { module.exports = {
extends: '@jsdoc' extends: ['@jsdoc', 'plugin:prettier/recommended'],
}; };

View File

@ -40,8 +40,8 @@ Your debug output here
### Your environment ### Your environment
| Software | Version | Software | Version |
| ---------------- | ------- | ---------------- | ------- |
| JSDoc | | JSDoc |
| Node.js | | Node.js |
| npm | | npm |

View File

@ -5,14 +5,14 @@ https://github.com/jsdoc3/jsdoc/blob/master/CONTRIBUTING.md
https://github.com/jsdoc3/jsdoc/blob/master/CODE_OF_CONDUCT.md https://github.com/jsdoc3/jsdoc/blob/master/CODE_OF_CONDUCT.md
--> -->
| Q | A | Q | A |
| ---------------- | --- | ---------------- | ---------------------------------------------------------------- |
| Bug fix? | yes/no | Bug fix? | yes/no |
| New feature? | yes/no | New feature? | yes/no |
| Breaking change? | yes/no | Breaking change? | yes/no |
| Deprecations? | yes/no | Deprecations? | yes/no |
| Tests added? | yes/no | Tests added? | yes/no |
| Fixed issues | comma-separated list of issues fixed by the pull request, if any | Fixed issues | comma-separated list of issues fixed by the pull request, if any |
| License | Apache-2.0 | License | Apache-2.0 |
<!-- Describe your changes below in as much detail as possible. --> <!-- Describe your changes below in as much detail as possible. -->

5
.prettierignore Normal file
View File

@ -0,0 +1,5 @@
# Ignore test fixtures.
**/test/fixtures/**
# Ignore code coverage reports.
.nyc_output/

3
.prettierrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
...require('./packages/jsdoc-prettier-config'),
};

View File

@ -1,12 +1,8 @@
{ {
"extends": [ "extends": ["config:base"],
"config:base" "statusCheckVerify": true,
], "ignoreDeps": ["taffydb"],
"statusCheckVerify": true, "automerge": true,
"ignoreDeps": [ "automergeType": "branch",
"taffydb" "rangeStrategy": "bump"
],
"automerge": true,
"automergeType": "branch",
"rangeStrategy": "bump"
} }

1170
CHANGES.md

File diff suppressed because it is too large Load Diff

View File

@ -11,20 +11,20 @@ of experience, nationality, personal appearance, race, religion, or sexual ident
Examples of behavior that contributes to creating a positive environment include: Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language - Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences - Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism - Gracefully accepting constructive criticism
* Focusing on what is best for the community - Focusing on what is best for the community
* Showing empathy towards other community members - Showing empathy towards other community members
Examples of unacceptable behavior by participants include: Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances - The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks - Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment - Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit - Publishing others' private information, such as a physical or electronic address, without explicit
permission permission
* Other conduct which could reasonably be considered inappropriate in a professional setting - Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities ## Our Responsibilities

View File

@ -1,69 +1,68 @@
Pull Requests ## Pull Requests
-------------
If you're thinking about making some changes, maybe fixing a bug, or adding a If you're thinking about making some changes, maybe fixing a bug, or adding a
snazzy new feature, first, thank you. Contributions are very welcome. Things snazzy new feature, first, thank you. Contributions are very welcome. Things
need to be manageable for the maintainers, however. So below you'll find **The need to be manageable for the maintainers, however. So below you'll find **The
fastest way to get your pull request merged in.** Some things, particularly how fastest way to get your pull request merged in.** Some things, particularly how
you set up your branches and work with git, are just suggestions, but pretty good you set up your branches and work with git, are just suggestions, but pretty good
ones. ones.
1. **Create a remote to track the base jsdoc/jsdoc repository** 1. **Create a remote to track the base jsdoc/jsdoc repository**
This is just a convenience to make it easier to update your ```<tracking branch>``` This is just a convenience to make it easier to update your `<tracking branch>`
(more on that shortly). You would execute something like: (more on that shortly). You would execute something like:
git remote add base git://github.com/jsdoc/jsdoc.git git remote add base git://github.com/jsdoc/jsdoc.git
Here 'base' is the name of the remote. Feel free to use whatever you want. Here 'base' is the name of the remote. Feel free to use whatever you want.
2. **Set up a tracking branch for the base repository** 2. **Set up a tracking branch for the base repository**
We're gonna call this your ```<tracking branch>```. You will only ever update We're gonna call this your `<tracking branch>`. You will only ever update
this branch by pulling from the 'base' remote. (as opposed to 'origin') this branch by pulling from the 'base' remote. (as opposed to 'origin')
git branch --track pullpost base/master git branch --track pullpost base/master
git checkout pullpost git checkout pullpost
Here 'pullpost' is the name of the branch. Fell free to use whatever you want. Here 'pullpost' is the name of the branch. Fell free to use whatever you want.
3. **Create your change branch** 3. **Create your change branch**
Once you are in ```<tracking branch>```, make sure it's up to date, then create Once you are in `<tracking branch>`, make sure it's up to date, then create
a branch for your changes off of that one. a branch for your changes off of that one.
git branch fix-for-issue-395 git branch fix-for-issue-395
git checkout fix-for-issue-395 git checkout fix-for-issue-395
Here 'fix-for-issue-395' is the name of the branch. Feel free to use whatever Here 'fix-for-issue-395' is the name of the branch. Feel free to use whatever
you want. We'll call this the ```<change branch>```. This is the branch that you want. We'll call this the `<change branch>`. This is the branch that
you will eventually issue your pull request from. you will eventually issue your pull request from.
The purpose of these first three steps is to make sure that your merge request The purpose of these first three steps is to make sure that your merge request
has a nice clean diff that only involves the changes related to your fix/feature. has a nice clean diff that only involves the changes related to your fix/feature.
4. **Make your changes** 4. **Make your changes**
On your ```<change branch>``` make any changes relevant to your fix/feature. Don't On your `<change branch>` make any changes relevant to your fix/feature. Don't
group fixes for multiple unrelated issues or multiple unrelated features together. group fixes for multiple unrelated issues or multiple unrelated features together.
Create a separate branch for each unrelated changeset. For instance, if you're Create a separate branch for each unrelated changeset. For instance, if you're
fixing a bug in the parser and adding some new UI to the default template, those fixing a bug in the parser and adding some new UI to the default template, those
should be separate branches and merge requests. should be separate branches and merge requests.
5. **Add tests** 5. **Add tests**
Add tests for your change. If you are submitting a bugfix, include a test that Add tests for your change. If you are submitting a bugfix, include a test that
verifies the existence of the bug along with your fix. If you are submitting verifies the existence of the bug along with your fix. If you are submitting
a new feature, include tests that verify proper feature function, if applicable. a new feature, include tests that verify proper feature function, if applicable.
See the readme in the 'test' directory for more information See the readme in the 'test' directory for more information
6. **Commit and publish** 6. **Commit and publish**
Commit your changes and publish your branch (or push it if it's already published) Commit your changes and publish your branch (or push it if it's already published)
7. **Issue your pull request** 7. **Issue your pull request**
On github.com, switch to your ```<change branch>``` and click the 'Pull Request' On github.com, switch to your `<change branch>` and click the 'Pull Request'
button. Enter some meaningful information about the pull request. If it's a bugfix, button. Enter some meaningful information about the pull request. If it's a bugfix,
that doesn't already have an issue associated with it, provide some info on what that doesn't already have an issue associated with it, provide some info on what
situations that bug occurs in and a sense of it's severity. If it does already have situations that bug occurs in and a sense of it's severity. If it does already have
an issue, make sure the include the hash and issue number (e.g. '#100') so github an issue, make sure the include the hash and issue number (e.g. '#100') so github
links it. links it.
If it's a feature, provide some context about the motivations behind the feature, If it's a feature, provide some context about the motivations behind the feature,
why it's important/useful/cool/necessary and what it does/how it works. Don't why it's important/useful/cool/necessary and what it does/how it works. Don't
worry about being too verbose. Folks will be much more amenable to reading through worry about being too verbose. Folks will be much more amenable to reading through
your code if they know what its supposed to be about. your code if they know what its supposed to be about.

View File

@ -6,8 +6,7 @@ An API documentation generator for JavaScript.
Want to contribute to JSDoc? Please read [`CONTRIBUTING.md`](CONTRIBUTING.md). Want to contribute to JSDoc? Please read [`CONTRIBUTING.md`](CONTRIBUTING.md).
Installation and Usage ## Installation and Usage
----------------------
JSDoc supports stable versions of Node.js 8.15.0 and later. You can install JSDoc supports stable versions of Node.js 8.15.0 and later. You can install
JSDoc globally or in your project's `node_modules` folder. JSDoc globally or in your project's `node_modules` folder.
@ -51,41 +50,41 @@ and customize your documentation. Here are a few of them:
### Templates ### Templates
+ [jaguarjs-jsdoc](https://github.com/davidshimjs/jaguarjs-jsdoc) - [jaguarjs-jsdoc](https://github.com/davidshimjs/jaguarjs-jsdoc)
+ [DocStrap](https://github.com/docstrap/docstrap) - [DocStrap](https://github.com/docstrap/docstrap)
([example](https://docstrap.github.io/docstrap)) ([example](https://docstrap.github.io/docstrap))
+ [jsdoc3Template](https://github.com/DBCDK/jsdoc3Template) - [jsdoc3Template](https://github.com/DBCDK/jsdoc3Template)
([example](https://github.com/danyg/jsdoc3Template/wiki#wiki-screenshots)) ([example](https://github.com/danyg/jsdoc3Template/wiki#wiki-screenshots))
+ [minami](https://github.com/Nijikokun/minami) - [minami](https://github.com/Nijikokun/minami)
+ [docdash](https://github.com/clenemt/docdash) - [docdash](https://github.com/clenemt/docdash)
([example](http://clenemt.github.io/docdash/)) ([example](http://clenemt.github.io/docdash/))
+ [tui-jsdoc-template](https://github.com/nhnent/tui.jsdoc-template) - [tui-jsdoc-template](https://github.com/nhnent/tui.jsdoc-template)
([example](https://nhnent.github.io/tui.jsdoc-template/latest/)) ([example](https://nhnent.github.io/tui.jsdoc-template/latest/))
+ [better-docs](https://github.com/SoftwareBrothers/better-docs) - [better-docs](https://github.com/SoftwareBrothers/better-docs)
([example](https://softwarebrothers.github.io/admin-bro-dev/index.html)) ([example](https://softwarebrothers.github.io/admin-bro-dev/index.html))
### Build tools ### Build tools
+ [JSDoc Grunt plugin](https://github.com/krampstudio/grunt-jsdoc) - [JSDoc Grunt plugin](https://github.com/krampstudio/grunt-jsdoc)
+ [JSDoc Gulp plugin](https://github.com/mlucool/gulp-jsdoc3) - [JSDoc Gulp plugin](https://github.com/mlucool/gulp-jsdoc3)
+ [JSDoc GitHub Action](https://github.com/andstor/jsdoc-action) - [JSDoc GitHub Action](https://github.com/andstor/jsdoc-action)
### Other tools ### Other tools
+ [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown) - [jsdoc-to-markdown](https://github.com/jsdoc2md/jsdoc-to-markdown)
+ [Integrating GitBook with - [Integrating GitBook with
JSDoc](https://medium.com/@kevinast/integrate-gitbook-jsdoc-974be8df6fb3) JSDoc](https://medium.com/@kevinast/integrate-gitbook-jsdoc-974be8df6fb3)
## For more information ## For more information
+ Documentation is available at [jsdoc.app](https://jsdoc.app/). - Documentation is available at [jsdoc.app](https://jsdoc.app/).
+ Contribute to the docs at - Contribute to the docs at
[jsdoc/jsdoc.github.io](https://github.com/jsdoc/jsdoc.github.io). [jsdoc/jsdoc.github.io](https://github.com/jsdoc/jsdoc.github.io).
+ [Join JSDoc's Slack channel](https://jsdoc-slack.appspot.com/). - [Join JSDoc's Slack channel](https://jsdoc-slack.appspot.com/).
+ Ask for help on the - Ask for help on the
[JSDoc Users mailing list](http://groups.google.com/group/jsdoc-users). [JSDoc Users mailing list](http://groups.google.com/group/jsdoc-users).
+ Post questions tagged `jsdoc` to - Post questions tagged `jsdoc` to
[Stack Overflow](http://stackoverflow.com/questions/tagged/jsdoc). [Stack Overflow](http://stackoverflow.com/questions/tagged/jsdoc).
## License ## License

View File

@ -2,50 +2,53 @@ const eslint = require('gulp-eslint');
const { exec } = require('child_process'); const { exec } = require('child_process');
const gulp = require('gulp'); const gulp = require('gulp');
const path = require('path'); const path = require('path');
const prettier = require('gulp-prettier');
function execCb(cb, err, stdout, stderr) { function execCb(cb, err, stdout, stderr) {
console.log(stdout); console.log(stdout);
console.error(stderr); console.error(stderr);
cb(err); cb(err);
} }
const options = { const options = {
coveragePaths: [ coveragePaths: [
'*.js', '*.js',
'packages/**/*.js', 'packages/**/*.js',
'!packages/**/test/*.js', '!packages/**/test/*.js',
'!packages/**/test/**/*.js', '!packages/**/test/**/*.js',
'!packages/**/static/*.js' '!packages/**/static/*.js',
], ],
lintPaths: [ lintPaths: ['*.js', 'packages/**/*.js', '!packages/**/static/*.js'],
'*.js', nodeBin: path.resolve(__dirname, './packages/jsdoc/jsdoc.js'),
'packages/**/*.js', nodePath: process.execPath,
'!packages/**/static/*.js'
],
nodeBin: path.resolve(__dirname, './packages/jsdoc/jsdoc.js'),
nodePath: process.execPath
}; };
function coverage(cb) { function coverage(cb) {
const cmd = `./node_modules/.bin/nyc --reporter=html ${options.nodeBin} -T`; const cmd = `./node_modules/.bin/nyc --reporter=html ${options.nodeBin} -T`;
return exec(cmd, execCb.bind(null, cb)); return exec(cmd, execCb.bind(null, cb));
}
function format() {
return gulp.src(options.lintPaths).pipe(prettier());
} }
function lint() { function lint() {
return gulp.src(options.lintPaths) return gulp
.pipe(eslint()) .src(options.lintPaths)
.pipe(eslint.formatEach()) .pipe(eslint())
.pipe(eslint.failAfterError()); .pipe(eslint.formatEach())
.pipe(eslint.failAfterError());
} }
function test(cb) { function test(cb) {
const cmd = `${options.nodePath} "${options.nodeBin}" -T`; const cmd = `${options.nodePath} "${options.nodeBin}" -T`;
return exec(cmd, execCb.bind(null, cb)); return exec(cmd, execCb.bind(null, cb));
} }
exports.coverage = coverage; exports.coverage = coverage;
exports.default = gulp.series(lint, test); exports.default = gulp.series(lint, test);
exports.format = format;
exports.lint = lint; exports.lint = lint;
exports.test = test; exports.test = test;

View File

@ -1,6 +1,4 @@
{ {
"packages": [ "packages": ["packages/*"],
"packages/*"
],
"version": "independent" "version": "independent"
} }

16671
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,14 +7,18 @@
"@jsdoc/test-matchers": "^0.1.6", "@jsdoc/test-matchers": "^0.1.6",
"ajv": "^8.6.3", "ajv": "^8.6.3",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-eslint": "^6.0.0", "gulp-eslint": "^6.0.0",
"gulp-prettier": "^4.0.0",
"jasmine": "^3.9.0", "jasmine": "^3.9.0",
"jasmine-console-reporter": "^3.1.0", "jasmine-console-reporter": "^3.1.0",
"klaw-sync": "^6.0.0", "klaw-sync": "^6.0.0",
"lerna": "^4.0.0", "lerna": "^4.0.0",
"mock-fs": "^5.1.0", "mock-fs": "^5.1.0",
"nyc": "^15.1.0" "nyc": "^15.1.0",
"prettier": "^2.4.1"
}, },
"engines": { "engines": {
"node": ">=v14.17.6" "node": ">=v14.17.6"

View File

@ -3,21 +3,19 @@ const { EventBus } = require('@jsdoc/util');
const flags = require('./flags'); const flags = require('./flags');
const help = require('./help'); const help = require('./help');
const { LEVELS, Logger } = require('./logger'); const { LEVELS, Logger } = require('./logger');
const {default: ow} = require('ow'); const { default: ow } = require('ow');
const yargs = require('yargs-parser'); const yargs = require('yargs-parser');
function validateChoice(flagInfo, choices, values) { function validateChoice(flagInfo, choices, values) {
let flagNames = flagInfo.alias ? `-${flagInfo.alias}/` : ''; let flagNames = flagInfo.alias ? `-${flagInfo.alias}/` : '';
flagNames += `--${flagInfo.name}`; flagNames += `--${flagInfo.name}`;
for (let value of values) { for (let value of values) {
if (!choices.includes(value)) { if (!choices.includes(value)) {
throw new TypeError( throw new TypeError(`The flag ${flagNames} accepts only these values: ${choices.join(', ')}`);
`The flag ${flagNames} accepts only these values: ${choices.join(', ')}`
);
}
} }
}
} }
/** /**
@ -30,54 +28,54 @@ function validateChoice(flagInfo, choices, values) {
* @private * @private
*/ */
const { KNOWN_FLAGS, YARGS_FLAGS } = (() => { const { KNOWN_FLAGS, YARGS_FLAGS } = (() => {
const names = new Set(); const names = new Set();
const opts = { const opts = {
alias: {}, alias: {},
array: [], array: [],
boolean: [], boolean: [],
coerce: {}, coerce: {},
narg: {}, narg: {},
normalize: [] normalize: [],
}; };
// `_` contains unparsed arguments. // `_` contains unparsed arguments.
names.add('_'); names.add('_');
Object.keys(flags).forEach(flag => { Object.keys(flags).forEach((flag) => {
const value = flags[flag]; const value = flags[flag];
names.add(flag); names.add(flag);
if (value.alias) { if (value.alias) {
names.add(value.alias); names.add(value.alias);
opts.alias[flag] = [value.alias]; opts.alias[flag] = [value.alias];
} }
if (value.array) { if (value.array) {
opts.array.push(flag); opts.array.push(flag);
} }
if (value.boolean) { if (value.boolean) {
opts.boolean.push(flag); opts.boolean.push(flag);
} }
if (value.coerce) { if (value.coerce) {
opts.coerce[flag] = value.coerce; opts.coerce[flag] = value.coerce;
} }
if (value.normalize) { if (value.normalize) {
opts.normalize.push(flag); opts.normalize.push(flag);
} }
if (value.requiresArg) { if (value.requiresArg) {
opts.narg[flag] = 1; opts.narg[flag] = 1;
} }
}); });
return { return {
KNOWN_FLAGS: names, KNOWN_FLAGS: names,
YARGS_FLAGS: opts YARGS_FLAGS: opts,
}; };
})(); })();
/** /**
@ -86,148 +84,146 @@ const { KNOWN_FLAGS, YARGS_FLAGS } = (() => {
* @alias module:@jsdoc/cli * @alias module:@jsdoc/cli
*/ */
class Engine { class Engine {
/** /**
* Create an instance of the CLI engine. * Create an instance of the CLI engine.
* *
* @param {Object} opts - Options for the CLI engine. * @param {Object} opts - Options for the CLI engine.
* @param {number} [opts.logLevel] - The maximum logging level to print to the console. Must be * @param {number} [opts.logLevel] - The maximum logging level to print to the console. Must be
* an enumerated value of `module:@jsdoc/cli.LOG_LEVELS`. The default value is * an enumerated value of `module:@jsdoc/cli.LOG_LEVELS`. The default value is
* `module:@jsdoc/cli.LOG_LEVELS.WARN`. * `module:@jsdoc/cli.LOG_LEVELS.WARN`.
* @param {string} [opts.version] - The version of JSDoc that is running. * @param {string} [opts.version] - The version of JSDoc that is running.
* @param {Date} [opts.revision] - A timestamp for the version of JSDoc that is running. * @param {Date} [opts.revision] - A timestamp for the version of JSDoc that is running.
*/ */
constructor(opts = {}) { constructor(opts = {}) {
ow(opts, ow.object); ow(opts, ow.object);
// The `Logger` class validates `opts.level`, so no need to validate it here. // The `Logger` class validates `opts.level`, so no need to validate it here.
ow(opts.revision, ow.optional.date); ow(opts.revision, ow.optional.date);
ow(opts.version, ow.optional.string); ow(opts.version, ow.optional.string);
this._bus = new EventBus('jsdoc', { this._bus = new EventBus('jsdoc', {
cache: _.isBoolean(opts._cacheEventBus) ? opts._cacheEventBus : true cache: _.isBoolean(opts._cacheEventBus) ? opts._cacheEventBus : true,
}); });
this._logger = new Logger({ this._logger = new Logger({
emitter: this._bus, emitter: this._bus,
level: opts.logLevel level: opts.logLevel,
}); });
this.flags = []; this.flags = [];
this.revision = opts.revision; this.revision = opts.revision;
this.version = opts.version; this.version = opts.version;
}
/**
* The log level to use. Messages are logged only if they are at or above this level.
* Must be an enumerated value of {@link module:@jsdoc/cli.LOG_LEVELS}.
*
* The default value is `module:@jsdoc/cli.LOG_LEVELS.WARN`.
*/
get logLevel() {
return this._logger.level;
}
set logLevel(level) {
this._logger.level = level;
}
/**
* Get help text for JSDoc.
*
* You can specify the maximum line length for the help text. This method attempts to fit each
* line within the maximum length, but it only splits on word boundaries. If you specify a small
* length, such as `10`, some lines will exceed that length.
*
* @param {Object} [opts] - Options for formatting the help text.
* @param {number} [opts.maxLength=Infinity] - The desired maximum length of each line in the
* formatted text.
* @return {string} The formatted help text.
*/
help(opts = {}) {
ow(opts, ow.object);
ow(opts.maxLength, ow.optional.number);
const maxLength = opts.maxLength || Infinity;
return (
`Options:\n${help({ maxLength })}\n\n` + 'Visit https://jsdoc.app/ for more information.'
);
}
/**
* Details about the command-line flags that JSDoc recognizes.
*/
get knownFlags() {
return flags;
}
/**
* Parse an array of command-line flags (also known as "options").
*
* Use the instance's `flags` property to retrieve the parsed flags later.
*
* @param {Array<string>} cliFlags - The command-line flags to parse.
* @returns {Object} The name and value for each flag. The `_` property contains all arguments
* other than flags and their values.
*/
parseFlags(cliFlags) {
ow(cliFlags, ow.array);
let normalizedFlags;
let parsed;
let parsedFlags;
let parsedFlagNames;
normalizedFlags = Object.keys(flags);
parsed = yargs.detailed(cliFlags, YARGS_FLAGS);
if (parsed.error) {
throw parsed.error;
} }
parsedFlags = parsed.argv;
parsedFlagNames = new Set(Object.keys(parsedFlags));
/** // Check all parsed flags for unknown flag names.
* The log level to use. Messages are logged only if they are at or above this level. for (let flag of parsedFlagNames) {
* Must be an enumerated value of {@link module:@jsdoc/cli.LOG_LEVELS}. if (!KNOWN_FLAGS.has(flag)) {
* throw new TypeError(
* The default value is `module:@jsdoc/cli.LOG_LEVELS.WARN`. 'Unknown command-line option: ' + (flag.length === 1 ? `-${flag}` : `--${flag}`)
*/
get logLevel() {
return this._logger.level;
}
set logLevel(level) {
this._logger.level = level;
}
/**
* Get help text for JSDoc.
*
* You can specify the maximum line length for the help text. This method attempts to fit each
* line within the maximum length, but it only splits on word boundaries. If you specify a small
* length, such as `10`, some lines will exceed that length.
*
* @param {Object} [opts] - Options for formatting the help text.
* @param {number} [opts.maxLength=Infinity] - The desired maximum length of each line in the
* formatted text.
* @return {string} The formatted help text.
*/
help(opts = {}) {
ow(opts, ow.object);
ow(opts.maxLength, ow.optional.number);
const maxLength = opts.maxLength || Infinity;
return (
`Options:\n${help({ maxLength })}\n\n` +
'Visit https://jsdoc.app/ for more information.'
); );
}
} }
/** // Validate the values of known flags.
* Details about the command-line flags that JSDoc recognizes. for (let flag of normalizedFlags) {
*/ if (parsedFlags[flag] && flags[flag].choices) {
get knownFlags() { let flagInfo = {
return flags; name: flag,
alias: flags[flag].alias,
};
validateChoice(flagInfo, flags[flag].choices, parsedFlags[flag]);
}
} }
/** // Only keep the long name of each flag.
* Parse an array of command-line flags (also known as "options"). this.flags = _.pick(parsedFlags, normalizedFlags.concat(['_']));
*
* Use the instance's `flags` property to retrieve the parsed flags later.
*
* @param {Array<string>} cliFlags - The command-line flags to parse.
* @returns {Object} The name and value for each flag. The `_` property contains all arguments
* other than flags and their values.
*/
parseFlags(cliFlags) {
ow(cliFlags, ow.array);
let normalizedFlags; return this.flags;
let parsed; }
let parsedFlags;
let parsedFlagNames;
normalizedFlags = Object.keys(flags); /**
parsed = yargs.detailed(cliFlags, YARGS_FLAGS); * A string that describes the current JSDoc version.
if (parsed.error) { */
throw parsed.error; get versionDetails() {
} let revision = '';
parsedFlags = parsed.argv;
parsedFlagNames = new Set(Object.keys(parsedFlags));
// Check all parsed flags for unknown flag names. if (!this.version) {
for (let flag of parsedFlagNames) { return '';
if (!KNOWN_FLAGS.has(flag)) {
throw new TypeError(
'Unknown command-line option: ' +
(flag.length === 1 ? `-${flag}` : `--${flag}`)
);
}
}
// Validate the values of known flags.
for (let flag of normalizedFlags) {
if (parsedFlags[flag] && flags[flag].choices) {
let flagInfo = {
name: flag,
alias: flags[flag].alias
};
validateChoice(flagInfo, flags[flag].choices, parsedFlags[flag]);
}
}
// Only keep the long name of each flag.
this.flags = _.pick(parsedFlags, normalizedFlags.concat(['_']));
return this.flags;
} }
/** if (this.revision) {
* A string that describes the current JSDoc version. revision = `(${this.revision.toUTCString()})`;
*/
get versionDetails() {
let revision = '';
if (!this.version) {
return '';
}
if (this.revision) {
revision = `(${this.revision.toUTCString()})`;
}
return `JSDoc ${this.version} ${revision}`.trim();
} }
return `JSDoc ${this.version} ${revision}`.trim();
}
} }
Engine.LOG_LEVELS = LEVELS; Engine.LOG_LEVELS = LEVELS;

View File

@ -8,100 +8,100 @@ const querystring = require('querystring');
* @alias module:@jsdoc/cli/lib/flags * @alias module:@jsdoc/cli/lib/flags
*/ */
module.exports = { module.exports = {
access: { access: {
alias: 'a', alias: 'a',
array: true, array: true,
choices: ['all', 'package', 'private', 'protected', 'public', 'undefined'], choices: ['all', 'package', 'private', 'protected', 'public', 'undefined'],
defaultDescription: 'All except `private`', defaultDescription: 'All except `private`',
description: 'Document only symbols with the specified access level.', description: 'Document only symbols with the specified access level.',
requiresArg: true requiresArg: true,
}, },
configure: { configure: {
alias: 'c', alias: 'c',
description: 'The configuration file to use.', description: 'The configuration file to use.',
normalize: true, normalize: true,
requiresArg: true requiresArg: true,
}, },
debug: { debug: {
boolean: true, boolean: true,
description: 'Log information to help with debugging.' description: 'Log information to help with debugging.',
}, },
destination: { destination: {
alias: 'd', alias: 'd',
default: './out', default: './out',
description: 'The output directory.', description: 'The output directory.',
normalize: true, normalize: true,
requiresArg: true requiresArg: true,
}, },
encoding: { encoding: {
alias: 'e', alias: 'e',
default: 'utf8', default: 'utf8',
description: 'The encoding to assume when reading source files.', description: 'The encoding to assume when reading source files.',
requiresArg: true requiresArg: true,
}, },
explain: { explain: {
alias: 'X', alias: 'X',
boolean: true, boolean: true,
description: 'Print the parse results to the console and exit.' description: 'Print the parse results to the console and exit.',
}, },
help: { help: {
alias: 'h', alias: 'h',
boolean: true, boolean: true,
description: 'Print help information and exit.' description: 'Print help information and exit.',
}, },
match: { match: {
description: 'Run only tests whose names contain this value.', description: 'Run only tests whose names contain this value.',
requiresArg: true requiresArg: true,
}, },
package: { package: {
alias: 'P', alias: 'P',
description: 'The path to the `package.json` file to use.', description: 'The path to the `package.json` file to use.',
normalize: true, normalize: true,
requiresArg: true requiresArg: true,
}, },
pedantic: { pedantic: {
boolean: true, boolean: true,
description: 'Treat errors as fatal errors, and treat warnings as errors.' description: 'Treat errors as fatal errors, and treat warnings as errors.',
}, },
private: { private: {
alias: 'p', alias: 'p',
boolean: true, boolean: true,
description: 'Document private symbols (equivalent to `--access all`).' description: 'Document private symbols (equivalent to `--access all`).',
}, },
query: { query: {
alias: 'q', alias: 'q',
coerce: ((str) => cast(querystring.parse(str))), coerce: (str) => cast(querystring.parse(str)),
description: 'A query string to parse and store (for example, `foo=bar&baz=true`).', description: 'A query string to parse and store (for example, `foo=bar&baz=true`).',
requiresArg: true requiresArg: true,
}, },
readme: { readme: {
alias: 'R', alias: 'R',
description: 'The `README` file to include in the documentation.', description: 'The `README` file to include in the documentation.',
normalize: true, normalize: true,
requiresArg: true requiresArg: true,
}, },
recurse: { recurse: {
alias: 'r', alias: 'r',
boolean: true, boolean: true,
description: 'Recurse into subdirectories to find source files.' description: 'Recurse into subdirectories to find source files.',
}, },
template: { template: {
alias: 't', alias: 't',
description: 'The template package to use.', description: 'The template package to use.',
requiresArg: true requiresArg: true,
}, },
test: { test: {
alias: 'T', alias: 'T',
boolean: true, boolean: true,
description: 'Run all tests and exit.' description: 'Run all tests and exit.',
}, },
verbose: { verbose: {
boolean: true, boolean: true,
description: 'Log detailed information to the console.' description: 'Log detailed information to the console.',
}, },
version: { version: {
alias: 'v', alias: 'v',
boolean: true, boolean: true,
description: 'Display the version number and exit.' description: 'Display the version number and exit.',
} },
}; };

View File

@ -1,34 +1,34 @@
const flags = require('./flags'); const flags = require('./flags');
function padLeft(str, length) { function padLeft(str, length) {
return str.padStart(str.length + length); return str.padStart(str.length + length);
} }
function padRight(str, length) { function padRight(str, length) {
return str.padEnd(str.length + length); return str.padEnd(str.length + length);
} }
function findMaxLength(arr) { function findMaxLength(arr) {
let max = 0; let max = 0;
arr.forEach(({length}) => { arr.forEach(({ length }) => {
max = Math.max(max, length); max = Math.max(max, length);
}); });
return max; return max;
} }
function concatWithMaxLength(items, maxLength) { function concatWithMaxLength(items, maxLength) {
let result = ''; let result = '';
// to prevent endless loops, always use the first item, regardless of length // to prevent endless loops, always use the first item, regardless of length
result += items.shift(); result += items.shift();
while (items.length && (result.length + items[0].length < maxLength)) { while (items.length && result.length + items[0].length < maxLength) {
result += ` ${items.shift()}`; result += ` ${items.shift()}`;
} }
return result; return result;
} }
/** /**
@ -48,41 +48,41 @@ function concatWithMaxLength(items, maxLength) {
* @param {Object} opts - Options for formatting the text. * @param {Object} opts - Options for formatting the text.
* @param {number} opts.maxLength - The maximum length of each line. * @param {number} opts.maxLength - The maximum length of each line.
*/ */
function formatHelpInfo({names, descriptions}, {maxLength}) { function formatHelpInfo({ names, descriptions }, { maxLength }) {
const MARGIN_SIZE = 4; const MARGIN_SIZE = 4;
const GUTTER_SIZE = MARGIN_SIZE; const GUTTER_SIZE = MARGIN_SIZE;
const results = []; const results = [];
const maxNameLength = findMaxLength(names); const maxNameLength = findMaxLength(names);
const wrapDescriptionAt = maxLength - (MARGIN_SIZE * 2) - GUTTER_SIZE - maxNameLength; const wrapDescriptionAt = maxLength - MARGIN_SIZE * 2 - GUTTER_SIZE - maxNameLength;
// Build the string for each flag. // Build the string for each flag.
names.forEach((name, i) => { names.forEach((name, i) => {
let result; let result;
let partialDescription; let partialDescription;
let words; let words;
// Add some whitespace before the name. // Add some whitespace before the name.
result = padLeft(name, MARGIN_SIZE); result = padLeft(name, MARGIN_SIZE);
// Make the descriptions left-justified, with a gutter between the names and descriptions. // Make the descriptions left-justified, with a gutter between the names and descriptions.
result = padRight(result, maxNameLength - name.length + GUTTER_SIZE); result = padRight(result, maxNameLength - name.length + GUTTER_SIZE);
// Split the description on spaces. // Split the description on spaces.
words = descriptions[i].split(' '); words = descriptions[i].split(' ');
// Add as much of the description as we can fit on the first line. // Add as much of the description as we can fit on the first line.
result += concatWithMaxLength(words, wrapDescriptionAt); result += concatWithMaxLength(words, wrapDescriptionAt);
// If there's anything left, keep going until we've consumed the entire description. // If there's anything left, keep going until we've consumed the entire description.
while (words.length) { while (words.length) {
// Add whitespace for the name column and the gutter. // Add whitespace for the name column and the gutter.
partialDescription = padLeft('', MARGIN_SIZE + maxNameLength + GUTTER_SIZE); partialDescription = padLeft('', MARGIN_SIZE + maxNameLength + GUTTER_SIZE);
partialDescription += concatWithMaxLength(words, wrapDescriptionAt); partialDescription += concatWithMaxLength(words, wrapDescriptionAt);
result += `\n${partialDescription}`; result += `\n${partialDescription}`;
} }
results.push(result); results.push(result);
}); });
return results; return results;
} }
/** /**
@ -95,41 +95,41 @@ function formatHelpInfo({names, descriptions}, {maxLength}) {
* @private * @private
*/ */
module.exports = ({ maxLength }) => { module.exports = ({ maxLength }) => {
const flagInfo = { const flagInfo = {
names: [], names: [],
descriptions: [] descriptions: [],
}; };
Object.keys(flags) Object.keys(flags)
.sort() .sort()
.forEach(flagName => { .forEach((flagName) => {
const flagDetail = flags[flagName]; const flagDetail = flags[flagName];
let description = ''; let description = '';
let name = ''; let name = '';
if (flagDetail.alias) { if (flagDetail.alias) {
name += `-${flagDetail.alias}, `; name += `-${flagDetail.alias}, `;
} }
name += `--${flagName}`; name += `--${flagName}`;
if (flagDetail.requiresArg) { if (flagDetail.requiresArg) {
name += ' <value>'; name += ' <value>';
} }
description += flagDetail.description; description += flagDetail.description;
if (flagDetail.array) { if (flagDetail.array) {
description += ' Can be specified more than once.'; description += ' Can be specified more than once.';
} }
if (flagDetail.choices) { if (flagDetail.choices) {
description += ` Accepts these values: ${flagDetail.choices.join(', ')}`; description += ` Accepts these values: ${flagDetail.choices.join(', ')}`;
} }
flagInfo.names.push(name); flagInfo.names.push(name);
flagInfo.descriptions.push(description); flagInfo.descriptions.push(description);
}); });
return `${formatHelpInfo(flagInfo, {maxLength}).join('\n')}`; return `${formatHelpInfo(flagInfo, { maxLength }).join('\n')}`;
}; };

View File

@ -1,5 +1,5 @@
const _ = require('lodash'); const _ = require('lodash');
const {default: ow} = require('ow'); const { default: ow } = require('ow');
/** /**
* Logging levels for the JSDoc logger. The default logging level is * Logging levels for the JSDoc logger. The default logging level is
@ -10,150 +10,152 @@ const {default: ow} = require('ow');
* @type {number} * @type {number}
*/ */
const LEVELS = { const LEVELS = {
/** /**
* Do not log any messages. * Do not log any messages.
* *
* @alias module:@jsdoc/cli.LOG_LEVELS.SILENT * @alias module:@jsdoc/cli.LOG_LEVELS.SILENT
*/ */
SILENT: 0, SILENT: 0,
/** /**
* Log fatal errors that prevent JSDoc from running. * Log fatal errors that prevent JSDoc from running.
* *
* @alias module:@jsdoc/cli.LOG_LEVELS.FATAL * @alias module:@jsdoc/cli.LOG_LEVELS.FATAL
*/ */
FATAL: 10, FATAL: 10,
/** /**
* Log all errors, including errors from which JSDoc can recover. * Log all errors, including errors from which JSDoc can recover.
* *
* @alias module:@jsdoc/cli.LOG_LEVELS.ERROR * @alias module:@jsdoc/cli.LOG_LEVELS.ERROR
*/ */
ERROR: 20, ERROR: 20,
/** /**
* Log the following messages: * Log the following messages:
* *
* + Warnings * + Warnings
* + Errors * + Errors
* *
* @alias module:@jsdoc/cli.LOG_LEVELS.WARN * @alias module:@jsdoc/cli.LOG_LEVELS.WARN
*/ */
WARN: 30, WARN: 30,
/** /**
* Log the following messages: * Log the following messages:
* *
* + Informational messages * + Informational messages
* + Warnings * + Warnings
* + Errors * + Errors
* *
* @alias module:@jsdoc/cli.LOG_LEVELS.INFO * @alias module:@jsdoc/cli.LOG_LEVELS.INFO
*/ */
INFO: 40, INFO: 40,
/** /**
* Log the following messages: * Log the following messages:
* *
* + Debugging messages * + Debugging messages
* + Informational messages * + Informational messages
* + Warnings * + Warnings
* + Errors * + Errors
* *
* @alias module:@jsdoc/cli.LOG_LEVELS.DEBUG * @alias module:@jsdoc/cli.LOG_LEVELS.DEBUG
*/ */
DEBUG: 50, DEBUG: 50,
/** /**
* Log all messages. * Log all messages.
* *
* @alias module:@jsdoc/cli.LOG_LEVELS.VERBOSE * @alias module:@jsdoc/cli.LOG_LEVELS.VERBOSE
*/ */
VERBOSE: 1000 VERBOSE: 1000,
}; };
const DEFAULT_LEVEL = LEVELS.WARN; const DEFAULT_LEVEL = LEVELS.WARN;
const FUNCS = { const FUNCS = {
[LEVELS.DEBUG]: 'debug', [LEVELS.DEBUG]: 'debug',
[LEVELS.ERROR]: 'error', [LEVELS.ERROR]: 'error',
[LEVELS.INFO]: 'info', [LEVELS.INFO]: 'info',
[LEVELS.FATAL]: 'error', [LEVELS.FATAL]: 'error',
[LEVELS.VERBOSE]: 'debug', [LEVELS.VERBOSE]: 'debug',
[LEVELS.WARN]: 'warn' [LEVELS.WARN]: 'warn',
}; };
const LEVELS_BY_NUMBER = _.invert(LEVELS); const LEVELS_BY_NUMBER = _.invert(LEVELS);
const PREFIXES = { const PREFIXES = {
[LEVELS.DEBUG]: 'DEBUG: ', [LEVELS.DEBUG]: 'DEBUG: ',
[LEVELS.ERROR]: 'ERROR: ', [LEVELS.ERROR]: 'ERROR: ',
[LEVELS.FATAL]: 'FATAL: ', [LEVELS.FATAL]: 'FATAL: ',
[LEVELS.WARN]: 'WARNING: ' [LEVELS.WARN]: 'WARNING: ',
}; };
// Add a prefix to a log message if necessary. // Add a prefix to a log message if necessary.
function addPrefix(level, args) { function addPrefix(level, args) {
const prefix = PREFIXES[level]; const prefix = PREFIXES[level];
if (prefix && _.isString(args[0])) { if (prefix && _.isString(args[0])) {
args[0] = prefix + args[0]; args[0] = prefix + args[0];
} }
return args; return args;
} }
class Logger { class Logger {
constructor(opts) { constructor(opts) {
ow(opts, ow.object); ow(opts, ow.object);
// We validate `opts.level` in the setter, so no need to validate it here. // We validate `opts.level` in the setter, so no need to validate it here.
ow(opts.emitter, ow.object.partialShape({ ow(
off: ow.function, opts.emitter,
on: ow.function, ow.object.partialShape({
once: ow.function off: ow.function,
})); on: ow.function,
once: ow.function,
})
);
this._console = opts._console || console; this._console = opts._console || console;
const emitter = this._emitter = opts.emitter; const emitter = (this._emitter = opts.emitter);
this.level = opts.level || DEFAULT_LEVEL; this.level = opts.level || DEFAULT_LEVEL;
for (const levelName of Object.keys(LEVELS)) { for (const levelName of Object.keys(LEVELS)) {
let levelNameLower; let levelNameLower;
let levelNumber; let levelNumber;
// `logger:silent` events are not a thing. // `logger:silent` events are not a thing.
if (levelName === 'SILENT') { if (levelName === 'SILENT') {
continue; continue;
} }
levelNameLower = levelName.toLowerCase(); levelNameLower = levelName.toLowerCase();
levelNumber = LEVELS[levelName]; levelNumber = LEVELS[levelName];
emitter.on( emitter.on(`logger:${levelNameLower}`, (...args) => this._maybeLog(levelNumber, args));
`logger:${levelNameLower}`, }
(...args) => this._maybeLog(levelNumber, args) }
);
} _maybeLog(level, args) {
if (this._level >= level) {
args = addPrefix(level, args);
this._console[FUNCS[level]](...args);
}
}
get level() {
return this._level;
}
set level(level) {
let errorMsg;
if (_.isUndefined(LEVELS_BY_NUMBER[level])) {
errorMsg = `Unrecognized logging level ${level}. Known levels are: `;
errorMsg += Object.keys(LEVELS)
.map((k) => `${k}: ${LEVELS[k]}`)
.join(', ');
throw new TypeError(errorMsg);
} }
_maybeLog(level, args) { this._level = level;
if (this._level >= level) { }
args = addPrefix(level, args);
this._console[FUNCS[level]](...args);
}
}
get level() {
return this._level;
}
set level(level) {
let errorMsg;
if (_.isUndefined(LEVELS_BY_NUMBER[level])) {
errorMsg = `Unrecognized logging level ${level}. Known levels are: `;
errorMsg += Object.keys(LEVELS).map(k => `${k}: ${LEVELS[k]}`).join(', ');
throw new TypeError(errorMsg);
}
this._level = level;
}
} }
module.exports = { module.exports = {
LEVELS, LEVELS,
Logger Logger,
}; };

View File

@ -1,8 +1,129 @@
{ {
"name": "@jsdoc/cli", "name": "@jsdoc/cli",
"version": "0.2.5", "version": "0.2.5",
"lockfileVersion": 1, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": {
"": {
"name": "@jsdoc/cli",
"version": "0.2.5",
"license": "Apache-2.0",
"dependencies": {
"lodash": "^4.17.21",
"ow": "^0.27.0",
"strip-bom": "^4.0.0",
"yargs-parser": "^20.2.9"
},
"engines": {
"node": ">=v14.17.6"
}
},
"node_modules/@sindresorhus/is": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz",
"integrity": "sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/dot-prop": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
"integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
"dependencies": {
"is-obj": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-obj": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
"engines": {
"node": ">=8"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"node_modules/ow": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz",
"integrity": "sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ==",
"dependencies": {
"@sindresorhus/is": "^4.0.1",
"callsites": "^3.1.0",
"dot-prop": "^6.0.1",
"lodash.isequal": "^4.5.0",
"type-fest": "^1.2.1",
"vali-date": "^1.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-bom": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
"integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
"engines": {
"node": ">=8"
}
},
"node_modules/type-fest": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.2.2.tgz",
"integrity": "sha512-pfkPYCcuV0TJoo/jlsUeWNV8rk7uMU6ocnYNvca1Vu+pyKi8Rl8Zo2scPt9O72gCsXIm+dMxOOWuA3VFDSdzWA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/vali-date": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
"integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"engines": {
"node": ">=10"
}
}
},
"dependencies": { "dependencies": {
"@sindresorhus/is": { "@sindresorhus/is": {
"version": "4.0.1", "version": "4.0.1",

View File

@ -1,9 +1,9 @@
const Engine = require('../../index'); const Engine = require('../../index');
describe('@jsdoc/cli', () => { describe('@jsdoc/cli', () => {
it('is lib/engine', () => { it('is lib/engine', () => {
const engine = require('../../lib/engine'); const engine = require('../../lib/engine');
expect(Engine).toBe(engine); expect(Engine).toBe(engine);
}); });
}); });

View File

@ -6,221 +6,221 @@ const TYPE_ERROR = 'TypeError';
// Wrapper to prevent reuse of the event bus, which leads to `MaxListenersExceededWarning` messages. // Wrapper to prevent reuse of the event bus, which leads to `MaxListenersExceededWarning` messages.
class Engine extends RealEngine { class Engine extends RealEngine {
constructor(opts) { constructor(opts) {
opts = opts || {}; opts = opts || {};
opts._cacheEventBus = false; opts._cacheEventBus = false;
super(opts); super(opts);
} }
} }
describe('@jsdoc/cli/lib/engine', () => { describe('@jsdoc/cli/lib/engine', () => {
it('exists', () => { it('exists', () => {
expect(Engine).toBeFunction(); expect(Engine).toBeFunction();
}); });
it('works with no input', () => {
expect(() => new Engine()).not.toThrow();
});
it('has a static LOG_LEVELS property', () => {
expect(Engine.LOG_LEVELS).toBeObject();
});
it('has an empty array of flags by default', () => {
expect(new Engine().flags).toBeEmptyArray();
});
it('has a property that contains the known flags', () => {
expect(new Engine().knownFlags).toBe(flags);
});
it('has a logLevel property that defaults to LEVELS.WARN', () => {
expect(new Engine().logLevel).toBe(LEVELS.WARN);
});
it('has an undefined revision property by default', () => {
expect(new Engine().revision).toBeUndefined();
});
it('has an undefined version property by default', () => {
expect(new Engine().version).toBeUndefined();
});
it('has a versionDetails property that is an empty string by default', () => {
expect(new Engine().versionDetails).toBeEmptyString();
});
it('throws if the input is not an object', () => {
expect(() => new Engine('hi')).toThrow();
});
it('sets the logLevel if provided', () => {
const logLevel = LEVELS.VERBOSE;
const instance = new Engine({ logLevel });
expect(instance.logLevel).toBe(logLevel);
});
it('throws if the logLevel is invalid', () => {
const logLevel = LEVELS.VERBOSE + 1;
expect(() => new Engine({ logLevel })).toThrowErrorOfType(TYPE_ERROR);
});
it('sets the revision if provided', () => {
const revision = new Date();
const instance = new Engine({ revision });
expect(instance.revision).toBe(revision);
});
it('throws if the revision is not a date', () => {
expect(() => new Engine({ revision: '1' })).toThrow();
});
it('sets the version if provided', () => {
expect(new Engine({ version: '1.2.3' }).version).toBe('1.2.3');
});
it('throws if the version is not a string', () => {
expect(() => new Engine({ version: 1 })).toThrow();
});
describe('help', () => {
const instance = new Engine();
it('works with no input', () => { it('works with no input', () => {
expect(() => new Engine()).not.toThrow(); expect(() => instance.help()).not.toThrow();
}); });
it('has a static LOG_LEVELS property', () => { it('throws on bad input', () => {
expect(Engine.LOG_LEVELS).toBeObject(); expect(() => instance.help('hi')).toThrow();
}); });
it('has an empty array of flags by default', () => { it('returns a string', () => {
expect(new Engine().flags).toBeEmptyArray(); expect(instance.help()).toBeNonEmptyString();
}); });
it('has a property that contains the known flags', () => { it('honors a reasonable maxLength option', () => {
expect(new Engine().knownFlags).toBe(flags); const max = 70;
const help = instance.help({ maxLength: max }).split('\n');
for (let line of help) {
expect(line.length).toBeLessThanOrEqualTo(max);
}
}); });
it('has a logLevel property that defaults to LEVELS.WARN', () => { it('throws on a bad maxLength option', () => {
expect(new Engine().logLevel).toBe(LEVELS.WARN); expect(() => instance.help({ maxLength: 'long' })).toThrow();
});
});
describe('LOG_LEVELS', () => {
it('is lib/logger.LEVELS', () => {
expect(Engine.LOG_LEVELS).toBe(LEVELS);
});
});
describe('parseFlags', () => {
it('throws with no input', () => {
expect(() => new Engine().parseFlags()).toThrow();
}); });
it('has an undefined revision property by default', () => { it('throws if the input is not an array', () => {
expect(new Engine().revision).toBeUndefined(); expect(() => new Engine().parseFlags({ foo: 'bar' })).toThrow();
}); });
it('has an undefined version property by default', () => { it('parses flags with no values', () => {
expect(new Engine().version).toBeUndefined(); expect(new Engine().parseFlags(['--help']).help).toBeTrue();
}); });
it('has a versionDetails property that is an empty string by default', () => { it('parses flags with values', () => {
expect(new Engine().versionDetails).toBeEmptyString(); const parsed = new Engine().parseFlags(['--configure', 'conf.json']);
expect(parsed.configure).toBe('conf.json');
}); });
it('throws if the input is not an object', () => { it('stores the flags in the `flags` property', () => {
expect(() => new Engine('hi')).toThrow(); const instance = new Engine();
instance.parseFlags(['--help']);
expect(instance.flags.help).toBeTrue();
}); });
it('sets the logLevel if provided', () => { it('throws on unrecognized flags', () => {
const logLevel = LEVELS.VERBOSE; expect(() => new Engine().parseFlags(['--notarealflag'])).toThrow();
const instance = new Engine({ logLevel });
expect(instance.logLevel).toBe(logLevel);
}); });
it('throws if the logLevel is invalid', () => { it('throws on invalid flag values', () => {
const logLevel = LEVELS.VERBOSE + 1; expect(() => new Engine().parseFlags(['--access', 'maybe'])).toThrow();
expect(() => new Engine({ logLevel })).toThrowErrorOfType(TYPE_ERROR);
}); });
it('sets the revision if provided', () => { it('includes the long and short name in the error if a value is invalid', () => {
const revision = new Date(); let error;
const instance = new Engine({ revision });
expect(instance.revision).toBe(revision); try {
new Engine().parseFlags(['--access', 'just-this-once']);
} catch (e) {
error = e;
}
expect(error.message).toContain('-a/--access');
}); });
it('throws if the revision is not a date', () => { it('includes the allowed values in the error if a value is invalid', () => {
expect(() => new Engine({ revision: '1' })).toThrow(); let error;
try {
new Engine().parseFlags(['--access', 'maybe-later']);
} catch (e) {
error = e;
}
expect(error.message).toContain(flags.access.choices.join(', '));
}); });
it('sets the version if provided', () => { it('throws if a required value is missing', () => {
expect(new Engine({ version: '1.2.3' }).version).toBe('1.2.3'); expect(() => new Engine().parseFlags(['--template'])).toThrow();
}); });
it('throws if the version is not a string', () => { it('always uses the long flag name in the parsed flags', () => {
expect(() => new Engine({ version: 1 })).toThrow(); expect(new Engine().parseFlags(['-h']).help).toBeTrue();
}); });
describe('help', () => { it('coerces values to other types when appropriate', () => {
const instance = new Engine(); const parsed = new Engine().parseFlags(['--query', 'foo=bar&baz=true']);
it('works with no input', () => { expect(parsed.query).toEqual({
expect(() => instance.help()).not.toThrow(); foo: 'bar',
}); baz: true,
});
});
});
it('throws on bad input', () => { describe('versionDetails', () => {
expect(() => instance.help('hi')).toThrow(); it('works with a version but no revision', () => {
}); const instance = new Engine({ version: '1.2.3' });
it('returns a string', () => { expect(instance.versionDetails).toBe('JSDoc 1.2.3');
expect(instance.help()).toBeNonEmptyString();
});
it('honors a reasonable maxLength option', () => {
const max = 70;
const help = instance.help({ maxLength: max }).split('\n');
for (let line of help) {
expect(line.length).toBeLessThanOrEqualTo(max);
}
});
it('throws on a bad maxLength option', () => {
expect(() => instance.help({ maxLength: 'long' })).toThrow();
});
}); });
describe('LOG_LEVELS', () => { it('contains an empty string with a revision but no version', () => {
it('is lib/logger.LEVELS', () => { const revision = new Date();
expect(Engine.LOG_LEVELS).toBe(LEVELS); const instance = new Engine({ revision });
});
expect(instance.versionDetails).toBeEmptyString();
}); });
describe('parseFlags', () => { it('works with a version and a revision', () => {
it('throws with no input', () => { const revision = new Date();
expect(() => new Engine().parseFlags()).toThrow(); const instance = new Engine({
}); version: '1.2.3',
revision,
});
it('throws if the input is not an array', () => { expect(instance.versionDetails).toBe(`JSDoc 1.2.3 (${revision.toUTCString()})`);
expect(() => new Engine().parseFlags({ foo: 'bar' })).toThrow();
});
it('parses flags with no values', () => {
expect(new Engine().parseFlags(['--help']).help).toBeTrue();
});
it('parses flags with values', () => {
const parsed = new Engine().parseFlags(['--configure', 'conf.json']);
expect(parsed.configure).toBe('conf.json');
});
it('stores the flags in the `flags` property', () => {
const instance = new Engine();
instance.parseFlags(['--help']);
expect(instance.flags.help).toBeTrue();
});
it('throws on unrecognized flags', () => {
expect(() => new Engine().parseFlags(['--notarealflag'])).toThrow();
});
it('throws on invalid flag values', () => {
expect(() => new Engine().parseFlags(['--access', 'maybe'])).toThrow();
});
it('includes the long and short name in the error if a value is invalid', () => {
let error;
try {
new Engine().parseFlags(['--access', 'just-this-once']);
} catch (e) {
error = e;
}
expect(error.message).toContain('-a/--access');
});
it('includes the allowed values in the error if a value is invalid', () => {
let error;
try {
new Engine().parseFlags(['--access', 'maybe-later']);
} catch (e) {
error = e;
}
expect(error.message).toContain(flags.access.choices.join(', '));
});
it('throws if a required value is missing', () => {
expect(() => new Engine().parseFlags(['--template'])).toThrow();
});
it('always uses the long flag name in the parsed flags', () => {
expect(new Engine().parseFlags(['-h']).help).toBeTrue();
});
it('coerces values to other types when appropriate', () => {
const parsed = new Engine().parseFlags(['--query', 'foo=bar&baz=true']);
expect(parsed.query).toEqual({
foo: 'bar',
baz: true
});
});
});
describe('versionDetails', () => {
it('works with a version but no revision', () => {
const instance = new Engine({ version: '1.2.3' });
expect(instance.versionDetails).toBe('JSDoc 1.2.3');
});
it('contains an empty string with a revision but no version', () => {
const revision = new Date();
const instance = new Engine({ revision });
expect(instance.versionDetails).toBeEmptyString();
});
it('works with a version and a revision', () => {
const revision = new Date();
const instance = new Engine({
version: '1.2.3',
revision
});
expect(instance.versionDetails).toBe(`JSDoc 1.2.3 (${revision.toUTCString()})`);
});
}); });
});
}); });

View File

@ -1,40 +1,40 @@
const flags = require('../../../lib/flags'); const flags = require('../../../lib/flags');
const {default: ow} = require('ow'); const { default: ow } = require('ow');
function validate(name, opts) { function validate(name, opts) {
name = `--${name}`; name = `--${name}`;
if (!opts.description) { if (!opts.description) {
throw new TypeError(`${name} is missing its description`); throw new TypeError(`${name} is missing its description`);
} }
if (opts.array && opts.boolean) { if (opts.array && opts.boolean) {
throw new TypeError(`${name} can be an array or a boolean, but not both`); throw new TypeError(`${name} can be an array or a boolean, but not both`);
} }
if (opts.requiresArg && opts.boolean) { if (opts.requiresArg && opts.boolean) {
throw new TypeError(`${name} can require an argument or be a boolean, but not both`); throw new TypeError(`${name} can require an argument or be a boolean, but not both`);
} }
try { try {
ow(opts.coerce, ow.optional.function); ow(opts.coerce, ow.optional.function);
} catch (e) { } catch (e) {
throw new TypeError(`The coerce value for ${name} is not a function`); throw new TypeError(`The coerce value for ${name} is not a function`);
} }
if (opts.choices && !opts.requiresArg) { if (opts.choices && !opts.requiresArg) {
throw new TypeError(`${name} specifies choices, but not requiresArg`); throw new TypeError(`${name} specifies choices, but not requiresArg`);
} }
} }
describe('@jsdoc/cli/lib/flags', () => { describe('@jsdoc/cli/lib/flags', () => {
it('is an object', () => { it('is an object', () => {
expect(flags).toBeObject(); expect(flags).toBeObject();
}); });
it('has reasonable settings for each flag', () => { it('has reasonable settings for each flag', () => {
for (let flag of Object.keys(flags)) { for (let flag of Object.keys(flags)) {
expect(() => validate(flag, flags[flag])).not.toThrow(); expect(() => validate(flag, flags[flag])).not.toThrow();
} }
}); });
}); });

View File

@ -1,3 +1,3 @@
describe('@jsdoc/cli/lib/help', () => { describe('@jsdoc/cli/lib/help', () => {
// Tested indirectly by the tests for `@jsdoc/cli/lib/engine`. // Tested indirectly by the tests for `@jsdoc/cli/lib/engine`.
}); });

View File

@ -5,203 +5,204 @@ const ARGUMENT_ERROR = 'ArgumentError';
const TYPE_ERROR = 'TypeError'; const TYPE_ERROR = 'TypeError';
describe('@jsdoc/cli/lib/logger', () => { describe('@jsdoc/cli/lib/logger', () => {
describe('Logger', () => { describe('Logger', () => {
let bus; let bus;
let logger; let logger;
beforeEach(() => { beforeEach(() => {
bus = new EventBus('loggerTest', { bus = new EventBus('loggerTest', {
_console: console, _console: console,
cache: false cache: false,
}); });
logger = new Logger({ emitter: bus }); logger = new Logger({ emitter: bus });
['debug', 'error', 'info', 'warn'].forEach(func => spyOn(console, func)); ['debug', 'error', 'info', 'warn'].forEach((func) => spyOn(console, func));
});
it('exports a Logger constructor', () => {
expect(() => new Logger({ emitter: bus })).not.toThrow();
});
it('exports a LEVELS enum', () => {
expect(LEVELS).toBeNonEmptyObject();
});
describe('constructor', () => {
it('throws on invalid input', () => {
expect(() => new Logger()).toThrowErrorOfType(ARGUMENT_ERROR);
});
it('accepts a valid emitter', () => {
expect(() => new Logger({ emitter: bus })).not.toThrow();
});
it('throws on an invalid emitter', () => {
expect(() => new Logger({ emitter: {} })).toThrowErrorOfType(ARGUMENT_ERROR);
});
it('accepts a valid level', () => {
expect(() => new Logger({
emitter: bus,
level: LEVELS.VERBOSE
})).not.toThrow();
});
it('throws on an invalid level', () => {
expect(() => new Logger({
emitter: bus,
level: LEVELS.VERBOSE + 1
})).toThrowErrorOfType(TYPE_ERROR);
});
});
describe('events', () => {
it('passes all event arguments through', () => {
const args = [
'My name is %s %s %s',
'foo',
'bar',
'baz'
];
const eventType = 'logger:info';
logger.level = LEVELS.VERBOSE;
bus.emit(eventType, ...args);
expect(console.info).toHaveBeenCalledWith(...args);
});
it('logs logger:fatal events by default', () => {
bus.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');
expect(console.error).not.toHaveBeenCalled();
});
it('logs logger:error events by default', () => {
bus.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');
expect(console.error).not.toHaveBeenCalled();
});
it('logs logger:warn events by default', () => {
bus.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');
expect(console.warn).not.toHaveBeenCalled();
});
it('does not log logger:info events by default', () => {
bus.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');
expect(console.info).toHaveBeenCalled();
});
it('does not log logger:debug events by default', () => {
bus.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');
expect(console.debug).toHaveBeenCalled();
});
it('does not log logger:verbose events by default', () => {
bus.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');
expect(console.debug).toHaveBeenCalled();
});
});
describe('level', () => {
it('contains the current log level', () => {
expect(logger.level).toBe(LEVELS.WARN);
});
it('throws when set to an invalid value', () => {
expect(() => {
logger.level = LEVELS.VERBOSE + 1;
}).toThrowErrorOfType(TYPE_ERROR);
});
// The `events` tests set this property to valid values, so no need to test that
// behavior again here.
});
}); });
describe('LEVELS', () => { it('exports a Logger constructor', () => {
it('has a numeric SILENT property', () => { expect(() => new Logger({ emitter: bus })).not.toThrow();
expect(LEVELS.SILENT).toBeWholeNumber();
});
it('has a numeric FATAL property', () => {
expect(LEVELS.FATAL).toBeWholeNumber();
});
it('has a numeric ERROR property', () => {
expect(LEVELS.ERROR).toBeWholeNumber();
});
it('has a numeric WARN property', () => {
expect(LEVELS.WARN).toBeWholeNumber();
});
it('has a numeric INFO property', () => {
expect(LEVELS.INFO).toBeWholeNumber();
});
it('has a numeric DEBUG property', () => {
expect(LEVELS.DEBUG).toBeWholeNumber();
});
it('has a numeric VERBOSE property', () => {
expect(LEVELS.VERBOSE).toBeWholeNumber();
});
it('orders the log levels correctly', () => {
expect(LEVELS.SILENT).toBeLessThan(LEVELS.FATAL);
expect(LEVELS.FATAL).toBeLessThan(LEVELS.ERROR);
expect(LEVELS.ERROR).toBeLessThan(LEVELS.WARN);
expect(LEVELS.WARN).toBeLessThan(LEVELS.INFO);
expect(LEVELS.INFO).toBeLessThan(LEVELS.DEBUG);
expect(LEVELS.DEBUG).toBeLessThan(LEVELS.VERBOSE);
});
}); });
it('exports a LEVELS enum', () => {
expect(LEVELS).toBeNonEmptyObject();
});
describe('constructor', () => {
it('throws on invalid input', () => {
expect(() => new Logger()).toThrowErrorOfType(ARGUMENT_ERROR);
});
it('accepts a valid emitter', () => {
expect(() => new Logger({ emitter: bus })).not.toThrow();
});
it('throws on an invalid emitter', () => {
expect(() => new Logger({ emitter: {} })).toThrowErrorOfType(ARGUMENT_ERROR);
});
it('accepts a valid level', () => {
expect(
() =>
new Logger({
emitter: bus,
level: LEVELS.VERBOSE,
})
).not.toThrow();
});
it('throws on an invalid level', () => {
expect(
() =>
new Logger({
emitter: bus,
level: LEVELS.VERBOSE + 1,
})
).toThrowErrorOfType(TYPE_ERROR);
});
});
describe('events', () => {
it('passes all event arguments through', () => {
const args = ['My name is %s %s %s', 'foo', 'bar', 'baz'];
const eventType = 'logger:info';
logger.level = LEVELS.VERBOSE;
bus.emit(eventType, ...args);
expect(console.info).toHaveBeenCalledWith(...args);
});
it('logs logger:fatal events by default', () => {
bus.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');
expect(console.error).not.toHaveBeenCalled();
});
it('logs logger:error events by default', () => {
bus.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');
expect(console.error).not.toHaveBeenCalled();
});
it('logs logger:warn events by default', () => {
bus.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');
expect(console.warn).not.toHaveBeenCalled();
});
it('does not log logger:info events by default', () => {
bus.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');
expect(console.info).toHaveBeenCalled();
});
it('does not log logger:debug events by default', () => {
bus.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');
expect(console.debug).toHaveBeenCalled();
});
it('does not log logger:verbose events by default', () => {
bus.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');
expect(console.debug).toHaveBeenCalled();
});
});
describe('level', () => {
it('contains the current log level', () => {
expect(logger.level).toBe(LEVELS.WARN);
});
it('throws when set to an invalid value', () => {
expect(() => {
logger.level = LEVELS.VERBOSE + 1;
}).toThrowErrorOfType(TYPE_ERROR);
});
// The `events` tests set this property to valid values, so no need to test that
// behavior again here.
});
});
describe('LEVELS', () => {
it('has a numeric SILENT property', () => {
expect(LEVELS.SILENT).toBeWholeNumber();
});
it('has a numeric FATAL property', () => {
expect(LEVELS.FATAL).toBeWholeNumber();
});
it('has a numeric ERROR property', () => {
expect(LEVELS.ERROR).toBeWholeNumber();
});
it('has a numeric WARN property', () => {
expect(LEVELS.WARN).toBeWholeNumber();
});
it('has a numeric INFO property', () => {
expect(LEVELS.INFO).toBeWholeNumber();
});
it('has a numeric DEBUG property', () => {
expect(LEVELS.DEBUG).toBeWholeNumber();
});
it('has a numeric VERBOSE property', () => {
expect(LEVELS.VERBOSE).toBeWholeNumber();
});
it('orders the log levels correctly', () => {
expect(LEVELS.SILENT).toBeLessThan(LEVELS.FATAL);
expect(LEVELS.FATAL).toBeLessThan(LEVELS.ERROR);
expect(LEVELS.ERROR).toBeLessThan(LEVELS.WARN);
expect(LEVELS.WARN).toBeLessThan(LEVELS.INFO);
expect(LEVELS.INFO).toBeLessThan(LEVELS.DEBUG);
expect(LEVELS.DEBUG).toBeLessThan(LEVELS.VERBOSE);
});
});
}); });

View File

@ -8,6 +8,6 @@ const config = require('./lib/config');
const name = require('./lib/name'); const name = require('./lib/name');
module.exports = { module.exports = {
config, config,
name name,
}; };

View File

@ -11,125 +11,119 @@ const stripJsonComments = require('strip-json-comments');
const MODULE_NAME = 'jsdoc'; const MODULE_NAME = 'jsdoc';
const defaults = exports.defaults = { const defaults = (exports.defaults = {
// TODO(hegemonic): Integrate CLI options with other options. // TODO(hegemonic): Integrate CLI options with other options.
opts: { opts: {
destination: './out', destination: './out',
encoding: 'utf8' encoding: 'utf8',
}, },
/**
* The JSDoc plugins to load.
*/
plugins: [],
// TODO(hegemonic): Move to `source` or remove.
recurseDepth: 10,
/**
* Settings for loading and parsing source files.
*/
source: {
/** /**
* The JSDoc plugins to load. * A regular expression that matches source files to exclude from processing.
*
* To exclude files if any portion of their path begins with an underscore, use the value
* `(^|\\/|\\\\)_`.
*/ */
plugins: [], excludePattern: '',
// TODO(hegemonic): Move to `source` or remove.
recurseDepth: 10,
/** /**
* Settings for loading and parsing source files. * A regular expression that matches source files that JSDoc should process.
*
* By default, all source files with the extensions `.js`, `.jsdoc`, and `.jsx` are
* processed.
*/ */
source: { includePattern: '.+\\.js(doc|x)?$',
/**
* A regular expression that matches source files to exclude from processing.
*
* To exclude files if any portion of their path begins with an underscore, use the value
* `(^|\\/|\\\\)_`.
*/
excludePattern: '',
/**
* A regular expression that matches source files that JSDoc should process.
*
* By default, all source files with the extensions `.js`, `.jsdoc`, and `.jsx` are
* processed.
*/
includePattern: '.+\\.js(doc|x)?$',
/**
* The type of source file. In general, you should use the value `module`. If none of your
* source files use ECMAScript >=2015 syntax, you can use the value `script`.
*/
type: 'module'
},
/** /**
* Settings for interpreting JSDoc tags. * The type of source file. In general, you should use the value `module`. If none of your
* source files use ECMAScript >=2015 syntax, you can use the value `script`.
*/ */
tags: { type: 'module',
/** },
* Set to `true` to allow tags that JSDoc does not recognize. /**
*/ * Settings for interpreting JSDoc tags.
allowUnknownTags: true, */
// TODO(hegemonic): Use module paths, not magic strings. tags: {
/**
* The JSDoc tag dictionaries to load.
*
* If you specify two or more tag dictionaries, and a tag is defined in multiple
* dictionaries, JSDoc uses the definition from the first dictionary that includes that tag.
*/
dictionaries: [
'jsdoc',
'closure'
]
},
/** /**
* Settings for generating output with JSDoc templates. Some JSDoc templates might ignore these * Set to `true` to allow tags that JSDoc does not recognize.
* settings.
*/ */
templates: { allowUnknownTags: true,
/** // TODO(hegemonic): Use module paths, not magic strings.
* Set to `true` to use a monospaced font for links to other code symbols, but not links to /**
* websites. * The JSDoc tag dictionaries to load.
*/ *
cleverLinks: false, * If you specify two or more tag dictionaries, and a tag is defined in multiple
/** * dictionaries, JSDoc uses the definition from the first dictionary that includes that tag.
* Set to `true` to use a monospaced font for all links. */
*/ dictionaries: ['jsdoc', 'closure'],
monospaceLinks: false },
} /**
}; * Settings for generating output with JSDoc templates. Some JSDoc templates might ignore these
* settings.
*/
templates: {
/**
* Set to `true` to use a monospaced font for links to other code symbols, but not links to
* websites.
*/
cleverLinks: false,
/**
* Set to `true` to use a monospaced font for all links.
*/
monospaceLinks: false,
},
});
// TODO: Consider exporting this class. // TODO: Consider exporting this class.
class Config { class Config {
constructor(filepath, config) { constructor(filepath, config) {
this.config = config; this.config = config;
this.filepath = filepath; this.filepath = filepath;
} }
} }
function loadJson(filepath, content) { function loadJson(filepath, content) {
return defaultLoaders['.json'](filepath, stripBom(stripJsonComments(content))); return defaultLoaders['.json'](filepath, stripBom(stripJsonComments(content)));
} }
function loadYaml(filepath, content) { function loadYaml(filepath, content) {
return defaultLoaders['.yaml'](filepath, stripBom(content)); return defaultLoaders['.yaml'](filepath, stripBom(content));
} }
const explorerSync = cosmiconfigSync(MODULE_NAME, { const explorerSync = cosmiconfigSync(MODULE_NAME, {
cache: false, cache: false,
loaders: { loaders: {
'.json': loadJson, '.json': loadJson,
'.yaml': loadYaml, '.yaml': loadYaml,
'.yml': loadYaml, '.yml': loadYaml,
noExt: loadYaml noExt: loadYaml,
}, },
searchPlaces: [ searchPlaces: [
'package.json', 'package.json',
`.${MODULE_NAME}rc`, `.${MODULE_NAME}rc`,
`.${MODULE_NAME}rc.json`, `.${MODULE_NAME}rc.json`,
`.${MODULE_NAME}rc.yaml`, `.${MODULE_NAME}rc.yaml`,
`.${MODULE_NAME}rc.yml`, `.${MODULE_NAME}rc.yml`,
`.${MODULE_NAME}rc.js`, `.${MODULE_NAME}rc.js`,
`${MODULE_NAME}.config.js` `${MODULE_NAME}.config.js`,
] ],
}); });
exports.loadSync = (filepath) => { exports.loadSync = (filepath) => {
let loaded; let loaded;
if (filepath) { if (filepath) {
loaded = explorerSync.load(filepath); loaded = explorerSync.load(filepath);
} else { } else {
loaded = explorerSync.search() || {}; loaded = explorerSync.search() || {};
} }
return new Config( return new Config(loaded.filepath, _.defaultsDeep({}, loaded.config, defaults));
loaded.filepath,
_.defaultsDeep({}, loaded.config, defaults)
);
}; };

View File

@ -16,10 +16,10 @@ const hasOwnProp = Object.prototype.hasOwnProperty;
* @memberof module:jsdoc/name * @memberof module:jsdoc/name
*/ */
exports.LONGNAMES = { exports.LONGNAMES = {
/** Longname used for doclets that do not have a longname, such as anonymous functions. */ /** Longname used for doclets that do not have a longname, such as anonymous functions. */
ANONYMOUS: '<anonymous>', ANONYMOUS: '<anonymous>',
/** Longname that represents global scope. */ /** Longname that represents global scope. */
GLOBAL: '<global>' GLOBAL: '<global>',
}; };
// Module namespace prefix. // Module namespace prefix.
@ -32,26 +32,26 @@ exports.MODULE_NAMESPACE = 'module:';
* @static * @static
* @memberof module:jsdoc/name * @memberof module:jsdoc/name
*/ */
const SCOPE = exports.SCOPE = { const SCOPE = (exports.SCOPE = {
NAMES: { NAMES: {
GLOBAL: 'global', GLOBAL: 'global',
INNER: 'inner', INNER: 'inner',
INSTANCE: 'instance', INSTANCE: 'instance',
STATIC: 'static' STATIC: 'static',
}, },
PUNC: { PUNC: {
INNER: '~', INNER: '~',
INSTANCE: '#', INSTANCE: '#',
STATIC: '.' STATIC: '.',
} },
}; });
// Keys must be lowercase. // Keys must be lowercase.
const SCOPE_TO_PUNC = exports.SCOPE_TO_PUNC = { const SCOPE_TO_PUNC = (exports.SCOPE_TO_PUNC = {
inner: SCOPE.PUNC.INNER, inner: SCOPE.PUNC.INNER,
instance: SCOPE.PUNC.INSTANCE, instance: SCOPE.PUNC.INSTANCE,
static: SCOPE.PUNC.STATIC static: SCOPE.PUNC.STATIC,
}; });
exports.PUNC_TO_SCOPE = _.invert(SCOPE_TO_PUNC); exports.PUNC_TO_SCOPE = _.invert(SCOPE_TO_PUNC);
@ -78,9 +78,9 @@ const REGEXP_NAME_DESCRIPTION = new RegExp(`^(\\[[^\\]]+\\]|\\S+)${DESCRIPTION}`
* parent; otherwise, `false`. * parent; otherwise, `false`.
*/ */
exports.nameIsLongname = (name, memberof) => { exports.nameIsLongname = (name, memberof) => {
const regexp = new RegExp(`^${escape(memberof)}${SCOPE_PUNC_STRING}`); const regexp = new RegExp(`^${escape(memberof)}${SCOPE_PUNC_STRING}`);
return regexp.test(name); return regexp.test(name);
}; };
/** /**
@ -91,14 +91,14 @@ exports.nameIsLongname = (name, memberof) => {
* @param {string} name - The name in which to change `prototype` to `#`. * @param {string} name - The name in which to change `prototype` to `#`.
* @returns {string} The updated name. * @returns {string} The updated name.
*/ */
const prototypeToPunc = exports.prototypeToPunc = name => { const prototypeToPunc = (exports.prototypeToPunc = (name) => {
// Don't mangle symbols named `prototype`. // Don't mangle symbols named `prototype`.
if (name === 'prototype') { if (name === 'prototype') {
return name; return name;
} }
return name.replace(/(?:^|\.)prototype\.?/g, SCOPE.PUNC.INSTANCE); return name.replace(/(?:^|\.)prototype\.?/g, SCOPE.PUNC.INSTANCE);
}; });
/** /**
* Check whether a name begins with a character that identifies a scope. * Check whether a name begins with a character that identifies a scope.
@ -106,7 +106,7 @@ const prototypeToPunc = exports.prototypeToPunc = name => {
* @param {string} name - The name to check. * @param {string} name - The name to check.
* @returns {boolean} `true` if the name begins with a scope character; otherwise, `false`. * @returns {boolean} `true` if the name begins with a scope character; otherwise, `false`.
*/ */
exports.hasLeadingScope = name => REGEXP_LEADING_SCOPE.test(name); exports.hasLeadingScope = (name) => REGEXP_LEADING_SCOPE.test(name);
/** /**
* Check whether a name ends with a character that identifies a scope. * Check whether a name ends with a character that identifies a scope.
@ -114,7 +114,7 @@ exports.hasLeadingScope = name => REGEXP_LEADING_SCOPE.test(name);
* @param {string} name - The name to check. * @param {string} name - The name to check.
* @returns {boolean} `true` if the name ends with a scope character; otherwise, `false`. * @returns {boolean} `true` if the name ends with a scope character; otherwise, `false`.
*/ */
exports.hasTrailingScope = name => REGEXP_TRAILING_SCOPE.test(name); exports.hasTrailingScope = (name) => REGEXP_TRAILING_SCOPE.test(name);
/** /**
* Get a symbol's basename, which is the first part of its full name before any punctuation (other * Get a symbol's basename, which is the first part of its full name before any punctuation (other
@ -128,94 +128,92 @@ exports.hasTrailingScope = name => REGEXP_TRAILING_SCOPE.test(name);
* @param {?string} [name] - The symbol's full name. * @param {?string} [name] - The symbol's full name.
* @returns {?string} The symbol's basename. * @returns {?string} The symbol's basename.
*/ */
exports.getBasename = name => { exports.getBasename = (name) => {
if (!name) { if (!name) {
return null; return null;
} }
return name.replace(/^([$a-z_][$a-z_0-9]*).*?$/i, '$1'); return name.replace(/^([$a-z_][$a-z_0-9]*).*?$/i, '$1');
}; };
// TODO: docs // TODO: docs
exports.stripNamespace = longname => longname.replace(/^[a-zA-Z]+:/, ''); exports.stripNamespace = (longname) => longname.replace(/^[a-zA-Z]+:/, '');
// TODO: docs // TODO: docs
function slice(longname, sliceChars, forcedMemberof) { function slice(longname, sliceChars, forcedMemberof) {
let i; let i;
let memberof = ''; let memberof = '';
let name = ''; let name = '';
let parts; let parts;
let partsRegExp; let partsRegExp;
let scopePunc = ''; let scopePunc = '';
let token; let token;
const tokens = []; const tokens = [];
let variation; let variation;
sliceChars = sliceChars || SCOPE_PUNC; sliceChars = sliceChars || SCOPE_PUNC;
// Quoted strings in a longname are atomic, so we convert them to tokens: // Quoted strings in a longname are atomic, so we convert them to tokens:
// foo["bar"] => foo.@{1}@ // foo["bar"] => foo.@{1}@
// Foo.prototype["bar"] => Foo#@{1} // Foo.prototype["bar"] => Foo#@{1}
longname = longname.replace(/(prototype|#)?(\[?["'].+?["']\]?)/g, ($, p1, p2) => { longname = longname.replace(/(prototype|#)?(\[?["'].+?["']\]?)/g, ($, p1, p2) => {
let punc = ''; let punc = '';
// Is there a leading bracket? // Is there a leading bracket?
if ( /^\[/.test(p2) ) { if (/^\[/.test(p2)) {
// Is it a static or instance member? // Is it a static or instance member?
punc = p1 ? SCOPE.PUNC.INSTANCE : SCOPE.PUNC.STATIC; punc = p1 ? SCOPE.PUNC.INSTANCE : SCOPE.PUNC.STATIC;
p2 = p2.replace(/^\[/g, '') p2 = p2.replace(/^\[/g, '').replace(/\]$/g, '');
.replace(/\]$/g, '');
}
token = `@{${tokens.length}}@`;
tokens.push(p2);
return punc + token;
});
longname = prototypeToPunc(longname);
if (typeof forcedMemberof !== 'undefined') {
partsRegExp = new RegExp(`^(.*?)([${sliceChars.join()}]?)$`);
name = longname.substr(forcedMemberof.length);
parts = forcedMemberof.match(partsRegExp);
if (parts[1]) {
memberof = parts[1] || forcedMemberof;
}
if (parts[2]) {
scopePunc = parts[2];
}
}
else if (longname) {
parts = longname.match(new RegExp(`^(:?(.+)([${sliceChars.join()}]))?(.+?)$`)) || [];
name = parts.pop() || '';
scopePunc = parts.pop() || '';
memberof = parts.pop() || '';
} }
// Like `@name foo.bar(2)`. token = `@{${tokens.length}}@`;
if (/(.+)\(([^)]+)\)$/.test(name)) { tokens.push(p2);
name = RegExp.$1;
variation = RegExp.$2;
}
// Restore quoted strings. return punc + token;
i = tokens.length; });
while (i--) {
longname = longname.replace(`@{${i}}@`, tokens[i]);
memberof = memberof.replace(`@{${i}}@`, tokens[i]);
scopePunc = scopePunc.replace(`@{${i}}@`, tokens[i]);
name = name.replace(`@{${i}}@`, tokens[i]);
}
return { longname = prototypeToPunc(longname);
longname: longname,
memberof: memberof, if (typeof forcedMemberof !== 'undefined') {
scope: scopePunc, partsRegExp = new RegExp(`^(.*?)([${sliceChars.join()}]?)$`);
name: name, name = longname.substr(forcedMemberof.length);
variation: variation parts = forcedMemberof.match(partsRegExp);
};
if (parts[1]) {
memberof = parts[1] || forcedMemberof;
}
if (parts[2]) {
scopePunc = parts[2];
}
} else if (longname) {
parts = longname.match(new RegExp(`^(:?(.+)([${sliceChars.join()}]))?(.+?)$`)) || [];
name = parts.pop() || '';
scopePunc = parts.pop() || '';
memberof = parts.pop() || '';
}
// Like `@name foo.bar(2)`.
if (/(.+)\(([^)]+)\)$/.test(name)) {
name = RegExp.$1;
variation = RegExp.$2;
}
// Restore quoted strings.
i = tokens.length;
while (i--) {
longname = longname.replace(`@{${i}}@`, tokens[i]);
memberof = memberof.replace(`@{${i}}@`, tokens[i]);
scopePunc = scopePunc.replace(`@{${i}}@`, tokens[i]);
name = name.replace(`@{${i}}@`, tokens[i]);
}
return {
longname: longname,
memberof: memberof,
scope: scopePunc,
name: name,
variation: variation,
};
} }
/** /**
@ -231,9 +229,7 @@ function slice(longname, sliceChars, forcedMemberof) {
* @param {string} forcedMemberof * @param {string} forcedMemberof
* @returns {object} Representing the properties of the given name. * @returns {object} Representing the properties of the given name.
*/ */
exports.toParts = (longname, forcedMemberof) => slice( exports.toParts = (longname, forcedMemberof) => slice(longname, null, forcedMemberof);
longname, null, forcedMemberof
);
// TODO: docs // TODO: docs
/** /**
@ -242,16 +238,16 @@ exports.toParts = (longname, forcedMemberof) => slice(
* @returns {string} The longname with the namespace applied. * @returns {string} The longname with the namespace applied.
*/ */
exports.applyNamespace = (longname, ns) => { exports.applyNamespace = (longname, ns) => {
const nameParts = slice(longname); const nameParts = slice(longname);
const name = nameParts.name; const name = nameParts.name;
longname = nameParts.longname; longname = nameParts.longname;
if (!/^[a-zA-Z]+?:.+$/i.test(name)) { if (!/^[a-zA-Z]+?:.+$/i.test(name)) {
longname = longname.replace(new RegExp(`${escape(name)}$`), `${ns}:${name}`); longname = longname.replace(new RegExp(`${escape(name)}$`), `${ns}:${name}`);
} }
return longname; return longname;
}; };
/** /**
@ -262,70 +258,66 @@ exports.applyNamespace = (longname, ns) => {
* @return {boolean} `true` if the parent is an ancestor of the child; otherwise, `false`. * @return {boolean} `true` if the parent is an ancestor of the child; otherwise, `false`.
*/ */
exports.hasAncestor = (parent, child) => { exports.hasAncestor = (parent, child) => {
let hasAncestor = false; let hasAncestor = false;
let memberof = child; let memberof = child;
if (!parent || !child) {
return hasAncestor;
}
// Fast path for obvious non-ancestors.
if (child.indexOf(parent) !== 0) {
return hasAncestor;
}
do {
memberof = slice(memberof).memberof;
if (memberof === parent) {
hasAncestor = true;
}
} while (!hasAncestor && memberof);
if (!parent || !child) {
return hasAncestor; return hasAncestor;
}
// Fast path for obvious non-ancestors.
if (child.indexOf(parent) !== 0) {
return hasAncestor;
}
do {
memberof = slice(memberof).memberof;
if (memberof === parent) {
hasAncestor = true;
}
} while (!hasAncestor && memberof);
return hasAncestor;
}; };
// TODO: docs // TODO: docs
const fromParts = exports.fromParts = ({memberof, scope, name, variation}) => [ const fromParts = (exports.fromParts = ({ memberof, scope, name, variation }) =>
(memberof || ''), [memberof || '', scope || '', name || '', variation ? `(${variation})` : ''].join(''));
(scope || ''),
(name || ''),
(variation ? `(${variation})` : '')
].join('');
// TODO: docs // TODO: docs
exports.stripVariation = name => { exports.stripVariation = (name) => {
const parts = slice(name); const parts = slice(name);
parts.variation = ''; parts.variation = '';
return fromParts(parts); return fromParts(parts);
}; };
function splitLongname(longname, options) { function splitLongname(longname, options) {
const chunks = []; const chunks = [];
let currentNameInfo; let currentNameInfo;
const nameInfo = {}; const nameInfo = {};
let previousName = longname; let previousName = longname;
const splitters = SCOPE_PUNC.concat('/'); const splitters = SCOPE_PUNC.concat('/');
options = _.defaults(options || {}, { options = _.defaults(options || {}, {
includeVariation: true includeVariation: true,
}); });
do { do {
if (!options.includeVariation) { if (!options.includeVariation) {
previousName = exports.stripVariation(previousName); previousName = exports.stripVariation(previousName);
} }
currentNameInfo = nameInfo[previousName] = slice(previousName, splitters); currentNameInfo = nameInfo[previousName] = slice(previousName, splitters);
previousName = currentNameInfo.memberof; previousName = currentNameInfo.memberof;
chunks.push(currentNameInfo.scope + currentNameInfo.name); chunks.push(currentNameInfo.scope + currentNameInfo.name);
} while (previousName); } while (previousName);
return { return {
chunks: chunks.reverse(), chunks: chunks.reverse(),
nameInfo: nameInfo nameInfo: nameInfo,
}; };
} }
/** /**
@ -411,43 +403,43 @@ function splitLongname(longname, options) {
* @return {Object} A tree with information about each longname in the format shown above. * @return {Object} A tree with information about each longname in the format shown above.
*/ */
exports.longnamesToTree = (longnames, doclets) => { exports.longnamesToTree = (longnames, doclets) => {
const splitOptions = { includeVariation: false }; const splitOptions = { includeVariation: false };
const tree = {}; const tree = {};
longnames.forEach(longname => { longnames.forEach((longname) => {
let currentLongname = ''; let currentLongname = '';
let currentParent = tree; let currentParent = tree;
let nameInfo; let nameInfo;
let processed; let processed;
// Don't try to add empty longnames to the tree. // Don't try to add empty longnames to the tree.
if (!longname) { if (!longname) {
return; return;
} }
processed = splitLongname(longname, splitOptions); processed = splitLongname(longname, splitOptions);
nameInfo = processed.nameInfo; nameInfo = processed.nameInfo;
processed.chunks.forEach(chunk => { processed.chunks.forEach((chunk) => {
currentLongname += chunk; currentLongname += chunk;
if (currentParent !== tree) { if (currentParent !== tree) {
currentParent.children = currentParent.children || {}; currentParent.children = currentParent.children || {};
currentParent = currentParent.children; currentParent = currentParent.children;
} }
if (!hasOwnProp.call(currentParent, chunk)) { if (!hasOwnProp.call(currentParent, chunk)) {
currentParent[chunk] = nameInfo[currentLongname]; currentParent[chunk] = nameInfo[currentLongname];
} }
if (currentParent[chunk]) { if (currentParent[chunk]) {
currentParent[chunk].doclet = doclets ? doclets[currentLongname] : null; currentParent[chunk].doclet = doclets ? doclets[currentLongname] : null;
currentParent = currentParent[chunk]; currentParent = currentParent[chunk];
} }
});
}); });
});
return tree; return tree;
}; };
/** /**
@ -459,69 +451,68 @@ exports.longnamesToTree = (longnames, doclets) => {
* @returns {?Object} Hash with "name" and "description" properties. * @returns {?Object} Hash with "name" and "description" properties.
*/ */
function splitNameMatchingBrackets(nameDesc) { function splitNameMatchingBrackets(nameDesc) {
const buffer = []; const buffer = [];
let c; let c;
let stack = 0; let stack = 0;
let stringEnd = null; let stringEnd = null;
for (var i = 0; i < nameDesc.length; ++i) { for (var i = 0; i < nameDesc.length; ++i) {
c = nameDesc[i]; c = nameDesc[i];
buffer.push(c); buffer.push(c);
if (stringEnd) { if (stringEnd) {
if (c === '\\' && i + 1 < nameDesc.length) { if (c === '\\' && i + 1 < nameDesc.length) {
buffer.push(nameDesc[++i]); buffer.push(nameDesc[++i]);
} else if (c === stringEnd) { } else if (c === stringEnd) {
stringEnd = null; stringEnd = null;
} }
} else if (c === '"' || c === "'") { } else if (c === '"' || c === "'") {
stringEnd = c; stringEnd = c;
} else if (c === '[') { } else if (c === '[') {
++stack; ++stack;
} else if (c === ']') { } else if (c === ']') {
if (--stack === 0) { if (--stack === 0) {
break; break;
} }
}
} }
}
if (stack || stringEnd) { if (stack || stringEnd) {
return null; return null;
} }
nameDesc.substr(i).match(REGEXP_DESCRIPTION); nameDesc.substr(i).match(REGEXP_DESCRIPTION);
return { return {
name: buffer.join(''), name: buffer.join(''),
description: RegExp.$1 description: RegExp.$1,
}; };
} }
/** /**
* Split a string that starts with a name and ends with a description into separate parts. * Split a string that starts with a name and ends with a description into separate parts.
* @param {string} str - The string that contains the name and description. * @param {string} str - The string that contains the name and description.
* @returns {object} An object with `name` and `description` properties. * @returns {object} An object with `name` and `description` properties.
*/ */
exports.splitNameAndDescription = str => { exports.splitNameAndDescription = (str) => {
// Like: `name`, `[name]`, `name text`, `[name] text`, `name - text`, or `[name] - text`. // Like: `name`, `[name]`, `name text`, `[name] text`, `name - text`, or `[name] - text`.
// To ensure that we don't get confused by leading dashes in Markdown list items, the hyphen // To ensure that we don't get confused by leading dashes in Markdown list items, the hyphen
// must be on the same line as the name. // must be on the same line as the name.
// Optional values get special treatment, // Optional values get special treatment,
let result = null; let result = null;
if (str[0] === '[') { if (str[0] === '[') {
result = splitNameMatchingBrackets(str); result = splitNameMatchingBrackets(str);
if (result !== null) { if (result !== null) {
return result; return result;
}
} }
}
str.match(REGEXP_NAME_DESCRIPTION); str.match(REGEXP_NAME_DESCRIPTION);
return { return {
name: RegExp.$1, name: RegExp.$1,
description: RegExp.$2 description: RegExp.$2,
}; };
}; };

View File

@ -1,8 +1,279 @@
{ {
"name": "@jsdoc/core", "name": "@jsdoc/core",
"version": "0.4.0", "version": "0.4.0",
"lockfileVersion": 1, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": {
"": {
"name": "@jsdoc/core",
"version": "0.4.0",
"license": "Apache-2.0",
"dependencies": {
"cosmiconfig": "^7.0.1",
"escape-string-regexp": "^4.0.0",
"lodash": "^4.17.21",
"strip-bom": "^4.0.0",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": ">=v14.17.6"
}
},
"node_modules/@babel/code-frame": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
"integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==",
"dependencies": {
"@babel/highlight": "^7.14.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.14.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz",
"integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz",
"integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==",
"dependencies": {
"@babel/helper-validator-identifier": "^7.14.5",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/chalk/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"node_modules/cosmiconfig": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
"integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==",
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"engines": {
"node": ">=4"
}
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dependencies": {
"callsites": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"engines": {
"node": ">=8"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"engines": {
"node": ">=4"
}
},
"node_modules/strip-bom": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
"integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"engines": {
"node": ">= 6"
}
}
},
"dependencies": { "dependencies": {
"@babel/code-frame": { "@babel/code-frame": {
"version": "7.14.5", "version": "7.14.5",

View File

@ -1,23 +1,23 @@
const core = require('../../index'); const core = require('../../index');
describe('@jsdoc/core', () => { describe('@jsdoc/core', () => {
it('is an object', () => { it('is an object', () => {
expect(core).toBeObject(); expect(core).toBeObject();
});
describe('config', () => {
it('is lib/config', () => {
const config = require('../../lib/config');
expect(core.config).toBe(config);
}); });
});
describe('config', () => { describe('name', () => {
it('is lib/config', () => { it('is lib/name', () => {
const config = require('../../lib/config'); const name = require('../../lib/name');
expect(core.config).toBe(config); expect(core.name).toBe(name);
});
});
describe('name', () => {
it('is lib/name', () => {
const name = require('../../lib/name');
expect(core.name).toBe(name);
});
}); });
});
}); });

View File

@ -2,189 +2,189 @@ const mockFs = require('mock-fs');
const config = require('../../../lib/config'); const config = require('../../../lib/config');
describe('@jsdoc/core/lib/config', () => { describe('@jsdoc/core/lib/config', () => {
// Explicitly require `yaml` before we run any tests. `cosmiconfig` tries to load `yaml` lazily, // Explicitly require `yaml` before we run any tests. `cosmiconfig` tries to load `yaml` lazily,
// but that doesn't work when the file system is mocked. // but that doesn't work when the file system is mocked.
beforeAll(() => require('yaml')); beforeAll(() => require('yaml'));
afterEach(() => mockFs.restore()); afterEach(() => mockFs.restore());
it('is an object', () => {
expect(config).toBeObject();
});
describe('loadSync', () => {
it('is a function', () => {
expect(config.loadSync).toBeFunction();
});
it('returns an object with `config` and `filepath` properties', () => {
mockFs({
'conf.json': '{}',
});
const conf = config.loadSync('conf.json');
expect(conf.config).toBeObject();
expect(conf.filepath).toEndWith('conf.json');
});
it('loads settings from the specified filepath if there is one', () => {
mockFs({
'conf.json': '{"foo":"bar"}',
});
const conf = config.loadSync('conf.json');
expect(conf.config.foo).toBe('bar');
});
it('finds the config file when no filepath is specified', () => {
mockFs({
'package.json': '{"jsdoc":{"foo":"bar"}}',
});
const conf = config.loadSync();
expect(conf.config.foo).toBe('bar');
});
it('parses JSON config files that have an extension and contain comments', () => {
mockFs({
'.jsdocrc.json': '// comment\n{"foo":"bar"}',
});
const conf = config.loadSync();
expect(conf.config.foo).toBe('bar');
});
it('parses JSON files that start with a BOM', () => {
mockFs({
'.jsdocrc.json': '\uFEFF{"foo":"bar"}',
});
const conf = config.loadSync();
expect(conf.config.foo).toBe('bar');
});
it('parses YAML files that start with a BOM', () => {
mockFs({
'.jsdocrc.yaml': '\uFEFF{"foo":"bar"}',
});
const conf = config.loadSync();
expect(conf.config.foo).toBe('bar');
});
it('provides the default config if the user config is an empty object', () => {
mockFs({
'.jsdocrc.json': '{}',
});
const conf = config.loadSync();
expect(conf.config).toEqual(config.defaults);
});
it('provides the default config if there is no user config', () => {
const conf = config.loadSync();
expect(conf.config).toEqual(config.defaults);
});
it('merges nested defaults with nested user settings as expected', () => {
mockFs({
'.jsdocrc.json': '{"tags":{"foo":"bar"}}',
});
const conf = config.loadSync();
expect(conf.config.tags.allowUnknownTags).toBe(config.defaults.tags.allowUnknownTags);
expect(conf.config.tags.foo).toBe('bar');
});
});
describe('defaults', () => {
const { defaults } = config;
it('is an object', () => { it('is an object', () => {
expect(config).toBeObject(); expect(defaults).toBeObject();
}); });
describe('loadSync', () => { describe('plugins', () => {
it('is a function', () => { it('is an array', () => {
expect(config.loadSync).toBeFunction(); expect(defaults.plugins).toBeArray();
}); });
it('returns an object with `config` and `filepath` properties', () => {
mockFs({
'conf.json': '{}'
});
const conf = config.loadSync('conf.json');
expect(conf.config).toBeObject();
expect(conf.filepath).toEndWith('conf.json');
});
it('loads settings from the specified filepath if there is one', () => {
mockFs({
'conf.json': '{"foo":"bar"}'
});
const conf = config.loadSync('conf.json');
expect(conf.config.foo).toBe('bar');
});
it('finds the config file when no filepath is specified', () => {
mockFs({
'package.json': '{"jsdoc":{"foo":"bar"}}'
});
const conf = config.loadSync();
expect(conf.config.foo).toBe('bar');
});
it('parses JSON config files that have an extension and contain comments', () => {
mockFs({
'.jsdocrc.json': '// comment\n{"foo":"bar"}'
});
const conf = config.loadSync();
expect(conf.config.foo).toBe('bar');
});
it('parses JSON files that start with a BOM', () => {
mockFs({
'.jsdocrc.json': '\uFEFF{"foo":"bar"}'
});
const conf = config.loadSync();
expect(conf.config.foo).toBe('bar');
});
it('parses YAML files that start with a BOM', () => {
mockFs({
'.jsdocrc.yaml': '\uFEFF{"foo":"bar"}'
});
const conf = config.loadSync();
expect(conf.config.foo).toBe('bar');
});
it('provides the default config if the user config is an empty object', () => {
mockFs({
'.jsdocrc.json': '{}'
});
const conf = config.loadSync();
expect(conf.config).toEqual(config.defaults);
});
it('provides the default config if there is no user config', () => {
const conf = config.loadSync();
expect(conf.config).toEqual(config.defaults);
});
it('merges nested defaults with nested user settings as expected', () => {
mockFs({
'.jsdocrc.json': '{"tags":{"foo":"bar"}}'
});
const conf = config.loadSync();
expect(conf.config.tags.allowUnknownTags).toBe(config.defaults.tags.allowUnknownTags);
expect(conf.config.tags.foo).toBe('bar');
});
}); });
describe('defaults', () => { describe('source', () => {
const { defaults } = config; it('is an object', () => {
expect(defaults.source).toBeObject();
});
describe('excludePattern', () => {
it('is a string', () => {
expect(defaults.source.excludePattern).toBeString();
});
it('represents a valid regexp', () => {
expect(() => new RegExp(defaults.source.excludePattern)).not.toThrow();
});
});
describe('includePattern', () => {
it('is a string', () => {
expect(defaults.source.includePattern).toBeString();
});
it('represents a valid regexp', () => {
expect(() => new RegExp(defaults.source.includePattern)).not.toThrow();
});
});
describe('type', () => {
it('is a string', () => {
expect(defaults.source.type).toBeString();
});
});
describe('tags', () => {
it('is an object', () => { it('is an object', () => {
expect(defaults).toBeObject(); expect(defaults.tags).toBeObject();
}); });
describe('plugins', () => { describe('allowUnknownTags', () => {
it('is an array', () => { it('is a boolean', () => {
expect(defaults.plugins).toBeArray(); expect(defaults.tags.allowUnknownTags).toBeBoolean();
}); });
}); });
describe('source', () => { describe('dictionaries', () => {
it('is an object', () => { it('is an array of strings', () => {
expect(defaults.source).toBeObject(); expect(defaults.tags.dictionaries).toBeArrayOfStrings();
}); });
describe('excludePattern', () => {
it('is a string', () => {
expect(defaults.source.excludePattern).toBeString();
});
it('represents a valid regexp', () => {
expect(() => new RegExp(defaults.source.excludePattern)).not.toThrow();
});
});
describe('includePattern', () => {
it('is a string', () => {
expect(defaults.source.includePattern).toBeString();
});
it('represents a valid regexp', () => {
expect(() => new RegExp(defaults.source.includePattern)).not.toThrow();
});
});
describe('type', () => {
it('is a string', () => {
expect(defaults.source.type).toBeString();
});
});
describe('tags', () => {
it('is an object', () => {
expect(defaults.tags).toBeObject();
});
describe('allowUnknownTags', () => {
it('is a boolean', () => {
expect(defaults.tags.allowUnknownTags).toBeBoolean();
});
});
describe('dictionaries', () => {
it('is an array of strings', () => {
expect(defaults.tags.dictionaries).toBeArrayOfStrings();
});
});
});
describe('templates', () => {
it('is an object', () => {
expect(defaults.templates).toBeObject();
});
describe('cleverLinks', () => {
it('is a boolean', () => {
expect(defaults.templates.cleverLinks).toBeBoolean();
});
});
describe('monospaceLinks', () => {
it('is a boolean', () => {
expect(defaults.templates.monospaceLinks).toBeBoolean();
});
});
});
}); });
});
describe('templates', () => {
it('is an object', () => {
expect(defaults.templates).toBeObject();
});
describe('cleverLinks', () => {
it('is a boolean', () => {
expect(defaults.templates.cleverLinks).toBeBoolean();
});
});
describe('monospaceLinks', () => {
it('is a boolean', () => {
expect(defaults.templates.monospaceLinks).toBeBoolean();
});
});
});
}); });
});
}); });

View File

@ -1,439 +1,434 @@
describe('@jsdoc/core.name', () => { describe('@jsdoc/core.name', () => {
const { name } = require('@jsdoc/core'); const { name } = require('@jsdoc/core');
it('exists', () => { it('exists', () => {
expect(name).toBeObject(); expect(name).toBeObject();
});
it('has an applyNamespace method', () => {
expect(name.applyNamespace).toBeFunction();
});
it('has a fromParts method', () => {
expect(name.fromParts).toBeFunction();
});
it('has a getBasename method', () => {
expect(name.getBasename).toBeFunction();
});
it('has a hasAncestor method', () => {
expect(name.hasAncestor).toBeFunction();
});
it('has a hasLeadingScope method', () => {
expect(name.hasLeadingScope).toBeFunction();
});
it('has a hasTrailingScope method', () => {
expect(name.hasTrailingScope).toBeFunction();
});
it('has a LONGNAMES enum', () => {
expect(name.LONGNAMES).toBeObject();
});
it('has a longnamesToTree method', () => {
expect(name.longnamesToTree).toBeFunction();
});
it('has a MODULE_NAMESPACE property', () => {
expect(name.MODULE_NAMESPACE).toBeString();
});
it('has a nameIsLongname method', () => {
expect(name.nameIsLongname).toBeFunction();
});
it('has a prototypeToPunc method', () => {
expect(name.prototypeToPunc).toBeFunction();
});
it('has a PUNC_TO_SCOPE enum', () => {
expect(name.PUNC_TO_SCOPE).toBeObject();
});
it('has a SCOPE enum', () => {
expect(name.SCOPE).toBeObject();
});
it('has a SCOPE_TO_PUNC enum', () => {
expect(name.SCOPE_TO_PUNC).toBeObject();
});
it('has a splitNameAndDescription method', () => {
expect(name.splitNameAndDescription).toBeFunction();
});
it('has a stripNamespace method', () => {
expect(name.stripNamespace).toBeFunction();
});
it('has a stripVariation method', () => {
expect(name.stripVariation).toBeFunction();
});
it('has a toParts method', () => {
expect(name.toParts).toBeFunction();
});
describe('applyNamespace', () => {
it('applies the namespace to the name part of the longname', () => {
expect(name.applyNamespace('lib.Panel#open', 'event')).toBe('lib.Panel#event:open');
}); });
it('has an applyNamespace method', () => { it('applies the namespace to the start of a top-level longname', () => {
expect(name.applyNamespace).toBeFunction(); expect(name.applyNamespace('math/bigint', 'module')).toBe('module:math/bigint');
}); });
it('has a fromParts method', () => { // TODO(hegemonic): This has never worked
expect(name.fromParts).toBeFunction(); xit('handles longnames with quoted portions', () => {
expect(name.applyNamespace('foo."*don\'t.look~in#here!"', 'event')).toBe(
'foo.event:"*don\'t.look~in#here!"'
);
}); });
it('has a getBasename method', () => { it('handles longnames that already have namespaces', () => {
expect(name.getBasename).toBeFunction(); expect(name.applyNamespace('lib.Panel#event:open', 'event')).toBe('lib.Panel#event:open');
});
});
xdescribe('fromParts', () => {
// TODO: tests
});
describe('getBasename', () => {
it('returns null on empty input', () => {
expect(name.getBasename()).toBeNull();
}); });
it('has a hasAncestor method', () => { it('returns the original value if it has no punctuation except underscores', () => {
expect(name.hasAncestor).toBeFunction(); expect(name.getBasename('foo_bar')).toBe('foo_bar');
}); });
it('has a hasLeadingScope method', () => { it('returns the basename if the original value has punctuation', () => {
expect(name.hasLeadingScope).toBeFunction(); expect(name.getBasename('foo.Bar#baz')).toBe('foo');
});
});
describe('hasAncestor', () => {
it('returns false if no parent is specified', () => {
expect(name.hasAncestor(null, 'foo')).toBe(false);
}); });
it('has a hasTrailingScope method', () => { it('returns false if no child is specified', () => {
expect(name.hasTrailingScope).toBeFunction(); expect(name.hasAncestor('foo')).toBe(false);
}); });
it('has a LONGNAMES enum', () => { it('returns true when the ancestor is the immediate parent', () => {
expect(name.LONGNAMES).toBeObject(); expect(name.hasAncestor('module:foo', 'module:foo~bar')).toBe(true);
}); });
it('has a longnamesToTree method', () => { it('returns true when the ancestor is not the immediate parent', () => {
expect(name.longnamesToTree).toBeFunction(); expect(name.hasAncestor('module:foo', 'module:foo~bar.Baz#qux')).toBe(true);
}); });
it('has a MODULE_NAMESPACE property', () => { it('returns false when a non-ancestor is passed in', () => {
expect(name.MODULE_NAMESPACE).toBeString(); expect(name.hasAncestor('module:foo', 'foo')).toBe(false);
}); });
it('has a nameIsLongname method', () => { it('returns false if the parent and child are the same', () => {
expect(name.nameIsLongname).toBeFunction(); expect(name.hasAncestor('module:foo', 'module:foo')).toBe(false);
});
});
describe('hasLeadingScope', () => {
it('returns true if the string starts with a scope character', () => {
expect(name.hasLeadingScope('#foo')).toBeTrue();
}); });
it('has a prototypeToPunc method', () => { it('returns false if the string does not start with a scope character', () => {
expect(name.prototypeToPunc).toBeFunction(); expect(name.hasLeadingScope('!foo')).toBeFalse();
});
});
describe('hasTrailingScope', () => {
it('returns true if the string ends with a scope character', () => {
expect(name.hasTrailingScope('Foo#')).toBeTrue();
}); });
it('has a PUNC_TO_SCOPE enum', () => { it('returns false if the string does not end with a scope character', () => {
expect(name.PUNC_TO_SCOPE).toBeObject(); expect(name.hasTrailingScope('Foo!')).toBeFalse();
});
});
describe('LONGNAMES', () => {
it('has an ANONYMOUS property', () => {
expect(name.LONGNAMES.ANONYMOUS).toBeString();
}); });
it('has a SCOPE enum', () => { it('has a GLOBAL property', () => {
expect(name.SCOPE).toBeObject(); expect(name.LONGNAMES.GLOBAL).toBeString();
});
});
xdescribe('longnamesToTree', () => {
// TODO: tests
});
describe('MODULE_NAMESPACE', () => {
// This is just a string, so nothing to test.
});
xdescribe('nameIsLongname', () => {
// TODO(hegemonic)
});
xdescribe('prototypeToPunc', () => {
// TODO(hegemonic)
});
describe('PUNC_TO_SCOPE', () => {
it('has the same number of properties as SCOPE_TO_PUNC', () => {
expect(Object.keys(name.PUNC_TO_SCOPE).length).toBe(Object.keys(name.SCOPE_TO_PUNC).length);
});
});
describe('SCOPE', () => {
const SCOPE = name.SCOPE;
it('has a NAMES enum', () => {
expect(SCOPE.NAMES).toBeObject();
}); });
it('has a SCOPE_TO_PUNC enum', () => { it('has a PUNC enum', () => {
expect(name.SCOPE_TO_PUNC).toBeObject(); expect(SCOPE.PUNC).toBeObject();
}); });
it('has a splitNameAndDescription method', () => { describe('NAMES', () => {
expect(name.splitNameAndDescription).toBeFunction(); it('has a GLOBAL property', () => {
expect(SCOPE.NAMES.GLOBAL).toBeString();
});
it('has an INNER property', () => {
expect(SCOPE.NAMES.INNER).toBeString();
});
it('has an INSTANCE property', () => {
expect(SCOPE.NAMES.INSTANCE).toBeString();
});
it('has a STATIC property', () => {
expect(SCOPE.NAMES.STATIC).toBeString();
});
}); });
it('has a stripNamespace method', () => { describe('PUNC', () => {
expect(name.stripNamespace).toBeFunction(); it('has an INNER property', () => {
expect(SCOPE.PUNC.INNER).toBeString();
});
it('has an INSTANCE property', () => {
expect(SCOPE.PUNC.INSTANCE).toBeString();
});
it('has a STATIC property', () => {
expect(SCOPE.PUNC.STATIC).toBeString();
});
});
});
describe('SCOPE_TO_PUNC', () => {
it('has an inner property', () => {
expect(name.SCOPE_TO_PUNC.inner).toBeString();
}); });
it('has a stripVariation method', () => { it('has an instance property', () => {
expect(name.stripVariation).toBeFunction(); expect(name.SCOPE_TO_PUNC.instance).toBeString();
}); });
it('has a toParts method', () => { it('has a static property', () => {
expect(name.toParts).toBeFunction(); expect(name.SCOPE_TO_PUNC.static).toBeString();
});
});
describe('splitNameAndDescription', () => {
// TODO(hegemonic): This has never worked
xit('separates the name from the description', () => {
const parts = name.splitNameAndDescription(
'ns.Page#"last \\"sentence\\"".words~sort(2) - This is a description. '
);
expect(parts.name).toBe('ns.Page#"last \\"sentence\\"".words~sort(2)');
expect(parts.description).toBe('This is a description.');
}); });
describe('applyNamespace', () => { it('strips a separator when it starts on the same line as the name', () => {
it('applies the namespace to the name part of the longname', () => { const parts = name.splitNameAndDescription('socket - The networking kind, not the wrench.');
expect(name.applyNamespace('lib.Panel#open', 'event')).toBe('lib.Panel#event:open');
});
it('applies the namespace to the start of a top-level longname', () => { expect(parts.name).toBe('socket');
expect(name.applyNamespace('math/bigint', 'module')).toBe('module:math/bigint'); expect(parts.description).toBe('The networking kind, not the wrench.');
});
// TODO(hegemonic): This has never worked
xit('handles longnames with quoted portions', () => {
expect(name.applyNamespace('foo."*don\'t.look~in#here!"', 'event'))
.toBe('foo.event:"*don\'t.look~in#here!"');
});
it('handles longnames that already have namespaces', () => {
expect(name.applyNamespace('lib.Panel#event:open', 'event'))
.toBe('lib.Panel#event:open');
});
}); });
xdescribe('fromParts', () => { it('does not strip a separator that is preceded by a line break', () => {
// TODO: tests const parts = name.splitNameAndDescription('socket\n - The networking kind, not the wrench.');
expect(parts.name).toBe('socket');
expect(parts.description).toBe('- The networking kind, not the wrench.');
}); });
describe('getBasename', () => { it('allows default values to contain square brackets', () => {
it('returns null on empty input', () => { const parts = name.splitNameAndDescription(
expect(name.getBasename()).toBeNull(); '[path=["home", "user"]] - Path split into components'
}); );
it('returns the original value if it has no punctuation except underscores', () => { expect(parts.name).toBe('[path=["home", "user"]]');
expect(name.getBasename('foo_bar')).toBe('foo_bar'); expect(parts.description).toBe('Path split into components');
});
it('returns the basename if the original value has punctuation', () => {
expect(name.getBasename('foo.Bar#baz')).toBe('foo');
});
}); });
describe('hasAncestor', () => { it('allows default values to contain unmatched square brackets inside strings', () => {
it('returns false if no parent is specified', () => { const parts = name.splitNameAndDescription(
expect(name.hasAncestor(null, 'foo')).toBe(false); '[path=["Unmatched begin: ["]] - Path split into components'
}); );
it('returns false if no child is specified', () => { expect(parts.name).toBe('[path=["Unmatched begin: ["]]');
expect(name.hasAncestor('foo')).toBe(false); expect(parts.description).toBe('Path split into components');
});
it('returns true when the ancestor is the immediate parent', () => {
expect(name.hasAncestor('module:foo', 'module:foo~bar')).toBe(true);
});
it('returns true when the ancestor is not the immediate parent', () => {
expect(name.hasAncestor('module:foo', 'module:foo~bar.Baz#qux')).toBe(true);
});
it('returns false when a non-ancestor is passed in', () => {
expect(name.hasAncestor('module:foo', 'foo')).toBe(false);
});
it('returns false if the parent and child are the same', () => {
expect(name.hasAncestor('module:foo', 'module:foo')).toBe(false);
});
}); });
describe('hasLeadingScope', () => { it('fails gracefully when the default value has an unmatched square bracket', () => {
it('returns true if the string starts with a scope character', () => { const parts = name.splitNameAndDescription(
expect(name.hasLeadingScope('#foo')).toBeTrue(); '[path=["home", "user"] - Path split into components'
}); );
it('returns false if the string does not start with a scope character', () => { expect(parts).toBeObject();
expect(name.hasLeadingScope('!foo')).toBeFalse(); expect(parts.name).toBe('[path=["home", "user"]');
}); expect(parts.description).toBe('Path split into components');
}); });
describe('hasTrailingScope', () => { it('fails gracefully when the default value has an unmatched quote', () => {
it('returns true if the string ends with a scope character', () => { const parts = name.splitNameAndDescription(
expect(name.hasTrailingScope('Foo#')).toBeTrue(); '[path=["home", "user] - Path split into components'
}); );
it('returns false if the string does not end with a scope character', () => { expect(parts).toBeObject();
expect(name.hasTrailingScope('Foo!')).toBeFalse(); expect(parts.name).toBe('[path=["home", "user]');
}); expect(parts.description).toBe('Path split into components');
});
});
describe('stripNamespace', () => {
it('removes the namespace from the longname', () => {
expect(name.stripNamespace('module:foo/bar/baz')).toBe('foo/bar/baz');
}); });
describe('LONGNAMES', () => { it('does not remove the namespace from a child member', () => {
it('has an ANONYMOUS property', () => { expect(name.stripNamespace('foo/bar.baz~event:qux')).toBe('foo/bar.baz~event:qux');
expect(name.LONGNAMES.ANONYMOUS).toBeString();
});
it('has a GLOBAL property', () => {
expect(name.LONGNAMES.GLOBAL).toBeString();
});
}); });
xdescribe('longnamesToTree', () => { it('does not change longnames that do not have a namespace', () => {
// TODO: tests expect(name.stripNamespace('Foo#bar')).toBe('Foo#bar');
});
});
describe('stripVariation', () => {
it('does not change longnames with no variation', () => {
expect(name.stripVariation('Foo#bar')).toBe('Foo#bar');
}); });
describe('MODULE_NAMESPACE', () => { it('removes the variation if present', () => {
// This is just a string, so nothing to test. expect(name.stripVariation('Foo#bar(qux)')).toBe('Foo#bar');
});
});
describe('toParts', () => {
it('breaks up a longname into the correct parts', () => {
const parts = name.toParts('lib.Panel#open');
expect(parts.name).toBe('open');
expect(parts.memberof).toBe('lib.Panel');
expect(parts.scope).toBe('#');
}); });
xdescribe('nameIsLongname', () => { it('handles static names', () => {
// TODO(hegemonic) const parts = name.toParts('elements.selected.getVisible');
expect(parts.name).toBe('getVisible');
expect(parts.memberof).toBe('elements.selected');
expect(parts.scope).toBe('.');
}); });
xdescribe('prototypeToPunc', () => { it('handles members of a prototype', () => {
// TODO(hegemonic) const parts = name.toParts('Validator.prototype.$element');
expect(parts.name).toEqual('$element');
expect(parts.memberof).toEqual('Validator');
expect(parts.scope).toEqual('#');
}); });
describe('PUNC_TO_SCOPE', () => { it('handles inner names', () => {
it('has the same number of properties as SCOPE_TO_PUNC', () => { const parts = name.toParts('Button~_onclick');
expect(Object.keys(name.PUNC_TO_SCOPE).length)
.toBe(Object.keys(name.SCOPE_TO_PUNC).length); expect(parts.name).toEqual('_onclick');
}); expect(parts.memberof).toEqual('Button');
expect(parts.scope).toEqual('~');
}); });
describe('SCOPE', () => { it('handles global names', () => {
const SCOPE = name.SCOPE; const parts = name.toParts('close');
it('has a NAMES enum', () => { expect(parts.name).toEqual('close');
expect(SCOPE.NAMES).toBeObject(); expect(parts.memberof).toEqual('');
}); expect(parts.scope).toEqual('');
it('has a PUNC enum', () => {
expect(SCOPE.PUNC).toBeObject();
});
describe('NAMES', () => {
it('has a GLOBAL property', () => {
expect(SCOPE.NAMES.GLOBAL).toBeString();
});
it('has an INNER property', () => {
expect(SCOPE.NAMES.INNER).toBeString();
});
it('has an INSTANCE property', () => {
expect(SCOPE.NAMES.INSTANCE).toBeString();
});
it('has a STATIC property', () => {
expect(SCOPE.NAMES.STATIC).toBeString();
});
});
describe('PUNC', () => {
it('has an INNER property', () => {
expect(SCOPE.PUNC.INNER).toBeString();
});
it('has an INSTANCE property', () => {
expect(SCOPE.PUNC.INSTANCE).toBeString();
});
it('has a STATIC property', () => {
expect(SCOPE.PUNC.STATIC).toBeString();
});
});
}); });
describe('SCOPE_TO_PUNC', () => { it('handles a single property that uses bracket notation', () => {
it('has an inner property', () => { const parts = name.toParts('channels["#ops"]#open');
expect(name.SCOPE_TO_PUNC.inner).toBeString();
});
it('has an instance property', () => { expect(parts.name).toEqual('open');
expect(name.SCOPE_TO_PUNC.instance).toBeString(); expect(parts.memberof).toEqual('channels."#ops"');
}); expect(parts.scope).toEqual('#');
it('has a static property', () => {
expect(name.SCOPE_TO_PUNC.static).toBeString();
});
}); });
describe('splitNameAndDescription', () => { it('handles consecutive properties that use bracket notation', () => {
// TODO(hegemonic): This has never worked const parts = name.toParts('channels["#bots"]["log.max"]');
xit('separates the name from the description', () => {
const parts = name.splitNameAndDescription(
'ns.Page#"last \\"sentence\\"".words~sort(2) - This is a description. '
);
expect(parts.name).toBe('ns.Page#"last \\"sentence\\"".words~sort(2)'); expect(parts.name).toEqual('"log.max"');
expect(parts.description).toBe('This is a description.'); expect(parts.memberof).toEqual('channels."#bots"');
}); expect(parts.scope).toEqual('.');
it('strips a separator when it starts on the same line as the name', () => {
const parts = name.splitNameAndDescription(
'socket - The networking kind, not the wrench.'
);
expect(parts.name).toBe('socket');
expect(parts.description).toBe('The networking kind, not the wrench.');
});
it('does not strip a separator that is preceded by a line break', () => {
const parts = name.splitNameAndDescription(
'socket\n - The networking kind, not the wrench.'
);
expect(parts.name).toBe('socket');
expect(parts.description).toBe('- The networking kind, not the wrench.');
});
it('allows default values to contain square brackets', () => {
const parts = name.splitNameAndDescription(
'[path=["home", "user"]] - Path split into components'
);
expect(parts.name).toBe('[path=["home", "user"]]');
expect(parts.description).toBe('Path split into components');
});
it('allows default values to contain unmatched square brackets inside strings', () => {
const parts = name.splitNameAndDescription(
'[path=["Unmatched begin: ["]] - Path split into components'
);
expect(parts.name).toBe('[path=["Unmatched begin: ["]]');
expect(parts.description).toBe('Path split into components');
});
it('fails gracefully when the default value has an unmatched square bracket', () => {
const parts = name.splitNameAndDescription(
'[path=["home", "user"] - Path split into components'
);
expect(parts).toBeObject();
expect(parts.name).toBe('[path=["home", "user"]');
expect(parts.description).toBe('Path split into components');
});
it('fails gracefully when the default value has an unmatched quote', () => {
const parts = name.splitNameAndDescription(
'[path=["home", "user] - Path split into components'
);
expect(parts).toBeObject();
expect(parts.name).toBe('[path=["home", "user]');
expect(parts.description).toBe('Path split into components');
});
}); });
describe('stripNamespace', () => { it('handles a property that uses single-quoted bracket notation', () => {
it('removes the namespace from the longname', () => { const parts = name.toParts("channels['#ops']");
expect(name.stripNamespace('module:foo/bar/baz')).toBe('foo/bar/baz');
});
it('does not remove the namespace from a child member', () => { expect(parts.name).toBe("'#ops'");
expect(name.stripNamespace('foo/bar.baz~event:qux')).toBe('foo/bar.baz~event:qux'); expect(parts.memberof).toBe('channels');
}); expect(parts.scope).toBe('.');
it('does not change longnames that do not have a namespace', () => {
expect(name.stripNamespace('Foo#bar')).toBe('Foo#bar');
});
}); });
describe('stripVariation', () => { it('handles double-quoted strings', () => {
it('does not change longnames with no variation', () => { const parts = name.toParts('"foo.bar"');
expect(name.stripVariation('Foo#bar')).toBe('Foo#bar');
});
it('removes the variation if present', () => { expect(parts.name).toEqual('"foo.bar"');
expect(name.stripVariation('Foo#bar(qux)')).toBe('Foo#bar'); expect(parts.longname).toEqual('"foo.bar"');
}); expect(parts.memberof).toEqual('');
expect(parts.scope).toEqual('');
}); });
describe('toParts', () => { it('handles single-quoted strings', () => {
it('breaks up a longname into the correct parts', () => { const parts = name.toParts("'foo.bar'");
const parts = name.toParts('lib.Panel#open');
expect(parts.name).toBe('open'); expect(parts.name).toBe("'foo.bar'");
expect(parts.memberof).toBe('lib.Panel'); expect(parts.longname).toBe("'foo.bar'");
expect(parts.scope).toBe('#'); expect(parts.memberof).toBe('');
}); expect(parts.scope).toBe('');
it('handles static names', () => {
const parts = name.toParts('elements.selected.getVisible');
expect(parts.name).toBe('getVisible');
expect(parts.memberof).toBe('elements.selected');
expect(parts.scope).toBe('.');
});
it('handles members of a prototype', () => {
const parts = name.toParts('Validator.prototype.$element');
expect(parts.name).toEqual('$element');
expect(parts.memberof).toEqual('Validator');
expect(parts.scope).toEqual('#');
});
it('handles inner names', () => {
const parts = name.toParts('Button~_onclick');
expect(parts.name).toEqual('_onclick');
expect(parts.memberof).toEqual('Button');
expect(parts.scope).toEqual('~');
});
it('handles global names', () => {
const parts = name.toParts('close');
expect(parts.name).toEqual('close');
expect(parts.memberof).toEqual('');
expect(parts.scope).toEqual('');
});
it('handles a single property that uses bracket notation', () => {
const parts = name.toParts('channels["#ops"]#open');
expect(parts.name).toEqual('open');
expect(parts.memberof).toEqual('channels."#ops"');
expect(parts.scope).toEqual('#');
});
it('handles consecutive properties that use bracket notation', () => {
const parts = name.toParts('channels["#bots"]["log.max"]');
expect(parts.name).toEqual('"log.max"');
expect(parts.memberof).toEqual('channels."#bots"');
expect(parts.scope).toEqual('.');
});
it('handles a property that uses single-quoted bracket notation', () => {
const parts = name.toParts("channels['#ops']");
expect(parts.name).toBe("'#ops'");
expect(parts.memberof).toBe('channels');
expect(parts.scope).toBe('.');
});
it('handles double-quoted strings', () => {
const parts = name.toParts('"foo.bar"');
expect(parts.name).toEqual('"foo.bar"');
expect(parts.longname).toEqual('"foo.bar"');
expect(parts.memberof).toEqual('');
expect(parts.scope).toEqual('');
});
it('handles single-quoted strings', () => {
const parts = name.toParts("'foo.bar'");
expect(parts.name).toBe("'foo.bar'");
expect(parts.longname).toBe("'foo.bar'");
expect(parts.memberof).toBe('');
expect(parts.scope).toBe('');
});
it('handles variations', () => {
const parts = name.toParts('anim.fadein(2)');
expect(parts.variation).toEqual('2');
expect(parts.name).toEqual('fadein');
expect(parts.longname).toEqual('anim.fadein(2)');
});
}); });
it('handles variations', () => {
const parts = name.toParts('anim.fadein(2)');
expect(parts.variation).toEqual('2');
expect(parts.name).toEqual('fadein');
expect(parts.longname).toEqual('anim.fadein(2)');
});
});
}); });

View File

@ -1,359 +1,362 @@
module.exports = { module.exports = {
env: { env: {
es6: true, es6: true,
jasmine: true, jasmine: true,
node: true node: true,
}, },
parserOptions: { parserOptions: {
ecmaVersion: 2018, ecmaVersion: 2018,
sourceType: 'module' sourceType: 'module',
}, },
rules: { rules: {
// Possible errors // Possible errors
'for-direction': 'error', 'for-direction': 'error',
'getter-return': 'error', 'getter-return': 'error',
'no-async-promise-executor': 'error', 'no-async-promise-executor': 'error',
'no-await-in-loop': 'error', 'no-await-in-loop': 'error',
'no-compare-neg-zero': 'error', 'no-compare-neg-zero': 'error',
'no-cond-assign': 'error', 'no-cond-assign': 'error',
'no-console': 'off', 'no-console': 'off',
'no-constant-condition': 'off', 'no-constant-condition': 'off',
'no-control-regex': 'error', 'no-control-regex': 'error',
'no-debugger': 'error', 'no-debugger': 'error',
'no-dupe-args': 'error', 'no-dupe-args': 'error',
'no-dupe-else-if': 'error', 'no-dupe-else-if': 'error',
'no-dupe-keys': 'error', 'no-dupe-keys': 'error',
'no-duplicate-case': 'error', 'no-duplicate-case': 'error',
'no-empty': 'error', 'no-empty': 'error',
'no-empty-character-class': 'error', 'no-empty-character-class': 'error',
'no-ex-assign': 'error', 'no-ex-assign': 'error',
'no-extra-boolean-cast': 'error', 'no-extra-boolean-cast': 'error',
'no-extra-parens': 'off', 'no-extra-parens': 'off',
'no-extra-semi': 'error', 'no-extra-semi': 'error',
'no-func-assign': 'error', 'no-func-assign': 'error',
'no-import-assign': 'error', 'no-import-assign': 'error',
'no-inner-declarations': ['error', 'functions'], 'no-inner-declarations': ['error', 'functions'],
'no-invalid-regexp': 'error', 'no-invalid-regexp': 'error',
'no-irregular-whitespace': 'error', 'no-irregular-whitespace': 'error',
'no-loss-of-precision': 'error', 'no-loss-of-precision': 'error',
'no-misleading-character-class': 'error', 'no-misleading-character-class': 'error',
'no-obj-calls': 'error', 'no-obj-calls': 'error',
'no-prototype-builtins': 'error', 'no-prototype-builtins': 'error',
'no-regex-spaces': 'error', 'no-regex-spaces': 'error',
'no-setter-return': 'error', 'no-setter-return': 'error',
'no-sparse-arrays': 'error', 'no-sparse-arrays': 'error',
'no-template-curly-in-string': 'error', 'no-template-curly-in-string': 'error',
'no-unexpected-multiline': 'error', 'no-unexpected-multiline': 'error',
'no-unreachable': 'error', 'no-unreachable': 'error',
'no-unreachable-loop': 'error', 'no-unreachable-loop': 'error',
'no-unsafe-finally': 'error', 'no-unsafe-finally': 'error',
'no-unsafe-negation': 'error', 'no-unsafe-negation': 'error',
'no-unsafe-optional-chaining': 'error', 'no-unsafe-optional-chaining': 'error',
'no-useless-backreference': 'error', 'no-useless-backreference': 'error',
'require-atomic-updates': 'error', 'require-atomic-updates': 'error',
'use-isnan': 'error', 'use-isnan': 'error',
'valid-typeof': 'error', 'valid-typeof': 'error',
// Best practices // Best practices
'accessor-pairs': 'error', 'accessor-pairs': 'error',
'array-callback-return': 'error', 'array-callback-return': 'error',
'block-scoped-var': 'off', 'block-scoped-var': 'off',
'class-methods-use-this': 'off', 'class-methods-use-this': 'off',
complexity: 'off', // TODO: enable complexity: 'off', // TODO: enable
'consistent-return': 'error', 'consistent-return': 'error',
curly: ['error', 'all'], curly: ['error', 'all'],
'default-case': 'error', 'default-case': 'error',
'default-case-last': 'error', 'default-case-last': 'error',
'default-param-last': 'error', 'default-param-last': 'error',
'dot-location': ['error', 'property'], 'dot-location': ['error', 'property'],
'dot-notation': 'error', 'dot-notation': 'error',
eqeqeq: ['error', 'smart'], eqeqeq: ['error', 'smart'],
'grouped-accessor-pairs': 'error', 'grouped-accessor-pairs': 'error',
'guard-for-in': 'error', 'guard-for-in': 'error',
'max-classes-per-file': 'off', 'max-classes-per-file': 'off',
'no-alert': 'error', 'no-alert': 'error',
'no-caller': 'error', 'no-caller': 'error',
'no-case-declarations': 'error', 'no-case-declarations': 'error',
'no-constructor-return': 'off', 'no-constructor-return': 'off',
'no-div-regex': 'error', 'no-div-regex': 'error',
'no-else-return': 'off', 'no-else-return': 'off',
'no-empty-function': 'error', 'no-empty-function': 'error',
'no-empty-pattern': 'error', 'no-empty-pattern': 'error',
'no-eq-null': 'error', 'no-eq-null': 'error',
'no-eval': 'error', 'no-eval': 'error',
'no-extend-native': 'error', 'no-extend-native': 'error',
'no-extra-bind': 'error', 'no-extra-bind': 'error',
'no-extra-label': 'error', 'no-extra-label': 'error',
'no-fallthrough': 'off', // disabled due to bug in ESLint 'no-fallthrough': 'off', // disabled due to bug in ESLint
'no-floating-decimal': 'error', 'no-floating-decimal': 'error',
'no-global-assign': 'error', 'no-global-assign': 'error',
'no-implicit-coercion': 'error', 'no-implicit-coercion': 'error',
'no-implicit-globals': 'error', 'no-implicit-globals': 'error',
'no-implied-eval': 'error', 'no-implied-eval': 'error',
'no-invalid-this': 'error', 'no-invalid-this': 'error',
'no-iterator': 'error', 'no-iterator': 'error',
'no-labels': 'error', 'no-labels': 'error',
'no-lone-blocks': 'error', 'no-lone-blocks': 'error',
'no-loop-func': 'error', 'no-loop-func': 'error',
'no-magic-numbers': 'off', // TODO: enable? 'no-magic-numbers': 'off', // TODO: enable?
'no-multi-spaces': 'error', 'no-multi-spaces': 'error',
'no-multi-str': 'error', 'no-multi-str': 'error',
'no-new': 'error', 'no-new': 'error',
'no-new-func': 'error', 'no-new-func': 'error',
'no-new-wrappers': 'error', 'no-new-wrappers': 'error',
'no-nonoctal-decimal-escape': 'error', 'no-nonoctal-decimal-escape': 'error',
'no-octal': 'error', 'no-octal': 'error',
'no-octal-escape': 'error', 'no-octal-escape': 'error',
'no-param-reassign': 'off', 'no-param-reassign': 'off',
'no-proto': 'error', 'no-proto': 'error',
'no-redeclare': 'error', 'no-redeclare': 'error',
'no-restricted-properties': 'off', 'no-restricted-properties': 'off',
'no-return-assign': 'error', 'no-return-assign': 'error',
'no-return-await': 'error', 'no-return-await': 'error',
'no-script-url': 'error', 'no-script-url': 'error',
'no-self-assign': 'error', 'no-self-assign': 'error',
'no-self-compare': 'error', 'no-self-compare': 'error',
'no-sequences': 'error', 'no-sequences': 'error',
'no-throw-literal': 'error', 'no-throw-literal': 'error',
'no-unmodified-loop-condition': 'error', 'no-unmodified-loop-condition': 'error',
'no-unused-expressions': 'error', 'no-unused-expressions': 'error',
'no-unused-labels': 'error', 'no-unused-labels': 'error',
'no-useless-call': 'error', 'no-useless-call': 'error',
'no-useless-catch': 'error', 'no-useless-catch': 'error',
'no-useless-concat': 'error', 'no-useless-concat': 'error',
'no-useless-escape': 'error', 'no-useless-escape': 'error',
'no-useless-return': 'error', 'no-useless-return': 'error',
'no-void': 'error', 'no-void': 'error',
'no-warning-comments': 'off', 'no-warning-comments': 'off',
'no-with': 'error', 'no-with': 'error',
'prefer-named-capture-group': 'off', // TODO: enable 'prefer-named-capture-group': 'off', // TODO: enable
'prefer-promise-reject-errors': 'error', 'prefer-promise-reject-errors': 'error',
'prefer-regex-literals': 'error', 'prefer-regex-literals': 'error',
radix: 'error', radix: 'error',
'require-await': 'error', 'require-await': 'error',
'require-unicode-regexp': 'off', 'require-unicode-regexp': 'off',
'vars-on-top': 'off', // TODO: enable 'vars-on-top': 'off', // TODO: enable
'wrap-iife': ['error', 'inside'], 'wrap-iife': ['error', 'inside'],
yoda: 'error', yoda: 'error',
// Strict mode // Strict mode
strict: ['error', 'global'], strict: ['error', 'global'],
// Variables // Variables
'init-declarations': 'off', 'init-declarations': 'off',
'no-delete-var': 'error', 'no-delete-var': 'error',
'no-label-var': 'error', 'no-label-var': 'error',
'no-restricted-globals': ['error', 'app', 'env'], 'no-restricted-globals': ['error', 'app', 'env'],
'no-shadow': 'error', 'no-shadow': 'error',
'no-shadow-restricted-names': 'error', 'no-shadow-restricted-names': 'error',
'no-undef': 'error', 'no-undef': 'error',
'no-undef-init': 'error', 'no-undef-init': 'error',
'no-undefined': 'off', 'no-undefined': 'off',
'no-unused-vars': 'error', 'no-unused-vars': 'error',
'no-use-before-define': 'error', 'no-use-before-define': 'error',
// Stylistic issues // Stylistic issues
'array-bracket-newline': 'off', 'array-bracket-newline': 'off',
'array-bracket-spacing': ['error', 'never'], 'array-bracket-spacing': ['error', 'never'],
'array-element-newline': 'off', 'array-element-newline': 'off',
'block-spacing': ['error', 'always'], 'block-spacing': ['error', 'always'],
'brace-style': 'off', // TODO: enable with "stroustrup" (or "1tbsp" + lots of cleanup) 'brace-style': 'off', // TODO: enable with "stroustrup" (or "1tbsp" + lots of cleanup)
camelcase: 'error', camelcase: 'error',
'capitalized-comments': 'off', 'capitalized-comments': 'off',
'comma-dangle': 'error', 'comma-dangle': 'error',
'comma-spacing': [ 'comma-spacing': [
'error', 'error',
{ {
before: false, before: false,
after: true after: true,
} },
], ],
'comma-style': ['error', 'last'], 'comma-style': ['error', 'last'],
'computed-property-spacing': ['error', 'never'], 'computed-property-spacing': ['error', 'never'],
'consistent-this': ['error', 'self'], 'consistent-this': ['error', 'self'],
'eol-last': 'error', 'eol-last': 'error',
'func-call-spacing': ['error', 'never'], 'func-call-spacing': ['error', 'never'],
'func-name-matching': ['error', 'always'], 'func-name-matching': ['error', 'always'],
'func-names': 'off', 'func-names': 'off',
'func-style': 'off', 'func-style': 'off',
'function-call-argument-newline:': 'off', 'function-call-argument-newline:': 'off',
'function-paren-newline': 'off', 'function-paren-newline': 'off',
'id-denylist': 'off', 'id-denylist': 'off',
'id-length': 'off', 'id-length': 'off',
'id-match': 'off', 'id-match': 'off',
'implicit-arrow-linebreak': 'off', 'implicit-arrow-linebreak': 'off',
indent: [ indent: [
'error', 'error',
4, 2,
{ {
SwitchCase: 1 SwitchCase: 1,
} },
], ],
'jsx-quotes': ['error', 'prefer-double'], 'jsx-quotes': ['error', 'prefer-double'],
'key-spacing': [ 'key-spacing': [
'error', 'error',
{ {
beforeColon: false, beforeColon: false,
afterColon: true afterColon: true,
} },
], ],
'keyword-spacing': [ 'keyword-spacing': [
'error', 'error',
{ {
before: true, before: true,
after: true after: true,
} },
], ],
'line-comment-position': 'off', 'line-comment-position': 'off',
'linebreak-style': 'off', 'linebreak-style': 'off',
'lines-around-comment': 'off', 'lines-around-comment': 'off',
'lines-between-class-members': 'off', 'lines-between-class-members': 'off',
'max-depth': 'off', // TODO: enable 'max-depth': 'off', // TODO: enable
'max-len': 'off', // TODO: enable 'max-len': 'off', // TODO: enable
'max-lines': 'off', 'max-lines': 'off',
'max-lines-per-function': 'off', 'max-lines-per-function': 'off',
'max-nested-callbacks': 'off', 'max-nested-callbacks': 'off',
'max-params': 'off', // TODO: enable 'max-params': 'off', // TODO: enable
'max-statements': 'off', 'max-statements': 'off',
'max-statements-per-line': 'off', 'max-statements-per-line': 'off',
'multiline-comment-style': 'off', 'multiline-comment-style': 'off',
'multiline-ternary': 'off', 'multiline-ternary': 'off',
'new-cap': 'error', 'new-cap': 'error',
'new-parens': 'error', 'new-parens': 'error',
'newline-per-chained-call': 'off', // TODO: enable 'newline-per-chained-call': 'off', // TODO: enable
'no-array-constructor': 'error', 'no-array-constructor': 'error',
'no-bitwise': 'error', 'no-bitwise': 'error',
'no-continue': 'off', 'no-continue': 'off',
'no-inline-comments': 'off', 'no-inline-comments': 'off',
'no-lonely-if': 'error', 'no-lonely-if': 'error',
'no-mixed-operators': 'error', 'no-mixed-operators': 'error',
'no-mixed-spaces-and-tabs': 'error', 'no-mixed-spaces-and-tabs': 'error',
'no-multi-assign': 'off', 'no-multi-assign': 'off',
'no-multiple-empty-lines': [ 'no-multiple-empty-lines': [
'error', 'error',
{ {
max: 2 max: 2,
} },
], ],
'no-negated-condition': 'off', 'no-negated-condition': 'off',
'no-nested-ternary': 'error', 'no-nested-ternary': 'error',
'no-new-object': 'error', 'no-new-object': 'error',
'no-plusplus': 'off', 'no-plusplus': 'off',
'no-restricted-syntax': 'off', 'no-restricted-syntax': 'off',
'no-tabs': 'error', 'no-tabs': 'error',
'no-ternary': 'off', 'no-ternary': 'off',
'no-trailing-spaces': 'error', 'no-trailing-spaces': 'error',
'no-underscore-dangle': 'off', 'no-underscore-dangle': 'off',
'no-unneeded-ternary': 'error', 'no-unneeded-ternary': 'error',
'no-whitespace-before-property': 'error', 'no-whitespace-before-property': 'error',
'nonblock-statement-body-position': 'off', 'nonblock-statement-body-position': 'off',
'object-curly-newline': 'off', 'object-curly-newline': 'off',
'object-curly-spacing': 'off', 'object-curly-spacing': 'off',
'object-property-newline': 'error', 'object-property-newline': 'error',
'one-var': 'off', 'one-var': 'off',
'one-var-declaration-per-line': 'error', 'one-var-declaration-per-line': 'error',
'operator-assignment': 'off', 'operator-assignment': 'off',
'operator-linebreak': ['error', 'after'], 'operator-linebreak': ['error', 'after'],
'padded-blocks': ['error', 'never'], 'padded-blocks': ['error', 'never'],
'padding-line-between-statements': [ 'padding-line-between-statements': [
'error', 'error',
{ {
blankLine: 'always', blankLine: 'always',
prev: '*', prev: '*',
next: 'return' next: 'return',
}, },
{ {
blankLine: 'always', blankLine: 'always',
prev: ['const', 'let', 'var'], prev: ['const', 'let', 'var'],
next: '*' next: '*',
}, },
{ {
blankLine: 'any', blankLine: 'any',
prev: ['const', 'let', 'var'], prev: ['const', 'let', 'var'],
next: ['const', 'let', 'var'] next: ['const', 'let', 'var'],
} },
], ],
'prefer-exponentiation-operator': 'error', 'prefer-exponentiation-operator': 'error',
'prefer-object-spread': 'off', 'prefer-object-spread': 'off',
'quote-props': 'off', 'quote-props': 'off',
quotes: ['error', 'single', 'avoid-escape'], quotes: ['error', 'single', 'avoid-escape'],
semi: ['error', 'always'], semi: ['error', 'always'],
'semi-spacing': 'error', 'semi-spacing': 'error',
'semi-style': ['error', 'last'], 'semi-style': ['error', 'last'],
'sort-keys': 'off', 'sort-keys': 'off',
'sort-vars': 'off', // TODO: enable? 'sort-vars': 'off', // TODO: enable?
'space-before-blocks': ['error', 'always'], 'space-before-blocks': ['error', 'always'],
'space-before-function-paren': ['error', { 'space-before-function-paren': [
anonymous: 'never', 'error',
named: 'never', {
asyncArrow: 'always' anonymous: 'never',
}], named: 'never',
'space-in-parens': 'off', // TODO: enable? asyncArrow: 'always',
'space-infix-ops': 'error', },
'space-unary-ops': 'error', ],
'spaced-comment': ['error', 'always'], 'space-in-parens': 'off', // TODO: enable?
'switch-colon-spacing': [ 'space-infix-ops': 'error',
'error', 'space-unary-ops': 'error',
{ 'spaced-comment': ['error', 'always'],
after: true, 'switch-colon-spacing': [
before: false 'error',
} {
], after: true,
'template-tag-spacing': ['error', 'never'], before: false,
'unicode-bom': ['error', 'never'], },
'wrap-regex': 'off', ],
'template-tag-spacing': ['error', 'never'],
'unicode-bom': ['error', 'never'],
'wrap-regex': 'off',
// ECMAScript 2015 // ECMAScript 2015
'arrow-body-style': ['error', 'as-needed'], 'arrow-body-style': ['error', 'as-needed'],
'arrow-parens': 'off', 'arrow-parens': 'off',
'arrow-spacing': [ 'arrow-spacing': [
'error', 'error',
{ {
before: true, before: true,
after: true after: true,
} },
], ],
'constructor-super': 'error', 'constructor-super': 'error',
'generator-star-spacing': [ 'generator-star-spacing': [
'error', 'error',
{ {
before: true, before: true,
after: false after: false,
} },
], ],
'no-class-assign': 'error', 'no-class-assign': 'error',
'no-confusing-arrow': 'error', 'no-confusing-arrow': 'error',
'no-const-assign': 'error', 'no-const-assign': 'error',
'no-dupe-class-members': 'error', 'no-dupe-class-members': 'error',
'no-duplicate-imports': [ 'no-duplicate-imports': [
'error', 'error',
{ {
includeExports: true includeExports: true,
} },
], ],
'no-new-symbol': 'error', 'no-new-symbol': 'error',
'no-restricted-exports': 'off', 'no-restricted-exports': 'off',
'no-restricted-imports': 'off', 'no-restricted-imports': 'off',
'no-this-before-super': 'error', 'no-this-before-super': 'error',
'no-useless-computed-key': 'error', 'no-useless-computed-key': 'error',
'no-useless-constructor': 'off', 'no-useless-constructor': 'off',
'no-useless-rename': 'error', 'no-useless-rename': 'error',
'no-var': 'off', // TODO: enable 'no-var': 'off', // TODO: enable
'object-shorthand': 'off', 'object-shorthand': 'off',
'prefer-arrow-callback': 'off', 'prefer-arrow-callback': 'off',
'prefer-const': 'off', 'prefer-const': 'off',
'prefer-destructuring': 'off', 'prefer-destructuring': 'off',
'prefer-numeric-literals': 'off', 'prefer-numeric-literals': 'off',
'prefer-rest-params': 'off', 'prefer-rest-params': 'off',
'prefer-spread': 'off', 'prefer-spread': 'off',
'prefer-template': 'off', 'prefer-template': 'off',
'require-yield': 'error', 'require-yield': 'error',
'rest-spread-spacing': ['error', 'never'], 'rest-spread-spacing': ['error', 'never'],
'sort-imports': 'error', 'sort-imports': 'error',
'symbol-description': 'error', 'symbol-description': 'error',
'template-curly-spacing': ['error', 'never'], 'template-curly-spacing': ['error', 'never'],
'yield-star-spacing': ['error', 'before'] 'yield-star-spacing': ['error', 'before'],
} },
}; };

View File

@ -3,7 +3,7 @@ const astNode = require('./lib/ast-node');
const { Syntax } = require('./lib/syntax'); const { Syntax } = require('./lib/syntax');
module.exports = { module.exports = {
AstBuilder, AstBuilder,
astNode, astNode,
Syntax Syntax,
}; };

View File

@ -3,63 +3,68 @@ const babelParser = require('@babel/parser');
const { log } = require('@jsdoc/util'); const { log } = require('@jsdoc/util');
// Exported so we can use them in tests. // Exported so we can use them in tests.
const parserOptions = exports.parserOptions = { const parserOptions = (exports.parserOptions = {
allowAwaitOutsideFunction: true, allowAwaitOutsideFunction: true,
allowImportExportEverywhere: true, allowImportExportEverywhere: true,
allowReturnOutsideFunction: true, allowReturnOutsideFunction: true,
allowSuperOutsideMethod: true, allowSuperOutsideMethod: true,
allowUndeclaredExports: true, allowUndeclaredExports: true,
plugins: [ plugins: [
'asyncGenerators', 'asyncGenerators',
'bigInt', 'bigInt',
'classPrivateMethods', 'classPrivateMethods',
'classPrivateProperties', 'classPrivateProperties',
'classProperties', 'classProperties',
['decorators', { [
decoratorsBeforeExport: true 'decorators',
}], {
'doExpressions', decoratorsBeforeExport: true,
'dynamicImport', },
'estree',
'exportDefaultFrom',
'exportNamespaceFrom',
'functionBind',
'functionSent',
'importMeta',
'jsx',
'logicalAssignment',
'nullishCoalescingOperator',
'numericSeparator',
'objectRestSpread',
'optionalCatchBinding',
'optionalChaining',
['pipelineOperator', {
proposal: 'minimal'
}],
'throwExpressions'
], ],
ranges: true 'doExpressions',
}; 'dynamicImport',
'estree',
'exportDefaultFrom',
'exportNamespaceFrom',
'functionBind',
'functionSent',
'importMeta',
'jsx',
'logicalAssignment',
'nullishCoalescingOperator',
'numericSeparator',
'objectRestSpread',
'optionalCatchBinding',
'optionalChaining',
[
'pipelineOperator',
{
proposal: 'minimal',
},
],
'throwExpressions',
],
ranges: true,
});
function parse(source, filename, sourceType) { function parse(source, filename, sourceType) {
let ast; let ast;
const options = _.defaults({}, parserOptions, {sourceType}); const options = _.defaults({}, parserOptions, { sourceType });
try { try {
ast = babelParser.parse(source, options); ast = babelParser.parse(source, options);
} } catch (e) {
catch (e) { log.error(`Unable to parse ${filename}: ${e.message}`);
log.error(`Unable to parse ${filename}: ${e.message}`); }
}
return ast; return ast;
} }
// TODO: docs // TODO: docs
class AstBuilder { class AstBuilder {
// TODO: docs // TODO: docs
static build(source, filename, sourceType) { static build(source, filename, sourceType) {
return parse(source, filename, sourceType); return parse(source, filename, sourceType);
} }
} }
exports.AstBuilder = AstBuilder; exports.AstBuilder = AstBuilder;

View File

@ -15,23 +15,26 @@ let uid = 100000000;
* @param {(Object|string)} node - The AST node to check, or the `type` property of a node. * @param {(Object|string)} node - The AST node to check, or the `type` property of a node.
* @return {boolean} Set to `true` if the node is a function or `false` in all other cases. * @return {boolean} Set to `true` if the node is a function or `false` in all other cases.
*/ */
const isFunction = exports.isFunction = node => { const isFunction = (exports.isFunction = (node) => {
let type; let type;
if (!node) { if (!node) {
return false; return false;
} }
if (typeof node === 'string') { if (typeof node === 'string') {
type = node; type = node;
} } else {
else { type = node.type;
type = node.type; }
}
return type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression || return (
type === Syntax.MethodDefinition || type === Syntax.ArrowFunctionExpression; type === Syntax.FunctionDeclaration ||
}; type === Syntax.FunctionExpression ||
type === Syntax.MethodDefinition ||
type === Syntax.ArrowFunctionExpression
);
});
/** /**
* Check whether an AST node creates a new scope. * Check whether an AST node creates a new scope.
@ -40,513 +43,521 @@ const isFunction = exports.isFunction = node => {
* @param {Object} node - The AST node to check. * @param {Object} node - The AST node to check.
* @return {Boolean} Set to `true` if the node creates a new scope, or `false` in all other cases. * @return {Boolean} Set to `true` if the node creates a new scope, or `false` in all other cases.
*/ */
exports.isScope = node => // TODO: handle blocks with "let" declarations exports.isScope = (
Boolean(node) && typeof node === 'object' && (node.type === Syntax.CatchClause || node // TODO: handle blocks with "let" declarations
node.type === Syntax.ClassDeclaration || node.type === Syntax.ClassExpression || isFunction(node)); ) =>
Boolean(node) &&
typeof node === 'object' &&
(node.type === Syntax.CatchClause ||
node.type === Syntax.ClassDeclaration ||
node.type === Syntax.ClassExpression ||
isFunction(node));
// TODO: docs // TODO: docs
exports.addNodeProperties = node => { exports.addNodeProperties = (node) => {
const newProperties = {}; const newProperties = {};
if (!node || typeof node !== 'object') { if (!node || typeof node !== 'object') {
return null; return null;
} }
if (!node.nodeId) { if (!node.nodeId) {
newProperties.nodeId = { newProperties.nodeId = {
value: `astnode${uid++}`, value: `astnode${uid++}`,
enumerable: true enumerable: true,
}; };
} }
if (_.isUndefined(node.parent)) { if (_.isUndefined(node.parent)) {
newProperties.parent = { newProperties.parent = {
// `null` means 'no parent', so use `undefined` for now // `null` means 'no parent', so use `undefined` for now
value: undefined, value: undefined,
writable: true writable: true,
}; };
} }
if (_.isUndefined(node.enclosingScope)) { if (_.isUndefined(node.enclosingScope)) {
newProperties.enclosingScope = { newProperties.enclosingScope = {
// `null` means 'no enclosing scope', so use `undefined` for now // `null` means 'no enclosing scope', so use `undefined` for now
value: undefined, value: undefined,
writable: true writable: true,
}; };
} }
if (_.isUndefined(node.parentId)) { if (_.isUndefined(node.parentId)) {
newProperties.parentId = { newProperties.parentId = {
enumerable: true, enumerable: true,
get() { get() {
return this.parent ? this.parent.nodeId : null; return this.parent ? this.parent.nodeId : null;
} },
}; };
} }
if (_.isUndefined(node.enclosingScopeId)) { if (_.isUndefined(node.enclosingScopeId)) {
newProperties.enclosingScopeId = { newProperties.enclosingScopeId = {
enumerable: true, enumerable: true,
get() { get() {
return this.enclosingScope ? this.enclosingScope.nodeId : null; return this.enclosingScope ? this.enclosingScope.nodeId : null;
} },
}; };
} }
Object.defineProperties(node, newProperties); Object.defineProperties(node, newProperties);
return node; return node;
}; };
// TODO: docs // TODO: docs
const nodeToValue = exports.nodeToValue = node => { const nodeToValue = (exports.nodeToValue = (node) => {
let key; let key;
let parent; let parent;
let str; let str;
let tempObject; let tempObject;
switch (node.type) { switch (node.type) {
case Syntax.ArrayExpression: case Syntax.ArrayExpression:
tempObject = []; tempObject = [];
node.elements.forEach((el, i) => { node.elements.forEach((el, i) => {
// handle sparse arrays. use `null` to represent missing values, consistent with // handle sparse arrays. use `null` to represent missing values, consistent with
// JSON.stringify([,]). // JSON.stringify([,]).
if (!el) { if (!el) {
tempObject[i] = null; tempObject[i] = null;
} } else {
else { tempObject[i] = nodeToValue(el);
tempObject[i] = nodeToValue(el); }
} });
});
str = JSON.stringify(tempObject); str = JSON.stringify(tempObject);
break; break;
case Syntax.AssignmentExpression: case Syntax.AssignmentExpression:
// falls through // falls through
case Syntax.AssignmentPattern: case Syntax.AssignmentPattern:
str = nodeToValue(node.left); str = nodeToValue(node.left);
break; break;
case Syntax.BigIntLiteral: case Syntax.BigIntLiteral:
str = node.value; str = node.value;
break; break;
case Syntax.ClassDeclaration: case Syntax.ClassDeclaration:
str = nodeToValue(node.id); str = nodeToValue(node.id);
break; break;
case Syntax.ClassPrivateProperty: case Syntax.ClassPrivateProperty:
// TODO: Strictly speaking, the name should be '#' plus node.key, but because we // TODO: Strictly speaking, the name should be '#' plus node.key, but because we
// already use '#' as scope punctuation, that causes JSDoc to get extremely confused. // already use '#' as scope punctuation, that causes JSDoc to get extremely confused.
// The solution probably involves quoting part or all of the name, but JSDoc doesn't // The solution probably involves quoting part or all of the name, but JSDoc doesn't
// deal with quoted names very nicely right now, and most people probably won't want to // deal with quoted names very nicely right now, and most people probably won't want to
// document class private properties anyhow. So for now, we'll just cheat and omit the // document class private properties anyhow. So for now, we'll just cheat and omit the
// leading '#'. // leading '#'.
str = nodeToValue(node.key.id); str = nodeToValue(node.key.id);
break; break;
case Syntax.ClassProperty: case Syntax.ClassProperty:
str = nodeToValue(node.key); str = nodeToValue(node.key);
break; break;
case Syntax.ExportAllDeclaration: case Syntax.ExportAllDeclaration:
// falls through // falls through
case Syntax.ExportDefaultDeclaration: case Syntax.ExportDefaultDeclaration:
str = 'module.exports'; str = 'module.exports';
break; break;
case Syntax.ExportNamedDeclaration: case Syntax.ExportNamedDeclaration:
if (node.declaration) { if (node.declaration) {
// like `var` in: export var foo = 'bar'; // like `var` in: export var foo = 'bar';
// we need a single value, so we use the first variable name // we need a single value, so we use the first variable name
if (node.declaration.declarations) { if (node.declaration.declarations) {
str = `exports.${nodeToValue(node.declaration.declarations[0])}`; str = `exports.${nodeToValue(node.declaration.declarations[0])}`;
} } else {
else { str = `exports.${nodeToValue(node.declaration)}`;
str = `exports.${nodeToValue(node.declaration)}`; }
} }
}
// otherwise we'll use the ExportSpecifier nodes // otherwise we'll use the ExportSpecifier nodes
break; break;
case Syntax.ExportSpecifier: case Syntax.ExportSpecifier:
str = `exports.${nodeToValue(node.exported)}`; str = `exports.${nodeToValue(node.exported)}`;
break; break;
case Syntax.ArrowFunctionExpression: case Syntax.ArrowFunctionExpression:
// falls through // falls through
case Syntax.FunctionDeclaration: case Syntax.FunctionDeclaration:
// falls through // falls through
case Syntax.FunctionExpression: case Syntax.FunctionExpression:
if (node.id && node.id.name) { if (node.id && node.id.name) {
str = node.id.name; str = node.id.name;
} }
break; break;
case Syntax.Identifier: case Syntax.Identifier:
str = node.name; str = node.name;
break; break;
case Syntax.Literal: case Syntax.Literal:
str = node.value; str = node.value;
break; break;
case Syntax.MemberExpression: case Syntax.MemberExpression:
// could be computed (like foo['bar']) or not (like foo.bar) // could be computed (like foo['bar']) or not (like foo.bar)
str = nodeToValue(node.object); str = nodeToValue(node.object);
if (node.computed) { if (node.computed) {
str += `[${node.property.raw}]`; str += `[${node.property.raw}]`;
} } else {
else { str += `.${nodeToValue(node.property)}`;
str += `.${nodeToValue(node.property)}`; }
} break;
break;
case Syntax.MethodDefinition: case Syntax.MethodDefinition:
parent = node.parent.parent; parent = node.parent.parent;
// for class expressions, we want the name of the variable the class is assigned to // for class expressions, we want the name of the variable the class is assigned to
// (but there won't be a name if the class is returned by an arrow function expression) // (but there won't be a name if the class is returned by an arrow function expression)
// TODO: we should use `LONGNAMES.ANONYMOUS` instead of an empty string, but that // TODO: we should use `LONGNAMES.ANONYMOUS` instead of an empty string, but that
// causes problems downstream if the parent class has an `@alias` tag // causes problems downstream if the parent class has an `@alias` tag
if (parent.type === Syntax.ClassExpression) { if (parent.type === Syntax.ClassExpression) {
str = nodeToValue(parent.parent) || ''; str = nodeToValue(parent.parent) || '';
} }
// for the constructor of a module's default export, use a special name // for the constructor of a module's default export, use a special name
else if (node.kind === 'constructor' && parent.parent && else if (
parent.parent.type === Syntax.ExportDefaultDeclaration) { node.kind === 'constructor' &&
str = 'module.exports'; parent.parent &&
} parent.parent.type === Syntax.ExportDefaultDeclaration
// for the constructor of a module's named export, use the name of the export ) {
// declaration str = 'module.exports';
else if (node.kind === 'constructor' && parent.parent && }
parent.parent.type === Syntax.ExportNamedDeclaration) { // for the constructor of a module's named export, use the name of the export
str = nodeToValue(parent.parent); // declaration
} else if (
// for other constructors, use the name of the parent class node.kind === 'constructor' &&
else if (node.kind === 'constructor') { parent.parent &&
str = nodeToValue(parent); parent.parent.type === Syntax.ExportNamedDeclaration
} ) {
// if the method is a member of a module's default export, ignore the name, because it's str = nodeToValue(parent.parent);
// irrelevant }
else if (parent.parent && parent.parent.type === Syntax.ExportDefaultDeclaration) { // for other constructors, use the name of the parent class
str = ''; else if (node.kind === 'constructor') {
} str = nodeToValue(parent);
// otherwise, use the class's name }
else { // if the method is a member of a module's default export, ignore the name, because it's
str = parent.id ? nodeToValue(parent.id) : ''; // irrelevant
} else if (parent.parent && parent.parent.type === Syntax.ExportDefaultDeclaration) {
str = '';
}
// otherwise, use the class's name
else {
str = parent.id ? nodeToValue(parent.id) : '';
}
if (node.kind !== 'constructor') { if (node.kind !== 'constructor') {
if (str) { if (str) {
str += node.static ? SCOPE.PUNC.STATIC : SCOPE.PUNC.INSTANCE; str += node.static ? SCOPE.PUNC.STATIC : SCOPE.PUNC.INSTANCE;
} }
str += nodeToValue(node.key); str += nodeToValue(node.key);
} }
break; break;
case Syntax.ObjectExpression: case Syntax.ObjectExpression:
tempObject = {}; tempObject = {};
node.properties.forEach(prop => { node.properties.forEach((prop) => {
// ExperimentalSpreadProperty have no key // ExperimentalSpreadProperty have no key
// like var hello = {...hi}; // like var hello = {...hi};
if (!prop.key) { if (!prop.key) {
return; return;
} }
key = prop.key.name; key = prop.key.name;
// preserve literal values so that the JSON form shows the correct type // preserve literal values so that the JSON form shows the correct type
if (prop.value.type === Syntax.Literal) { if (prop.value.type === Syntax.Literal) {
tempObject[key] = prop.value.value; tempObject[key] = prop.value.value;
} } else {
else { tempObject[key] = nodeToValue(prop);
tempObject[key] = nodeToValue(prop); }
} });
});
str = JSON.stringify(tempObject); str = JSON.stringify(tempObject);
break; break;
case Syntax.RestElement: case Syntax.RestElement:
str = nodeToValue(node.argument); str = nodeToValue(node.argument);
break; break;
case Syntax.ThisExpression: case Syntax.ThisExpression:
str = 'this'; str = 'this';
break; break;
case Syntax.UnaryExpression: case Syntax.UnaryExpression:
// like -1. in theory, operator can be prefix or postfix. in practice, any value with a // like -1. in theory, operator can be prefix or postfix. in practice, any value with a
// valid postfix operator (such as -- or ++) is not a UnaryExpression. // valid postfix operator (such as -- or ++) is not a UnaryExpression.
str = nodeToValue(node.argument); str = nodeToValue(node.argument);
if (node.prefix === true) { if (node.prefix === true) {
str = cast(node.operator + str); str = cast(node.operator + str);
} } else {
else { // this shouldn't happen
// this shouldn't happen throw new Error(`Found a UnaryExpression with a postfix operator: ${node}`);
throw new Error(`Found a UnaryExpression with a postfix operator: ${node}`); }
} break;
break;
case Syntax.VariableDeclarator: case Syntax.VariableDeclarator:
str = nodeToValue(node.id); str = nodeToValue(node.id);
break; break;
default: default:
str = ''; str = '';
} }
return str; return str;
}; });
// backwards compatibility // backwards compatibility
exports.nodeToString = nodeToValue; exports.nodeToString = nodeToValue;
// TODO: docs // TODO: docs
const getParamNames = exports.getParamNames = node => { const getParamNames = (exports.getParamNames = (node) => {
let params; let params;
if (!node || !node.params) { if (!node || !node.params) {
return []; return [];
} }
params = node.params.slice(0); params = node.params.slice(0);
return params.map(param => nodeToValue(param)); return params.map((param) => nodeToValue(param));
}; });
// TODO: docs // TODO: docs
const isAccessor = exports.isAccessor = node => Boolean(node) && typeof node === 'object' && const isAccessor = (exports.isAccessor = (node) =>
(node.type === Syntax.Property || node.type === Syntax.MethodDefinition) && Boolean(node) &&
(node.kind === 'get' || node.kind === 'set'); typeof node === 'object' &&
(node.type === Syntax.Property || node.type === Syntax.MethodDefinition) &&
(node.kind === 'get' || node.kind === 'set'));
// TODO: docs // TODO: docs
exports.isAssignment = node => Boolean(node) && typeof node === 'object' && exports.isAssignment = (node) =>
(node.type === Syntax.AssignmentExpression || node.type === Syntax.VariableDeclarator); Boolean(node) &&
typeof node === 'object' &&
(node.type === Syntax.AssignmentExpression || node.type === Syntax.VariableDeclarator);
// TODO: docs // TODO: docs
/** /**
* Retrieve information about the node, including its name and type. * Retrieve information about the node, including its name and type.
*/ */
exports.getInfo = node => { exports.getInfo = (node) => {
const info = {}; const info = {};
switch (node.type) { switch (node.type) {
// like the function in: "var foo = () => {}" // like the function in: "var foo = () => {}"
case Syntax.ArrowFunctionExpression: case Syntax.ArrowFunctionExpression:
info.node = node; info.node = node;
info.name = ''; info.name = '';
info.type = info.node.type; info.type = info.node.type;
info.paramnames = getParamNames(node); info.paramnames = getParamNames(node);
break; break;
// like: "foo = 'bar'" (after declaring foo) // like: "foo = 'bar'" (after declaring foo)
// like: "MyClass.prototype.myMethod = function() {}" (after declaring MyClass) // like: "MyClass.prototype.myMethod = function() {}" (after declaring MyClass)
case Syntax.AssignmentExpression: case Syntax.AssignmentExpression:
info.node = node.right; info.node = node.right;
info.name = nodeToValue(node.left); info.name = nodeToValue(node.left);
info.type = info.node.type; info.type = info.node.type;
info.value = nodeToValue(info.node); info.value = nodeToValue(info.node);
// if the assigned value is a function, we need to capture the parameter names here // if the assigned value is a function, we need to capture the parameter names here
info.paramnames = getParamNames(node.right); info.paramnames = getParamNames(node.right);
break; break;
// like "bar='baz'" in: function foo(bar='baz') {} // like "bar='baz'" in: function foo(bar='baz') {}
case Syntax.AssignmentPattern: case Syntax.AssignmentPattern:
info.node = node; info.node = node;
info.name = nodeToValue(node.left); info.name = nodeToValue(node.left);
info.type = info.node.type; info.type = info.node.type;
info.value = nodeToValue(info.node); info.value = nodeToValue(info.node);
break; break;
// like: "class Foo {}" // like: "class Foo {}"
// or "class" in: "export default class {}" // or "class" in: "export default class {}"
case Syntax.ClassDeclaration: case Syntax.ClassDeclaration:
info.node = node; info.node = node;
// if this class is the default export, we need to use a special name // if this class is the default export, we need to use a special name
if (node.parent && node.parent.type === Syntax.ExportDefaultDeclaration) { if (node.parent && node.parent.type === Syntax.ExportDefaultDeclaration) {
info.name = 'module.exports'; info.name = 'module.exports';
} } else {
else { info.name = node.id ? nodeToValue(node.id) : '';
info.name = node.id ? nodeToValue(node.id) : ''; }
} info.type = info.node.type;
info.type = info.node.type; info.paramnames = [];
info.paramnames = [];
node.body.body.some(({kind, value}) => { node.body.body.some(({ kind, value }) => {
if (kind === 'constructor') { if (kind === 'constructor') {
info.paramnames = getParamNames(value); info.paramnames = getParamNames(value);
return true; return true;
} }
return false; return false;
}); });
break; break;
// like "#b = 1;" in: "class A { #b = 1; }" // like "#b = 1;" in: "class A { #b = 1; }"
case Syntax.ClassPrivateProperty: case Syntax.ClassPrivateProperty:
info.node = node; info.node = node;
info.name = nodeToValue(info.node); info.name = nodeToValue(info.node);
info.type = info.node.type; info.type = info.node.type;
break; break;
// like "b = 1;" in: "class A { b = 1; }" // like "b = 1;" in: "class A { b = 1; }"
case Syntax.ClassProperty: case Syntax.ClassProperty:
info.node = node; info.node = node;
info.name = nodeToValue(info.node); info.name = nodeToValue(info.node);
info.type = info.node.type; info.type = info.node.type;
break; break;
// like: "export * from 'foo'" // like: "export * from 'foo'"
case Syntax.ExportAllDeclaration: case Syntax.ExportAllDeclaration:
info.node = node; info.node = node;
info.name = nodeToValue(info.node); info.name = nodeToValue(info.node);
info.type = info.node.type; info.type = info.node.type;
break; break;
// like: "export default 'foo'" // like: "export default 'foo'"
case Syntax.ExportDefaultDeclaration: case Syntax.ExportDefaultDeclaration:
info.node = node.declaration; info.node = node.declaration;
info.name = nodeToValue(node); info.name = nodeToValue(node);
info.type = info.node.type; info.type = info.node.type;
if ( isFunction(info.node) ) { if (isFunction(info.node)) {
info.paramnames = getParamNames(info.node); info.paramnames = getParamNames(info.node);
} }
break; break;
// like: "export var foo;" (has declaration) // like: "export var foo;" (has declaration)
// or: "export {foo}" (no declaration) // or: "export {foo}" (no declaration)
case Syntax.ExportNamedDeclaration: case Syntax.ExportNamedDeclaration:
info.node = node; info.node = node;
info.name = nodeToValue(info.node); info.name = nodeToValue(info.node);
info.type = info.node.declaration ? info.node.declaration.type : info.type = info.node.declaration ? info.node.declaration.type : Syntax.ObjectExpression;
Syntax.ObjectExpression;
if (info.node.declaration) { if (info.node.declaration) {
if ( isFunction(info.node.declaration) ) { if (isFunction(info.node.declaration)) {
info.paramnames = getParamNames(info.node.declaration); info.paramnames = getParamNames(info.node.declaration);
} }
// TODO: This duplicates logic for another node type in `jsdoc/src/visitor` in // TODO: This duplicates logic for another node type in `jsdoc/src/visitor` in
// `makeSymbolFoundEvent()`. Is there a way to combine the logic for both node types // `makeSymbolFoundEvent()`. Is there a way to combine the logic for both node types
// into a single module? // into a single module?
if (info.node.declaration.kind === 'const') { if (info.node.declaration.kind === 'const') {
info.kind = 'constant'; info.kind = 'constant';
} }
} }
break; break;
// like "foo as bar" in: "export {foo as bar}" // like "foo as bar" in: "export {foo as bar}"
case Syntax.ExportSpecifier: case Syntax.ExportSpecifier:
info.node = node; info.node = node;
info.name = nodeToValue(info.node); info.name = nodeToValue(info.node);
info.type = info.node.local.type; info.type = info.node.local.type;
if ( isFunction(info.node.local) ) { if (isFunction(info.node.local)) {
info.paramnames = getParamNames(info.node.local); info.paramnames = getParamNames(info.node.local);
} }
break; break;
// like: "function foo() {}" // like: "function foo() {}"
// or the function in: "export default function() {}" // or the function in: "export default function() {}"
case Syntax.FunctionDeclaration: case Syntax.FunctionDeclaration:
info.node = node; info.node = node;
info.name = node.id ? nodeToValue(node.id) : ''; info.name = node.id ? nodeToValue(node.id) : '';
info.type = info.node.type; info.type = info.node.type;
info.paramnames = getParamNames(node); info.paramnames = getParamNames(node);
break; break;
// like the function in: "var foo = function() {}" // like the function in: "var foo = function() {}"
case Syntax.FunctionExpression: case Syntax.FunctionExpression:
info.node = node; info.node = node;
// TODO: should we add a name for, e.g., "var foo = function bar() {}"? // TODO: should we add a name for, e.g., "var foo = function bar() {}"?
info.name = ''; info.name = '';
info.type = info.node.type; info.type = info.node.type;
info.paramnames = getParamNames(node); info.paramnames = getParamNames(node);
break; break;
// like the param "bar" in: "function foo(bar) {}" // like the param "bar" in: "function foo(bar) {}"
case Syntax.Identifier: case Syntax.Identifier:
info.node = node; info.node = node;
info.name = nodeToValue(info.node); info.name = nodeToValue(info.node);
info.type = info.node.type; info.type = info.node.type;
break; break;
// like "a.b.c" // like "a.b.c"
case Syntax.MemberExpression: case Syntax.MemberExpression:
info.node = node; info.node = node;
info.name = nodeToValue(info.node); info.name = nodeToValue(info.node);
info.type = info.node.type; info.type = info.node.type;
break; break;
// like: "foo() {}" // like: "foo() {}"
case Syntax.MethodDefinition: case Syntax.MethodDefinition:
info.node = node; info.node = node;
info.name = nodeToValue(info.node); info.name = nodeToValue(info.node);
info.type = info.node.type; info.type = info.node.type;
info.paramnames = getParamNames(node.value); info.paramnames = getParamNames(node.value);
break; break;
// like "a: 0" in "var foo = {a: 0}" // like "a: 0" in "var foo = {a: 0}"
case Syntax.Property: case Syntax.Property:
info.node = node.value; info.node = node.value;
info.name = nodeToValue(node.key); info.name = nodeToValue(node.key);
info.value = nodeToValue(info.node); info.value = nodeToValue(info.node);
// property names with unsafe characters must be quoted // property names with unsafe characters must be quoted
if ( !/^[$_a-zA-Z0-9]*$/.test(info.name) ) { if (!/^[$_a-zA-Z0-9]*$/.test(info.name)) {
info.name = `"${String(info.name).replace(/"/g, '\\"')}"`; info.name = `"${String(info.name).replace(/"/g, '\\"')}"`;
} }
if ( isAccessor(node) ) { if (isAccessor(node)) {
info.type = nodeToValue(info.node); info.type = nodeToValue(info.node);
info.paramnames = getParamNames(info.node); info.paramnames = getParamNames(info.node);
} } else {
else { info.type = info.node.type;
info.type = info.node.type; }
}
break; break;
// like "...bar" in: function foo(...bar) {} // like "...bar" in: function foo(...bar) {}
case Syntax.RestElement: case Syntax.RestElement:
info.node = node; info.node = node;
info.name = nodeToValue(info.node.argument); info.name = nodeToValue(info.node.argument);
info.type = info.node.type; info.type = info.node.type;
break; break;
// like: "var i = 0" (has init property) // like: "var i = 0" (has init property)
// like: "var i" (no init property) // like: "var i" (no init property)
case Syntax.VariableDeclarator: case Syntax.VariableDeclarator:
info.node = node.init || node.id; info.node = node.init || node.id;
info.name = node.id.name; info.name = node.id.name;
if (node.init) { if (node.init) {
info.type = info.node.type; info.type = info.node.type;
info.value = nodeToValue(info.node); info.value = nodeToValue(info.node);
} }
break; break;
default: default:
info.node = node; info.node = node;
info.type = info.node.type; info.type = info.node.type;
} }
return info; return info;
}; };

View File

@ -1,96 +1,96 @@
// TODO: docs // TODO: docs
exports.Syntax = { exports.Syntax = {
ArrayExpression: 'ArrayExpression', ArrayExpression: 'ArrayExpression',
ArrayPattern: 'ArrayPattern', ArrayPattern: 'ArrayPattern',
ArrowFunctionExpression: 'ArrowFunctionExpression', ArrowFunctionExpression: 'ArrowFunctionExpression',
AssignmentExpression: 'AssignmentExpression', AssignmentExpression: 'AssignmentExpression',
AssignmentPattern: 'AssignmentPattern', AssignmentPattern: 'AssignmentPattern',
AwaitExpression: 'AwaitExpression', AwaitExpression: 'AwaitExpression',
BigIntLiteral: 'BigIntLiteral', BigIntLiteral: 'BigIntLiteral',
BinaryExpression: 'BinaryExpression', BinaryExpression: 'BinaryExpression',
BindExpression: 'BindExpression', BindExpression: 'BindExpression',
BlockStatement: 'BlockStatement', BlockStatement: 'BlockStatement',
BreakStatement: 'BreakStatement', BreakStatement: 'BreakStatement',
CallExpression: 'CallExpression', CallExpression: 'CallExpression',
CatchClause: 'CatchClause', CatchClause: 'CatchClause',
ClassBody: 'ClassBody', ClassBody: 'ClassBody',
ClassDeclaration: 'ClassDeclaration', ClassDeclaration: 'ClassDeclaration',
ClassExpression: 'ClassExpression', ClassExpression: 'ClassExpression',
ClassPrivateProperty: 'ClassPrivateProperty', ClassPrivateProperty: 'ClassPrivateProperty',
ClassProperty: 'ClassProperty', ClassProperty: 'ClassProperty',
ComprehensionBlock: 'ComprehensionBlock', ComprehensionBlock: 'ComprehensionBlock',
ComprehensionExpression: 'ComprehensionExpression', ComprehensionExpression: 'ComprehensionExpression',
ConditionalExpression: 'ConditionalExpression', ConditionalExpression: 'ConditionalExpression',
ContinueStatement: 'ContinueStatement', ContinueStatement: 'ContinueStatement',
DebuggerStatement: 'DebuggerStatement', DebuggerStatement: 'DebuggerStatement',
Decorator: 'Decorator', Decorator: 'Decorator',
DoExpression: 'DoExpression', DoExpression: 'DoExpression',
DoWhileStatement: 'DoWhileStatement', DoWhileStatement: 'DoWhileStatement',
EmptyStatement: 'EmptyStatement', EmptyStatement: 'EmptyStatement',
ExperimentalRestProperty: 'ExperimentalRestProperty', ExperimentalRestProperty: 'ExperimentalRestProperty',
ExperimentalSpreadProperty: 'ExperimentalSpreadProperty', ExperimentalSpreadProperty: 'ExperimentalSpreadProperty',
ExportAllDeclaration: 'ExportAllDeclaration', ExportAllDeclaration: 'ExportAllDeclaration',
ExportDefaultDeclaration: 'ExportDefaultDeclaration', ExportDefaultDeclaration: 'ExportDefaultDeclaration',
ExportDefaultSpecifier: 'ExportDefaultSpecifier', ExportDefaultSpecifier: 'ExportDefaultSpecifier',
ExportNamedDeclaration: 'ExportNamedDeclaration', ExportNamedDeclaration: 'ExportNamedDeclaration',
ExportNamespaceSpecifier: 'ExportNamespaceSpecifier', ExportNamespaceSpecifier: 'ExportNamespaceSpecifier',
ExportSpecifier: 'ExportSpecifier', ExportSpecifier: 'ExportSpecifier',
ExpressionStatement: 'ExpressionStatement', ExpressionStatement: 'ExpressionStatement',
File: 'File', File: 'File',
ForInStatement: 'ForInStatement', ForInStatement: 'ForInStatement',
ForOfStatement: 'ForOfStatement', ForOfStatement: 'ForOfStatement',
ForStatement: 'ForStatement', ForStatement: 'ForStatement',
FunctionDeclaration: 'FunctionDeclaration', FunctionDeclaration: 'FunctionDeclaration',
FunctionExpression: 'FunctionExpression', FunctionExpression: 'FunctionExpression',
Identifier: 'Identifier', Identifier: 'Identifier',
IfStatement: 'IfStatement', IfStatement: 'IfStatement',
Import: 'Import', Import: 'Import',
ImportDeclaration: 'ImportDeclaration', ImportDeclaration: 'ImportDeclaration',
ImportDefaultSpecifier: 'ImportDefaultSpecifier', ImportDefaultSpecifier: 'ImportDefaultSpecifier',
ImportNamespaceSpecifier: 'ImportNamespaceSpecifier', ImportNamespaceSpecifier: 'ImportNamespaceSpecifier',
ImportSpecifier: 'ImportSpecifier', ImportSpecifier: 'ImportSpecifier',
JSXAttribute: 'JSXAttribute', JSXAttribute: 'JSXAttribute',
JSXClosingElement: 'JSXClosingElement', JSXClosingElement: 'JSXClosingElement',
JSXElement: 'JSXElement', JSXElement: 'JSXElement',
JSXEmptyExpression: 'JSXEmptyExpression', JSXEmptyExpression: 'JSXEmptyExpression',
JSXExpressionContainer: 'JSXExpressionContainer', JSXExpressionContainer: 'JSXExpressionContainer',
JSXIdentifier: 'JSXIdentifier', JSXIdentifier: 'JSXIdentifier',
JSXMemberExpression: 'JSXMemberExpression', JSXMemberExpression: 'JSXMemberExpression',
JSXNamespacedName: 'JSXNamespacedName', JSXNamespacedName: 'JSXNamespacedName',
JSXOpeningElement: 'JSXOpeningElement', JSXOpeningElement: 'JSXOpeningElement',
JSXSpreadAttribute: 'JSXSpreadAttribute', JSXSpreadAttribute: 'JSXSpreadAttribute',
JSXText: 'JSXText', JSXText: 'JSXText',
LabeledStatement: 'LabeledStatement', LabeledStatement: 'LabeledStatement',
LetStatement: 'LetStatement', LetStatement: 'LetStatement',
Literal: 'Literal', Literal: 'Literal',
LogicalExpression: 'LogicalExpression', LogicalExpression: 'LogicalExpression',
MemberExpression: 'MemberExpression', MemberExpression: 'MemberExpression',
MetaProperty: 'MetaProperty', MetaProperty: 'MetaProperty',
MethodDefinition: 'MethodDefinition', MethodDefinition: 'MethodDefinition',
NewExpression: 'NewExpression', NewExpression: 'NewExpression',
ObjectExpression: 'ObjectExpression', ObjectExpression: 'ObjectExpression',
ObjectPattern: 'ObjectPattern', ObjectPattern: 'ObjectPattern',
PrivateName: 'PrivateName', PrivateName: 'PrivateName',
Program: 'Program', Program: 'Program',
Property: 'Property', Property: 'Property',
RestElement: 'RestElement', RestElement: 'RestElement',
ReturnStatement: 'ReturnStatement', ReturnStatement: 'ReturnStatement',
SequenceExpression: 'SequenceExpression', SequenceExpression: 'SequenceExpression',
SpreadElement: 'SpreadElement', SpreadElement: 'SpreadElement',
Super: 'Super', Super: 'Super',
SwitchCase: 'SwitchCase', SwitchCase: 'SwitchCase',
SwitchStatement: 'SwitchStatement', SwitchStatement: 'SwitchStatement',
TaggedTemplateExpression: 'TaggedTemplateExpression', TaggedTemplateExpression: 'TaggedTemplateExpression',
TemplateElement: 'TemplateElement', TemplateElement: 'TemplateElement',
TemplateLiteral: 'TemplateLiteral', TemplateLiteral: 'TemplateLiteral',
ThisExpression: 'ThisExpression', ThisExpression: 'ThisExpression',
ThrowStatement: 'ThrowStatement', ThrowStatement: 'ThrowStatement',
TryStatement: 'TryStatement', TryStatement: 'TryStatement',
UnaryExpression: 'UnaryExpression', UnaryExpression: 'UnaryExpression',
UpdateExpression: 'UpdateExpression', UpdateExpression: 'UpdateExpression',
VariableDeclaration: 'VariableDeclaration', VariableDeclaration: 'VariableDeclaration',
VariableDeclarator: 'VariableDeclarator', VariableDeclarator: 'VariableDeclarator',
WhileStatement: 'WhileStatement', WhileStatement: 'WhileStatement',
WithStatement: 'WithStatement', WithStatement: 'WithStatement',
YieldExpression: 'YieldExpression' YieldExpression: 'YieldExpression',
}; };

View File

@ -1,31 +1,31 @@
const parse = require('../../index'); const parse = require('../../index');
describe('@jsdoc/parse', () => { describe('@jsdoc/parse', () => {
it('is an object', () => { it('is an object', () => {
expect(parse).toBeObject(); expect(parse).toBeObject();
});
describe('AstBuilder', () => {
it('is lib/ast-builder.AstBuilder', () => {
const { AstBuilder } = require('../../lib/ast-builder');
expect(parse.AstBuilder).toBe(AstBuilder);
}); });
});
describe('AstBuilder', () => { describe('astNode', () => {
it('is lib/ast-builder.AstBuilder', () => { it('is lib/ast-node', () => {
const { AstBuilder } = require('../../lib/ast-builder'); const astNode = require('../../lib/ast-node');
expect(parse.AstBuilder).toBe(AstBuilder); expect(parse.astNode).toBe(astNode);
});
}); });
});
describe('astNode', () => { describe('Syntax', () => {
it('is lib/ast-node', () => { it('is lib/syntax.Syntax', () => {
const astNode = require('../../lib/ast-node'); const { Syntax } = require('../../lib/syntax');
expect(parse.astNode).toBe(astNode); expect(parse.Syntax).toBe(Syntax);
});
});
describe('Syntax', () => {
it('is lib/syntax.Syntax', () => {
const { Syntax } = require('../../lib/syntax');
expect(parse.Syntax).toBe(Syntax);
});
}); });
});
}); });

View File

@ -1,41 +1,41 @@
/* global jsdoc */ /* global jsdoc */
describe('@jsdoc/parse/lib/ast-builder', () => { describe('@jsdoc/parse/lib/ast-builder', () => {
const astBuilder = require('../../../lib/ast-builder'); const astBuilder = require('../../../lib/ast-builder');
it('is an object', () => { it('is an object', () => {
expect(astBuilder).toBeObject(); expect(astBuilder).toBeObject();
});
it('exports an AstBuilder class', () => {
expect(astBuilder.AstBuilder).toBeFunction();
});
it('exports a parserOptions object', () => {
expect(astBuilder.parserOptions).toBeObject();
});
describe('AstBuilder', () => {
const { AstBuilder } = astBuilder;
// TODO: more tests
it('has a "build" static method', () => {
expect(AstBuilder.build).toBeFunction();
}); });
it('exports an AstBuilder class', () => { describe('build', () => {
expect(astBuilder.AstBuilder).toBeFunction(); // TODO: more tests
it('logs (not throws) an error when a file cannot be parsed', () => {
function parse() {
AstBuilder.build('qwerty!!!!!', 'bad.js');
}
expect(parse).not.toThrow();
expect(jsdoc.didLog(parse, 'error')).toBeTrue();
});
}); });
});
it('exports a parserOptions object', () => { describe('parserOptions', () => {
expect(astBuilder.parserOptions).toBeObject(); // TODO: tests
}); });
describe('AstBuilder', () => {
const { AstBuilder } = astBuilder;
// TODO: more tests
it('has a "build" static method', () => {
expect(AstBuilder.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');
}
expect(parse).not.toThrow();
expect(jsdoc.didLog(parse, 'error')).toBeTrue();
});
});
});
describe('parserOptions', () => {
// TODO: tests
});
}); });

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,13 @@
describe('@jsdoc/parse.Syntax', () => { describe('@jsdoc/parse.Syntax', () => {
const { Syntax } = require('../../../index'); const { Syntax } = require('../../../index');
it('is an object', () => { it('is an object', () => {
expect(Syntax).toBeObject(); expect(Syntax).toBeObject();
}); });
it('has values identical to their keys', () => { it('has values identical to their keys', () => {
for (const key of Object.keys(Syntax)) { for (const key of Object.keys(Syntax)) {
expect(key).toBe(Syntax[key]); expect(key).toBe(Syntax[key]);
} }
}); });
}); });

View File

@ -0,0 +1,14 @@
.editorconfig
.eslintignore
.eslintrc.js
.gitignore
.github/
.renovaterc.json
.travis.yml
CHANGES.md
CODE_OF_CONDUCT.md
CONTRIBUTING.md
gulpfile.js
lerna.json
packages/
test/

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,3 @@
# `@jsdoc/prettier-config`
A Prettier (https://prettier.io/) configuration for JSDoc.

View File

@ -0,0 +1,5 @@
// https://prettier.io/docs/en/options.html
module.exports = {
printWidth: 100,
singleQuote: true,
};

View File

@ -0,0 +1,31 @@
{
"name": "@jsdoc/prettier-config",
"version": "0.0.1",
"description": "A Prettier (https://prettier.io/) configuration for JSDoc.",
"keywords": [
"prettier",
"jsdoc"
],
"author": "Jeff Williams <jeffrey.l.williams@gmail.com>",
"homepage": "https://github.com/jsdoc/jsdoc",
"license": "Apache-2.0",
"main": "index.js",
"peerDependencies": {
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.4.1"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/jsdoc/jsdoc.git"
},
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1"
},
"bugs": {
"url": "https://github.com/jsdoc/jsdoc/issues"
}
}

View File

@ -2,6 +2,6 @@ const inline = require('./lib/inline');
const type = require('./lib/type'); const type = require('./lib/type');
module.exports = { module.exports = {
inline, inline,
type type,
}; };

View File

@ -45,7 +45,7 @@
* @returns {RegExp} A regular expression that matches the requested inline tag. * @returns {RegExp} A regular expression that matches the requested inline tag.
*/ */
function regExpFactory(tagName = '\\S+', prefix = '', suffix = '') { function regExpFactory(tagName = '\\S+', prefix = '', suffix = '') {
return new RegExp(`${prefix}\\{@${tagName}\\s+((?:.|\n)+?)\\}${suffix}`, 'i'); return new RegExp(`${prefix}\\{@${tagName}\\s+((?:.|\n)+?)\\}${suffix}`, 'i');
} }
/** /**
@ -70,43 +70,43 @@ exports.isInlineTag = (string, tagName) => regExpFactory(tagName, '^', '$').test
* @return {module:@jsdoc/tag.inline.InlineTagResult} The updated string, as well as information * @return {module:@jsdoc/tag.inline.InlineTagResult} The updated string, as well as information
* about the inline tags that were found. * about the inline tags that were found.
*/ */
const replaceInlineTags = exports.replaceInlineTags = (string, replacers) => { const replaceInlineTags = (exports.replaceInlineTags = (string, replacers) => {
const tagInfo = []; const tagInfo = [];
function replaceMatch(replacer, tag, match, text) { function replaceMatch(replacer, tag, match, text) {
const matchedTag = { const matchedTag = {
completeTag: match, completeTag: match,
tag: tag, tag: tag,
text: text text: text,
};
tagInfo.push(matchedTag);
return replacer(string, matchedTag);
}
string = string || '';
Object.keys(replacers).forEach(replacer => {
const tagRegExp = regExpFactory(replacer);
let matches;
let previousString;
// call the replacer once for each match
do {
matches = tagRegExp.exec(string);
if (matches) {
previousString = string;
string = replaceMatch(replacers[replacer], replacer, matches[0], matches[1]);
}
} while (matches && previousString !== string);
});
return {
tags: tagInfo,
newString: string.trim()
}; };
};
tagInfo.push(matchedTag);
return replacer(string, matchedTag);
}
string = string || '';
Object.keys(replacers).forEach((replacer) => {
const tagRegExp = regExpFactory(replacer);
let matches;
let previousString;
// call the replacer once for each match
do {
matches = tagRegExp.exec(string);
if (matches) {
previousString = string;
string = replaceMatch(replacers[replacer], replacer, matches[0], matches[1]);
}
} while (matches && previousString !== string);
});
return {
tags: tagInfo,
newString: string.trim(),
};
});
/** /**
* Replace all instances of an inline tag with other text. * Replace all instances of an inline tag with other text.
@ -118,13 +118,13 @@ const replaceInlineTags = exports.replaceInlineTags = (string, replacers) => {
* @return {module:@jsdoc/tag.inline.InlineTagResult} The updated string, as well as information * @return {module:@jsdoc/tag.inline.InlineTagResult} The updated string, as well as information
* about the inline tags that were found. * about the inline tags that were found.
*/ */
const replaceInlineTag = exports.replaceInlineTag = (string, tag, replacer) => { const replaceInlineTag = (exports.replaceInlineTag = (string, tag, replacer) => {
const replacers = {}; const replacers = {};
replacers[tag] = replacer; replacers[tag] = replacer;
return replaceInlineTags(string, replacers); return replaceInlineTags(string, replacers);
}; });
/** /**
* Extract inline tags from a string, replacing them with an empty string. * Extract inline tags from a string, replacing them with an empty string.
@ -135,5 +135,4 @@ const replaceInlineTag = exports.replaceInlineTag = (string, tag, replacer) => {
* about the inline tags that were found. * about the inline tags that were found.
*/ */
exports.extractInlineTag = (string, tag) => exports.extractInlineTag = (string, tag) =>
replaceInlineTag(string, tag, (str, {completeTag}) => replaceInlineTag(string, tag, (str, { completeTag }) => str.replace(completeTag, ''));
str.replace(completeTag, ''));

View File

@ -18,8 +18,7 @@ const { splitNameAndDescription } = require('@jsdoc/core').name;
/** @private */ /** @private */
function unescapeBraces(text) { function unescapeBraces(text) {
return text.replace(/\\\{/g, '{') return text.replace(/\\\{/g, '{').replace(/\\\}/g, '}');
.replace(/\\\}/g, '}');
} }
/** /**
@ -30,87 +29,87 @@ function unescapeBraces(text) {
* @return {module:@jsdoc/tag.type.TypeExpressionInfo} The type expression and updated tag text. * @return {module:@jsdoc/tag.type.TypeExpressionInfo} The type expression and updated tag text.
*/ */
function extractTypeExpression(string) { function extractTypeExpression(string) {
let completeExpression; let completeExpression;
let count = 0; let count = 0;
let position = 0; let position = 0;
let expression = ''; let expression = '';
const startIndex = string.search(/\{[^@]/); const startIndex = string.search(/\{[^@]/);
let textStartIndex; let textStartIndex;
if (startIndex !== -1) { if (startIndex !== -1) {
// advance to the first character in the type expression // advance to the first character in the type expression
position = textStartIndex = startIndex + 1; position = textStartIndex = startIndex + 1;
count++; count++;
while (position < string.length) { while (position < string.length) {
switch (string[position]) { switch (string[position]) {
case '\\': case '\\':
// backslash is an escape character, so skip the next character // backslash is an escape character, so skip the next character
position++; position++;
break; break;
case '{': case '{':
count++; count++;
break; break;
case '}': case '}':
count--; count--;
break; break;
default: default:
// do nothing // do nothing
} }
if (count === 0) { if (count === 0) {
completeExpression = string.slice(startIndex, position + 1); completeExpression = string.slice(startIndex, position + 1);
expression = string.slice(textStartIndex, position).trim(); expression = string.slice(textStartIndex, position).trim();
break; break;
} }
position++; position++;
}
} }
}
string = completeExpression ? string.replace(completeExpression, '') : string; string = completeExpression ? string.replace(completeExpression, '') : string;
return { return {
expression: unescapeBraces(expression), expression: unescapeBraces(expression),
newString: string.trim() newString: string.trim(),
}; };
} }
/** @private */ /** @private */
function getTagInfo(tagValue, canHaveName, canHaveType) { function getTagInfo(tagValue, canHaveName, canHaveType) {
let name = ''; let name = '';
let typeExpression = ''; let typeExpression = '';
let text = tagValue; let text = tagValue;
let expressionAndText; let expressionAndText;
let nameAndDescription; let nameAndDescription;
let typeOverride; let typeOverride;
if (canHaveType) { if (canHaveType) {
expressionAndText = extractTypeExpression(text); expressionAndText = extractTypeExpression(text);
typeExpression = expressionAndText.expression; typeExpression = expressionAndText.expression;
text = expressionAndText.newString; text = expressionAndText.newString;
}
if (canHaveName) {
nameAndDescription = splitNameAndDescription(text);
name = nameAndDescription.name;
text = nameAndDescription.description;
}
// an inline @type tag, like {@type Foo}, overrides the type expression
if (canHaveType) {
typeOverride = extractInlineTag(text, 'type');
if (typeOverride.tags && typeOverride.tags[0]) {
typeExpression = typeOverride.tags[0].text;
} }
text = typeOverride.newString;
}
if (canHaveName) { return {
nameAndDescription = splitNameAndDescription(text); name: name,
name = nameAndDescription.name; typeExpression: typeExpression,
text = nameAndDescription.description; text: text,
} };
// an inline @type tag, like {@type Foo}, overrides the type expression
if (canHaveType) {
typeOverride = extractInlineTag(text, 'type');
if (typeOverride.tags && typeOverride.tags[0]) {
typeExpression = typeOverride.tags[0].text;
}
text = typeOverride.newString;
}
return {
name: name,
typeExpression: typeExpression,
text: text
};
} }
/** /**
@ -142,80 +141,80 @@ function getTagInfo(tagValue, canHaveName, canHaveType) {
* @return {module:@jsdoc/tag.type.TagInfo} Updated information from the tag. * @return {module:@jsdoc/tag.type.TagInfo} Updated information from the tag.
*/ */
function parseName(tagInfo) { function parseName(tagInfo) {
// like '[foo]' or '[ foo ]' or '[foo=bar]' or '[ foo=bar ]' or '[ foo = bar ]' // like '[foo]' or '[ foo ]' or '[foo=bar]' or '[ foo=bar ]' or '[ foo = bar ]'
// or 'foo=bar' or 'foo = bar' // or 'foo=bar' or 'foo = bar'
if ( /^(\[)?\s*(.+?)\s*(\])?$/.test(tagInfo.name) ) { if (/^(\[)?\s*(.+?)\s*(\])?$/.test(tagInfo.name)) {
tagInfo.name = RegExp.$2; tagInfo.name = RegExp.$2;
// were the "optional" brackets present? // were the "optional" brackets present?
if (RegExp.$1 && RegExp.$3) { if (RegExp.$1 && RegExp.$3) {
tagInfo.optional = true; tagInfo.optional = true;
}
// like 'foo=bar' or 'foo = bar'
if ( /^(.+?)\s*=\s*(.+)$/.test(tagInfo.name) ) {
tagInfo.name = RegExp.$1;
tagInfo.defaultvalue = cast(RegExp.$2);
}
} }
return tagInfo; // like 'foo=bar' or 'foo = bar'
if (/^(.+?)\s*=\s*(.+)$/.test(tagInfo.name)) {
tagInfo.name = RegExp.$1;
tagInfo.defaultvalue = cast(RegExp.$2);
}
}
return tagInfo;
} }
/** @private */ /** @private */
function getTypeStrings(parsedType, isOutermostType) { function getTypeStrings(parsedType, isOutermostType) {
let applications; let applications;
let typeString; let typeString;
let types = []; let types = [];
const TYPES = catharsis.Types; const TYPES = catharsis.Types;
switch (parsedType.type) { switch (parsedType.type) {
case TYPES.AllLiteral: case TYPES.AllLiteral:
types.push('*'); types.push('*');
break; break;
case TYPES.FunctionType: case TYPES.FunctionType:
types.push('function'); types.push('function');
break; break;
case TYPES.NameExpression: case TYPES.NameExpression:
types.push(parsedType.name); types.push(parsedType.name);
break; break;
case TYPES.NullLiteral: case TYPES.NullLiteral:
types.push('null'); types.push('null');
break; break;
case TYPES.RecordType: case TYPES.RecordType:
types.push('Object'); types.push('Object');
break; break;
case TYPES.TypeApplication: case TYPES.TypeApplication:
// if this is the outermost type, we strip the modifiers; otherwise, we keep them // if this is the outermost type, we strip the modifiers; otherwise, we keep them
if (isOutermostType) { if (isOutermostType) {
applications = parsedType.applications.map(application => applications = parsedType.applications
catharsis.stringify(application)).join(', '); .map((application) => catharsis.stringify(application))
typeString = `${getTypeStrings(parsedType.expression)[0]}.<${applications}>`; .join(', ');
typeString = `${getTypeStrings(parsedType.expression)[0]}.<${applications}>`;
types.push(typeString); types.push(typeString);
} } else {
else { types.push(catharsis.stringify(parsedType));
types.push( catharsis.stringify(parsedType) ); }
} break;
break; case TYPES.TypeUnion:
case TYPES.TypeUnion: parsedType.elements.forEach((element) => {
parsedType.elements.forEach(element => { types = types.concat(getTypeStrings(element));
types = types.concat( getTypeStrings(element) ); });
}); break;
break; case TYPES.UndefinedLiteral:
case TYPES.UndefinedLiteral: types.push('undefined');
types.push('undefined'); break;
break; case TYPES.UnknownLiteral:
case TYPES.UnknownLiteral: types.push('?');
types.push('?'); break;
break; default:
default: // this shouldn't happen
// this shouldn't happen throw new Error(`unrecognized type ${parsedType.type} in parsed type: ${parsedType}`);
throw new Error(`unrecognized type ${parsedType.type} in parsed type: ${parsedType}`); }
}
return types; return types;
} }
/** /**
@ -227,40 +226,39 @@ function getTypeStrings(parsedType, isOutermostType) {
* @return {module:@jsdoc/tag.type.TagInfo} Updated information from the tag. * @return {module:@jsdoc/tag.type.TagInfo} Updated information from the tag.
*/ */
function parseTypeExpression(tagInfo) { function parseTypeExpression(tagInfo) {
let parsedType; let parsedType;
// don't try to parse empty type expressions
if (!tagInfo.typeExpression) {
return tagInfo;
}
try {
parsedType = catharsis.parse(tagInfo.typeExpression, {
jsdoc: true,
useCache: false
});
}
catch (e) {
// always re-throw so the caller has a chance to report which file was bad
throw new Error(`Invalid type expression "${tagInfo.typeExpression}": ${e.message}`);
}
tagInfo.type = tagInfo.type.concat( getTypeStrings(parsedType, true) );
tagInfo.parsedType = parsedType;
// Catharsis and JSDoc use the same names for 'optional' and 'nullable'...
['optional', 'nullable'].forEach(key => {
if (parsedType[key] !== null && parsedType[key] !== undefined) {
tagInfo[key] = parsedType[key];
}
});
// ...but not 'variable'.
if (parsedType.repeatable !== null && parsedType.repeatable !== undefined) {
tagInfo.variable = parsedType.repeatable;
}
// don't try to parse empty type expressions
if (!tagInfo.typeExpression) {
return tagInfo; return tagInfo;
}
try {
parsedType = catharsis.parse(tagInfo.typeExpression, {
jsdoc: true,
useCache: false,
});
} catch (e) {
// always re-throw so the caller has a chance to report which file was bad
throw new Error(`Invalid type expression "${tagInfo.typeExpression}": ${e.message}`);
}
tagInfo.type = tagInfo.type.concat(getTypeStrings(parsedType, true));
tagInfo.parsedType = parsedType;
// Catharsis and JSDoc use the same names for 'optional' and 'nullable'...
['optional', 'nullable'].forEach((key) => {
if (parsedType[key] !== null && parsedType[key] !== undefined) {
tagInfo[key] = parsedType[key];
}
});
// ...but not 'variable'.
if (parsedType.repeatable !== null && parsedType.repeatable !== undefined) {
tagInfo.variable = parsedType.repeatable;
}
return tagInfo;
} }
// TODO: allow users to add/remove type parsers (perhaps via plugins) // TODO: allow users to add/remove type parsers (perhaps via plugins)
@ -278,23 +276,23 @@ const typeParsers = [parseName, parseTypeExpression];
* @throws {Error} Thrown if a type expression cannot be parsed. * @throws {Error} Thrown if a type expression cannot be parsed.
*/ */
exports.parse = (tagValue, canHaveName, canHaveType) => { exports.parse = (tagValue, canHaveName, canHaveType) => {
let tagInfo; let tagInfo;
if (typeof tagValue !== 'string') { if (typeof tagValue !== 'string') {
tagValue = ''; tagValue = '';
} }
tagInfo = getTagInfo(tagValue, canHaveName, canHaveType); tagInfo = getTagInfo(tagValue, canHaveName, canHaveType);
tagInfo.type = tagInfo.type || []; tagInfo.type = tagInfo.type || [];
typeParsers.forEach(parser => { typeParsers.forEach((parser) => {
tagInfo = parser(tagInfo); tagInfo = parser(tagInfo);
}); });
// if we wanted a type, but the parsers didn't add any type names, use the type expression // if we wanted a type, but the parsers didn't add any type names, use the type expression
if (canHaveType && !tagInfo.type.length && tagInfo.typeExpression) { if (canHaveType && !tagInfo.type.length && tagInfo.typeExpression) {
tagInfo.type = [tagInfo.typeExpression]; tagInfo.type = [tagInfo.typeExpression];
} }
return tagInfo; return tagInfo;
}; };

View File

@ -1,23 +1,23 @@
const tag = require('../../index'); const tag = require('../../index');
describe('@jsdoc/tag', () => { describe('@jsdoc/tag', () => {
it('is an object', () => { it('is an object', () => {
expect(tag).toBeObject(); expect(tag).toBeObject();
});
describe('inline', () => {
it('is lib/inline', () => {
const inline = require('../../lib/inline');
expect(tag.inline).toBe(inline);
}); });
});
describe('inline', () => { describe('type', () => {
it('is lib/inline', () => { it('is lib/type', () => {
const inline = require('../../lib/inline'); const type = require('../../lib/type');
expect(tag.inline).toBe(inline); expect(tag.type).toBe(type);
});
});
describe('type', () => {
it('is lib/type', () => {
const type = require('../../lib/type');
expect(tag.type).toBe(type);
});
}); });
});
}); });

View File

@ -1,256 +1,256 @@
describe('@jsdoc/tag/lib/inline', () => { describe('@jsdoc/tag/lib/inline', () => {
const inline = require('../../../lib/inline'); const inline = require('../../../lib/inline');
it('is an object', () => { it('is an object', () => {
expect(inline).toBeObject(); expect(inline).toBeObject();
});
it('exports an isInlineTag function', () => {
expect(inline.isInlineTag).toBeFunction();
});
it('exports a replaceInlineTag function', () => {
expect(inline.replaceInlineTag).toBeFunction();
});
it('exports an extractInlineTag function', () => {
expect(inline.extractInlineTag).toBeFunction();
});
describe('isInlineTag', () => {
const isInlineTag = inline.isInlineTag;
it('identifies an inline tag', () => {
expect(isInlineTag('{@mytag hooray}', 'mytag')).toBeTrue();
}); });
it('exports an isInlineTag function', () => { it('identifies when something is not an inline tag', () => {
expect(inline.isInlineTag).toBeFunction(); expect(isInlineTag('mytag hooray', 'mytag')).toBeFalse();
}); });
it('exports a replaceInlineTag function', () => { it('reports that a string containing an inline tag is not an inline tag', () => {
expect(inline.replaceInlineTag).toBeFunction(); expect(isInlineTag('this is {@mytag hooray}', 'mytag')).toBeFalse();
}); });
it('exports an extractInlineTag function', () => { it('allows any inline tag by default', () => {
expect(inline.extractInlineTag).toBeFunction(); expect(isInlineTag('{@anyoldtag will do}')).toBeTrue();
}); });
describe('isInlineTag', () => { it('identifies things that are not inline tags when a tag name is not provided', () => {
const isInlineTag = inline.isInlineTag; expect(isInlineTag('mytag hooray')).toBeFalse();
it('identifies an inline tag', () => {
expect(isInlineTag('{@mytag hooray}', 'mytag')).toBeTrue();
});
it('identifies when something is not an inline tag', () => {
expect(isInlineTag('mytag hooray', 'mytag')).toBeFalse();
});
it('reports that a string containing an inline tag is not an inline tag', () => {
expect(isInlineTag('this is {@mytag hooray}', 'mytag')).toBeFalse();
});
it('allows any inline tag by default', () => {
expect(isInlineTag('{@anyoldtag will do}')).toBeTrue();
});
it('identifies things that are not inline tags when a tag name is not provided', () => {
expect(isInlineTag('mytag hooray')).toBeFalse();
});
it('allows regexp characters in the tag name', () => {
expect( isInlineTag('{@mytags hooray}', 'mytag\\S') ).toBeTrue();
});
it('returns false (rather than throwing) with invalid input', () => {
function badInput() {
return isInlineTag();
}
expect(badInput).not.toThrow();
expect(badInput()).toBeFalse();
});
}); });
describe('replaceInlineTag', () => { it('allows regexp characters in the tag name', () => {
it('throws if the tag is matched and the replacer is invalid', () => { expect(isInlineTag('{@mytags hooray}', 'mytag\\S')).toBeTrue();
function badReplacerUndefined() {
inline.replaceInlineTag('{@foo tag}', 'foo');
}
function badReplacerString() {
inline.replaceInlineTag('{@foo tag}', 'foo', 'hello');
}
expect(badReplacerUndefined).toThrow();
expect(badReplacerString).toThrow();
});
it('does not find anything if there is no text in braces', () => {
const replacer = jasmine.createSpy('replacer');
inline.replaceInlineTag('braceless text', 'foo', replacer);
expect(replacer).not.toHaveBeenCalled();
});
it('copes with bad escapement at the end of the string', () => {
const replacer = jasmine.createSpy('replacer');
inline.replaceInlineTag('bad {@foo escapement \\', 'foo', replacer);
expect(replacer).not.toHaveBeenCalled();
});
it('works if the tag is the entire string', () => {
function replacer(string, {completeTag, text}) {
expect(string).toBe('{@foo text in braces}');
expect(completeTag).toBe('{@foo text in braces}');
expect(text).toBe('text in braces');
return completeTag;
}
const result = inline.replaceInlineTag('{@foo text in braces}', 'foo',
replacer);
expect(result.tags[0]).toBeObject();
expect(result.tags[0].tag).toBe('foo');
expect(result.tags[0].text).toBe('text in braces');
expect(result.newString).toBe('{@foo text in braces}');
});
it('works if the tag is at the beginning of the string', () => {
function replacer(string, {completeTag, text}) {
expect(string).toBe('{@foo test string} ahoy');
expect(completeTag).toBe('{@foo test string}');
expect(text).toBe('test string');
return string;
}
const result = inline.replaceInlineTag('{@foo test string} ahoy', 'foo',
replacer);
expect(result.tags[0]).toBeObject();
expect(result.tags[0].tag).toBe('foo');
expect(result.tags[0].text).toBe('test string');
expect(result.newString).toBe('{@foo test string} ahoy');
});
it('works if the tag is in the middle of the string', () => {
function replacer(string, {completeTag, text}) {
expect(string).toBe('a {@foo test string} yay');
expect(completeTag).toBe('{@foo test string}');
expect(text).toBe('test string');
return string;
}
const result = inline.replaceInlineTag('a {@foo test string} yay', 'foo',
replacer);
expect(result.tags[0]).toBeObject();
expect(result.tags[0].tag).toBe('foo');
expect(result.tags[0].text).toBe('test string');
expect(result.newString).toBe('a {@foo test string} yay');
});
it('works if the tag is at the end of the string', () => {
function replacer(string, {completeTag, text}) {
expect(string).toBe('a {@foo test string}');
expect(completeTag).toBe('{@foo test string}');
expect(text).toBe('test string');
return string;
}
const result = inline.replaceInlineTag('a {@foo test string}', 'foo', replacer);
expect(result.tags[0]).toBeObject();
expect(result.tags[0].tag).toBe('foo');
expect(result.tags[0].text).toBe('test string');
expect(result.newString).toBe('a {@foo test string}');
});
it('replaces the string with the specified value', () => {
function replacer() {
return 'REPLACED!';
}
const result = inline.replaceInlineTag('a {@foo test string}', 'foo', replacer);
expect(result.newString).toBe('REPLACED!');
});
it('processes all occurrences of a tag', () => {
function replacer(string, {completeTag}) {
return string.replace(completeTag, 'stuff');
}
const result = inline.replaceInlineTag('some {@foo text} with multiple ' +
'{@foo tags}, {@foo like} {@foo this}', 'foo', replacer);
expect(result.tags.length).toBe(4);
expect(result.tags[0]).toBeObject();
expect(result.tags[0].tag).toBe('foo');
expect(result.tags[0].text).toBe('text');
expect(result.tags[1]).toBeObject();
expect(result.tags[1].tag).toBe('foo');
expect(result.tags[1].text).toBe('tags');
expect(result.tags[2]).toBeObject();
expect(result.tags[2].tag).toBe('foo');
expect(result.tags[2].text).toBe('like');
expect(result.tags[3]).toBeObject();
expect(result.tags[3].tag).toBe('foo');
expect(result.tags[3].text).toBe('this');
expect(result.newString).toBe('some stuff with multiple stuff, stuff stuff');
});
}); });
// Largely covered by the `replaceInlineTag()` tests. it('returns false (rather than throwing) with invalid input', () => {
describe('replaceInlineTags', () => { function badInput() {
it('works with an empty replacer object', () => { return isInlineTag();
const replacers = {}; }
const text = 'some {@foo text} to parse';
const result = inline.replaceInlineTags(text, replacers);
expect(result.newString).toBe(text); expect(badInput).not.toThrow();
}); expect(badInput()).toBeFalse();
});
});
it('works with one replacer', () => { describe('replaceInlineTag', () => {
const text = 'some {@foo text} with {@bar multiple} tags'; it('throws if the tag is matched and the replacer is invalid', () => {
const replacers = { function badReplacerUndefined() {
foo(string, tagInfo) { inline.replaceInlineTag('{@foo tag}', 'foo');
expect(tagInfo.completeTag).toBe('{@foo text}'); }
expect(tagInfo.text).toBe('text');
return string.replace(tagInfo.completeTag, 'stuff'); function badReplacerString() {
} inline.replaceInlineTag('{@foo tag}', 'foo', 'hello');
}; }
const result = inline.replaceInlineTags(text, replacers);
expect(result.newString).toBe('some stuff with {@bar multiple} tags'); expect(badReplacerUndefined).toThrow();
}); expect(badReplacerString).toThrow();
it('works with multiple replacers', () => {
const text = 'some {@foo text} with {@bar multiple} tags';
const replacers = {
foo(string, tagInfo) {
expect(tagInfo.completeTag).toBe('{@foo text}');
expect(tagInfo.text).toBe('text');
return string.replace(tagInfo.completeTag, 'stuff');
},
bar(string, tagInfo) {
expect(tagInfo.completeTag).toBe('{@bar multiple}');
expect(tagInfo.text).toBe('multiple');
return string.replace(tagInfo.completeTag, 'awesome');
}
};
const result = inline.replaceInlineTags(text, replacers);
expect(result.newString).toBe('some stuff with awesome tags');
});
}); });
// Largely covered by the `replaceInlineTag()` tests. it('does not find anything if there is no text in braces', () => {
describe('extractInlineTag', () => { const replacer = jasmine.createSpy('replacer');
it('works when a tag is specified', () => {
const result = inline.extractInlineTag('some {@tagged text}', 'tagged');
expect(result.tags[0]).toBeObject(); inline.replaceInlineTag('braceless text', 'foo', replacer);
expect(result.tags[0].tag).toBe('tagged');
expect(result.tags[0].text).toBe('text'); expect(replacer).not.toHaveBeenCalled();
expect(result.newString).toBe('some');
});
}); });
it('copes with bad escapement at the end of the string', () => {
const replacer = jasmine.createSpy('replacer');
inline.replaceInlineTag('bad {@foo escapement \\', 'foo', replacer);
expect(replacer).not.toHaveBeenCalled();
});
it('works if the tag is the entire string', () => {
function replacer(string, { completeTag, text }) {
expect(string).toBe('{@foo text in braces}');
expect(completeTag).toBe('{@foo text in braces}');
expect(text).toBe('text in braces');
return completeTag;
}
const result = inline.replaceInlineTag('{@foo text in braces}', 'foo', replacer);
expect(result.tags[0]).toBeObject();
expect(result.tags[0].tag).toBe('foo');
expect(result.tags[0].text).toBe('text in braces');
expect(result.newString).toBe('{@foo text in braces}');
});
it('works if the tag is at the beginning of the string', () => {
function replacer(string, { completeTag, text }) {
expect(string).toBe('{@foo test string} ahoy');
expect(completeTag).toBe('{@foo test string}');
expect(text).toBe('test string');
return string;
}
const result = inline.replaceInlineTag('{@foo test string} ahoy', 'foo', replacer);
expect(result.tags[0]).toBeObject();
expect(result.tags[0].tag).toBe('foo');
expect(result.tags[0].text).toBe('test string');
expect(result.newString).toBe('{@foo test string} ahoy');
});
it('works if the tag is in the middle of the string', () => {
function replacer(string, { completeTag, text }) {
expect(string).toBe('a {@foo test string} yay');
expect(completeTag).toBe('{@foo test string}');
expect(text).toBe('test string');
return string;
}
const result = inline.replaceInlineTag('a {@foo test string} yay', 'foo', replacer);
expect(result.tags[0]).toBeObject();
expect(result.tags[0].tag).toBe('foo');
expect(result.tags[0].text).toBe('test string');
expect(result.newString).toBe('a {@foo test string} yay');
});
it('works if the tag is at the end of the string', () => {
function replacer(string, { completeTag, text }) {
expect(string).toBe('a {@foo test string}');
expect(completeTag).toBe('{@foo test string}');
expect(text).toBe('test string');
return string;
}
const result = inline.replaceInlineTag('a {@foo test string}', 'foo', replacer);
expect(result.tags[0]).toBeObject();
expect(result.tags[0].tag).toBe('foo');
expect(result.tags[0].text).toBe('test string');
expect(result.newString).toBe('a {@foo test string}');
});
it('replaces the string with the specified value', () => {
function replacer() {
return 'REPLACED!';
}
const result = inline.replaceInlineTag('a {@foo test string}', 'foo', replacer);
expect(result.newString).toBe('REPLACED!');
});
it('processes all occurrences of a tag', () => {
function replacer(string, { completeTag }) {
return string.replace(completeTag, 'stuff');
}
const result = inline.replaceInlineTag(
'some {@foo text} with multiple ' + '{@foo tags}, {@foo like} {@foo this}',
'foo',
replacer
);
expect(result.tags.length).toBe(4);
expect(result.tags[0]).toBeObject();
expect(result.tags[0].tag).toBe('foo');
expect(result.tags[0].text).toBe('text');
expect(result.tags[1]).toBeObject();
expect(result.tags[1].tag).toBe('foo');
expect(result.tags[1].text).toBe('tags');
expect(result.tags[2]).toBeObject();
expect(result.tags[2].tag).toBe('foo');
expect(result.tags[2].text).toBe('like');
expect(result.tags[3]).toBeObject();
expect(result.tags[3].tag).toBe('foo');
expect(result.tags[3].text).toBe('this');
expect(result.newString).toBe('some stuff with multiple stuff, stuff stuff');
});
});
// Largely covered by the `replaceInlineTag()` tests.
describe('replaceInlineTags', () => {
it('works with an empty replacer object', () => {
const replacers = {};
const text = 'some {@foo text} to parse';
const result = inline.replaceInlineTags(text, replacers);
expect(result.newString).toBe(text);
});
it('works with one replacer', () => {
const text = 'some {@foo text} with {@bar multiple} tags';
const replacers = {
foo(string, tagInfo) {
expect(tagInfo.completeTag).toBe('{@foo text}');
expect(tagInfo.text).toBe('text');
return string.replace(tagInfo.completeTag, 'stuff');
},
};
const result = inline.replaceInlineTags(text, replacers);
expect(result.newString).toBe('some stuff with {@bar multiple} tags');
});
it('works with multiple replacers', () => {
const text = 'some {@foo text} with {@bar multiple} tags';
const replacers = {
foo(string, tagInfo) {
expect(tagInfo.completeTag).toBe('{@foo text}');
expect(tagInfo.text).toBe('text');
return string.replace(tagInfo.completeTag, 'stuff');
},
bar(string, tagInfo) {
expect(tagInfo.completeTag).toBe('{@bar multiple}');
expect(tagInfo.text).toBe('multiple');
return string.replace(tagInfo.completeTag, 'awesome');
},
};
const result = inline.replaceInlineTags(text, replacers);
expect(result.newString).toBe('some stuff with awesome tags');
});
});
// Largely covered by the `replaceInlineTag()` tests.
describe('extractInlineTag', () => {
it('works when a tag is specified', () => {
const result = inline.extractInlineTag('some {@tagged text}', 'tagged');
expect(result.tags[0]).toBeObject();
expect(result.tags[0].tag).toBe('tagged');
expect(result.tags[0].text).toBe('text');
expect(result.newString).toBe('some');
});
});
}); });

View File

@ -1,266 +1,266 @@
function buildText(type, name, desc) { function buildText(type, name, desc) {
let text = ''; let text = '';
if (type) { if (type) {
text += `{${type}}`; text += `{${type}}`;
if (name || desc) { if (name || desc) {
text += ' '; text += ' ';
}
}
if (name) {
text += name;
if (desc) {
text += ' ';
}
} }
}
if (name) {
text += name;
if (desc) { if (desc) {
text += desc; text += ' ';
} }
}
return text; if (desc) {
text += desc;
}
return text;
} }
describe('@jsdoc/tag/lib/type', () => { describe('@jsdoc/tag/lib/type', () => {
const type = require('../../../lib/type'); const type = require('../../../lib/type');
it('is an object', () => { it('is an object', () => {
expect(type).toBeObject(); expect(type).toBeObject();
});
it('exports a parse function', () => {
expect(type.parse).toBeFunction();
});
describe('parse', () => {
it('returns an object with name, type, and text properties', () => {
const info = type.parse('');
expect(info.name).toBeString();
expect(info.type).toBeArray();
expect(info.text).toBeString();
}); });
it('exports a parse function', () => { it('does not extract a name or type if canHaveName and canHaveType are not set', () => {
expect(type.parse).toBeFunction(); const desc = '{number} foo The foo parameter.';
const info = type.parse(desc);
expect(info.type).toBeEmptyArray();
expect(info.name).toBe('');
expect(info.text).toBe(desc);
}); });
describe('parse', () => { it('extracts a name, but not a type, if canHaveName is true and canHaveType is false', () => {
it('returns an object with name, type, and text properties', () => { const name = 'bar';
const info = type.parse(''); const desc = 'The bar parameter.';
const info = type.parse(buildText(null, name, desc), true, false);
expect(info.name).toBeString(); expect(info.type).toBeEmptyArray();
expect(info.type).toBeArray(); expect(info.name).toBe(name);
expect(info.text).toBeString(); expect(info.text).toBe(desc);
});
it('does not extract a name or type if canHaveName and canHaveType are not set', () => {
const desc = '{number} foo The foo parameter.';
const info = type.parse(desc);
expect(info.type).toBeEmptyArray();
expect(info.name).toBe('');
expect(info.text).toBe(desc);
});
it('extracts a name, but not a type, if canHaveName is true and canHaveType is false', () => {
const name = 'bar';
const desc = 'The bar parameter.';
const info = type.parse( buildText(null, name, desc), true, false );
expect(info.type).toBeEmptyArray();
expect(info.name).toBe(name);
expect(info.text).toBe(desc);
});
it('extracts a type, but not a name, if canHaveName is false and canHaveType is true', () => {
const typeString = 'boolean';
const desc = 'Set to true on alternate Thursdays.';
const info = type.parse(buildText(typeString, null, desc), false, true);
expect(info.type).toEqual([typeString]);
expect(info.name).toBe('');
expect(info.text).toBe(desc);
});
it('extracts a name and type if canHaveName and canHaveType are true', () => {
const typeString = 'string';
const name = 'baz';
const desc = 'The baz parameter.';
const info = type.parse(buildText(typeString, name, desc), true, true);
expect(info.type).toEqual([typeString]);
expect(info.name).toBe(name);
expect(info.text).toBe(desc);
});
it('reports optional types correctly for both JSDoc and Closure syntax', () => {
let desc = '{string} [foo]';
let info = type.parse(desc, true, true);
expect(info.optional).toBeTrue();
desc = '{string=} [foo]';
info = type.parse(desc, true, true);
expect(info.optional).toBeTrue();
desc = '[foo]';
info = type.parse(desc, true, true);
expect(info.optional).toBeTrue();
});
it('returnsthe types as an array', () => {
const desc = '{string} foo';
const info = type.parse(desc, true, true);
expect(info.type).toEqual( ['string'] );
});
it('recognizes the entire list of possible types', () => {
let desc = '{(string|number)} foo';
let info = type.parse(desc, true, true);
expect(info.type).toEqual( ['string', 'number'] );
desc = '{ ( string | number ) } foo';
info = type.parse(desc, true, true);
expect(info.type).toEqual( ['string', 'number'] );
desc = '{ ( string | number)} foo';
info = type.parse(desc, true, true);
expect(info.type).toEqual( ['string', 'number'] );
desc = '{(string|number|boolean|function)} foo';
info = type.parse(desc, true, true);
expect(info.type).toEqual( ['string', 'number', 'boolean', 'function'] );
});
it('does not find any type if there is no text in braces', () => {
const desc = 'braceless text';
const info = type.parse(desc, false, true);
expect(info.type).toBeEmptyArray();
});
it('copes with bad escapement at the end of the string', () => {
const desc = 'bad {escapement \\';
const info = type.parse(desc, false, true);
expect(info.type).toBeEmptyArray();
expect(info.text).toBe(desc);
});
it('handles escaped braces correctly', () => {
const desc = '{weirdObject."with\\}AnnoyingProperty"}';
const info = type.parse(desc, false, true);
expect(info.type[0]).toBe('weirdObject."with}AnnoyingProperty"');
});
it('works if the type expression is the entire string', () => {
const desc = '{textInBraces}';
const info = type.parse(desc, false, true);
expect(info.type[0]).toBe('textInBraces');
});
it('works if the type expression is at the beginning of the string', () => {
const desc = '{testString} ahoy';
const info = type.parse(desc, false, true);
expect(info.type[0]).toBe('testString');
expect(info.text).toBe('ahoy');
});
it('works if the type expression is in the middle of the string', () => {
const desc = 'a {testString} yay';
const info = type.parse(desc, false, true);
expect(info.type[0]).toBe('testString');
expect(info.text).toBe('a yay');
});
it('works if the tag is at the end of the string', () => {
const desc = 'a {testString}';
const info = type.parse(desc, false, true);
expect(info.type[0]).toBe('testString');
expect(info.text).toBe('a');
});
it('works when there are nested braces', () => {
const desc = 'some {{double}} braces';
const info = type.parse(desc, false, true);
// we currently stringify all record types as 'Object'
expect(info.type[0]).toBe('Object');
expect(info.text).toBe('some braces');
});
it('overrides the type expression if an inline @type tag is specified', () => {
let desc = '{Object} cookie {@type Monster}';
let info = type.parse(desc, true, true);
expect(info.type).toEqual( ['Monster'] );
expect(info.text).toBe('');
desc = '{Object} cookie - {@type Monster}';
info = type.parse(desc, true, true);
expect(info.type).toEqual( ['Monster'] );
expect(info.text).toBe('');
desc = '{Object} cookie - The cookie parameter. {@type Monster}';
info = type.parse(desc, true, true);
expect(info.type).toEqual( ['Monster'] );
expect(info.text).toBe('The cookie parameter.');
desc = '{Object} cookie - The cookie parameter. {@type (Monster|Jar)}';
info = type.parse(desc, true, true);
expect(info.type).toEqual( ['Monster', 'Jar'] );
expect(info.text).toBe('The cookie parameter.');
desc = '{Object} cookie - The cookie parameter. {@type (Monster|Jar)} Mmm, cookie.';
info = type.parse(desc, true, true);
expect(info.type).toEqual( ['Monster', 'Jar'] );
expect(info.text).toBe('The cookie parameter. Mmm, cookie.');
});
describe('JSDoc-style type info', () => {
it('parses JSDoc-style optional parameters', () => {
let name = '[qux]';
const desc = 'The qux parameter.';
let info = type.parse( buildText(null, name, desc), true, false );
expect(info.name).toBe('qux');
expect(info.text).toBe(desc);
expect(info.optional).toBeTrue();
name = '[ qux ]';
info = type.parse( buildText(null, name, desc), true, false );
expect(info.name).toBe('qux');
expect(info.text).toBe(desc);
expect(info.optional).toBeTrue();
name = '[qux=hooray]';
info = type.parse( buildText(null, name, desc), true, false );
expect(info.name).toBe('qux');
expect(info.text).toBe(desc);
expect(info.optional).toBeTrue();
expect(info.defaultvalue).toBe('hooray');
name = '[ qux = hooray ]';
info = type.parse( buildText(null, name, desc), true, false );
expect(info.name).toBe('qux');
expect(info.text).toBe(desc);
expect(info.optional).toBeTrue();
expect(info.defaultvalue).toBe('hooray');
});
});
// TODO: Add more tests related to how JSDoc mangles the Catharsis parse results.
describe('Closure Compiler-style type info', () => {
it('recognizes variable (repeatable) parameters', () => {
const desc = '{...string} foo - Foo.';
const info = type.parse(desc, true, true);
expect(info.type).toEqual( ['string'] );
expect(info.variable).toBeTrue();
});
it('sets the type correctly for type applications that contain type unions', () => {
const desc = '{Array.<(string|number)>} foo - Foo.';
const info = type.parse(desc, true, true);
expect(info.type).toEqual(['Array.<(string|number)>']);
});
});
}); });
it('extracts a type, but not a name, if canHaveName is false and canHaveType is true', () => {
const typeString = 'boolean';
const desc = 'Set to true on alternate Thursdays.';
const info = type.parse(buildText(typeString, null, desc), false, true);
expect(info.type).toEqual([typeString]);
expect(info.name).toBe('');
expect(info.text).toBe(desc);
});
it('extracts a name and type if canHaveName and canHaveType are true', () => {
const typeString = 'string';
const name = 'baz';
const desc = 'The baz parameter.';
const info = type.parse(buildText(typeString, name, desc), true, true);
expect(info.type).toEqual([typeString]);
expect(info.name).toBe(name);
expect(info.text).toBe(desc);
});
it('reports optional types correctly for both JSDoc and Closure syntax', () => {
let desc = '{string} [foo]';
let info = type.parse(desc, true, true);
expect(info.optional).toBeTrue();
desc = '{string=} [foo]';
info = type.parse(desc, true, true);
expect(info.optional).toBeTrue();
desc = '[foo]';
info = type.parse(desc, true, true);
expect(info.optional).toBeTrue();
});
it('returnsthe types as an array', () => {
const desc = '{string} foo';
const info = type.parse(desc, true, true);
expect(info.type).toEqual(['string']);
});
it('recognizes the entire list of possible types', () => {
let desc = '{(string|number)} foo';
let info = type.parse(desc, true, true);
expect(info.type).toEqual(['string', 'number']);
desc = '{ ( string | number ) } foo';
info = type.parse(desc, true, true);
expect(info.type).toEqual(['string', 'number']);
desc = '{ ( string | number)} foo';
info = type.parse(desc, true, true);
expect(info.type).toEqual(['string', 'number']);
desc = '{(string|number|boolean|function)} foo';
info = type.parse(desc, true, true);
expect(info.type).toEqual(['string', 'number', 'boolean', 'function']);
});
it('does not find any type if there is no text in braces', () => {
const desc = 'braceless text';
const info = type.parse(desc, false, true);
expect(info.type).toBeEmptyArray();
});
it('copes with bad escapement at the end of the string', () => {
const desc = 'bad {escapement \\';
const info = type.parse(desc, false, true);
expect(info.type).toBeEmptyArray();
expect(info.text).toBe(desc);
});
it('handles escaped braces correctly', () => {
const desc = '{weirdObject."with\\}AnnoyingProperty"}';
const info = type.parse(desc, false, true);
expect(info.type[0]).toBe('weirdObject."with}AnnoyingProperty"');
});
it('works if the type expression is the entire string', () => {
const desc = '{textInBraces}';
const info = type.parse(desc, false, true);
expect(info.type[0]).toBe('textInBraces');
});
it('works if the type expression is at the beginning of the string', () => {
const desc = '{testString} ahoy';
const info = type.parse(desc, false, true);
expect(info.type[0]).toBe('testString');
expect(info.text).toBe('ahoy');
});
it('works if the type expression is in the middle of the string', () => {
const desc = 'a {testString} yay';
const info = type.parse(desc, false, true);
expect(info.type[0]).toBe('testString');
expect(info.text).toBe('a yay');
});
it('works if the tag is at the end of the string', () => {
const desc = 'a {testString}';
const info = type.parse(desc, false, true);
expect(info.type[0]).toBe('testString');
expect(info.text).toBe('a');
});
it('works when there are nested braces', () => {
const desc = 'some {{double}} braces';
const info = type.parse(desc, false, true);
// we currently stringify all record types as 'Object'
expect(info.type[0]).toBe('Object');
expect(info.text).toBe('some braces');
});
it('overrides the type expression if an inline @type tag is specified', () => {
let desc = '{Object} cookie {@type Monster}';
let info = type.parse(desc, true, true);
expect(info.type).toEqual(['Monster']);
expect(info.text).toBe('');
desc = '{Object} cookie - {@type Monster}';
info = type.parse(desc, true, true);
expect(info.type).toEqual(['Monster']);
expect(info.text).toBe('');
desc = '{Object} cookie - The cookie parameter. {@type Monster}';
info = type.parse(desc, true, true);
expect(info.type).toEqual(['Monster']);
expect(info.text).toBe('The cookie parameter.');
desc = '{Object} cookie - The cookie parameter. {@type (Monster|Jar)}';
info = type.parse(desc, true, true);
expect(info.type).toEqual(['Monster', 'Jar']);
expect(info.text).toBe('The cookie parameter.');
desc = '{Object} cookie - The cookie parameter. {@type (Monster|Jar)} Mmm, cookie.';
info = type.parse(desc, true, true);
expect(info.type).toEqual(['Monster', 'Jar']);
expect(info.text).toBe('The cookie parameter. Mmm, cookie.');
});
describe('JSDoc-style type info', () => {
it('parses JSDoc-style optional parameters', () => {
let name = '[qux]';
const desc = 'The qux parameter.';
let info = type.parse(buildText(null, name, desc), true, false);
expect(info.name).toBe('qux');
expect(info.text).toBe(desc);
expect(info.optional).toBeTrue();
name = '[ qux ]';
info = type.parse(buildText(null, name, desc), true, false);
expect(info.name).toBe('qux');
expect(info.text).toBe(desc);
expect(info.optional).toBeTrue();
name = '[qux=hooray]';
info = type.parse(buildText(null, name, desc), true, false);
expect(info.name).toBe('qux');
expect(info.text).toBe(desc);
expect(info.optional).toBeTrue();
expect(info.defaultvalue).toBe('hooray');
name = '[ qux = hooray ]';
info = type.parse(buildText(null, name, desc), true, false);
expect(info.name).toBe('qux');
expect(info.text).toBe(desc);
expect(info.optional).toBeTrue();
expect(info.defaultvalue).toBe('hooray');
});
});
// TODO: Add more tests related to how JSDoc mangles the Catharsis parse results.
describe('Closure Compiler-style type info', () => {
it('recognizes variable (repeatable) parameters', () => {
const desc = '{...string} foo - Foo.';
const info = type.parse(desc, true, true);
expect(info.type).toEqual(['string']);
expect(info.variable).toBeTrue();
});
it('sets the type correctly for type applications that contain type unions', () => {
const desc = '{Array.<(string|number)>} foo - Foo.';
const info = type.parse(desc, true, true);
expect(info.type).toEqual(['Array.<(string|number)>']);
});
});
});
}); });

View File

@ -2,6 +2,6 @@ const Task = require('./lib/task');
const TaskRunner = require('./lib/task-runner'); const TaskRunner = require('./lib/task-runner');
module.exports = { module.exports = {
Task, Task,
TaskRunner TaskRunner,
}; };

View File

@ -1,7 +1,7 @@
const _ = require('lodash'); const _ = require('lodash');
const { DepGraph } = require('dependency-graph'); const { DepGraph } = require('dependency-graph');
const Emittery = require('emittery'); const Emittery = require('emittery');
const {default: ow} = require('ow'); const { default: ow } = require('ow');
const Queue = require('p-queue').default; const Queue = require('p-queue').default;
const v = require('./validators'); const v = require('./validators');
@ -9,319 +9,320 @@ const v = require('./validators');
DepGraph.prototype.dependentsOf = DepGraph.prototype.dependantsOf; DepGraph.prototype.dependentsOf = DepGraph.prototype.dependantsOf;
module.exports = class TaskRunner extends Emittery { module.exports = class TaskRunner extends Emittery {
constructor(context) { constructor(context) {
super(); super();
ow(context, ow.optional.object); ow(context, ow.optional.object);
this._init(context); this._init(context);
} }
_addOrRemoveTasks(tasks, func, action) { _addOrRemoveTasks(tasks, func, action) {
func = _.bind(func, this); func = _.bind(func, this);
if (Array.isArray(tasks)) { if (Array.isArray(tasks)) {
tasks.forEach((task, i) => { tasks.forEach((task, i) => {
try {
func(task);
} catch (e) {
e.message = `Can't ${action} task ${i}: ${e.message}`;
throw e;
}
});
} else if (tasks !== null && typeof tasks === 'object') {
for (const task of Object.keys(tasks)) {
try {
func(tasks[task]);
} catch (e) {
e.message = `Can't ${action} task "${task}": ${e.message}`;
throw e;
}
}
}
}
_addTaskEmitters(task) {
const u = {};
u.start = task.on('start', t => this.emit('taskStart', t));
u.end = task.on('end', t => this.emit('taskEnd', t));
u.error = task.on('error', (e => {
this.emit('taskError', {
task: e.task,
error: e.error
});
if (!this._error) {
this._error = e.error;
}
}));
this._unsubscribers.set(task.name, u);
}
_bindTaskFunc(task) {
return _.bind(task.run, task, this._context);
}
_createTaskSequence(tasks) {
if (!tasks.length) {
return null;
}
return () => tasks.reduce((p, taskName) => {
const task = this._nameToTask.get(taskName);
return p.then(
this._bindTaskFunc(task),
e => Promise.reject(e)
);
}, Promise.resolve());
}
_init(context) {
this._context = context;
this._deps = new Map();
this._error = null;
this._queue = new Queue();
this._taskToName = new WeakMap();
this._nameToTask = new Map();
this._running = false;
this._unsubscribers = new Map();
this._queue.pause();
}
_newDependencyCycleError(cyclePath) {
return new v.DependencyCycleError(
`Tasks have circular dependencies: ${cyclePath.join(' > ')}`,
cyclePath
);
}
_newStateError() {
return new v.StateError('The task runner is already running.');
}
_newUnknownDepsError(dependent, unknownDeps) {
let errorText;
if (unknownDeps.length === 1) {
errorText = 'an unknown task';
} else {
errorText = 'unknown tasks';
}
return new v.UnknownDependencyError(`The task ${dependent} depends on ${errorText}: ` +
`${unknownDeps.join(', ')}`);
}
_orderTasks() {
let error;
const graph = new DepGraph();
let parallel;
let sequential;
for (const [task] of this._nameToTask) {
graph.addNode(task);
}
for (const [dependent] of this._deps) {
const unknownDeps = [];
for (const dependency of this._deps.get(dependent)) {
if (!this._nameToTask.has(dependency)) {
unknownDeps.push(dependency);
} else {
graph.addDependency(dependent, dependency);
}
if (unknownDeps.length) {
error = this._newUnknownDepsError(dependency, unknownDeps);
break;
}
}
}
if (!error) {
try {
// Get standalone tasks with no dependencies and no dependents.
parallel = graph.overallOrder(true)
.filter(task => !(graph.dependentsOf(task).length));
// Get tasks with dependencies, in a correctly ordered list.
sequential = graph.overallOrder().filter(task => !parallel.includes(task));
} catch (e) {
error = this._newDependencyCycleError(e.cyclePath);
}
}
return {
error,
parallel,
sequential
};
}
_rejectIfRunning() {
if (this.running) {
return Promise.reject(this._newStateError());
}
return null;
}
_throwIfRunning() {
if (this.running) {
throw this._newStateError();
}
}
_throwIfUnknownDeps(dependent, unknownDeps) {
if (!unknownDeps.length) {
return;
}
throw this._newUnknownDepsError(dependent, unknownDeps);
}
addTask(task) {
ow(task, v.checkTaskOrString);
this._throwIfRunning();
this._nameToTask.set(task.name, task);
if (task.dependsOn) {
this._deps.set(task.name, task.dependsOn);
}
this._taskToName.set(task, task.name);
this._addTaskEmitters(task);
return this;
}
addTasks(tasks) {
ow(tasks, ow.any(ow.array, ow.object));
this._addOrRemoveTasks(tasks, this.addTask, 'add');
return this;
}
end() {
this.emit('end', {
error: this._error
});
this._queue.clear();
this._init();
}
removeTask(task) {
let unsubscribers;
ow(task, v.checkTaskOrString);
this._throwIfRunning();
if (typeof task === 'string') {
task = this._nameToTask.get(task);
if (!task) {
throw new v.UnknownTaskError(`Unknown task: ${task}`);
}
} else if (typeof task === 'object') {
if (!this._taskToName.has(task)) {
throw new v.UnknownTaskError(`Unknown task: ${task}`);
}
}
this._nameToTask.delete(task.name);
this._taskToName.delete(task);
this._deps.delete(task.name);
unsubscribers = this._unsubscribers.get(task.name);
for (const u of Object.keys(unsubscribers)) {
unsubscribers[u]();
}
this._unsubscribers.delete(task.name);
return this;
}
removeTasks(tasks) {
ow(tasks, ow.any(ow.array, ow.object));
this._addOrRemoveTasks(tasks, this.removeTask, 'remove');
return this;
}
run(context) {
ow(context, ow.optional.object);
let endPromise;
const { error, parallel, sequential } = this._orderTasks();
let runningPromise;
let taskFuncs = [];
let taskSequence;
// First, fail if the runner is already running.
runningPromise = this._rejectIfRunning();
if (runningPromise) {
return runningPromise;
}
// Then fail if the tasks couldn't be ordered.
if (error) {
return Promise.reject(error);
}
this._context = context || this._context;
for (const taskName of parallel) {
taskFuncs.push(this._bindTaskFunc(this._nameToTask.get(taskName)));
}
taskSequence = this._createTaskSequence(sequential);
if (taskSequence) {
taskFuncs.push(taskSequence);
}
endPromise = this._queue.addAll(taskFuncs).then(() => {
this.end();
if (this._error) {
return Promise.reject(this._error);
} else {
return Promise.resolve();
}
}, e => {
this.end();
return Promise.reject(e);
});
this.emit('start');
this._running = true;
try { try {
this._queue.start(); func(task);
return endPromise;
} catch (e) { } catch (e) {
this._error = e; e.message = `Can't ${action} task ${i}: ${e.message}`;
this.end(); throw e;
return Promise.reject(e);
} }
});
} else if (tasks !== null && typeof tasks === 'object') {
for (const task of Object.keys(tasks)) {
try {
func(tasks[task]);
} catch (e) {
e.message = `Can't ${action} task "${task}": ${e.message}`;
throw e;
}
}
}
}
_addTaskEmitters(task) {
const u = {};
u.start = task.on('start', (t) => this.emit('taskStart', t));
u.end = task.on('end', (t) => this.emit('taskEnd', t));
u.error = task.on('error', (e) => {
this.emit('taskError', {
task: e.task,
error: e.error,
});
if (!this._error) {
this._error = e.error;
}
});
this._unsubscribers.set(task.name, u);
}
_bindTaskFunc(task) {
return _.bind(task.run, task, this._context);
}
_createTaskSequence(tasks) {
if (!tasks.length) {
return null;
} }
get running() { return () =>
return this._running; tasks.reduce((p, taskName) => {
const task = this._nameToTask.get(taskName);
return p.then(this._bindTaskFunc(task), (e) => Promise.reject(e));
}, Promise.resolve());
}
_init(context) {
this._context = context;
this._deps = new Map();
this._error = null;
this._queue = new Queue();
this._taskToName = new WeakMap();
this._nameToTask = new Map();
this._running = false;
this._unsubscribers = new Map();
this._queue.pause();
}
_newDependencyCycleError(cyclePath) {
return new v.DependencyCycleError(
`Tasks have circular dependencies: ${cyclePath.join(' > ')}`,
cyclePath
);
}
_newStateError() {
return new v.StateError('The task runner is already running.');
}
_newUnknownDepsError(dependent, unknownDeps) {
let errorText;
if (unknownDeps.length === 1) {
errorText = 'an unknown task';
} else {
errorText = 'unknown tasks';
} }
get tasks() { return new v.UnknownDependencyError(
const entries = []; `The task ${dependent} depends on ${errorText}: ` + `${unknownDeps.join(', ')}`
);
}
for (const entry of this._nameToTask.entries()) { _orderTasks() {
entries.push(entry); let error;
const graph = new DepGraph();
let parallel;
let sequential;
for (const [task] of this._nameToTask) {
graph.addNode(task);
}
for (const [dependent] of this._deps) {
const unknownDeps = [];
for (const dependency of this._deps.get(dependent)) {
if (!this._nameToTask.has(dependency)) {
unknownDeps.push(dependency);
} else {
graph.addDependency(dependent, dependency);
} }
return _.fromPairs(entries); if (unknownDeps.length) {
error = this._newUnknownDepsError(dependency, unknownDeps);
break;
}
}
} }
if (!error) {
try {
// Get standalone tasks with no dependencies and no dependents.
parallel = graph.overallOrder(true).filter((task) => !graph.dependentsOf(task).length);
// Get tasks with dependencies, in a correctly ordered list.
sequential = graph.overallOrder().filter((task) => !parallel.includes(task));
} catch (e) {
error = this._newDependencyCycleError(e.cyclePath);
}
}
return {
error,
parallel,
sequential,
};
}
_rejectIfRunning() {
if (this.running) {
return Promise.reject(this._newStateError());
}
return null;
}
_throwIfRunning() {
if (this.running) {
throw this._newStateError();
}
}
_throwIfUnknownDeps(dependent, unknownDeps) {
if (!unknownDeps.length) {
return;
}
throw this._newUnknownDepsError(dependent, unknownDeps);
}
addTask(task) {
ow(task, v.checkTaskOrString);
this._throwIfRunning();
this._nameToTask.set(task.name, task);
if (task.dependsOn) {
this._deps.set(task.name, task.dependsOn);
}
this._taskToName.set(task, task.name);
this._addTaskEmitters(task);
return this;
}
addTasks(tasks) {
ow(tasks, ow.any(ow.array, ow.object));
this._addOrRemoveTasks(tasks, this.addTask, 'add');
return this;
}
end() {
this.emit('end', {
error: this._error,
});
this._queue.clear();
this._init();
}
removeTask(task) {
let unsubscribers;
ow(task, v.checkTaskOrString);
this._throwIfRunning();
if (typeof task === 'string') {
task = this._nameToTask.get(task);
if (!task) {
throw new v.UnknownTaskError(`Unknown task: ${task}`);
}
} else if (typeof task === 'object') {
if (!this._taskToName.has(task)) {
throw new v.UnknownTaskError(`Unknown task: ${task}`);
}
}
this._nameToTask.delete(task.name);
this._taskToName.delete(task);
this._deps.delete(task.name);
unsubscribers = this._unsubscribers.get(task.name);
for (const u of Object.keys(unsubscribers)) {
unsubscribers[u]();
}
this._unsubscribers.delete(task.name);
return this;
}
removeTasks(tasks) {
ow(tasks, ow.any(ow.array, ow.object));
this._addOrRemoveTasks(tasks, this.removeTask, 'remove');
return this;
}
run(context) {
ow(context, ow.optional.object);
let endPromise;
const { error, parallel, sequential } = this._orderTasks();
let runningPromise;
let taskFuncs = [];
let taskSequence;
// First, fail if the runner is already running.
runningPromise = this._rejectIfRunning();
if (runningPromise) {
return runningPromise;
}
// Then fail if the tasks couldn't be ordered.
if (error) {
return Promise.reject(error);
}
this._context = context || this._context;
for (const taskName of parallel) {
taskFuncs.push(this._bindTaskFunc(this._nameToTask.get(taskName)));
}
taskSequence = this._createTaskSequence(sequential);
if (taskSequence) {
taskFuncs.push(taskSequence);
}
endPromise = this._queue.addAll(taskFuncs).then(
() => {
this.end();
if (this._error) {
return Promise.reject(this._error);
} else {
return Promise.resolve();
}
},
(e) => {
this.end();
return Promise.reject(e);
}
);
this.emit('start');
this._running = true;
try {
this._queue.start();
return endPromise;
} catch (e) {
this._error = e;
this.end();
return Promise.reject(e);
}
}
get running() {
return this._running;
}
get tasks() {
const entries = [];
for (const entry of this._nameToTask.entries()) {
entries.push(entry);
}
return _.fromPairs(entries);
}
}; };

View File

@ -1,52 +1,49 @@
const Emittery = require('emittery'); const Emittery = require('emittery');
const {default: ow} = require('ow'); const { default: ow } = require('ow');
module.exports = class Task extends Emittery { module.exports = class Task extends Emittery {
constructor(opts = {}) { constructor(opts = {}) {
let deps; let deps;
super(); super();
ow(opts.name, ow.optional.string); ow(opts.name, ow.optional.string);
ow(opts.func, ow.optional.function); ow(opts.func, ow.optional.function);
ow(opts.dependsOn, ow.any( ow(opts.dependsOn, ow.any(ow.optional.string, ow.optional.array.ofType(ow.string)));
ow.optional.string,
ow.optional.array.ofType(ow.string)
));
if (typeof opts.dependsOn === 'string') { if (typeof opts.dependsOn === 'string') {
deps = [opts.dependsOn]; deps = [opts.dependsOn];
} else if (Array.isArray(opts.dependsOn)) { } else if (Array.isArray(opts.dependsOn)) {
deps = opts.dependsOn.slice(0); deps = opts.dependsOn.slice(0);
}
this.name = opts.name || null;
this.func = opts.func || null;
this.dependsOn = deps || [];
} }
run(context) { this.name = opts.name || null;
ow(this.name, ow.string); this.func = opts.func || null;
ow(this.func, ow.function); this.dependsOn = deps || [];
ow(this.dependsOn, ow.array.ofType(ow.string)); }
this.emit('start', this); run(context) {
ow(this.name, ow.string);
ow(this.func, ow.function);
ow(this.dependsOn, ow.array.ofType(ow.string));
return this.func(context).then( this.emit('start', this);
() => {
this.emit('end', this);
return Promise.resolve(); return this.func(context).then(
}, () => {
error => { this.emit('end', this);
this.emit('error', {
task: this,
error
});
this.emit('end', this);
return Promise.reject(error); return Promise.resolve();
} },
); (error) => {
} this.emit('error', {
task: this,
error,
});
this.emit('end', this);
return Promise.reject(error);
}
);
}
}; };

View File

@ -1,47 +1,47 @@
const {default: ow} = require('ow'); const { default: ow } = require('ow');
const Task = require('./task'); const Task = require('./task');
function checkTask(t) { function checkTask(t) {
return { return {
validator: t instanceof Task, validator: t instanceof Task,
message: `Expected ${t} to be a Task object` message: `Expected ${t} to be a Task object`,
}; };
} }
module.exports = { module.exports = {
checkTaskOrString: ow.any(ow.object.validate(checkTask), ow.string), checkTaskOrString: ow.any(ow.object.validate(checkTask), ow.string),
DependencyCycleError: class DependencyCycleError extends Error { DependencyCycleError: class DependencyCycleError extends Error {
constructor(message, cyclePath) { constructor(message, cyclePath) {
ow(message, ow.string); ow(message, ow.string);
ow(cyclePath, ow.array.ofType(ow.string)); ow(cyclePath, ow.array.ofType(ow.string));
super(message); super(message);
this.cyclePath = cyclePath; this.cyclePath = cyclePath;
this.name = 'DependencyCycleError'; this.name = 'DependencyCycleError';
}
},
StateError: class StateError extends Error {
constructor(message) {
ow(message, ow.string);
super(message);
this.name = 'StateError';
}
},
UnknownDependencyError: class UnknownDependencyError extends Error {
constructor(message) {
ow(message, ow.string);
super(message);
this.name = 'UnknownDependencyError';
}
},
UnknownTaskError: class UnknownTaskError extends Error {
constructor(message) {
ow(message, ow.string);
super(message);
this.name = 'UnknownTaskError';
}
} }
},
StateError: class StateError extends Error {
constructor(message) {
ow(message, ow.string);
super(message);
this.name = 'StateError';
}
},
UnknownDependencyError: class UnknownDependencyError extends Error {
constructor(message) {
ow(message, ow.string);
super(message);
this.name = 'UnknownDependencyError';
}
},
UnknownTaskError: class UnknownTaskError extends Error {
constructor(message) {
ow(message, ow.string);
super(message);
this.name = 'UnknownTaskError';
}
},
}; };

View File

@ -1,8 +1,166 @@
{ {
"name": "@jsdoc/task-runner", "name": "@jsdoc/task-runner",
"version": "0.1.10", "version": "0.1.10",
"lockfileVersion": 1, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": {
"": {
"name": "@jsdoc/task-runner",
"version": "0.1.10",
"license": "Apache-2.0",
"dependencies": {
"dependency-graph": "^0.11.0",
"emittery": "^0.10.0",
"ow": "^0.27.0",
"p-queue": "^6.6.2"
},
"engines": {
"node": ">=v14.17.6"
}
},
"node_modules/@sindresorhus/is": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz",
"integrity": "sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/dependency-graph": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz",
"integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/dot-prop": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
"integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
"dependencies": {
"is-obj": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/emittery": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.0.tgz",
"integrity": "sha512-AGvFfs+d0JKCJQ4o01ASQLGPmSCxgfU9RFXvzPvZdjKK8oscynksuJhWrSTSw7j7Ep/sZct5b5ZhYCi8S/t0HQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sindresorhus/emittery?sponsor=1"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"node_modules/is-obj": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
"engines": {
"node": ">=8"
}
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"node_modules/ow": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz",
"integrity": "sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ==",
"dependencies": {
"@sindresorhus/is": "^4.0.1",
"callsites": "^3.1.0",
"dot-prop": "^6.0.1",
"lodash.isequal": "^4.5.0",
"type-fest": "^1.2.1",
"vali-date": "^1.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
"engines": {
"node": ">=4"
}
},
"node_modules/p-queue": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
"dependencies": {
"eventemitter3": "^4.0.4",
"p-timeout": "^3.2.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"dependencies": {
"p-finally": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/type-fest": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.2.2.tgz",
"integrity": "sha512-pfkPYCcuV0TJoo/jlsUeWNV8rk7uMU6ocnYNvca1Vu+pyKi8Rl8Zo2scPt9O72gCsXIm+dMxOOWuA3VFDSdzWA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/vali-date": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
"integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=",
"engines": {
"node": ">=0.10.0"
}
}
},
"dependencies": { "dependencies": {
"@sindresorhus/is": { "@sindresorhus/is": {
"version": "4.0.1", "version": "4.0.1",

View File

@ -1,23 +1,23 @@
const taskRunner = require('../../index'); const taskRunner = require('../../index');
describe('@jsdoc/task-runner', () => { describe('@jsdoc/task-runner', () => {
it('is an object', () => { it('is an object', () => {
expect(taskRunner).toBeObject(); expect(taskRunner).toBeObject();
});
describe('Task', () => {
it('is lib/task', () => {
const Task = require('../../lib/task');
expect(taskRunner.Task).toBe(Task);
}); });
});
describe('Task', () => { describe('TaskRunner', () => {
it('is lib/task', () => { it('is lib/task-runner', () => {
const Task = require('../../lib/task'); const TaskRunner = require('../../lib/task-runner');
expect(taskRunner.Task).toBe(Task); expect(taskRunner.TaskRunner).toBe(TaskRunner);
});
});
describe('TaskRunner', () => {
it('is lib/task-runner', () => {
const TaskRunner = require('../../lib/task-runner');
expect(taskRunner.TaskRunner).toBe(TaskRunner);
});
}); });
});
}); });

File diff suppressed because it is too large Load Diff

View File

@ -4,201 +4,200 @@ const Task = require('../../../lib/task');
const ARGUMENT_ERROR = 'ArgumentError'; const ARGUMENT_ERROR = 'ArgumentError';
describe('@jsdoc/task-runner/lib/task', () => { describe('@jsdoc/task-runner/lib/task', () => {
it('is a function', () => { it('is a function', () => {
expect(Task).toBeFunction(); expect(Task).toBeFunction();
});
it('inherits from emittery', () => {
expect(new Task() instanceof Emittery).toBeTrue();
});
it('can be constructed with no arguments', () => {
function factory() {
return new Task();
}
expect(factory).not.toThrow();
});
it('uses the provided name', () => {
const task = new Task({ name: 'foo' });
expect(task.name).toBe('foo');
});
it('uses the provided function', () => {
const func = () => Promise.resolve();
const task = new Task({ func });
expect(task.func).toBe(func);
});
describe('dependsOn', () => {
it('accepts an array of task names as dependencies', () => {
const dependsOn = ['bar', 'baz'];
const task = new Task({
name: 'foo',
func: () => Promise.resolve(),
dependsOn,
});
expect(task.dependsOn).toEqual(dependsOn);
}); });
it('inherits from emittery', () => { it('accepts a single task name as a dependency', () => {
expect(new Task() instanceof Emittery).toBeTrue(); const task = new Task({
name: 'foo',
func: () => Promise.resolve(),
dependsOn: 'bar',
});
expect(task.dependsOn).toEqual(['bar']);
}); });
it('can be constructed with no arguments', () => { it('fails with non-string, non-array dependencies', () => {
function factory() { function factory() {
return new Task(); return new Task({
name: 'foo',
func: () => Promise.resolve(),
dependsOn: 7,
});
}
expect(factory).toThrowErrorOfType(ARGUMENT_ERROR);
});
it('fails with non-string arrays of dependencies', () => {
function factory() {
return new Task({
name: 'foo',
func: () => Promise.resolve(),
dependsOn: [7],
});
}
expect(factory).toThrowErrorOfType(ARGUMENT_ERROR);
});
});
it('uses the provided dependencies', () => {
const dependsOn = ['foo', 'bar'];
const task = new Task({ dependsOn });
expect(task.dependsOn).toEqual(dependsOn);
});
describe('run', () => {
it('requires a name', async () => {
let error;
async function start() {
const task = new Task({
func: () => Promise.resolve(),
});
await task.run();
}
try {
await start();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
});
it('requires a function', async () => {
let error;
async function run() {
const task = new Task({
name: 'foo',
});
await task.run();
}
try {
await run();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
});
it('accepts a context object', async () => {
const context = {};
const task = new Task({
name: 'foo',
func: (c) => {
c.foo = 'bar';
return Promise.resolve();
},
});
await task.run(context);
expect(context.foo).toBe('bar');
});
describe('events', () => {
it('emits a `start` event', async () => {
let event;
const task = new Task({
name: 'foo',
func: () => Promise.resolve(),
});
task.on('start', (e) => {
event = e;
});
await task.run();
expect(event).toBe(task);
});
it('emits an `end` event', async () => {
let event;
const task = new Task({
name: 'foo',
func: () => Promise.resolve(),
});
task.on('end', (e) => {
event = e;
});
await task.run();
expect(event).toBe(task);
});
it('emits an `error` event if necessary', async () => {
let error = new Error('oh no!');
let event;
const task = new Task({
name: 'foo',
func: () => Promise.reject(error),
});
task.on('error', (e) => {
event = e;
});
try {
await task.run();
} catch (e) {
// Expected behavior.
} }
expect(factory).not.toThrow(); expect(event.error).toBe(error);
}); expect(event.task).toBe(task);
});
it('uses the provided name', () => {
const task = new Task({ name: 'foo' });
expect(task.name).toBe('foo');
});
it('uses the provided function', () => {
const func = () => Promise.resolve();
const task = new Task({ func });
expect(task.func).toBe(func);
});
describe('dependsOn', () => {
it('accepts an array of task names as dependencies', () => {
const dependsOn = ['bar', 'baz'];
const task = new Task({
name: 'foo',
func: () => Promise.resolve(),
dependsOn
});
expect(task.dependsOn).toEqual(dependsOn);
});
it('accepts a single task name as a dependency', () => {
const task = new Task({
name: 'foo',
func: () => Promise.resolve(),
dependsOn: 'bar'
});
expect(task.dependsOn).toEqual(['bar']);
});
it('fails with non-string, non-array dependencies', () => {
function factory() {
return new Task({
name: 'foo',
func: () => Promise.resolve(),
dependsOn: 7
});
}
expect(factory).toThrowErrorOfType(ARGUMENT_ERROR);
});
it('fails with non-string arrays of dependencies', () => {
function factory() {
return new Task({
name: 'foo',
func: () => Promise.resolve(),
dependsOn: [7]
});
}
expect(factory).toThrowErrorOfType(ARGUMENT_ERROR);
});
});
it('uses the provided dependencies', () => {
const dependsOn = ['foo', 'bar'];
const task = new Task({ dependsOn });
expect(task.dependsOn).toEqual(dependsOn);
});
describe('run', () => {
it('requires a name', async () => {
let error;
async function start() {
const task = new Task({
func: () => Promise.resolve()
});
await task.run();
}
try {
await start();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
});
it('requires a function', async () => {
let error;
async function run() {
const task = new Task({
name: 'foo'
});
await task.run();
}
try {
await run();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
});
it('accepts a context object', async () => {
const context = {};
const task = new Task({
name: 'foo',
func: c => {
c.foo = 'bar';
return Promise.resolve();
}
});
await task.run(context);
expect(context.foo).toBe('bar');
});
describe('events', () => {
it('emits a `start` event', async () => {
let event;
const task = new Task({
name: 'foo',
func: () => Promise.resolve()
});
task.on('start', e => {
event = e;
});
await task.run();
expect(event).toBe(task);
});
it('emits an `end` event', async () => {
let event;
const task = new Task({
name: 'foo',
func: () => Promise.resolve()
});
task.on('end', e => {
event = e;
});
await task.run();
expect(event).toBe(task);
});
it('emits an `error` event if necessary', async () => {
let error = new Error('oh no!');
let event;
const task = new Task({
name: 'foo',
func: () => Promise.reject(error)
});
task.on('error', e => {
event = e;
});
try {
await task.run();
} catch (e) {
// Expected behavior.
}
expect(event.error).toBe(error);
expect(event.task).toBe(task);
});
});
}); });
});
}); });

View File

@ -3,32 +3,32 @@ const { addMatchers } = require('add-matchers');
require('jasmine-expect'); require('jasmine-expect');
addMatchers({ addMatchers({
toBeError(value) { toBeError(value) {
return value instanceof Error; return value instanceof Error;
}, },
toBeErrorOfType(other, value) { toBeErrorOfType(other, value) {
return value instanceof Error && value.name === other; return value instanceof Error && value.name === other;
}, },
toBeInstanceOf(other, value) { toBeInstanceOf(other, value) {
let otherName; let otherName;
let valueName; let valueName;
if (typeof value !== 'object') { if (typeof value !== 'object') {
throw new TypeError(`Expected object value, got ${typeof value}`); throw new TypeError(`Expected object value, got ${typeof value}`);
}
valueName = value.constructor.name;
// Class name.
if (typeof other === 'string') {
otherName = other;
// Class constructor.
} else if (typeof other === 'function') {
otherName = other.name;
} else {
otherName = other.constructor.name;
}
return valueName === otherName;
} }
valueName = value.constructor.name;
// Class name.
if (typeof other === 'string') {
otherName = other;
// Class constructor.
} else if (typeof other === 'function') {
otherName = other.name;
} else {
otherName = other.constructor.name;
}
return valueName === otherName;
},
}); });

View File

@ -1,8 +1,35 @@
{ {
"name": "@jsdoc/test-matchers", "name": "@jsdoc/test-matchers",
"version": "0.1.6", "version": "0.1.6",
"lockfileVersion": 1, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": {
"": {
"name": "@jsdoc/test-matchers",
"version": "0.1.6",
"license": "Apache-2.0",
"dependencies": {
"add-matchers": "^0.6.2",
"jasmine-expect": "^5.0.0"
},
"engines": {
"node": ">=v14.17.6"
}
},
"node_modules/add-matchers": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/add-matchers/-/add-matchers-0.6.2.tgz",
"integrity": "sha512-hVO2wodMei9RF00qe+506MoeJ/NEOdCMEkSJ12+fC3hx/5Z4zmhNiP92nJEF6XhmXokeB0hOtuQrjHCx2vmXrQ=="
},
"node_modules/jasmine-expect": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/jasmine-expect/-/jasmine-expect-5.0.0.tgz",
"integrity": "sha512-byn1zq0EQBA9UKs5A+H6gk5TRcanV+TqQMRxrjurGuqKkclaqgjw/vV6aT/jtf5tabXGonTH6VDZJ33Z1pxSxw==",
"dependencies": {
"add-matchers": "0.6.2"
}
}
},
"dependencies": { "dependencies": {
"add-matchers": { "add-matchers": {
"version": "0.6.2", "version": "0.6.2",

View File

@ -10,8 +10,8 @@ const fs = require('./lib/fs');
const log = require('./lib/log'); const log = require('./lib/log');
module.exports = { module.exports = {
cast, cast,
EventBus, EventBus,
fs, fs,
log log,
}; };

View File

@ -1,6 +1,6 @@
const _ = require('lodash'); const _ = require('lodash');
const EventEmitter = require('events').EventEmitter; const EventEmitter = require('events').EventEmitter;
const {default: ow} = require('ow'); const { default: ow } = require('ow');
let cache = {}; let cache = {};
const hasOwnProp = Object.prototype.hasOwnProperty; const hasOwnProp = Object.prototype.hasOwnProperty;
@ -22,31 +22,31 @@ const hasOwnProp = Object.prototype.hasOwnProperty;
* @extends module:events.EventEmitter * @extends module:events.EventEmitter
*/ */
class EventBus extends EventEmitter { class EventBus extends EventEmitter {
/** /**
* Create a new event bus, or retrieve the cached event bus for the ID you specify. * Create a new event bus, or retrieve the cached event bus for the ID you specify.
* *
* @param {(string|Symbol)} id - The ID for the event bus. * @param {(string|Symbol)} id - The ID for the event bus.
* @param {Object} opts - Options for the event bus. * @param {Object} opts - Options for the event bus.
* @param {boolean} [opts.cache=true] - Set to `false` to prevent the event bus from being * @param {boolean} [opts.cache=true] - Set to `false` to prevent the event bus from being
* cached, and to return a new event bus even if there is already an event bus with the same ID. * cached, and to return a new event bus even if there is already an event bus with the same ID.
*/ */
constructor(id, opts = {}) { constructor(id, opts = {}) {
super(); super();
ow(id, ow.any(ow.string, ow.symbol)); ow(id, ow.any(ow.string, ow.symbol));
const shouldCache = _.isBoolean(opts.cache) ? opts.cache : true; const shouldCache = _.isBoolean(opts.cache) ? opts.cache : true;
if (hasOwnProp.call(cache, id) && shouldCache) { if (hasOwnProp.call(cache, id) && shouldCache) {
return cache[id]; return cache[id];
}
this._id = id;
if (shouldCache) {
cache[id] = this;
}
} }
this._id = id;
if (shouldCache) {
cache[id] = this;
}
}
} }
module.exports = EventBus; module.exports = EventBus;

View File

@ -13,49 +13,47 @@
* @return {(string|number|boolean)} The converted value. * @return {(string|number|boolean)} The converted value.
*/ */
function castString(str) { function castString(str) {
let number; let number;
let result; let result;
switch (str) { switch (str) {
case 'true': case 'true':
result = true; result = true;
break; break;
case 'false': case 'false':
result = false; result = false;
break; break;
case 'NaN': case 'NaN':
result = NaN; result = NaN;
break; break;
case 'null': case 'null':
result = null; result = null;
break; break;
case 'undefined': case 'undefined':
result = undefined; result = undefined;
break; break;
default: default:
if (typeof str === 'string') { if (typeof str === 'string') {
if (str.includes('.')) { if (str.includes('.')) {
number = parseFloat(str); number = parseFloat(str);
} } else {
else { number = parseInt(str, 10);
number = parseInt(str, 10); }
}
if (String(number) === str && !isNaN(number)) { if (String(number) === str && !isNaN(number)) {
result = number; result = number;
} } else {
else { result = str;
result = str; }
} }
} }
}
return result; return result;
} }
/** /**
@ -69,27 +67,24 @@ function castString(str) {
* @param {(string|Object|Array)} item - The item whose type or types will be converted. * @param {(string|Object|Array)} item - The item whose type or types will be converted.
* @return {*?} The converted value. * @return {*?} The converted value.
*/ */
const cast = module.exports = item => { const cast = (module.exports = (item) => {
let result; let result;
if (Array.isArray(item)) { if (Array.isArray(item)) {
result = []; result = [];
for (let i = 0, l = item.length; i < l; i++) { for (let i = 0, l = item.length; i < l; i++) {
result[i] = cast(item[i]); result[i] = cast(item[i]);
}
}
else if (typeof item === 'object' && item !== null) {
result = {};
Object.keys(item).forEach(prop => {
result[prop] = cast(item[prop]);
});
}
else if (typeof item === 'string') {
result = castString(item);
}
else {
result = item;
} }
} else if (typeof item === 'object' && item !== null) {
result = {};
Object.keys(item).forEach((prop) => {
result[prop] = cast(item[prop]);
});
} else if (typeof item === 'string') {
result = castString(item);
} else {
result = item;
}
return result; return result;
}; });

View File

@ -6,14 +6,14 @@ const _ = require('lodash');
const klawSync = require('klaw-sync'); const klawSync = require('klaw-sync');
const path = require('path'); const path = require('path');
exports.lsSync = ((dir, opts = {}) => { exports.lsSync = (dir, opts = {}) => {
const depth = _.has(opts, 'depth') ? opts.depth : -1; const depth = _.has(opts, 'depth') ? opts.depth : -1;
const files = klawSync(dir, { const files = klawSync(dir, {
depthLimit: depth, depthLimit: depth,
filter: (f => !path.basename(f.path).startsWith('.')), filter: (f) => !path.basename(f.path).startsWith('.'),
nodir: true nodir: true,
}); });
return files.map(f => f.path); return files.map((f) => f.path);
}); };

View File

@ -3,8 +3,8 @@ const EventBus = require('./bus');
const bus = new EventBus('jsdoc'); const bus = new EventBus('jsdoc');
const loggerFuncs = {}; const loggerFuncs = {};
['debug', 'error', 'info', 'fatal', 'verbose', 'warn'].forEach(fn => { ['debug', 'error', 'info', 'fatal', 'verbose', 'warn'].forEach((fn) => {
loggerFuncs[fn] = (...args) => bus.emit(`logger:${fn}`, ...args); loggerFuncs[fn] = (...args) => bus.emit(`logger:${fn}`, ...args);
}); });
module.exports = loggerFuncs; module.exports = loggerFuncs;

View File

@ -1,8 +1,125 @@
{ {
"name": "@jsdoc/util", "name": "@jsdoc/util",
"version": "0.2.4", "version": "0.2.4",
"lockfileVersion": 1, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": {
"": {
"name": "@jsdoc/util",
"version": "0.2.4",
"license": "Apache-2.0",
"dependencies": {
"klaw-sync": "^6.0.0",
"lodash": "^4.17.21",
"ow": "^0.27.0"
},
"engines": {
"node": ">=v14.17.6"
}
},
"node_modules/@sindresorhus/is": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz",
"integrity": "sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/dot-prop": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
"integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
"dependencies": {
"is-obj": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/graceful-fs": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ=="
},
"node_modules/is-obj": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
"engines": {
"node": ">=8"
}
},
"node_modules/klaw-sync": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
"dependencies": {
"graceful-fs": "^4.1.11"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"node_modules/ow": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz",
"integrity": "sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ==",
"dependencies": {
"@sindresorhus/is": "^4.0.1",
"callsites": "^3.1.0",
"dot-prop": "^6.0.1",
"lodash.isequal": "^4.5.0",
"type-fest": "^1.2.1",
"vali-date": "^1.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/type-fest": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.2.2.tgz",
"integrity": "sha512-pfkPYCcuV0TJoo/jlsUeWNV8rk7uMU6ocnYNvca1Vu+pyKi8Rl8Zo2scPt9O72gCsXIm+dMxOOWuA3VFDSdzWA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/vali-date": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
"integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=",
"engines": {
"node": ">=0.10.0"
}
}
},
"dependencies": { "dependencies": {
"@sindresorhus/is": { "@sindresorhus/is": {
"version": "4.0.1", "version": "4.0.1",

View File

@ -1,31 +1,31 @@
const util = require('../../index'); const util = require('../../index');
describe('@jsdoc/util', () => { describe('@jsdoc/util', () => {
it('is an object', () => { it('is an object', () => {
expect(util).toBeObject(); expect(util).toBeObject();
});
describe('cast', () => {
it('is lib/cast', () => {
const cast = require('../../lib/cast');
expect(util.cast).toBe(cast);
}); });
});
describe('cast', () => { describe('EventBus', () => {
it('is lib/cast', () => { it('is lib/bus', () => {
const cast = require('../../lib/cast'); const bus = require('../../lib/bus');
expect(util.cast).toBe(cast); expect(util.EventBus).toBe(bus);
});
}); });
});
describe('EventBus', () => { describe('fs', () => {
it('is lib/bus', () => { it('is lib/fs', () => {
const bus = require('../../lib/bus'); const fs = require('../../lib/fs');
expect(util.EventBus).toBe(bus); expect(util.fs).toBe(fs);
});
});
describe('fs', () => {
it('is lib/fs', () => {
const fs = require('../../lib/fs');
expect(util.fs).toBe(fs);
});
}); });
});
}); });

View File

@ -1,66 +1,66 @@
describe('@jsdoc/util/lib/bus', () => { describe('@jsdoc/util/lib/bus', () => {
const EventBus = require('../../../lib/bus'); const EventBus = require('../../../lib/bus');
const EventEmitter = require('events').EventEmitter; const EventEmitter = require('events').EventEmitter;
const ignoreCache = { cache: false }; const ignoreCache = { cache: false };
it('inherits from EventEmitter', () => { it('inherits from EventEmitter', () => {
expect(new EventBus('foo', ignoreCache) instanceof EventEmitter).toBeTrue(); expect(new EventBus('foo', ignoreCache) instanceof EventEmitter).toBeTrue();
});
it('accepts a string for the ID', () => {
function makeBus() {
return new EventBus('foo', ignoreCache);
}
expect(makeBus).not.toThrow();
});
it('accepts a Symbol for the ID', () => {
function makeBus() {
return new EventBus(Symbol('foo'), ignoreCache);
}
expect(makeBus).not.toThrow();
});
it('throws on bad IDs', () => {
function crashBus() {
return new EventBus(true, ignoreCache);
}
expect(crashBus).toThrowError();
});
it('uses a cache by default', () => {
let fired = false;
const id = Symbol('cache-test');
const bus1 = new EventBus(id);
const bus2 = new EventBus(id);
bus1.once('foo', () => {
fired = true;
}); });
it('accepts a string for the ID', () => { bus2.emit('foo');
function makeBus() {
return new EventBus('foo', ignoreCache);
}
expect(makeBus).not.toThrow(); expect(bus1).toBe(bus2);
expect(fired).toBeTrue();
});
it('ignores the cache when asked', () => {
let fired = false;
const id = Symbol('cache-test');
const bus1 = new EventBus(id, ignoreCache);
const bus2 = new EventBus(id, ignoreCache);
bus1.once('foo', () => {
fired = true;
}); });
it('accepts a Symbol for the ID', () => { bus2.emit('foo');
function makeBus() {
return new EventBus(Symbol('foo'), ignoreCache);
}
expect(makeBus).not.toThrow(); expect(bus1).not.toBe(bus2);
}); expect(fired).toBeFalse();
});
it('throws on bad IDs', () => {
function crashBus() {
return new EventBus(true, ignoreCache);
}
expect(crashBus).toThrowError();
});
it('uses a cache by default', () => {
let fired = false;
const id = Symbol('cache-test');
const bus1 = new EventBus(id);
const bus2 = new EventBus(id);
bus1.once('foo', () => {
fired = true;
});
bus2.emit('foo');
expect(bus1).toBe(bus2);
expect(fired).toBeTrue();
});
it('ignores the cache when asked', () => {
let fired = false;
const id = Symbol('cache-test');
const bus1 = new EventBus(id, ignoreCache);
const bus2 = new EventBus(id, ignoreCache);
bus1.once('foo', () => {
fired = true;
});
bus2.emit('foo');
expect(bus1).not.toBe(bus2);
expect(fired).toBeFalse();
});
}); });

View File

@ -1,66 +1,66 @@
describe('@jsdoc/util/lib/cast', () => { describe('@jsdoc/util/lib/cast', () => {
const cast = require('../../../lib/cast'); const cast = require('../../../lib/cast');
it('is a function', () => { it('is a function', () => {
expect(cast).toBeFunction(); expect(cast).toBeFunction();
});
it('does not modify values that are not strings, objects, or arrays', () => {
expect(cast(8)).toBe(8);
});
it('does not modify strings that are neither boolean-ish nor number-ish', () => {
expect(cast('hello world')).toBe('hello world');
});
it('casts "true" and "false" to booleans', () => {
expect(cast('true')).toBeTrue();
expect(cast('false')).toBeFalse();
});
it('casts "null" to null', () => {
expect(cast('null')).toBeNull();
});
it('casts "undefined" to undefined', () => {
expect(cast('undefined')).toBeUndefined();
});
it('casts positive number-ish strings to numbers', () => {
expect(cast('17.35')).toBe(17.35);
});
it('casts negative number-ish strings to numbers', () => {
expect(cast('-17.35')).toBe(-17.35);
});
it('casts "NaN" to NaN', () => {
expect(cast('NaN')).toBeNaN();
});
it('casts values of object properties', () => {
expect(cast({ foo: 'true' })).toEqual({ foo: true });
});
it('casts values of properties in nested objects', () => {
const result = cast({
foo: {
bar: 'true',
},
}); });
it('does not modify values that are not strings, objects, or arrays', () => { expect(result).toEqual({
expect(cast(8)).toBe(8); foo: {
bar: true,
},
}); });
});
it('does not modify strings that are neither boolean-ish nor number-ish', () => { it('casts values in an array', () => {
expect(cast('hello world')).toBe('hello world'); expect(cast(['true', '17.35'])).toEqual([true, 17.35]);
}); });
it('casts "true" and "false" to booleans', () => { it('casts values in a nested array', () => {
expect(cast('true')).toBeTrue(); expect(cast(['true', ['17.35']])).toEqual([true, [17.35]]);
expect(cast('false')).toBeFalse(); });
});
it('casts "null" to null', () => {
expect(cast('null')).toBeNull();
});
it('casts "undefined" to undefined', () => {
expect(cast('undefined')).toBeUndefined();
});
it('casts positive number-ish strings to numbers', () => {
expect(cast('17.35')).toBe(17.35);
});
it('casts negative number-ish strings to numbers', () => {
expect(cast('-17.35')).toBe(-17.35);
});
it('casts "NaN" to NaN', () => {
expect(cast('NaN')).toBeNaN();
});
it('casts values of object properties', () => {
expect(cast({ foo: 'true' })).toEqual({ foo: true });
});
it('casts values of properties in nested objects', () => {
const result = cast({
foo: {
bar: 'true'
}
});
expect(result).toEqual({
foo: {
bar: true
}
});
});
it('casts values in an array', () => {
expect(cast(['true', '17.35'])).toEqual([true, 17.35]);
});
it('casts values in a nested array', () => {
expect(cast(['true', ['17.35']])).toEqual([true, [17.35]]);
});
}); });

View File

@ -1,71 +1,66 @@
describe('@jsdoc/util/lib/fs', () => { describe('@jsdoc/util/lib/fs', () => {
const mockFs = require('mock-fs'); const mockFs = require('mock-fs');
const fsUtil = require('../../../lib/fs'); const fsUtil = require('../../../lib/fs');
const path = require('path'); const path = require('path');
afterEach(() => mockFs.restore()); afterEach(() => mockFs.restore());
it('has an lsSync method', () => { it('has an lsSync method', () => {
expect(fsUtil.lsSync).toBeFunction(); expect(fsUtil.lsSync).toBeFunction();
});
describe('lsSync', () => {
beforeEach(() => {
mockFs({
head: {
eyes: '',
ears: '',
mouth: '',
nose: '',
shoulders: {
knees: {
meniscus: '',
toes: {
phalanx: '',
'.big-toe-phalanx': '',
},
},
},
},
});
}); });
describe('lsSync', () => { const cwd = process.cwd();
beforeEach(() => {
mockFs({
head: {
eyes: '',
ears: '',
mouth: '',
nose: '',
shoulders: {
knees: {
meniscus: '',
toes: {
phalanx: '',
'.big-toe-phalanx': ''
}
}
}
}
});
});
const cwd = process.cwd(); function resolvePaths(files) {
return files.map((f) => path.join(cwd, f)).sort();
}
function resolvePaths(files) { const allFiles = resolvePaths([
return files.map(f => path.join(cwd, f)).sort(); 'head/eyes',
} 'head/ears',
'head/mouth',
'head/nose',
'head/shoulders/knees/meniscus',
'head/shoulders/knees/toes/phalanx',
]);
const allFiles = resolvePaths([ it('gets all non-hidden files from all levels by default', () => {
'head/eyes', const files = fsUtil.lsSync(cwd).sort();
'head/ears',
'head/mouth',
'head/nose',
'head/shoulders/knees/meniscus',
'head/shoulders/knees/toes/phalanx'
]);
it('gets all non-hidden files from all levels by default', () => { expect(files).toEqual(allFiles);
const files = fsUtil.lsSync(cwd).sort();
expect(files).toEqual(allFiles);
});
it('limits recursion depth when asked', () => {
const files = fsUtil.lsSync(cwd, { depth: 1 }).sort();
expect(files).toEqual(resolvePaths([
'head/eyes',
'head/ears',
'head/mouth',
'head/nose'
]));
});
it('treats a depth of -1 as infinite', () => {
const files = fsUtil.lsSync('head', { depth: -1 }).sort();
expect(files).toEqual(allFiles);
});
}); });
it('limits recursion depth when asked', () => {
const files = fsUtil.lsSync(cwd, { depth: 1 }).sort();
expect(files).toEqual(resolvePaths(['head/eyes', 'head/ears', 'head/mouth', 'head/nose']));
});
it('treats a depth of -1 as infinite', () => {
const files = fsUtil.lsSync('head', { depth: -1 }).sort();
expect(files).toEqual(allFiles);
});
});
}); });

View File

@ -1,33 +1,33 @@
describe('@jsdoc/util/lib/log', () => { describe('@jsdoc/util/lib/log', () => {
const EventBus = require('../../../lib/bus'); const EventBus = require('../../../lib/bus');
const log = require('../../../lib/log'); const log = require('../../../lib/log');
const fns = ['debug', 'error', 'info', 'fatal', 'verbose', 'warn']; const fns = ['debug', 'error', 'info', 'fatal', 'verbose', 'warn'];
it('is an object', () => { it('is an object', () => {
expect(log).toBeObject(); expect(log).toBeObject();
});
it('provides the expected functions', () => {
fns.forEach((fn) => {
expect(log[fn]).toBeFunction();
}); });
});
it('provides the expected functions', () => { describe('functions', () => {
fns.forEach(fn => { const bus = new EventBus('jsdoc');
expect(log[fn]).toBeFunction();
}); it('sends events to the event bus', () => {
}); fns.forEach((fn) => {
let event;
describe('functions', () => {
const bus = new EventBus('jsdoc'); bus.once(`logger:${fn}`, (e) => {
event = e;
it('sends events to the event bus', () => {
fns.forEach(fn => {
let event;
bus.once(`logger:${fn}`, e => {
event = e;
});
log[fn]('testing');
expect(event).toBe('testing');
});
}); });
log[fn]('testing');
expect(event).toBe('testing');
});
}); });
});
}); });

View File

@ -15,398 +15,383 @@ const Promise = require('bluebird');
* @private * @private
*/ */
module.exports = (() => { module.exports = (() => {
const props = { const props = {
docs: [], docs: [],
packageJson: null, packageJson: null,
shouldExitWithError: false, shouldExitWithError: false,
shouldPrintHelp: false, shouldPrintHelp: false,
tmpdir: null tmpdir: null,
};
const bus = new EventBus('jsdoc');
const cli = {};
const engine = new Engine();
const FATAL_ERROR_MESSAGE =
'Exiting JSDoc because an error occurred. See the previous log ' + 'messages for details.';
const LOG_LEVELS = Engine.LOG_LEVELS;
// TODO: docs
cli.setVersionInfo = () => {
const fs = require('fs');
// allow this to throw--something is really wrong if we can't read our own package file
const info = JSON.parse(
stripBom(fs.readFileSync(path.join(env.dirname, 'package.json'), 'utf8'))
);
const revision = new Date(parseInt(info.revision, 10));
env.version = {
number: info.version,
revision: revision.toUTCString(),
}; };
const bus = new EventBus('jsdoc'); engine.version = env.version.number;
const cli = {}; engine.revision = revision;
const engine = new Engine();
const FATAL_ERROR_MESSAGE = 'Exiting JSDoc because an error occurred. See the previous log ' +
'messages for details.';
const LOG_LEVELS = Engine.LOG_LEVELS;
// TODO: docs
cli.setVersionInfo = () => {
const fs = require('fs');
// allow this to throw--something is really wrong if we can't read our own package file
const info = JSON.parse(stripBom(fs.readFileSync(path.join(env.dirname, 'package.json'),
'utf8')));
const revision = new Date(parseInt(info.revision, 10));
env.version = {
number: info.version,
revision: revision.toUTCString()
};
engine.version = env.version.number;
engine.revision = revision;
return cli;
};
// TODO: docs
cli.loadConfig = () => {
const _ = require('lodash');
let conf;
try {
env.opts = engine.parseFlags(env.args);
}
catch (e) {
props.shouldPrintHelp = true;
cli.exit(
1,
`${e.message}\n`
);
return cli;
}
try {
conf = config.loadSync(env.opts.configure);
env.conf = conf.config;
}
catch (e) {
cli.exit(
1,
`Cannot parse the config file ${conf.filepath}: ${e}\n${FATAL_ERROR_MESSAGE}`
);
return cli;
}
// look for options on the command line, then in the config
env.opts = _.defaults(env.opts, env.conf.opts);
return cli;
};
// TODO: docs
cli.configureLogger = () => {
function recoverableError() {
props.shouldExitWithError = true;
}
function fatalError() {
cli.exit(1);
}
if (env.opts.test) {
engine.logLevel = LOG_LEVELS.SILENT;
} else {
if (env.opts.debug) {
engine.logLevel = LOG_LEVELS.DEBUG;
}
else if (env.opts.verbose) {
engine.logLevel = LOG_LEVELS.INFO;
}
if (env.opts.pedantic) {
bus.once('logger:warn', recoverableError);
bus.once('logger:error', fatalError);
}
else {
bus.once('logger:error', recoverableError);
}
bus.once('logger:fatal', fatalError);
}
return cli;
};
// TODO: docs
cli.logStart = () => {
log.debug(engine.versionDetails);
log.debug('Environment info: %j', {
env: {
conf: env.conf,
opts: env.opts
}
});
};
// TODO: docs
cli.logFinish = () => {
let delta;
let deltaSeconds;
if (env.run.finish && env.run.start) {
delta = env.run.finish.getTime() - env.run.start.getTime();
}
if (delta !== undefined) {
deltaSeconds = (delta / 1000).toFixed(2);
log.info(`Finished running in ${deltaSeconds} seconds.`);
}
};
// TODO: docs
cli.runCommand = () => {
let cmd;
const opts = env.opts;
// If we already need to exit with an error, don't do any more work.
if (props.shouldExitWithError) {
cmd = () => Promise.resolve(0);
}
else if (opts.help) {
cmd = cli.printHelp;
}
else if (opts.test) {
cmd = cli.runTests;
}
else if (opts.version) {
cmd = cli.printVersion;
}
else {
cmd = cli.main;
}
return cmd().then(errorCode => {
if (!errorCode && props.shouldExitWithError) {
errorCode = 1;
}
cli.logFinish();
cli.exit(errorCode || 0);
});
};
// TODO: docs
cli.printHelp = () => {
cli.printVersion();
console.log(engine.help({ maxLength: process.stdout.columns }));
return Promise.resolve(0);
};
// TODO: docs
cli.runTests = () => require('./test')();
// TODO: docs
cli.printVersion = () => {
console.log(engine.versionDetails);
return Promise.resolve(0);
};
// TODO: docs
cli.main = () => {
cli.scanFiles();
if (env.sourceFiles.length === 0) {
console.log('There are no input files to process.');
return Promise.resolve(0);
} else {
return cli.createParser()
.parseFiles()
.processParseResults()
.then(() => {
env.run.finish = new Date();
return 0;
});
}
};
function readPackageJson(filepath) {
const fs = require('fs');
try {
return stripJsonComments( fs.readFileSync(filepath, 'utf8') );
}
catch (e) {
log.error(`Unable to read the package file ${filepath}`);
return null;
}
}
function buildSourceList() {
let packageJson;
let sourceFile;
let sourceFiles = env.opts._ ? env.opts._.slice(0) : [];
if (env.conf.source && env.conf.source.include) {
sourceFiles = sourceFiles.concat(env.conf.source.include);
}
// load the user-specified package file, if any
if (env.opts.package) {
packageJson = readPackageJson(env.opts.package);
}
// source files named `package.json` or `README.md` get special treatment, unless the user
// explicitly specified a package and/or README file
for (let i = 0, l = sourceFiles.length; i < l; i++) {
sourceFile = sourceFiles[i];
if ( !env.opts.package && /\bpackage\.json$/i.test(sourceFile) ) {
packageJson = readPackageJson(sourceFile);
sourceFiles.splice(i--, 1);
}
if ( !env.opts.readme && /(\bREADME|\.md)$/i.test(sourceFile) ) {
env.opts.readme = sourceFile;
sourceFiles.splice(i--, 1);
}
}
// Resolve the path to the README.
if (env.opts.readme) {
env.opts.readme = path.resolve(env.opts.readme);
}
props.packageJson = packageJson;
return sourceFiles;
}
// TODO: docs
cli.scanFiles = () => {
const { Filter } = require('jsdoc/src/filter');
const { Scanner } = require('jsdoc/src/scanner');
let filter;
let scanner;
env.opts._ = buildSourceList();
// are there any files to scan and parse?
if (env.conf.source && env.opts._.length) {
filter = new Filter(env.conf.source);
scanner = new Scanner();
env.sourceFiles = scanner.scan(env.opts._,
(env.opts.recurse ? env.conf.recurseDepth : undefined), filter);
}
return cli;
};
cli.createParser = () => {
const handlers = require('jsdoc/src/handlers');
const parser = require('jsdoc/src/parser');
const plugins = require('jsdoc/plugins');
props.parser = parser.createParser(env.conf.parser, env.conf);
if (env.conf.plugins) {
plugins.installPlugins(env.conf.plugins, props.parser);
}
handlers.attachTo(props.parser);
return cli;
};
cli.parseFiles = () => {
const augment = require('jsdoc/augment');
const borrow = require('jsdoc/borrow');
const Package = require('jsdoc/package').Package;
let docs;
let packageDocs;
props.docs = docs = props.parser.parse(env.sourceFiles, env.opts.encoding);
// If there is no package.json, just create an empty package
packageDocs = new Package(props.packageJson);
packageDocs.files = env.sourceFiles || [];
docs.push(packageDocs);
log.debug('Adding inherited symbols, mixins, and interface implementations...');
augment.augmentAll(docs);
log.debug('Adding borrowed doclets...');
borrow.resolveBorrows(docs);
log.debug('Post-processing complete.');
props.parser.fireProcessingComplete(docs);
return cli;
};
cli.processParseResults = () => {
if (env.opts.explain) {
cli.dumpParseResults();
return Promise.resolve();
}
else {
return cli.generateDocs();
}
};
cli.dumpParseResults = () => {
console.log(JSON.stringify(props.docs, null, 4));
return cli;
};
cli.generateDocs = () => {
let message;
const taffy = require('taffydb').taffy;
let template;
env.opts.template = env.opts.template || path.join(__dirname, 'templates', 'default');
try {
// TODO: Just look for a `publish` function in the specified module, not a `publish.js`
// file _and_ a `publish` function.
template = require(`${env.opts.template}/publish`);
}
catch (e) {
log.fatal(`Unable to load template: ${e.message}` || e);
}
// templates should include a publish.js file that exports a "publish" function
if (template.publish && typeof template.publish === 'function') {
let publishPromise;
log.info('Generating output files...');
publishPromise = template.publish(
taffy(props.docs),
env.opts
);
return Promise.resolve(publishPromise);
}
else {
message = `${env.opts.template} does not export a "publish" function. ` +
'Global "publish" functions are no longer supported.';
log.fatal(message);
return Promise.reject(new Error(message));
}
};
// TODO: docs
cli.exit = (exitCode, message) => {
if (exitCode > 0) {
props.shouldExitWithError = true;
if (message) {
console.error(message);
}
}
process.on('exit', () => {
if (props.shouldPrintHelp) {
cli.printHelp();
}
process.exit(exitCode);
});
};
return cli; return cli;
};
// TODO: docs
cli.loadConfig = () => {
const _ = require('lodash');
let conf;
try {
env.opts = engine.parseFlags(env.args);
} catch (e) {
props.shouldPrintHelp = true;
cli.exit(1, `${e.message}\n`);
return cli;
}
try {
conf = config.loadSync(env.opts.configure);
env.conf = conf.config;
} catch (e) {
cli.exit(1, `Cannot parse the config file ${conf.filepath}: ${e}\n${FATAL_ERROR_MESSAGE}`);
return cli;
}
// look for options on the command line, then in the config
env.opts = _.defaults(env.opts, env.conf.opts);
return cli;
};
// TODO: docs
cli.configureLogger = () => {
function recoverableError() {
props.shouldExitWithError = true;
}
function fatalError() {
cli.exit(1);
}
if (env.opts.test) {
engine.logLevel = LOG_LEVELS.SILENT;
} else {
if (env.opts.debug) {
engine.logLevel = LOG_LEVELS.DEBUG;
} else if (env.opts.verbose) {
engine.logLevel = LOG_LEVELS.INFO;
}
if (env.opts.pedantic) {
bus.once('logger:warn', recoverableError);
bus.once('logger:error', fatalError);
} else {
bus.once('logger:error', recoverableError);
}
bus.once('logger:fatal', fatalError);
}
return cli;
};
// TODO: docs
cli.logStart = () => {
log.debug(engine.versionDetails);
log.debug('Environment info: %j', {
env: {
conf: env.conf,
opts: env.opts,
},
});
};
// TODO: docs
cli.logFinish = () => {
let delta;
let deltaSeconds;
if (env.run.finish && env.run.start) {
delta = env.run.finish.getTime() - env.run.start.getTime();
}
if (delta !== undefined) {
deltaSeconds = (delta / 1000).toFixed(2);
log.info(`Finished running in ${deltaSeconds} seconds.`);
}
};
// TODO: docs
cli.runCommand = () => {
let cmd;
const opts = env.opts;
// If we already need to exit with an error, don't do any more work.
if (props.shouldExitWithError) {
cmd = () => Promise.resolve(0);
} else if (opts.help) {
cmd = cli.printHelp;
} else if (opts.test) {
cmd = cli.runTests;
} else if (opts.version) {
cmd = cli.printVersion;
} else {
cmd = cli.main;
}
return cmd().then((errorCode) => {
if (!errorCode && props.shouldExitWithError) {
errorCode = 1;
}
cli.logFinish();
cli.exit(errorCode || 0);
});
};
// TODO: docs
cli.printHelp = () => {
cli.printVersion();
console.log(engine.help({ maxLength: process.stdout.columns }));
return Promise.resolve(0);
};
// TODO: docs
cli.runTests = () => require('./test')();
// TODO: docs
cli.printVersion = () => {
console.log(engine.versionDetails);
return Promise.resolve(0);
};
// TODO: docs
cli.main = () => {
cli.scanFiles();
if (env.sourceFiles.length === 0) {
console.log('There are no input files to process.');
return Promise.resolve(0);
} else {
return cli
.createParser()
.parseFiles()
.processParseResults()
.then(() => {
env.run.finish = new Date();
return 0;
});
}
};
function readPackageJson(filepath) {
const fs = require('fs');
try {
return stripJsonComments(fs.readFileSync(filepath, 'utf8'));
} catch (e) {
log.error(`Unable to read the package file ${filepath}`);
return null;
}
}
function buildSourceList() {
let packageJson;
let sourceFile;
let sourceFiles = env.opts._ ? env.opts._.slice(0) : [];
if (env.conf.source && env.conf.source.include) {
sourceFiles = sourceFiles.concat(env.conf.source.include);
}
// load the user-specified package file, if any
if (env.opts.package) {
packageJson = readPackageJson(env.opts.package);
}
// source files named `package.json` or `README.md` get special treatment, unless the user
// explicitly specified a package and/or README file
for (let i = 0, l = sourceFiles.length; i < l; i++) {
sourceFile = sourceFiles[i];
if (!env.opts.package && /\bpackage\.json$/i.test(sourceFile)) {
packageJson = readPackageJson(sourceFile);
sourceFiles.splice(i--, 1);
}
if (!env.opts.readme && /(\bREADME|\.md)$/i.test(sourceFile)) {
env.opts.readme = sourceFile;
sourceFiles.splice(i--, 1);
}
}
// Resolve the path to the README.
if (env.opts.readme) {
env.opts.readme = path.resolve(env.opts.readme);
}
props.packageJson = packageJson;
return sourceFiles;
}
// TODO: docs
cli.scanFiles = () => {
const { Filter } = require('jsdoc/src/filter');
const { Scanner } = require('jsdoc/src/scanner');
let filter;
let scanner;
env.opts._ = buildSourceList();
// are there any files to scan and parse?
if (env.conf.source && env.opts._.length) {
filter = new Filter(env.conf.source);
scanner = new Scanner();
env.sourceFiles = scanner.scan(
env.opts._,
env.opts.recurse ? env.conf.recurseDepth : undefined,
filter
);
}
return cli;
};
cli.createParser = () => {
const handlers = require('jsdoc/src/handlers');
const parser = require('jsdoc/src/parser');
const plugins = require('jsdoc/plugins');
props.parser = parser.createParser(env.conf.parser, env.conf);
if (env.conf.plugins) {
plugins.installPlugins(env.conf.plugins, props.parser);
}
handlers.attachTo(props.parser);
return cli;
};
cli.parseFiles = () => {
const augment = require('jsdoc/augment');
const borrow = require('jsdoc/borrow');
const Package = require('jsdoc/package').Package;
let docs;
let packageDocs;
props.docs = docs = props.parser.parse(env.sourceFiles, env.opts.encoding);
// If there is no package.json, just create an empty package
packageDocs = new Package(props.packageJson);
packageDocs.files = env.sourceFiles || [];
docs.push(packageDocs);
log.debug('Adding inherited symbols, mixins, and interface implementations...');
augment.augmentAll(docs);
log.debug('Adding borrowed doclets...');
borrow.resolveBorrows(docs);
log.debug('Post-processing complete.');
props.parser.fireProcessingComplete(docs);
return cli;
};
cli.processParseResults = () => {
if (env.opts.explain) {
cli.dumpParseResults();
return Promise.resolve();
} else {
return cli.generateDocs();
}
};
cli.dumpParseResults = () => {
console.log(JSON.stringify(props.docs, null, 4));
return cli;
};
cli.generateDocs = () => {
let message;
const taffy = require('taffydb').taffy;
let template;
env.opts.template = env.opts.template || path.join(__dirname, 'templates', 'default');
try {
// TODO: Just look for a `publish` function in the specified module, not a `publish.js`
// file _and_ a `publish` function.
template = require(`${env.opts.template}/publish`);
} catch (e) {
log.fatal(`Unable to load template: ${e.message}` || e);
}
// templates should include a publish.js file that exports a "publish" function
if (template.publish && typeof template.publish === 'function') {
let publishPromise;
log.info('Generating output files...');
publishPromise = template.publish(taffy(props.docs), env.opts);
return Promise.resolve(publishPromise);
} else {
message =
`${env.opts.template} does not export a "publish" function. ` +
'Global "publish" functions are no longer supported.';
log.fatal(message);
return Promise.reject(new Error(message));
}
};
// TODO: docs
cli.exit = (exitCode, message) => {
if (exitCode > 0) {
props.shouldExitWithError = true;
if (message) {
console.error(message);
}
}
process.on('exit', () => {
if (props.shouldPrintHelp) {
cli.printHelp();
}
process.exit(exitCode);
});
};
return cli;
})(); })();

View File

@ -2,34 +2,34 @@
// initialize the environment for Node.js // initialize the environment for Node.js
(() => { (() => {
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
let env; let env;
let jsdocPath = __dirname; let jsdocPath = __dirname;
// Create a custom require method that adds `lib/jsdoc` and `node_modules` to the module // Create a custom require method that adds `lib/jsdoc` and `node_modules` to the module
// lookup path. This makes it possible to `require('jsdoc/foo')` from external templates and // lookup path. This makes it possible to `require('jsdoc/foo')` from external templates and
// plugins, and within JSDoc itself. It also allows external templates and plugins to // plugins, and within JSDoc itself. It also allows external templates and plugins to
// require JSDoc's module dependencies without installing them locally. // require JSDoc's module dependencies without installing them locally.
/* eslint-disable no-global-assign, no-redeclare */ /* eslint-disable no-global-assign, no-redeclare */
require = require('requizzle')({ require = require('requizzle')({
requirePaths: { requirePaths: {
before: [path.join(__dirname, 'lib')], before: [path.join(__dirname, 'lib')],
after: [path.join(__dirname, 'node_modules')] after: [path.join(__dirname, 'node_modules')],
}, },
infect: true infect: true,
}); });
/* eslint-enable no-global-assign, no-redeclare */ /* eslint-enable no-global-assign, no-redeclare */
// resolve the path if it's a symlink // resolve the path if it's a symlink
if ( fs.statSync(jsdocPath).isSymbolicLink() ) { if (fs.statSync(jsdocPath).isSymbolicLink()) {
jsdocPath = path.resolve( path.dirname(jsdocPath), fs.readlinkSync(jsdocPath) ); jsdocPath = path.resolve(path.dirname(jsdocPath), fs.readlinkSync(jsdocPath));
} }
env = require('./lib/jsdoc/env'); env = require('./lib/jsdoc/env');
env.dirname = jsdocPath; env.dirname = jsdocPath;
env.args = process.argv.slice(2); env.args = process.argv.slice(2);
})(); })();
/** /**
@ -44,12 +44,9 @@
global.env = (() => require('./lib/jsdoc/env'))(); global.env = (() => require('./lib/jsdoc/env'))();
(async () => { (async () => {
const cli = require('./cli'); const cli = require('./cli');
cli.setVersionInfo() cli.setVersionInfo().loadConfig().configureLogger().logStart();
.loadConfig()
.configureLogger()
.logStart();
await cli.runCommand(); await cli.runCommand();
})(); })();

View File

@ -6,116 +6,116 @@
const _ = require('lodash'); const _ = require('lodash');
const { fromParts, SCOPE, toParts } = require('@jsdoc/core').name; const { fromParts, SCOPE, toParts } = require('@jsdoc/core').name;
const jsdoc = { const jsdoc = {
doclet: require('jsdoc/doclet') doclet: require('jsdoc/doclet'),
}; };
const hasOwnProp = Object.prototype.hasOwnProperty; const hasOwnProp = Object.prototype.hasOwnProperty;
function mapDependencies(index, propertyName) { function mapDependencies(index, propertyName) {
const dependencies = {}; const dependencies = {};
let doc; let doc;
let doclets; let doclets;
const kinds = ['class', 'external', 'interface', 'mixin']; const kinds = ['class', 'external', 'interface', 'mixin'];
let len = 0; let len = 0;
Object.keys(index).forEach(indexName => { Object.keys(index).forEach((indexName) => {
doclets = index[indexName]; doclets = index[indexName];
for (let i = 0, ii = doclets.length; i < ii; i++) { for (let i = 0, ii = doclets.length; i < ii; i++) {
doc = doclets[i]; doc = doclets[i];
if (kinds.includes(doc.kind)) { if (kinds.includes(doc.kind)) {
dependencies[indexName] = {}; dependencies[indexName] = {};
if (hasOwnProp.call(doc, propertyName)) { if (hasOwnProp.call(doc, propertyName)) {
len = doc[propertyName].length; len = doc[propertyName].length;
for (let j = 0; j < len; j++) { for (let j = 0; j < len; j++) {
dependencies[indexName][doc[propertyName][j]] = true; dependencies[indexName][doc[propertyName][j]] = true;
} }
}
}
} }
}); }
}
});
return dependencies; return dependencies;
} }
class Sorter { class Sorter {
constructor(dependencies) { constructor(dependencies) {
this.dependencies = dependencies; this.dependencies = dependencies;
this.visited = {}; this.visited = {};
this.sorted = []; this.sorted = [];
} }
visit(key) { visit(key) {
if (!(key in this.visited)) { if (!(key in this.visited)) {
this.visited[key] = true; this.visited[key] = true;
if (this.dependencies[key]) { if (this.dependencies[key]) {
Object.keys(this.dependencies[key]).forEach(path => { Object.keys(this.dependencies[key]).forEach((path) => {
this.visit(path); this.visit(path);
});
}
this.sorted.push(key);
}
}
sort() {
Object.keys(this.dependencies).forEach(key => {
this.visit(key);
}); });
}
return this.sorted; this.sorted.push(key);
} }
}
sort() {
Object.keys(this.dependencies).forEach((key) => {
this.visit(key);
});
return this.sorted;
}
} }
function sort(dependencies) { function sort(dependencies) {
const sorter = new Sorter(dependencies); const sorter = new Sorter(dependencies);
return sorter.sort(); return sorter.sort();
} }
function getMembers(longname, {index}, scopes) { function getMembers(longname, { index }, scopes) {
const memberof = index.memberof[longname] || []; const memberof = index.memberof[longname] || [];
const members = []; const members = [];
memberof.forEach(candidate => { memberof.forEach((candidate) => {
if (scopes.includes(candidate.scope)) { if (scopes.includes(candidate.scope)) {
members.push(candidate); members.push(candidate);
} }
}); });
return members; return members;
} }
function getDocumentedLongname(longname, {index}) { function getDocumentedLongname(longname, { index }) {
const doclets = index.documented[longname] || []; const doclets = index.documented[longname] || [];
return doclets[doclets.length - 1]; return doclets[doclets.length - 1];
} }
function addDocletProperty(doclets, propName, value) { function addDocletProperty(doclets, propName, value) {
for (let i = 0, l = doclets.length; i < l; i++) { for (let i = 0, l = doclets.length; i < l; i++) {
doclets[i][propName] = value; doclets[i][propName] = value;
} }
} }
function reparentDoclet({longname}, child) { function reparentDoclet({ longname }, child) {
const parts = toParts(child.longname); const parts = toParts(child.longname);
parts.memberof = longname; parts.memberof = longname;
child.memberof = longname; child.memberof = longname;
child.longname = fromParts(parts); child.longname = fromParts(parts);
} }
function parentIsClass({kind}) { function parentIsClass({ kind }) {
return kind === 'class'; return kind === 'class';
} }
function staticToInstance(doclet) { function staticToInstance(doclet) {
const parts = toParts(doclet.longname); const parts = toParts(doclet.longname);
parts.scope = SCOPE.PUNC.INSTANCE; parts.scope = SCOPE.PUNC.INSTANCE;
doclet.longname = fromParts(parts); doclet.longname = fromParts(parts);
doclet.scope = SCOPE.NAMES.INSTANCE; doclet.scope = SCOPE.NAMES.INSTANCE;
} }
/** /**
@ -137,15 +137,14 @@ function staticToInstance(doclet) {
* @return {void} * @return {void}
*/ */
function updateAddedDoclets(doclet, additions, indexes) { function updateAddedDoclets(doclet, additions, indexes) {
if (typeof indexes[doclet.longname] !== 'undefined') { if (typeof indexes[doclet.longname] !== 'undefined') {
// replace the existing doclet // replace the existing doclet
additions[indexes[doclet.longname]] = doclet; additions[indexes[doclet.longname]] = doclet;
} } else {
else { // add the doclet to the array, and track its index
// add the doclet to the array, and track its index additions.push(doclet);
additions.push(doclet); indexes[doclet.longname] = additions.length - 1;
indexes[doclet.longname] = additions.length - 1; }
}
} }
/** /**
@ -158,11 +157,11 @@ function updateAddedDoclets(doclet, additions, indexes) {
* @return {void} * @return {void}
*/ */
function updateDocumentedDoclets(doclet, documented) { function updateDocumentedDoclets(doclet, documented) {
if ( !hasOwnProp.call(documented, doclet.longname) ) { if (!hasOwnProp.call(documented, doclet.longname)) {
documented[doclet.longname] = []; documented[doclet.longname] = [];
} }
documented[doclet.longname].push(doclet); documented[doclet.longname].push(doclet);
} }
/** /**
@ -175,363 +174,360 @@ function updateDocumentedDoclets(doclet, documented) {
* @return {void} * @return {void}
*/ */
function updateMemberofDoclets(doclet, memberof) { function updateMemberofDoclets(doclet, memberof) {
if (doclet.memberof) { if (doclet.memberof) {
if ( !hasOwnProp.call(memberof, doclet.memberof) ) { if (!hasOwnProp.call(memberof, doclet.memberof)) {
memberof[doclet.memberof] = []; memberof[doclet.memberof] = [];
}
memberof[doclet.memberof].push(doclet);
} }
memberof[doclet.memberof].push(doclet);
}
} }
function explicitlyInherits(doclets) { function explicitlyInherits(doclets) {
let doclet; let doclet;
let inherits = false; let inherits = false;
for (let i = 0, l = doclets.length; i < l; i++) { for (let i = 0, l = doclets.length; i < l; i++) {
doclet = doclets[i]; doclet = doclets[i];
if (typeof doclet.inheritdoc !== 'undefined' || typeof doclet.override !== 'undefined') { if (typeof doclet.inheritdoc !== 'undefined' || typeof doclet.override !== 'undefined') {
inherits = true; inherits = true;
break; break;
}
} }
}
return inherits; return inherits;
} }
function changeMemberof(longname, newMemberof) { function changeMemberof(longname, newMemberof) {
const atoms = toParts(longname); const atoms = toParts(longname);
atoms.memberof = newMemberof; atoms.memberof = newMemberof;
return fromParts(atoms); return fromParts(atoms);
} }
// TODO: try to reduce overlap with similar methods // TODO: try to reduce overlap with similar methods
function getInheritedAdditions(doclets, docs, {documented, memberof}) { function getInheritedAdditions(doclets, docs, { documented, memberof }) {
let additionIndexes; let additionIndexes;
const additions = []; const additions = [];
let childDoclet; let childDoclet;
let childLongname; let childLongname;
let doc; let doc;
let parentDoclet; let parentDoclet;
let parentMembers; let parentMembers;
let parents; let parents;
let member; let member;
let parts; let parts;
// doclets will be undefined if the inherited symbol isn't documented // doclets will be undefined if the inherited symbol isn't documented
doclets = doclets || []; doclets = doclets || [];
for (let i = 0, ii = doclets.length; i < ii; i++) { for (let i = 0, ii = doclets.length; i < ii; i++) {
doc = doclets[i]; doc = doclets[i];
parents = doc.augments; parents = doc.augments;
if ( parents && (doc.kind === 'class' || doc.kind === 'interface') ) { if (parents && (doc.kind === 'class' || doc.kind === 'interface')) {
// reset the lookup table of added doclet indexes by longname // reset the lookup table of added doclet indexes by longname
additionIndexes = {}; additionIndexes = {};
for (let j = 0, jj = parents.length; j < jj; j++) { for (let j = 0, jj = parents.length; j < jj; j++) {
parentMembers = getMembers(parents[j], docs, ['instance']); parentMembers = getMembers(parents[j], docs, ['instance']);
for (let k = 0, kk = parentMembers.length; k < kk; k++) { for (let k = 0, kk = parentMembers.length; k < kk; k++) {
parentDoclet = parentMembers[k]; parentDoclet = parentMembers[k];
// We only care about symbols that are documented. // We only care about symbols that are documented.
if (parentDoclet.undocumented) { if (parentDoclet.undocumented) {
continue; continue;
} }
childLongname = changeMemberof(parentDoclet.longname, doc.longname); childLongname = changeMemberof(parentDoclet.longname, doc.longname);
childDoclet = getDocumentedLongname(childLongname, docs) || {}; childDoclet = getDocumentedLongname(childLongname, docs) || {};
// We don't want to fold in properties from the child doclet if it had an // We don't want to fold in properties from the child doclet if it had an
// `@inheritdoc` tag. // `@inheritdoc` tag.
if (hasOwnProp.call(childDoclet, 'inheritdoc')) { if (hasOwnProp.call(childDoclet, 'inheritdoc')) {
childDoclet = {}; childDoclet = {};
} }
member = jsdoc.doclet.combine(childDoclet, parentDoclet); member = jsdoc.doclet.combine(childDoclet, parentDoclet);
if (!member.inherited) { if (!member.inherited) {
member.inherits = member.longname; member.inherits = member.longname;
} }
member.inherited = true; member.inherited = true;
member.memberof = doc.longname; member.memberof = doc.longname;
parts = toParts(member.longname); parts = toParts(member.longname);
parts.memberof = doc.longname; parts.memberof = doc.longname;
member.longname = fromParts(parts); member.longname = fromParts(parts);
// Indicate what the descendant is overriding. (We only care about the closest // Indicate what the descendant is overriding. (We only care about the closest
// ancestor. For classes A > B > C, if B#a overrides A#a, and C#a inherits B#a, // ancestor. For classes A > B > C, if B#a overrides A#a, and C#a inherits B#a,
// we don't want the doclet for C#a to say that it overrides A#a.) // we don't want the doclet for C#a to say that it overrides A#a.)
if ( hasOwnProp.call(docs.index.longname, member.longname) ) { if (hasOwnProp.call(docs.index.longname, member.longname)) {
member.overrides = parentDoclet.longname; member.overrides = parentDoclet.longname;
} } else {
else { delete member.overrides;
delete member.overrides; }
}
// Add the ancestor's docs unless the descendant overrides the ancestor AND // Add the ancestor's docs unless the descendant overrides the ancestor AND
// documents the override. // documents the override.
if ( !hasOwnProp.call(documented, member.longname) ) { if (!hasOwnProp.call(documented, member.longname)) {
updateAddedDoclets(member, additions, additionIndexes); updateAddedDoclets(member, additions, additionIndexes);
updateDocumentedDoclets(member, documented); updateDocumentedDoclets(member, documented);
updateMemberofDoclets(member, memberof); updateMemberofDoclets(member, memberof);
} }
// If the descendant used an @inheritdoc or @override tag, add the ancestor's // If the descendant used an @inheritdoc or @override tag, add the ancestor's
// docs, and ignore the existing doclets. // docs, and ignore the existing doclets.
else if ( explicitlyInherits(documented[member.longname]) ) { else if (explicitlyInherits(documented[member.longname])) {
// Ignore any existing doclets. (This is safe because we only get here if // Ignore any existing doclets. (This is safe because we only get here if
// `member.longname` is an own property of `documented`.) // `member.longname` is an own property of `documented`.)
addDocletProperty(documented[member.longname], 'ignore', true); addDocletProperty(documented[member.longname], 'ignore', true);
updateAddedDoclets(member, additions, additionIndexes); updateAddedDoclets(member, additions, additionIndexes);
updateDocumentedDoclets(member, documented); updateDocumentedDoclets(member, documented);
updateMemberofDoclets(member, memberof); updateMemberofDoclets(member, memberof);
// Remove property that's no longer accurate. // Remove property that's no longer accurate.
if (member.virtual) { if (member.virtual) {
delete member.virtual; delete member.virtual;
}
// Remove properties that we no longer need.
if (member.inheritdoc) {
delete member.inheritdoc;
}
if (member.override) {
delete member.override;
}
}
// If the descendant overrides the ancestor and documents the override,
// update the doclets to indicate what the descendant is overriding.
else {
addDocletProperty(documented[member.longname], 'overrides',
parentDoclet.longname);
}
}
} }
// Remove properties that we no longer need.
if (member.inheritdoc) {
delete member.inheritdoc;
}
if (member.override) {
delete member.override;
}
}
// If the descendant overrides the ancestor and documents the override,
// update the doclets to indicate what the descendant is overriding.
else {
addDocletProperty(documented[member.longname], 'overrides', parentDoclet.longname);
}
} }
}
} }
}
return additions; return additions;
} }
function updateMixes(mixedDoclet, mixedLongname) { function updateMixes(mixedDoclet, mixedLongname) {
let idx; let idx;
let mixedName; let mixedName;
let names; let names;
// take the fast path if there's no array of mixed-in longnames // take the fast path if there's no array of mixed-in longnames
if (!mixedDoclet.mixes) { if (!mixedDoclet.mixes) {
mixedDoclet.mixes = [mixedLongname]; mixedDoclet.mixes = [mixedLongname];
} else {
// find the short name of the longname we're mixing in
mixedName = toParts(mixedLongname).name;
// find the short name of each previously mixed-in symbol
// TODO: why do we run a map if we always shorten the same value? this looks like a bug...
names = mixedDoclet.mixes.map(() => toParts(mixedDoclet.longname).name);
// if we're mixing `myMethod` into `MixinC` from `MixinB`, and `MixinB` had the method mixed
// in from `MixinA`, don't show `MixinA.myMethod` in the `mixes` list
idx = names.indexOf(mixedName);
if (idx !== -1) {
mixedDoclet.mixes.splice(idx, 1);
} }
else {
// find the short name of the longname we're mixing in
mixedName = toParts(mixedLongname).name;
// find the short name of each previously mixed-in symbol
// TODO: why do we run a map if we always shorten the same value? this looks like a bug...
names = mixedDoclet.mixes.map(() => toParts(mixedDoclet.longname).name);
// if we're mixing `myMethod` into `MixinC` from `MixinB`, and `MixinB` had the method mixed mixedDoclet.mixes.push(mixedLongname);
// in from `MixinA`, don't show `MixinA.myMethod` in the `mixes` list }
idx = names.indexOf(mixedName);
if (idx !== -1) {
mixedDoclet.mixes.splice(idx, 1);
}
mixedDoclet.mixes.push(mixedLongname);
}
} }
// TODO: try to reduce overlap with similar methods // TODO: try to reduce overlap with similar methods
function getMixedInAdditions(mixinDoclets, allDoclets, {documented, memberof}) { function getMixedInAdditions(mixinDoclets, allDoclets, { documented, memberof }) {
let additionIndexes; let additionIndexes;
const additions = []; const additions = [];
const commentedDoclets = documented; const commentedDoclets = documented;
let doclet; let doclet;
let mixedDoclet; let mixedDoclet;
let mixedDoclets; let mixedDoclets;
let mixes; let mixes;
// mixinDoclets will be undefined if the mixed-in symbol isn't documented // mixinDoclets will be undefined if the mixed-in symbol isn't documented
mixinDoclets = mixinDoclets || []; mixinDoclets = mixinDoclets || [];
for (let i = 0, ii = mixinDoclets.length; i < ii; i++) { for (let i = 0, ii = mixinDoclets.length; i < ii; i++) {
doclet = mixinDoclets[i]; doclet = mixinDoclets[i];
mixes = doclet.mixes; mixes = doclet.mixes;
if (mixes) { if (mixes) {
// reset the lookup table of added doclet indexes by longname // reset the lookup table of added doclet indexes by longname
additionIndexes = {}; additionIndexes = {};
for (let j = 0, jj = mixes.length; j < jj; j++) { for (let j = 0, jj = mixes.length; j < jj; j++) {
mixedDoclets = getMembers(mixes[j], allDoclets, ['static']); mixedDoclets = getMembers(mixes[j], allDoclets, ['static']);
for (let k = 0, kk = mixedDoclets.length; k < kk; k++) { for (let k = 0, kk = mixedDoclets.length; k < kk; k++) {
// We only care about symbols that are documented. // We only care about symbols that are documented.
if (mixedDoclets[k].undocumented) { if (mixedDoclets[k].undocumented) {
continue; continue;
} }
mixedDoclet = _.cloneDeep(mixedDoclets[k]); mixedDoclet = _.cloneDeep(mixedDoclets[k]);
updateMixes(mixedDoclet, mixedDoclet.longname); updateMixes(mixedDoclet, mixedDoclet.longname);
mixedDoclet.mixed = true; mixedDoclet.mixed = true;
reparentDoclet(doclet, mixedDoclet); reparentDoclet(doclet, mixedDoclet);
// if we're mixing into a class, treat the mixed-in symbol as an instance member // if we're mixing into a class, treat the mixed-in symbol as an instance member
if (parentIsClass(doclet)) { if (parentIsClass(doclet)) {
staticToInstance(mixedDoclet); staticToInstance(mixedDoclet);
} }
updateAddedDoclets(mixedDoclet, additions, additionIndexes); updateAddedDoclets(mixedDoclet, additions, additionIndexes);
updateDocumentedDoclets(mixedDoclet, commentedDoclets); updateDocumentedDoclets(mixedDoclet, commentedDoclets);
updateMemberofDoclets(mixedDoclet, memberof); updateMemberofDoclets(mixedDoclet, memberof);
}
}
} }
}
} }
}
return additions; return additions;
} }
function updateImplements(implDoclets, implementedLongname) { function updateImplements(implDoclets, implementedLongname) {
if ( !Array.isArray(implDoclets) ) { if (!Array.isArray(implDoclets)) {
implDoclets = [implDoclets]; implDoclets = [implDoclets];
}
implDoclets.forEach((implDoclet) => {
if (!hasOwnProp.call(implDoclet, 'implements')) {
implDoclet.implements = [];
} }
implDoclets.forEach(implDoclet => { if (!implDoclet.implements.includes(implementedLongname)) {
if ( !hasOwnProp.call(implDoclet, 'implements') ) { implDoclet.implements.push(implementedLongname);
implDoclet.implements = []; }
} });
if (!implDoclet.implements.includes(implementedLongname)) {
implDoclet.implements.push(implementedLongname);
}
});
} }
// TODO: try to reduce overlap with similar methods // TODO: try to reduce overlap with similar methods
function getImplementedAdditions(implDoclets, allDoclets, {documented, memberof}) { function getImplementedAdditions(implDoclets, allDoclets, { documented, memberof }) {
let additionIndexes; let additionIndexes;
const additions = []; const additions = [];
let childDoclet; let childDoclet;
let childLongname; let childLongname;
const commentedDoclets = documented; const commentedDoclets = documented;
let doclet; let doclet;
let implementations; let implementations;
let implExists; let implExists;
let implementationDoclet; let implementationDoclet;
let interfaceDoclets; let interfaceDoclets;
let parentDoclet; let parentDoclet;
// interfaceDoclets will be undefined if the implemented symbol isn't documented // interfaceDoclets will be undefined if the implemented symbol isn't documented
implDoclets = implDoclets || []; implDoclets = implDoclets || [];
for (let i = 0, ii = implDoclets.length; i < ii; i++) { for (let i = 0, ii = implDoclets.length; i < ii; i++) {
doclet = implDoclets[i]; doclet = implDoclets[i];
implementations = doclet.implements; implementations = doclet.implements;
if (implementations) { if (implementations) {
// reset the lookup table of added doclet indexes by longname // reset the lookup table of added doclet indexes by longname
additionIndexes = {}; additionIndexes = {};
for (let j = 0, jj = implementations.length; j < jj; j++) { for (let j = 0, jj = implementations.length; j < jj; j++) {
interfaceDoclets = getMembers(implementations[j], allDoclets, ['instance']); interfaceDoclets = getMembers(implementations[j], allDoclets, ['instance']);
for (let k = 0, kk = interfaceDoclets.length; k < kk; k++) { for (let k = 0, kk = interfaceDoclets.length; k < kk; k++) {
parentDoclet = interfaceDoclets[k]; parentDoclet = interfaceDoclets[k];
// We only care about symbols that are documented. // We only care about symbols that are documented.
if (parentDoclet.undocumented) { if (parentDoclet.undocumented) {
continue; continue;
} }
childLongname = changeMemberof(parentDoclet.longname, doclet.longname); childLongname = changeMemberof(parentDoclet.longname, doclet.longname);
childDoclet = getDocumentedLongname(childLongname, allDoclets) || {}; childDoclet = getDocumentedLongname(childLongname, allDoclets) || {};
// We don't want to fold in properties from the child doclet if it had an // We don't want to fold in properties from the child doclet if it had an
// `@inheritdoc` tag. // `@inheritdoc` tag.
if (hasOwnProp.call(childDoclet, 'inheritdoc')) { if (hasOwnProp.call(childDoclet, 'inheritdoc')) {
childDoclet = {}; childDoclet = {};
} }
implementationDoclet = jsdoc.doclet.combine(childDoclet, parentDoclet); implementationDoclet = jsdoc.doclet.combine(childDoclet, parentDoclet);
reparentDoclet(doclet, implementationDoclet); reparentDoclet(doclet, implementationDoclet);
updateImplements(implementationDoclet, parentDoclet.longname); updateImplements(implementationDoclet, parentDoclet.longname);
// If there's no implementation, move along. // If there's no implementation, move along.
implExists = hasOwnProp.call(allDoclets.index.longname, implExists = hasOwnProp.call(allDoclets.index.longname, implementationDoclet.longname);
implementationDoclet.longname); if (!implExists) {
if (!implExists) { continue;
continue; }
}
// Add the interface's docs unless the implementation is already documented. // Add the interface's docs unless the implementation is already documented.
if ( !hasOwnProp.call(commentedDoclets, implementationDoclet.longname) ) { if (!hasOwnProp.call(commentedDoclets, implementationDoclet.longname)) {
updateAddedDoclets(implementationDoclet, additions, additionIndexes); updateAddedDoclets(implementationDoclet, additions, additionIndexes);
updateDocumentedDoclets(implementationDoclet, commentedDoclets); updateDocumentedDoclets(implementationDoclet, commentedDoclets);
updateMemberofDoclets(implementationDoclet, memberof); updateMemberofDoclets(implementationDoclet, memberof);
} }
// If the implementation used an @inheritdoc or @override tag, add the // If the implementation used an @inheritdoc or @override tag, add the
// interface's docs, and ignore the existing doclets. // interface's docs, and ignore the existing doclets.
else if ( explicitlyInherits(commentedDoclets[implementationDoclet.longname]) ) { else if (explicitlyInherits(commentedDoclets[implementationDoclet.longname])) {
// Ignore any existing doclets. (This is safe because we only get here if // Ignore any existing doclets. (This is safe because we only get here if
// `implementationDoclet.longname` is an own property of // `implementationDoclet.longname` is an own property of
// `commentedDoclets`.) // `commentedDoclets`.)
addDocletProperty(commentedDoclets[implementationDoclet.longname], 'ignore', addDocletProperty(commentedDoclets[implementationDoclet.longname], 'ignore', true);
true);
updateAddedDoclets(implementationDoclet, additions, additionIndexes); updateAddedDoclets(implementationDoclet, additions, additionIndexes);
updateDocumentedDoclets(implementationDoclet, commentedDoclets); updateDocumentedDoclets(implementationDoclet, commentedDoclets);
updateMemberofDoclets(implementationDoclet, memberof); updateMemberofDoclets(implementationDoclet, memberof);
// Remove property that's no longer accurate. // Remove property that's no longer accurate.
if (implementationDoclet.virtual) { if (implementationDoclet.virtual) {
delete implementationDoclet.virtual; delete implementationDoclet.virtual;
}
// Remove properties that we no longer need.
if (implementationDoclet.inheritdoc) {
delete implementationDoclet.inheritdoc;
}
if (implementationDoclet.override) {
delete implementationDoclet.override;
}
}
// If there's an implementation, and it's documented, update the doclets to
// indicate what the implementation is implementing.
else {
updateImplements(commentedDoclets[implementationDoclet.longname],
parentDoclet.longname);
}
}
} }
// Remove properties that we no longer need.
if (implementationDoclet.inheritdoc) {
delete implementationDoclet.inheritdoc;
}
if (implementationDoclet.override) {
delete implementationDoclet.override;
}
}
// If there's an implementation, and it's documented, update the doclets to
// indicate what the implementation is implementing.
else {
updateImplements(
commentedDoclets[implementationDoclet.longname],
parentDoclet.longname
);
}
} }
}
} }
}
return additions; return additions;
} }
function augment(doclets, propertyName, docletFinder) { function augment(doclets, propertyName, docletFinder) {
const index = doclets.index.longname; const index = doclets.index.longname;
const dependencies = sort( mapDependencies(index, propertyName) ); const dependencies = sort(mapDependencies(index, propertyName));
dependencies.forEach(depName => { dependencies.forEach((depName) => {
const additions = docletFinder(index[depName], doclets, doclets.index); const additions = docletFinder(index[depName], doclets, doclets.index);
additions.forEach(addition => { additions.forEach((addition) => {
const longname = addition.longname; const longname = addition.longname;
if ( !hasOwnProp.call(index, longname) ) { if (!hasOwnProp.call(index, longname)) {
index[longname] = []; index[longname] = [];
} }
index[longname].push(addition); index[longname].push(addition);
doclets.push(addition); doclets.push(addition);
});
}); });
});
} }
/** /**
@ -544,8 +540,8 @@ function augment(doclets, propertyName, docletFinder) {
* @param {!Object} doclets.index - The doclet index. * @param {!Object} doclets.index - The doclet index.
* @return {void} * @return {void}
*/ */
exports.addInherited = doclets => { exports.addInherited = (doclets) => {
augment(doclets, 'augments', getInheritedAdditions); augment(doclets, 'augments', getInheritedAdditions);
}; };
/** /**
@ -563,8 +559,8 @@ exports.addInherited = doclets => {
* @param {!Object} doclets.index - The doclet index. * @param {!Object} doclets.index - The doclet index.
* @return {void} * @return {void}
*/ */
exports.addMixedIn = doclets => { exports.addMixedIn = (doclets) => {
augment(doclets, 'mixes', getMixedInAdditions); augment(doclets, 'mixes', getMixedInAdditions);
}; };
/** /**
@ -584,8 +580,8 @@ exports.addMixedIn = doclets => {
* @param {!Object} doclets.index - The doclet index. * @param {!Object} doclets.index - The doclet index.
* @return {void} * @return {void}
*/ */
exports.addImplemented = doclets => { exports.addImplemented = (doclets) => {
augment(doclets, 'implements', getImplementedAdditions); augment(doclets, 'implements', getImplementedAdditions);
}; };
/** /**
@ -599,10 +595,10 @@ exports.addImplemented = doclets => {
* *
* @return {void} * @return {void}
*/ */
exports.augmentAll = doclets => { exports.augmentAll = (doclets) => {
exports.addMixedIn(doclets); exports.addMixedIn(doclets);
exports.addImplemented(doclets); exports.addImplemented(doclets);
exports.addInherited(doclets); exports.addInherited(doclets);
// look for implemented doclets again, in case we inherited an interface // look for implemented doclets again, in case we inherited an interface
exports.addImplemented(doclets); exports.addImplemented(doclets);
}; };

View File

@ -5,35 +5,34 @@
const _ = require('lodash'); const _ = require('lodash');
const { SCOPE } = require('@jsdoc/core').name; const { SCOPE } = require('@jsdoc/core').name;
function cloneBorrowedDoclets({borrowed, longname}, doclets) { function cloneBorrowedDoclets({ borrowed, longname }, doclets) {
borrowed.forEach(({from, as}) => { borrowed.forEach(({ from, as }) => {
const borrowedDoclets = doclets.index.longname[from]; const borrowedDoclets = doclets.index.longname[from];
let borrowedAs = as || from; let borrowedAs = as || from;
let parts; let parts;
let scopePunc; let scopePunc;
if (borrowedDoclets) { if (borrowedDoclets) {
borrowedAs = borrowedAs.replace(/^prototype\./, SCOPE.PUNC.INSTANCE); borrowedAs = borrowedAs.replace(/^prototype\./, SCOPE.PUNC.INSTANCE);
_.cloneDeep(borrowedDoclets).forEach(clone => { _.cloneDeep(borrowedDoclets).forEach((clone) => {
// TODO: this will fail on longnames like '"Foo#bar".baz' // TODO: this will fail on longnames like '"Foo#bar".baz'
parts = borrowedAs.split(SCOPE.PUNC.INSTANCE); parts = borrowedAs.split(SCOPE.PUNC.INSTANCE);
if (parts.length === 2) { if (parts.length === 2) {
clone.scope = SCOPE.NAMES.INSTANCE; clone.scope = SCOPE.NAMES.INSTANCE;
scopePunc = SCOPE.PUNC.INSTANCE; scopePunc = SCOPE.PUNC.INSTANCE;
} } else {
else { clone.scope = SCOPE.NAMES.STATIC;
clone.scope = SCOPE.NAMES.STATIC; scopePunc = SCOPE.PUNC.STATIC;
scopePunc = SCOPE.PUNC.STATIC;
}
clone.name = parts.pop();
clone.memberof = longname;
clone.longname = clone.memberof + scopePunc + clone.name;
doclets.push(clone);
});
} }
});
clone.name = parts.pop();
clone.memberof = longname;
clone.longname = clone.memberof + scopePunc + clone.name;
doclets.push(clone);
});
}
});
} }
/** /**
@ -42,11 +41,11 @@ function cloneBorrowedDoclets({borrowed, longname}, doclets) {
moving docs from the "borrowed" array and into the general docs, then moving docs from the "borrowed" array and into the general docs, then
deleting the "borrowed" array. deleting the "borrowed" array.
*/ */
exports.resolveBorrows = doclets => { exports.resolveBorrows = (doclets) => {
for (let doclet of doclets.index.borrowed) { for (let doclet of doclets.index.borrowed) {
cloneBorrowedDoclets(doclet, doclets); cloneBorrowedDoclets(doclet, doclets);
delete doclet.borrowed; delete doclet.borrowed;
} }
doclets.index.borrowed = []; doclets.index.borrowed = [];
}; };

File diff suppressed because it is too large Load Diff

View File

@ -5,73 +5,73 @@
* @module jsdoc/env * @module jsdoc/env
*/ */
module.exports = { module.exports = {
/** /**
* The times at which JSDoc started and finished. * The times at which JSDoc started and finished.
* *
* @type {Object} * @type {Object}
* @property {Date} start - The time at which JSDoc started running. * @property {Date} start - The time at which JSDoc started running.
* @property {Date} finish - The time at which JSDoc finished running. * @property {Date} finish - The time at which JSDoc finished running.
*/ */
run: { run: {
start: new Date(), start: new Date(),
finish: null finish: null,
}, },
/** /**
* The command-line arguments passed to JSDoc. * The command-line arguments passed to JSDoc.
* *
* @type {Array<*>} * @type {Array<*>}
*/ */
args: [], args: [],
/** /**
* The data parsed from JSDoc's configuration file. * The data parsed from JSDoc's configuration file.
* *
* @type Object<string, *> * @type Object<string, *>
*/ */
conf: {}, conf: {},
/** /**
* The absolute path to the base directory in which JSDoc is located. Set at startup. * The absolute path to the base directory in which JSDoc is located. Set at startup.
* *
* @private * @private
* @type {string} * @type {string}
*/ */
dirname: null, dirname: null,
/** /**
* The user's working directory at the time when JSDoc started running. * The user's working directory at the time when JSDoc started running.
* *
* @private * @private
* @type {string} * @type {string}
*/ */
pwd: null, pwd: null,
/** /**
* The command-line arguments, parsed into a key/value hash. * The command-line arguments, parsed into a key/value hash.
* *
* @type {Object} * @type {Object}
* @example if (global.env.opts.help) { console.log('Helpful message.'); } * @example if (global.env.opts.help) { console.log('Helpful message.'); }
*/ */
opts: {}, opts: {},
/** /**
* The source files that JSDoc will parse. * The source files that JSDoc will parse.
* *
* @type {Array<string>} * @type {Array<string>}
* @memberof env * @memberof env
*/ */
sourceFiles: [], sourceFiles: [],
/** /**
* The JSDoc version number and revision date. * The JSDoc version number and revision date.
* *
* @type {Object<string, string>} * @type {Object<string, string>}
* @property {string} number - The JSDoc version number. * @property {string} number - The JSDoc version number.
* @property {string} revision - The JSDoc revision number, expressed as a UTC date string. * @property {string} revision - The JSDoc revision number, expressed as a UTC date string.
*/ */
version: { version: {
number: null, number: null,
revision: null revision: null,
} },
}; };

View File

@ -10,13 +10,13 @@ const stripBom = require('strip-bom');
// Collect all of the license information from a `package.json` file. // Collect all of the license information from a `package.json` file.
function getLicenses(packageInfo) { function getLicenses(packageInfo) {
const licenses = packageInfo.licenses ? packageInfo.licenses.slice(0) : []; const licenses = packageInfo.licenses ? packageInfo.licenses.slice(0) : [];
if (packageInfo.license) { if (packageInfo.license) {
licenses.push({ type: packageInfo.license }); licenses.push({ type: packageInfo.license });
} }
return licenses; return licenses;
} }
/** /**
@ -63,195 +63,194 @@ function getLicenses(packageInfo) {
* object may not use the format documented here. * object may not use the format documented here.
*/ */
class Package { class Package {
/**
* @param {string} json - The contents of the `package.json` file.
*/
constructor(json) {
let packageInfo;
/** /**
* @param {string} json - The contents of the `package.json` file. * The string identifier that is shared by all `Package` objects.
*
* @readonly
* @default
* @type {string}
*/ */
constructor(json) { this.kind = 'package';
let packageInfo;
/** try {
* The string identifier that is shared by all `Package` objects. packageInfo = JSON.parse(json ? stripBom(json) : '{}');
* } catch (e) {
* @readonly log.error(`Unable to parse the package file: ${e.message}`);
* @default packageInfo = {};
* @type {string}
*/
this.kind = 'package';
try {
packageInfo = JSON.parse(json ? stripBom(json) : '{}');
}
catch (e) {
log.error(`Unable to parse the package file: ${e.message}`);
packageInfo = {};
}
if (packageInfo.name) {
/**
* The package name.
*
* @type {string}
*/
this.name = packageInfo.name;
}
/**
* The unique longname for this `Package` object.
*
* @type {string}
*/
this.longname = `${this.kind}:${this.name}`;
if (packageInfo.author) {
/**
* The author of this package. Contains either a
* {@link module:jsdoc/package.Package~PersonInfo PersonInfo} object or a string with
* information about the author.
*
* @type {(module:jsdoc/package.Package~PersonInfo|string)}
* @since 3.3.0
*/
this.author = packageInfo.author;
}
if (packageInfo.bugs) {
/**
* Information about where to report bugs in the project. May contain a URL, a string, or an
* object with more detailed information.
*
* @type {(string|module:jsdoc/package.Package~BugInfo)}
* @since 3.3.0
*/
this.bugs = packageInfo.bugs;
}
if (packageInfo.contributors) {
/**
* The contributors to this package.
*
* @type {Array.<(module:jsdoc/package.Package~PersonInfo|string)>}
* @since 3.3.0
*/
this.contributors = packageInfo.contributors;
}
if (packageInfo.dependencies) {
/**
* The dependencies for this package.
*
* @type {Object}
* @since 3.3.0
*/
this.dependencies = packageInfo.dependencies;
}
if (packageInfo.description) {
/**
* A brief description of the package.
*
* @type {string}
*/
this.description = packageInfo.description;
}
if (packageInfo.devDependencies) {
/**
* The development dependencies for this package.
*
* @type {Object}
* @since 3.3.0
*/
this.devDependencies = packageInfo.devDependencies;
}
if (packageInfo.engines) {
/**
* The JavaScript engines that this package supports. Each key is a string that identifies
* the engine (for example, `node`). Each value is a
* [semver](https://www.npmjs.org/doc/misc/semver.html)-compliant version number for the
* engine.
*
* @type {Object}
* @since 3.3.0
*/
this.engines = packageInfo.engines;
}
/**
* The source files associated with the package.
*
* New `Package` objects always contain an empty array, regardless of whether the `package.json`
* file includes a `files` property.
*
* After JSDoc parses your input files, it sets this property to a list of paths to your input
* files.
*
* @type {Array.<string>}
*/
this.files = [];
if (packageInfo.homepage) {
/**
* The URL for the package's homepage.
*
* @type {string}
* @since 3.3.0
*/
this.homepage = packageInfo.homepage;
}
if (packageInfo.keywords) {
/**
* Keywords to help users find the package.
*
* @type {Array.<string>}
* @since 3.3.0
*/
this.keywords = packageInfo.keywords;
}
if (packageInfo.license || packageInfo.licenses) {
/**
* The licenses used by this package. Combines information from the `package.json` file's
* `license` property and the deprecated `licenses` property.
*
* @type {Array.<module:jsdoc/package.Package~LicenseInfo>}
*/
this.licenses = getLicenses(packageInfo);
}
if (packageInfo.main) {
/**
* The module ID that provides the primary entry point to the package. For example, if your
* package is a CommonJS module, and the value of this property is `foo`, users should be
* able to load your module with `require('foo')`.
*
* @type {string}
* @since 3.3.0
*/
this.main = packageInfo.main;
}
if (packageInfo.repository) {
/**
* The version-control repository for the package.
*
* @type {module:jsdoc/package.Package~RepositoryInfo}
* @since 3.3.0
*/
this.repository = packageInfo.repository;
}
if (packageInfo.version) {
/**
* The [semver](https://www.npmjs.org/doc/misc/semver.html)-compliant version number of the
* package.
*
* @type {string}
* @since 3.2.0
*/
this.version = packageInfo.version;
}
} }
if (packageInfo.name) {
/**
* The package name.
*
* @type {string}
*/
this.name = packageInfo.name;
}
/**
* The unique longname for this `Package` object.
*
* @type {string}
*/
this.longname = `${this.kind}:${this.name}`;
if (packageInfo.author) {
/**
* The author of this package. Contains either a
* {@link module:jsdoc/package.Package~PersonInfo PersonInfo} object or a string with
* information about the author.
*
* @type {(module:jsdoc/package.Package~PersonInfo|string)}
* @since 3.3.0
*/
this.author = packageInfo.author;
}
if (packageInfo.bugs) {
/**
* Information about where to report bugs in the project. May contain a URL, a string, or an
* object with more detailed information.
*
* @type {(string|module:jsdoc/package.Package~BugInfo)}
* @since 3.3.0
*/
this.bugs = packageInfo.bugs;
}
if (packageInfo.contributors) {
/**
* The contributors to this package.
*
* @type {Array.<(module:jsdoc/package.Package~PersonInfo|string)>}
* @since 3.3.0
*/
this.contributors = packageInfo.contributors;
}
if (packageInfo.dependencies) {
/**
* The dependencies for this package.
*
* @type {Object}
* @since 3.3.0
*/
this.dependencies = packageInfo.dependencies;
}
if (packageInfo.description) {
/**
* A brief description of the package.
*
* @type {string}
*/
this.description = packageInfo.description;
}
if (packageInfo.devDependencies) {
/**
* The development dependencies for this package.
*
* @type {Object}
* @since 3.3.0
*/
this.devDependencies = packageInfo.devDependencies;
}
if (packageInfo.engines) {
/**
* The JavaScript engines that this package supports. Each key is a string that identifies
* the engine (for example, `node`). Each value is a
* [semver](https://www.npmjs.org/doc/misc/semver.html)-compliant version number for the
* engine.
*
* @type {Object}
* @since 3.3.0
*/
this.engines = packageInfo.engines;
}
/**
* The source files associated with the package.
*
* New `Package` objects always contain an empty array, regardless of whether the `package.json`
* file includes a `files` property.
*
* After JSDoc parses your input files, it sets this property to a list of paths to your input
* files.
*
* @type {Array.<string>}
*/
this.files = [];
if (packageInfo.homepage) {
/**
* The URL for the package's homepage.
*
* @type {string}
* @since 3.3.0
*/
this.homepage = packageInfo.homepage;
}
if (packageInfo.keywords) {
/**
* Keywords to help users find the package.
*
* @type {Array.<string>}
* @since 3.3.0
*/
this.keywords = packageInfo.keywords;
}
if (packageInfo.license || packageInfo.licenses) {
/**
* The licenses used by this package. Combines information from the `package.json` file's
* `license` property and the deprecated `licenses` property.
*
* @type {Array.<module:jsdoc/package.Package~LicenseInfo>}
*/
this.licenses = getLicenses(packageInfo);
}
if (packageInfo.main) {
/**
* The module ID that provides the primary entry point to the package. For example, if your
* package is a CommonJS module, and the value of this property is `foo`, users should be
* able to load your module with `require('foo')`.
*
* @type {string}
* @since 3.3.0
*/
this.main = packageInfo.main;
}
if (packageInfo.repository) {
/**
* The version-control repository for the package.
*
* @type {module:jsdoc/package.Package~RepositoryInfo}
* @since 3.3.0
*/
this.repository = packageInfo.repository;
}
if (packageInfo.version) {
/**
* The [semver](https://www.npmjs.org/doc/misc/semver.html)-compliant version number of the
* package.
*
* @type {string}
* @since 3.2.0
*/
this.version = packageInfo.version;
}
}
} }
exports.Package = Package; exports.Package = Package;

View File

@ -5,31 +5,31 @@
const dictionary = require('jsdoc/tag/dictionary'); const dictionary = require('jsdoc/tag/dictionary');
function addHandlers(handlers, parser) { function addHandlers(handlers, parser) {
Object.keys(handlers).forEach(eventName => { Object.keys(handlers).forEach((eventName) => {
parser.on(eventName, handlers[eventName]); parser.on(eventName, handlers[eventName]);
}); });
} }
exports.installPlugins = (plugins, parser) => { exports.installPlugins = (plugins, parser) => {
let plugin; let plugin;
for (let pluginModule of plugins) { for (let pluginModule of plugins) {
plugin = require(pluginModule); plugin = require(pluginModule);
// allow user-defined plugins to... // allow user-defined plugins to...
// ...register event handlers // ...register event handlers
if (plugin.handlers) { if (plugin.handlers) {
addHandlers(plugin.handlers, parser); addHandlers(plugin.handlers, parser);
}
// ...define tags
if (plugin.defineTags) {
plugin.defineTags(dictionary);
}
// ...add an ESTree node visitor
if (plugin.astNodeVisitor) {
parser.addAstNodeVisitor(plugin.astNodeVisitor);
}
} }
// ...define tags
if (plugin.defineTags) {
plugin.defineTags(dictionary);
}
// ...add an ESTree node visitor
if (plugin.astNodeVisitor) {
parser.addAstNodeVisitor(plugin.astNodeVisitor);
}
}
}; };

File diff suppressed because it is too large Load Diff

View File

@ -4,60 +4,59 @@
const path = require('path'); const path = require('path');
function makeRegExp(config) { function makeRegExp(config) {
let regExp = null; let regExp = null;
if (config) { if (config) {
regExp = (typeof config === 'string') ? new RegExp(config) : config; regExp = typeof config === 'string' ? new RegExp(config) : config;
} }
return regExp; return regExp;
} }
/** /**
* @alias module:jsdoc/src/filter.Filter * @alias module:jsdoc/src/filter.Filter
*/ */
class Filter { class Filter {
/** /**
* @param {Object} opts * @param {Object} opts
* @param {string[]} opts.exclude - Specific files to exclude. * @param {string[]} opts.exclude - Specific files to exclude.
* @param {(string|RegExp)} opts.includePattern * @param {(string|RegExp)} opts.includePattern
* @param {(string|RegExp)} opts.excludePattern * @param {(string|RegExp)} opts.excludePattern
*/ */
constructor({exclude, includePattern, excludePattern}) { constructor({ exclude, includePattern, excludePattern }) {
this._cwd = process.cwd(); this._cwd = process.cwd();
this.exclude = exclude && Array.isArray(exclude) ? this.exclude =
exclude.map($ => path.resolve(this._cwd, $)) : exclude && Array.isArray(exclude) ? exclude.map(($) => path.resolve(this._cwd, $)) : null;
null; this.includePattern = makeRegExp(includePattern);
this.includePattern = makeRegExp(includePattern); this.excludePattern = makeRegExp(excludePattern);
this.excludePattern = makeRegExp(excludePattern); }
/**
* @param {string} filepath - The filepath to check.
* @returns {boolean} Should the given file be included?
*/
isIncluded(filepath) {
let included = true;
filepath = path.resolve(this._cwd, filepath);
if (this.includePattern && !this.includePattern.test(filepath)) {
included = false;
} }
/** if (this.excludePattern && this.excludePattern.test(filepath)) {
* @param {string} filepath - The filepath to check. included = false;
* @returns {boolean} Should the given file be included?
*/
isIncluded(filepath) {
let included = true;
filepath = path.resolve(this._cwd, filepath);
if ( this.includePattern && !this.includePattern.test(filepath) ) {
included = false;
}
if ( this.excludePattern && this.excludePattern.test(filepath) ) {
included = false;
}
if (this.exclude) {
this.exclude.forEach(exclude => {
if ( filepath.indexOf(exclude) === 0 ) {
included = false;
}
});
}
return included;
} }
if (this.exclude) {
this.exclude.forEach((exclude) => {
if (filepath.indexOf(exclude) === 0) {
included = false;
}
});
}
return included;
}
} }
exports.Filter = Filter; exports.Filter = Filter;

View File

@ -10,37 +10,36 @@ const { Syntax } = require('@jsdoc/parse');
let currentModule = null; let currentModule = null;
class CurrentModule { class CurrentModule {
constructor(doclet) { constructor(doclet) {
this.doclet = doclet; this.doclet = doclet;
this.longname = doclet.longname; this.longname = doclet.longname;
this.originalName = doclet.meta.code.name || ''; this.originalName = doclet.meta.code.name || '';
} }
} }
function filterByLongname({longname}) { function filterByLongname({ longname }) {
// you can't document prototypes // you can't document prototypes
if ( /#$/.test(longname) ) { if (/#$/.test(longname)) {
return true; return true;
} }
return false; return false;
} }
function createDoclet(comment, e) { function createDoclet(comment, e) {
let doclet; let doclet;
let flatComment; let flatComment;
let msg; let msg;
try { try {
doclet = new Doclet(comment, e); doclet = new Doclet(comment, e);
} } catch (error) {
catch (error) { flatComment = comment.replace(/[\r\n]/g, '');
flatComment = comment.replace(/[\r\n]/g, ''); msg = `cannot create a doclet for the comment "${flatComment}": ${error.message}`;
msg = `cannot create a doclet for the comment "${flatComment}": ${error.message}`; log.error(msg);
log.error(msg); doclet = new Doclet('', e);
doclet = new Doclet('', e); }
}
return doclet; return doclet;
} }
/** /**
@ -63,301 +62,301 @@ function createDoclet(comment, e) {
* @private * @private
*/ */
function createSymbolDoclet(comment, e) { function createSymbolDoclet(comment, e) {
let doclet = createDoclet(comment, e); let doclet = createDoclet(comment, e);
if (doclet.name) { if (doclet.name) {
// try again, without the comment // try again, without the comment
e.comment = '@undocumented'; e.comment = '@undocumented';
doclet = createDoclet(e.comment, e); doclet = createDoclet(e.comment, e);
} }
return doclet; return doclet;
} }
function setCurrentModule(doclet) { function setCurrentModule(doclet) {
if (doclet.kind === 'module') { if (doclet.kind === 'module') {
currentModule = new CurrentModule(doclet); currentModule = new CurrentModule(doclet);
} }
} }
function setModuleScopeMemberOf(parser, doclet) { function setModuleScopeMemberOf(parser, doclet) {
let parentDoclet; let parentDoclet;
let skipMemberof; let skipMemberof;
// handle module symbols that are _not_ assigned to module.exports // handle module symbols that are _not_ assigned to module.exports
if (currentModule && currentModule.longname !== doclet.name) { if (currentModule && currentModule.longname !== doclet.name) {
if (!doclet.scope) { if (!doclet.scope) {
// is this a method definition? if so, we usually get the scope from the node directly // is this a method definition? if so, we usually get the scope from the node directly
if (doclet.meta && doclet.meta.code && doclet.meta.code.node && if (
doclet.meta.code.node.type === Syntax.MethodDefinition) { doclet.meta &&
// special case for constructors of classes that have @alias tags doclet.meta.code &&
if (doclet.meta.code.node.kind === 'constructor') { doclet.meta.code.node &&
parentDoclet = parser._getDocletById( doclet.meta.code.node.type === Syntax.MethodDefinition
doclet.meta.code.node.parent.parent.nodeId ) {
); // special case for constructors of classes that have @alias tags
if (doclet.meta.code.node.kind === 'constructor') {
parentDoclet = parser._getDocletById(doclet.meta.code.node.parent.parent.nodeId);
if (parentDoclet && parentDoclet.alias) { if (parentDoclet && parentDoclet.alias) {
// the constructor should use the same name as the class // the constructor should use the same name as the class
doclet.addTag('alias', parentDoclet.alias); doclet.addTag('alias', parentDoclet.alias);
doclet.addTag('name', parentDoclet.alias); doclet.addTag('name', parentDoclet.alias);
// and we shouldn't try to set a memberof value // and we shouldn't try to set a memberof value
skipMemberof = true; skipMemberof = true;
} }
} } else if (doclet.meta.code.node.static) {
else if (doclet.meta.code.node.static) { doclet.addTag('static');
doclet.addTag('static'); } else {
} doclet.addTag('instance');
else {
doclet.addTag('instance');
}
}
// is this something that the module exports? if so, it's a static member
else if (doclet.meta && doclet.meta.code && doclet.meta.code.node &&
doclet.meta.code.node.parent &&
doclet.meta.code.node.parent.type === Syntax.ExportNamedDeclaration) {
doclet.addTag('static');
}
// otherwise, it must be an inner member
else {
doclet.addTag('inner');
}
}
// if the doclet isn't a memberof anything yet, and it's not a global, it must be a memberof
// the current module (unless we were told to skip adding memberof)
if (!doclet.memberof && doclet.scope !== SCOPE.NAMES.GLOBAL && !skipMemberof) {
doclet.addTag('memberof', currentModule.longname);
} }
}
// is this something that the module exports? if so, it's a static member
else if (
doclet.meta &&
doclet.meta.code &&
doclet.meta.code.node &&
doclet.meta.code.node.parent &&
doclet.meta.code.node.parent.type === Syntax.ExportNamedDeclaration
) {
doclet.addTag('static');
}
// otherwise, it must be an inner member
else {
doclet.addTag('inner');
}
} }
// if the doclet isn't a memberof anything yet, and it's not a global, it must be a memberof
// the current module (unless we were told to skip adding memberof)
if (!doclet.memberof && doclet.scope !== SCOPE.NAMES.GLOBAL && !skipMemberof) {
doclet.addTag('memberof', currentModule.longname);
}
}
} }
function setDefaultScope(doclet) { function setDefaultScope(doclet) {
// module doclets don't get a default scope // module doclets don't get a default scope
if (!doclet.scope && doclet.kind !== 'module') { if (!doclet.scope && doclet.kind !== 'module') {
doclet.setScope(SCOPE.NAMES.GLOBAL); doclet.setScope(SCOPE.NAMES.GLOBAL);
} }
} }
function addDoclet(parser, newDoclet) { function addDoclet(parser, newDoclet) {
let e; let e;
if (newDoclet) { if (newDoclet) {
setCurrentModule(newDoclet); setCurrentModule(newDoclet);
e = { doclet: newDoclet }; e = { doclet: newDoclet };
parser.emit('newDoclet', e); parser.emit('newDoclet', e);
if ( !e.defaultPrevented && !filterByLongname(e.doclet) ) { if (!e.defaultPrevented && !filterByLongname(e.doclet)) {
parser.addResult(e.doclet); parser.addResult(e.doclet);
}
} }
}
} }
function processAlias(parser, doclet, astNode) { function processAlias(parser, doclet, astNode) {
let memberofName; let memberofName;
if (doclet.alias === '{@thisClass}') { if (doclet.alias === '{@thisClass}') {
memberofName = parser.resolveThis(astNode); memberofName = parser.resolveThis(astNode);
// "class" refers to the owner of the prototype, not the prototype itself // "class" refers to the owner of the prototype, not the prototype itself
if ( /^(.+?)(\.prototype|#)$/.test(memberofName) ) { if (/^(.+?)(\.prototype|#)$/.test(memberofName)) {
memberofName = RegExp.$1; memberofName = RegExp.$1;
}
doclet.alias = memberofName;
} }
doclet.alias = memberofName;
}
doclet.addTag('name', doclet.alias); doclet.addTag('name', doclet.alias);
doclet.postProcess(); doclet.postProcess();
} }
// TODO: separate code that resolves `this` from code that resolves the module object // TODO: separate code that resolves `this` from code that resolves the module object
function findSymbolMemberof(parser, doclet, astNode, nameStartsWith, trailingPunc) { function findSymbolMemberof(parser, doclet, astNode, nameStartsWith, trailingPunc) {
let memberof = ''; let memberof = '';
let nameAndPunc; let nameAndPunc;
let scopePunc = ''; let scopePunc = '';
// handle computed properties like foo['bar'] // handle computed properties like foo['bar']
if (trailingPunc === '[') { if (trailingPunc === '[') {
// we don't know yet whether the symbol is a static or instance member // we don't know yet whether the symbol is a static or instance member
trailingPunc = null; trailingPunc = null;
}
nameAndPunc = nameStartsWith + (trailingPunc || '');
// remove stuff that indicates module membership (but don't touch the name `module.exports`,
// which identifies the module object itself)
if (doclet.name !== 'module.exports') {
doclet.name = doclet.name.replace(nameAndPunc, '');
}
// like `bar` in:
// exports.bar = 1;
// module.exports.bar = 1;
// module.exports = MyModuleObject; MyModuleObject.bar = 1;
if (nameStartsWith !== 'this' && currentModule && doclet.name !== 'module.exports') {
memberof = currentModule.longname;
scopePunc = SCOPE.PUNC.STATIC;
}
// like: module.exports = 1;
else if (doclet.name === 'module.exports' && currentModule) {
doclet.addTag('name', currentModule.longname);
doclet.postProcess();
} else {
memberof = parser.resolveThis(astNode);
// like the following at the top level of a module:
// this.foo = 1;
if (nameStartsWith === 'this' && currentModule && !memberof) {
memberof = currentModule.longname;
scopePunc = SCOPE.PUNC.STATIC;
} else {
scopePunc = SCOPE.PUNC.INSTANCE;
} }
}
nameAndPunc = nameStartsWith + (trailingPunc || ''); return {
memberof: memberof,
// remove stuff that indicates module membership (but don't touch the name `module.exports`, scopePunc: scopePunc,
// which identifies the module object itself) };
if (doclet.name !== 'module.exports') {
doclet.name = doclet.name.replace(nameAndPunc, '');
}
// like `bar` in:
// exports.bar = 1;
// module.exports.bar = 1;
// module.exports = MyModuleObject; MyModuleObject.bar = 1;
if (nameStartsWith !== 'this' && currentModule && doclet.name !== 'module.exports') {
memberof = currentModule.longname;
scopePunc = SCOPE.PUNC.STATIC;
}
// like: module.exports = 1;
else if (doclet.name === 'module.exports' && currentModule) {
doclet.addTag('name', currentModule.longname);
doclet.postProcess();
}
else {
memberof = parser.resolveThis(astNode);
// like the following at the top level of a module:
// this.foo = 1;
if (nameStartsWith === 'this' && currentModule && !memberof) {
memberof = currentModule.longname;
scopePunc = SCOPE.PUNC.STATIC;
}
else {
scopePunc = SCOPE.PUNC.INSTANCE;
}
}
return {
memberof: memberof,
scopePunc: scopePunc
};
} }
function addSymbolMemberof(parser, doclet, astNode) { function addSymbolMemberof(parser, doclet, astNode) {
let basename; let basename;
let memberof; let memberof;
let memberofInfo; let memberofInfo;
let moduleOriginalName = ''; let moduleOriginalName = '';
let resolveTargetRegExp; let resolveTargetRegExp;
let scopePunc; let scopePunc;
let unresolved; let unresolved;
if (!astNode) { if (!astNode) {
return; return;
} }
// check to see if the doclet name is an unresolved reference to the module object, or to `this` // check to see if the doclet name is an unresolved reference to the module object, or to `this`
// TODO: handle cases where the module object is shadowed in the current scope // TODO: handle cases where the module object is shadowed in the current scope
if (currentModule) { if (currentModule) {
moduleOriginalName = `|${currentModule.originalName}`; moduleOriginalName = `|${currentModule.originalName}`;
} }
resolveTargetRegExp = new RegExp(`^((?:module.)?exports|this${moduleOriginalName})(\\.|\\[|$)`); resolveTargetRegExp = new RegExp(`^((?:module.)?exports|this${moduleOriginalName})(\\.|\\[|$)`);
unresolved = resolveTargetRegExp.exec(doclet.name); unresolved = resolveTargetRegExp.exec(doclet.name);
if (unresolved) { if (unresolved) {
memberofInfo = findSymbolMemberof(parser, doclet, astNode, unresolved[1], unresolved[2]); memberofInfo = findSymbolMemberof(parser, doclet, astNode, unresolved[1], unresolved[2]);
memberof = memberofInfo.memberof; memberof = memberofInfo.memberof;
scopePunc = memberofInfo.scopePunc; scopePunc = memberofInfo.scopePunc;
if (memberof) {
doclet.name = doclet.name ?
memberof + scopePunc + doclet.name :
memberof;
}
}
else {
memberofInfo = parser.astnodeToMemberof(astNode);
basename = memberofInfo.basename;
memberof = memberofInfo.memberof;
}
// if we found a memberof name, apply it to the doclet
if (memberof) { if (memberof) {
doclet.addTag('memberof', memberof); doclet.name = doclet.name ? memberof + scopePunc + doclet.name : memberof;
if (basename) {
doclet.name = (doclet.name || '')
.replace(new RegExp(`^${escape(basename)}.`), '');
}
} }
// otherwise, add the defaults for a module (if we're currently in a module) } else {
else { memberofInfo = parser.astnodeToMemberof(astNode);
setModuleScopeMemberOf(parser, doclet); basename = memberofInfo.basename;
memberof = memberofInfo.memberof;
}
// if we found a memberof name, apply it to the doclet
if (memberof) {
doclet.addTag('memberof', memberof);
if (basename) {
doclet.name = (doclet.name || '').replace(new RegExp(`^${escape(basename)}.`), '');
} }
}
// otherwise, add the defaults for a module (if we're currently in a module)
else {
setModuleScopeMemberOf(parser, doclet);
}
} }
function newSymbolDoclet(parser, docletSrc, e) { function newSymbolDoclet(parser, docletSrc, e) {
const newDoclet = createSymbolDoclet(docletSrc, e); const newDoclet = createSymbolDoclet(docletSrc, e);
// if there's an alias, use that as the symbol name // if there's an alias, use that as the symbol name
if (newDoclet.alias) { if (newDoclet.alias) {
processAlias(parser, newDoclet, e.astnode); processAlias(parser, newDoclet, e.astnode);
} }
// otherwise, get the symbol name from the code // otherwise, get the symbol name from the code
else if (e.code && typeof e.code.name !== 'undefined' && e.code.name !== '') { else if (e.code && typeof e.code.name !== 'undefined' && e.code.name !== '') {
newDoclet.addTag('name', e.code.name); newDoclet.addTag('name', e.code.name);
if (!newDoclet.memberof) { if (!newDoclet.memberof) {
addSymbolMemberof(parser, newDoclet, e.astnode); addSymbolMemberof(parser, newDoclet, e.astnode);
}
newDoclet.postProcess();
}
else {
return false;
} }
// set the scope to global unless any of the following are true: newDoclet.postProcess();
// a) the doclet is a memberof something } else {
// b) the doclet represents a module return false;
// c) we're in a module that exports only this symbol }
if ( !newDoclet.memberof && newDoclet.kind !== 'module' &&
(!currentModule || currentModule.longname !== newDoclet.name) ) {
newDoclet.scope = SCOPE.NAMES.GLOBAL;
}
// handle cases where the doclet kind is auto-detected from the node type // set the scope to global unless any of the following are true:
if (e.code.kind && newDoclet.kind === 'member') { // a) the doclet is a memberof something
newDoclet.kind = e.code.kind; // b) the doclet represents a module
} // c) we're in a module that exports only this symbol
if (
!newDoclet.memberof &&
newDoclet.kind !== 'module' &&
(!currentModule || currentModule.longname !== newDoclet.name)
) {
newDoclet.scope = SCOPE.NAMES.GLOBAL;
}
addDoclet(parser, newDoclet); // handle cases where the doclet kind is auto-detected from the node type
e.doclet = newDoclet; if (e.code.kind && newDoclet.kind === 'member') {
newDoclet.kind = e.code.kind;
}
return true; addDoclet(parser, newDoclet);
e.doclet = newDoclet;
return true;
} }
/** /**
* Attach these event handlers to a particular instance of a parser. * Attach these event handlers to a particular instance of a parser.
* @param parser * @param parser
*/ */
exports.attachTo = parser => { exports.attachTo = (parser) => {
// Handle JSDoc "virtual comments" that include one of the following: // Handle JSDoc "virtual comments" that include one of the following:
// + A `@name` tag // + A `@name` tag
// + Another tag that accepts a name, such as `@function` // + Another tag that accepts a name, such as `@function`
parser.on('jsdocCommentFound', e => { parser.on('jsdocCommentFound', (e) => {
const comments = e.comment.split(/@also\b/g); const comments = e.comment.split(/@also\b/g);
let newDoclet; let newDoclet;
for (let i = 0, l = comments.length; i < l; i++) { for (let i = 0, l = comments.length; i < l; i++) {
newDoclet = createDoclet(comments[i], e); newDoclet = createDoclet(comments[i], e);
// we're only interested in virtual comments here // we're only interested in virtual comments here
if (!newDoclet.name) { if (!newDoclet.name) {
continue; continue;
} }
// add the default scope/memberof for a module (if we're in a module) // add the default scope/memberof for a module (if we're in a module)
setModuleScopeMemberOf(parser, newDoclet); setModuleScopeMemberOf(parser, newDoclet);
newDoclet.postProcess(); newDoclet.postProcess();
// if we _still_ don't have a scope, use the default // if we _still_ don't have a scope, use the default
setDefaultScope(newDoclet); setDefaultScope(newDoclet);
addDoclet(parser, newDoclet); addDoclet(parser, newDoclet);
e.doclet = newDoclet; e.doclet = newDoclet;
} }
}); });
// Handle named symbols in the code. May or may not have a JSDoc comment attached. // Handle named symbols in the code. May or may not have a JSDoc comment attached.
parser.on('symbolFound', e => { parser.on('symbolFound', (e) => {
const comments = e.comment.split(/@also\b/g); const comments = e.comment.split(/@also\b/g);
for (let i = 0, l = comments.length; i < l; i++) { for (let i = 0, l = comments.length; i < l; i++) {
newSymbolDoclet(parser, comments[i], e); newSymbolDoclet(parser, comments[i], e);
} }
}); });
parser.on('fileComplete', () => { parser.on('fileComplete', () => {
currentModule = null; currentModule = null;
}); });
}; };

File diff suppressed because it is too large Load Diff

View File

@ -11,54 +11,52 @@ const { statSync } = require('fs');
* @extends module:events.EventEmitter * @extends module:events.EventEmitter
*/ */
class Scanner extends EventEmitter { class Scanner extends EventEmitter {
constructor() { constructor() {
super(); super();
} }
/** /**
* Recursively searches the given searchPaths for js files. * Recursively searches the given searchPaths for js files.
* @param {Array.<string>} searchPaths * @param {Array.<string>} searchPaths
* @param {number} [depth] * @param {number} [depth]
* @fires sourceFileFound * @fires sourceFileFound
*/ */
scan(searchPaths, depth, filter) { scan(searchPaths, depth, filter) {
let currentFile; let currentFile;
let filePaths = []; let filePaths = [];
searchPaths = searchPaths || []; searchPaths = searchPaths || [];
depth = depth || 1; depth = depth || 1;
searchPaths.forEach($ => { searchPaths.forEach(($) => {
const filepath = path.resolve(process.cwd(), decodeURIComponent($)); const filepath = path.resolve(process.cwd(), decodeURIComponent($));
try { try {
currentFile = statSync(filepath); currentFile = statSync(filepath);
} } catch (e) {
catch (e) { log.error(`Unable to find the source file or directory ${filepath}`);
log.error(`Unable to find the source file or directory ${filepath}`);
return; return;
} }
if ( currentFile.isFile() ) { if (currentFile.isFile()) {
filePaths.push(filepath); filePaths.push(filepath);
} } else {
else { filePaths = filePaths.concat(lsSync(filepath, depth));
filePaths = filePaths.concat(lsSync(filepath, depth)); }
} });
});
filePaths = filePaths.filter($ => filter.isIncluded($)); filePaths = filePaths.filter(($) => filter.isIncluded($));
filePaths = filePaths.filter($ => { filePaths = filePaths.filter(($) => {
const e = { fileName: $ }; const e = { fileName: $ };
this.emit('sourceFileFound', e); this.emit('sourceFileFound', e);
return !e.defaultPrevented; return !e.defaultPrevented;
}); });
return filePaths; return filePaths;
} }
} }
exports.Scanner = Scanner; exports.Scanner = Scanner;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -6,115 +6,115 @@ const env = require('jsdoc/env');
const { log } = require('@jsdoc/util'); const { log } = require('@jsdoc/util');
const path = require('path'); const path = require('path');
const tag = { const tag = {
dictionary: require('jsdoc/tag/dictionary'), dictionary: require('jsdoc/tag/dictionary'),
validator: require('jsdoc/tag/validator'), validator: require('jsdoc/tag/validator'),
type: require('@jsdoc/tag').type type: require('@jsdoc/tag').type,
}; };
// Check whether the text is the same as a symbol name with leading or trailing whitespace. If so, // Check whether the text is the same as a symbol name with leading or trailing whitespace. If so,
// the whitespace must be preserved, and the text cannot be trimmed. // the whitespace must be preserved, and the text cannot be trimmed.
function mustPreserveWhitespace(text, meta) { function mustPreserveWhitespace(text, meta) {
return meta && meta.code && meta.code.name === text && text.match(/(?:^\s+)|(?:\s+$)/); return meta && meta.code && meta.code.name === text && text.match(/(?:^\s+)|(?:\s+$)/);
} }
function trim(text, opts, meta) { function trim(text, opts, meta) {
let indentMatcher; let indentMatcher;
let match; let match;
opts = opts || {}; opts = opts || {};
text = String(typeof text === 'undefined' ? '' : text); text = String(typeof text === 'undefined' ? '' : text);
if ( mustPreserveWhitespace(text, meta) ) { if (mustPreserveWhitespace(text, meta)) {
text = `"${text}"`; text = `"${text}"`;
} } else if (opts.keepsWhitespace) {
else if (opts.keepsWhitespace) { text = text.replace(/^[\n\r\f]+|[\n\r\f]+$/g, '');
text = text.replace(/^[\n\r\f]+|[\n\r\f]+$/g, ''); if (opts.removesIndent) {
if (opts.removesIndent) { match = text.match(/^([ \t]+)/);
match = text.match(/^([ \t]+)/); if (match && match[1]) {
if (match && match[1]) { indentMatcher = new RegExp(`^${match[1]}`, 'gm');
indentMatcher = new RegExp(`^${match[1]}`, 'gm'); text = text.replace(indentMatcher, '');
text = text.replace(indentMatcher, ''); }
}
}
}
else {
text = text.replace(/^\s+|\s+$/g, '');
} }
} else {
text = text.replace(/^\s+|\s+$/g, '');
}
return text; return text;
} }
function addHiddenProperty(obj, propName, propValue) { function addHiddenProperty(obj, propName, propValue) {
Object.defineProperty(obj, propName, { Object.defineProperty(obj, propName, {
value: propValue, value: propValue,
writable: true, writable: true,
enumerable: Boolean(env.opts.debug), enumerable: Boolean(env.opts.debug),
configurable: true configurable: true,
}); });
} }
function parseType({text, originalTitle}, {canHaveName, canHaveType}, meta) { function parseType({ text, originalTitle }, { canHaveName, canHaveType }, meta) {
try { try {
return tag.type.parse(text, canHaveName, canHaveType); return tag.type.parse(text, canHaveName, canHaveType);
} } catch (e) {
catch (e) { log.error(
log.error( 'Unable to parse a tag\'s type expression%s with tag title "%s" and text "%s": %s',
'Unable to parse a tag\'s type expression%s with tag title "%s" and text "%s": %s', meta.filename
meta.filename ? ( ` for source file ${path.join(meta.path, meta.filename)}${meta.lineno ? (` in line ${meta.lineno}`) : ''}` ) : '', ? ` for source file ${path.join(meta.path, meta.filename)}${
originalTitle, meta.lineno ? ` in line ${meta.lineno}` : ''
text, }`
e.message : '',
); originalTitle,
text,
e.message
);
return {}; return {};
} }
} }
function processTagText(tagInstance, tagDef, meta) { function processTagText(tagInstance, tagDef, meta) {
let tagType; let tagType;
if (tagDef.onTagText) { if (tagDef.onTagText) {
tagInstance.text = tagDef.onTagText(tagInstance.text); tagInstance.text = tagDef.onTagText(tagInstance.text);
}
if (tagDef.canHaveType || tagDef.canHaveName) {
/** The value property represents the result of parsing the tag text. */
tagInstance.value = {};
tagType = parseType(tagInstance, tagDef, meta);
// It is possible for a tag to *not* have a type but still have
// optional or defaultvalue, e.g. '@param [foo]'.
// Although tagType.type.length == 0 we should still copy the other properties.
if (tagType.type) {
if (tagType.type.length) {
tagInstance.value.type = {
names: tagType.type,
};
addHiddenProperty(tagInstance.value.type, 'parsedType', tagType.parsedType);
}
['optional', 'nullable', 'variable', 'defaultvalue'].forEach((prop) => {
if (typeof tagType[prop] !== 'undefined') {
tagInstance.value[prop] = tagType[prop];
}
});
} }
if (tagDef.canHaveType || tagDef.canHaveName) { if (tagType.text && tagType.text.length) {
/** The value property represents the result of parsing the tag text. */ tagInstance.value.description = tagType.text;
tagInstance.value = {};
tagType = parseType(tagInstance, tagDef, meta);
// It is possible for a tag to *not* have a type but still have
// optional or defaultvalue, e.g. '@param [foo]'.
// Although tagType.type.length == 0 we should still copy the other properties.
if (tagType.type) {
if (tagType.type.length) {
tagInstance.value.type = {
names: tagType.type
};
addHiddenProperty(tagInstance.value.type, 'parsedType', tagType.parsedType);
}
['optional', 'nullable', 'variable', 'defaultvalue'].forEach(prop => {
if (typeof tagType[prop] !== 'undefined') {
tagInstance.value[prop] = tagType[prop];
}
});
}
if (tagType.text && tagType.text.length) {
tagInstance.value.description = tagType.text;
}
if (tagDef.canHaveName) {
// note the dash is a special case: as a param name it means "no name"
if (tagType.name && tagType.name !== '-') {
tagInstance.value.name = tagType.name;
}
}
} }
else {
tagInstance.value = tagInstance.text; if (tagDef.canHaveName) {
// note the dash is a special case: as a param name it means "no name"
if (tagType.name && tagType.name !== '-') {
tagInstance.value.name = tagType.name;
}
} }
} else {
tagInstance.value = tagInstance.text;
}
} }
/** /**
@ -127,62 +127,62 @@ function processTagText(tagInstance, tagDef, meta) {
* @param {module:jsdoc/tag/dictionary.Dictionary} dict - The new tag dictionary. * @param {module:jsdoc/tag/dictionary.Dictionary} dict - The new tag dictionary.
*/ */
exports._replaceDictionary = function _replaceDictionary(dict) { exports._replaceDictionary = function _replaceDictionary(dict) {
tag.dictionary = dict; tag.dictionary = dict;
}; };
/** /**
* Represents a single doclet tag. * Represents a single doclet tag.
*/ */
class Tag { class Tag {
/**
* Constructs a new tag object. Calls the tag validator.
*
* @param {string} tagTitle
* @param {string=} tagBody
* @param {object=} meta
*/
constructor(tagTitle, tagBody, meta) {
let tagDef;
let trimOpts;
meta = meta || {};
this.originalTitle = trim(tagTitle);
/** The title of the tag (for example, `title` in `@title text`). */
this.title = tag.dictionary.normalize(this.originalTitle);
tagDef = tag.dictionary.lookUp(this.title);
trimOpts = {
keepsWhitespace: tagDef.keepsWhitespace,
removesIndent: tagDef.removesIndent,
};
/** /**
* Constructs a new tag object. Calls the tag validator. * The text following the tag (for example, `text` in `@title text`).
* *
* @param {string} tagTitle * Whitespace is trimmed from the tag text as follows:
* @param {string=} tagBody *
* @param {object=} meta * + If the tag's `keepsWhitespace` option is falsy, all leading and trailing whitespace are
* removed.
* + If the tag's `keepsWhitespace` option is set to `true`, leading and trailing whitespace are
* not trimmed, unless the `removesIndent` option is also enabled.
* + If the tag's `removesIndent` option is set to `true`, any indentation that is shared by
* every line in the string is removed. This option is ignored unless `keepsWhitespace` is set
* to `true`.
*
* **Note**: If the tag text is the name of a symbol, and the symbol's name includes leading or
* trailing whitespace (for example, the property names in `{ ' ': true, ' foo ': false }`),
* the tag text is not trimmed. Instead, the tag text is wrapped in double quotes to prevent the
* whitespace from being trimmed.
*/ */
constructor(tagTitle, tagBody, meta) { this.text = trim(tagBody, trimOpts, meta);
let tagDef;
let trimOpts;
meta = meta || {}; if (this.text) {
processTagText(this, tagDef, meta);
this.originalTitle = trim(tagTitle);
/** The title of the tag (for example, `title` in `@title text`). */
this.title = tag.dictionary.normalize(this.originalTitle);
tagDef = tag.dictionary.lookUp(this.title);
trimOpts = {
keepsWhitespace: tagDef.keepsWhitespace,
removesIndent: tagDef.removesIndent
};
/**
* The text following the tag (for example, `text` in `@title text`).
*
* Whitespace is trimmed from the tag text as follows:
*
* + If the tag's `keepsWhitespace` option is falsy, all leading and trailing whitespace are
* removed.
* + If the tag's `keepsWhitespace` option is set to `true`, leading and trailing whitespace are
* not trimmed, unless the `removesIndent` option is also enabled.
* + If the tag's `removesIndent` option is set to `true`, any indentation that is shared by
* every line in the string is removed. This option is ignored unless `keepsWhitespace` is set
* to `true`.
*
* **Note**: If the tag text is the name of a symbol, and the symbol's name includes leading or
* trailing whitespace (for example, the property names in `{ ' ': true, ' foo ': false }`),
* the tag text is not trimmed. Instead, the tag text is wrapped in double quotes to prevent the
* whitespace from being trimmed.
*/
this.text = trim(tagBody, trimOpts, meta);
if (this.text) {
processTagText(this, tagDef, meta);
}
tag.validator.validate(this, tagDef, meta);
} }
tag.validator.validate(this, tagDef, meta);
}
} }
exports.Tag = Tag; exports.Tag = Tag;

View File

@ -6,168 +6,171 @@ const { log } = require('@jsdoc/util');
const hasOwnProp = Object.prototype.hasOwnProperty; const hasOwnProp = Object.prototype.hasOwnProperty;
const DEFINITIONS = { const DEFINITIONS = {
closure: 'closureTags', closure: 'closureTags',
jsdoc: 'jsdocTags' jsdoc: 'jsdocTags',
}; };
let dictionary; let dictionary;
/** @private */ /** @private */
class TagDefinition { class TagDefinition {
constructor(dict, title, etc) { constructor(dict, title, etc) {
const self = this; const self = this;
etc = etc || {}; etc = etc || {};
this.title = dict.normalize(title); this.title = dict.normalize(title);
Object.defineProperty(this, '_dictionary', { Object.defineProperty(this, '_dictionary', {
value: dict value: dict,
}); });
Object.keys(etc).forEach(p => { Object.keys(etc).forEach((p) => {
self[p] = etc[p]; self[p] = etc[p];
}); });
} }
/** @private */ /** @private */
synonym(synonymName) { synonym(synonymName) {
this._dictionary.defineSynonym(this.title, synonymName); this._dictionary.defineSynonym(this.title, synonymName);
return this; return this;
} }
} }
/** /**
* @alias module:jsdoc/tag/dictionary.Dictionary * @alias module:jsdoc/tag/dictionary.Dictionary
*/ */
class Dictionary { class Dictionary {
constructor() { constructor() {
// TODO: Consider adding internal tags in the constructor, ideally as fallbacks that aren't // TODO: Consider adding internal tags in the constructor, ideally as fallbacks that aren't
// used to confirm whether a tag is defined/valid, rather than requiring every set of tag // used to confirm whether a tag is defined/valid, rather than requiring every set of tag
// definitions to contain the internal tags. // definitions to contain the internal tags.
this._tags = {}; this._tags = {};
this._tagSynonyms = {}; this._tagSynonyms = {};
// The longnames for `Package` objects include a `package` namespace. There's no `package` // The longnames for `Package` objects include a `package` namespace. There's no `package`
// tag, though, so we declare the namespace here. // tag, though, so we declare the namespace here.
// TODO: Consider making this a fallback as suggested above for internal tags. // TODO: Consider making this a fallback as suggested above for internal tags.
this._namespaces = ['package']; this._namespaces = ['package'];
}
_defineNamespace(title) {
title = this.normalize(title || '');
if (title && !this._namespaces.includes(title)) {
this._namespaces.push(title);
} }
_defineNamespace(title) { return this;
title = this.normalize(title || ''); }
if (title && !this._namespaces.includes(title)) { defineTag(title, opts) {
this._namespaces.push(title); const tagDef = new TagDefinition(this, title, opts);
}
return this; this._tags[tagDef.title] = tagDef;
if (tagDef.isNamespace) {
this._defineNamespace(tagDef.title);
}
if (tagDef.synonyms) {
tagDef.synonyms.forEach((synonym) => {
this.defineSynonym(title, synonym);
});
} }
defineTag(title, opts) { return this._tags[tagDef.title];
const tagDef = new TagDefinition(this, title, opts); }
this._tags[tagDef.title] = tagDef; defineTags(tagDefs) {
const tags = {};
if (tagDef.isNamespace) { for (const title of Object.keys(tagDefs)) {
this._defineNamespace(tagDef.title); tags[title] = this.defineTag(title, tagDefs[title]);
}
if (tagDef.synonyms) {
tagDef.synonyms.forEach(synonym => {
this.defineSynonym(title, synonym);
});
}
return this._tags[tagDef.title];
} }
defineTags(tagDefs) { return tags;
const tags = {}; }
for (const title of Object.keys(tagDefs)) { defineSynonym(title, synonym) {
tags[title] = this.defineTag(title, tagDefs[title]); this._tagSynonyms[synonym.toLowerCase()] = this.normalize(title);
} }
return tags; static fromConfig(env) {
} let dictionaries = env.conf.tags.dictionaries;
const dict = new Dictionary();
defineSynonym(title, synonym) { if (!dictionaries) {
this._tagSynonyms[synonym.toLowerCase()] = this.normalize(title); log.error(
} 'The configuration setting "tags.dictionaries" is undefined. ' +
'Unable to load tag definitions.'
);
} else {
dictionaries
.slice()
.reverse()
.forEach((dictName) => {
const tagDefs = definitions[DEFINITIONS[dictName]];
static fromConfig(env) { if (!tagDefs) {
let dictionaries = env.conf.tags.dictionaries;
const dict = new Dictionary();
if (!dictionaries) {
log.error( log.error(
'The configuration setting "tags.dictionaries" is undefined. ' + 'The configuration setting "tags.dictionaries" contains ' +
'Unable to load tag definitions.' `the unknown dictionary name ${dictName}. Ignoring the dictionary.`
); );
} else {
dictionaries.slice().reverse().forEach(dictName => {
const tagDefs = definitions[DEFINITIONS[dictName]];
if (!tagDefs) { return;
log.error( }
'The configuration setting "tags.dictionaries" contains ' +
`the unknown dictionary name ${dictName}. Ignoring the dictionary.`
);
return; dict.defineTags(tagDefs);
} });
dict.defineTags(tagDefs); dict.defineTags(definitions.internalTags);
});
dict.defineTags(definitions.internalTags);
}
return dict;
} }
getNamespaces() { return dict;
return this._namespaces.slice(); }
getNamespaces() {
return this._namespaces.slice();
}
isNamespace(kind) {
if (kind) {
kind = this.normalize(kind);
if (this._namespaces.includes(kind)) {
return true;
}
} }
isNamespace(kind) { return false;
if (kind) { }
kind = this.normalize(kind);
if (this._namespaces.includes(kind)) {
return true;
}
}
return false; lookup(title) {
title = this.normalize(title);
if (hasOwnProp.call(this._tags, title)) {
return this._tags[title];
} }
lookup(title) { return false;
title = this.normalize(title); }
if ( hasOwnProp.call(this._tags, title) ) { lookUp(title) {
return this._tags[title]; return this.lookup(title);
} }
return false; normalise(title) {
return this.normalize(title);
}
normalize(title) {
const canonicalName = title.toLowerCase();
if (hasOwnProp.call(this._tagSynonyms, canonicalName)) {
return this._tagSynonyms[canonicalName];
} }
lookUp(title) { return canonicalName;
return this.lookup(title); }
}
normalise(title) {
return this.normalize(title);
}
normalize(title) {
const canonicalName = title.toLowerCase();
if ( hasOwnProp.call(this._tagSynonyms, canonicalName) ) {
return this._tagSynonyms[canonicalName];
}
return canonicalName;
}
} }
// initialize the default dictionary // initialize the default dictionary

File diff suppressed because it is too large Load Diff

View File

@ -5,47 +5,47 @@
const env = require('jsdoc/env'); const env = require('jsdoc/env');
const { log } = require('@jsdoc/util'); const { log } = require('@jsdoc/util');
function buildMessage(tagName, {filename, lineno, comment}, desc) { function buildMessage(tagName, { filename, lineno, comment }, desc) {
let result = `The @${tagName} tag ${desc}. File: ${filename}, line: ${lineno}`; let result = `The @${tagName} tag ${desc}. File: ${filename}, line: ${lineno}`;
if (comment) { if (comment) {
result += `\n${comment}`; result += `\n${comment}`;
} }
return result; return result;
} }
/** /**
* Validate the given tag. * Validate the given tag.
*/ */
exports.validate = ({title, text, value}, tagDef, meta) => { exports.validate = ({ title, text, value }, tagDef, meta) => {
const allowUnknownTags = env.conf.tags.allowUnknownTags; const allowUnknownTags = env.conf.tags.allowUnknownTags;
// handle cases where the tag definition does not exist // handle cases where the tag definition does not exist
if (!tagDef) { if (!tagDef) {
// log an error if unknown tags are not allowed // log an error if unknown tags are not allowed
if (!allowUnknownTags || if (
(Array.isArray(allowUnknownTags) && !allowUnknownTags ||
!allowUnknownTags.includes(title))) { (Array.isArray(allowUnknownTags) && !allowUnknownTags.includes(title))
log.error(buildMessage(title, meta, 'is not a known tag')); ) {
} log.error(buildMessage(title, meta, 'is not a known tag'));
// stop validation, since there's nothing to validate against
return;
} }
// check for errors that make the tag useless // stop validation, since there's nothing to validate against
if (!text && tagDef.mustHaveValue) { return;
log.error(buildMessage(title, meta, 'requires a value')); }
}
// check for minor issues that are usually harmless // check for errors that make the tag useless
else if (text && tagDef.mustNotHaveValue) { if (!text && tagDef.mustHaveValue) {
log.warn(buildMessage(title, meta, log.error(buildMessage(title, meta, 'requires a value'));
'does not permit a value; the value will be ignored')); }
}
else if (value && value.description && tagDef.mustNotHaveDescription) { // check for minor issues that are usually harmless
log.warn(buildMessage(title, meta, else if (text && tagDef.mustNotHaveValue) {
'does not permit a description; the description will be ignored')); log.warn(buildMessage(title, meta, 'does not permit a value; the value will be ignored'));
} } else if (value && value.description && tagDef.mustNotHaveDescription) {
log.warn(
buildMessage(title, meta, 'does not permit a description; the description will be ignored')
);
}
}; };

View File

@ -10,71 +10,71 @@ const path = require('path');
* Template helper. * Template helper.
*/ */
class Template { class Template {
/** /**
* @param {string} filepath - Templates directory. * @param {string} filepath - Templates directory.
*/ */
constructor(filepath) { constructor(filepath) {
this.path = filepath; this.path = filepath;
this.layout = null; this.layout = null;
this.cache = {}; this.cache = {};
// override default template tag settings // override default template tag settings
this.settings = { this.settings = {
evaluate: /<\?js([\s\S]+?)\?>/g, evaluate: /<\?js([\s\S]+?)\?>/g,
interpolate: /<\?js=([\s\S]+?)\?>/g, interpolate: /<\?js=([\s\S]+?)\?>/g,
escape: /<\?js~([\s\S]+?)\?>/g escape: /<\?js~([\s\S]+?)\?>/g,
}; };
}
/**
* Loads template from given file.
* @param {string} file - Template filename.
* @return {function} Returns template closure.
*/
load(file) {
return _.template(fs.readFileSync(file, 'utf8'), this.settings);
}
/**
* Renders template using given data.
*
* This is low-level function, for rendering full templates use {@link Template.render()}.
*
* @param {string} file - Template filename.
* @param {object} data - Template variables (doesn't have to be object, but passing variables dictionary is best way and most common use).
* @return {string} Rendered template.
*/
partial(file, data) {
file = path.resolve(this.path, file);
// load template into cache
if (!(file in this.cache)) {
this.cache[file] = this.load(file);
} }
/** // keep template helper context
* Loads template from given file. return this.cache[file].call(this, data);
* @param {string} file - Template filename. }
* @return {function} Returns template closure.
*/ /**
load(file) { * Renders template with given data.
return _.template(fs.readFileSync(file, 'utf8'), this.settings); *
* This method automaticaly applies layout if set.
*
* @param {string} file - Template filename.
* @param {object} data - Template variables (doesn't have to be object, but passing variables dictionary is best way and most common use).
* @return {string} Rendered template.
*/
render(file, data) {
// main content
let content = this.partial(file, data);
// apply layout
if (this.layout) {
data.content = content;
content = this.partial(this.layout, data);
} }
/** return content;
* Renders template using given data. }
*
* This is low-level function, for rendering full templates use {@link Template.render()}.
*
* @param {string} file - Template filename.
* @param {object} data - Template variables (doesn't have to be object, but passing variables dictionary is best way and most common use).
* @return {string} Rendered template.
*/
partial(file, data) {
file = path.resolve(this.path, file);
// load template into cache
if (!(file in this.cache)) {
this.cache[file] = this.load(file);
}
// keep template helper context
return this.cache[file].call(this, data);
}
/**
* Renders template with given data.
*
* This method automaticaly applies layout if set.
*
* @param {string} file - Template filename.
* @param {object} data - Template variables (doesn't have to be object, but passing variables dictionary is best way and most common use).
* @return {string} Rendered template.
*/
render(file, data) {
// main content
let content = this.partial(file, data);
// apply layout
if (this.layout) {
data.content = content;
content = this.partial(this.layout, data);
}
return content;
}
} }
exports.Template = Template; exports.Template = Template;

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,134 @@
{ {
"name": "jsdoc", "name": "jsdoc",
"version": "4.0.0-dev.16", "version": "4.0.0-dev.16",
"lockfileVersion": 1, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": {
"": {
"name": "jsdoc",
"version": "4.0.0-dev.16",
"license": "Apache-2.0",
"dependencies": {
"@babel/parser": "^7.15.7",
"bluebird": "^3.7.2",
"catharsis": "^0.9.0",
"code-prettify": "^0.1.0",
"color-themes-for-google-code-prettify": "^2.0.4",
"common-path-prefix": "^3.0.0",
"escape-string-regexp": "^4.0.0",
"lodash": "^4.17.21",
"open-sans-fonts": "^1.6.2",
"requizzle": "^0.2.3",
"strip-bom": "^4.0.0",
"strip-json-comments": "^3.1.1",
"taffydb": "2.6.2"
},
"bin": {
"jsdoc": "jsdoc.js"
},
"engines": {
"node": ">=v14.17.6"
}
},
"node_modules/@babel/parser": {
"version": "7.15.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.7.tgz",
"integrity": "sha512-rycZXvQ+xS9QyIcJ9HXeDWf1uxqlbVFAUq0Rq0dbc50Zb/+wUe/ehyfzGfm9KZZF0kBejYgxltBXocP+gKdL2g==",
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
},
"node_modules/catharsis": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz",
"integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==",
"dependencies": {
"lodash": "^4.17.15"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/code-prettify": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/code-prettify/-/code-prettify-0.1.0.tgz",
"integrity": "sha1-RocMyMGlDQm61TmzOpg9vUqjSx4="
},
"node_modules/color-themes-for-google-code-prettify": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/color-themes-for-google-code-prettify/-/color-themes-for-google-code-prettify-2.0.4.tgz",
"integrity": "sha1-3urPZX/WhXaGR1TU5IbXjf2x54Q=",
"engines": {
"node": ">=5.9.0"
}
},
"node_modules/common-path-prefix": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
"integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w=="
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/open-sans-fonts": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/open-sans-fonts/-/open-sans-fonts-1.6.2.tgz",
"integrity": "sha512-vsJ6/Mm0TdUKQJqxfkXJy+0K2X0QeRuTmxQq9YE1ycziw6CbDPolDsHhQ6+ImoV/7OTh8K8ZTGklY1Z5nUAwug=="
},
"node_modules/requizzle": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz",
"integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==",
"dependencies": {
"lodash": "^4.17.14"
}
},
"node_modules/strip-bom": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
"integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/taffydb": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz",
"integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg="
}
},
"dependencies": { "dependencies": {
"@babel/parser": { "@babel/parser": {
"version": "7.15.7", "version": "7.15.7",

View File

@ -5,17 +5,17 @@
* @module plugins/commentConvert * @module plugins/commentConvert
*/ */
exports.handlers = { exports.handlers = {
/// ///
/// Convert ///-style comments into jsdoc comments. /// Convert ///-style comments into jsdoc comments.
/// @param e /// @param e
/// @param e.filename /// @param e.filename
/// @param e.source /// @param e.source
/// ///
beforeParse(e) { beforeParse(e) {
e.source = e.source.replace(/(\n[ \t]*\/\/\/[^\n]*)+/g, $ => { e.source = e.source.replace(/(\n[ \t]*\/\/\/[^\n]*)+/g, ($) => {
const replacement = `\n/**${$.replace(/^[ \t]*\/\/\//mg, '').replace(/(\n$|$)/, '*/$1')}`; const replacement = `\n/**${$.replace(/^[ \t]*\/\/\//gm, '').replace(/(\n$|$)/, '*/$1')}`;
return replacement; return replacement;
}); });
} },
}; };

View File

@ -4,14 +4,14 @@
* @module plugins/commentsOnly * @module plugins/commentsOnly
*/ */
exports.handlers = { exports.handlers = {
beforeParse(e) { beforeParse(e) {
// a JSDoc comment looks like: /**[one or more chars]*/ // a JSDoc comment looks like: /**[one or more chars]*/
const comments = e.source.match(/\/\*\*[\s\S]+?\*\//g); const comments = e.source.match(/\/\*\*[\s\S]+?\*\//g);
if (comments) { if (comments) {
e.source = comments.join('\n\n'); e.source = comments.join('\n\n');
} else { } else {
e.source = ''; // If file has no comments, parser should still receive no code e.source = ''; // If file has no comments, parser should still receive no code
}
} }
},
}; };

View File

@ -4,15 +4,15 @@
* @module plugins/escapeHtml * @module plugins/escapeHtml
*/ */
exports.handlers = { exports.handlers = {
/** /**
* Translate HTML tags in descriptions into safe entities. Replaces <, & and newlines * Translate HTML tags in descriptions into safe entities. Replaces <, & and newlines
*/ */
newDoclet({doclet}) { newDoclet({ doclet }) {
if (doclet.description) { if (doclet.description) {
doclet.description = doclet.description doclet.description = doclet.description
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
.replace(/\r\n|\n|\r/g, '<br>'); .replace(/\r\n|\n|\r/g, '<br>');
}
} }
},
}; };

View File

@ -10,20 +10,20 @@ const conf = env.conf.eventDumper || {};
// Dump the included parser events (defaults to all events) // Dump the included parser events (defaults to all events)
let events = conf.include || [ let events = conf.include || [
'parseBegin', 'parseBegin',
'fileBegin', 'fileBegin',
'beforeParse', 'beforeParse',
'jsdocCommentFound', 'jsdocCommentFound',
'symbolFound', 'symbolFound',
'newDoclet', 'newDoclet',
'fileComplete', 'fileComplete',
'parseComplete', 'parseComplete',
'processingComplete' 'processingComplete',
]; ];
// Don't dump the excluded parser events // Don't dump the excluded parser events
if (conf.exclude) { if (conf.exclude) {
events = _.difference(events, conf.exclude); events = _.difference(events, conf.exclude);
} }
/** /**
@ -33,25 +33,25 @@ if (conf.exclude) {
* @return {Object} The modified object. * @return {Object} The modified object.
*/ */
function replaceNodeObjects(o) { function replaceNodeObjects(o) {
const OBJECT_PLACEHOLDER = '<Object>'; const OBJECT_PLACEHOLDER = '<Object>';
if (o.code && o.code.node) { if (o.code && o.code.node) {
// don't break the original object! // don't break the original object!
o.code = _.cloneDeep(o.code); o.code = _.cloneDeep(o.code);
o.code.node = OBJECT_PLACEHOLDER; o.code.node = OBJECT_PLACEHOLDER;
} }
if (o.doclet && o.doclet.meta && o.doclet.meta.code && o.doclet.meta.code.node) { if (o.doclet && o.doclet.meta && o.doclet.meta.code && o.doclet.meta.code.node) {
// don't break the original object! // don't break the original object!
o.doclet.meta.code = _.cloneDeep(o.doclet.meta.code); o.doclet.meta.code = _.cloneDeep(o.doclet.meta.code);
o.doclet.meta.code.node = OBJECT_PLACEHOLDER; o.doclet.meta.code.node = OBJECT_PLACEHOLDER;
} }
if (o.astnode) { if (o.astnode) {
o.astnode = OBJECT_PLACEHOLDER; o.astnode = OBJECT_PLACEHOLDER;
} }
return o; return o;
} }
/** /**
@ -61,35 +61,43 @@ function replaceNodeObjects(o) {
* @return {object} The fixed-up object. * @return {object} The fixed-up object.
*/ */
function cleanse(e) { function cleanse(e) {
let result = {}; let result = {};
Object.keys(e).forEach(prop => { Object.keys(e).forEach((prop) => {
// by default, don't stringify properties that contain an array of functions // by default, don't stringify properties that contain an array of functions
if (!conf.includeFunctions && Array.isArray(e[prop]) && e[prop][0] && if (
String(typeof e[prop][0]) === 'function') { !conf.includeFunctions &&
result[prop] = `function[${e[prop].length}]`; Array.isArray(e[prop]) &&
} e[prop][0] &&
// never include functions that belong to the object String(typeof e[prop][0]) === 'function'
else if (typeof e[prop] !== 'function') { ) {
result[prop] = e[prop]; result[prop] = `function[${e[prop].length}]`;
}
});
// allow users to omit node objects, which can be enormous
if (conf.omitNodes) {
result = replaceNodeObjects(result);
} }
// never include functions that belong to the object
else if (typeof e[prop] !== 'function') {
result[prop] = e[prop];
}
});
return result; // allow users to omit node objects, which can be enormous
if (conf.omitNodes) {
result = replaceNodeObjects(result);
}
return result;
} }
exports.handlers = {}; exports.handlers = {};
events.forEach(eventType => { events.forEach((eventType) => {
exports.handlers[eventType] = e => { exports.handlers[eventType] = (e) => {
console.log(JSON.stringify({ console.log(
type: eventType, JSON.stringify({
content: cleanse(e) type: eventType,
}), null, 4); content: cleanse(e),
}; }),
null,
4
);
};
}); });

View File

@ -38,144 +38,143 @@
let functionDoclets; let functionDoclets;
function hasUniqueValues(obj) { function hasUniqueValues(obj) {
let isUnique = true; let isUnique = true;
const seen = []; const seen = [];
Object.keys(obj).forEach(key => { Object.keys(obj).forEach((key) => {
if (seen.includes(obj[key])) { if (seen.includes(obj[key])) {
isUnique = false; isUnique = false;
} }
seen.push(obj[key]); seen.push(obj[key]);
}); });
return isUnique; return isUnique;
} }
function getParamNames(params) { function getParamNames(params) {
const names = []; const names = [];
params.forEach(param => { params.forEach((param) => {
let name = param.name || ''; let name = param.name || '';
if (param.variable) { if (param.variable) {
name = `...${name}`; name = `...${name}`;
} }
if (name !== '') { if (name !== '') {
names.push(name); names.push(name);
} }
}); });
return names.length ? names.join(', ') : ''; return names.length ? names.join(', ') : '';
} }
function getParamVariation({params}) { function getParamVariation({ params }) {
return getParamNames(params || []); return getParamNames(params || []);
} }
function getUniqueVariations(doclets) { function getUniqueVariations(doclets) {
let counter = 0; let counter = 0;
const variations = {}; const variations = {};
const docletKeys = Object.keys(doclets); const docletKeys = Object.keys(doclets);
function getUniqueNumbers() { function getUniqueNumbers() {
docletKeys.forEach(doclet => { docletKeys.forEach((doclet) => {
let newLongname; let newLongname;
while (true) { while (true) {
counter++; counter++;
variations[doclet] = String(counter); variations[doclet] = String(counter);
// is this longname + variation unique? // is this longname + variation unique?
newLongname = `${doclets[doclet].longname}(${variations[doclet]})`; newLongname = `${doclets[doclet].longname}(${variations[doclet]})`;
if ( !functionDoclets[newLongname] ) { if (!functionDoclets[newLongname]) {
break; break;
}
}
});
}
function getUniqueNames() {
// start by trying to preserve existing variations
docletKeys.forEach(doclet => {
variations[doclet] = doclets[doclet].variation || getParamVariation(doclets[doclet]);
});
// if they're identical, try again, without preserving existing variations
if ( !hasUniqueValues(variations) ) {
docletKeys.forEach(doclet => {
variations[doclet] = getParamVariation(doclets[doclet]);
});
// if they're STILL identical, switch to numeric variations
if ( !hasUniqueValues(variations) ) {
getUniqueNumbers();
}
} }
} }
});
}
// are we already using numeric variations? if so, keep doing that function getUniqueNames() {
if (functionDoclets[`${doclets.newDoclet.longname}(1)`]) { // start by trying to preserve existing variations
docletKeys.forEach((doclet) => {
variations[doclet] = doclets[doclet].variation || getParamVariation(doclets[doclet]);
});
// if they're identical, try again, without preserving existing variations
if (!hasUniqueValues(variations)) {
docletKeys.forEach((doclet) => {
variations[doclet] = getParamVariation(doclets[doclet]);
});
// if they're STILL identical, switch to numeric variations
if (!hasUniqueValues(variations)) {
getUniqueNumbers(); getUniqueNumbers();
}
} }
else { }
getUniqueNames();
}
return variations; // are we already using numeric variations? if so, keep doing that
if (functionDoclets[`${doclets.newDoclet.longname}(1)`]) {
getUniqueNumbers();
} else {
getUniqueNames();
}
return variations;
} }
function ensureUniqueLongname(newDoclet) { function ensureUniqueLongname(newDoclet) {
const doclets = { const doclets = {
oldDoclet: functionDoclets[newDoclet.longname], oldDoclet: functionDoclets[newDoclet.longname],
newDoclet: newDoclet newDoclet: newDoclet,
}; };
const docletKeys = Object.keys(doclets); const docletKeys = Object.keys(doclets);
let oldDocletLongname; let oldDocletLongname;
let variations = {}; let variations = {};
if (doclets.oldDoclet) { if (doclets.oldDoclet) {
oldDocletLongname = doclets.oldDoclet.longname; oldDocletLongname = doclets.oldDoclet.longname;
// if the shared longname has a variation, like MyClass#myLongname(variation), // if the shared longname has a variation, like MyClass#myLongname(variation),
// remove the variation // remove the variation
if (doclets.oldDoclet.variation || doclets.oldDoclet.variation === '') { if (doclets.oldDoclet.variation || doclets.oldDoclet.variation === '') {
docletKeys.forEach(doclet => { docletKeys.forEach((doclet) => {
doclets[doclet].longname = doclets[doclet].longname.replace(/\([\s\S]*\)$/, ''); doclets[doclet].longname = doclets[doclet].longname.replace(/\([\s\S]*\)$/, '');
doclets[doclet].variation = null; doclets[doclet].variation = null;
}); });
}
variations = getUniqueVariations(doclets);
// update the longnames/variations
docletKeys.forEach(doclet => {
doclets[doclet].longname += `(${variations[doclet]})`;
doclets[doclet].variation = variations[doclet];
});
// update the old doclet in the lookup table
functionDoclets[oldDocletLongname] = null;
functionDoclets[doclets.oldDoclet.longname] = doclets.oldDoclet;
} }
// always store the new doclet in the lookup table variations = getUniqueVariations(doclets);
functionDoclets[doclets.newDoclet.longname] = doclets.newDoclet;
return doclets.newDoclet; // update the longnames/variations
docletKeys.forEach((doclet) => {
doclets[doclet].longname += `(${variations[doclet]})`;
doclets[doclet].variation = variations[doclet];
});
// update the old doclet in the lookup table
functionDoclets[oldDocletLongname] = null;
functionDoclets[doclets.oldDoclet.longname] = doclets.oldDoclet;
}
// always store the new doclet in the lookup table
functionDoclets[doclets.newDoclet.longname] = doclets.newDoclet;
return doclets.newDoclet;
} }
exports.handlers = { exports.handlers = {
parseBegin() { parseBegin() {
functionDoclets = {}; functionDoclets = {};
}, },
newDoclet(e) { newDoclet(e) {
if (e.doclet.kind === 'function') { if (e.doclet.kind === 'function') {
e.doclet = ensureUniqueLongname(e.doclet); e.doclet = ensureUniqueLongname(e.doclet);
}
},
parseComplete() {
functionDoclets = null;
} }
},
parseComplete() {
functionDoclets = null;
},
}; };

Some files were not shown because too many files have changed in this diff Show More