Skip to content

Commit 2f6897d

Browse files
committed
feat: new rule tag-line; fixes gajus#93
1 parent af137bd commit 2f6897d

File tree

8 files changed

+582
-3
lines changed

8 files changed

+582
-3
lines changed

.README/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,4 +581,5 @@ selector).
581581
{"gitdown": "include", "file": "./rules/require-throws.md"}
582582
{"gitdown": "include", "file": "./rules/require-yields.md"}
583583
{"gitdown": "include", "file": "./rules/require-yields-check.md"}
584+
{"gitdown": "include", "file": "./rules/tag-lines.md"}
584585
{"gitdown": "include", "file": "./rules/valid-types.md"}

.README/rules/tag-lines.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
### `tag-lines`
2+
3+
Enforces lines (or no lines) between tags.
4+
5+
#### Options
6+
7+
The first option is a single string set to "always" or "never" (defaults to
8+
"never").
9+
10+
The second option is an object with the following optional properties.
11+
12+
##### `count` (defaults to 1)
13+
14+
Use with "always" to indicate the number of lines to require be present.
15+
16+
##### `noEndLine` (defaults to `false`)
17+
18+
Use with "always" to indicate tag lines should not be added at the end.
19+
20+
|||
21+
|---|---|
22+
|Context|everywhere|
23+
|Tags|Any|
24+
|Recommended|false|
25+
|Settings|N/A|
26+
|Options|(a string matching `"always" or "never"` and optional object with `count` and `noEndLine`)|
27+
28+
<!-- assertions tagLines -->

