Skip to content

Commit 6e2b99d

Browse files
renathoRenatho De Carli Rosabrettz9
authored
feat(check-line-alignment): add rule for line alignment (#636)
Co-authored-by: Renatho De Carli Rosa <[email protected]> Co-authored-by: Brett Zamir <[email protected]>
1 parent 4f73e9c commit 6e2b99d

File tree

5 files changed

+844
-0
lines changed

5 files changed

+844
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
### `check-lines-alignment`
2+
3+
Reports invalid alignment of JSDoc block lines. This is a
4+
[standard recommended to WordPress code](https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/javascript/#aligning-comments), for example.
5+
6+
#### Options
7+
8+
This rule allows one optional string argument. If it is `"always"` then a
9+
problem is raised when the lines are not aligned. If it is `"never"` then
10+
a problem should be raised when there is more than one space between the
11+
lines parts. Only the non-default `"always"` is implemented for now.
12+
13+
|||
14+
|---|---|
15+
|Context|everywhere|
16+
|Options|(a string matching `"always"|"never"`)|
17+
|Tags|`param`, `arg`, `argument`, `property`, `prop`|
18+
19+
The following patterns are considered problems:
20+
21+
````js
22+
/**
23+
* Function description.
24+
*
25+
* @param {string} lorem Description.
26+
* @param {int} sit Description multi words.
27+
*/
28+
const fn = ( lorem, sit ) => {}
29+
// Options: ["always"]
30+
// Message: Expected JSDoc block lines to be aligned.
31+
32+
/**
33+
* My object.
34+
*
35+
* @typedef {Object} MyObject
36+
*
37+
* @property {string} lorem Description.
38+
* @property {int} sit Description multi words.
39+
*/
40+
// Options: ["always"]
41+
// Message: Expected JSDoc block lines to be aligned.
42+
43+
The following patterns are not considered problems:
44+
45+
````js
46+
/**
47+
* Function description.
48+
*
49+
* @param {string} lorem Description.
50+
* @param {int} sit Description multi words.
51+
*/
52+
const fn = ( lorem, sit ) => {}
53+
// Options: ["always"]
54+
55+
/**
56+
* My object.
57+
*
58+
* @typedef {Object} MyObject
59+
*
60+
* @property {string} lorem Description.
61+
* @property {int} sit Description multi words.
62+
*/
63+
// Options: ["always"]
64+
````

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import checkAccess from './rules/checkAccess';
33
import checkAlignment from './rules/checkAlignment';
44
import checkExamples from './rules/checkExamples';
55
import checkIndentation from './rules/checkIndentation';
6+
import checkLinesAlignment from './rules/checkLinesAlignment';
67
import checkParamNames from './rules/checkParamNames';
78
import checkPropertyNames from './rules/checkPropertyNames';
89
import checkSyntax from './rules/checkSyntax';
@@ -47,6 +48,7 @@ export default {
4748
'jsdoc/check-alignment': 'warn',
4849
'jsdoc/check-examples': 'off',
4950
'jsdoc/check-indentation': 'off',
51+
'jsdoc/check-lines-alignment': 'off',
5052
'jsdoc/check-param-names': 'warn',
5153
'jsdoc/check-property-names': 'warn',
5254
'jsdoc/check-syntax': 'off',
@@ -88,6 +90,7 @@ export default {
8890
'check-alignment': checkAlignment,
8991
'check-examples': checkExamples,
9092
'check-indentation': checkIndentation,
93+
'check-lines-alignment': checkLinesAlignment,
9194
'check-param-names': checkParamNames,
9295
'check-property-names': checkPropertyNames,
9396
'check-syntax': checkSyntax,

src/rules/checkLinesAlignment.js

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import {
2+
set,
3+
} from 'lodash';
4+
import iterateJsdoc from '../iterateJsdoc';
5+
6+
/**
7+
* Aux method until we consider the dev envs support `String.prototype.matchAll` (Node 12+).
8+
*
9+
* @param {string} string String that will be checked.
10+
* @param {RegExp} regexp Regular expression to run.
11+
* @param {Function} callback Function to be called each iteration.
12+
* @param {int} limit Limit of matches that we want to exec.
13+
*
14+
* @todo [engine:node@>=12]: Remove function and use `String.prototype.matchAll` instead.
15+
*/
16+
const matchAll = (string, regexp, callback, limit) => {
17+
let result;
18+
let index = 0;
19+
20+
while ((result = regexp.exec(string)) && index <= limit - 1) {
21+
// eslint-disable-next-line promise/prefer-await-to-callbacks
22+
callback(result, index++);
23+
}
24+
};
25+
26+
/**
27+
* Get the full description from a line.
28+
*
29+
* @param {string} lineString The line string.
30+
*
31+
* @returns {string} The full description.
32+
*/
33+
const getFullDescription = (lineString) => {
34+
return /(?:\S+\s+){4}(.*)/.exec(lineString)[1];
35+
};
36+
37+
/**
38+
* Get the expected positions for each part.
39+
*
40+
* @param {int[]} partsMaxLength Max length of each part.
41+
* @param {int} indentLevel JSDoc indent level.
42+
*
43+
* @returns {int[]} Expected position for each part.
44+
*/
45+
const getExpectedPositions = (partsMaxLength, indentLevel) => {
46+
// eslint-disable-next-line unicorn/no-reduce
47+
return partsMaxLength.reduce(
48+
(acc, cur, index) => {
49+
return [...acc, cur + acc[index] + 1];
50+
},
51+
[indentLevel],
52+
);
53+
};
54+
55+
/**
56+
* Check is not aligned.
57+
*
58+
* @param {int[]} expectedPositions Expected position for each part.
59+
* @param {Array[]} partsMatrix Parts matrix.
60+
*
61+
* @returns {boolean}
62+
*/
63+
const isNotAligned = (expectedPositions, partsMatrix) => {
64+
return partsMatrix.some((line) => {
65+
return line.some(
66+
({position}, partIndex) => {
67+
return position !== expectedPositions[partIndex];
68+
},
69+
);
70+
});
71+
};
72+
73+
/**
74+
* Fix function creator for the report. It creates a function which fix
75+
* the JSDoc with the correct alignment.
76+
*
77+
* @param {object} comment Comment node.
78+
* @param {int[]} expectedPositions Array with the expected positions.
79+
* @param {Array[]} partsMatrix Parts matrix.
80+
* @param {RegExp} lineRegExp Line regular expression.
81+
* @param {string} tagIndentation Tag indentation.
82+
*
83+
* @returns {Function} Function which fixes the JSDoc alignment.
84+
*/
85+
const createFixer = (comment, expectedPositions, partsMatrix, lineRegExp, tagIndentation) => {
86+
return (fixer) => {
87+
let lineIndex = 0;
88+
89+
// Replace every line with the correct spacings.
90+
const fixed = comment.value.replace(lineRegExp, () => {
91+
// eslint-disable-next-line unicorn/no-reduce
92+
return partsMatrix[lineIndex++].reduce(
93+
(acc, {string}, index) => {
94+
const spacings = ' '.repeat(expectedPositions[index] - acc.length);
95+
96+
return acc + (index === 0 ? tagIndentation : spacings) + string;
97+
},
98+
'',
99+
);
100+
});
101+
102+
return fixer.replaceText(comment, '/*' + fixed + '*/');
103+
};
104+
};
105+
106+
/**
107+
* Check comment per tag.
108+
*
109+
* @param {object} comment Comment node.
110+
* @param {string} tag Tag string.
111+
* @param {string} tagIndentation Tag indentation.
112+
* @param {Function} report Report function.
113+
*/
114+
const checkCommentPerTag = (comment, tag, tagIndentation, report) => {
115+
const lineRegExp = new RegExp(`.*@${tag}[\\s].*`, 'gm');
116+
const lines = comment.value.match(lineRegExp);
117+
118+
if (!lines) {
119+
return;
120+
}
121+
122+
/**
123+
* A matrix containing the current position and the string of each part for each line.
124+
* 0 - Asterisk.
125+
* 1 - Tag.
126+
* 2 - Type.
127+
* 3 - Variable name.
128+
* 4 - Description (Optional).
129+
*/
130+
const partsMatrix = [];
131+
132+
/**
133+
* The max length of each part, comparing all the lines.
134+
*/
135+
const partsMaxLength = [];
136+
137+
// Loop (lines x parts) to populate partsMatrix and partsMaxLength.
138+
lines.forEach((lineString, lineIndex) => {
139+
// All line parts until the first word of the description (if description exists).
140+
matchAll(
141+
lineString,
142+
/\S+/g,
143+
({0: match, index: position}, partIndex) => {
144+
set(partsMatrix, [lineIndex, partIndex], {
145+
position,
146+
string: partIndex === 4 ? getFullDescription(lineString) : match,
147+
});
148+
149+
const partLength = match.length;
150+
const maxLength = partsMaxLength[partIndex];
151+
152+
partsMaxLength[partIndex] = maxLength > partLength ? maxLength : partLength;
153+
},
154+
5,
155+
);
156+
});
157+
158+
const expectedPositions = getExpectedPositions(partsMaxLength, tagIndentation.length);
159+
160+
if (isNotAligned(expectedPositions, partsMatrix)) {
161+
report(
162+
'Expected JSDoc block lines to be aligned.',
163+
createFixer(
164+
comment,
165+
expectedPositions,
166+
partsMatrix,
167+
lineRegExp,
168+
tagIndentation,
169+
),
170+
);
171+
}
172+
};
173+
174+
export default iterateJsdoc(({
175+
jsdocNode,
176+
report,
177+
context,
178+
indent,
179+
}) => {
180+
if (context.options[0] === 'never') {
181+
report('The `never` option is not yet implemented for this rule.');
182+
183+
return;
184+
}
185+
186+
if (context.options[0] !== 'always') {
187+
return;
188+
}
189+
190+
// `indent` is whitespace from line 1 (`/**`), so slice and account for "/".
191+
const tagIndentation = indent + ' ';
192+
193+
['param', 'arg', 'argument', 'property', 'prop'].forEach((tag) => {
194+
checkCommentPerTag(jsdocNode, tag, tagIndentation, report);
195+
});
196+
}, {
197+
iterateAllJsdocs: true,
198+
meta: {
199+
docs: {
200+
description: 'Reports invalid alignment of JSDoc block lines.',
201+
url: 'https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-lines-alignment',
202+
},
203+
fixable: 'whitespace',
204+
schema: [
205+
{
206+
enum: ['always', 'never'],
207+
type: 'string',
208+
},
209+
],
210+
type: 'layout',
211+
},
212+
});

0 commit comments

Comments
 (0)