Skip to content

Add lines alignment rule #636

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Sep 14, 2020
Merged
64 changes: 64 additions & 0 deletions .README/rules/check-lines-alignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
### `check-lines-alignment`

Reports invalid alignment of JSDoc block lines. This is a
[standard recommended to WordPress code](https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/javascript/#aligning-comments), for example.

#### Options

This rule allows one optional string argument. If it is `"always"` then a
problem is raised when the lines are not aligned. If it is `"never"` then
a problem should be raised when there is more than one space between the
lines parts. Only the non-default `"always"` is implemented for now.

|||
|---|---|
|Context|everywhere|
|Options|(a string matching `"always"|"never"`)|
|Tags|`param`, `arg`, `argument`, `property`, `prop`|

The following patterns are considered problems:

````js
/**
* Function description.
*
* @param {string} lorem Description.
* @param {int} sit Description multi words.
*/
const fn = ( lorem, sit ) => {}
// Options: ["always"]
// Message: Expected JSDoc block lines to be aligned.

/**
* My object.
*
* @typedef {Object} MyObject
*
* @property {string} lorem Description.
* @property {int} sit Description multi words.
*/
// Options: ["always"]
// Message: Expected JSDoc block lines to be aligned.

The following patterns are not considered problems:

````js
/**
* Function description.
*
* @param {string} lorem Description.
* @param {int} sit Description multi words.
*/
const fn = ( lorem, sit ) => {}
// Options: ["always"]

/**
* My object.
*
* @typedef {Object} MyObject
*
* @property {string} lorem Description.
* @property {int} sit Description multi words.
*/
// Options: ["always"]
````
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import checkAccess from './rules/checkAccess';
import checkAlignment from './rules/checkAlignment';
import checkExamples from './rules/checkExamples';
import checkIndentation from './rules/checkIndentation';
import checkLinesAlignment from './rules/checkLinesAlignment';
import checkParamNames from './rules/checkParamNames';
import checkPropertyNames from './rules/checkPropertyNames';
import checkSyntax from './rules/checkSyntax';
Expand Down Expand Up @@ -47,6 +48,7 @@ export default {
'jsdoc/check-alignment': 'warn',
'jsdoc/check-examples': 'off',
'jsdoc/check-indentation': 'off',
'jsdoc/check-lines-alignment': 'off',
'jsdoc/check-param-names': 'warn',
'jsdoc/check-property-names': 'warn',
'jsdoc/check-syntax': 'off',
Expand Down Expand Up @@ -88,6 +90,7 @@ export default {
'check-alignment': checkAlignment,
'check-examples': checkExamples,
'check-indentation': checkIndentation,
'check-lines-alignment': checkLinesAlignment,
'check-param-names': checkParamNames,
'check-property-names': checkPropertyNames,
'check-syntax': checkSyntax,
Expand Down
212 changes: 212 additions & 0 deletions src/rules/checkLinesAlignment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import {
set,
} from 'lodash';
import iterateJsdoc from '../iterateJsdoc';

/**
* Aux method until we consider the dev envs support `String.prototype.matchAll` (Node 12+).
*
* @param {string} string String that will be checked.
* @param {RegExp} regexp Regular expression to run.
* @param {Function} callback Function to be called each iteration.
* @param {int} limit Limit of matches that we want to exec.
*
* @todo [engine:node@>=12]: Remove function and use `String.prototype.matchAll` instead.
*/
const matchAll = (string, regexp, callback, limit) => {
let result;
let index = 0;

while ((result = regexp.exec(string)) && index <= limit - 1) {
// eslint-disable-next-line promise/prefer-await-to-callbacks
callback(result, index++);
}
};

/**
* Get the full description from a line.
*
* @param {string} lineString The line string.
*
* @returns {string} The full description.
*/
const getFullDescription = (lineString) => {
return /(?:\S+\s+){4}(.*)/.exec(lineString)[1];
};

/**
* Get the expected positions for each part.
*
* @param {int[]} partsMaxLength Max length of each part.
* @param {int} indentLevel JSDoc indent level.
*
* @returns {int[]} Expected position for each part.
*/
const getExpectedPositions = (partsMaxLength, indentLevel) => {
// eslint-disable-next-line unicorn/no-reduce
return partsMaxLength.reduce(
(acc, cur, index) => {
return [...acc, cur + acc[index] + 1];
},
[indentLevel],
);
};

/**
* Check is not aligned.
*
* @param {int[]} expectedPositions Expected position for each part.
* @param {Array[]} partsMatrix Parts matrix.
*
* @returns {boolean}
*/
const isNotAligned = (expectedPositions, partsMatrix) => {
return partsMatrix.some((line) => {
return line.some(
({position}, partIndex) => {
return position !== expectedPositions[partIndex];
},
);
});
};

/**
* Fix function creator for the report. It creates a function which fix
* the JSDoc with the correct alignment.
*
* @param {object} comment Comment node.
* @param {int[]} expectedPositions Array with the expected positions.
* @param {Array[]} partsMatrix Parts matrix.
* @param {RegExp} lineRegExp Line regular expression.
* @param {string} tagIndentation Tag indentation.
*
* @returns {Function} Function which fixes the JSDoc alignment.
*/
const createFixer = (comment, expectedPositions, partsMatrix, lineRegExp, tagIndentation) => {
return (fixer) => {
let lineIndex = 0;

// Replace every line with the correct spacings.
const fixed = comment.value.replace(lineRegExp, () => {
// eslint-disable-next-line unicorn/no-reduce
return partsMatrix[lineIndex++].reduce(
(acc, {string}, index) => {
const spacings = ' '.repeat(expectedPositions[index] - acc.length);

return acc + (index === 0 ? tagIndentation : spacings) + string;
},
'',
);
});

return fixer.replaceText(comment, '/*' + fixed + '*/');
};
};

/**
* Check comment per tag.
*
* @param {object} comment Comment node.
* @param {string} tag Tag string.
* @param {string} tagIndentation Tag indentation.
* @param {Function} report Report function.
*/
const checkCommentPerTag = (comment, tag, tagIndentation, report) => {
const lineRegExp = new RegExp(`.*@${tag}[\\s].*`, 'gm');
const lines = comment.value.match(lineRegExp);

if (!lines) {
return;
}

/**
* A matrix containing the current position and the string of each part for each line.
* 0 - Asterisk.
* 1 - Tag.
* 2 - Type.
* 3 - Variable name.
* 4 - Description (Optional).
*/
const partsMatrix = [];

/**
* The max length of each part, comparing all the lines.
*/
const partsMaxLength = [];

// Loop (lines x parts) to populate partsMatrix and partsMaxLength.
lines.forEach((lineString, lineIndex) => {
// All line parts until the first word of the description (if description exists).
matchAll(
lineString,
/\S+/g,
({0: match, index: position}, partIndex) => {
set(partsMatrix, [lineIndex, partIndex], {
position,
string: partIndex === 4 ? getFullDescription(lineString) : match,
});

const partLength = match.length;
const maxLength = partsMaxLength[partIndex];

partsMaxLength[partIndex] = maxLength > partLength ? maxLength : partLength;
},
5,
);
});

const expectedPositions = getExpectedPositions(partsMaxLength, tagIndentation.length);

if (isNotAligned(expectedPositions, partsMatrix)) {
report(
'Expected JSDoc block lines to be aligned.',
createFixer(
comment,
expectedPositions,
partsMatrix,
lineRegExp,
tagIndentation,
),
);
}
};

export default iterateJsdoc(({
jsdocNode,
report,
context,
indent,
}) => {
if (context.options[0] === 'never') {
report('The `never` option is not yet implemented for this rule.');

return;
}

if (context.options[0] !== 'always') {
return;
}

// `indent` is whitespace from line 1 (`/**`), so slice and account for "/".
const tagIndentation = indent + ' ';

['param', 'arg', 'argument', 'property', 'prop'].forEach((tag) => {
checkCommentPerTag(jsdocNode, tag, tagIndentation, report);
});
}, {
iterateAllJsdocs: true,
meta: {
docs: {
description: 'Reports invalid alignment of JSDoc block lines.',
url: 'https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-lines-alignment',
},
fixable: 'whitespace',
schema: [
{
enum: ['always', 'never'],
type: 'string',
},
],
type: 'layout',
},
});
Loading