routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021-PRESENT Anthony Fu <https://github.com/antfu>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# eslint-plugin-antfu
|
||||
|
||||
[![npm version][npm-version-src]][npm-version-href]
|
||||
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
||||
|
||||
Anthony extended ESLint rules. For [antfu/eslint-config](https://github.com/antfu/eslint-config).
|
||||
|
||||
[Rules List](./src/rules)
|
||||
|
||||
## Sponsors
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg">
|
||||
<img src='https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg'/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE) License © 2023-PRESENT [Anthony Fu](https://github.com/antfu)
|
||||
|
||||
<!-- Badges -->
|
||||
|
||||
[npm-version-src]: https://img.shields.io/npm/v/eslint-plugin-antfu?style=flat&colorA=080f12&colorB=1fa669
|
||||
[npm-version-href]: https://npmjs.com/package/eslint-plugin-antfu
|
||||
[npm-downloads-src]: https://img.shields.io/npm/dm/eslint-plugin-antfu?style=flat&colorA=080f12&colorB=1fa669
|
||||
[npm-downloads-href]: https://npmjs.com/package/eslint-plugin-antfu
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { Rule, Linter } from 'eslint';
|
||||
|
||||
type RuleModule<T extends readonly unknown[]> = Rule.RuleModule & {
|
||||
defaultOptions: T;
|
||||
};
|
||||
|
||||
type Options$2 = [
|
||||
{
|
||||
indent?: number;
|
||||
tags?: string[];
|
||||
}
|
||||
];
|
||||
|
||||
type Options$1 = [
|
||||
{
|
||||
ArrayExpression?: boolean;
|
||||
ArrayPattern?: boolean;
|
||||
ArrowFunctionExpression?: boolean;
|
||||
CallExpression?: boolean;
|
||||
ExportNamedDeclaration?: boolean;
|
||||
FunctionDeclaration?: boolean;
|
||||
FunctionExpression?: boolean;
|
||||
IfStatement?: boolean;
|
||||
ImportDeclaration?: boolean;
|
||||
JSONArrayExpression?: boolean;
|
||||
JSONObjectExpression?: boolean;
|
||||
JSXOpeningElement?: boolean;
|
||||
NewExpression?: boolean;
|
||||
ObjectExpression?: boolean;
|
||||
ObjectPattern?: boolean;
|
||||
TSFunctionType?: boolean;
|
||||
TSInterfaceDeclaration?: boolean;
|
||||
TSTupleType?: boolean;
|
||||
TSTypeLiteral?: boolean;
|
||||
TSTypeParameterDeclaration?: boolean;
|
||||
TSTypeParameterInstantiation?: boolean;
|
||||
}
|
||||
];
|
||||
|
||||
type Options = [
|
||||
{
|
||||
allowLeadingPropertyAccess?: boolean;
|
||||
}
|
||||
];
|
||||
|
||||
declare const plugin: {
|
||||
meta: {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
rules: {
|
||||
'consistent-chaining': RuleModule<Options>;
|
||||
'consistent-list-newline': RuleModule<Options$1>;
|
||||
curly: RuleModule<[]>;
|
||||
'if-newline': RuleModule<[]>;
|
||||
'import-dedupe': RuleModule<[]>;
|
||||
'indent-unindent': RuleModule<Options$2>;
|
||||
'no-import-dist': RuleModule<[]>;
|
||||
'no-import-node-modules-by-path': RuleModule<[]>;
|
||||
'no-top-level-await': RuleModule<[]>;
|
||||
'no-ts-export-equal': RuleModule<[]>;
|
||||
'top-level-function': RuleModule<[]>;
|
||||
};
|
||||
};
|
||||
|
||||
type RuleDefinitions = typeof plugin['rules'];
|
||||
type RuleOptions = {
|
||||
[K in keyof RuleDefinitions]: RuleDefinitions[K]['defaultOptions'];
|
||||
};
|
||||
type Rules = {
|
||||
[K in keyof RuleOptions]: Linter.RuleEntry<RuleOptions[K]>;
|
||||
};
|
||||
|
||||
export { plugin as default };
|
||||
export type { RuleOptions, Rules };
|
||||
+912
@@ -0,0 +1,912 @@
|
||||
const version = "3.2.2";
|
||||
|
||||
const hasDocs = [
|
||||
"consistent-chaining",
|
||||
"consistent-list-newline",
|
||||
"curly",
|
||||
"if-newline",
|
||||
"import-dedupe",
|
||||
"indent-unindent",
|
||||
"top-level-function"
|
||||
];
|
||||
const blobUrl = "https://github.com/antfu/eslint-plugin-antfu/blob/main/src/rules/";
|
||||
function RuleCreator(urlCreator) {
|
||||
return function createNamedRule({
|
||||
name,
|
||||
meta,
|
||||
...rule
|
||||
}) {
|
||||
return createRule({
|
||||
meta: {
|
||||
...meta,
|
||||
docs: {
|
||||
...meta.docs,
|
||||
url: urlCreator(name)
|
||||
}
|
||||
},
|
||||
...rule
|
||||
});
|
||||
};
|
||||
}
|
||||
function createRule({
|
||||
create,
|
||||
defaultOptions,
|
||||
meta
|
||||
}) {
|
||||
return {
|
||||
create: ((context) => {
|
||||
const optionsWithDefault = context.options.map((options, index) => {
|
||||
return {
|
||||
...defaultOptions?.[index] || {},
|
||||
...options || {}
|
||||
};
|
||||
});
|
||||
return create(context, optionsWithDefault);
|
||||
}),
|
||||
defaultOptions,
|
||||
meta
|
||||
};
|
||||
}
|
||||
const createEslintRule = RuleCreator(
|
||||
(ruleName) => hasDocs.includes(ruleName) ? `${blobUrl}${ruleName}.md` : `${blobUrl}${ruleName}.test.ts`
|
||||
);
|
||||
|
||||
const RULE_NAME$9 = "consistent-chaining";
|
||||
const consistentChaining = createEslintRule({
|
||||
name: RULE_NAME$9,
|
||||
meta: {
|
||||
type: "layout",
|
||||
docs: {
|
||||
description: "Having line breaks styles to object, array and named imports"
|
||||
},
|
||||
fixable: "whitespace",
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowLeadingPropertyAccess: {
|
||||
type: "boolean",
|
||||
description: "Allow leading property access to be on the same line",
|
||||
default: true
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
shouldWrap: "Should have line breaks between items, in node {{name}}",
|
||||
shouldNotWrap: "Should not have line breaks between items, in node {{name}}"
|
||||
}
|
||||
},
|
||||
defaultOptions: [
|
||||
{
|
||||
allowLeadingPropertyAccess: true
|
||||
}
|
||||
],
|
||||
create: (context) => {
|
||||
const knownRoot = /* @__PURE__ */ new WeakSet();
|
||||
const {
|
||||
allowLeadingPropertyAccess = true
|
||||
} = context.options[0] || {};
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
let root = node;
|
||||
while (root.parent && (root.parent.type === "MemberExpression" || root.parent.type === "CallExpression"))
|
||||
root = root.parent;
|
||||
if (knownRoot.has(root))
|
||||
return;
|
||||
knownRoot.add(root);
|
||||
const members = [];
|
||||
let current = root;
|
||||
while (current) {
|
||||
switch (current.type) {
|
||||
case "MemberExpression": {
|
||||
if (!current.computed)
|
||||
members.unshift(current);
|
||||
current = current.object;
|
||||
break;
|
||||
}
|
||||
case "CallExpression": {
|
||||
current = current.callee;
|
||||
break;
|
||||
}
|
||||
case "TSNonNullExpression": {
|
||||
current = current.expression;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
current = void 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let leadingPropertyAcccess = allowLeadingPropertyAccess;
|
||||
let mode = null;
|
||||
members.forEach((m) => {
|
||||
const token = context.sourceCode.getTokenBefore(m.property);
|
||||
const tokenBefore = context.sourceCode.getTokenBefore(token);
|
||||
const currentMode = token.loc.start.line === tokenBefore.loc.end.line ? "single" : "multi";
|
||||
const object = m.object.type === "TSNonNullExpression" ? m.object.expression : m.object;
|
||||
if (leadingPropertyAcccess && (object.type === "ThisExpression" || object.type === "Identifier" || object.type === "MemberExpression" || object.type === "Literal") && currentMode === "single") {
|
||||
return;
|
||||
}
|
||||
leadingPropertyAcccess = false;
|
||||
if (mode == null) {
|
||||
mode = currentMode;
|
||||
return;
|
||||
}
|
||||
if (mode !== currentMode) {
|
||||
context.report({
|
||||
messageId: mode === "single" ? "shouldNotWrap" : "shouldWrap",
|
||||
loc: token.loc,
|
||||
data: {
|
||||
name: root.type
|
||||
},
|
||||
fix(fixer) {
|
||||
if (mode === "multi")
|
||||
return fixer.insertTextAfter(tokenBefore, "\n");
|
||||
else
|
||||
return fixer.removeRange([tokenBefore.range[1], token.range[0]]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const RULE_NAME$8 = "consistent-list-newline";
|
||||
function isCommaToken(token) {
|
||||
return token.type === "Punctuator" && token.value === ",";
|
||||
}
|
||||
const consistentListNewline = createEslintRule({
|
||||
name: RULE_NAME$8,
|
||||
meta: {
|
||||
type: "layout",
|
||||
docs: {
|
||||
description: "Having line breaks styles to object, array and named imports"
|
||||
},
|
||||
fixable: "whitespace",
|
||||
schema: [{
|
||||
type: "object",
|
||||
properties: {
|
||||
ArrayExpression: { type: "boolean" },
|
||||
ArrayPattern: { type: "boolean" },
|
||||
ArrowFunctionExpression: { type: "boolean" },
|
||||
CallExpression: { type: "boolean" },
|
||||
ExportNamedDeclaration: { type: "boolean" },
|
||||
FunctionDeclaration: { type: "boolean" },
|
||||
FunctionExpression: { type: "boolean" },
|
||||
IfStatement: { type: "boolean" },
|
||||
ImportDeclaration: { type: "boolean" },
|
||||
JSONArrayExpression: { type: "boolean" },
|
||||
JSONObjectExpression: { type: "boolean" },
|
||||
JSXOpeningElement: { type: "boolean" },
|
||||
NewExpression: { type: "boolean" },
|
||||
ObjectExpression: { type: "boolean" },
|
||||
ObjectPattern: { type: "boolean" },
|
||||
TSFunctionType: { type: "boolean" },
|
||||
TSInterfaceDeclaration: { type: "boolean" },
|
||||
TSTupleType: { type: "boolean" },
|
||||
TSTypeLiteral: { type: "boolean" },
|
||||
TSTypeParameterDeclaration: { type: "boolean" },
|
||||
TSTypeParameterInstantiation: { type: "boolean" }
|
||||
},
|
||||
additionalProperties: false
|
||||
}],
|
||||
messages: {
|
||||
shouldWrap: "Should have line breaks between items, in node {{name}}",
|
||||
shouldNotWrap: "Should not have line breaks between items, in node {{name}}"
|
||||
}
|
||||
},
|
||||
defaultOptions: [{}],
|
||||
create: (context, [options = {}] = [{}]) => {
|
||||
const multilineNodes = /* @__PURE__ */ new Set([
|
||||
"ArrayExpression",
|
||||
"FunctionDeclaration",
|
||||
"IfStatement",
|
||||
"ObjectExpression",
|
||||
"ObjectPattern",
|
||||
"TSTypeLiteral",
|
||||
"TSTupleType",
|
||||
"TSInterfaceDeclaration"
|
||||
]);
|
||||
function removeLines(fixer, start, end, delimiter) {
|
||||
const range = [start, end];
|
||||
const code = context.sourceCode.text.slice(...range);
|
||||
return fixer.replaceTextRange(range, code.replace(/(\r\n|\n)/g, delimiter ?? ""));
|
||||
}
|
||||
function getDelimiter(root, current) {
|
||||
if (root.type !== "TSInterfaceDeclaration" && root.type !== "TSTypeLiteral")
|
||||
return;
|
||||
const currentContent = context.sourceCode.text.slice(current.range[0], current.range[1]);
|
||||
return currentContent.match(/(?:,|;)$/) ? void 0 : ",";
|
||||
}
|
||||
function hasComments(current) {
|
||||
let program = current;
|
||||
while (program.type !== "Program")
|
||||
program = program.parent;
|
||||
const currentRange = current.range;
|
||||
return !!program.comments?.some((comment) => {
|
||||
const commentRange = comment.range;
|
||||
return commentRange[0] > currentRange[0] && commentRange[1] < currentRange[1];
|
||||
});
|
||||
}
|
||||
function check(node, children, nextNode) {
|
||||
const items = children.filter(Boolean);
|
||||
if (items.length === 0)
|
||||
return;
|
||||
let startToken = ["CallExpression", "NewExpression"].includes(node.type) ? void 0 : context.sourceCode.getFirstToken(node);
|
||||
if (node.type === "CallExpression") {
|
||||
startToken = context.sourceCode.getTokenAfter(
|
||||
node.typeArguments ? node.typeArguments : node.callee.type === "MemberExpression" ? node.callee.property : node.callee
|
||||
);
|
||||
}
|
||||
if (startToken?.type !== "Punctuator")
|
||||
startToken = context.sourceCode.getTokenBefore(items[0]);
|
||||
const endToken = context.sourceCode.getTokenAfter(items[items.length - 1]);
|
||||
const startLine = startToken.loc.start.line;
|
||||
if (startToken.loc.start.line === endToken.loc.end.line)
|
||||
return;
|
||||
let mode = null;
|
||||
let lastLine = startLine;
|
||||
items.forEach((item, idx) => {
|
||||
if (mode == null) {
|
||||
mode = item.loc.start.line === lastLine ? "inline" : "newline";
|
||||
lastLine = item.loc.end.line;
|
||||
return;
|
||||
}
|
||||
const currentStart = item.loc.start.line;
|
||||
if (mode === "newline" && currentStart === lastLine) {
|
||||
context.report({
|
||||
node: item,
|
||||
messageId: "shouldWrap",
|
||||
data: {
|
||||
name: node.type
|
||||
},
|
||||
*fix(fixer) {
|
||||
yield fixer.insertTextBefore(item, "\n");
|
||||
}
|
||||
});
|
||||
} else if (mode === "inline" && currentStart !== lastLine) {
|
||||
const lastItem2 = items[idx - 1];
|
||||
if (context.sourceCode.getCommentsBefore(item).length > 0)
|
||||
return;
|
||||
const content = context.sourceCode.text.slice(lastItem2.range[1], item.range[0]);
|
||||
if (content.includes("\n")) {
|
||||
context.report({
|
||||
node: item,
|
||||
messageId: "shouldNotWrap",
|
||||
data: {
|
||||
name: node.type
|
||||
},
|
||||
*fix(fixer) {
|
||||
yield removeLines(fixer, lastItem2.range[1], item.range[0], getDelimiter(node, lastItem2));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
lastLine = item.loc.end.line;
|
||||
});
|
||||
const endRange = nextNode ? Math.min(
|
||||
context.sourceCode.getTokenBefore(nextNode).range[0],
|
||||
node.range[1]
|
||||
) : node.range[1];
|
||||
const endLoc = context.sourceCode.getLocFromIndex(endRange);
|
||||
const lastItem = items[items.length - 1];
|
||||
if (mode === "newline" && endLoc.line === lastLine) {
|
||||
context.report({
|
||||
node: lastItem,
|
||||
messageId: "shouldWrap",
|
||||
data: {
|
||||
name: node.type
|
||||
},
|
||||
*fix(fixer) {
|
||||
yield fixer.insertTextAfter(lastItem, "\n");
|
||||
}
|
||||
});
|
||||
} else if (mode === "inline" && endLoc.line !== lastLine) {
|
||||
if (items.length === 1 && !multilineNodes.has(node.type))
|
||||
return;
|
||||
const nextToken = context.sourceCode.getTokenAfter(lastItem);
|
||||
if (context.sourceCode.getCommentsAfter(nextToken && isCommaToken(nextToken) ? nextToken : lastItem).length > 0)
|
||||
return;
|
||||
const content = context.sourceCode.text.slice(lastItem.range[1], endRange);
|
||||
if (content.includes("\n")) {
|
||||
context.report({
|
||||
node: lastItem,
|
||||
messageId: "shouldNotWrap",
|
||||
data: {
|
||||
name: node.type
|
||||
},
|
||||
*fix(fixer) {
|
||||
const delimiter = items.length === 1 ? "" : getDelimiter(node, lastItem);
|
||||
yield removeLines(fixer, lastItem.range[1], endRange, delimiter);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const listenser = {
|
||||
ObjectExpression: (node) => {
|
||||
check(node, node.properties);
|
||||
},
|
||||
ArrayExpression: (node) => {
|
||||
check(node, node.elements);
|
||||
},
|
||||
ImportDeclaration: (node) => {
|
||||
check(
|
||||
node,
|
||||
node.specifiers[0]?.type === "ImportDefaultSpecifier" ? node.specifiers.slice(1) : node.specifiers
|
||||
);
|
||||
},
|
||||
ExportNamedDeclaration: (node) => {
|
||||
check(node, node.specifiers);
|
||||
},
|
||||
FunctionDeclaration: (node) => {
|
||||
check(
|
||||
node,
|
||||
node.params,
|
||||
node.returnType || node.body
|
||||
);
|
||||
},
|
||||
FunctionExpression: (node) => {
|
||||
check(
|
||||
node,
|
||||
node.params,
|
||||
node.returnType || node.body
|
||||
);
|
||||
},
|
||||
IfStatement: (node) => {
|
||||
check(node, [node.test], node.consequent);
|
||||
},
|
||||
ArrowFunctionExpression: (node) => {
|
||||
if (node.params.length <= 1)
|
||||
return;
|
||||
check(
|
||||
node,
|
||||
node.params,
|
||||
node.returnType || node.body
|
||||
);
|
||||
},
|
||||
CallExpression: (node) => {
|
||||
check(node, node.arguments);
|
||||
},
|
||||
TSInterfaceDeclaration: (node) => {
|
||||
check(node, node.body.body);
|
||||
},
|
||||
TSTypeLiteral: (node) => {
|
||||
check(node, node.members);
|
||||
},
|
||||
TSTupleType: (node) => {
|
||||
check(node, node.elementTypes);
|
||||
},
|
||||
TSFunctionType: (node) => {
|
||||
check(node, node.params);
|
||||
},
|
||||
NewExpression: (node) => {
|
||||
check(node, node.arguments);
|
||||
},
|
||||
TSTypeParameterDeclaration(node) {
|
||||
check(node, node.params);
|
||||
},
|
||||
TSTypeParameterInstantiation(node) {
|
||||
check(node, node.params);
|
||||
},
|
||||
ObjectPattern(node) {
|
||||
check(node, node.properties, node.typeAnnotation);
|
||||
},
|
||||
ArrayPattern(node) {
|
||||
check(node, node.elements);
|
||||
},
|
||||
JSXOpeningElement(node) {
|
||||
if (node.attributes.some((attr) => attr.loc.start.line !== attr.loc.end.line))
|
||||
return;
|
||||
check(node, node.attributes);
|
||||
},
|
||||
JSONArrayExpression(node) {
|
||||
if (hasComments(node))
|
||||
return;
|
||||
check(node, node.elements);
|
||||
},
|
||||
JSONObjectExpression(node) {
|
||||
if (hasComments(node))
|
||||
return;
|
||||
check(node, node.properties);
|
||||
}
|
||||
};
|
||||
Object.keys(options).forEach((key) => {
|
||||
if (options[key] === false)
|
||||
delete listenser[key];
|
||||
});
|
||||
return listenser;
|
||||
}
|
||||
});
|
||||
|
||||
const RULE_NAME$7 = "curly";
|
||||
const curly = createEslintRule({
|
||||
name: RULE_NAME$7,
|
||||
meta: {
|
||||
type: "layout",
|
||||
docs: {
|
||||
description: "Enforce Anthony's style of curly bracket"
|
||||
},
|
||||
fixable: "whitespace",
|
||||
schema: [],
|
||||
messages: {
|
||||
missingCurlyBrackets: "Expect curly brackets"
|
||||
}
|
||||
},
|
||||
defaultOptions: [],
|
||||
create: (context) => {
|
||||
function requireCurly(body) {
|
||||
if (!body)
|
||||
return false;
|
||||
if (body.type === "BlockStatement")
|
||||
return true;
|
||||
if (["IfStatement", "WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"].includes(body.type))
|
||||
return true;
|
||||
const statement = body.type === "ExpressionStatement" ? body.expression : body;
|
||||
if (statement.loc.start.line !== statement.loc.end.line)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
function wrapCurlyIfNeeded(body) {
|
||||
if (body.type === "BlockStatement")
|
||||
return;
|
||||
context.report({
|
||||
node: body,
|
||||
messageId: "missingCurlyBrackets",
|
||||
*fix(fixer) {
|
||||
yield fixer.insertTextAfter(body, "\n}");
|
||||
const token = context.sourceCode.getTokenBefore(body);
|
||||
yield fixer.insertTextAfterRange(token.range, " {");
|
||||
}
|
||||
});
|
||||
}
|
||||
function check(bodies, additionalChecks = []) {
|
||||
const requires = [...bodies, ...additionalChecks].map((body) => requireCurly(body));
|
||||
if (requires.some((i) => i))
|
||||
bodies.map((body) => wrapCurlyIfNeeded(body));
|
||||
}
|
||||
return {
|
||||
IfStatement(node) {
|
||||
const parent = node.parent;
|
||||
if (parent.type === "IfStatement" && parent.alternate === node)
|
||||
return;
|
||||
const statements = [];
|
||||
const tests = [];
|
||||
function addIf(node2) {
|
||||
statements.push(node2.consequent);
|
||||
if (node2.test)
|
||||
tests.push(node2.test);
|
||||
if (node2.alternate) {
|
||||
if (node2.alternate.type === "IfStatement")
|
||||
addIf(node2.alternate);
|
||||
else
|
||||
statements.push(node2.alternate);
|
||||
}
|
||||
}
|
||||
addIf(node);
|
||||
check(statements, tests);
|
||||
},
|
||||
WhileStatement(node) {
|
||||
check([node.body], [node.test]);
|
||||
},
|
||||
DoWhileStatement(node) {
|
||||
check([node.body], [node.test]);
|
||||
},
|
||||
ForStatement(node) {
|
||||
check([node.body]);
|
||||
},
|
||||
ForInStatement(node) {
|
||||
check([node.body]);
|
||||
},
|
||||
ForOfStatement(node) {
|
||||
check([node.body]);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const RULE_NAME$6 = "if-newline";
|
||||
const ifNewline = createEslintRule({
|
||||
name: RULE_NAME$6,
|
||||
meta: {
|
||||
type: "layout",
|
||||
docs: {
|
||||
description: "Newline after if"
|
||||
},
|
||||
fixable: "whitespace",
|
||||
schema: [],
|
||||
messages: {
|
||||
missingIfNewline: "Expect newline after if"
|
||||
}
|
||||
},
|
||||
defaultOptions: [],
|
||||
create: (context) => {
|
||||
return {
|
||||
IfStatement(node) {
|
||||
if (!node.consequent)
|
||||
return;
|
||||
if (node.consequent.type === "BlockStatement")
|
||||
return;
|
||||
if (node.test.loc.end.line === node.consequent.loc.start.line) {
|
||||
context.report({
|
||||
node,
|
||||
loc: {
|
||||
start: node.test.loc.end,
|
||||
end: node.consequent.loc.start
|
||||
},
|
||||
messageId: "missingIfNewline",
|
||||
fix(fixer) {
|
||||
return fixer.replaceTextRange([node.consequent.range[0], node.consequent.range[0]], "\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const RULE_NAME$5 = "import-dedupe";
|
||||
const importDedupe = createEslintRule({
|
||||
name: RULE_NAME$5,
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Fix duplication in imports"
|
||||
},
|
||||
fixable: "code",
|
||||
schema: [],
|
||||
messages: {
|
||||
importDedupe: "Expect no duplication in imports"
|
||||
}
|
||||
},
|
||||
defaultOptions: [],
|
||||
create: (context) => {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
if (node.specifiers.length <= 1)
|
||||
return;
|
||||
const names = /* @__PURE__ */ new Set();
|
||||
node.specifiers.forEach((n) => {
|
||||
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 s = n.range[0];
|
||||
let e = n.range[1];
|
||||
if (context.sourceCode.text[e] === ",")
|
||||
e += 1;
|
||||
return fixer.removeRange([s, e]);
|
||||
}
|
||||
});
|
||||
}
|
||||
names.add(id);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const _reFullWs = /^\s*$/;
|
||||
function unindent(str) {
|
||||
const lines = (typeof str === "string" ? str : str[0]).split("\n");
|
||||
const whitespaceLines = lines.map((line) => _reFullWs.test(line));
|
||||
const commonIndent = lines.reduce((min, line, idx) => {
|
||||
if (whitespaceLines[idx])
|
||||
return min;
|
||||
const indent = line.match(/^\s*/)?.[0].length;
|
||||
return indent === void 0 ? min : Math.min(min, indent);
|
||||
}, Number.POSITIVE_INFINITY);
|
||||
let emptyLinesHead = 0;
|
||||
while (emptyLinesHead < lines.length && whitespaceLines[emptyLinesHead])
|
||||
emptyLinesHead++;
|
||||
let emptyLinesTail = 0;
|
||||
while (emptyLinesTail < lines.length && whitespaceLines[lines.length - emptyLinesTail - 1])
|
||||
emptyLinesTail++;
|
||||
return lines.slice(emptyLinesHead, lines.length - emptyLinesTail).map((line) => line.slice(commonIndent)).join("\n");
|
||||
}
|
||||
|
||||
const indentUnindent = createEslintRule({
|
||||
name: "indent-unindent",
|
||||
meta: {
|
||||
type: "layout",
|
||||
docs: {
|
||||
description: "Enforce consistent indentation in `unindent` template tag"
|
||||
},
|
||||
fixable: "code",
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
indent: {
|
||||
type: "number",
|
||||
minimum: 0,
|
||||
default: 2
|
||||
},
|
||||
tags: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
}
|
||||
],
|
||||
messages: {
|
||||
"indent-unindent": "Consistent indentation in unindent tag"
|
||||
}
|
||||
},
|
||||
defaultOptions: [{}],
|
||||
create(context) {
|
||||
const {
|
||||
tags = ["$", "unindent", "unIndent"],
|
||||
indent = 2
|
||||
} = context.options?.[0] ?? {};
|
||||
return {
|
||||
TaggedTemplateExpression(node) {
|
||||
const id = node.tag;
|
||||
if (!id || id.type !== "Identifier")
|
||||
return;
|
||||
if (!tags.includes(id.name))
|
||||
return;
|
||||
if (node.quasi.quasis.length !== 1)
|
||||
return;
|
||||
const quasi = node.quasi.quasis[0];
|
||||
const value = quasi.value.raw;
|
||||
const lineStartIndex = context.sourceCode.getIndexFromLoc({
|
||||
line: node.loc.start.line,
|
||||
column: 0
|
||||
});
|
||||
const baseIndent = context.sourceCode.text.slice(lineStartIndex).match(/^\s*/)?.[0] ?? "";
|
||||
const targetIndent = baseIndent + " ".repeat(indent);
|
||||
const pure = unindent([value]);
|
||||
let final = pure.split("\n").map((line) => targetIndent + line).join("\n");
|
||||
final = `
|
||||
${final}
|
||||
${baseIndent}`;
|
||||
if (final !== value) {
|
||||
context.report({
|
||||
node: quasi,
|
||||
messageId: "indent-unindent",
|
||||
fix: (fixer) => fixer.replaceText(quasi, `\`${final}\``)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const RULE_NAME$4 = "no-import-dist";
|
||||
const noImportDist = createEslintRule({
|
||||
name: RULE_NAME$4,
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Prevent importing modules in `dist` folder"
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
noImportDist: "Do not import modules in `dist` folder, got {{path}}"
|
||||
}
|
||||
},
|
||||
defaultOptions: [],
|
||||
create: (context) => {
|
||||
function isDist(path) {
|
||||
return Boolean(path.startsWith(".") && path.match(/\/dist(\/|$)/)) || path === "dist";
|
||||
}
|
||||
return {
|
||||
ImportDeclaration: (node) => {
|
||||
if (isDist(node.source.value)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noImportDist",
|
||||
data: {
|
||||
path: node.source.value
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const RULE_NAME$3 = "no-import-node-modules-by-path";
|
||||
const noImportNodeModulesByPath = createEslintRule({
|
||||
name: RULE_NAME$3,
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Prevent importing modules in `node_modules` folder by relative or absolute path"
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
noImportNodeModulesByPath: "Do not import modules in `node_modules` folder by path"
|
||||
}
|
||||
},
|
||||
defaultOptions: [],
|
||||
create: (context) => {
|
||||
return {
|
||||
"ImportDeclaration": (node) => {
|
||||
if (node.source.value.includes("/node_modules/")) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noImportNodeModulesByPath"
|
||||
});
|
||||
}
|
||||
},
|
||||
'CallExpression[callee.name="require"]': (node) => {
|
||||
const value = node.arguments[0]?.value;
|
||||
if (typeof value === "string" && value.includes("/node_modules/")) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noImportNodeModulesByPath"
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const RULE_NAME$2 = "no-top-level-await";
|
||||
const noTopLevelAwait = createEslintRule({
|
||||
name: RULE_NAME$2,
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Prevent using top-level await"
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
NoTopLevelAwait: "Do not use top-level await"
|
||||
}
|
||||
},
|
||||
defaultOptions: [],
|
||||
create: (context) => {
|
||||
return {
|
||||
AwaitExpression: (node) => {
|
||||
let parent = node.parent;
|
||||
while (parent) {
|
||||
if (parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression") {
|
||||
return;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
context.report({
|
||||
node,
|
||||
messageId: "NoTopLevelAwait"
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const RULE_NAME$1 = "no-ts-export-equal";
|
||||
const noTsExportEqual = createEslintRule({
|
||||
name: RULE_NAME$1,
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Do not use `exports =`"
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
noTsExportEqual: "Use ESM `export default` instead"
|
||||
}
|
||||
},
|
||||
defaultOptions: [],
|
||||
create: (context) => {
|
||||
const extension = context.filename.split(".").pop();
|
||||
if (!extension)
|
||||
return {};
|
||||
if (!["ts", "tsx", "mts", "cts"].includes(extension))
|
||||
return {};
|
||||
return {
|
||||
TSExportAssignment(node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: "noTsExportEqual"
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const RULE_NAME = "top-level-function";
|
||||
const topLevelFunction = createEslintRule({
|
||||
name: RULE_NAME,
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Enforce top-level functions to be declared with function keyword"
|
||||
},
|
||||
fixable: "code",
|
||||
schema: [],
|
||||
messages: {
|
||||
topLevelFunctionDeclaration: "Top-level functions should be declared with function keyword"
|
||||
}
|
||||
},
|
||||
defaultOptions: [],
|
||||
create: (context) => {
|
||||
return {
|
||||
VariableDeclaration(node) {
|
||||
if (node.parent.type !== "Program" && node.parent.type !== "ExportNamedDeclaration")
|
||||
return;
|
||||
if (node.declarations.length !== 1)
|
||||
return;
|
||||
if (node.kind !== "const")
|
||||
return;
|
||||
if (node.declare)
|
||||
return;
|
||||
const declaration = node.declarations[0];
|
||||
if (declaration.init?.type !== "ArrowFunctionExpression" && declaration.init?.type !== "FunctionExpression") {
|
||||
return;
|
||||
}
|
||||
if (declaration.id?.type !== "Identifier")
|
||||
return;
|
||||
if (declaration.id.typeAnnotation)
|
||||
return;
|
||||
if (declaration.init.body.type !== "BlockStatement" && declaration.id?.loc.start.line === declaration.init?.body.loc.end.line) {
|
||||
return;
|
||||
}
|
||||
const fnExpression = declaration.init;
|
||||
const body = declaration.init.body;
|
||||
const id = declaration.id;
|
||||
context.report({
|
||||
node,
|
||||
loc: {
|
||||
start: id.loc.start,
|
||||
end: body.loc.start
|
||||
},
|
||||
messageId: "topLevelFunctionDeclaration",
|
||||
fix(fixer) {
|
||||
const code = context.sourceCode.text;
|
||||
const textName = code.slice(id.range[0], id.range[1]);
|
||||
const textArgs = fnExpression.params.length ? code.slice(fnExpression.params[0].range[0], fnExpression.params[fnExpression.params.length - 1].range[1]) : "";
|
||||
const textBody = body.type === "BlockStatement" ? code.slice(body.range[0], body.range[1]) : `{
|
||||
return ${code.slice(body.range[0], body.range[1])}
|
||||
}`;
|
||||
const textGeneric = fnExpression.typeParameters ? code.slice(fnExpression.typeParameters.range[0], fnExpression.typeParameters.range[1]) : "";
|
||||
const textTypeReturn = fnExpression.returnType ? code.slice(fnExpression.returnType.range[0], fnExpression.returnType.range[1]) : "";
|
||||
const textAsync = fnExpression.async ? "async " : "";
|
||||
const final = `${textAsync}function ${textName} ${textGeneric}(${textArgs})${textTypeReturn} ${textBody}`;
|
||||
return fixer.replaceTextRange([node.range[0], node.range[1]], final);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const plugin = {
|
||||
meta: {
|
||||
name: "antfu",
|
||||
version
|
||||
},
|
||||
// @keep-sorted
|
||||
rules: {
|
||||
"consistent-chaining": consistentChaining,
|
||||
"consistent-list-newline": consistentListNewline,
|
||||
"curly": curly,
|
||||
"if-newline": ifNewline,
|
||||
"import-dedupe": importDedupe,
|
||||
"indent-unindent": indentUnindent,
|
||||
"no-import-dist": noImportDist,
|
||||
"no-import-node-modules-by-path": noImportNodeModulesByPath,
|
||||
"no-top-level-await": noTopLevelAwait,
|
||||
"no-ts-export-equal": noTsExportEqual,
|
||||
"top-level-function": topLevelFunction
|
||||
}
|
||||
};
|
||||
|
||||
export { plugin as default };
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "eslint-plugin-antfu",
|
||||
"type": "module",
|
||||
"version": "3.2.2",
|
||||
"description": "Anthony's opinionated ESLint rules",
|
||||
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
||||
"license": "MIT",
|
||||
"funding": "https://github.com/sponsors/antfu",
|
||||
"homepage": "https://github.com/antfu/eslint-plugin-antfu#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/antfu/eslint-plugin-antfu.git"
|
||||
},
|
||||
"bugs": "https://github.com/antfu/eslint-plugin-antfu/issues",
|
||||
"keywords": [
|
||||
"eslint-plugin"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": "./dist/index.mjs"
|
||||
},
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.mts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"eslint": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^7.4.2",
|
||||
"@antfu/ni": "^28.2.0",
|
||||
"@antfu/utils": "^9.3.0",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/node": "^25.2.3",
|
||||
"@typescript-eslint/typescript-estree": "^8.55.0",
|
||||
"@typescript-eslint/utils": "^8.55.0",
|
||||
"bumpp": "^10.4.1",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-vitest-rule-tester": "^3.1.0",
|
||||
"jsonc-eslint-parser": "^2.4.2",
|
||||
"lint-staged": "^16.2.7",
|
||||
"simple-git-hooks": "^2.13.1",
|
||||
"skills-npm": "^1.0.0",
|
||||
"tsup": "^8.5.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"unbuild": "^3.6.1",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"resolutions": {
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-plugin-antfu": "workspace:*"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "npx lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "unbuild",
|
||||
"dev": "unbuild --stub",
|
||||
"lint": "pnpm run dev && eslint .",
|
||||
"release": "bumpp",
|
||||
"start": "tsx src/index.ts",
|
||||
"test": "vitest",
|
||||
"typecheck": "tsc --noEmit"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user