From 89f2ad0f531bcba4b9c7ddf6385eac62f904c479 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sat, 11 Jul 2020 18:26:24 +0800 Subject: [PATCH] feat(`check-types`, `no-undefined-types`, `valid-types`): Add `structuredTags` setting to control whether the type and namepath portions should be checked for validity and whether such portions are required, and to let user-defined "namepath-defining" tags be added to defined types. Closes #481 BREAKING CHANGE: Drops `checkSeesForNamepaths` setting. Use `{settings: {jsdoc: {structuredTags: {name: 'namepath', type: false, required: ['name'],}}}}` instead. Also: 1. Clarifies in more cases where a problem is specific to the mode or not 2. Reports simultaneous invalid name *and* type errors 3. `typdef` now requires `allowEmptyNamepaths: false,` to report empty names (as with other tags) 4. Requires a name for `event` and `external` (and `extends` in jsdoc mode); some tweaking of other tags per docs --- .README/README.md | 43 ++ .README/rules/check-types.md | 6 +- .README/rules/no-undefined-types.md | 7 +- .README/rules/valid-types.md | 26 +- README.md | 243 ++++++++++- src/getDefaultTagStructureForMode.js | 464 ++++++++++++++++++++++ src/iterateJsdoc.js | 102 ++++- src/jsdocUtils.js | 350 +++++----------- src/rules/requireJsdoc.js | 3 + src/rules/validTypes.js | 132 +++--- test/eslint/getJSDocComment.js | 3 + test/rules/assertions/checkParamNames.js | 24 ++ test/rules/assertions/checkTypes.js | 43 ++ test/rules/assertions/noBadBlocks.js | 21 + test/rules/assertions/noUndefinedTypes.js | 100 +++++ test/rules/assertions/requireJsdoc.js | 24 ++ test/rules/assertions/validTypes.js | 251 +++++++++++- 17 files changed, 1458 insertions(+), 384 deletions(-) create mode 100644 src/getDefaultTagStructureForMode.js diff --git a/.README/README.md b/.README/README.md index 0401c82bb..7dac6a01d 100644 --- a/.README/README.md +++ b/.README/README.md @@ -380,6 +380,49 @@ key nor the value will be reported. Thus in `check-types`, this fact can be used to allow both `object` and `Object` if one has a `preferredTypes` key `object: 'Object'` and `Object: 'object'`. +### `structuredTags` + +An object indicating tags whose types and names/namepaths (whether defining or +referencing namepaths) will be checked, subject to configuration. If the tags +have predefined behavior or `allowEmptyNamepaths` behavior, this option will +override that behavior for any specified tags, though this option can also be +used for tags without predefined behavior. Its keys are tag names and its +values are objects with the following optional properties: + - `name` - String set to one of the following: + - `"text"` - When a name is present, plain text will be allowed in the + name position (non-whitespace immediately after the tag and whitespace), + e.g., in `@throws This is an error`, "This" would normally be the name, + but "text" allows non-name text here also. This is the default. + - `"namepath-defining"` - As with `namepath-referencing`, but also + indicates the tag adds a namepath to definitions, e.g., to prevent + `no-undefined-types` from reporting references to that namepath. + - `"namepath-referencing"` - This will cause any name position to be + checked to ensure it is a valid namepath. You might use this to ensure + that tags which normally allow free text, e.g., `@see` will instead + require a namepath. + - `false` - This will disallow any text in the name position. + - `type`: + - `true` - Allows valid types within brackets. This is the default. + - `false` - Explicitly disallows any brackets or bracketed type. You + might use this with `@throws` to suggest that only free form text + is being input or with `@augments` (for jsdoc mode) to disallow + Closure-style bracketed usage along with a required namepath. + - `required` - Array of one of the following (defaults to an empty array, + meaning none are required): + - One or both of the following strings (if both are included, then both + are required): + - `"name"` - Indicates that a name position is required (not just that + if present, it is a valid namepath). You might use this with `see` + to insist that a value (or namepath, depending on the `name` value) + is always present. + - `"type"` - Indicates that the type position (within curly brackets) + is required (not just that if present, it is a valid type). You + might use this with `@throws` or `@typedef` which might otherwise + normally have their types optional. See the type groups 3-5 above. + - `"typeOrName"` - Must have either type (e.g., `@throws {aType}`) or + name (`@throws Some text`); does not require that both exist but + disallows just an empty tag. + ## Advanced ### AST and Selectors diff --git a/.README/rules/check-types.md b/.README/rules/check-types.md index 06a2d9860..1b84de147 100644 --- a/.README/rules/check-types.md +++ b/.README/rules/check-types.md @@ -120,6 +120,10 @@ Boolean | **boolean** | **boolean** | `(true) instanceof Boolean` -> **`false`** Number | **number** | **number** | `(41) instanceof Number` -> **`false`** String | **string** | **string** | `("test") instanceof String` -> **`false`** +If you define your own tags and don't wish their bracketed portions checked +for types, you can use `settings.jsdoc.structuredTags` with a tag `type` of +`false`. + ||| |---|---| |Context|everywhere| @@ -127,6 +131,6 @@ String | **string** | **string** | `("test") instanceof String` -> **`false`** |Aliases|`constructor`, `const`, `extends`, `var`, `arg`, `argument`, `prop`, `return`, `exception`, `yield`| |Closure-only|`package`, `private`, `protected`, `public`, `static`| |Options|`noDefaults`, `exemptTagContexts`, `unifyParentAndChildTypeChecks`| -|Settings|`preferredTypes`, `mode`| +|Settings|`preferredTypes`, `mode`, `structuredTags`| diff --git a/.README/rules/no-undefined-types.md b/.README/rules/no-undefined-types.md index a6081d704..e72fbbbd7 100644 --- a/.README/rules/no-undefined-types.md +++ b/.README/rules/no-undefined-types.md @@ -36,6 +36,11 @@ Also note that if there is an error [parsing](https://github.com/jsdoctypeparser types for a tag, the function will silently ignore that tag, leaving it to the `valid-types` rule to report parsing errors. +If you define your own tags, you can use `settings.jsdoc.structuredTags` +to indicate that a tag's `name` is "namepath-defining" (and should prevent +reporting on use of that namepath elsewhere) and/or that a tag's `type` is +`false` (and should not be checked for types). + #### Options An option object may have the following key: @@ -51,6 +56,6 @@ An option object may have the following key: |Aliases|`constructor`, `const`, `extends`, `var`, `arg`, `argument`, `prop`, `return`, `exception`, `yield`| |Closure-only|`package`, `private`, `protected`, `public`, `static`| |Options|`definedTypes`| -|Settings|`preferredTypes`, `mode`| +|Settings|`preferredTypes`, `mode`, `structuredTags`| diff --git a/.README/rules/valid-types.md b/.README/rules/valid-types.md index a0d8b7ea0..b0b1b2175 100644 --- a/.README/rules/valid-types.md +++ b/.README/rules/valid-types.md @@ -31,15 +31,15 @@ e.g., `@modifies`): The following tags have their name/namepath portion (the non-whitespace text after the tag name) checked: -1. Name(path)-defining tags requiring namepath: `@external`, `@host`, - `@name`, `@typedef`, and `@template` (TypeScript/Closure only); - `@param` (`@arg`, `@argument`) and `@property` +1. Name(path)-defining tags requiring namepath: `@event`, `@callback`, + `@external`, `@host`, `@name`, `@typedef`, and `@template` + (TypeScript/Closure only); `@param` (`@arg`, `@argument`) and `@property` (`@prop`) also fall into this category, but while this rule will check their namepath validity, we leave the requiring of the name portion to the rules `require-param-name` and `require-property-name`, respectively. 1. Name(path)-defining tags (which may have value without namepath or their - namepath can be expressed elsewhere on the block): `@event`, `@callback`, + namepath can be expressed elsewhere on the block): `@class`, `@constructor`, `@constant`, `@const`, `@function`, `@func`, `@method`, `@interface` (TypeScript tag only), `@member`, `@var`, `@mixin`, `@namespace`, `@module` (module paths are not planned for @@ -70,16 +70,22 @@ text after the tag name) checked: allow `#`, `.`, or `~` at the end (which is not allowed at the end of normal paths). +If you define your own tags, `settings.jsdoc.structuredTags` will allow +these custom tags to be checked, with the name portion of tags checked for +valid namepaths (based on the tag's `name` value), their type portions checked +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 empty name paths with namepath groups 2 and 4 (these might often be expected to have an accompanying name path, though they have some indicative value without one; these may also allow names to be defined - in another manner elsewhere in the block) -- `checkSeesForNamepaths` (default: false) - Set this to `true` to insist - that `@see` only use name paths (the tag is normally permitted to - allow other text) + in another manner elsewhere in the block); you can use + `settings.jsdoc.structuredTags` with the `required` key set to "name" if you + wish to require name paths on a tag-by-tag basis. ||| |---|---| @@ -87,7 +93,7 @@ text after the tag name) checked: |Tags|For name only unless otherwise stated: `alias`, `augments`, `borrows`, `callback`, `class` (for name and type), `constant` (for name and type), `enum` (for type), `event`, `external`, `fires`, `function`, `implements` (for type), `interface`, `lends`, `listens`, `member` (for name and type), `memberof`, `memberof!`, `mixes`, `mixin`, `modifies`, `module` (for name and type), `name`, `namespace` (for name and type), `param` (for name and type), `property` (for name and type), `returns` (for type), `see` (optionally for name), `this`, `throws` (for type), `type` (for type), `typedef` (for name and type), `yields` (for type)| |Aliases|`extends`, `constructor`, `const`, `host`, `emits`, `func`, `method`, `var`, `arg`, `argument`, `prop`, `return`, `exception`, `yield`| |Closure-only|For type only: `package`, `private`, `protected`, `public`, `static`| -|Options|`allowEmptyNamepaths`, `checkSeesForNamepaths`| -|Settings|`mode`| +|Options|`allowEmptyNamepaths`| +|Settings|`mode`, `structuredTags`| diff --git a/README.md b/README.md index cf3064ede..c6c5a0e54 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ JSDoc linting rules for ESLint. * [Alias Preference](#eslint-plugin-jsdoc-settings-alias-preference) * [`@override`/`@augments`/`@extends`/`@implements` Without Accompanying `@param`/`@description`/`@example`/`@returns`](#eslint-plugin-jsdoc-settings-override-augments-extends-implements-without-accompanying-param-description-example-returns) * [Settings to Configure `check-types` and `no-undefined-types`](#eslint-plugin-jsdoc-settings-settings-to-configure-check-types-and-no-undefined-types) + * [`structuredTags`](#eslint-plugin-jsdoc-settings-structuredtags) * [Advanced](#eslint-plugin-jsdoc-advanced) * [AST and Selectors](#eslint-plugin-jsdoc-advanced-ast-and-selectors) * [Rules](#eslint-plugin-jsdoc-rules) @@ -442,6 +443,50 @@ key nor the value will be reported. Thus in `check-types`, this fact can be used to allow both `object` and `Object` if one has a `preferredTypes` key `object: 'Object'` and `Object: 'object'`. + +### structuredTags + +An object indicating tags whose types and names/namepaths (whether defining or +referencing namepaths) will be checked, subject to configuration. If the tags +have predefined behavior or `allowEmptyNamepaths` behavior, this option will +override that behavior for any specified tags, though this option can also be +used for tags without predefined behavior. Its keys are tag names and its +values are objects with the following optional properties: + - `name` - String set to one of the following: + - `"text"` - When a name is present, plain text will be allowed in the + name position (non-whitespace immediately after the tag and whitespace), + e.g., in `@throws This is an error`, "This" would normally be the name, + but "text" allows non-name text here also. This is the default. + - `"namepath-defining"` - As with `namepath-referencing`, but also + indicates the tag adds a namepath to definitions, e.g., to prevent + `no-undefined-types` from reporting references to that namepath. + - `"namepath-referencing"` - This will cause any name position to be + checked to ensure it is a valid namepath. You might use this to ensure + that tags which normally allow free text, e.g., `@see` will instead + require a namepath. + - `false` - This will disallow any text in the name position. + - `type`: + - `true` - Allows valid types within brackets. This is the default. + - `false` - Explicitly disallows any brackets or bracketed type. You + might use this with `@throws` to suggest that only free form text + is being input or with `@augments` (for jsdoc mode) to disallow + Closure-style bracketed usage along with a required namepath. + - `required` - Array of one of the following (defaults to an empty array, + meaning none are required): + - One or both of the following strings (if both are included, then both + are required): + - `"name"` - Indicates that a name position is required (not just that + if present, it is a valid namepath). You might use this with `see` + to insist that a value (or namepath, depending on the `name` value) + is always present. + - `"type"` - Indicates that the type position (within curly brackets) + is required (not just that if present, it is a valid type). You + might use this with `@throws` or `@typedef` which might otherwise + normally have their types optional. See the type groups 3-5 above. + - `"typeOrName"` - Must have either type (e.g., `@throws {aType}`) or + name (`@throws Some text`); does not require that both exist but + disallows just an empty tag. + ## Advanced @@ -2049,6 +2094,15 @@ function testingEslint(options: { return one + two + three; } // Message: Missing @param "options.three" + +/** + * + */ +function quux() { + +} +// Settings: {"jsdoc":{"structuredTags":{"see":{"name":false,"required":["name"]}}}} +// Message: Cannot add "name" to `require` with the tag's `name` set to `false` ```` The following patterns are not considered problems: @@ -3500,6 +3554,10 @@ Boolean | **boolean** | **boolean** | `(true) instanceof Boolean` -> **`false`** Number | **number** | **number** | `(41) instanceof Number` -> **`false`** String | **string** | **string** | `("test") instanceof String` -> **`false`** +If you define your own tags and don't wish their bracketed portions checked +for types, you can use `settings.jsdoc.structuredTags` with a tag `type` of +`false`. + ||| |---|---| |Context|everywhere| @@ -3507,7 +3565,7 @@ String | **string** | **string** | `("test") instanceof String` -> **`false`** |Aliases|`constructor`, `const`, `extends`, `var`, `arg`, `argument`, `prop`, `return`, `exception`, `yield`| |Closure-only|`package`, `private`, `protected`, `public`, `static`| |Options|`noDefaults`, `exemptTagContexts`, `unifyParentAndChildTypeChecks`| -|Settings|`preferredTypes`, `mode`| +|Settings|`preferredTypes`, `mode`, `structuredTags`| The following patterns are considered problems: @@ -4081,6 +4139,12 @@ function a () {} function b () {} // Settings: {"jsdoc":{"mode":"typescript","preferredTypes":{"object":"Object"}}} // Message: Invalid JSDoc @typedef "foo" type "object"; prefer: "Object". + +/** + * @aCustomTag {Number} foo + */ +// Settings: {"jsdoc":{"structuredTags":{"aCustomTag":{"type":true}}}} +// Message: Invalid JSDoc @aCustomTag "foo" type "Number"; prefer: "number". ```` The following patterns are not considered problems: @@ -4345,6 +4409,11 @@ function a () {} */ function b () {} // Settings: {"jsdoc":{"mode":"typescript"}} + +/** + * @aCustomTag {Number} foo + */ +// Settings: {"jsdoc":{"structuredTags":{"aCustomTag":{"type":false}}}} ```` @@ -6057,6 +6126,11 @@ Also note that if there is an error [parsing](https://github.com/jsdoctypeparser types for a tag, the function will silently ignore that tag, leaving it to the `valid-types` rule to report parsing errors. +If you define your own tags, you can use `settings.jsdoc.structuredTags` +to indicate that a tag's `name` is "namepath-defining" (and should prevent +reporting on use of that namepath elsewhere) and/or that a tag's `type` is +`false` (and should not be checked for types). + #### Options @@ -6073,7 +6147,7 @@ An option object may have the following key: |Aliases|`constructor`, `const`, `extends`, `var`, `arg`, `argument`, `prop`, `return`, `exception`, `yield`| |Closure-only|`package`, `private`, `protected`, `public`, `static`| |Options|`definedTypes`| -|Settings|`preferredTypes`, `mode`| +|Settings|`preferredTypes`, `mode`, `structuredTags`| The following patterns are considered problems: @@ -6218,6 +6292,30 @@ function quux () {} function quux () {} // Settings: {"jsdoc":{"mode":"closure"}} // Message: The type 'SomeType' is undefined. + +/** + * @aCustomTag {SomeType} + */ +function quux () {} +// Settings: {"jsdoc":{"structuredTags":{"aCustomTag":{"type":true}}}} +// Message: The type 'SomeType' is undefined. + +/** + * @namepathDefiner SomeType + */ +/** + * @type {SomeType} + */ +// Settings: {"jsdoc":{"structuredTags":{"namepathDefiner":{"name":"namepath-referencing"}}}} +// Message: The type 'SomeType' is undefined. + +/** + * @namepathDefiner SomeType + */ +/** + * @type {SomeType} + */ +// Message: The type 'SomeType' is undefined. ```` The following patterns are not considered problems: @@ -6510,6 +6608,20 @@ exports.resolve1 = function resolve1(value) { * @typedef {ValueType} ValueFunc */ // Settings: {"jsdoc":{"mode":"typescript"}} + +/** + * @aCustomTag {SomeType} + */ +function quux () {} +// Settings: {"jsdoc":{"structuredTags":{"aCustomTag":{"type":false}}}} + +/** + * @namepathDefiner SomeType + */ +/** + * @type {SomeType} + */ +// Settings: {"jsdoc":{"structuredTags":{"namepathDefiner":{"name":"namepath-defining"}}}} ```` @@ -9091,6 +9203,15 @@ class Foo { } // Options: [{"exemptEmptyConstructors":true,"require":{"MethodDefinition":true}}] // Message: Missing JSDoc comment. + +/** + * + */ +function quux() { + +} +// Settings: {"jsdoc":{"structuredTags":{"see":{"name":false,"required":["name"]}}}} +// Message: Cannot add "name" to `require` with the tag's `name` set to `false` ```` The following patterns are not considered problems: @@ -13599,15 +13720,15 @@ e.g., `@modifies`): The following tags have their name/namepath portion (the non-whitespace text after the tag name) checked: -1. Name(path)-defining tags requiring namepath: `@external`, `@host`, - `@name`, `@typedef`, and `@template` (TypeScript/Closure only); - `@param` (`@arg`, `@argument`) and `@property` +1. Name(path)-defining tags requiring namepath: `@event`, `@callback`, + `@external`, `@host`, `@name`, `@typedef`, and `@template` + (TypeScript/Closure only); `@param` (`@arg`, `@argument`) and `@property` (`@prop`) also fall into this category, but while this rule will check their namepath validity, we leave the requiring of the name portion to the rules `require-param-name` and `require-property-name`, respectively. 1. Name(path)-defining tags (which may have value without namepath or their - namepath can be expressed elsewhere on the block): `@event`, `@callback`, + namepath can be expressed elsewhere on the block): `@class`, `@constructor`, `@constant`, `@const`, `@function`, `@func`, `@method`, `@interface` (TypeScript tag only), `@member`, `@var`, `@mixin`, `@namespace`, `@module` (module paths are not planned for @@ -13638,6 +13759,13 @@ text after the tag name) checked: allow `#`, `.`, or `~` at the end (which is not allowed at the end of normal paths). +If you define your own tags, `settings.jsdoc.structuredTags` will allow +these custom tags to be checked, with the name portion of tags checked for +valid namepaths (based on the tag's `name` value), their type portions checked +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 @@ -13645,10 +13773,9 @@ text after the tag name) checked: empty name paths with namepath groups 2 and 4 (these might often be expected to have an accompanying name path, though they have some indicative value without one; these may also allow names to be defined - in another manner elsewhere in the block) -- `checkSeesForNamepaths` (default: false) - Set this to `true` to insist - that `@see` only use name paths (the tag is normally permitted to - allow other text) + in another manner elsewhere in the block); you can use + `settings.jsdoc.structuredTags` with the `required` key set to "name" if you + wish to require name paths on a tag-by-tag basis. ||| |---|---| @@ -13656,8 +13783,8 @@ text after the tag name) checked: |Tags|For name only unless otherwise stated: `alias`, `augments`, `borrows`, `callback`, `class` (for name and type), `constant` (for name and type), `enum` (for type), `event`, `external`, `fires`, `function`, `implements` (for type), `interface`, `lends`, `listens`, `member` (for name and type), `memberof`, `memberof!`, `mixes`, `mixin`, `modifies`, `module` (for name and type), `name`, `namespace` (for name and type), `param` (for name and type), `property` (for name and type), `returns` (for type), `see` (optionally for name), `this`, `throws` (for type), `type` (for type), `typedef` (for name and type), `yields` (for type)| |Aliases|`extends`, `constructor`, `const`, `host`, `emits`, `func`, `method`, `var`, `arg`, `argument`, `prop`, `return`, `exception`, `yield`| |Closure-only|For type only: `package`, `private`, `protected`, `public`, `static`| -|Options|`allowEmptyNamepaths`, `checkSeesForNamepaths`| -|Settings|`mode`| +|Options|`allowEmptyNamepaths`| +|Settings|`mode`, `structuredTags`| The following patterns are considered problems: @@ -13732,7 +13859,7 @@ function quux() { function quux() { } -// Options: [{"checkSeesForNamepaths":true}] +// Settings: {"jsdoc":{"structuredTags":{"see":{"name":"namepath-referencing","required":["name"]}}}} // Message: Syntax error in namepath: foo% /** @@ -13758,7 +13885,7 @@ function quux() { } // Options: [{"allowEmptyNamepaths":false}] -// Message: Tag @callback must have a name/namepath +// Message: Tag @callback must have a name/namepath. /** * @constant {str%ng} @@ -13777,16 +13904,24 @@ function quux() { // Message: Syntax error in namepath: UserStr%ng /** - * @extends + * @this */ class Bar {}; -// Message: Tag @extends must have either a type or namepath +// Options: [{"allowEmptyNamepaths":false}] +// Message: Tag @this must have either a type or namepath in "jsdoc" mode. + +/** + * @aCustomTag + */ +// Settings: {"jsdoc":{"structuredTags":{"aCustomTag":{"required":["typeOrNameRequired"]}}}} +// Options: [{"allowEmptyNamepaths":false}] +// Message: Tag @aCustomTag must have either a type or namepath. /** * @type */ let foo; -// Message: Tag @type must have a type +// Message: Tag @type must have a type. /** * @modifies {bar | foo<} @@ -13813,14 +13948,14 @@ function quux () {} */ function quux () {} // Settings: {"jsdoc":{"mode":"closure"}} -// Message: Tag @define must have a type +// Message: Tag @define must have a type in "closure" mode. /** * @this */ let foo; // Settings: {"jsdoc":{"mode":"closure"}} -// Message: Tag @this must have a type +// Message: Tag @this must have a type in "closure" mode. /** * Foo function. @@ -13849,12 +13984,19 @@ function foo(bar) {} // Settings: {"jsdoc":{"mode":"closure"}} // Message: @interface should not have a name in "closure" mode. +/** + * @aCustomTag name + */ +// Settings: {"jsdoc":{"structuredTags":{"aCustomTag":{"name":false}}}} +// Message: @aCustomTag should not have a name. + /** * @typedef {SomeType} */ function quux () {} // Settings: {"jsdoc":{"mode":"jsdoc"}} -// Message: @typedef must have a name in "jsdoc" mode. +// Options: [{"allowEmptyNamepaths":false}] +// Message: Tag @typedef must have a name/namepath in "jsdoc" mode. /** * @private {SomeType} @@ -13862,6 +14004,49 @@ function quux () {} function quux () {} // Settings: {"jsdoc":{"mode":"jsdoc"}} // Message: @private should not have a bracketed type in "jsdoc" mode. + +/** + * @aCustomTag {SomeType} + */ +function quux () {} +// Settings: {"jsdoc":{"structuredTags":{"aCustomTag":{"type":false}}}} +// Message: @aCustomTag should not have a bracketed type. + +/** + * @see foo% + */ +function quux() { + +} +// Settings: {"jsdoc":{"structuredTags":{"see":{"name":false,"required":["name"]}}}} +// Message: Cannot add "name" to `require` with the tag's `name` set to `false` + +/** + * @see foo% + */ +function quux() { + +} +// Settings: {"jsdoc":{"structuredTags":{"see":{"required":["type"],"type":false}}}} +// Message: Cannot add "type" to `require` with the tag's `type` set to `false` + +/** + * @see foo% + */ +function quux() { + +} +// Settings: {"jsdoc":{"structuredTags":{"see":{"name":false,"required":["typeOrNameRequired"]}}}} +// Message: Cannot add "typeOrNameRequired" to `require` with the tag's `name` set to `false` + +/** + * @see foo% + */ +function quux() { + +} +// Settings: {"jsdoc":{"structuredTags":{"see":{"required":["typeOrNameRequired"],"type":false}}}} +// Message: Cannot add "typeOrNameRequired" to `require` with the tag's `type` set to `false` ```` The following patterns are not considered problems: @@ -13944,7 +14129,7 @@ function quux() { function quux() { } -// Options: [{"checkSeesForNamepaths":true}] +// Settings: {"jsdoc":{"structuredTags":{"see":{"name":"namepath-referencing","required":["name"]}}}} /** * @@ -13975,6 +14160,13 @@ function quux() { } +/** + * @aCustomTag + */ +function quux() { + +} + /** * @constant {string} */ @@ -14075,6 +14267,7 @@ function foo(bar) {} */ function quux () {} // Settings: {"jsdoc":{"mode":"closure"}} +// Options: [{"allowEmptyNamepaths":false}] /** * @private {SomeType} @@ -14089,6 +14282,14 @@ function quux() { } // Options: [{"allowEmptyNamepaths":false}] + +/** + * @see + */ +function quux() { + +} +// Settings: {"jsdoc":{"structuredTags":{"see":{"name":"namepath-referencing"}}}} ```` diff --git a/src/getDefaultTagStructureForMode.js b/src/getDefaultTagStructureForMode.js new file mode 100644 index 000000000..7aba7ad68 --- /dev/null +++ b/src/getDefaultTagStructureForMode.js @@ -0,0 +1,464 @@ +const getDefaultTagStructureForMode = (mode) => { + const isJsdoc = mode === 'jsdoc'; + const isClosure = mode === 'closure'; + const isTypescript = mode === 'typescript'; + const isPermissive = mode === 'permissive'; + + const isJsdocOrTypescript = isJsdoc || isTypescript; + const isTypescriptOrClosure = isTypescript || isClosure; + const isClosureOrPermissive = isClosure || isPermissive; + const isJsdocTypescriptOrPermissive = isJsdocOrTypescript || isPermissive; + + // Properties: + // `nameContents` - 'namepath-referencing'|'namepath-defining'|'text'|false + // `typeAllowed` - boolean + // `nameRequired` - boolean + // `typeRequired` - boolean + // `typeOrNameRequired` - boolean + + // All of `typeAllowed` have a signature with "type" except for + // `augments`/`extends` ("namepath") + // `param`/`arg`/`argument` (no signature) + // `property`/`prop` (no signature) + // `modifies` (undocumented) + + // None of the `nameContents: 'namepath-defining'` show as having curly + // brackets for their name/namepath + + // Among `namepath-defining` and `namepath-referencing`, these do not seem + // to allow curly brackets in their doc signature or examples (`modifies` + // references namepaths within its type brackets and `param` is + // name-defining but not namepath-defining, so not part of these groups) + + // Todo: Should support special processing for "name" as distinct from + // "namepath" (e.g., param can't define a namepath) + + // Once checking inline tags: + // Todo: Re: `typeOrNameRequired`, `@link` (or @linkcode/@linkplain) seems + // to require a namepath OR URL and might be checked as such. + // Todo: Should support a `tutorialID` type (for `@tutorial` block and + // inline) + + return new Map([ + ['alias', new Map([ + // Signature seems to require a "namepath" (and no counter-examples) + ['nameContents', 'namepath-referencing'], + + // "namepath" + ['typeOrNameRequired', true], + ])], + + ['arg', new Map([ + ['nameContents', 'namepath-defining'], + + // See `param` + ['nameRequired', true], + + // Has no formal signature in the docs but shows curly brackets + // in the examples + ['typeAllowed', true], + ])], + + ['argument', new Map([ + ['nameContents', 'namepath-defining'], + + // See `param` + ['nameRequired', true], + + // Has no formal signature in the docs but shows curly brackets + // in the examples + ['typeAllowed', true], + ])], + + ['augments', new Map([ + // Signature seems to require a "namepath" (and no counter-examples) + ['nameContents', 'namepath-referencing'], + + // Does not show curly brackets in either the signature or examples + ['typeAllowed', true], + + // "namepath" + ['typeOrNameRequired', true], + ])], + + ['borrows', new Map([ + // `borrows` has a different format, however, so needs special parsing; + // seems to require both, and as "namepath"'s + ['nameContents', 'namepath-referencing'], + + // "namepath" + ['typeOrNameRequired', true], + ])], + + ['callback', new Map([ + // Seems to require a "namepath" in the signature (with no + // counter-examples) + ['nameContents', 'namepath-defining'], + + // "namepath" + ['nameRequired', true], + ])], + + ['class', new Map([ + // Allows for "name"'s in signature, but indicated as optional + ['nameContents', 'namepath-defining'], + + ['typeAllowed', true], + ])], + + ['const', new Map([ + // Allows for "name"'s in signature, but indicated as optional + ['nameContents', 'namepath-defining'], + + ['typeAllowed', true], + ])], + ['constant', new Map([ + // Allows for "name"'s in signature, but indicated as optional + ['nameContents', 'namepath-defining'], + + ['typeAllowed', true], + ])], + ['constructor', new Map([ + // Allows for "name"'s in signature, but indicated as optional + ['nameContents', 'namepath-defining'], + + ['typeAllowed', true], + ])], + + ['define', new Map([ + ['typeRequired', isClosure], + ])], + + ['emits', new Map([ + // Signature seems to require a "name" (of an event) and no counter-examples + ['nameContents', 'namepath-referencing'], + ])], + + ['enum', new Map([ + // Has example showing curly brackets but not in doc signature + ['typeAllowed', true], + ])], + + ['event', new Map([ + // The doc signature of `event` seems to require a "name" + ['nameRequired', true], + + // Appears to require a "name" in its signature, albeit somewhat + // different from other "name"'s (including as described + // at https://jsdoc.app/about-namepaths.html ) + ['nameContents', 'namepath-defining'], + ])], + + ['exception', new Map([ + // Shows curly brackets in the signature and in the examples + ['typeAllowed', true], + ])], + + ['export', new Map([ + ['typeAllowed', isClosureOrPermissive], + ])], + + ['extends', new Map([ + // Signature seems to require a "namepath" (and no counter-examples) + ['nameContents', 'namepath-referencing'], + + // Does not show curly brackets in either the signature or examples + ['typeAllowed', isClosureOrPermissive], + + ['nameRequired', isJsdocOrTypescript], + + // "namepath" + ['typeOrNameRequired', isClosureOrPermissive], + ])], + + ['external', new Map([ + // Appears to require a "name" in its signature, albeit somewhat + // different from other "name"'s (including as described + // at https://jsdoc.app/about-namepaths.html ) + ['nameContents', 'namepath-defining'], + + // "name" (and a special syntax for the `external` name) + ['nameRequired', true], + ])], + + ['fires', new Map([ + // Signature seems to require a "name" (of an event) and no + // counter-examples + ['nameContents', 'namepath-referencing'], + ])], + + ['function', new Map([ + // Allows for "name"'s in signature, but indicated as optional + ['nameContents', 'namepath-defining'], + ])], + ['func', new Map([ + // Allows for "name"'s in signature, but indicated as optional + ['nameContents', 'namepath-defining'], + ])], + + ['host', new Map([ + // Appears to require a "name" in its signature, albeit somewhat + // different from other "name"'s (including as described + // at https://jsdoc.app/about-namepaths.html ) + ['nameContents', 'namepath-defining'], + + // See `external` + ['nameRequired', true], + + // "namepath" + ['typeOrNameRequired', true], + ])], + + ['interface', new Map([ + // Allows for "name" in signature, but indicates as optional + [ + 'nameContents', + isJsdocTypescriptOrPermissive ? 'namepath-defining' : false, + ], + ])], + + ['implements', new Map([ + // Shows curly brackets in the doc signature and examples + // "typeExpression" + ['typeRequired', true], + ])], + + ['lends', new Map([ + // Signature seems to require a "namepath" (and no counter-examples) + ['nameContents', 'namepath-referencing'], + + // "namepath" + ['typeOrNameRequired', true], + ])], + + ['listens', new Map([ + // Signature seems to require a "name" (of an event) and no + // counter-examples + ['nameContents', 'namepath-referencing'], + ])], + + ['member', new Map([ + // Allows for "name"'s in signature, but indicated as optional + ['nameContents', 'namepath-defining'], + + // Has example showing curly brackets but not in doc signature + ['typeAllowed', true], + ])], + + ['memberof', new Map([ + // Signature seems to require a "namepath" (and no counter-examples), + // though it allows an incomplete namepath ending with connecting symbol + ['nameContents', 'namepath-referencing'], + + // "namepath" + ['typeOrNameRequired', true], + ])], + ['memberof!', new Map([ + // Signature seems to require a "namepath" (and no counter-examples), + // though it allows an incomplete namepath ending with connecting symbol + ['nameContents', 'namepath-referencing'], + + // "namepath" + ['typeOrNameRequired', true], + ])], + + ['method', new Map([ + // Allows for "name"'s in signature, but indicated as optional + ['nameContents', 'namepath-defining'], + ])], + ['mixes', new Map([ + // Signature seems to require a "OtherObjectPath" with no + // counter-examples + ['nameContents', 'namepath-referencing'], + + // "OtherObjectPath" + ['typeOrNameRequired', true], + ])], + + ['mixin', new Map([ + // Allows for "name"'s in signature, but indicated as optional + ['nameContents', 'namepath-defining'], + ])], + + ['modifies', new Map([ + // Has no documentation, but test example has curly brackets, and + // "name" would be suggested rather than "namepath" based on example; + // not sure if name is required + ['typeAllowed', true], + ])], + + ['module', new Map([ + // Optional "name" and no curly brackets + // this block impacts `no-undefined-types` and `valid-types` (search for + // "isNamepathDefiningTag|tagMightHaveNamepath|tagMightHaveEitherTypeOrNamePosition") + ['nameContents', isJsdoc ? 'namepath-defining' : 'text'], + + // Shows the signature with curly brackets but not in the example + ['typeAllowed', true], + ])], + + ['name', new Map([ + // Seems to require a "namepath" in the signature (with no + // counter-examples) + ['nameContents', 'namepath-defining'], + + // "namepath" + ['nameRequired', true], + + // "namepath" + ['typeOrNameRequired', true], + ])], + + ['namespace', new Map([ + // Allows for "name"'s in signature, but indicated as optional + ['nameContents', 'namepath-defining'], + + // Shows the signature with curly brackets but not in the example + ['typeAllowed', true], + ])], + ['package', new Map([ + // Shows the signature with curly brackets but not in the example + // "typeExpression" + ['typeAllowed', isClosureOrPermissive], + ])], + + ['param', new Map([ + ['nameContents', 'namepath-defining'], + + // Though no signature provided requiring, per + // https://jsdoc.app/tags-param.html: + // "The @param tag requires you to specify the name of the parameter you + // are documenting." + ['nameRequired', true], + + // Has no formal signature in the docs but shows curly brackets + // in the examples + ['typeAllowed', true], + ])], + + ['private', new Map([ + // Shows the signature with curly brackets but not in the example + // "typeExpression" + ['typeAllowed', isClosureOrPermissive], + ])], + + ['prop', new Map([ + ['nameContents', 'namepath-defining'], + + // See `property` + ['nameRequired', true], + + // Has no formal signature in the docs but shows curly brackets + // in the examples + ['typeAllowed', true], + ])], + + ['property', new Map([ + ['nameContents', 'namepath-defining'], + + // No docs indicate required, but since parallel to `param`, we treat as + // such: + ['nameRequired', true], + + // Has no formal signature in the docs but shows curly brackets + // in the examples + ['typeAllowed', true], + ])], + + ['protected', new Map([ + // Shows the signature with curly brackets but not in the example + // "typeExpression" + ['typeAllowed', isClosureOrPermissive], + ])], + + ['public', new Map([ + // Does not show a signature nor show curly brackets in the example + ['typeAllowed', isClosureOrPermissive], + ])], + + ['returns', new Map([ + // Shows curly brackets in the signature and in the examples + ['typeAllowed', true], + ])], + ['return', new Map([ + // Shows curly brackets in the signature and in the examples + ['typeAllowed', true], + ])], + + ['see', new Map([ + // Signature allows for "namepath" or text, so user must configure to + // 'namepath-referencing' to enforce checks + ['nameContents', 'text'], + ])], + + ['static', new Map([ + // Does not show a signature nor show curly brackets in the example + ['typeAllowed', isClosureOrPermissive], + ])], + + ['template', new Map([ + ['nameContents', isJsdoc ? 'text' : 'namepath-referencing'], + + // Though defines `nameContents: 'namepath-defining'` in a sense, it is + // not parseable in the same way for template (e.g., allowing commas), + // so not adding + ['typeAllowed', isTypescriptOrClosure || isPermissive], + ])], + + ['this', new Map([ + // Signature seems to require a "namepath" (and no counter-examples) + // Not used with namepath in Closure/TypeScript, however + ['nameContents', isJsdoc ? 'namepath-referencing' : false], + + ['typeRequired', isTypescriptOrClosure], + + // namepath + ['typeOrNameRequired', isJsdoc], + ])], + + ['throws', new Map([ + // Shows curly brackets in the signature and in the examples + ['typeAllowed', true], + ])], + + ['type', new Map([ + // Shows curly brackets in the doc signature and examples + // "typeName" + ['typeRequired', true], + ])], + + ['typedef', new Map([ + // Seems to require a "namepath" in the signature (with no + // counter-examples) + ['nameContents', 'namepath-defining'], + + // "namepath" + ['nameRequired', isJsdocTypescriptOrPermissive], + + // Has example showing curly brackets but not in doc signature + ['typeAllowed', true], + + // "namepath" + ['typeOrNameRequired', true], + ])], + + ['var', new Map([ + // Allows for "name"'s in signature, but indicated as optional + ['nameContents', 'namepath-defining'], + + // Has example showing curly brackets but not in doc signature + ['typeAllowed', true], + ])], + + ['yields', new Map([ + // Shows curly brackets in the signature and in the examples + ['typeAllowed', true], + ])], + ['yield', new Map([ + // Shows curly brackets in the signature and in the examples + ['typeAllowed', true], + ])], + ]); +}; + +export default getDefaultTagStructureForMode; diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index 07830e70d..098d73d0c 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -271,32 +271,60 @@ const getUtils = ( return false; }; - utils.tagMustHaveEitherTypeOrNamePosition = (tagName) => { - return jsdocUtils.tagMustHaveEitherTypeOrNamePosition(mode, tagName); - }; + [ + 'tagMightHaveNamePosition', + 'tagMightHaveTypePosition', + ].forEach((method) => { + utils[method] = (tagName, otherModeMaps) => { + const result = jsdocUtils[method](tagName); + if (result) { + return true; + } - utils.tagMightHaveEitherTypeOrNamePosition = (tagName) => { - return jsdocUtils.tagMightHaveEitherTypeOrNamePosition(mode, tagName); - }; + if (!otherModeMaps) { + return false; + } - utils.tagMustHaveNamePosition = (tagName) => { - return jsdocUtils.tagMustHaveNamePosition(tagName); - }; + const otherResult = otherModeMaps.some((otherModeMap) => { + return jsdocUtils[method](tagName, otherModeMap); + }); - utils.tagMightHaveNamePosition = (tagName) => { - return jsdocUtils.tagMightHaveNamePosition(mode, tagName); - }; + return otherResult ? {otherMode: true} : false; + }; + }); - utils.tagMustHaveTypePosition = (tagName) => { - return jsdocUtils.tagMustHaveTypePosition(mode, tagName); - }; + [ + 'tagMustHaveNamePosition', + 'tagMustHaveTypePosition', + 'tagMissingRequiredTypeOrNamepath', + ].forEach((method) => { + utils[method] = (tagName, otherModeMaps) => { + const result = jsdocUtils[method](tagName); + if (!result) { + return false; + } - utils.tagMightHaveTypePosition = (tagName) => { - return jsdocUtils.tagMightHaveTypePosition(mode, tagName); - }; + // if (!otherModeMaps) { return true; } - utils.isNamepathDefiningTag = (tagName) => { - return jsdocUtils.isNamepathDefiningTag(mode, tagName); + const otherResult = otherModeMaps.every((otherModeMap) => { + return jsdocUtils[method](tagName, otherModeMap); + }); + + return otherResult ? true : {otherMode: false}; + }; + }); + + [ + 'isNamepathDefiningTag', + 'tagMightHaveNamepath', + ].forEach((method) => { + utils[method] = (tagName) => { + return jsdocUtils[method](tagName); + }; + }); + + utils.getTagStructureForMode = (mde) => { + return jsdocUtils.getTagStructureForMode(mde, settings.structuredTags); }; utils.hasDefinedTypeReturnTag = (tag) => { @@ -410,6 +438,9 @@ const getSettings = (context) => { // `check-types` and `no-undefined-types` preferredTypes: context.settings.jsdoc?.preferredTypes ?? {}, + // `check-types`, `no-undefined-types`, `valid-types` + structuredTags: context.settings.jsdoc?.structuredTags ?? {}, + // `require-param`, `require-description`, `require-example`, `require-returns` overrideReplacesDocs: context.settings.jsdoc?.overrideReplacesDocs, implementsReplacesDocs: context.settings.jsdoc?.implementsReplacesDocs, @@ -421,6 +452,23 @@ const getSettings = (context) => { }; /* eslint-enable sort-keys-fix/sort-keys-fix */ + jsdocUtils.setTagStructure(settings.mode); + try { + jsdocUtils.overrideTagStructure(settings.structuredTags); + } catch (error) { + context.report({ + loc: { + start: { + column: 1, + line: 1, + }, + }, + message: error.message, + }); + + return false; + } + return settings; }; @@ -537,9 +585,9 @@ const iterate = ( const iterateAllJsdocs = (iterator, ruleConfig) => { const trackedJsdocs = []; + let settings; const callIterator = (context, node, jsdocNodes, state, lastCall) => { const sourceCode = context.getSourceCode(); - const settings = getSettings(context); const {lines} = sourceCode; const utils = getBasicUtils(context, settings); @@ -565,7 +613,11 @@ const iterateAllJsdocs = (iterator, ruleConfig) => { return { create (context) { const sourceCode = context.getSourceCode(); - const settings = getSettings(context); + settings = getSettings(context); + if (!settings) { + return {}; + } + const state = {}; return { @@ -620,6 +672,9 @@ const checkFile = (iterator, ruleConfig) => { create (context) { const sourceCode = context.getSourceCode(); const settings = getSettings(context); + if (!settings) { + return {}; + } return { 'Program:exit' () { @@ -694,6 +749,9 @@ export default function iterateJsdoc (iterator, ruleConfig) { const sourceCode = context.getSourceCode(); const settings = getSettings(context); + if (!settings) { + return {}; + } const {lines} = sourceCode; const checkJsdoc = (node) => { diff --git a/src/jsdocUtils.js b/src/jsdocUtils.js index a287a41b8..0cac1357d 100644 --- a/src/jsdocUtils.js +++ b/src/jsdocUtils.js @@ -1,9 +1,16 @@ import _ from 'lodash'; import {jsdocTags, closureTags, typeScriptTags} from './tagNames'; import WarnSettings from './WarnSettings'; +import getDefaultTagStructureForMode from './getDefaultTagStructureForMode'; type ParserMode = "jsdoc"|"typescript"|"closure"; +let tagStructure; + +const setTagStructure = (mode) => { + tagStructure = getDefaultTagStructureForMode(mode); +}; + // Given a nested array of property names, reduce them to a single array, // appending the name of the root element along the way if present. const flattenRoots = (params, root = '') => { @@ -315,291 +322,129 @@ const hasDefinedTypeReturnTag = (tag) => { return true; }; -const tagsWithMandatoryTypePosition = new Set([ - // These both show curly brackets in the doc signature and examples - // "typeExpression" - 'implements', - - // "typeName" - 'type', -]); - -const tagsWithMandatoryTypePositionTypeScript = new Set([ - ...tagsWithMandatoryTypePosition, - 'this', -]); - -const tagsWithMandatoryTypePositionClosure = new Set([ - ...tagsWithMandatoryTypePositionTypeScript, - 'define', -]); - -// All of these have a signature with "type" except for -// `augments`/`extends` ("namepath") -// `param`/`arg`/`argument` (no signature) -// `property`/`prop` (no signature) -// `modifies` (undocumented) -const tagsWithOptionalTypePosition = new Set([ - // These have the example showing curly brackets but not in their doc signature, e.g.: https://jsdoc.app/tags-enum.html - 'enum', - 'member', 'var', - - 'typedef', - - // These do not show curly brackets in either the signature or examples - 'augments', 'extends', - - 'class', 'constructor', - 'constant', 'const', - - // These show the signature with curly brackets but not in the example - 'module', - 'namespace', - - // These have no formal signature in the docs but show curly brackets - // in the examples - 'param', 'arg', 'argument', - 'property', 'prop', - - // These show curly brackets in the signature and in the examples - 'returns', 'return', - 'throws', 'exception', - 'yields', 'yield', - - // Has no documentation, but test example has curly brackets, and - // "name" would be suggested rather than "namepath" based on example; not - // sure if name is required - 'modifies', -]); - -const tagsWithOptionalTypePositionTypescript = new Set([ - 'template', - ...tagsWithOptionalTypePosition, -]); - -const tagsWithOptionalTypePositionClosure = new Set([ - ...tagsWithOptionalTypePositionTypescript, - - 'export', - - // Shows the signature with curly brackets but not in the example - // "typeExpression" - 'package', - 'private', - 'protected', - - // These do not show a signature nor show curly brackets in the example - 'public', - 'static', -]); - -// None of these show as having curly brackets for their name/namepath -const closureNamepathDefiningTags = new Set([ - // Though defines in a sense, it is not parseable in the same way - // for template (e.g., allowing commas) - // 'template', - - // These appear to require a "name" in their signature, albeit these - // are somewhat different from other "name"'s (including as described - // at https://jsdoc.app/about-namepaths.html ) - 'external', 'host', - 'event', - - // These allow for "name"'s in their signature, but indicate as optional - 'class', 'constructor', - 'constant', 'const', - 'function', 'func', 'method', - 'member', 'var', - 'mixin', - 'namespace', - - // These seem to all require a "namepath" in their signatures (with no counter-examples) - 'name', - 'typedef', - 'callback', -]); - -const typescriptNamepathDefiningTags = new Set([ - ...closureNamepathDefiningTags, - - // Allows for "name" in signature, but indicates as optional - 'interface', -]); - -const namepathDefiningTags = new Set([ - ...typescriptNamepathDefiningTags, - - // Optional "name" and no curly brackets - // this block impacts `no-undefined-types` and `valid-types` (search for - // "isNamepathDefiningTag|tagMightHaveNamePosition|tagMightHaveEitherTypeOrNamePosition") - 'module', -]); - -// These *reference* names/namepaths and do not define them -const tagsWithOptionalNamePositionBase = new Set([ - // `borrows` has a different format, however, so needs special parsing; - // seems to require both, and as "namepath"'s - 'borrows', +const ensureMap = (map, tag) => { + if (!map.has(tag)) { + map.set(tag, new Map()); + } - // Signature seems to require a "name" (of an event) and no counter-examples - 'emits', 'fires', - 'listens', + return map.get(tag); +}; - // Signature seems to require a "namepath" (and no counter-examples) - 'alias', - 'augments', 'extends', - 'lends', +const overrideTagStructure = (structuredTags, tagMap = tagStructure) => { + Object.entries(structuredTags).forEach(([tag, { + name, type, required = [], + }]) => { + const tagStruct = ensureMap(tagMap, tag); - // Signature seems to require a "namepath" (and no counter-examples), - // though it allows an incomplete namepath ending with connecting symbol - 'memberof', 'memberof!', + tagStruct.set('nameContents', name); + tagStruct.set('typeAllowed', type); - // Signature seems to require a "OtherObjectPath" with no counter-examples - 'mixes', + const requiredName = required.includes('name'); + if (requiredName && name === false) { + throw new Error('Cannot add "name" to `require` with the tag\'s `name` set to `false`'); + } + tagStruct.set('nameRequired', requiredName); - // Signature allows for "namepath" or text - 'see', -]); + const requiredType = required.includes('type'); + if (requiredType && type === false) { + throw new Error('Cannot add "type" to `require` with the tag\'s `type` set to `false`'); + } + tagStruct.set('typeRequired', requiredType); -// The following do not seem to allow curly brackets in their doc -// signature or examples (besides `modifies` and `param`) -const tagsWithOptionalNamePosition = new Set([ - // Signature seems to require a "namepath" (and no counter-examples) - // Not used with namepath in Closure/TypeScript, however - 'this', - ...namepathDefiningTags, - ...tagsWithOptionalNamePositionBase, -]); + const typeOrNameRequired = required.includes('typeOrNameRequired'); + if (typeOrNameRequired && name === false) { + throw new Error('Cannot add "typeOrNameRequired" to `require` with the tag\'s `name` set to `false`'); + } + if (typeOrNameRequired && type === false) { + throw new Error('Cannot add "typeOrNameRequired" to `require` with the tag\'s `type` set to `false`'); + } + tagStruct.set('typeOrNameRequired', typeOrNameRequired); + }); +}; -const typescriptTagsWithOptionalNamePosition = new Set([ - 'template', - ...typescriptNamepathDefiningTags, - ...tagsWithOptionalNamePositionBase, -]); +const getTagStructureForMode = (mode, structuredTags) => { + const tagStruct = getDefaultTagStructureForMode(mode); -const closureTagsWithOptionalNamePosition = new Set([ - 'template', - ...closureNamepathDefiningTags, - ...tagsWithOptionalNamePositionBase, -]); + try { + overrideTagStructure(structuredTags, tagStruct); + } catch { + // + } -// Todo: `@link` seems to require a namepath OR URL and might be checked as such. + return tagStruct; +}; -// The doc signature of `event` seems to require a "name" -const tagsWithMandatoryNamePosition = new Set([ - // Though no signature provided requiring, per https://jsdoc.app/tags-param.html: - // "The @param tag requires you to specify the name of the parameter you are documenting." - 'param', - 'arg', - 'argument', +const isNamepathDefiningTag = (tag, tagMap = tagStructure) => { + const tagStruct = ensureMap(tagMap, tag); - // No docs indicate required, but since parallel to `param`, we treat as such: - 'property', - 'prop', + return tagStruct.get('nameContents') === 'namepath-defining'; +}; - // "name" (and a special syntax for the `external` name) - 'external', 'host', +const tagMustHaveTypePosition = (tag, tagMap = tagStructure) => { + const tagStruct = ensureMap(tagMap, tag); - // "namepath" - 'callback', - 'name', - 'typedef', -]); + return tagStruct.get('typeRequired'); +}; -const tagsWithMandatoryTypeOrNamePositionBase = new Set([ - // "namepath" - 'alias', - 'augments', 'extends', - 'borrows', - 'lends', - 'memberof', 'memberof!', - 'name', - 'typedef', +const tagMightHaveTypePosition = (tag, tagMap = tagStructure) => { + if (tagMustHaveTypePosition(tag, tagMap)) { + return true; + } - 'external', 'host', + const tagStruct = ensureMap(tagMap, tag); - // "OtherObjectPath" - 'mixes', -]); + const ret = tagStruct.get('typeAllowed'); -const tagsWithMandatoryTypeOrNamePosition = new Set([ - // namepath - 'this', - ...tagsWithMandatoryTypeOrNamePositionBase, -]); + return ret === undefined ? true : ret; +}; -const tagsWithMandatoryTypeOrNamePositionTypescript = new Set([ - ...tagsWithMandatoryTypeOrNamePositionBase, +const namepathTypes = new Set([ + 'namepath-defining', 'namepath-referencing', ]); -const tagsWithMandatoryTypeOrNamePositionClosure = new Set([ - ...tagsWithMandatoryTypeOrNamePositionBase, -]); +const tagMightHaveNamePosition = (tag, tagMap = tagStructure) => { + const tagStruct = ensureMap(tagMap, tag); -const isNamepathDefiningTag = (mode, tagName) => { - if (mode === 'closure') { - return closureNamepathDefiningTags.has(tagName); - } - if (mode === 'typescript') { - return typescriptNamepathDefiningTags.has(tagName); - } + const ret = tagStruct.get('nameContents'); - return namepathDefiningTags.has(tagName); + return ret === undefined ? true : Boolean(ret); }; -const tagMightHaveTypePosition = (mode, tag) => { - if (mode === 'closure') { - return tagsWithMandatoryTypePositionClosure.has(tag) || - tagsWithOptionalTypePositionClosure.has(tag); - } - if (mode === 'typescript') { - return tagsWithMandatoryTypePositionTypeScript.has(tag) || - tagsWithOptionalTypePositionTypescript.has(tag); - } +const tagMightHaveNamepath = (tag, tagMap = tagStructure) => { + const tagStruct = ensureMap(tagMap, tag); - return tagsWithMandatoryTypePosition.has(tag) || - tagsWithOptionalTypePosition.has(tag); + return namepathTypes.has(tagStruct.get('nameContents')); }; -const tagMustHaveTypePosition = (mode, tag) => { - if (mode === 'closure') { - return tagsWithMandatoryTypePositionClosure.has(tag); - } - if (mode === 'typescript') { - return tagsWithMandatoryTypePositionTypeScript.has(tag); - } +const tagMustHaveNamePosition = (tag, tagMap = tagStructure) => { + const tagStruct = ensureMap(tagMap, tag); - return tagsWithMandatoryTypePosition.has(tag); + return tagStruct.get('nameRequired'); }; -const tagMightHaveNamePosition = (mode, tag) => { - if (mode === 'closure') { - return closureTagsWithOptionalNamePosition.has(tag); - } - if (mode === 'typescript') { - return typescriptTagsWithOptionalNamePosition.has(tag); - } - - return tagsWithOptionalNamePosition.has(tag); +const tagMightHaveEitherTypeOrNamePosition = (tag, tagMap) => { + return tagMightHaveTypePosition(tag, tagMap) || tagMightHaveNamepath(tag, tagMap); }; -const tagMustHaveNamePosition = (tag) => { - return tagsWithMandatoryNamePosition.has(tag); -}; +const tagMustHaveEitherTypeOrNamePosition = (tag, tagMap) => { + const tagStruct = ensureMap(tagMap, tag); -const tagMightHaveEitherTypeOrNamePosition = (mode, tag) => { - return tagMightHaveTypePosition(mode, tag) || tagMightHaveNamePosition(mode, tag); + return tagStruct.get('typeOrNameRequired'); }; -const tagMustHaveEitherTypeOrNamePosition = (mode, tag) => { - if (mode === 'closure') { - return tagsWithMandatoryTypeOrNamePositionClosure.has(tag); - } - if (mode === 'typescript') { - return tagsWithMandatoryTypeOrNamePositionTypescript.has(tag); - } +const tagMissingRequiredTypeOrNamepath = (tag, tagMap = tagStructure) => { + const mustHaveTypePosition = tagMustHaveTypePosition(tag.tag, tagMap); + const mightHaveTypePosition = tagMightHaveTypePosition(tag.tag, tagMap); + const hasTypePosition = mightHaveTypePosition && Boolean(tag.type); + const hasNameOrNamepathPosition = ( + tagMustHaveNamePosition(tag.tag, tagMap) || + tagMightHaveNamepath(tag.tag, tagMap) + ) && Boolean(tag.name); + const mustHaveEither = tagMustHaveEitherTypeOrNamePosition(tag.tag, tagMap); + const hasEither = tagMightHaveEitherTypeOrNamePosition(tag.tag, tagMap) && + (hasTypePosition || hasNameOrNamepathPosition); - return tagsWithMandatoryTypeOrNamePosition.has(tag); + return mustHaveEither && !hasEither && !mustHaveTypePosition; }; /** @@ -882,6 +727,7 @@ export default { getJsdocTagsDeep, getPreferredTagName, getTagsByType, + getTagStructureForMode, hasATag, hasDefinedTypeReturnTag, hasReturnValue, @@ -892,11 +738,13 @@ export default { isNamepathDefiningTag, isSetter, isValidTag, + overrideTagStructure, parseClosureTemplateTag, - tagMightHaveEitherTypeOrNamePosition, + setTagStructure, + tagMightHaveNamepath, tagMightHaveNamePosition, tagMightHaveTypePosition, - tagMustHaveEitherTypeOrNamePosition, + tagMissingRequiredTypeOrNamepath, tagMustHaveNamePosition, tagMustHaveTypePosition, }; diff --git a/src/rules/requireJsdoc.js b/src/rules/requireJsdoc.js index e70629bd1..62a4a95f7 100644 --- a/src/rules/requireJsdoc.js +++ b/src/rules/requireJsdoc.js @@ -166,6 +166,9 @@ export default { create (context) { const sourceCode = context.getSourceCode(); const settings = getSettings(context); + if (!settings) { + return {}; + } const { require: requireOption, diff --git a/src/rules/validTypes.js b/src/rules/validTypes.js index fa1107a0a..3eed1753c 100644 --- a/src/rules/validTypes.js +++ b/src/rules/validTypes.js @@ -12,7 +12,6 @@ export default iterateJsdoc(({ }) => { const { allowEmptyNamepaths = true, - checkSeesForNamepaths = false, } = context.options[0] || {}; const {mode} = settings; if (!jsdoc.tags) { @@ -72,29 +71,7 @@ export default iterateJsdoc(({ return true; }; - const hasTypePosition = utils.tagMightHaveTypePosition(tag.tag) && Boolean(tag.type); - const mustHaveTypePosition = utils.tagMustHaveTypePosition(tag.tag); - - const hasNameOrNamepathPosition = ( - utils.tagMustHaveNamePosition(tag.tag) || - utils.tagMightHaveNamePosition(tag.tag) - ) && Boolean(tag.name) && !(tag.tag === 'see' && !checkSeesForNamepaths); - - // Don't handle `@param` here though it does require name as handled by - // `require-param-name` (`@property` would similarly seem to require one, - // but is handled by `require-property-name`) - const mustHaveNameOrNamepathPosition = ![ - 'param', 'arg', 'argument', - 'property', 'prop', - ].includes(tag.tag) && - utils.tagMustHaveNamePosition(tag.tag) && !allowEmptyNamepaths; - - const hasEither = utils.tagMightHaveEitherTypeOrNamePosition(tag.tag) && (hasTypePosition || hasNameOrNamepathPosition); - const mustHaveEither = utils.tagMustHaveEitherTypeOrNamePosition(tag.tag); - - let skip; - switch (tag.tag) { - case 'borrows': { + if (tag.tag === 'borrows') { const thisNamepath = tag.description.replace(asExpression, ''); if (!asExpression.test(tag.description) || !thisNamepath) { @@ -108,53 +85,82 @@ export default iterateJsdoc(({ validNamepathParsing(thatNamepath); } - break; + + return; } - case 'extends': - case 'package': case 'private': case 'protected': case 'public': case 'static': { - if (mode !== 'closure' && mode !== 'permissive' && tag.type) { - report(`@${tag.tag} should not have a bracketed type in "${mode}" mode.`, null, tag); - break; - } - skip = true; + + const otherModeMaps = ['jsdoc', 'typescript', 'closure', 'permissive'].filter( + (mde) => { + return mde !== mode; + }, + ).map((mde) => { + return utils.getTagStructureForMode(mde); + }); + + const tagMightHaveNamePosition = utils.tagMightHaveNamePosition(tag.tag, otherModeMaps); + if (tagMightHaveNamePosition !== true && tag.name) { + const modeInfo = tagMightHaveNamePosition === false ? '' : ` in "${mode}" mode`; + report(`@${tag.tag} should not have a name${modeInfo}.`, null, tag); + + return; } - // Fallthrough - case 'typedef': { - if (!skip && mode !== 'closure' && mode !== 'permissive' && !tag.name) { - report(`@typedef must have a name in "${mode}" mode.`, null, tag); - break; - } - skip = true; + const mightHaveTypePosition = utils.tagMightHaveTypePosition(tag.tag, otherModeMaps); + if (mightHaveTypePosition !== true && tag.type) { + const modeInfo = mightHaveTypePosition === false ? '' : ` in "${mode}" mode`; + report(`@${tag.tag} should not have a bracketed type${modeInfo}.`, null, tag); + + return; } - // Fallthrough - case 'interface': { - if (!skip && mode === 'closure' && tag.name) { - report('@interface should not have a name in "closure" mode.', null, tag); - break; - } + // REQUIRED NAME + const tagMustHaveNamePosition = utils.tagMustHaveNamePosition(tag.tag, otherModeMaps); + + // Don't handle `@param` here though it does require name as handled by + // `require-param-name` (`@property` would similarly seem to require one, + // but is handled by `require-property-name`) + if (tagMustHaveNamePosition !== false && !tag.name && !allowEmptyNamepaths && ![ + 'param', 'arg', 'argument', + 'property', 'prop', + ].includes(tag.tag)) { + const modeInfo = tagMustHaveNamePosition === true ? '' : ` in "${mode}" mode`; + report(`Tag @${tag.tag} must have a name/namepath${modeInfo}.`, null, tag); + + return; } - // Fallthrough - default: { - if (mustHaveEither && !hasEither && !mustHaveTypePosition) { - report(`Tag @${tag.tag} must have either a type or namepath`, null, tag); + // REQUIRED TYPE + const mustHaveTypePosition = utils.tagMustHaveTypePosition(tag.tag, otherModeMaps); + if (mustHaveTypePosition !== false && !tag.type) { + const modeInfo = mustHaveTypePosition === true ? '' : ` in "${mode}" mode`; + report(`Tag @${tag.tag} must have a type${modeInfo}.`, null, tag); - return; - } - if (hasTypePosition) { - validTypeParsing(tag.type); - } else if (mustHaveTypePosition) { - report(`Tag @${tag.tag} must have a type`, null, tag); - } + return; + } - if (hasNameOrNamepathPosition) { - validNamepathParsing(tag.name, tag.tag); - } else if (mustHaveNameOrNamepathPosition) { - report(`Tag @${tag.tag} must have a name/namepath`, null, tag); - } + // REQUIRED TYPE OR NAME/NAMEPATH + const tagMissingRequiredTypeOrNamepath = utils.tagMissingRequiredTypeOrNamepath(tag, otherModeMaps); + if (tagMissingRequiredTypeOrNamepath !== false && !allowEmptyNamepaths) { + const modeInfo = tagMissingRequiredTypeOrNamepath === true ? '' : ` in "${mode}" mode`; + report(`Tag @${tag.tag} must have either a type or namepath${modeInfo}.`, null, tag); + + return; } + + // VALID TYPE + const hasTypePosition = mightHaveTypePosition === true && Boolean(tag.type); + if (hasTypePosition) { + validTypeParsing(tag.type); + } + + // VALID NAME/NAMEPATH + const hasNameOrNamepathPosition = ( + tagMustHaveNamePosition !== false || + utils.tagMightHaveNamepath(tag.tag) + ) && Boolean(tag.name); + + if (hasNameOrNamepathPosition) { + validNamepathParsing(tag.name, tag.tag); } }); }, { @@ -171,10 +177,6 @@ export default iterateJsdoc(({ default: true, type: 'boolean', }, - checkSeesForNamepaths: { - default: false, - type: 'boolean', - }, }, type: 'object', }, diff --git a/test/eslint/getJSDocComment.js b/test/eslint/getJSDocComment.js index 957bc8dcf..5634e7c42 100644 --- a/test/eslint/getJSDocComment.js +++ b/test/eslint/getJSDocComment.js @@ -8,6 +8,9 @@ const rule = { create (context) { const sourceCode = context.getSourceCode(); const settings = getSettings(context); + if (!settings) { + return {}; + } return { ObjectExpression (node) { diff --git a/test/rules/assertions/checkParamNames.js b/test/rules/assertions/checkParamNames.js index f70caaa96..159e04d0c 100644 --- a/test/rules/assertions/checkParamNames.js +++ b/test/rules/assertions/checkParamNames.js @@ -852,6 +852,30 @@ export default { ], parser: require.resolve('@typescript-eslint/parser'), }, + { + code: ` + /** + * + */ + function quux() { + + } + `, + errors: [{ + line: 1, + message: 'Cannot add "name" to `require` with the tag\'s `name` set to `false`', + }], + settings: { + jsdoc: { + structuredTags: { + see: { + name: false, + required: ['name'], + }, + }, + }, + }, + }, ], valid: [ { diff --git a/test/rules/assertions/checkTypes.js b/test/rules/assertions/checkTypes.js index 90475c97f..daebad665 100644 --- a/test/rules/assertions/checkTypes.js +++ b/test/rules/assertions/checkTypes.js @@ -1962,6 +1962,33 @@ export default { }, }, }, + { + code: ` + /** + * @aCustomTag {Number} foo + */ + `, + errors: [ + { + line: 3, + message: 'Invalid JSDoc @aCustomTag "foo" type "Number"; prefer: "number".', + }, + ], + output: ` + /** + * @aCustomTag {number} foo + */ + `, + settings: { + jsdoc: { + structuredTags: { + aCustomTag: { + type: true, + }, + }, + }, + }, + }, ], valid: [ { @@ -2483,5 +2510,21 @@ export default { }, }, }, + { + code: ` + /** + * @aCustomTag {Number} foo + */ + `, + settings: { + jsdoc: { + structuredTags: { + aCustomTag: { + type: false, + }, + }, + }, + }, + }, ], }; diff --git a/test/rules/assertions/noBadBlocks.js b/test/rules/assertions/noBadBlocks.js index 79a6e3a09..d51036c9c 100644 --- a/test/rules/assertions/noBadBlocks.js +++ b/test/rules/assertions/noBadBlocks.js @@ -42,6 +42,27 @@ export default { */ `, }, + { + code: ` + function quux() { + + } + `, + errors: [{ + line: 1, + message: 'Cannot add "name" to `require` with the tag\'s `name` set to `false`', + }], + settings: { + jsdoc: { + structuredTags: { + see: { + name: false, + required: ['name'], + }, + }, + }, + }, + }, ], valid: [ { diff --git a/test/rules/assertions/noUndefinedTypes.js b/test/rules/assertions/noUndefinedTypes.js index 2b3048fa6..250771ffe 100644 --- a/test/rules/assertions/noUndefinedTypes.js +++ b/test/rules/assertions/noUndefinedTypes.js @@ -314,6 +314,70 @@ export default { }, }, }, + { + code: ` + /** + * @aCustomTag {SomeType} + */ + function quux () {} + `, + errors: [ + { + line: 3, + message: 'The type \'SomeType\' is undefined.', + }, + ], + settings: { + jsdoc: { + structuredTags: { + aCustomTag: { + type: true, + }, + }, + }, + }, + }, + { + code: ` + /** + * @namepathDefiner SomeType + */ + /** + * @type {SomeType} + */ + `, + errors: [ + { + line: 6, + message: 'The type \'SomeType\' is undefined.', + }, + ], + settings: { + jsdoc: { + structuredTags: { + namepathDefiner: { + name: 'namepath-referencing', + }, + }, + }, + }, + }, + { + code: ` + /** + * @namepathDefiner SomeType + */ + /** + * @type {SomeType} + */ + `, + errors: [ + { + line: 6, + message: 'The type \'SomeType\' is undefined.', + }, + ], + }, ], valid: [ { @@ -782,5 +846,41 @@ export default { }, }, }, + { + code: ` + /** + * @aCustomTag {SomeType} + */ + function quux () {} + `, + settings: { + jsdoc: { + structuredTags: { + aCustomTag: { + type: false, + }, + }, + }, + }, + }, + { + code: ` + /** + * @namepathDefiner SomeType + */ + /** + * @type {SomeType} + */ + `, + settings: { + jsdoc: { + structuredTags: { + namepathDefiner: { + name: 'namepath-defining', + }, + }, + }, + }, + }, ], }; diff --git a/test/rules/assertions/requireJsdoc.js b/test/rules/assertions/requireJsdoc.js index 9a9f9c13d..bad5d6571 100644 --- a/test/rules/assertions/requireJsdoc.js +++ b/test/rules/assertions/requireJsdoc.js @@ -2404,6 +2404,30 @@ export default { } `, }, + { + code: ` + /** + * + */ + function quux() { + + } + `, + errors: [{ + line: 1, + message: 'Cannot add "name" to `require` with the tag\'s `name` set to `false`', + }], + settings: { + jsdoc: { + structuredTags: { + see: { + name: false, + required: ['name'], + }, + }, + }, + }, + }, ], valid: [{ code: ` diff --git a/test/rules/assertions/validTypes.js b/test/rules/assertions/validTypes.js index 66ff0ee68..366ccd7fa 100644 --- a/test/rules/assertions/validTypes.js +++ b/test/rules/assertions/validTypes.js @@ -133,9 +133,16 @@ export default { line: 3, message: 'Syntax error in namepath: foo%', }], - options: [{ - checkSeesForNamepaths: true, - }], + settings: { + jsdoc: { + structuredTags: { + see: { + name: 'namepath-referencing', + required: ['name'], + }, + }, + }, + }, }, { code: ` @@ -176,7 +183,7 @@ export default { `, errors: [{ line: 3, - message: 'Tag @callback must have a name/namepath', + message: 'Tag @callback must have a name/namepath.', }], options: [{ allowEmptyNamepaths: false, @@ -225,17 +232,49 @@ export default { { code: ` /** - * @extends + * @this */ class Bar {}; `, errors: [ { line: 3, - message: 'Tag @extends must have either a type or namepath', + message: 'Tag @this must have either a type or namepath in "jsdoc" mode.', + }, + ], + options: [ + { + allowEmptyNamepaths: false, }, ], }, + { + code: ` + /** + * @aCustomTag + */ + `, + errors: [ + { + line: 3, + message: 'Tag @aCustomTag must have either a type or namepath.', + }, + ], + options: [ + { + allowEmptyNamepaths: false, + }, + ], + settings: { + jsdoc: { + structuredTags: { + aCustomTag: { + required: ['typeOrNameRequired'], + }, + }, + }, + }, + }, { code: ` /** @@ -246,7 +285,7 @@ export default { errors: [ { line: 3, - message: 'Tag @type must have a type', + message: 'Tag @type must have a type.', }, ], }, @@ -310,7 +349,7 @@ export default { errors: [ { line: 3, - message: 'Tag @define must have a type', + message: 'Tag @define must have a type in "closure" mode.', }, ], settings: { @@ -329,7 +368,7 @@ export default { errors: [ { line: 3, - message: 'Tag @this must have a type', + message: 'Tag @this must have a type in "closure" mode.', }, ], settings: { @@ -410,6 +449,27 @@ export default { }, }, }, + { + code: ` + /** + * @aCustomTag name + */ + `, + errors: [ + { + message: '@aCustomTag should not have a name.', + }, + ], + settings: { + jsdoc: { + structuredTags: { + aCustomTag: { + name: false, + }, + }, + }, + }, + }, { code: ` /** @@ -420,7 +480,12 @@ export default { `, errors: [ { - message: '@typedef must have a name in "jsdoc" mode.', + message: 'Tag @typedef must have a name/namepath in "jsdoc" mode.', + }, + ], + options: [ + { + allowEmptyNamepaths: false, }, ], settings: { @@ -448,6 +513,125 @@ export default { }, }, }, + { + code: ` + /** + * @aCustomTag {SomeType} + */ + function quux () {} + + `, + errors: [ + { + message: '@aCustomTag should not have a bracketed type.', + }, + ], + settings: { + jsdoc: { + structuredTags: { + aCustomTag: { + type: false, + }, + }, + }, + }, + }, + { + code: ` + /** + * @see foo% + */ + function quux() { + + } + `, + errors: [{ + line: 1, + message: 'Cannot add "name" to `require` with the tag\'s `name` set to `false`', + }], + settings: { + jsdoc: { + structuredTags: { + see: { + name: false, + required: ['name'], + }, + }, + }, + }, + }, + { + code: ` + /** + * @see foo% + */ + function quux() { + + } + `, + errors: [{ + line: 1, + message: 'Cannot add "type" to `require` with the tag\'s `type` set to `false`', + }], + settings: { + jsdoc: { + structuredTags: { + see: { + required: ['type'], + type: false, + }, + }, + }, + }, + }, + { + code: ` + /** + * @see foo% + */ + function quux() { + + } + `, + errors: [{ + line: 1, + message: 'Cannot add "typeOrNameRequired" to `require` with the tag\'s `name` set to `false`', + }], + settings: { + jsdoc: { + structuredTags: { + see: { + name: false, + required: ['typeOrNameRequired'], + }, + }, + }, + }, + }, + { + code: ` + /** + * @see foo% + */ + function quux() { + + } + `, + errors: [{ + line: 1, + message: 'Cannot add "typeOrNameRequired" to `require` with the tag\'s `type` set to `false`', + }], + settings: { + jsdoc: { + structuredTags: { + see: { + required: ['typeOrNameRequired'], + type: false, + }, + }, + }, + }, + }, ], valid: [ { @@ -562,9 +746,16 @@ export default { } `, - options: [{ - checkSeesForNamepaths: true, - }], + settings: { + jsdoc: { + structuredTags: { + see: { + name: 'namepath-referencing', + required: ['name'], + }, + }, + }, + }, }, { code: ` @@ -607,6 +798,16 @@ export default { } `, }, + { + code: ` + /** + * @aCustomTag + */ + function quux() { + + } + `, + }, { code: ` /** @@ -791,6 +992,11 @@ export default { function quux () {} `, + options: [ + { + allowEmptyNamepaths: false, + }, + ], settings: { jsdoc: { mode: 'closure', @@ -826,5 +1032,24 @@ export default { }, ], }, + { + code: ` + /** + * @see + */ + function quux() { + + } + `, + settings: { + jsdoc: { + structuredTags: { + see: { + name: 'namepath-referencing', + }, + }, + }, + }, + }, ], };