UNPKG

@so1ve/eslint-plugin

Version:
563 lines (552 loc) 17.9 kB
// src/rules/function-style.ts import { AST_NODE_TYPES } from "@typescript-eslint/types"; // src/utils/index.ts import { ESLintUtils } from "@typescript-eslint/utils"; var createEslintRule = ESLintUtils.RuleCreator((ruleName) => ruleName); function getPreviousNode(node) { if (!node) { return; } const { parent } = node; if (parent && "body" in parent) { const { body } = parent; if (!Array.isArray(body)) { return; } const index = body.indexOf(node); if (index > 0) { return body[index - 1]; } } } // src/rules/function-style.ts var RULE_NAME = "function-style"; var function_style_default = createEslintRule({ name: RULE_NAME, meta: { type: "problem", docs: { description: "Enforce function style.", recommended: "stylistic" }, fixable: "code", schema: [], messages: { arrow: "Expected an arrow function shorthand.", declaration: "Expected a function declaration." } }, defaultOptions: [], create: (context) => { const sourceCode = context.sourceCode; function getLoneReturnStatement(node) { const { body } = node; if (body.type !== AST_NODE_TYPES.BlockStatement) { return; } const { body: blockBody } = body; const allComments = sourceCode.getCommentsInside(node); if (blockBody.length !== 1) { return; } const [statement] = blockBody; const statementComments = sourceCode.getCommentsInside(statement); if (allComments.length !== statementComments.length) { return; } if (statement?.type === AST_NODE_TYPES.ReturnStatement) { return statement; } } function generateFunction(type, name, node, rawStatement, asVariable = true) { const async = node.async ? "async " : ""; const generics = node.typeParameters ? sourceCode.getText(node.typeParameters) : ""; const params = node.params.map((param) => sourceCode.getText(param)).join(", "); const returnType = node.returnType ? sourceCode.getText(node.returnType) : ""; const body = sourceCode.getText(node.body); const variableDeclaration = asVariable && name ? `const ${name} = ` : ""; return type === "arrow" ? `${variableDeclaration}${async}${generics}(${params})${returnType} => ${rawStatement};` : `${async}function ${name}${generics}(${params})${returnType} ${body}`; } const scopeStack = []; let haveThisAccess = false; function setupScope(node) { scopeStack.push(sourceCode.getScope(node)); } function clearThisAccess() { scopeStack.pop(); haveThisAccess = false; } return { "FunctionExpression": setupScope, "FunctionExpression:exit"(node) { if (node.parent?.id?.typeAnnotation || node.parent?.type !== AST_NODE_TYPES.VariableDeclarator || haveThisAccess) { clearThisAccess(); return; } const name = node.parent.id.name; context.report({ node, messageId: "declaration", fix: (fixer) => fixer.replaceText( node.parent.parent, generateFunction("declaration", name, node) ) }); clearThisAccess(); }, "FunctionDeclaration:not(TSDeclareFunction + FunctionDeclaration)": setupScope, "FunctionDeclaration:not(TSDeclareFunction + FunctionDeclaration):exit"(node) { if (haveThisAccess) { return; } const previousNode = getPreviousNode(node.parent); if (previousNode?.type === AST_NODE_TYPES.ExportNamedDeclaration && previousNode.declaration?.type === AST_NODE_TYPES.TSDeclareFunction) { return; } const statement = getLoneReturnStatement(node); const isExportDefault = node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration; if (!statement?.argument || !node.id?.name && !isExportDefault || node.generator) { clearThisAccess(); return; } const returnVal = `(${sourceCode.getText(statement.argument)})`; context.report({ node, messageId: "arrow", fix: (fixer) => fixer.replaceText( node, generateFunction( "arrow", node.id?.name ?? null, node, returnVal, !isExportDefault ) ) }); clearThisAccess(); }, "ArrowFunctionExpression": setupScope, "ArrowFunctionExpression:exit"(node) { if (haveThisAccess) { return; } const { body, parent } = node; const statement = getLoneReturnStatement(node); if (statement?.argument) { const returnVal = `(${sourceCode.getText(statement.argument)})`; context.report({ node, messageId: "arrow", fix: (fixer) => fixer.replaceText(node.body, returnVal) }); } else if (body.type === AST_NODE_TYPES.BlockStatement && !parent?.id?.typeAnnotation) { const { body: blockBody } = body; if (blockBody.length > 0 && node.parent?.parent?.type === AST_NODE_TYPES.VariableDeclaration) { const { parent: grandParent } = node.parent; context.report({ node: grandParent, messageId: "declaration", fix: (fixer) => fixer.replaceText( grandParent, generateFunction( "declaration", node.parent.id.name, node ) ) }); } } clearThisAccess(); }, ThisExpression(node) { haveThisAccess = scopeStack.includes(sourceCode.getScope(node)); } }; } }); // src/rules/import-dedupe.ts var RULE_NAME2 = "import-dedupe"; var import_dedupe_default = createEslintRule({ name: RULE_NAME2, meta: { type: "problem", docs: { description: "Fix duplication in imports.", recommended: "recommended" }, fixable: "code", schema: [], messages: { importDedupe: "Expect no duplication in imports." } }, defaultOptions: [], create: (context) => ({ ImportDeclaration(node) { if (node.specifiers.length <= 1) { return; } const names = /* @__PURE__ */ new Set(); for (const n of node.specifiers) { const id = n.local.name; if (names.has(id)) { context.report({ node, loc: { start: n.loc.end, end: n.loc.start }, messageId: "importDedupe", fix(fixer) { const start = n.range[0]; let end = n.range[1]; const nextToken = context.sourceCode.getTokenAfter(n); if (nextToken && nextToken.value === ",") { end = nextToken.range[1]; } return fixer.removeRange([start, end]); } }); } names.add(id); } } }) }); // src/rules/no-import-promises-as.ts var RULE_NAME3 = "no-import-promises-as"; var _POSSIBLE_IMPORT_SOURCES = ["dns", "fs", "readline", "stream"]; var POSSIBLE_IMPORT_SOURCES = [ ..._POSSIBLE_IMPORT_SOURCES, ..._POSSIBLE_IMPORT_SOURCES.map((s) => `node:${s}`) ]; var no_import_promises_as_default = createEslintRule({ name: RULE_NAME3, meta: { type: "problem", docs: { description: "Disallow import promises as.", recommended: "stylistic" }, fixable: "code", schema: [], messages: { noImportPromisesAs: "Expect no import promises as." } }, defaultOptions: [], create: (context) => { const sourceCode = context.sourceCode; const { text } = sourceCode; return { ImportDeclaration(node) { if (!POSSIBLE_IMPORT_SOURCES.includes(node.source.value)) { return; } const promisesSpecifier = node.specifiers.find( (s) => s.type === "ImportSpecifier" && s.imported.name === "promises" && s.local.name !== "promises" ); const as = promisesSpecifier?.local.name; if (!promisesSpecifier || !as) { return; } context.report({ node, messageId: "noImportPromisesAs", *fix(fixer) { const s = promisesSpecifier.range[0]; let e = promisesSpecifier.range[1]; if (text[e] === ",") { e += 1; } yield fixer.removeRange([s, e]); yield fixer.insertTextAfter( node, ` import ${as} from "${node.source.value}/promises";` ); } }); } }; } }); // src/rules/no-inline-type-import.ts import { AST_NODE_TYPES as AST_NODE_TYPES2 } from "@typescript-eslint/types"; var RULE_NAME4 = "no-inline-type-import"; var no_inline_type_import_default = createEslintRule({ name: RULE_NAME4, meta: { type: "layout", docs: { description: "Disallow inline type import.", recommended: "stylistic" }, fixable: "code", schema: [], messages: { noInlineTypeImport: "Expected no inline type import." } }, defaultOptions: [], create: (context) => { const sourceCode = context.sourceCode; return { ImportDeclaration: (node) => { const { specifiers } = node; const typeSpecifiers = specifiers.filter( (s) => s.type === AST_NODE_TYPES2.ImportSpecifier && s.importKind === "type" ); const valueSpecifiers = specifiers.filter( (s) => s.type === AST_NODE_TYPES2.ImportSpecifier && s.importKind === "value" ); const defaultImportSpecifier = specifiers.find( (s) => s.type === AST_NODE_TYPES2.ImportDefaultSpecifier ); if (typeSpecifiers.length > 0 && valueSpecifiers.length > 0) { context.report({ node, messageId: "noInlineTypeImport", fix(fixer) { const typeSpecifiersText = typeSpecifiers.map((s) => sourceCode.getText(s).replace("type ", "")).join(", "); const valueSpecifiersText = valueSpecifiers.map((s) => sourceCode.getText(s)).join(", "); const defaultImportSpecifierText = sourceCode.getText( defaultImportSpecifier ); const defaultAndValueSpecifiersText = defaultImportSpecifier ? `import ${defaultImportSpecifierText}, { ${valueSpecifiersText} } from "${node.source.value}";` : `import { ${valueSpecifiersText} } from "${node.source.value}";`; const texts = [ `import type { ${typeSpecifiersText} } from "${node.source.value}";`, defaultAndValueSpecifiersText ]; return fixer.replaceText(node, texts.join("\n")); } }); } else if (typeSpecifiers.length > 0) { context.report({ node, messageId: "noInlineTypeImport", fix(fixer) { const typeSpecifiersText = typeSpecifiers.map((s) => sourceCode.getText(s).replace("type ", "")).join(", "); return fixer.replaceText( node, `import type { ${typeSpecifiersText} } from "${node.source.value}";` ); } }); } } }; } }); // src/rules/no-negated-comparison.ts import { AST_NODE_TYPES as AST_NODE_TYPES3 } from "@typescript-eslint/types"; var RULE_NAME5 = "no-negated-comparison"; var negatedToPositive = { "==": "!=", "===": "!==", "!=": "==", "!==": "===", "<": ">=", "<=": ">", ">": "<=", ">=": "<" }; var negatives = Object.keys(negatedToPositive); var no_negated_comparison_default = createEslintRule({ name: RULE_NAME5, meta: { type: "problem", docs: { description: "Disallow negated comparison.", recommended: "stylistic" }, fixable: "code", schema: [], messages: { noNegatedComparison: "Expect no negated comparison." } }, defaultOptions: [], create: (context) => ({ BinaryExpression(node) { const { parent, left, right, operator } = node; if (!parent) { return; } if (negatives.includes(operator) && parent.type === AST_NODE_TYPES3.UnaryExpression && // Is this necessary? parent.operator === "!") { context.report({ node, messageId: "noNegatedComparison", *fix(fixer) { const operatorRange = [left.range[1], right.range[0]]; const fixedOperator = negatedToPositive[operator]; yield fixer.replaceTextRange(operatorRange, fixedOperator); yield fixer.removeRange([parent.range[0], parent.range[0] + 1]); } }); } } }) }); // src/rules/no-useless-template-string.ts var RULE_NAME6 = "no-useless-template-string"; var no_useless_template_string_default = createEslintRule({ name: RULE_NAME6, meta: { type: "problem", docs: { description: "No useless template string.", recommended: "stylistic" }, fixable: "code", schema: [], messages: { noUselessTemplateString: "No useless template string." } }, defaultOptions: [], create: (context) => ({ "TemplateLiteral:not(TaggedTemplateExpression > TemplateLiteral)"(node) { const { quasis } = node; const isSafe = !quasis.some( ({ value: { raw } }) => raw.includes('"') || raw.includes("'") || raw.includes("\n") ); if (node.expressions.length === 0 && isSafe) { context.report({ node, messageId: "noUselessTemplateString", fix(fixer) { return fixer.replaceTextRange( node.range, `"${node.quasis[0].value.raw}"` ); } }); } } }) }); // src/rules/pad-after-last-import.ts var RULE_NAME7 = "pad-after-last-import"; var pad_after_last_import_default = createEslintRule({ name: RULE_NAME7, meta: { type: "problem", docs: { description: "Pad after the last import.", recommended: "stylistic" }, fixable: "code", schema: [], messages: { padAfterLastImport: "Expected a blank line after the last import." } }, defaultOptions: [], create: (context) => { const sourceCode = context.sourceCode; let lastImportNode = null; return { ImportDeclaration(node) { lastImportNode = node; }, "Program:exit"() { if (lastImportNode) { const nextToken = sourceCode.getTokenAfter(lastImportNode); const firstCommentAfterTokenStartLine = sourceCode.getCommentsAfter(lastImportNode)[0]?.loc.start.line; const expectedLine = lastImportNode.loc.end.line + 1; const nextTokenStartLine = nextToken?.loc.start.line; if (nextToken && // Workaround: Vue nextToken.value !== "</script>" && (expectedLine === nextTokenStartLine || expectedLine === firstCommentAfterTokenStartLine)) { context.report({ node: lastImportNode, messageId: "padAfterLastImport", fix: (fixer) => fixer.insertTextAfter(lastImportNode, "\n") }); } } } }; } }); // src/rules/require-async-with-await.ts import { TSESTree } from "@typescript-eslint/types"; var RULE_NAME8 = "require-async-with-await"; var require_async_with_await_default = createEslintRule({ name: RULE_NAME8, meta: { type: "problem", docs: { description: "Require using async keyword with await.", recommended: "recommended" }, fixable: "code", schema: [], messages: { requireAsyncWithAwait: "Expect using async keyword with await." } }, defaultOptions: [], create: (context) => { const functionNodeStack = []; function setupNode(node) { functionNodeStack.push(node); } function clearNode() { functionNodeStack.pop(); } return { "FunctionExpression": setupNode, "FunctionExpression:exit": clearNode, "FunctionDeclaration": setupNode, "FunctionDeclaration:exit": clearNode, "ArrowFunctionExpression": setupNode, "ArrowFunctionExpression:exit": clearNode, AwaitExpression() { const node = functionNodeStack[functionNodeStack.length - 1]; if (!node || node.async) { return; } let fixRange; if (node.type === TSESTree.AST_NODE_TYPES.ArrowFunctionExpression) { fixRange = node.range; } if (node.type === TSESTree.AST_NODE_TYPES.FunctionDeclaration || node.type === TSESTree.AST_NODE_TYPES.FunctionExpression) { if (node.parent.type === TSESTree.AST_NODE_TYPES.Property || node.parent.type === TSESTree.AST_NODE_TYPES.MethodDefinition) { if (node.parent.kind === "method" || node.parent.kind === "init") { fixRange = node.parent.key.range; } } else { fixRange = node.range; } } if (fixRange) { context.report({ node, messageId: "requireAsyncWithAwait", fix: (fixer) => fixer.insertTextBeforeRange(fixRange, "async ") }); } } }; } }); // src/index.ts var src_default = { rules: { "function-style": function_style_default, "import-dedupe": import_dedupe_default, "no-inline-type-import": no_inline_type_import_default, "no-negated-comparison": no_negated_comparison_default, "no-useless-template-string": no_useless_template_string_default, "no-import-promises-as": no_import_promises_as_default, "pad-after-last-import": pad_after_last_import_default, "require-async-with-await": require_async_with_await_default } }; export { src_default as default };