@blitz/eslint-plugin
Version:
An ESLint config to enforce a consistent code styles across StackBlitz projects
362 lines • 14.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultOptions = exports.ruleName = void 0;
const utils_1 = require("@typescript-eslint/utils");
const ast_utils_1 = require("@typescript-eslint/utils/ast-utils");
const util_1 = require("../util");
const getESLintCoreRule_1 = require("../util/getESLintCoreRule");
exports.ruleName = 'lines-around-comment';
const baseRule = (0, getESLintCoreRule_1.getESLintCoreRule)(exports.ruleName);
const END_REGION_PRAGMA = ' #endregion';
const COMMENTS_IGNORE_PATTERN = /^\s*(?:eslint|jshint\s+|jslint\s+|istanbul\s+|globals?\s+|exported\s+|jscs)/u;
const START_END_NODES = [
utils_1.AST_NODE_TYPES.TSEnumDeclaration,
utils_1.AST_NODE_TYPES.TSEnumBody,
utils_1.AST_NODE_TYPES.SwitchStatement,
utils_1.AST_NODE_TYPES.ArrayExpression,
utils_1.AST_NODE_TYPES.ArrayPattern,
utils_1.AST_NODE_TYPES.ObjectExpression,
utils_1.AST_NODE_TYPES.TSTypeLiteral,
];
function getEmptyLineNums(lines) {
const emptyLines = lines
.map((line, i) => {
return {
code: line.trim(),
num: i + 1,
};
})
.filter((line) => !line.code)
.map((line) => line.num);
return emptyLines;
}
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;
}
exports.defaultOptions = {
beforeBlockComment: true,
afterBlockComment: false,
beforeLineComment: true,
afterLineComment: false,
allowBlockStart: false,
allowBlockEnd: false,
allowClassStart: false,
allowClassEnd: false,
allowObjectStart: false,
allowObjectEnd: false,
allowArrayStart: false,
allowArrayEnd: false,
allowInterfaceStart: false,
allowInterfaceEnd: false,
allowTypeStart: false,
allowTypeEnd: false,
allowEnumStart: false,
allowEnumEnd: false,
allowModuleStart: false,
allowModuleEnd: false,
allowSwitchStart: false,
allowSwitchEnd: false,
allowMemberCallExpression: false,
ignorePattern: '',
applyDefaultIgnorePatterns: false,
};
exports.default = (0, util_1.createRule)({
name: exports.ruleName,
meta: {
type: 'layout',
docs: {
description: 'Require empty lines around comments',
},
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',
},
allowInterfaceStart: {
type: 'boolean',
},
allowInterfaceEnd: {
type: 'boolean',
},
allowTypeStart: {
type: 'boolean',
},
allowTypeEnd: {
type: 'boolean',
},
allowEnumStart: {
type: 'boolean',
},
allowEnumEnd: {
type: 'boolean',
},
allowModuleStart: {
type: 'boolean',
},
allowModuleEnd: {
type: 'boolean',
},
allowSwitchStart: {
type: 'boolean',
},
allowSwitchEnd: {
type: 'boolean',
},
allowMemberCallExpression: {
type: 'boolean',
},
ignorePattern: {
type: 'string',
},
applyDefaultIgnorePatterns: {
type: 'boolean',
},
afterHashbangComment: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
fixable: baseRule.meta.fixable,
messages: baseRule.meta.messages,
hasSuggestions: baseRule.meta.hasSuggestions,
},
defaultOptions: [exports.defaultOptions],
create(context, [options]) {
const { sourceCode } = context;
const comments = sourceCode.getAllComments();
const lines = sourceCode.lines;
const commentLines = getCommentLineNums(comments);
const emptyLines = getEmptyLineNums(lines);
const commentAndEmptyLines = new Set(commentLines.concat(emptyLines));
const defaultIgnoreRegExp = COMMENTS_IGNORE_PATTERN;
const customIgnoreRegExp = new RegExp(options.ignorePattern ?? '', 'u');
function getParentNodeOfToken(token) {
return sourceCode.getNodeByRangeIndex(token.range[0]);
}
function isParentNodeType(parent, nodeType) {
return parent && parent.type === nodeType;
}
function isCommentAtParentStart(token, nodeType) {
const parent = getParentNodeOfToken(token);
return parent && isParentNodeType(parent, nodeType) && token.loc.start.line - parent.loc.start.line === 1;
}
function isCommentAtParentEnd(token, nodeType) {
const parent = getParentNodeOfToken(token);
return parent && isParentNodeType(parent, nodeType) && parent.loc.end.line - token.loc.end.line === 1;
}
function isMemberExpression(node, objectType) {
if (node == null) {
return false;
}
return node.type === utils_1.AST_NODE_TYPES.MemberExpression && node.object.type === objectType;
}
function codeAroundComment(token) {
let currentToken = token;
do {
currentToken = sourceCode.getTokenBefore(currentToken, {
includeComments: true,
});
} while (currentToken && (0, ast_utils_1.isCommentToken)(currentToken));
if (currentToken && (0, ast_utils_1.isTokenOnSameLine)(currentToken, token)) {
return true;
}
currentToken = token;
do {
currentToken = sourceCode.getTokenAfter(currentToken, {
includeComments: true,
});
} while (currentToken && (0, ast_utils_1.isCommentToken)(currentToken));
if (currentToken && (0, ast_utils_1.isTokenOnSameLine)(token, currentToken)) {
return true;
}
return false;
}
function isEndRegionPragma(token) {
return token.type === utils_1.AST_TOKEN_TYPES.Line && token.value === END_REGION_PRAGMA;
}
function shouldHandleComment(token) {
const isKnownParentNode = [utils_1.AST_NODE_TYPES.MemberExpression, utils_1.AST_NODE_TYPES.IfStatement].some((nodeType) => getParentNodeOfToken(token)?.type === nodeType);
const isNearStartOrEndOfKnownNode = START_END_NODES.some((nodeType) => {
return isCommentAtParentStart(token, nodeType) || isCommentAtParentEnd(token, nodeType);
});
return isEndRegionPragma(token) || isKnownParentNode || isNearStartOrEndOfKnownNode;
}
function checkForEmptyLine(token, { before, after }) {
if (!shouldHandleComment(token)) {
return;
}
if (options.applyDefaultIgnorePatterns !== false && defaultIgnoreRegExp.test(token.value)) {
return;
}
if (options.ignorePattern && customIgnoreRegExp.test(token.value)) {
return;
}
if (isEndRegionPragma(token) && before) {
return;
}
if (codeAroundComment(token)) {
return;
}
const parentNode = getParentNodeOfToken(token);
// we always allow comments in an if-statement
if (parentNode?.type === utils_1.AST_NODE_TYPES.IfStatement) {
return;
}
const prevLineNum = token.loc.start.line - 1;
const nextLineNum = token.loc.end.line + 1;
const previousTokenOrComment = sourceCode.getTokenBefore(token, {
includeComments: true,
});
const nextTokenOrComment = sourceCode.getTokenAfter(token, {
includeComments: true,
});
const _report = (messageId) => {
const lineStart = token.range[0] - token.loc.start.column;
const range = [lineStart, lineStart];
context.report({
node: token,
messageId,
fix(fixer) {
return fixer.insertTextBeforeRange(range, '\n');
},
});
};
const isAtStart = START_END_NODES.some((nodeType) => isCommentAtParentStart(token, nodeType));
const isAtEnd = START_END_NODES.some((nodeType) => isCommentAtParentEnd(token, nodeType));
const isStartAllowed = options.allowEnumStart ||
options.allowSwitchStart ||
options.allowArrayStart ||
options.allowObjectStart ||
options.allowTypeStart;
const isEndAllowed = options.allowEnumEnd ||
options.allowSwitchEnd ||
options.allowArrayEnd ||
options.allowObjectEnd ||
options.allowTypeEnd;
const isPrevLineEmpty = commentAndEmptyLines.has(prevLineNum);
const isNextLineEmpty = commentAndEmptyLines.has(nextLineNum);
const isPrevTokenOnSameLine = (0, ast_utils_1.isCommentToken)(previousTokenOrComment) && (0, ast_utils_1.isTokenOnSameLine)(previousTokenOrComment, token);
const isNextTokenOnSameLine = (0, ast_utils_1.isCommentToken)(nextTokenOrComment) && (0, ast_utils_1.isTokenOnSameLine)(token, nextTokenOrComment);
if (isAtStart) {
if (!isStartAllowed && !isPrevLineEmpty && !isPrevTokenOnSameLine) {
_report('before');
}
}
else if (isMemberExpression(parentNode, utils_1.AST_NODE_TYPES.CallExpression) ||
isMemberExpression(parentNode, utils_1.AST_NODE_TYPES.Identifier)) {
if (!options.allowMemberCallExpression) {
_report('before');
}
}
else if (before && !isPrevLineEmpty) {
_report('before');
}
if (isAtEnd) {
if (!isEndAllowed && !isNextLineEmpty && !isNextTokenOnSameLine) {
_report('after');
}
}
else if (after && !isNextLineEmpty) {
_report('after');
}
}
const customReport = (descriptor) => {
if ('node' in descriptor) {
if (descriptor.node.type === utils_1.AST_TOKEN_TYPES.Line || descriptor.node.type === utils_1.AST_TOKEN_TYPES.Block) {
if (shouldHandleComment(descriptor.node)) {
return undefined;
}
}
}
return context.report(descriptor);
};
const customContext = { report: customReport };
/**
* We can't directly proxy `context` because its `report` property is non-configurable
* and non-writable. So we proxy `customContext` and redirect all
* property access to the original context except for `report`.
*/
const proxiedContext = new Proxy(customContext, {
get(target, path, receiver) {
if (path !== 'report') {
return Reflect.get(context, path, receiver);
}
return Reflect.get(target, path, receiver);
},
});
const rule = baseRule.create(proxiedContext);
return {
Program: (node) => {
rule.Program?.(node);
comments.forEach((token) => {
if (token.type === utils_1.AST_TOKEN_TYPES.Line) {
if (options.beforeLineComment || options.afterLineComment) {
checkForEmptyLine(token, {
after: options.afterLineComment,
before: options.beforeLineComment,
});
}
}
else if (token.type === utils_1.AST_TOKEN_TYPES.Block) {
if (options.beforeBlockComment || options.afterBlockComment) {
checkForEmptyLine(token, {
after: options.afterBlockComment,
before: options.beforeBlockComment,
});
}
}
});
},
};
},
});
//# sourceMappingURL=lines-around-comment.js.map