client/utils/eslint-rules/index.ts
Alois 4a841de073
(feat): improved mobile notifications
(feat): add lint rules
(feat): improve markdown inline code box
2026-08-05 21:41:55 +02:00

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),
];
},
});
},
};
},
};