Spaces:
Runtime error
Runtime error
/** | |
* @fileoverview Rule to enforce line breaks after each array element | |
* @author Jan Peer Stöcklmair <https://github.com/JPeer264> | |
*/ | |
; | |
const astUtils = require("./utils/ast-utils"); | |
//------------------------------------------------------------------------------ | |
// Rule Definition | |
//------------------------------------------------------------------------------ | |
module.exports = { | |
meta: { | |
type: "layout", | |
docs: { | |
description: "enforce line breaks after each array element", | |
category: "Stylistic Issues", | |
recommended: false, | |
url: "https://eslint.org/docs/rules/array-element-newline" | |
}, | |
fixable: "whitespace", | |
schema: { | |
definitions: { | |
basicConfig: { | |
oneOf: [ | |
{ | |
enum: ["always", "never", "consistent"] | |
}, | |
{ | |
type: "object", | |
properties: { | |
multiline: { | |
type: "boolean" | |
}, | |
minItems: { | |
type: ["integer", "null"], | |
minimum: 0 | |
} | |
}, | |
additionalProperties: false | |
} | |
] | |
} | |
}, | |
items: [ | |
{ | |
oneOf: [ | |
{ | |
$ref: "#/definitions/basicConfig" | |
}, | |
{ | |
type: "object", | |
properties: { | |
ArrayExpression: { | |
$ref: "#/definitions/basicConfig" | |
}, | |
ArrayPattern: { | |
$ref: "#/definitions/basicConfig" | |
} | |
}, | |
additionalProperties: false, | |
minProperties: 1 | |
} | |
] | |
} | |
] | |
}, | |
messages: { | |
unexpectedLineBreak: "There should be no linebreak here.", | |
missingLineBreak: "There should be a linebreak after this element." | |
} | |
}, | |
create(context) { | |
const sourceCode = context.getSourceCode(); | |
//---------------------------------------------------------------------- | |
// Helpers | |
//---------------------------------------------------------------------- | |
/** | |
* Normalizes a given option value. | |
* @param {string|Object|undefined} providedOption An option value to parse. | |
* @returns {{multiline: boolean, minItems: number}} Normalized option object. | |
*/ | |
function normalizeOptionValue(providedOption) { | |
let consistent = false; | |
let multiline = false; | |
let minItems; | |
const option = providedOption || "always"; | |
if (!option || option === "always" || option.minItems === 0) { | |
minItems = 0; | |
} else if (option === "never") { | |
minItems = Number.POSITIVE_INFINITY; | |
} else if (option === "consistent") { | |
consistent = true; | |
minItems = Number.POSITIVE_INFINITY; | |
} else { | |
multiline = Boolean(option.multiline); | |
minItems = option.minItems || Number.POSITIVE_INFINITY; | |
} | |
return { consistent, multiline, minItems }; | |
} | |
/** | |
* Normalizes a given option value. | |
* @param {string|Object|undefined} options An option value to parse. | |
* @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object. | |
*/ | |
function normalizeOptions(options) { | |
if (options && (options.ArrayExpression || options.ArrayPattern)) { | |
let expressionOptions, patternOptions; | |
if (options.ArrayExpression) { | |
expressionOptions = normalizeOptionValue(options.ArrayExpression); | |
} | |
if (options.ArrayPattern) { | |
patternOptions = normalizeOptionValue(options.ArrayPattern); | |
} | |
return { ArrayExpression: expressionOptions, ArrayPattern: patternOptions }; | |
} | |
const value = normalizeOptionValue(options); | |
return { ArrayExpression: value, ArrayPattern: value }; | |
} | |
/** | |
* Reports that there shouldn't be a line break after the first token | |
* @param {Token} token The token to use for the report. | |
* @returns {void} | |
*/ | |
function reportNoLineBreak(token) { | |
const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true }); | |
context.report({ | |
loc: { | |
start: tokenBefore.loc.end, | |
end: token.loc.start | |
}, | |
messageId: "unexpectedLineBreak", | |
fix(fixer) { | |
if (astUtils.isCommentToken(tokenBefore)) { | |
return null; | |
} | |
if (!astUtils.isTokenOnSameLine(tokenBefore, token)) { | |
return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], " "); | |
} | |
/* | |
* This will check if the comma is on the same line as the next element | |
* Following array: | |
* [ | |
* 1 | |
* , 2 | |
* , 3 | |
* ] | |
* | |
* will be fixed to: | |
* [ | |
* 1, 2, 3 | |
* ] | |
*/ | |
const twoTokensBefore = sourceCode.getTokenBefore(tokenBefore, { includeComments: true }); | |
if (astUtils.isCommentToken(twoTokensBefore)) { | |
return null; | |
} | |
return fixer.replaceTextRange([twoTokensBefore.range[1], tokenBefore.range[0]], ""); | |
} | |
}); | |
} | |
/** | |
* Reports that there should be a line break after the first token | |
* @param {Token} token The token to use for the report. | |
* @returns {void} | |
*/ | |
function reportRequiredLineBreak(token) { | |
const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true }); | |
context.report({ | |
loc: { | |
start: tokenBefore.loc.end, | |
end: token.loc.start | |
}, | |
messageId: "missingLineBreak", | |
fix(fixer) { | |
return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], "\n"); | |
} | |
}); | |
} | |
/** | |
* Reports a given node if it violated this rule. | |
* @param {ASTNode} node A node to check. This is an ObjectExpression node or an ObjectPattern node. | |
* @returns {void} | |
*/ | |
function check(node) { | |
const elements = node.elements; | |
const normalizedOptions = normalizeOptions(context.options[0]); | |
const options = normalizedOptions[node.type]; | |
if (!options) { | |
return; | |
} | |
let elementBreak = false; | |
/* | |
* MULTILINE: true | |
* loop through every element and check | |
* if at least one element has linebreaks inside | |
* this ensures that following is not valid (due to elements are on the same line): | |
* | |
* [ | |
* 1, | |
* 2, | |
* 3 | |
* ] | |
*/ | |
if (options.multiline) { | |
elementBreak = elements | |
.filter(element => element !== null) | |
.some(element => element.loc.start.line !== element.loc.end.line); | |
} | |
const linebreaksCount = node.elements.map((element, i) => { | |
const previousElement = elements[i - 1]; | |
if (i === 0 || element === null || previousElement === null) { | |
return false; | |
} | |
const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken); | |
const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken); | |
const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken); | |
return !astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement); | |
}).filter(isBreak => isBreak === true).length; | |
const needsLinebreaks = ( | |
elements.length >= options.minItems || | |
( | |
options.multiline && | |
elementBreak | |
) || | |
( | |
options.consistent && | |
linebreaksCount > 0 && | |
linebreaksCount < node.elements.length | |
) | |
); | |
elements.forEach((element, i) => { | |
const previousElement = elements[i - 1]; | |
if (i === 0 || element === null || previousElement === null) { | |
return; | |
} | |
const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken); | |
const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken); | |
const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken); | |
if (needsLinebreaks) { | |
if (astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) { | |
reportRequiredLineBreak(firstTokenOfCurrentElement); | |
} | |
} else { | |
if (!astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) { | |
reportNoLineBreak(firstTokenOfCurrentElement); | |
} | |
} | |
}); | |
} | |
//---------------------------------------------------------------------- | |
// Public | |
//---------------------------------------------------------------------- | |
return { | |
ArrayPattern: check, | |
ArrayExpression: check | |
}; | |
} | |
}; | |