diff --git a/.README/rules/no-defaults.md b/.README/rules/no-defaults.md new file mode 100644 index 000000000..642e7211d --- /dev/null +++ b/.README/rules/no-defaults.md @@ -0,0 +1,42 @@ +### `no-defaults` + +This rule reports defaults being used on the relevant portion of `@param` +or `@default`. It also optionally reports the presence of the +square-bracketed optional arguments at all. + +The rule is intended to prevent the indication of defaults on tags where +this would be redundant with ES6 default parameters (or for `@default`, +where it would be redundant with the context to which the `@default` +tag is attached). + +Unless your `@default` is on a function, you will need to set `contexts` +to an appropriate context, including, if you wish, "any". + +#### Options + +##### `noOptionalParamNames` + +Set this to `true` to report the presence of optional parameters. May be +used if the project is insisting on optionality being indicated by +the presence of ES6 default parameters (bearing in mind that such +"defaults" are only applied when the supplied value is missing or +`undefined` but not for `null` or other "falsey" values). + +##### `contexts` + +Set this to an array of strings representing the AST context +where you wish the rule to be applied. +Overrides the default contexts (see below). Set to `"any"` if you want +the rule to apply to any jsdoc block throughout your files (as is necessary +for finding function blocks not attached to a function declaration or +expression, i.e., `@callback` or `@function` (or its aliases `@func` or +`@method`) (including those associated with an `@interface`). + +||| +|---|---| +|Context|`ArrowFunctionExpression`, `FunctionDeclaration`, `FunctionExpression`; others when `contexts` option enabled| +|Tags|`param`, `default`| +|Aliases|`arg`, `argument`, `defaultvalue`| +|Options|`contexts`, `noOptionalParamNames`| + + diff --git a/src/index.js b/src/index.js index 841b4c530..afae83fe5 100644 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,7 @@ import emptyTags from './rules/emptyTags'; import implementsOnClasses from './rules/implementsOnClasses'; import matchDescription from './rules/matchDescription'; import newlineAfterDescription from './rules/newlineAfterDescription'; +import noDefaults from './rules/noDefaults'; import noTypes from './rules/noTypes'; import noUndefinedTypes from './rules/noUndefinedTypes'; import requireDescriptionCompleteSentence from './rules/requireDescriptionCompleteSentence'; @@ -54,6 +55,7 @@ export default { 'jsdoc/implements-on-classes': 'warn', 'jsdoc/match-description': 'off', 'jsdoc/newline-after-description': 'warn', + 'jsdoc/no-defaults': 'off', 'jsdoc/no-types': 'off', 'jsdoc/no-undefined-types': 'warn', 'jsdoc/require-description': 'off', @@ -93,6 +95,7 @@ export default { 'implements-on-classes': implementsOnClasses, 'match-description': matchDescription, 'newline-after-description': newlineAfterDescription, + 'no-defaults': noDefaults, 'no-types': noTypes, 'no-undefined-types': noUndefinedTypes, 'require-description': requireDescription, diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index 009f4c56c..9fad36516 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -29,12 +29,21 @@ const parseComment = (commentNode, indent, trim = true) => { // @see https://github.com/yavorskiy/comment-parser/issues/21 parsers: [ commentParser.PARSERS.parse_tag, - skipSeeLink(commentParser.PARSERS.parse_type), + skipSeeLink( + (str, data) => { + if (['default', 'defaultvalue'].includes(data.tag)) { + return null; + } + + return commentParser.PARSERS.parse_type(str, data); + }, + ), skipSeeLink( (str, data) => { if ([ 'example', 'return', 'returns', 'throws', 'exception', 'access', 'version', 'since', 'license', 'author', + 'default', 'defaultvalue', ].includes(data.tag)) { return null; } diff --git a/src/rules/noDefaults.js b/src/rules/noDefaults.js new file mode 100644 index 000000000..94806153f --- /dev/null +++ b/src/rules/noDefaults.js @@ -0,0 +1,52 @@ +import iterateJsdoc from '../iterateJsdoc'; + +export default iterateJsdoc(({ + context, + utils, +}) => { + const {noOptionalParamNames} = context.options[0] || {}; + const paramTags = utils.getPresentTags(['param', 'arg', 'argument']); + paramTags.forEach((tag) => { + if (noOptionalParamNames && tag.optional) { + utils.reportJSDoc(`Optional param names are not permitted on @${tag.tag}.`, tag, () => { + tag.default = ''; + tag.optional = false; + }); + } else if (tag.default) { + utils.reportJSDoc(`Defaults are not permitted on @${tag.tag}.`, tag, () => { + tag.default = ''; + }); + } + }); + const defaultTags = utils.getPresentTags(['default', 'defaultvalue']); + defaultTags.forEach((tag) => { + if (tag.description) { + utils.reportJSDoc(`Default values are not permitted on @${tag.tag}.`, tag, () => { + tag.description = ''; + }); + } + }); +}, { + contextDefaults: true, + meta: { + fixable: 'code', + schema: [ + { + additionalProperties: false, + properties: { + contexts: { + items: { + type: 'string', + }, + type: 'array', + }, + noOptionalParamNames: { + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, +}); diff --git a/test/rules/assertions/noDefaults.js b/test/rules/assertions/noDefaults.js new file mode 100644 index 000000000..5f1f45c00 --- /dev/null +++ b/test/rules/assertions/noDefaults.js @@ -0,0 +1,302 @@ +export default { + invalid: [ + { + code: ` + /** + * @param {number} [foo="7"] + */ + function quux (foo) { + + } + `, + errors: [ + { + line: 3, + message: 'Defaults are not permitted on @param.', + }, + ], + output: ` + /** + * @param {number} [foo] + */ + function quux (foo) { + + } + `, + }, + { + code: ` + /** + * @param {number} [foo="7"] + */ + function quux (foo) { + + } + `, + errors: [ + { + line: 3, + message: 'Optional param names are not permitted on @param.', + }, + ], + options: [ + { + noOptionalParamNames: true, + }, + ], + output: ` + /** + * @param {number} foo + */ + function quux (foo) { + + } + `, + }, + { + code: ` + /** + * @arg {number} [foo="7"] + */ + function quux (foo) { + + } + `, + errors: [ + { + line: 3, + message: 'Defaults are not permitted on @arg.', + }, + ], + output: ` + /** + * @arg {number} [foo] + */ + function quux (foo) { + + } + `, + settings: { + jsdoc: { + tagNamePreference: { + param: 'arg', + }, + }, + }, + }, + { + code: ` + /** + * @param {number} [foo="7"] + */ + function quux (foo) { + + } + `, + errors: [ + { + line: 3, + message: 'Defaults are not permitted on @param.', + }, + ], + options: [ + { + contexts: ['any'], + }, + ], + output: ` + /** + * @param {number} [foo] + */ + function quux (foo) { + + } + `, + }, + { + code: ` + /** + * @function + * @param {number} [foo="7"] + */ + `, + errors: [ + { + line: 4, + message: 'Defaults are not permitted on @param.', + }, + ], + options: [ + { + contexts: ['any'], + }, + ], + output: ` + /** + * @function + * @param {number} [foo] + */ + `, + }, + { + code: ` + /** + * @callback + * @param {number} [foo="7"] + */ + `, + errors: [ + { + line: 4, + message: 'Defaults are not permitted on @param.', + }, + ], + options: [ + { + contexts: ['any'], + }, + ], + output: ` + /** + * @callback + * @param {number} [foo] + */ + `, + }, + { + code: ` + /** + * @default {} + */ + const a = {}; + `, + errors: [ + { + line: 3, + message: 'Default values are not permitted on @default.', + }, + ], + options: [ + { + contexts: ['any'], + }, + ], + output: ` + /** + * @default + */ + const a = {}; + `, + }, + { + code: ` + /** + * @defaultvalue {} + */ + const a = {}; + `, + errors: [ + { + line: 3, + message: 'Default values are not permitted on @defaultvalue.', + }, + ], + options: [ + { + contexts: ['any'], + }, + ], + output: ` + /** + * @defaultvalue + */ + const a = {}; + `, + settings: { + jsdoc: { + tagNamePreference: { + default: 'defaultvalue', + }, + }, + }, + }, + ], + valid: [ + { + code: ` + /** + * @param foo + */ + function quux (foo) { + + } + `, + }, + { + code: ` + /** + * @param {number} foo + */ + function quux (foo) { + + } + `, + }, + { + code: ` + /** + * @param foo + */ + `, + options: [ + { + contexts: ['any'], + }, + ], + }, + { + code: ` + /** + * @function + * @param {number} foo + */ + `, + }, + { + code: ` + /** + * @callback + * @param {number} foo + */ + `, + }, + { + code: ` + /** + * @param {number} foo + */ + function quux (foo) { + + } + `, + options: [ + { + noOptionalParamNames: true, + }, + ], + }, + { + code: ` + /** + * @default + */ + const a = {}; + `, + options: [ + { + contexts: ['any'], + }, + ], + }, + ], +}; diff --git a/test/rules/index.js b/test/rules/index.js index 98b765c16..3a0dd9139 100644 --- a/test/rules/index.js +++ b/test/rules/index.js @@ -22,6 +22,7 @@ const ruleTester = new RuleTester(); 'implements-on-classes', 'match-description', 'newline-after-description', + 'no-defaults', 'no-types', 'no-undefined-types', 'require-description',