From 38d51ff9d45f1e9aefd87d4feab2aeadec1dc3fa Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 2 Feb 2022 21:14:56 +0800 Subject: [PATCH] feat: add `sort-tags` rule --- .README/README.md | 1 + .README/rules/sort-tags.md | 198 +++++++++++++++++ README.md | 348 +++++++++++++++++++++++++++++- src/defaultTagOrder.js | 156 ++++++++++++++ src/index.js | 3 + src/iterateJsdoc.js | 32 ++- src/rules/sortTags.js | 188 ++++++++++++++++ test/rules/assertions/sortTags.js | 348 ++++++++++++++++++++++++++++++ test/rules/ruleNames.json | 1 + 9 files changed, 1268 insertions(+), 7 deletions(-) create mode 100644 .README/rules/sort-tags.md create mode 100644 src/defaultTagOrder.js create mode 100644 src/rules/sortTags.js create mode 100644 test/rules/assertions/sortTags.js diff --git a/.README/README.md b/.README/README.md index 0db73ef59..83fb55ec5 100644 --- a/.README/README.md +++ b/.README/README.md @@ -589,5 +589,6 @@ selector). {"gitdown": "include", "file": "./rules/require-throws.md"} {"gitdown": "include", "file": "./rules/require-yields.md"} {"gitdown": "include", "file": "./rules/require-yields-check.md"} +{"gitdown": "include", "file": "./rules/sort-tags.md"} {"gitdown": "include", "file": "./rules/tag-lines.md"} {"gitdown": "include", "file": "./rules/valid-types.md"} diff --git a/.README/rules/sort-tags.md b/.README/rules/sort-tags.md new file mode 100644 index 000000000..2f24c9ccb --- /dev/null +++ b/.README/rules/sort-tags.md @@ -0,0 +1,198 @@ +### `sort-tags` + +Sorts tags by a specified sequence according to tag name. + +(Default order originally inspired by [`@homer0/prettier-plugin-jsdoc`](https://github.com/homer0/packages/tree/main/packages/public/prettier-plugin-jsdoc).) + +#### Options + +##### `tagSequence` + +An array of tag names indicating the preferred sequence for sorting tags. + +Tag names earlier in the list will be arranged first. The relative position of +tags of the same name will not be changed. + +Tags not in the list will be sorted alphabetically at the end (or in place of +the pseudo-tag `-other` placed within `tagSequence`) if `alphabetizeExtras` is +enabled and in their order of appearance otherwise (so if you want all your +tags alphabetized, supply an empty array with `alphabetizeExtras` enabled). + +Defaults to the array below. + +Please note that this order is still experimental, so if you want to retain +a fixed order that doesn't change into the future, supply your own +`tagSequence`. + +```js +[ + // Brief descriptions + 'summary', + 'typeSummary', + + // Module/file-level + 'module', + 'exports', + 'file', + 'fileoverview', + 'overview', + + // Identifying (name, type) + 'typedef', + 'interface', + 'record', + 'template', + 'name', + 'kind', + 'type', + 'alias', + 'external', + 'host', + 'callback', + 'func', + 'function', + 'method', + 'class', + 'constructor', + + // Relationships + 'modifies', + 'mixes', + 'mixin', + 'mixinClass', + 'mixinFunction', + 'namespace', + 'borrows', + 'constructs', + 'lends', + 'implements', + 'requires', + + // Long descriptions + 'desc', + 'description', + 'classdesc', + 'tutorial', + 'copyright', + 'license', + + // Simple annotations + 'const', + 'constant', + 'final', + 'global', + 'readonly', + 'abstract', + 'virtual', + 'var', + 'member', + 'memberof', + 'memberof!', + 'inner', + 'instance', + 'inheritdoc', + 'inheritDoc', + 'override', + 'hideconstructor', + + // Core function/object info + 'param', + 'arg', + 'argument', + 'prop', + 'property', + 'return', + 'returns', + + // Important behavior details + 'async', + 'generator', + 'default', + 'defaultvalue', + 'enum', + 'augments', + 'extends', + 'throws', + 'exception', + 'yield', + 'yields', + 'event', + 'fires', + 'emits', + 'listens', + 'this', + + // Access + 'static', + 'private', + 'protected', + 'public', + 'access', + 'package', + + '-other', + + // Supplementary descriptions + 'see', + 'example', + + // METADATA + + // Other Closure (undocumented) metadata + 'closurePrimitive', + 'customElement', + 'expose', + 'hidden', + 'idGenerator', + 'meaning', + 'ngInject', + 'owner', + 'wizaction', + + // Other Closure (documented) metadata + 'define', + 'dict', + 'export', + 'externs', + 'implicitCast', + 'noalias', + 'nocollapse', + 'nocompile', + 'noinline', + 'nosideeffects', + 'polymer', + 'polymerBehavior', + 'preserve', + 'struct', + 'suppress', + 'unrestricted', + + // @homer0/prettier-plugin-jsdoc metadata + 'category', + + // Non-Closure metadata + 'ignore', + 'author', + 'version', + 'variation', + 'since', + 'deprecated', + 'todo', +]; +``` + +##### `alphabetizeExtras` + +Defaults to `false`. Alphabetizes any items not within `tagSequence` after any +items within `tagSequence` (or in place of the special `-other` pseudo-tag) +are sorted. + +||| +|---|---| +|Context|everywhere| +|Tags|any| +|Recommended|false| +|Settings|| +|Options|`tagSequence`, `alphabetizeExtras`| + + diff --git a/README.md b/README.md index 4fb921b61..ef6611582 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ JSDoc linting rules for ESLint. * [`require-throws`](#eslint-plugin-jsdoc-rules-require-throws) * [`require-yields`](#eslint-plugin-jsdoc-rules-require-yields) * [`require-yields-check`](#eslint-plugin-jsdoc-rules-require-yields-check) + * [`sort-tags`](#eslint-plugin-jsdoc-rules-sort-tags) * [`tag-lines`](#eslint-plugin-jsdoc-rules-tag-lines) * [`valid-types`](#eslint-plugin-jsdoc-rules-valid-types) @@ -20056,12 +20057,349 @@ function * quux (foo) { ```` + +### sort-tags + +Sorts tags by a specified sequence according to tag name. + +(Default order originally inspired by [`@homer0/prettier-plugin-jsdoc`](https://github.com/homer0/packages/tree/main/packages/public/prettier-plugin-jsdoc).) + + +#### Options + + +##### tagSequence + +An array of tag names indicating the preferred sequence for sorting tags. + +Tag names earlier in the list will be arranged first. The relative position of +tags of the same name will not be changed. + +Tags not in the list will be sorted alphabetically at the end (or in place of +the pseudo-tag `-other` placed within `tagSequence`) if `alphabetizeExtras` is +enabled and in their order of appearance otherwise (so if you want all your +tags alphabetized, supply an empty array with `alphabetizeExtras` enabled). + +Defaults to the array below. + +Please note that this order is still experimental, so if you want to retain +a fixed order that doesn't change into the future, supply your own +`tagSequence`. + +```js +[ + // Brief descriptions + 'summary', + 'typeSummary', + + // Module/file-level + 'module', + 'exports', + 'file', + 'fileoverview', + 'overview', + + // Identifying (name, type) + 'typedef', + 'interface', + 'record', + 'template', + 'name', + 'kind', + 'type', + 'alias', + 'external', + 'host', + 'callback', + 'func', + 'function', + 'method', + 'class', + 'constructor', + + // Relationships + 'modifies', + 'mixes', + 'mixin', + 'mixinClass', + 'mixinFunction', + 'namespace', + 'borrows', + 'constructs', + 'lends', + 'implements', + 'requires', + + // Long descriptions + 'desc', + 'description', + 'classdesc', + 'tutorial', + 'copyright', + 'license', + + // Simple annotations + 'const', + 'constant', + 'final', + 'global', + 'readonly', + 'abstract', + 'virtual', + 'var', + 'member', + 'memberof', + 'memberof!', + 'inner', + 'instance', + 'inheritdoc', + 'inheritDoc', + 'override', + 'hideconstructor', + + // Core function/object info + 'param', + 'arg', + 'argument', + 'prop', + 'property', + 'return', + 'returns', + + // Important behavior details + 'async', + 'generator', + 'default', + 'defaultvalue', + 'enum', + 'augments', + 'extends', + 'throws', + 'exception', + 'yield', + 'yields', + 'event', + 'fires', + 'emits', + 'listens', + 'this', + + // Access + 'static', + 'private', + 'protected', + 'public', + 'access', + 'package', + + '-other', + + // Supplementary descriptions + 'see', + 'example', + + // METADATA + + // Other Closure (undocumented) metadata + 'closurePrimitive', + 'customElement', + 'expose', + 'hidden', + 'idGenerator', + 'meaning', + 'ngInject', + 'owner', + 'wizaction', + + // Other Closure (documented) metadata + 'define', + 'dict', + 'export', + 'externs', + 'implicitCast', + 'noalias', + 'nocollapse', + 'nocompile', + 'noinline', + 'nosideeffects', + 'polymer', + 'polymerBehavior', + 'preserve', + 'struct', + 'suppress', + 'unrestricted', + + // @homer0/prettier-plugin-jsdoc metadata + 'category', + + // Non-Closure metadata + 'ignore', + 'author', + 'version', + 'variation', + 'since', + 'deprecated', + 'todo', +]; +``` + + +##### alphabetizeExtras + +Defaults to `false`. Alphabetizes any items not within `tagSequence` after any +items within `tagSequence` (or in place of the special `-other` pseudo-tag) +are sorted. + +||| +|---|---| +|Context|everywhere| +|Tags|any| +|Recommended|false| +|Settings|| +|Options|`tagSequence`, `alphabetizeExtras`| + +The following patterns are considered problems: + +````js +/** + * @returns {string} + * @param b + * @param a + */ +function quux () {} +// Message: Tags are not in the prescribed order: summary, typeSummary, module, exports, file, fileoverview, overview, typedef, interface, record, template, name, kind, type, alias, external, host, callback, func, function, method, class, constructor, modifies, mixes, mixin, mixinClass, mixinFunction, namespace, borrows, constructs, lends, implements, requires, desc, description, classdesc, tutorial, copyright, license, const, constant, final, global, readonly, abstract, virtual, var, member, memberof, memberof!, inner, instance, inheritdoc, inheritDoc, override, hideconstructor, param, arg, argument, prop, property, return, returns, async, generator, default, defaultvalue, enum, augments, extends, throws, exception, yield, yields, event, fires, emits, listens, this, static, private, protected, public, access, package, -other, see, example, closurePrimitive, customElement, expose, hidden, idGenerator, meaning, ngInject, owner, wizaction, define, dict, export, externs, implicitCast, noalias, nocollapse, nocompile, noinline, nosideeffects, polymer, polymerBehavior, preserve, struct, suppress, unrestricted, category, ignore, author, version, variation, since, deprecated, todo + +/** + * Some description + * @returns {string} + * @param b + * @param a + */ +function quux () {} +// Message: Tags are not in the prescribed order: summary, typeSummary, module, exports, file, fileoverview, overview, typedef, interface, record, template, name, kind, type, alias, external, host, callback, func, function, method, class, constructor, modifies, mixes, mixin, mixinClass, mixinFunction, namespace, borrows, constructs, lends, implements, requires, desc, description, classdesc, tutorial, copyright, license, const, constant, final, global, readonly, abstract, virtual, var, member, memberof, memberof!, inner, instance, inheritdoc, inheritDoc, override, hideconstructor, param, arg, argument, prop, property, return, returns, async, generator, default, defaultvalue, enum, augments, extends, throws, exception, yield, yields, event, fires, emits, listens, this, static, private, protected, public, access, package, -other, see, example, closurePrimitive, customElement, expose, hidden, idGenerator, meaning, ngInject, owner, wizaction, define, dict, export, externs, implicitCast, noalias, nocollapse, nocompile, noinline, nosideeffects, polymer, polymerBehavior, preserve, struct, suppress, unrestricted, category, ignore, author, version, variation, since, deprecated, todo + +/** + * @returns {string} + * @param b A long + * description + * @param a + */ +function quux () {} +// Message: Tags are not in the prescribed order: summary, typeSummary, module, exports, file, fileoverview, overview, typedef, interface, record, template, name, kind, type, alias, external, host, callback, func, function, method, class, constructor, modifies, mixes, mixin, mixinClass, mixinFunction, namespace, borrows, constructs, lends, implements, requires, desc, description, classdesc, tutorial, copyright, license, const, constant, final, global, readonly, abstract, virtual, var, member, memberof, memberof!, inner, instance, inheritdoc, inheritDoc, override, hideconstructor, param, arg, argument, prop, property, return, returns, async, generator, default, defaultvalue, enum, augments, extends, throws, exception, yield, yields, event, fires, emits, listens, this, static, private, protected, public, access, package, -other, see, example, closurePrimitive, customElement, expose, hidden, idGenerator, meaning, ngInject, owner, wizaction, define, dict, export, externs, implicitCast, noalias, nocollapse, nocompile, noinline, nosideeffects, polymer, polymerBehavior, preserve, struct, suppress, unrestricted, category, ignore, author, version, variation, since, deprecated, todo + +/** + * Some description + * @returns {string} + * @param b A long + * description + * @param a + */ +function quux () {} +// Message: Tags are not in the prescribed order: summary, typeSummary, module, exports, file, fileoverview, overview, typedef, interface, record, template, name, kind, type, alias, external, host, callback, func, function, method, class, constructor, modifies, mixes, mixin, mixinClass, mixinFunction, namespace, borrows, constructs, lends, implements, requires, desc, description, classdesc, tutorial, copyright, license, const, constant, final, global, readonly, abstract, virtual, var, member, memberof, memberof!, inner, instance, inheritdoc, inheritDoc, override, hideconstructor, param, arg, argument, prop, property, return, returns, async, generator, default, defaultvalue, enum, augments, extends, throws, exception, yield, yields, event, fires, emits, listens, this, static, private, protected, public, access, package, -other, see, example, closurePrimitive, customElement, expose, hidden, idGenerator, meaning, ngInject, owner, wizaction, define, dict, export, externs, implicitCast, noalias, nocollapse, nocompile, noinline, nosideeffects, polymer, polymerBehavior, preserve, struct, suppress, unrestricted, category, ignore, author, version, variation, since, deprecated, todo + +/** + * @param b A long + * description + * @returns {string} + * @param a + */ +function quux () {} +// Message: Tags are not in the prescribed order: summary, typeSummary, module, exports, file, fileoverview, overview, typedef, interface, record, template, name, kind, type, alias, external, host, callback, func, function, method, class, constructor, modifies, mixes, mixin, mixinClass, mixinFunction, namespace, borrows, constructs, lends, implements, requires, desc, description, classdesc, tutorial, copyright, license, const, constant, final, global, readonly, abstract, virtual, var, member, memberof, memberof!, inner, instance, inheritdoc, inheritDoc, override, hideconstructor, param, arg, argument, prop, property, return, returns, async, generator, default, defaultvalue, enum, augments, extends, throws, exception, yield, yields, event, fires, emits, listens, this, static, private, protected, public, access, package, -other, see, example, closurePrimitive, customElement, expose, hidden, idGenerator, meaning, ngInject, owner, wizaction, define, dict, export, externs, implicitCast, noalias, nocollapse, nocompile, noinline, nosideeffects, polymer, polymerBehavior, preserve, struct, suppress, unrestricted, category, ignore, author, version, variation, since, deprecated, todo + +/** + * @def + * @xyz + * @abc + */ +function quux () {} +// "jsdoc/sort-tags": ["error"|"warn", {"alphabetizeExtras":true}] +// Message: Tags are not in the prescribed order: summary, typeSummary, module, exports, file, fileoverview, overview, typedef, interface, record, template, name, kind, type, alias, external, host, callback, func, function, method, class, constructor, modifies, mixes, mixin, mixinClass, mixinFunction, namespace, borrows, constructs, lends, implements, requires, desc, description, classdesc, tutorial, copyright, license, const, constant, final, global, readonly, abstract, virtual, var, member, memberof, memberof!, inner, instance, inheritdoc, inheritDoc, override, hideconstructor, param, arg, argument, prop, property, return, returns, async, generator, default, defaultvalue, enum, augments, extends, throws, exception, yield, yields, event, fires, emits, listens, this, static, private, protected, public, access, package, -other, see, example, closurePrimitive, customElement, expose, hidden, idGenerator, meaning, ngInject, owner, wizaction, define, dict, export, externs, implicitCast, noalias, nocollapse, nocompile, noinline, nosideeffects, polymer, polymerBehavior, preserve, struct, suppress, unrestricted, category, ignore, author, version, variation, since, deprecated, todo + +/** + * @xyz + * @def + * @abc + */ +function quux () {} +// "jsdoc/sort-tags": ["error"|"warn", {"tagSequence":["def","xyz","abc"]}] +// Message: Tags are not in the prescribed order: def, xyz, abc + +/** + * @returns {string} + * @ignore + * @param b A long + * description + * @param a + * @module + */ +function quux () {} +// Message: Tags are not in the prescribed order: summary, typeSummary, module, exports, file, fileoverview, overview, typedef, interface, record, template, name, kind, type, alias, external, host, callback, func, function, method, class, constructor, modifies, mixes, mixin, mixinClass, mixinFunction, namespace, borrows, constructs, lends, implements, requires, desc, description, classdesc, tutorial, copyright, license, const, constant, final, global, readonly, abstract, virtual, var, member, memberof, memberof!, inner, instance, inheritdoc, inheritDoc, override, hideconstructor, param, arg, argument, prop, property, return, returns, async, generator, default, defaultvalue, enum, augments, extends, throws, exception, yield, yields, event, fires, emits, listens, this, static, private, protected, public, access, package, -other, see, example, closurePrimitive, customElement, expose, hidden, idGenerator, meaning, ngInject, owner, wizaction, define, dict, export, externs, implicitCast, noalias, nocollapse, nocompile, noinline, nosideeffects, polymer, polymerBehavior, preserve, struct, suppress, unrestricted, category, ignore, author, version, variation, since, deprecated, todo + +/** + * @xyz + * @abc + * @abc + * @def + * @xyz + */ +function quux () {} +// "jsdoc/sort-tags": ["error"|"warn", {"alphabetizeExtras":true}] +// Message: Tags are not in the prescribed order: summary, typeSummary, module, exports, file, fileoverview, overview, typedef, interface, record, template, name, kind, type, alias, external, host, callback, func, function, method, class, constructor, modifies, mixes, mixin, mixinClass, mixinFunction, namespace, borrows, constructs, lends, implements, requires, desc, description, classdesc, tutorial, copyright, license, const, constant, final, global, readonly, abstract, virtual, var, member, memberof, memberof!, inner, instance, inheritdoc, inheritDoc, override, hideconstructor, param, arg, argument, prop, property, return, returns, async, generator, default, defaultvalue, enum, augments, extends, throws, exception, yield, yields, event, fires, emits, listens, this, static, private, protected, public, access, package, -other, see, example, closurePrimitive, customElement, expose, hidden, idGenerator, meaning, ngInject, owner, wizaction, define, dict, export, externs, implicitCast, noalias, nocollapse, nocompile, noinline, nosideeffects, polymer, polymerBehavior, preserve, struct, suppress, unrestricted, category, ignore, author, version, variation, since, deprecated, todo + +/** + * @param b A long + * description + * @module + */ +function quux () {} +// Message: Tags are not in the prescribed order: summary, typeSummary, module, exports, file, fileoverview, overview, typedef, interface, record, template, name, kind, type, alias, external, host, callback, func, function, method, class, constructor, modifies, mixes, mixin, mixinClass, mixinFunction, namespace, borrows, constructs, lends, implements, requires, desc, description, classdesc, tutorial, copyright, license, const, constant, final, global, readonly, abstract, virtual, var, member, memberof, memberof!, inner, instance, inheritdoc, inheritDoc, override, hideconstructor, param, arg, argument, prop, property, return, returns, async, generator, default, defaultvalue, enum, augments, extends, throws, exception, yield, yields, event, fires, emits, listens, this, static, private, protected, public, access, package, -other, see, example, closurePrimitive, customElement, expose, hidden, idGenerator, meaning, ngInject, owner, wizaction, define, dict, export, externs, implicitCast, noalias, nocollapse, nocompile, noinline, nosideeffects, polymer, polymerBehavior, preserve, struct, suppress, unrestricted, category, ignore, author, version, variation, since, deprecated, todo +```` + +The following patterns are not considered problems: + +````js +/** + * @param b + * @param a + * @returns {string} + */ +function quux () {} + +/** + * @abc + * @def + * @xyz + */ +function quux () {} +// "jsdoc/sort-tags": ["error"|"warn", {"alphabetizeExtras":true}] + +/** + * @def + * @xyz + * @abc + */ +function quux () {} +// "jsdoc/sort-tags": ["error"|"warn", {"alphabetizeExtras":false}] + +/** + * @def + * @xyz + * @abc + */ +function quux () {} +// "jsdoc/sort-tags": ["error"|"warn", {"tagSequence":["def","xyz","abc"]}] + +/** @def */ +function quux () {} +```` + + ### tag-lines Enforces lines (or no lines) between tags. - + #### Options The first option is a single string set to "always", "never", or "any" @@ -20072,18 +20410,18 @@ for particular tags). The second option is an object with the following optional properties. - + ##### count (defaults to 1) Use with "always" to indicate the number of lines to require be present. - + ##### noEndLines (defaults to false) Use with "always" to indicate the normal lines to be added after tags should not be added after the final tag. - + ##### tags (default to empty object) Overrides the default behavior depending on specific tags. @@ -20479,7 +20817,7 @@ for valid types (based on the tag's `type` value), and either portion checked for presence (based on `false` `name` or `type` values or their `required` value). See the setting for more details. - + #### Options - `allowEmptyNamepaths` (default: true) - Set to `false` to bulk disallow diff --git a/src/defaultTagOrder.js b/src/defaultTagOrder.js new file mode 100644 index 000000000..e2483ec6a --- /dev/null +++ b/src/defaultTagOrder.js @@ -0,0 +1,156 @@ +const defaultTagOrder = [ + // Brief descriptions + 'summary', + 'typeSummary', + + // Module/file-level + 'module', + 'exports', + 'file', + 'fileoverview', + 'overview', + + // Identifying (name, type) + 'typedef', + 'interface', + 'record', + 'template', + 'name', + 'kind', + 'type', + 'alias', + 'external', + 'host', + 'callback', + 'func', + 'function', + 'method', + 'class', + 'constructor', + + // Relationships + 'modifies', + 'mixes', + 'mixin', + 'mixinClass', + 'mixinFunction', + 'namespace', + 'borrows', + 'constructs', + 'lends', + 'implements', + 'requires', + + // Long descriptions + 'desc', + 'description', + 'classdesc', + 'tutorial', + 'copyright', + 'license', + + // Simple annotations + 'const', + 'constant', + 'final', + 'global', + 'readonly', + 'abstract', + 'virtual', + 'var', + 'member', + 'memberof', + 'memberof!', + 'inner', + 'instance', + 'inheritdoc', + 'inheritDoc', + 'override', + 'hideconstructor', + + // Core function/object info + 'param', + 'arg', + 'argument', + 'prop', + 'property', + 'return', + 'returns', + + // Important behavior details + 'async', + 'generator', + 'default', + 'defaultvalue', + 'enum', + 'augments', + 'extends', + 'throws', + 'exception', + 'yield', + 'yields', + 'event', + 'fires', + 'emits', + 'listens', + 'this', + + // Access + 'static', + 'private', + 'protected', + 'public', + 'access', + 'package', + + '-other', + + // Supplementary descriptions + 'see', + 'example', + + // METADATA + + // Other Closure (undocumented) metadata + 'closurePrimitive', + 'customElement', + 'expose', + 'hidden', + 'idGenerator', + 'meaning', + 'ngInject', + 'owner', + 'wizaction', + + // Other Closure (documented) metadata + 'define', + 'dict', + 'export', + 'externs', + 'implicitCast', + 'noalias', + 'nocollapse', + 'nocompile', + 'noinline', + 'nosideeffects', + 'polymer', + 'polymerBehavior', + 'preserve', + 'struct', + 'suppress', + 'unrestricted', + + // @homer0/prettier-plugin-jsdoc metadata + 'category', + + // Non-Closure metadata + 'ignore', + 'author', + 'version', + 'variation', + 'since', + 'deprecated', + 'todo', +]; + +export default defaultTagOrder; diff --git a/src/index.js b/src/index.js index 8c3588793..666620cfc 100644 --- a/src/index.js +++ b/src/index.js @@ -44,6 +44,7 @@ import requireReturnsType from './rules/requireReturnsType'; import requireThrows from './rules/requireThrows'; import requireYields from './rules/requireYields'; import requireYieldsCheck from './rules/requireYieldsCheck'; +import sortTags from './rules/sortTags'; import tagLines from './rules/tagLines'; import validTypes from './rules/validTypes'; @@ -100,6 +101,7 @@ export default { 'jsdoc/require-throws': 'off', 'jsdoc/require-yields': 'warn', 'jsdoc/require-yields-check': 'warn', + 'jsdoc/sort-tags': 'off', 'jsdoc/tag-lines': 'warn', 'jsdoc/valid-types': 'warn', }, @@ -152,6 +154,7 @@ export default { 'require-throws': requireThrows, 'require-yields': requireYields, 'require-yields-check': requireYieldsCheck, + 'sort-tags': sortTags, 'tag-lines': tagLines, 'valid-types': validTypes, }, diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index ff4efe012..5b0f92d75 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -276,10 +276,17 @@ const getUtils = ( ] of jsdoc.source.slice(lastIndex).entries()) { src.number = firstNumber + lastIndex + idx; } + + // Todo: Once rewiring of tags may be fixed in comment-parser to reflect missing tags, + // this step should be added here (so that, e.g., if accessing `jsdoc.tags`, + // such as to add a new tag, the correct information will be available) }; - utils.addTag = (targetTagName) => { - const number = (jsdoc.tags[jsdoc.tags.length - 1]?.source[0]?.number ?? 0) + 1; + utils.addTag = ( + targetTagName, + number = (jsdoc.tags[jsdoc.tags.length - 1]?.source[0]?.number ?? 0) + 1, + tokens = {}, + ) => { jsdoc.source.splice(number, 0, { number, source: '', @@ -288,6 +295,7 @@ const getUtils = ( postDelimiter: ' ', start: indent + ' ', tag: `@${targetTagName}`, + ...tokens, }), }); for (const src of jsdoc.source.slice(number + 1)) { @@ -295,6 +303,23 @@ const getUtils = ( } }; + utils.getFirstLine = () => { + let firstLine; + for (const { + number, + tokens: { + tag, + }, + } of jsdoc.source) { + if (tag) { + firstLine = number; + break; + } + } + + return firstLine; + }; + utils.seedTokens = seedTokens; utils.emptyTokens = (tokens) => { @@ -323,6 +348,9 @@ const getUtils = ( tokens: seedTokens(tokens), }); + for (const src of jsdoc.source.slice(number + 1)) { + src.number++; + } // If necessary, we can rewire the tags (misnamed method) // rewireSource(jsdoc); }; diff --git a/src/rules/sortTags.js b/src/rules/sortTags.js new file mode 100644 index 000000000..cbb06c43f --- /dev/null +++ b/src/rules/sortTags.js @@ -0,0 +1,188 @@ +import defaultTagOrder from '../defaultTagOrder'; +import iterateJsdoc from '../iterateJsdoc'; + +export default iterateJsdoc(({ + context, + jsdoc, + utils, +}) => { + const { + tagSequence = defaultTagOrder, + alphabetizeExtras = false, + } = context.options[0] || {}; + + const otherPos = tagSequence.indexOf('-other'); + const endPos = otherPos > -1 ? otherPos : tagSequence.length; + + let ongoingCount = 0; + for (const [ + idx, + tag, + ] of jsdoc.tags.entries()) { + tag.originalIndex = idx; + ongoingCount += tag.source.length; + tag.originalLine = ongoingCount; + } + + let firstChangedTagLine; + let firstChangedTagIndex; + const sortedTags = JSON.parse(JSON.stringify(jsdoc.tags)); + sortedTags.sort(({ + tag: tagNew, + }, { + originalIndex, + originalLine, + tag: tagOld, + }) => { + // Optimize: Just keep relative positions if the same tag name + if (tagNew === tagOld) { + return 0; + } + + const checkOrSetFirstChanged = () => { + if (!firstChangedTagLine || originalLine < firstChangedTagLine) { + firstChangedTagLine = originalLine; + firstChangedTagIndex = originalIndex; + } + }; + + const newPos = tagSequence.indexOf(tagNew); + const oldPos = tagSequence.indexOf(tagOld); + + const preferredNewPos = newPos === -1 ? endPos : newPos; + const preferredOldPos = oldPos === -1 ? endPos : oldPos; + + if (preferredNewPos < preferredOldPos) { + checkOrSetFirstChanged(); + return -1; + } + + if (preferredNewPos > preferredOldPos) { + return 1; + } + + // preferredNewPos === preferredOldPos + if ( + !alphabetizeExtras || + + // Optimize: If tagNew (or tagOld which is the same) was found in the + // priority array, it can maintain its relative position—without need + // of alphabetizing (secondary sorting) + newPos >= 0 + ) { + return 0; + } + + if (tagNew < tagOld) { + checkOrSetFirstChanged(); + return -1; + } + + // tagNew > tagOld + return 1; + }); + + if (firstChangedTagLine === undefined) { + return; + } + + const firstLine = utils.getFirstLine(); + + const fix = () => { + const itemsToMoveRange = [ + ...Array.from({ + length: jsdoc.tags.length - firstChangedTagIndex, + }).keys(), + ]; + + const unchangedPriorTagDescriptions = jsdoc.tags.slice( + 0, + firstChangedTagIndex, + ).reduce((ct, { + source, + }) => { + return ct + source.length - 1; + }, 0); + + // This offset includes not only the offset from where the first tag + // must begin, and the additional offset of where the first changed + // tag begins, but it must also account for prior descriptions + const initialOffset = firstLine + firstChangedTagIndex + + + // May be the first tag, so don't try finding a prior one if so + unchangedPriorTagDescriptions; + + // Use `firstChangedTagLine` for line number to begin reporting/splicing + for (const idx of itemsToMoveRange) { + utils.removeTag(idx + firstChangedTagIndex); + } + + const changedTags = sortedTags.slice(firstChangedTagIndex); + let extraTagCount = 0; + + for (const idx of itemsToMoveRange) { + const changedTag = changedTags[idx]; + + utils.addTag( + changedTag.tag, + extraTagCount + initialOffset + idx, + { + ...changedTag.source[0].tokens, + + // `comment-parser` puts the `end` within the `tags` section, so + // avoid adding another to jsdoc.source + end: '', + }, + ); + + for (const { + tokens, + } of changedTag.source.slice(1)) { + if (!tokens.end) { + utils.addLine( + extraTagCount + initialOffset + idx + 1, + { + ...tokens, + end: '', + }, + ); + extraTagCount++; + } + } + } + }; + + utils.reportJSDoc( + `Tags are not in the prescribed order: ${tagSequence.join(', ')}`, + jsdoc.tags[firstChangedTagIndex], + fix, + true, + ); +}, { + iterateAllJsdocs: true, + meta: { + docs: { + description: '', + url: 'https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-sort-tags', + }, + fixable: 'code', + schema: [ + { + additionalProperies: false, + properties: { + alphabetizeExtras: { + type: 'boolean', + }, + tagSequence: { + items: { + type: 'string', + }, + type: 'array', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, +}); diff --git a/test/rules/assertions/sortTags.js b/test/rules/assertions/sortTags.js new file mode 100644 index 000000000..464f38b18 --- /dev/null +++ b/test/rules/assertions/sortTags.js @@ -0,0 +1,348 @@ +import defaultTagOrder from '../../../src/defaultTagOrder'; + +export default { + invalid: [ + { + code: ` + /** + * @returns {string} + * @param b + * @param a + */ + function quux () {} + `, + errors: [ + { + line: 3, + message: 'Tags are not in the prescribed order: ' + defaultTagOrder.join(', '), + }, + ], + output: ` + /** + * @param b + * @param a + * @returns {string} + */ + function quux () {} + `, + }, + { + code: ` + /** + * Some description + * @returns {string} + * @param b + * @param a + */ + function quux () {} + `, + errors: [ + { + line: 4, + message: 'Tags are not in the prescribed order: ' + defaultTagOrder.join(', '), + }, + ], + output: ` + /** + * Some description + * @param b + * @param a + * @returns {string} + */ + function quux () {} + `, + }, + { + code: ` + /** + * @returns {string} + * @param b A long + * description + * @param a + */ + function quux () {} + `, + errors: [ + { + line: 3, + message: 'Tags are not in the prescribed order: ' + defaultTagOrder.join(', '), + }, + ], + output: ` + /** + * @param b A long + * description + * @param a + * @returns {string} + */ + function quux () {} + `, + }, + { + code: ` + /** + * Some description + * @returns {string} + * @param b A long + * description + * @param a + */ + function quux () {} + `, + errors: [ + { + line: 4, + message: 'Tags are not in the prescribed order: ' + defaultTagOrder.join(', '), + }, + ], + output: ` + /** + * Some description + * @param b A long + * description + * @param a + * @returns {string} + */ + function quux () {} + `, + }, + { + code: ` + /** + * @param b A long + * description + * @returns {string} + * @param a + */ + function quux () {} + `, + errors: [ + { + line: 5, + message: 'Tags are not in the prescribed order: ' + defaultTagOrder.join(', '), + }, + ], + output: ` + /** + * @param b A long + * description + * @param a + * @returns {string} + */ + function quux () {} + `, + }, + { + code: ` + /** + * @def + * @xyz + * @abc + */ + function quux () {} + `, + errors: [ + { + line: 3, + message: 'Tags are not in the prescribed order: ' + defaultTagOrder.join(', '), + }, + ], + options: [ + { + alphabetizeExtras: true, + }, + ], + output: ` + /** + * @abc + * @def + * @xyz + */ + function quux () {} + `, + }, + { + code: ` + /** + * @xyz + * @def + * @abc + */ + function quux () {} + `, + errors: [ + { + line: 3, + message: 'Tags are not in the prescribed order: def, xyz, abc', + }, + ], + options: [ + { + tagSequence: [ + 'def', 'xyz', 'abc', + ], + }, + ], + output: ` + /** + * @def + * @xyz + * @abc + */ + function quux () {} + `, + }, + { + code: ` + /** + * @returns {string} + * @ignore + * @param b A long + * description + * @param a + * @module + */ + function quux () {} + `, + errors: [ + { + line: 3, + message: 'Tags are not in the prescribed order: ' + defaultTagOrder.join(', '), + }, + ], + output: ` + /** + * @module + * @param b A long + * description + * @param a + * @returns {string} + * @ignore + */ + function quux () {} + `, + }, + { + code: ` + /** + * @xyz + * @abc + * @abc + * @def + * @xyz + */ + function quux () {} + `, + errors: [ + { + line: 3, + message: 'Tags are not in the prescribed order: ' + defaultTagOrder.join(', '), + }, + ], + options: [ + { + alphabetizeExtras: true, + }, + ], + output: ` + /** + * @abc + * @abc + * @def + * @xyz + * @xyz + */ + function quux () {} + `, + }, + { + code: ` + /** + * @param b A long + * description + * @module + */ + function quux () {} + `, + errors: [ + { + line: 3, + message: 'Tags are not in the prescribed order: ' + defaultTagOrder.join(', '), + }, + ], + output: ` + /** + * @module + * @param b A long + * description + */ + function quux () {} + `, + }, + ], + valid: [ + { + code: ` + /** + * @param b + * @param a + * @returns {string} + */ + function quux () {} + `, + }, + { + code: ` + /** + * @abc + * @def + * @xyz + */ + function quux () {} + `, + options: [ + { + alphabetizeExtras: true, + }, + ], + }, + { + code: ` + /** + * @def + * @xyz + * @abc + */ + function quux () {} + `, + options: [ + { + alphabetizeExtras: false, + }, + ], + }, + { + code: ` + /** + * @def + * @xyz + * @abc + */ + function quux () {} + `, + options: [ + { + tagSequence: [ + 'def', 'xyz', 'abc', + ], + }, + ], + }, + { + code: ` + /** @def */ + function quux () {} + `, + }, + ], +}; diff --git a/test/rules/ruleNames.json b/test/rules/ruleNames.json index d38553595..bc724aab6 100644 --- a/test/rules/ruleNames.json +++ b/test/rules/ruleNames.json @@ -45,6 +45,7 @@ "require-throws", "require-yields", "require-yields-check", + "sort-tags", "tag-lines", "valid-types" ]