README.md

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ JSDoc linting rules for ESLint.
6767
* [`require-throws`](#eslint-plugin-jsdoc-rules-require-throws)
6868
* [`require-yields`](#eslint-plugin-jsdoc-rules-require-yields)
6969
* [`require-yields-check`](#eslint-plugin-jsdoc-rules-require-yields-check)
70+
* [`tag-lines`](#eslint-plugin-jsdoc-rules-tag-lines)
7071
* [`valid-types`](#eslint-plugin-jsdoc-rules-valid-types)
7172

7273

@@ -18230,6 +18231,156 @@ function * quux (foo) {
1823018231
````
1823118232

1823218233

18234+
<a name="eslint-plugin-jsdoc-rules-tag-lines"></a>
18235+
### <code>tag-lines</code>
18236+
18237+
Enforces lines (or no lines) between tags.
18238+
18239+
<a name="eslint-plugin-jsdoc-rules-tag-lines-options-37"></a>
18240+
#### Options
18241+
18242+
The first option is a single string set to "always" or "never" (defaults to
18243+
"never").
18244+
18245+
The second option is an object with the following optional properties.
18246+
18247+
<a name="eslint-plugin-jsdoc-rules-tag-lines-options-37-count-defaults-to-1"></a>
18248+
##### <code>count</code> (defaults to 1)
18249+
18250+
Use with "always" to indicate the number of lines to require be present.
18251+
18252+
<a name="eslint-plugin-jsdoc-rules-tag-lines-options-37-noendline-defaults-to-false"></a>
18253+
##### <code>noEndLine</code> (defaults to <code>false</code>)
18254+
18255+
Use with "always" to indicate tag lines should not be added at the end.
18256+
18257+
|||
18258+
|---|---|
18259+
|Context|everywhere|
18260+
|Tags|Any|
18261+
|Recommended|false|
18262+
|Settings|N/A|
18263+
|Options|(a string matching `"always" or "never"` and optional object with `count` and `noEndLine`)|
18264+
18265+
The following patterns are considered problems:
18266+
18267+
````js
18268+
/**
18269+
* Some description
18270+
* @param {string} a
18271+
* @param {number} b
18272+
*/
18273+
// "jsdoc/tag-lines": ["error"|"warn", "always"]
18274+
// Message: Expected 1 line between tags but found 0
18275+
18276+
/**
18277+
* Some description
18278+
* @param {string} a
18279+
* @param {number} b
18280+
*/
18281+
// "jsdoc/tag-lines": ["error"|"warn", "always",{"count":2}]
18282+
// Message: Expected 2 lines between tags but found 0
18283+
18284+
/**
18285+
* Some description
18286+
* @param {string} a
18287+
*
18288+
* @param {number} b
18289+
*/
18290+
// "jsdoc/tag-lines": ["error"|"warn", "always",{"count":2}]
18291+
// Message: Expected 2 lines between tags but found 1
18292+
18293+
/**
18294+
* Some description
18295+
* @param {string} a
18296+
* @param {number} b
18297+
*/
18298+
// "jsdoc/tag-lines": ["error"|"warn", "always",{"noEndLine":true}]
18299+
// Message: Expected 1 line between tags but found 0
18300+
18301+
/**
18302+
* Some description
18303+
* @param {string} a
18304+
*
18305+
* @param {number} b
18306+
*
18307+
*/
18308+
// "jsdoc/tag-lines": ["error"|"warn", "never"]
18309+
// Message: Expected no lines between tags
18310+
18311+
/**
18312+
* Some description
18313+
* @param {string} a
18314+
*
18315+
* @param {number} b
18316+
*
18317+
*/
18318+
// Message: Expected no lines between tags
18319+
18320+
/**
18321+
* Some description
18322+
* @param {string} a
18323+
*
18324+
* @param {number} b
18325+
* @param {number} c
18326+
*/
18327+
// "jsdoc/tag-lines": ["error"|"warn", "always"]
18328+
// Message: Expected 1 line between tags but found 0
18329+
````
18330+
18331+
The following patterns are not considered problems:
18332+
18333+
````js
18334+
/**
18335+
* Some description
18336+
* @param {string} a
18337+
* @param {number} b
18338+
*/
18339+
18340+
/**
18341+
* Some description
18342+
* @param {string} a
18343+
* @param {number} b
18344+
*/
18345+
// "jsdoc/tag-lines": ["error"|"warn", "never"]
18346+
18347+
/**
18348+
* @param {string} a
18349+
*
18350+
* @param {string} a
18351+
*/
18352+
// "jsdoc/tag-lines": ["error"|"warn", "always",{"noEndLine":true}]
18353+
18354+
/**
18355+
* @param {string} a
18356+
*/
18357+
// "jsdoc/tag-lines": ["error"|"warn", "never",{"noEndLine":true}]
18358+
18359+
/** @param {number} b */
18360+
// "jsdoc/tag-lines": ["error"|"warn", "never",{"noEndLine":true}]
18361+
18362+
/**
18363+
* Some description
18364+
* @param {string} a
18365+
*
18366+
* @param {number} b
18367+
*
18368+
*/
18369+
// "jsdoc/tag-lines": ["error"|"warn", "always"]
18370+
18371+
/**
18372+
* Some description
18373+
* @param {string} a
18374+
*
18375+
*
18376+
* @param {number} b
18377+
*
18378+
*
18379+
*/
18380+
// "jsdoc/tag-lines": ["error"|"warn", "always",{"count":2}]
18381+
````
18382+
18383+
1823318384
<a name="eslint-plugin-jsdoc-rules-valid-types"></a>
1823418385
### <code>valid-types</code>
1823518386

@@ -18310,7 +18461,7 @@ for valid types (based on the tag's `type` value), and either portion checked
1831018461
for presence (based on `false` `name` or `type` values or their `required`
1831118462
value). See the setting for more details.
1831218463

18313-
<a name="eslint-plugin-jsdoc-rules-valid-types-options-37"></a>
18464+
<a name="eslint-plugin-jsdoc-rules-valid-types-options-38"></a>
1831418465
#### Options
1831518466

1831618467
- `allowEmptyNamepaths` (default: true) - Set to `false` to bulk disallow

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import requireReturnsType from './rules/requireReturnsType';
4141
import requireThrows from './rules/requireThrows';
4242
import requireYields from './rules/requireYields';
4343
import requireYieldsCheck from './rules/requireYieldsCheck';
44+
import tagLines from './rules/tagLines';
4445
import validTypes from './rules/validTypes';
4546

4647
export default {
@@ -91,6 +92,7 @@ export default {
9192
'jsdoc/require-throws': 'off',
9293
'jsdoc/require-yields': 'warn',
9394
'jsdoc/require-yields-check': 'warn',
95+
'jsdoc/tag-lines': 'warn',
9496
'jsdoc/valid-types': 'warn',
9597
},
9698
},
@@ -139,6 +141,7 @@ export default {
139141
'require-throws': requireThrows,
140142
'require-yields': requireYields,
141143
'require-yields-check': requireYieldsCheck,
144+
'tag-lines': tagLines,
142145
'valid-types': validTypes,
143146
},
144147
};

src/iterateJsdoc.js

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,11 @@ const getUtils = (
184184
}];
185185
};
186186

187-
utils.removeTag = (tagIndex) => {
187+
utils.removeTag = (idx) => {
188+
return utils.removeTagItem(idx);
189+
};
190+
191+
utils.removeTagItem = (tagIndex, tagSourceOffset = 0) => {
188192
const {source: tagSource} = jsdoc.tags[tagIndex];
189193
let lastIndex;
190194
const firstNumber = jsdoc.source[0].number;
@@ -206,7 +210,8 @@ const getUtils = (
206210

207211
return true;
208212
});
209-
jsdoc.source.splice(sourceIndex, spliceCount);
213+
jsdoc.source.splice(sourceIndex + tagSourceOffset, spliceCount - tagSourceOffset);
214+
tagSource.splice(tagIdx + tagSourceOffset, spliceCount - tagSourceOffset);
210215
lastIndex = sourceIndex;
211216

212217
return true;
@@ -237,6 +242,48 @@ const getUtils = (
237242
});
238243
};
239244

245+
utils.addLines = (tagIndex, tagSourceOffset, numLines) => {
246+
const {source: tagSource} = jsdoc.tags[tagIndex];
247+
let lastIndex;
248+
const firstNumber = jsdoc.source[0].number;
249+
tagSource.some(({number}) => {
250+
const makeLine = () => {
251+
return {
252+
number,
253+
source: '',
254+
tokens: seedTokens({
255+
delimiter: '*',
256+
start: indent + ' ',
257+
}),
258+
};
259+
};
260+
const makeLines = () => {
261+
return Array.from({length: numLines}, makeLine);
262+
};
263+
const sourceIndex = jsdoc.source.findIndex(({
264+
number: srcNumber, tokens: {end},
265+
}) => {
266+
return number === srcNumber && !end;
267+
});
268+
// istanbul ignore else
269+
if (sourceIndex > -1) {
270+
const lines = makeLines();
271+
jsdoc.source.splice(sourceIndex + tagSourceOffset, 0, ...lines);
272+
273+
// tagSource.splice(tagIdx + 1, 0, ...makeLines());
274+
lastIndex = sourceIndex;
275+
276+
return true;
277+
}
278+
279+
// istanbul ignore next
280+
return false;
281+
});
282+
jsdoc.source.slice(lastIndex).forEach((src, idx) => {
283+
src.number = firstNumber + lastIndex + idx;
284+
});
285+
};
286+
240287
utils.flattenRoots = (params) => {
241288
return jsdocUtils.flattenRoots(params);
242289
};

src/rules/tagLines.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import iterateJsdoc from '../iterateJsdoc';
2+
3+
export default iterateJsdoc(({
4+
context,
5+
jsdoc,
6+
utils,
7+
}) => {
8+
const [
9+
alwaysNever = 'never',
10+
{
11+
count = 1,
12+
noEndLine = false,
13+
} = {},
14+
] = context.options;
15+
16+
if (alwaysNever === 'never') {
17+
jsdoc.tags.some((tg, tagIdx) => {
18+
return tg.source.some(({tokens: {tag, name, type, description, end}}, idx) => {
19+
const fixer = () => {
20+
utils.removeTagItem(tagIdx, idx);
21+
};
22+
if (!tag && !name && !type && !description && !end) {
23+
utils.reportJSDoc(
24+
'Expected no lines between tags',
25+
{line: tg.source[0].number + 1},
26+
fixer,
27+
true,
28+
);
29+
30+
return true;
31+
}
32+
33+
return false;
34+
});
35+
});
36+
37+
return;
38+
}
39+
40+
(noEndLine ? jsdoc.tags.slice(0, -1) : jsdoc.tags).some((tg, tagIdx) => {
41+
const lines = [];
42+
43+
tg.source.forEach(({number, tokens: {tag, name, type, description, end}}, idx) => {
44+
if (!tag && !name && !type && !description && !end) {
45+
lines.push({idx, number});
46+
}
47+
});
48+
if (lines.length < count) {
49+
const fixer = () => {
50+
utils.addLines(tagIdx, lines[lines.length - 1]?.idx || 1, count - lines.length);
51+
};
52+
utils.reportJSDoc(
53+
`Expected ${count} line${count === 1 ? '' : 's'} between tags but found ${lines.length}`,
54+
{line: lines[lines.length - 1]?.number || tg.source[0].number},
55+
fixer,
56+
true,
57+
);
58+
59+
return true;
60+
}
61+
62+
return false;
63+
});
64+
}, {
65+
iterateAllJsdocs: true,
66+
meta: {
67+
docs: {
68+
description: 'Enforces lines (or no lines) between tags.',
69+
url: 'https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-tag-lines',
70+
},
71+
fixable: true,
72+
schema: [
73+
{
74+
enum: ['always', 'never'],
75+
type: 'string',
76+
},
77+
{
78+
additionalProperies: false,
79+
properties: {
80+
count: {
81+
type: 'integer',
82+
},
83+
noEndLine: {
84+
type: 'boolean',
85+
},
86+
},
87+
type: 'object',
88+
},
89+
],
90+
type: 'suggestion',
91+
},
92+
});

0 commit comments

Comments
 (0)