Skip to content

Commit 0265923

Browse files
committedAug 6, 2024·
feat(lines-before-block): add new rule; fixes #1209
BREAKING CHANGE: Adds new rule to recommended
1 parent ce230a8 commit 0265923

File tree

8 files changed

+540
-0
lines changed

8 files changed

+540
-0
lines changed
 

‎.README/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ non-default-recommended fixer).
247247
|: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|
248248
|: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)|
249249
|||[informative-docs](./docs/rules/informative-docs.md#readme)|Reports on JSDoc texts that serve only to restate their attached name.|
250+
|:heavy_check_mark:||[lines-before-block](./docs/rules/lines-before-block.md#readme)|Enforces minimum number of newlines before JSDoc comment blocks|
250251
|||[match-description](./docs/rules/match-description.md#readme)|Defines customizable regular expression rules for your tag descriptions|
251252
||: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|
252253
|: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|

‎.README/rules/lines-before-block.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# `lines-before-block`
2+
3+
This rule enforces minimum number of newlines before JSDoc comment blocks
4+
(except at the beginning of a file).
5+
6+
## Options
7+
8+
### `lines`
9+
10+
The minimum number of lines to require. Defaults to 1.
11+
12+
### `ignoreSameLine`
13+
14+
This option excludes cases where the JSDoc block occurs on the same line as a
15+
preceding code or comment. Defaults to `true`.
16+
17+
### `excludedTags`
18+
19+
An array of tags whose presence in the JSDoc block will prevent the
20+
application of the rule. Defaults to `['type']` (i.e., if `@type` is present,
21+
lines before the block will not be added).
22+
23+
|||
24+
|---|---|
25+
|Context|everywhere|
26+
|Tags|N/A|
27+
|Recommended|true|
28+
|Settings||
29+
|Options|`excludedTags`, `ignoreSameLine`, `lines`|
30+
31+
## Failing examples
32+
33+
<!-- assertions-failing linesBeforeBlock -->
34+
35+
## Passing examples
36+
37+
<!-- assertions-passing linesBeforeBlock -->

‎README.md

+1
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ non-default-recommended fixer).
277277
|: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|
278278
|: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)|
279279
|||[informative-docs](./docs/rules/informative-docs.md#readme)|Reports on JSDoc texts that serve only to restate their attached name.|
280+
|:heavy_check_mark:||[lines-before-block](./docs/rules/lines-before-block.md#readme)|Enforces minimum number of newlines before JSDoc comment blocks|
280281
|||[match-description](./docs/rules/match-description.md#readme)|Defines customizable regular expression rules for your tag descriptions|
281282
||: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|
282283
|: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|

‎docs/rules/lines-before-block.md

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<a name="user-content-lines-before-block"></a>
2+
<a name="lines-before-block"></a>
3+
# <code>lines-before-block</code>
4+
5+
This rule enforces minimum number of newlines before JSDoc comment blocks
6+
(except at the beginning of a file).
7+
8+
<a name="user-content-lines-before-block-options"></a>
9+
<a name="lines-before-block-options"></a>
10+
## Options
11+
12+
<a name="user-content-lines-before-block-options-lines"></a>
13+
<a name="lines-before-block-options-lines"></a>
14+
### <code>lines</code>
15+
16+
The minimum number of lines to require. Defaults to 1.
17+
18+
<a name="user-content-lines-before-block-options-ignoresameline"></a>
19+
<a name="lines-before-block-options-ignoresameline"></a>
20+
### <code>ignoreSameLine</code>
21+
22+
This option excludes cases where the JSDoc block occurs on the same line as a
23+
preceding code or comment. Defaults to `true`.
24+
25+
<a name="user-content-lines-before-block-options-excludedtags"></a>
26+
<a name="lines-before-block-options-excludedtags"></a>
27+
### <code>excludedTags</code>
28+
29+
An array of tags whose presence in the JSDoc block will prevent the
30+
application of the rule. Defaults to `['type']` (i.e., if `@type` is present,
31+
lines before the block will not be added).
32+
33+
|||
34+
|---|---|
35+
|Context|everywhere|
36+
|Tags|N/A|
37+
|Recommended|true|
38+
|Settings||
39+
|Options|`excludedTags`, `ignoreSameLine`, `lines`|
40+
41+
<a name="user-content-lines-before-block-failing-examples"></a>
42+
<a name="lines-before-block-failing-examples"></a>
43+
## Failing examples
44+
45+
The following patterns are considered problems:
46+
47+
````ts
48+
someCode;
49+
/**
50+
*
51+
*/
52+
// Message: Required 1 line(s) before JSDoc block
53+
54+
someCode; /**
55+
*
56+
*/
57+
// "jsdoc/lines-before-block": ["error"|"warn", {"ignoreSameLine":false}]
58+
// Message: Required 1 line(s) before JSDoc block
59+
60+
someCode; /** */
61+
// "jsdoc/lines-before-block": ["error"|"warn", {"ignoreSameLine":false}]
62+
// Message: Required 1 line(s) before JSDoc block
63+
64+
someCode;
65+
/**
66+
*
67+
*/
68+
// "jsdoc/lines-before-block": ["error"|"warn", {"lines":2}]
69+
// Message: Required 2 line(s) before JSDoc block
70+
71+
// Some comment
72+
/**
73+
*
74+
*/
75+
// Message: Required 1 line(s) before JSDoc block
76+
77+
/* Some comment */
78+
/**
79+
*
80+
*/
81+
// Message: Required 1 line(s) before JSDoc block
82+
83+
/**
84+
* Some comment
85+
*/
86+
/**
87+
*
88+
*/
89+
// Message: Required 1 line(s) before JSDoc block
90+
````
91+
92+
93+
94+
<a name="user-content-lines-before-block-passing-examples"></a>
95+
<a name="lines-before-block-passing-examples"></a>
96+
## Passing examples
97+
98+
The following patterns are not considered problems:
99+
100+
````ts
101+
/**
102+
*
103+
*/
104+
105+
someCode;
106+
107+
/**
108+
*
109+
*/
110+
111+
someCode;
112+
113+
114+
/**
115+
*
116+
*/
117+
// "jsdoc/lines-before-block": ["error"|"warn", {"lines":2}]
118+
119+
// Some comment
120+
121+
/**
122+
*
123+
*/
124+
125+
/* Some comment */
126+
127+
/**
128+
*
129+
*/
130+
131+
/**
132+
* Some comment
133+
*/
134+
135+
/**
136+
*
137+
*/
138+
139+
someCode; /** */
140+
141+
const a = {
142+
someProp: /** @type {SomeCast} */ (someVal)
143+
};
144+
145+
const a = /** @lends SomeClass */ {
146+
someProp: (someVal)
147+
};
148+
// "jsdoc/lines-before-block": ["error"|"warn", {"excludedTags":["lends"],"ignoreSameLine":false}]
149+
````
150+

‎src/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import emptyTags from './rules/emptyTags.js';
1515
import implementsOnClasses from './rules/implementsOnClasses.js';
1616
import importsAsDependencies from './rules/importsAsDependencies.js';
1717
import informativeDocs from './rules/informativeDocs.js';
18+
import linesBeforeBlock from './rules/linesBeforeBlock.js';
1819
import matchDescription from './rules/matchDescription.js';
1920
import matchName from './rules/matchName.js';
2021
import multilineBlocks from './rules/multilineBlocks.js';
@@ -92,6 +93,7 @@ const index = {
9293
'implements-on-classes': implementsOnClasses,
9394
'imports-as-dependencies': importsAsDependencies,
9495
'informative-docs': informativeDocs,
96+
'lines-before-block': linesBeforeBlock,
9597
'match-description': matchDescription,
9698
'match-name': matchName,
9799
'multiline-blocks': multilineBlocks,
@@ -167,6 +169,7 @@ const createRecommendedRuleset = (warnOrError, flatName) => {
167169
'jsdoc/implements-on-classes': warnOrError,
168170
'jsdoc/imports-as-dependencies': 'off',
169171
'jsdoc/informative-docs': 'off',
172+
'jsdoc/lines-before-block': warnOrError,
170173
'jsdoc/match-description': 'off',
171174
'jsdoc/match-name': 'off',
172175
'jsdoc/multiline-blocks': warnOrError,

‎src/rules/linesBeforeBlock.js

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import iterateJsdoc from '../iterateJsdoc.js';
2+
3+
export default iterateJsdoc(({
4+
context,
5+
jsdocNode,
6+
sourceCode,
7+
report,
8+
utils,
9+
}) => {
10+
const {
11+
lines = 1,
12+
ignoreSameLine = true,
13+
excludedTags = ['type']
14+
} = context.options[0] || {};
15+
16+
if (utils.hasATag(excludedTags)) {
17+
return;
18+
}
19+
20+
const tokensBefore = sourceCode.getTokensBefore(jsdocNode, {includeComments: true});
21+
const tokenBefore = tokensBefore.slice(-1)[0];
22+
if (!tokenBefore) {
23+
return;
24+
}
25+
26+
if (tokenBefore.loc?.end?.line + lines >=
27+
/** @type {number} */
28+
(jsdocNode.loc?.start?.line)
29+
) {
30+
const startLine = jsdocNode.loc?.start?.line;
31+
const sameLine = tokenBefore.loc?.end?.line === startLine;
32+
33+
if (sameLine && ignoreSameLine) {
34+
return;
35+
}
36+
37+
/** @type {import('eslint').Rule.ReportFixer} */
38+
const fix = (fixer) => {
39+
let indent = '';
40+
if (sameLine) {
41+
const spaceDiff = /** @type {number} */ (jsdocNode.loc?.start?.column) -
42+
/** @type {number} */ (tokenBefore.loc?.end?.column);
43+
// @ts-expect-error Should be a comment
44+
indent = /** @type {import('estree').Comment} */ (
45+
jsdocNode
46+
).value.match(/^\*\n([ \t]*) \*/)?.[1]?.slice(spaceDiff);
47+
if (!indent) {
48+
/** @type {import('eslint').AST.Token|import('estree').Comment|undefined} */
49+
let tokenPrior = tokenBefore;
50+
let startColumn;
51+
while (tokenPrior && tokenPrior?.loc?.start?.line === startLine) {
52+
startColumn = tokenPrior.loc?.start?.column;
53+
tokenPrior = tokensBefore.pop();
54+
}
55+
indent = ' '.repeat(
56+
/* c8 ignore next */
57+
/** @type {number} */ (startColumn ? startColumn - 1 : 0)
58+
);
59+
}
60+
}
61+
62+
return fixer.insertTextAfter(
63+
/** @type {import('eslint').AST.Token} */
64+
(tokenBefore),
65+
'\n'.repeat(lines) +
66+
(sameLine ? '\n' + indent : '')
67+
);
68+
};
69+
report(`Required ${lines} line(s) before JSDoc block`, fix);
70+
}
71+
}, {
72+
iterateAllJsdocs: true,
73+
meta: {
74+
fixable: 'code',
75+
docs: {
76+
description: 'Enforces minimum number of newlines before JSDoc comment blocks',
77+
url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/lines-before-block.md#repos-sticky-header',
78+
},
79+
schema: [
80+
{
81+
additionalProperties: false,
82+
properties: {
83+
excludedTags: {
84+
type: 'array',
85+
items: {
86+
type: 'string'
87+
}
88+
},
89+
ignoreSameLine: {
90+
type: 'boolean',
91+
},
92+
lines: {
93+
type: 'integer'
94+
}
95+
},
96+
type: 'object',
97+
},
98+
],
99+
type: 'suggestion',
100+
},
101+
});
+246
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
export default {
2+
invalid: [
3+
{
4+
code: `
5+
someCode;
6+
/**
7+
*
8+
*/
9+
`,
10+
errors: [
11+
{
12+
line: 3,
13+
message: 'Required 1 line(s) before JSDoc block',
14+
},
15+
],
16+
output: `
17+
someCode;
18+
19+
/**
20+
*
21+
*/
22+
`,
23+
},
24+
{
25+
code: `
26+
someCode; /**
27+
*
28+
*/
29+
`,
30+
errors: [
31+
{
32+
line: 2,
33+
message: 'Required 1 line(s) before JSDoc block',
34+
},
35+
],
36+
options: [
37+
{
38+
ignoreSameLine: false
39+
}
40+
],
41+
output: `
42+
someCode;
43+
44+
/**
45+
*
46+
*/
47+
`,
48+
},
49+
{
50+
code: `
51+
someCode; /** */
52+
`,
53+
errors: [
54+
{
55+
line: 2,
56+
message: 'Required 1 line(s) before JSDoc block',
57+
},
58+
],
59+
options: [
60+
{
61+
ignoreSameLine: false
62+
}
63+
],
64+
output: `
65+
someCode;
66+
67+
/** */
68+
`,
69+
},
70+
{
71+
code: `
72+
someCode;
73+
/**
74+
*
75+
*/
76+
`,
77+
errors: [
78+
{
79+
line: 3,
80+
message: 'Required 2 line(s) before JSDoc block',
81+
},
82+
],
83+
options: [
84+
{
85+
lines: 2,
86+
},
87+
],
88+
output: `
89+
someCode;
90+
91+
92+
/**
93+
*
94+
*/
95+
`,
96+
},
97+
{
98+
code: `
99+
// Some comment
100+
/**
101+
*
102+
*/
103+
`,
104+
errors: [
105+
{
106+
line: 3,
107+
message: 'Required 1 line(s) before JSDoc block',
108+
},
109+
],
110+
output: `
111+
// Some comment
112+
113+
/**
114+
*
115+
*/
116+
`,
117+
},
118+
{
119+
code: `
120+
/* Some comment */
121+
/**
122+
*
123+
*/
124+
`,
125+
errors: [
126+
{
127+
line: 3,
128+
message: 'Required 1 line(s) before JSDoc block',
129+
},
130+
],
131+
output: `
132+
/* Some comment */
133+
134+
/**
135+
*
136+
*/
137+
`,
138+
},
139+
{
140+
code: `
141+
/**
142+
* Some comment
143+
*/
144+
/**
145+
*
146+
*/
147+
`,
148+
errors: [
149+
{
150+
line: 5,
151+
message: 'Required 1 line(s) before JSDoc block',
152+
},
153+
],
154+
output: `
155+
/**
156+
* Some comment
157+
*/
158+
159+
/**
160+
*
161+
*/
162+
`,
163+
},
164+
],
165+
valid: [
166+
{
167+
code: `/**\n *\n */`,
168+
},
169+
{
170+
code: `
171+
someCode;
172+
173+
/**
174+
*
175+
*/
176+
`,
177+
},
178+
{
179+
code: `
180+
someCode;
181+
182+
183+
/**
184+
*
185+
*/
186+
`,
187+
options: [
188+
{
189+
lines: 2,
190+
},
191+
],
192+
},
193+
{
194+
code: `
195+
// Some comment
196+
197+
/**
198+
*
199+
*/
200+
`,
201+
},
202+
{
203+
code: `
204+
/* Some comment */
205+
206+
/**
207+
*
208+
*/
209+
`,
210+
},
211+
{
212+
code: `
213+
/**
214+
* Some comment
215+
*/
216+
217+
/**
218+
*
219+
*/
220+
`,
221+
},
222+
{
223+
code: `
224+
someCode; /** */
225+
`,
226+
},
227+
{
228+
code: `const a = {
229+
someProp: /** @type {SomeCast} */ (someVal)
230+
};
231+
`,
232+
},
233+
{
234+
code: `const a = /** @lends SomeClass */ {
235+
someProp: (someVal)
236+
};
237+
`,
238+
options: [
239+
{
240+
excludedTags: ['lends'],
241+
ignoreSameLine: false
242+
}
243+
]
244+
},
245+
],
246+
};

‎test/rules/ruleNames.json

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"implements-on-classes",
1717
"imports-as-dependencies",
1818
"informative-docs",
19+
"lines-before-block",
1920
"match-description",
2021
"match-name",
2122
"multiline-blocks",

0 commit comments

Comments
 (0)
Please sign in to comment.