| import iterateJsdoc from '../iterateJsdoc.js'; |
| |
| /** |
| * Punctuators that begin a logical group should not require a line before it skipped. Specifically |
| * `[` starts an array, `{` starts an object or block, `(` starts a grouping, and `=` starts a |
| * declaration (like a variable or a type alias). |
| */ |
| const startPunctuators = new Set([ |
| '(', '=', '[', '{', |
| ]); |
| |
| export default iterateJsdoc(({ |
| context, |
| jsdocNode, |
| report, |
| sourceCode, |
| utils, |
| }) => { |
| const { |
| checkBlockStarts, |
| excludedTags = [ |
| 'type', |
| ], |
| ignoreSameLine = true, |
| ignoreSingleLines = true, |
| lines = 1, |
| } = context.options[0] || {}; |
| |
| if (utils.hasATag(excludedTags)) { |
| return; |
| } |
| |
| const tokensBefore = sourceCode.getTokensBefore(jsdocNode, { |
| includeComments: true, |
| }); |
| const tokenBefore = tokensBefore.at(-1); |
| if ( |
| !tokenBefore || ( |
| tokenBefore.type === 'Punctuator' && |
| !checkBlockStarts && |
| startPunctuators.has(tokenBefore.value) |
| ) |
| ) { |
| 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; |
| } |
| |
| if (ignoreSingleLines && jsdocNode.loc?.start.line === jsdocNode.loc?.end.line) { |
| 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 ]*) \*/v)?.[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: { |
| 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', |
| }, |
| fixable: 'code', |
| schema: [ |
| { |
| additionalProperties: false, |
| properties: { |
| checkBlockStarts: { |
| description: `Whether to additionally check the start of blocks, such as classes or functions. |
| Defaults to \`false\`.`, |
| type: 'boolean', |
| }, |
| excludedTags: { |
| description: `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).`, |
| items: { |
| type: 'string', |
| }, |
| type: 'array', |
| }, |
| ignoreSameLine: { |
| description: `This option excludes cases where the JSDoc block occurs on the same line as a |
| preceding code or comment. Defaults to \`true\`.`, |
| type: 'boolean', |
| }, |
| ignoreSingleLines: { |
| description: `This option excludes cases where the JSDoc block is only one line long. |
| Defaults to \`true\`.`, |
| type: 'boolean', |
| }, |
| lines: { |
| description: 'The minimum number of lines to require. Defaults to 1.', |
| type: 'integer', |
| }, |
| }, |
| type: 'object', |
| }, |
| ], |
| type: 'suggestion', |
| }, |
| }); |