@so1ve/eslint-plugin
Version:
563 lines (552 loc) • 17.9 kB
JavaScript
// 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
};