UNPKG

@blitz/eslint-plugin

Version:

An ESLint config to enforce a consistent code styles across StackBlitz projects

362 lines 14.7 kB
"use strict"; 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