diff --git a/.README/rules/no-lines-after-blocks.md b/.README/rules/no-lines-after-blocks.md new file mode 100644 index 000000000..241d91153 --- /dev/null +++ b/.README/rules/no-lines-after-blocks.md @@ -0,0 +1,32 @@ +# `no-lines-after-blocks` + +Reports extra lines between functions (and other language structures) and their +JSDoc blocks. + +Standalone comments such as `@typedef` and `@callback` will not be reported even +if they are followed by a language structure. + +## Fixer + +Removes extra lines between functions (and other language structures) and their +JSDoc blocks. Uses the `maxLines` setting to determine whether to remove lines. + +## Options + +{"gitdown": "options"} + +||| +|---|---| +|Context|everywhere| +|Tags|N/A| +|Recommended|false| +|Settings|`maxLines`, `minLines`| +|Options|`contexts`, `enableFixer`, `exemptedBy`, `overrideDefaultExemptions`, `preferMinLines`| + +## Failing examples + + + +## Passing examples + + diff --git a/README.md b/README.md index 5d0c06dd4..37bd92b45 100644 --- a/README.md +++ b/README.md @@ -455,6 +455,7 @@ non-default-recommended fixer). ||:wrench:| [no-blank-block-descriptions](./docs/rules/no-blank-block-descriptions.md#readme) | If tags are present, this rule will prevent empty lines in the block description. If no tags are present, this rule will prevent extra empty lines in the block description. | ||:wrench:| [no-blank-blocks](./docs/rules/no-blank-blocks.md#readme) | Removes empty blocks with nothing but possibly line breaks | |:heavy_check_mark:|:wrench:| [no-defaults](./docs/rules/no-defaults.md#readme) | This rule reports defaults being used on the relevant portion of `@param` or `@default`. | +||:wrench:| [no-lines-after-blocks](./docs/rules/no-lines-after-blocks.md#readme) | Reports extra lines between functions (and other language structures) and their JSDoc blocks. | ||| [no-missing-syntax](./docs/rules/no-missing-syntax.md#readme) | Reports when certain comment structures are always expected. | |:heavy_check_mark:|:wrench:| [no-multi-asterisks](./docs/rules/no-multi-asterisks.md#readme) | Prevents use of multiple asterisks at the beginning of lines. | ||| [no-restricted-syntax](./docs/rules/no-restricted-syntax.md#readme) | Reports when certain comment structures are present. | diff --git a/docs/rules/no-lines-after-blocks.md b/docs/rules/no-lines-after-blocks.md new file mode 100644 index 000000000..9b5d08127 --- /dev/null +++ b/docs/rules/no-lines-after-blocks.md @@ -0,0 +1,181 @@ + + +# no-lines-after-blocks + +Reports extra lines between functions (and other language structures) and their +JSDoc blocks. + +Standalone comments such as `@typedef` and `@callback` will not be reported even +if they are followed by a language structure. + + + +## Fixer + +Removes extra lines between functions (and other language structures) and their +JSDoc blocks. Uses the `maxLines` setting to determine whether to remove lines. + + + +## Options + +A single options object has the following properties. + + + +### contexts + +Set this to an array of strings representing the AST context (or an object with +`context` and `comment` properties) where you wish the rule to be applied. + +`context` defaults to `any` and `comment` defaults to no specific comment context. + +Overrides the default contexts (`ArrowFunctionExpression`, `FunctionDeclaration`, +`FunctionExpression`). Setting to `"any"` may be problematic if you have +JSDoc-style comments at the top of your files. + +See the ["AST and Selectors"](#user-content-eslint-plugin-jsdoc-advanced-ast-and-selectors) +section of our Advanced docs for more on the expected format. + + +### enableFixer + +Whether to enable the fixer to remove line breaks + + +### exemptedBy + +Tag names to be added to those which will exempt reporting for a block. Defaults to: + +- 'callback' +- 'copyright' +- 'exports' +- 'interface' +- 'event' +- 'external' +- 'file' +- 'fileoverview' +- 'host' +- 'import' +- 'license' +- 'module' +- 'namespace' +- 'overview' +- 'typedef' + + + +### overrideDefaultExemptions + +Determines whether `exemptedBy` will override the default values. Defaults to `false`. + + +### preferMinLines + +Whether to use the setting `minLines` as the basis for fixing lines going past `maxLines` + + +||| +|---|---| +|Context|everywhere| +|Tags|N/A| +|Recommended|false| +|Settings|`maxLines`, `minLines`| +|Options|`contexts`, `enableFixer`, `exemptedBy`, `overrideDefaultExemptions`, `preferMinLines`| + + + +## Failing examples + +The following patterns are considered problems: + +````ts +/** This is a description of some function!*/ + + + + + + +function someFunction() {} +// Message: There should be no extra lines above structures with JSDoc blocks + +/** This is a description of some function!*/ + +function someFunction() {} +// "jsdoc/no-lines-after-blocks": ["error"|"warn", {"enableFixer":false}] +// Message: There should be no extra lines above structures with JSDoc blocks + +/** This is a description of some function!*/ + + +function someFunction() {} +// Settings: {"jsdoc":{"maxLines":2}} +// Message: There should be no extra lines above structures with JSDoc blocks + +/** This is a description of some function!*/ + + +function someFunction() {} +// Settings: {"jsdoc":{"maxLines":2,"minLines":1}} +// "jsdoc/no-lines-after-blocks": ["error"|"warn", {"preferMinLines":true}] +// Message: There should be no extra lines above structures with JSDoc blocks + +/** @typedef SomeType */ + +function someFunction() {} +// "jsdoc/no-lines-after-blocks": ["error"|"warn", {"exemptedBy":["function"],"overrideDefaultExemptions":true}] +// Message: There should be no extra lines above structures with JSDoc blocks +```` + + + + + +## Passing examples + +The following patterns are not considered problems: + +````ts +function someFunction() {} + +/** JSDoc */ function someFunction() {} + +/** This is a description of some function! */ +// extra comment +function someFunction() {} + +/** Standalone comment (e.g. a type definition) */ + +/** The actual description */ +function someFunction() {} + +/* Regular block comment */ + +function someFunction() {} + +// Regular line comment + +function someFunction() {} + +/** This is a description of some function!*/ + +function someFunction() {} +// Settings: {"jsdoc":{"maxLines":2}} + +/** @typedef {string} SomeType */ + +function someFunction() {} + +/** @function SomeType */ + +function someFunction() {} +// "jsdoc/no-lines-after-blocks": ["error"|"warn", {"exemptedBy":["function"]}] + +/** + * JSDoc block at top of file without import declaration context. + */ + +import {sth} from 'sth'; +```` + diff --git a/docs/rules/no-lines-before-blocks.md b/docs/rules/no-lines-before-blocks.md new file mode 100644 index 000000000..19bb01004 --- /dev/null +++ b/docs/rules/no-lines-before-blocks.md @@ -0,0 +1,115 @@ + + +# no-lines-before-blocks + +Reports extra lines between functions (and other language structures) and their +JSDoc blocks. + + + +## Fixer + +Removes extra lines between functions (and other language structures) and their +JSDoc blocks. Uses the `maxLines` setting to determine whether to remove lines. + + + +## Options + +A single options object has the following properties. + + + +### enableFixer + +Whether to enable the fixer to remove line breaks + + +### preferMinLines + +Whether to use the setting `minLines` as the basis for fixing lines going past `maxLines` + + +||| +|---|---| +|Context|everywhere| +|Tags|N/A| +|Recommended|true| +|Settings|`maxLines`, `minLines`| +|Options|`enableFixer`, `preferMinLines`| + + + +## Failing examples + +The following patterns are considered problems: + +````ts +/** This is a description of some function!*/ + + + + + + +function someFunction() {} +// Message: There should be no extra lines above structures with JSDoc blocks + +/** This is a description of some function!*/ + +function someFunction() {} +// "jsdoc/no-lines-before-blocks": ["error"|"warn", {"enableFixer":false}] +// Message: There should be no extra lines above structures with JSDoc blocks + +/** This is a description of some function!*/ + + +function someFunction() {} +// Settings: {"jsdoc":{"maxLines":2}} +// Message: There should be no extra lines above structures with JSDoc blocks + +/** This is a description of some function!*/ + + +function someFunction() {} +// Settings: {"jsdoc":{"maxLines":2,"minLines":1}} +// "jsdoc/no-lines-before-blocks": ["error"|"warn", {"preferMinLines":true}] +// Message: There should be no extra lines above structures with JSDoc blocks +```` + + + + + +## Passing examples + +The following patterns are not considered problems: + +````ts +function someFunction() {} + +/** JSDoc */ function someFunction() {} + +/** This is a description of some function! */ +// extra comment +function someFunction() {} + +/** Standalone comment (e.g. a type definition) */ + +/** The actual description */ +function someFunction() {} + +/* Regular block comment */ + +function someFunction() {} + +// Regular line comment + +function someFunction() {} + +/** This is a description of some function!*/ + +function someFunction() {} +// Settings: {"jsdoc":{"maxLines":2}} +```` + diff --git a/src/index-cjs.js b/src/index-cjs.js index 752fb60ec..c7a14455f 100644 --- a/src/index-cjs.js +++ b/src/index-cjs.js @@ -33,6 +33,7 @@ import noBadBlocks from './rules/noBadBlocks.js'; import noBlankBlockDescriptions from './rules/noBlankBlockDescriptions.js'; import noBlankBlocks from './rules/noBlankBlocks.js'; import noDefaults from './rules/noDefaults.js'; +import noLinesAfterBlocks from './rules/noLinesAfterBlocks.js'; import noMissingSyntax from './rules/noMissingSyntax.js'; import noMultiAsterisks from './rules/noMultiAsterisks.js'; import noRestrictedSyntax from './rules/noRestrictedSyntax.js'; @@ -114,6 +115,7 @@ index.rules = { 'no-blank-block-descriptions': noBlankBlockDescriptions, 'no-blank-blocks': noBlankBlocks, 'no-defaults': noDefaults, + 'no-lines-after-blocks': noLinesAfterBlocks, 'no-missing-syntax': noMissingSyntax, 'no-multi-asterisks': noMultiAsterisks, 'no-restricted-syntax': noRestrictedSyntax, @@ -299,6 +301,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => { 'jsdoc/no-blank-block-descriptions': 'off', 'jsdoc/no-blank-blocks': 'off', 'jsdoc/no-defaults': warnOrError, + 'jsdoc/no-lines-after-blocks': 'off', 'jsdoc/no-missing-syntax': 'off', 'jsdoc/no-multi-asterisks': warnOrError, 'jsdoc/no-restricted-syntax': 'off', diff --git a/src/index.js b/src/index.js index 7b6659868..a2b1dfc8f 100644 --- a/src/index.js +++ b/src/index.js @@ -39,6 +39,7 @@ import noBadBlocks from './rules/noBadBlocks.js'; import noBlankBlockDescriptions from './rules/noBlankBlockDescriptions.js'; import noBlankBlocks from './rules/noBlankBlocks.js'; import noDefaults from './rules/noDefaults.js'; +import noLinesAfterBlocks from './rules/noLinesAfterBlocks.js'; import noMissingSyntax from './rules/noMissingSyntax.js'; import noMultiAsterisks from './rules/noMultiAsterisks.js'; import noRestrictedSyntax from './rules/noRestrictedSyntax.js'; @@ -120,6 +121,7 @@ index.rules = { 'no-blank-block-descriptions': noBlankBlockDescriptions, 'no-blank-blocks': noBlankBlocks, 'no-defaults': noDefaults, + 'no-lines-after-blocks': noLinesAfterBlocks, 'no-missing-syntax': noMissingSyntax, 'no-multi-asterisks': noMultiAsterisks, 'no-restricted-syntax': noRestrictedSyntax, @@ -305,6 +307,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => { 'jsdoc/no-blank-block-descriptions': 'off', 'jsdoc/no-blank-blocks': 'off', 'jsdoc/no-defaults': warnOrError, + 'jsdoc/no-lines-after-blocks': 'off', 'jsdoc/no-missing-syntax': 'off', 'jsdoc/no-multi-asterisks': warnOrError, 'jsdoc/no-restricted-syntax': 'off', diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index 11b1235f7..2a72b9a8e 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -2134,6 +2134,7 @@ const getIndentAndJSDoc = function (lines, jsdocNode) { * @property {true} [nonGlobalSettings] Whether to avoid relying on settings for global contexts * @property {true} [noTracking] Whether to disable the tracking of visited comment nodes (as * non-tracked may conduct further actions) + * @property {Partial} [ruleSettings] Any additional settings * @property {true} [matchContext] Whether the rule expects contexts to be based on a match option * @property {(args: { * context: import('eslint').Rule.RuleContext, @@ -2314,7 +2315,10 @@ const iterateAllJsdocs = (iterator, ruleConfig, contexts, additiveCommentContext */ '*:not(Program)' (node) { const commentNode = getJSDocComment( - sourceCode, node, /** @type {Settings} */ (settings), + sourceCode, node, /** @type {Settings} */ ({ + ...settings, + ...ruleConfig.ruleSettings, + }), ); if (!ruleConfig.noTracking && trackedJsdocs.has(commentNode)) { return; diff --git a/src/rules.d.ts b/src/rules.d.ts index b8f1de56f..7ec1b85d7 100644 --- a/src/rules.d.ts +++ b/src/rules.d.ts @@ -1104,6 +1104,67 @@ export interface Rules { } ]; + /** Reports extra lines between functions (and other language structures) and their JSDoc blocks. */ + "jsdoc/no-lines-after-blocks": + | [] + | [ + { + /** + * Set this to an array of strings representing the AST context (or an object with + * `context` and `comment` properties) where you wish the rule to be applied. + * + * `context` defaults to `any` and `comment` defaults to no specific comment context. + * + * Overrides the default contexts (`ArrowFunctionExpression`, `FunctionDeclaration`, + * `FunctionExpression`). Setting to `"any"` may be problematic if you have + * JSDoc-style comments at the top of your files. + * + * See the ["AST and Selectors"](../#advanced-ast-and-selectors) + * section of our Advanced docs for more on the expected format. + */ + contexts?: ( + | string + | { + comment?: string; + context?: string; + } + )[]; + /** + * Whether to enable the fixer to remove line breaks + */ + enableFixer?: boolean; + /** + * Tag names to be added to those which will exempt reporting for a block. Defaults to: + * + * - 'callback' + * - 'copyright' + * - 'exports' + * - 'interface' + * - 'event' + * - 'external' + * - 'file' + * - 'fileoverview' + * - 'host' + * - 'import' + * - 'license' + * - 'module' + * - 'namespace' + * - 'overview' + * - 'typedef' + * + */ + exemptedBy?: string[]; + /** + * Determines whether `exemptedBy` will override the default values. Defaults to `false`. + */ + overrideDefaultExemptions?: boolean; + /** + * Whether to use the setting `minLines` as the basis for fixing lines going past `maxLines` + */ + preferMinLines?: boolean; + } + ]; + /** Reports when certain comment structures are always expected. */ "jsdoc/no-missing-syntax": | [] diff --git a/src/rules/noLinesAfterBlocks.js b/src/rules/noLinesAfterBlocks.js new file mode 100644 index 000000000..35e08e969 --- /dev/null +++ b/src/rules/noLinesAfterBlocks.js @@ -0,0 +1,170 @@ +import iterateJsdoc from '../iterateJsdoc.js'; + +export default iterateJsdoc(({ + context, + indent, + node, + settings, + sourceCode, + utils, +}) => { + if (!node) { + return; + } + + const { + enableFixer = true, + exemptedBy = [], + overrideDefaultExemptions = false, + preferMinLines = false, + } = context.options[0] || {}; + + if (utils.hasATag(overrideDefaultExemptions ? exemptedBy : [ + ...exemptedBy, + 'callback', + 'copyright', + 'exports', + 'interface', + 'event', + 'external', + 'file', + 'fileoverview', + 'host', + 'import', + 'license', + 'module', + 'namespace', + 'overview', + 'typedef', + ])) { + return; + } + + const { + maxLines, + minLines, + } = settings; + + const prevToken = sourceCode.getTokenBefore(node, { + includeComments: true, + }); + + /* c8 ignore next 3 -- TS */ + if (!prevToken) { + return; + } + + const interveningRange = /** @type {[number, number]} */ ([ + /** @type {number} */ (prevToken.range?.[1]), + /** @type {number} */ (node.range?.[0]), + ]); + + const ws = sourceCode.getText().slice(interveningRange[0], interveningRange[1]); + + const newLines = ws.match(/\n/gv)?.length ?? 0; + + if (newLines <= maxLines) { + return; + } + + utils.reportJSDoc( + 'There should be no extra lines above structures with JSDoc blocks', + null, + enableFixer ? (fixer) => { + return fixer.replaceTextRange( + interveningRange, + '\n'.repeat(preferMinLines ? minLines : maxLines) + indent, + ); + } : null, + ); +}, { + iterateAllJsdocs: true, + meta: { + docs: { + description: 'Reports extra lines between functions (and other language structures) and their JSDoc blocks.', + url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/no-lines-after-blocks.md#repos-sticky-header', + }, + fixable: 'whitespace', + schema: [ + { + additionalProperties: false, + properties: { + contexts: { + description: `Set this to an array of strings representing the AST context (or an object with +\`context\` and \`comment\` properties) where you wish the rule to be applied. + +\`context\` defaults to \`any\` and \`comment\` defaults to no specific comment context. + +Overrides the default contexts (\`ArrowFunctionExpression\`, \`FunctionDeclaration\`, +\`FunctionExpression\`). Setting to \`"any"\` may be problematic if you have +JSDoc-style comments at the top of your files. + +See the ["AST and Selectors"](../#advanced-ast-and-selectors) +section of our Advanced docs for more on the expected format.`, + items: { + anyOf: [ + { + type: 'string', + }, + { + additionalProperties: false, + properties: { + comment: { + type: 'string', + }, + context: { + type: 'string', + }, + }, + type: 'object', + }, + ], + }, + type: 'array', + }, + enableFixer: { + description: 'Whether to enable the fixer to remove line breaks', + type: 'boolean', + }, + exemptedBy: { + description: `Tag names to be added to those which will exempt reporting for a block. Defaults to: + +- 'callback' +- 'copyright' +- 'exports' +- 'interface' +- 'event' +- 'external' +- 'file' +- 'fileoverview' +- 'host' +- 'import' +- 'license' +- 'module' +- 'namespace' +- 'overview' +- 'typedef' +`, + items: { + type: 'string', + }, + type: 'array', + }, + overrideDefaultExemptions: { + description: 'Determines whether `exemptedBy` will override the default values. Defaults to `false`.', + type: 'boolean', + }, + preferMinLines: { + description: 'Whether to use the setting `minLines` as the basis for fixing lines going past `maxLines`', + type: 'boolean', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, + ruleSettings: { + maxLines: Number.POSITIVE_INFINITY, + }, +}); diff --git a/test/rules/assertions/noLinesAfterBlocks.js b/test/rules/assertions/noLinesAfterBlocks.js new file mode 100644 index 000000000..6829e22a0 --- /dev/null +++ b/test/rules/assertions/noLinesAfterBlocks.js @@ -0,0 +1,202 @@ +export default { + invalid: [ + { + code: ` + /** This is a description of some function!*/ + + + + + + + function someFunction() {} + `, + errors: [ + { + line: 2, + message: 'There should be no extra lines above structures with JSDoc blocks', + }, + ], + output: ` + /** This is a description of some function!*/ + function someFunction() {} + `, + }, + { + code: ` + /** This is a description of some function!*/ + + function someFunction() {} + `, + errors: [ + { + line: 2, + message: 'There should be no extra lines above structures with JSDoc blocks', + }, + ], + options: [ + { + enableFixer: false, + }, + ], + }, + { + code: ` + /** This is a description of some function!*/ + + + function someFunction() {} + `, + errors: [ + { + line: 2, + message: 'There should be no extra lines above structures with JSDoc blocks', + }, + ], + output: ` + /** This is a description of some function!*/ + + function someFunction() {} + `, + settings: { + jsdoc: { + maxLines: 2, + }, + }, + }, + { + code: ` + /** This is a description of some function!*/ + + + function someFunction() {} + `, + errors: [ + { + line: 2, + message: 'There should be no extra lines above structures with JSDoc blocks', + }, + ], + options: [ + { + preferMinLines: true, + }, + ], + output: ` + /** This is a description of some function!*/ + function someFunction() {} + `, + settings: { + jsdoc: { + maxLines: 2, + minLines: 1, + }, + }, + }, + { + code: ` + /** @typedef SomeType */ + + function someFunction() {} + `, + errors: [ + { + line: 2, + message: 'There should be no extra lines above structures with JSDoc blocks', + }, + ], + options: [ + { + exemptedBy: [ + 'function', + ], + overrideDefaultExemptions: true, + }, + ], + output: ` + /** @typedef SomeType */ + function someFunction() {} + `, + }, + ], + valid: [ + { + code: 'function someFunction() {}', + }, + { + code: '/** JSDoc */ function someFunction() {}', + }, + { + code: ` + /** This is a description of some function! */ + // extra comment + function someFunction() {} + `, + }, + { + code: ` + /** Standalone comment (e.g. a type definition) */ + + /** The actual description */ + function someFunction() {} + `, + }, + { + code: ` + /* Regular block comment */ + + function someFunction() {} + `, + }, + { + code: ` + // Regular line comment + + function someFunction() {} + `, + }, + { + code: ` + /** This is a description of some function!*/ + + function someFunction() {} + `, + settings: { + jsdoc: { + maxLines: 2, + }, + }, + }, + + { + code: ` + /** @typedef {string} SomeType */ + + function someFunction() {} + `, + }, + { + code: ` + /** @function SomeType */ + + function someFunction() {} + `, + options: [ + { + exemptedBy: [ + 'function', + ], + }, + ], + }, + { + code: ` + /** + * JSDoc block at top of file without import declaration context. + */ + + import {sth} from 'sth'; + `, + }, + ], +}; diff --git a/test/rules/ruleNames.json b/test/rules/ruleNames.json index 173f34ecf..f9d24537d 100644 --- a/test/rules/ruleNames.json +++ b/test/rules/ruleNames.json @@ -25,6 +25,7 @@ "no-blank-block-descriptions", "no-blank-blocks", "no-defaults", + "no-lines-after-blocks", "no-missing-syntax", "no-multi-asterisks", "no-restricted-syntax",