282 lines
7.5 KiB
TypeScript
282 lines
7.5 KiB
TypeScript
import type { Rule } from "eslint";
|
|
|
|
export const noReactNamespaceImport: Rule.RuleModule = {
|
|
meta: {
|
|
type: "suggestion",
|
|
docs: {
|
|
description: "Require named imports from React",
|
|
},
|
|
messages: {
|
|
namespaceImport:
|
|
"Import only the React exports used by this module instead of using a namespace import.",
|
|
},
|
|
schema: [],
|
|
},
|
|
create(context) {
|
|
return {
|
|
ImportDeclaration(node) {
|
|
if (
|
|
node.source.value === "react" &&
|
|
node.specifiers.some(
|
|
(specifier) => specifier.type === "ImportNamespaceSpecifier",
|
|
)
|
|
) {
|
|
context.report({ node, messageId: "namespaceImport" });
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
function isPropertyNamed(
|
|
member: {
|
|
computed: boolean;
|
|
property: { type: string; name?: string; value?: unknown };
|
|
},
|
|
name: string,
|
|
): boolean {
|
|
return member.computed
|
|
? member.property.type === "Literal" && member.property.value === name
|
|
: member.property.type === "Identifier" && member.property.name === name;
|
|
}
|
|
|
|
export const noWindowLocationReload: Rule.RuleModule = {
|
|
meta: {
|
|
type: "problem",
|
|
docs: {
|
|
description: "Disallow reloads that bypass TanStack Router",
|
|
},
|
|
messages: {
|
|
reload:
|
|
"Do not use window.location.reload(); use TanStack Router navigation instead.",
|
|
},
|
|
schema: [],
|
|
},
|
|
create(context) {
|
|
return {
|
|
CallExpression(node) {
|
|
const callee = node.callee;
|
|
if (
|
|
callee.type !== "MemberExpression" ||
|
|
!isPropertyNamed(callee, "reload") ||
|
|
callee.object.type !== "MemberExpression" ||
|
|
!isPropertyNamed(callee.object, "location") ||
|
|
callee.object.object.type !== "Identifier" ||
|
|
callee.object.object.name !== "window"
|
|
) {
|
|
return;
|
|
}
|
|
|
|
context.report({ node, messageId: "reload" });
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
interface AstNode {
|
|
type: string;
|
|
parent: AstNode | null;
|
|
range: [number, number];
|
|
}
|
|
|
|
interface TypeAliasDeclaration extends AstNode {
|
|
type: "TSTypeAliasDeclaration";
|
|
typeAnnotation: AstNode;
|
|
typeParameters?: unknown;
|
|
}
|
|
|
|
interface FunctionDeclaration extends AstNode {
|
|
type: "FunctionDeclaration";
|
|
async: boolean;
|
|
generator: boolean;
|
|
params: AstNode[];
|
|
body: AstNode & { body: AstNode[] };
|
|
}
|
|
|
|
interface ReturnStatement extends AstNode {
|
|
type: "ReturnStatement";
|
|
argument: AstNode | null;
|
|
}
|
|
|
|
function isExported(node: AstNode): boolean {
|
|
return (
|
|
node.parent?.type === "ExportNamedDeclaration" ||
|
|
node.parent?.type === "ExportDefaultDeclaration"
|
|
);
|
|
}
|
|
|
|
function containsContextSensitiveNode(value: unknown): boolean {
|
|
if (!value || typeof value !== "object") return false;
|
|
|
|
const node = value as { type?: string; [key: string]: unknown };
|
|
if (
|
|
node.type === "ThisExpression" ||
|
|
node.type === "Super" ||
|
|
node.type === "MetaProperty"
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return Object.entries(node).some(
|
|
([key, child]) =>
|
|
key !== "parent" &&
|
|
key !== "loc" &&
|
|
key !== "range" &&
|
|
containsContextSensitiveNode(child),
|
|
);
|
|
}
|
|
|
|
const unambiguousInlineTypes = new Set([
|
|
"TSAnyKeyword",
|
|
"TSBigIntKeyword",
|
|
"TSBooleanKeyword",
|
|
"TSIntrinsicKeyword",
|
|
"TSLiteralType",
|
|
"TSNeverKeyword",
|
|
"TSNullKeyword",
|
|
"TSNumberKeyword",
|
|
"TSObjectKeyword",
|
|
"TSStringKeyword",
|
|
"TSSymbolKeyword",
|
|
"TSThisType",
|
|
"TSTupleType",
|
|
"TSTypeLiteral",
|
|
"TSTypeReference",
|
|
"TSUndefinedKeyword",
|
|
"TSUnknownKeyword",
|
|
"TSVoidKeyword",
|
|
]);
|
|
|
|
export const inlineSingleUseDeclarations: Rule.RuleModule = {
|
|
meta: {
|
|
type: "suggestion",
|
|
docs: {
|
|
description: "Inline local types and functions that are used only once",
|
|
},
|
|
fixable: "code",
|
|
messages: {
|
|
function: "Inline this function at its only call site.",
|
|
type: "Inline this type at its only use site.",
|
|
},
|
|
schema: [],
|
|
},
|
|
create(context) {
|
|
const sourceCode = context.sourceCode;
|
|
|
|
return {
|
|
TSTypeAliasDeclaration(untypedNode: Rule.Node) {
|
|
const node = untypedNode as unknown as TypeAliasDeclaration;
|
|
if (node.typeParameters || isExported(node)) return;
|
|
|
|
const eslintNode = node as unknown as Rule.Node;
|
|
const [variable] = sourceCode.getDeclaredVariables(eslintNode);
|
|
if (!variable || variable.references.length !== 1) return;
|
|
|
|
const reference = variable.references[0]
|
|
.identifier as unknown as AstNode;
|
|
if (
|
|
reference.range[0] >= node.range[0] &&
|
|
reference.range[1] <= node.range[1]
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const referenceParent = reference.parent;
|
|
if (
|
|
!referenceParent ||
|
|
referenceParent.type !== "TSTypeReference" ||
|
|
referenceParent.parent?.type === "TSClassImplements" ||
|
|
referenceParent.parent?.type === "TSInterfaceHeritage"
|
|
) {
|
|
return;
|
|
}
|
|
|
|
context.report({
|
|
node: eslintNode,
|
|
messageId: "type",
|
|
fix(fixer) {
|
|
const annotation = sourceCode.getText(
|
|
node.typeAnnotation as unknown as Rule.Node,
|
|
);
|
|
const replacement = unambiguousInlineTypes.has(
|
|
node.typeAnnotation.type,
|
|
)
|
|
? annotation
|
|
: `(${annotation})`;
|
|
|
|
return [
|
|
fixer.replaceText(
|
|
referenceParent as unknown as Rule.Node,
|
|
replacement,
|
|
),
|
|
fixer.remove(eslintNode),
|
|
];
|
|
},
|
|
});
|
|
},
|
|
|
|
FunctionDeclaration(untypedNode: Rule.Node) {
|
|
const node = untypedNode as unknown as FunctionDeclaration;
|
|
if (
|
|
node.async ||
|
|
node.generator ||
|
|
node.params.length !== 0 ||
|
|
node.body.body.length !== 1 ||
|
|
isExported(node)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const statement = node.body.body[0] as ReturnStatement;
|
|
if (statement.type !== "ReturnStatement" || !statement.argument) return;
|
|
|
|
const eslintNode = node as unknown as Rule.Node;
|
|
const functionScope = sourceCode.getScope(eslintNode);
|
|
if (
|
|
functionScope.references.length !== 0 ||
|
|
functionScope.through.length !== 0 ||
|
|
containsContextSensitiveNode(statement.argument)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const [variable] = sourceCode.getDeclaredVariables(eslintNode);
|
|
if (!variable || variable.references.length !== 1) return;
|
|
|
|
const reference = variable.references[0]
|
|
.identifier as unknown as AstNode;
|
|
const call = reference.parent;
|
|
if (
|
|
!call ||
|
|
call.type !== "CallExpression" ||
|
|
(call as AstNode & { callee: AstNode }).callee !== reference ||
|
|
(call as AstNode & { arguments: AstNode[] }).arguments.length !== 0 ||
|
|
(call as AstNode & { optional?: boolean }).optional ||
|
|
(reference.range[0] >= node.range[0] &&
|
|
reference.range[1] <= node.range[1])
|
|
) {
|
|
return;
|
|
}
|
|
|
|
context.report({
|
|
node: eslintNode,
|
|
messageId: "function",
|
|
fix(fixer) {
|
|
const expression = sourceCode.getText(
|
|
statement.argument! as unknown as Rule.Node,
|
|
);
|
|
const replacement =
|
|
statement.argument!.type === "Literal"
|
|
? expression
|
|
: `(${expression})`;
|
|
|
|
return [
|
|
fixer.replaceText(call as unknown as Rule.Node, replacement),
|
|
fixer.remove(eslintNode),
|
|
];
|
|
},
|
|
});
|
|
},
|
|
};
|
|
},
|
|
};
|