From 0265923c8748b55cd2d7ed3e1f9f73cc024e2005 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 5 Aug 2024 00:06:22 +0800 Subject: [PATCH] feat(`lines-before-block`): add new rule; fixes #1209 BREAKING CHANGE: Adds new rule to recommended --- .README/README.md | 1 + .README/rules/lines-before-block.md | 37 ++++ README.md | 1 + docs/rules/lines-before-block.md | 150 +++++++++++++ src/index.js | 3 + src/rules/linesBeforeBlock.js | 101 +++++++++ test/rules/assertions/linesBeforeBlock.js | 246 ++++++++++++++++++++++ test/rules/ruleNames.json | 1 + 8 files changed, 540 insertions(+) create mode 100644 .README/rules/lines-before-block.md create mode 100644 docs/rules/lines-before-block.md create mode 100644 src/rules/linesBeforeBlock.js create mode 100644 test/rules/assertions/linesBeforeBlock.js diff --git a/.README/README.md b/.README/README.md index 31487f62..bb122ae0 100644 --- a/.README/README.md +++ b/.README/README.md @@ -247,6 +247,7 @@ non-default-recommended fixer). |:heavy_check_mark:|:wrench:|[empty-tags](./docs/rules/empty-tags.md#readme)|Checks tags that are expected to be empty (e.g., `@abstract` or `@async`), reporting if they have content| |:heavy_check_mark:||[implements-on-classes](./docs/rules/implements-on-classes.md#readme)|Prohibits use of `@implements` on non-constructor functions (to enforce the tag only being used on classes/constructors)| |||[informative-docs](./docs/rules/informative-docs.md#readme)|Reports on JSDoc texts that serve only to restate their attached name.| +|:heavy_check_mark:||[lines-before-block](./docs/rules/lines-before-block.md#readme)|Enforces minimum number of newlines before JSDoc comment blocks| |||[match-description](./docs/rules/match-description.md#readme)|Defines customizable regular expression rules for your tag descriptions| ||:wrench:|[match-name](./docs/rules/match-name.md#readme)|Reports the name portion of a JSDoc tag if matching or not matching a given regular expression| |:heavy_check_mark:|:wrench:|[multiline-blocks](./docs/rules/multiline-blocks.md#readme)|Controls how and whether jsdoc blocks can be expressed as single or multiple line blocks| diff --git a/.README/rules/lines-before-block.md b/.README/rules/lines-before-block.md new file mode 100644 index 00000000..441cb2c8 --- /dev/null +++ b/.README/rules/lines-before-block.md @@ -0,0 +1,37 @@ +# `lines-before-block` + +This rule enforces minimum number of newlines before JSDoc comment blocks +(except at the beginning of a file). + +## Options + +### `lines` + +The minimum number of lines to require. Defaults to 1. + +### `ignoreSameLine` + +This option excludes cases where the JSDoc block occurs on the same line as a +preceding code or comment. Defaults to `true`. + +### `excludedTags` + +An array of tags whose presence in the JSDoc block will prevent the +application of the rule. Defaults to `['type']` (i.e., if `@type` is present, +lines before the block will not be added). + +||| +|---|---| +|Context|everywhere| +|Tags|N/A| +|Recommended|true| +|Settings|| +|Options|`excludedTags`, `ignoreSameLine`, `lines`| + +## Failing examples + + + +## Passing examples + + diff --git a/README.md b/README.md index a9dae4cb..09975474 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,7 @@ non-default-recommended fixer). |:heavy_check_mark:|:wrench:|[empty-tags](./docs/rules/empty-tags.md#readme)|Checks tags that are expected to be empty (e.g., `@abstract` or `@async`), reporting if they have content| |:heavy_check_mark:||[implements-on-classes](./docs/rules/implements-on-classes.md#readme)|Prohibits use of `@implements` on non-constructor functions (to enforce the tag only being used on classes/constructors)| |||[informative-docs](./docs/rules/informative-docs.md#readme)|Reports on JSDoc texts that serve only to restate their attached name.| +|:heavy_check_mark:||[lines-before-block](./docs/rules/lines-before-block.md#readme)|Enforces minimum number of newlines before JSDoc comment blocks| |||[match-description](./docs/rules/match-description.md#readme)|Defines customizable regular expression rules for your tag descriptions| ||:wrench:|[match-name](./docs/rules/match-name.md#readme)|Reports the name portion of a JSDoc tag if matching or not matching a given regular expression| |:heavy_check_mark:|:wrench:|[multiline-blocks](./docs/rules/multiline-blocks.md#readme)|Controls how and whether jsdoc blocks can be expressed as single or multiple line blocks| diff --git a/docs/rules/lines-before-block.md b/docs/rules/lines-before-block.md new file mode 100644 index 00000000..28d1f57d --- /dev/null +++ b/docs/rules/lines-before-block.md @@ -0,0 +1,150 @@ + + +# lines-before-block + +This rule enforces minimum number of newlines before JSDoc comment blocks +(except at the beginning of a file). + + + +## Options + + + +### lines + +The minimum number of lines to require. Defaults to 1. + + + +### ignoreSameLine + +This option excludes cases where the JSDoc block occurs on the same line as a +preceding code or comment. Defaults to `true`. + + + +### excludedTags + +An array of tags whose presence in the JSDoc block will prevent the +application of the rule. Defaults to `['type']` (i.e., if `@type` is present, +lines before the block will not be added). + +||| +|---|---| +|Context|everywhere| +|Tags|N/A| +|Recommended|true| +|Settings|| +|Options|`excludedTags`, `ignoreSameLine`, `lines`| + + + +## Failing examples + +The following patterns are considered problems: + +````ts +someCode; +/** + * + */ +// Message: Required 1 line(s) before JSDoc block + +someCode; /** + * + */ +// "jsdoc/lines-before-block": ["error"|"warn", {"ignoreSameLine":false}] +// Message: Required 1 line(s) before JSDoc block + +someCode; /** */ +// "jsdoc/lines-before-block": ["error"|"warn", {"ignoreSameLine":false}] +// Message: Required 1 line(s) before JSDoc block + +someCode; +/** + * + */ +// "jsdoc/lines-before-block": ["error"|"warn", {"lines":2}] +// Message: Required 2 line(s) before JSDoc block + +// Some comment +/** + * + */ +// Message: Required 1 line(s) before JSDoc block + +/* Some comment */ +/** + * + */ +// Message: Required 1 line(s) before JSDoc block + +/** + * Some comment + */ +/** + * + */ +// Message: Required 1 line(s) before JSDoc block +```` + + + + + +## Passing examples + +The following patterns are not considered problems: + +````ts +/** +* +*/ + +someCode; + +/** + * + */ + +someCode; + + +/** + * + */ +// "jsdoc/lines-before-block": ["error"|"warn", {"lines":2}] + +// Some comment + +/** + * + */ + +/* Some comment */ + +/** + * + */ + +/** + * Some comment + */ + +/** + * + */ + +someCode; /** */ + +const a = { + someProp: /** @type {SomeCast} */ (someVal) +}; + +const a = /** @lends SomeClass */ { + someProp: (someVal) +}; +// "jsdoc/lines-before-block": ["error"|"warn", {"excludedTags":["lends"],"ignoreSameLine":false}] +```` + diff --git a/src/index.js b/src/index.js index b01eacd3..61c6fe76 100644 --- a/src/index.js +++ b/src/index.js @@ -15,6 +15,7 @@ import emptyTags from './rules/emptyTags.js'; import implementsOnClasses from './rules/implementsOnClasses.js'; import importsAsDependencies from './rules/importsAsDependencies.js'; import informativeDocs from './rules/informativeDocs.js'; +import linesBeforeBlock from './rules/linesBeforeBlock.js'; import matchDescription from './rules/matchDescription.js'; import matchName from './rules/matchName.js'; import multilineBlocks from './rules/multilineBlocks.js'; @@ -92,6 +93,7 @@ const index = { 'implements-on-classes': implementsOnClasses, 'imports-as-dependencies': importsAsDependencies, 'informative-docs': informativeDocs, + 'lines-before-block': linesBeforeBlock, 'match-description': matchDescription, 'match-name': matchName, 'multiline-blocks': multilineBlocks, @@ -167,6 +169,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => { 'jsdoc/implements-on-classes': warnOrError, 'jsdoc/imports-as-dependencies': 'off', 'jsdoc/informative-docs': 'off', + 'jsdoc/lines-before-block': warnOrError, 'jsdoc/match-description': 'off', 'jsdoc/match-name': 'off', 'jsdoc/multiline-blocks': warnOrError, diff --git a/src/rules/linesBeforeBlock.js b/src/rules/linesBeforeBlock.js new file mode 100644 index 00000000..086193c2 --- /dev/null +++ b/src/rules/linesBeforeBlock.js @@ -0,0 +1,101 @@ +import iterateJsdoc from '../iterateJsdoc.js'; + +export default iterateJsdoc(({ + context, + jsdocNode, + sourceCode, + report, + utils, +}) => { + const { + lines = 1, + ignoreSameLine = true, + excludedTags = ['type'] + } = context.options[0] || {}; + + if (utils.hasATag(excludedTags)) { + return; + } + + const tokensBefore = sourceCode.getTokensBefore(jsdocNode, {includeComments: true}); + const tokenBefore = tokensBefore.slice(-1)[0]; + if (!tokenBefore) { + return; + } + + if (tokenBefore.loc?.end?.line + lines >= + /** @type {number} */ + (jsdocNode.loc?.start?.line) + ) { + const startLine = jsdocNode.loc?.start?.line; + const sameLine = tokenBefore.loc?.end?.line === startLine; + + if (sameLine && ignoreSameLine) { + return; + } + + /** @type {import('eslint').Rule.ReportFixer} */ + const fix = (fixer) => { + let indent = ''; + if (sameLine) { + const spaceDiff = /** @type {number} */ (jsdocNode.loc?.start?.column) - + /** @type {number} */ (tokenBefore.loc?.end?.column); + // @ts-expect-error Should be a comment + indent = /** @type {import('estree').Comment} */ ( + jsdocNode + ).value.match(/^\*\n([ \t]*) \*/)?.[1]?.slice(spaceDiff); + if (!indent) { + /** @type {import('eslint').AST.Token|import('estree').Comment|undefined} */ + let tokenPrior = tokenBefore; + let startColumn; + while (tokenPrior && tokenPrior?.loc?.start?.line === startLine) { + startColumn = tokenPrior.loc?.start?.column; + tokenPrior = tokensBefore.pop(); + } + indent = ' '.repeat( + /* c8 ignore next */ + /** @type {number} */ (startColumn ? startColumn - 1 : 0) + ); + } + } + + return fixer.insertTextAfter( + /** @type {import('eslint').AST.Token} */ + (tokenBefore), + '\n'.repeat(lines) + + (sameLine ? '\n' + indent : '') + ); + }; + report(`Required ${lines} line(s) before JSDoc block`, fix); + } +}, { + iterateAllJsdocs: true, + meta: { + fixable: 'code', + docs: { + description: 'Enforces minimum number of newlines before JSDoc comment blocks', + url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/lines-before-block.md#repos-sticky-header', + }, + schema: [ + { + additionalProperties: false, + properties: { + excludedTags: { + type: 'array', + items: { + type: 'string' + } + }, + ignoreSameLine: { + type: 'boolean', + }, + lines: { + type: 'integer' + } + }, + type: 'object', + }, + ], + type: 'suggestion', + }, +}); diff --git a/test/rules/assertions/linesBeforeBlock.js b/test/rules/assertions/linesBeforeBlock.js new file mode 100644 index 00000000..51132b20 --- /dev/null +++ b/test/rules/assertions/linesBeforeBlock.js @@ -0,0 +1,246 @@ +export default { + invalid: [ + { + code: ` + someCode; + /** + * + */ + `, + errors: [ + { + line: 3, + message: 'Required 1 line(s) before JSDoc block', + }, + ], + output: ` + someCode; + + /** + * + */ + `, + }, + { + code: ` + someCode; /** + * + */ + `, + errors: [ + { + line: 2, + message: 'Required 1 line(s) before JSDoc block', + }, + ], + options: [ + { + ignoreSameLine: false + } + ], + output: ` + someCode; + + /** + * + */ + `, + }, + { + code: ` + someCode; /** */ + `, + errors: [ + { + line: 2, + message: 'Required 1 line(s) before JSDoc block', + }, + ], + options: [ + { + ignoreSameLine: false + } + ], + output: ` + someCode; + + /** */ + `, + }, + { + code: ` + someCode; + /** + * + */ + `, + errors: [ + { + line: 3, + message: 'Required 2 line(s) before JSDoc block', + }, + ], + options: [ + { + lines: 2, + }, + ], + output: ` + someCode; + + + /** + * + */ + `, + }, + { + code: ` + // Some comment + /** + * + */ + `, + errors: [ + { + line: 3, + message: 'Required 1 line(s) before JSDoc block', + }, + ], + output: ` + // Some comment + + /** + * + */ + `, + }, + { + code: ` + /* Some comment */ + /** + * + */ + `, + errors: [ + { + line: 3, + message: 'Required 1 line(s) before JSDoc block', + }, + ], + output: ` + /* Some comment */ + + /** + * + */ + `, + }, + { + code: ` + /** + * Some comment + */ + /** + * + */ + `, + errors: [ + { + line: 5, + message: 'Required 1 line(s) before JSDoc block', + }, + ], + output: ` + /** + * Some comment + */ + + /** + * + */ + `, + }, + ], + valid: [ + { + code: `/**\n *\n */`, + }, + { + code: ` + someCode; + + /** + * + */ + `, + }, + { + code: ` + someCode; + + + /** + * + */ + `, + options: [ + { + lines: 2, + }, + ], + }, + { + code: ` + // Some comment + + /** + * + */ + `, + }, + { + code: ` + /* Some comment */ + + /** + * + */ + `, + }, + { + code: ` + /** + * Some comment + */ + + /** + * + */ + `, + }, + { + code: ` + someCode; /** */ + `, + }, + { + code: `const a = { + someProp: /** @type {SomeCast} */ (someVal) + }; + `, + }, + { + code: `const a = /** @lends SomeClass */ { + someProp: (someVal) + }; + `, + options: [ + { + excludedTags: ['lends'], + ignoreSameLine: false + } + ] + }, + ], +}; diff --git a/test/rules/ruleNames.json b/test/rules/ruleNames.json index 1540d80b..df037553 100644 --- a/test/rules/ruleNames.json +++ b/test/rules/ruleNames.json @@ -16,6 +16,7 @@ "implements-on-classes", "imports-as-dependencies", "informative-docs", + "lines-before-block", "match-description", "match-name", "multiline-blocks",