582 lines
15 KiB
JavaScript
582 lines
15 KiB
JavaScript
/**
|
|
* @fileoverview Enforces empty lines around comments.
|
|
* @author Jamund Ferguson
|
|
* @deprecated in ESLint v8.53.0
|
|
*/
|
|
"use strict";
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Requirements
|
|
//------------------------------------------------------------------------------
|
|
|
|
const astUtils = require("./utils/ast-utils");
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Helpers
|
|
//------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Return an array with any line numbers that are empty.
|
|
* @param {Array} lines An array of each line of the file.
|
|
* @returns {Array} An array of line numbers.
|
|
*/
|
|
function getEmptyLineNums(lines) {
|
|
const emptyLines = lines
|
|
.map((line, i) => ({
|
|
code: line.trim(),
|
|
num: i + 1,
|
|
}))
|
|
.filter(line => !line.code)
|
|
.map(line => line.num);
|
|
|
|
return emptyLines;
|
|
}
|
|
|
|
/**
|
|
* Return an array with any line numbers that contain comments.
|
|
* @param {Array} comments An array of comment tokens.
|
|
* @returns {Array} An array of line numbers.
|
|
*/
|
|
function getCommentLineNums(comments) {
|
|
const lines = [];
|
|
|
|
comments.forEach(token => {
|
|
const start = token.loc.start.line;
|
|
const end = token.loc.end.line;
|
|
|
|
lines.push(start, end);
|
|
});
|
|
return lines;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Rule Definition
|
|
//------------------------------------------------------------------------------
|
|
|
|
/** @type {import('../types').Rule.RuleModule} */
|
|
module.exports = {
|
|
meta: {
|
|
deprecated: {
|
|
message: "Formatting rules are being moved out of ESLint core.",
|
|
url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
|
|
deprecatedSince: "8.53.0",
|
|
availableUntil: "10.0.0",
|
|
replacedBy: [
|
|
{
|
|
message:
|
|
"ESLint Stylistic now maintains deprecated stylistic core rules.",
|
|
url: "https://eslint.style/guide/migration",
|
|
plugin: {
|
|
name: "@stylistic/eslint-plugin-js",
|
|
url: "https://eslint.style/packages/js",
|
|
},
|
|
rule: {
|
|
name: "lines-around-comment",
|
|
url: "https://eslint.style/rules/js/lines-around-comment",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
type: "layout",
|
|
|
|
docs: {
|
|
description: "Require empty lines around comments",
|
|
recommended: false,
|
|
url: "https://eslint.org/docs/latest/rules/lines-around-comment",
|
|
},
|
|
|
|
fixable: "whitespace",
|
|
|
|
schema: [
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
beforeBlockComment: {
|
|
type: "boolean",
|
|
default: true,
|
|
},
|
|
afterBlockComment: {
|
|
type: "boolean",
|
|
default: false,
|
|
},
|
|
beforeLineComment: {
|
|
type: "boolean",
|
|
default: false,
|
|
},
|
|
afterLineComment: {
|
|
type: "boolean",
|
|
default: false,
|
|
},
|
|
allowBlockStart: {
|
|
type: "boolean",
|
|
default: false,
|
|
},
|
|
allowBlockEnd: {
|
|
type: "boolean",
|
|
default: false,
|
|
},
|
|
allowClassStart: {
|
|
type: "boolean",
|
|
},
|
|
allowClassEnd: {
|
|
type: "boolean",
|
|
},
|
|
allowObjectStart: {
|
|
type: "boolean",
|
|
},
|
|
allowObjectEnd: {
|
|
type: "boolean",
|
|
},
|
|
allowArrayStart: {
|
|
type: "boolean",
|
|
},
|
|
allowArrayEnd: {
|
|
type: "boolean",
|
|
},
|
|
ignorePattern: {
|
|
type: "string",
|
|
},
|
|
applyDefaultIgnorePatterns: {
|
|
type: "boolean",
|
|
},
|
|
afterHashbangComment: {
|
|
type: "boolean",
|
|
default: false,
|
|
},
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
],
|
|
messages: {
|
|
after: "Expected line after comment.",
|
|
before: "Expected line before comment.",
|
|
},
|
|
},
|
|
|
|
create(context) {
|
|
const options = Object.assign({}, context.options[0]);
|
|
const ignorePattern = options.ignorePattern;
|
|
const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN;
|
|
const customIgnoreRegExp = new RegExp(ignorePattern, "u");
|
|
const applyDefaultIgnorePatterns =
|
|
options.applyDefaultIgnorePatterns !== false;
|
|
|
|
options.beforeBlockComment =
|
|
typeof options.beforeBlockComment !== "undefined"
|
|
? options.beforeBlockComment
|
|
: true;
|
|
|
|
const sourceCode = context.sourceCode;
|
|
|
|
const lines = sourceCode.lines,
|
|
numLines = lines.length + 1,
|
|
comments = sourceCode.getAllComments(),
|
|
commentLines = getCommentLineNums(comments),
|
|
emptyLines = getEmptyLineNums(lines),
|
|
commentAndEmptyLines = new Set(commentLines.concat(emptyLines));
|
|
|
|
/**
|
|
* Returns whether or not comments are on lines starting with or ending with code
|
|
* @param {token} token The comment token to check.
|
|
* @returns {boolean} True if the comment is not alone.
|
|
*/
|
|
function codeAroundComment(token) {
|
|
let currentToken = token;
|
|
|
|
do {
|
|
currentToken = sourceCode.getTokenBefore(currentToken, {
|
|
includeComments: true,
|
|
});
|
|
} while (currentToken && astUtils.isCommentToken(currentToken));
|
|
|
|
if (
|
|
currentToken &&
|
|
astUtils.isTokenOnSameLine(currentToken, token)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
currentToken = token;
|
|
do {
|
|
currentToken = sourceCode.getTokenAfter(currentToken, {
|
|
includeComments: true,
|
|
});
|
|
} while (currentToken && astUtils.isCommentToken(currentToken));
|
|
|
|
if (
|
|
currentToken &&
|
|
astUtils.isTokenOnSameLine(token, currentToken)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are inside a node type or not.
|
|
* @param {ASTNode} parent The Comment parent node.
|
|
* @param {string} nodeType The parent type to check against.
|
|
* @returns {boolean} True if the comment is inside nodeType.
|
|
*/
|
|
function isParentNodeType(parent, nodeType) {
|
|
return (
|
|
parent.type === nodeType ||
|
|
(parent.body && parent.body.type === nodeType) ||
|
|
(parent.consequent && parent.consequent.type === nodeType)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the parent node that contains the given token.
|
|
* @param {token} token The token to check.
|
|
* @returns {ASTNode|null} The parent node that contains the given token.
|
|
*/
|
|
function getParentNodeOfToken(token) {
|
|
const node = sourceCode.getNodeByRangeIndex(token.range[0]);
|
|
|
|
/*
|
|
* For the purpose of this rule, the comment token is in a `StaticBlock` node only
|
|
* if it's inside the braces of that `StaticBlock` node.
|
|
*
|
|
* Example where this function returns `null`:
|
|
*
|
|
* static
|
|
* // comment
|
|
* {
|
|
* }
|
|
*
|
|
* Example where this function returns `StaticBlock` node:
|
|
*
|
|
* static
|
|
* {
|
|
* // comment
|
|
* }
|
|
*
|
|
*/
|
|
if (node && node.type === "StaticBlock") {
|
|
const openingBrace = sourceCode.getFirstToken(node, {
|
|
skip: 1,
|
|
}); // skip the `static` token
|
|
|
|
return token.range[0] >= openingBrace.range[0] ? node : null;
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the parent start or not.
|
|
* @param {token} token The Comment token.
|
|
* @param {string} nodeType The parent type to check against.
|
|
* @returns {boolean} True if the comment is at parent start.
|
|
*/
|
|
function isCommentAtParentStart(token, nodeType) {
|
|
const parent = getParentNodeOfToken(token);
|
|
|
|
if (parent && isParentNodeType(parent, nodeType)) {
|
|
let parentStartNodeOrToken = parent;
|
|
|
|
if (parent.type === "StaticBlock") {
|
|
parentStartNodeOrToken = sourceCode.getFirstToken(parent, {
|
|
skip: 1,
|
|
}); // opening brace of the static block
|
|
} else if (parent.type === "SwitchStatement") {
|
|
parentStartNodeOrToken = sourceCode.getTokenAfter(
|
|
parent.discriminant,
|
|
{
|
|
filter: astUtils.isOpeningBraceToken,
|
|
},
|
|
); // opening brace of the switch statement
|
|
}
|
|
|
|
return (
|
|
token.loc.start.line -
|
|
parentStartNodeOrToken.loc.start.line ===
|
|
1
|
|
);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the parent end or not.
|
|
* @param {token} token The Comment token.
|
|
* @param {string} nodeType The parent type to check against.
|
|
* @returns {boolean} True if the comment is at parent end.
|
|
*/
|
|
function isCommentAtParentEnd(token, nodeType) {
|
|
const parent = getParentNodeOfToken(token);
|
|
|
|
return (
|
|
!!parent &&
|
|
isParentNodeType(parent, nodeType) &&
|
|
parent.loc.end.line - token.loc.end.line === 1
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the block start or not.
|
|
* @param {token} token The Comment token.
|
|
* @returns {boolean} True if the comment is at block start.
|
|
*/
|
|
function isCommentAtBlockStart(token) {
|
|
return (
|
|
isCommentAtParentStart(token, "ClassBody") ||
|
|
isCommentAtParentStart(token, "BlockStatement") ||
|
|
isCommentAtParentStart(token, "StaticBlock") ||
|
|
isCommentAtParentStart(token, "SwitchCase") ||
|
|
isCommentAtParentStart(token, "SwitchStatement")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the block end or not.
|
|
* @param {token} token The Comment token.
|
|
* @returns {boolean} True if the comment is at block end.
|
|
*/
|
|
function isCommentAtBlockEnd(token) {
|
|
return (
|
|
isCommentAtParentEnd(token, "ClassBody") ||
|
|
isCommentAtParentEnd(token, "BlockStatement") ||
|
|
isCommentAtParentEnd(token, "StaticBlock") ||
|
|
isCommentAtParentEnd(token, "SwitchCase") ||
|
|
isCommentAtParentEnd(token, "SwitchStatement")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the class start or not.
|
|
* @param {token} token The Comment token.
|
|
* @returns {boolean} True if the comment is at class start.
|
|
*/
|
|
function isCommentAtClassStart(token) {
|
|
return isCommentAtParentStart(token, "ClassBody");
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the class end or not.
|
|
* @param {token} token The Comment token.
|
|
* @returns {boolean} True if the comment is at class end.
|
|
*/
|
|
function isCommentAtClassEnd(token) {
|
|
return isCommentAtParentEnd(token, "ClassBody");
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the object start or not.
|
|
* @param {token} token The Comment token.
|
|
* @returns {boolean} True if the comment is at object start.
|
|
*/
|
|
function isCommentAtObjectStart(token) {
|
|
return (
|
|
isCommentAtParentStart(token, "ObjectExpression") ||
|
|
isCommentAtParentStart(token, "ObjectPattern")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the object end or not.
|
|
* @param {token} token The Comment token.
|
|
* @returns {boolean} True if the comment is at object end.
|
|
*/
|
|
function isCommentAtObjectEnd(token) {
|
|
return (
|
|
isCommentAtParentEnd(token, "ObjectExpression") ||
|
|
isCommentAtParentEnd(token, "ObjectPattern")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the array start or not.
|
|
* @param {token} token The Comment token.
|
|
* @returns {boolean} True if the comment is at array start.
|
|
*/
|
|
function isCommentAtArrayStart(token) {
|
|
return (
|
|
isCommentAtParentStart(token, "ArrayExpression") ||
|
|
isCommentAtParentStart(token, "ArrayPattern")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not comments are at the array end or not.
|
|
* @param {token} token The Comment token.
|
|
* @returns {boolean} True if the comment is at array end.
|
|
*/
|
|
function isCommentAtArrayEnd(token) {
|
|
return (
|
|
isCommentAtParentEnd(token, "ArrayExpression") ||
|
|
isCommentAtParentEnd(token, "ArrayPattern")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Checks if a comment token has lines around it (ignores inline comments)
|
|
* @param {token} token The Comment token.
|
|
* @param {Object} opts Options to determine the newline.
|
|
* @param {boolean} opts.after Should have a newline after this line.
|
|
* @param {boolean} opts.before Should have a newline before this line.
|
|
* @returns {void}
|
|
*/
|
|
function checkForEmptyLine(token, opts) {
|
|
if (
|
|
applyDefaultIgnorePatterns &&
|
|
defaultIgnoreRegExp.test(token.value)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (ignorePattern && customIgnoreRegExp.test(token.value)) {
|
|
return;
|
|
}
|
|
|
|
let after = opts.after,
|
|
before = opts.before;
|
|
|
|
const prevLineNum = token.loc.start.line - 1,
|
|
nextLineNum = token.loc.end.line + 1,
|
|
commentIsNotAlone = codeAroundComment(token);
|
|
|
|
const blockStartAllowed =
|
|
options.allowBlockStart &&
|
|
isCommentAtBlockStart(token) &&
|
|
!(
|
|
options.allowClassStart === false &&
|
|
isCommentAtClassStart(token)
|
|
),
|
|
blockEndAllowed =
|
|
options.allowBlockEnd &&
|
|
isCommentAtBlockEnd(token) &&
|
|
!(
|
|
options.allowClassEnd === false &&
|
|
isCommentAtClassEnd(token)
|
|
),
|
|
classStartAllowed =
|
|
options.allowClassStart && isCommentAtClassStart(token),
|
|
classEndAllowed =
|
|
options.allowClassEnd && isCommentAtClassEnd(token),
|
|
objectStartAllowed =
|
|
options.allowObjectStart && isCommentAtObjectStart(token),
|
|
objectEndAllowed =
|
|
options.allowObjectEnd && isCommentAtObjectEnd(token),
|
|
arrayStartAllowed =
|
|
options.allowArrayStart && isCommentAtArrayStart(token),
|
|
arrayEndAllowed =
|
|
options.allowArrayEnd && isCommentAtArrayEnd(token);
|
|
|
|
const exceptionStartAllowed =
|
|
blockStartAllowed ||
|
|
classStartAllowed ||
|
|
objectStartAllowed ||
|
|
arrayStartAllowed;
|
|
const exceptionEndAllowed =
|
|
blockEndAllowed ||
|
|
classEndAllowed ||
|
|
objectEndAllowed ||
|
|
arrayEndAllowed;
|
|
|
|
// ignore top of the file and bottom of the file
|
|
if (prevLineNum < 1) {
|
|
before = false;
|
|
}
|
|
if (nextLineNum >= numLines) {
|
|
after = false;
|
|
}
|
|
|
|
// we ignore all inline comments
|
|
if (commentIsNotAlone) {
|
|
return;
|
|
}
|
|
|
|
const previousTokenOrComment = sourceCode.getTokenBefore(token, {
|
|
includeComments: true,
|
|
});
|
|
const nextTokenOrComment = sourceCode.getTokenAfter(token, {
|
|
includeComments: true,
|
|
});
|
|
|
|
// check for newline before
|
|
if (
|
|
!exceptionStartAllowed &&
|
|
before &&
|
|
!commentAndEmptyLines.has(prevLineNum) &&
|
|
!(
|
|
astUtils.isCommentToken(previousTokenOrComment) &&
|
|
astUtils.isTokenOnSameLine(previousTokenOrComment, token)
|
|
)
|
|
) {
|
|
const lineStart = token.range[0] - token.loc.start.column;
|
|
const range = [lineStart, lineStart];
|
|
|
|
context.report({
|
|
node: token,
|
|
messageId: "before",
|
|
fix(fixer) {
|
|
return fixer.insertTextBeforeRange(range, "\n");
|
|
},
|
|
});
|
|
}
|
|
|
|
// check for newline after
|
|
if (
|
|
!exceptionEndAllowed &&
|
|
after &&
|
|
!commentAndEmptyLines.has(nextLineNum) &&
|
|
!(
|
|
astUtils.isCommentToken(nextTokenOrComment) &&
|
|
astUtils.isTokenOnSameLine(token, nextTokenOrComment)
|
|
)
|
|
) {
|
|
context.report({
|
|
node: token,
|
|
messageId: "after",
|
|
fix(fixer) {
|
|
return fixer.insertTextAfter(token, "\n");
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Public
|
|
//--------------------------------------------------------------------------
|
|
|
|
return {
|
|
Program() {
|
|
comments.forEach(token => {
|
|
if (token.type === "Line") {
|
|
if (
|
|
options.beforeLineComment ||
|
|
options.afterLineComment
|
|
) {
|
|
checkForEmptyLine(token, {
|
|
after: options.afterLineComment,
|
|
before: options.beforeLineComment,
|
|
});
|
|
}
|
|
} else if (token.type === "Block") {
|
|
if (
|
|
options.beforeBlockComment ||
|
|
options.afterBlockComment
|
|
) {
|
|
checkForEmptyLine(token, {
|
|
after: options.afterBlockComment,
|
|
before: options.beforeBlockComment,
|
|
});
|
|
}
|
|
} else if (token.type === "Shebang") {
|
|
if (options.afterHashbangComment) {
|
|
checkForEmptyLine(token, {
|
|
after: options.afterHashbangComment,
|
|
before: false,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
},
|
|
};
|
|
},
|
|
};
|