mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 21:46:35 +00:00
chore: update lint rules
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Local ESLint plugin "conventions".
|
||||
*
|
||||
* Cross-cutting code conventions that the recommended rule sets don't enforce
|
||||
* (and plain @typescript-eslint rules cannot express in one shot):
|
||||
*
|
||||
* - interface-props: Svelte 5 props always go through a local `interface
|
||||
* Props` + `let { ... }: Props = $props()` (no inline generics, no inline
|
||||
* type imports, no untyped destructuring);
|
||||
* - no-string-union-alias: string-literal union aliases (`type Kind = 'a' |
|
||||
* 'b'`) are banned in favor of a single `as const` object + indexed access,
|
||||
* so literal sets live in exactly one place.
|
||||
*
|
||||
* Rules are AST-only (no filesystem), see the tests in eslint-plugins/__tests__.
|
||||
*/
|
||||
import interfaceProps from "./interface-props.js";
|
||||
import noStringUnionAlias from "./no-string-union-alias.js";
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
name: "conventions",
|
||||
version: "0.1.0",
|
||||
},
|
||||
rules: {
|
||||
"interface-props": interfaceProps,
|
||||
"no-string-union-alias": noStringUnionAlias,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
// Rule: SVELTE 5 PROPS MUST USE A LOCAL `interface Props`.
|
||||
//
|
||||
// Convention (AGENTS.md): every typed props of a component is described by a
|
||||
// local `interface Props`, and the props are destructured with the annotation:
|
||||
//
|
||||
// interface Props {
|
||||
// label: string;
|
||||
// accent?: boolean;
|
||||
// children?: Snippet;
|
||||
// }
|
||||
// let { label, accent = false, children }: Props = $props();
|
||||
//
|
||||
// Banned instead:
|
||||
// - the inline generic `$props<{ ... }>()` — hard to read and splits the
|
||||
// type away from the file structure;
|
||||
// - untyped destructuring `let { ... } = $props()`;
|
||||
// - binding the whole props object (`const props = $props()`);
|
||||
// - any props type name other than the local `interface Props`.
|
||||
//
|
||||
// Inline `import('...')` type queries are NOT checked here: they are already
|
||||
// banned by @typescript-eslint/consistent-type-imports.
|
||||
//
|
||||
// The check is purely syntactic (AST-level). Svelte's runes model guarantees
|
||||
// $props() only exists in instance <script> blocks, so no scope/type info is
|
||||
// needed. Interfaces are collected from every <script> (module + instance).
|
||||
|
||||
import { isInsideScriptElement } from "./utils.js";
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
fixable: "code",
|
||||
docs: {
|
||||
description:
|
||||
"Require a local `interface Props` and `let { ... }: Props = $props()` for Svelte 5 props; ban inline generics, untyped destructuring and non-local props type names.",
|
||||
category: "Svelte conventions",
|
||||
recommended: true,
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
inlineGeneric:
|
||||
"Avoid the inline generic `$props<T>()`; declare a local `interface Props` and destructure it: `let { ... }: Props = $props()`.",
|
||||
untypedDestructure:
|
||||
"Annotate the props destructuring with a local `interface Props`: `let { ... }: Props = $props()`.",
|
||||
noDestructure:
|
||||
"Destructure props with `let { ... }: Props = $props()` instead of binding the whole `$props()` object.",
|
||||
notNamedProps:
|
||||
"The props type must be the local `interface Props` (found '{{name}}').",
|
||||
inlineObjectType:
|
||||
"Declare a local `interface Props` instead of an inline object props type.",
|
||||
missingInterface:
|
||||
"No local `interface Props` is declared in this component; add one and use it as the props type.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const interfaces = new Set();
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
for (const child of node.body) {
|
||||
if (child.type !== "SvelteScriptElement") continue;
|
||||
for (const stmt of child.body ?? []) {
|
||||
if (stmt.type !== "TSInterfaceDeclaration") continue;
|
||||
interfaces.add(stmt.id.name);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee.type !== "Identifier" ||
|
||||
node.callee.name !== "$props"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!isInsideScriptElement(node)) return;
|
||||
|
||||
// Inline generic $props<{ ... }>() is banned outright.
|
||||
if (node.typeArguments?.params?.length) {
|
||||
context.report({ node: node.callee, messageId: "inlineGeneric" });
|
||||
return;
|
||||
}
|
||||
|
||||
let declarator = node.parent;
|
||||
while (declarator && declarator.type !== "VariableDeclarator") {
|
||||
declarator = declarator.parent;
|
||||
}
|
||||
if (!declarator) return;
|
||||
const id = declarator.id;
|
||||
|
||||
if (id.type === "ObjectPattern") {
|
||||
const annotation = id.typeAnnotation?.typeAnnotation ?? null;
|
||||
if (annotation) {
|
||||
if (annotation.type === "TSTypeReference") {
|
||||
const typeName = annotation.typeName;
|
||||
if (!typeName || typeName.type !== "Identifier") return;
|
||||
if (typeName.name !== "Props") {
|
||||
context.report({
|
||||
node: typeName,
|
||||
messageId: "notNamedProps",
|
||||
data: { name: typeName.name },
|
||||
});
|
||||
} else if (!interfaces.has("Props")) {
|
||||
context.report({
|
||||
node: typeName,
|
||||
messageId: "missingInterface",
|
||||
});
|
||||
}
|
||||
} else if (annotation.type !== "TSImportType") {
|
||||
context.report({
|
||||
node: annotation,
|
||||
messageId: "inlineObjectType",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
context.report({
|
||||
node: id,
|
||||
messageId: "untypedDestructure",
|
||||
...(interfaces.has("Props")
|
||||
? {
|
||||
fix: (fixer) => fixer.insertTextAfter(id, ": Props"),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (id.type === "Identifier") {
|
||||
context.report({ node: id, messageId: "noDestructure" });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
// Rule: NO STRING-LITERAL UNION TYPE ALIASES.
|
||||
//
|
||||
// A type alias whose members are all string literals (`type Kind = 'a' | 'b'`)
|
||||
// duplicates the literal set: the same set is re-typed in many places, and new
|
||||
// "derived" aliases (`type Kind2 = 'a' | 'c'`) appear that drift from the
|
||||
// source. Prefer a single `as const` object as the source of truth and derive
|
||||
// the type from it with indexed access.
|
||||
//
|
||||
// const KIND = { a: ..., b: ..., c: ... } as const;
|
||||
// type Kind = (typeof KIND)[keyof typeof KIND];
|
||||
//
|
||||
// Only TSTypeAliasDeclaration is checked — inline unions in parameter or
|
||||
// property types (one-off uses) are left alone.
|
||||
|
||||
/** True when the union and all its (possibly nested) members are string literals. */
|
||||
function allStringLiterals(/** @type {any} */ union) {
|
||||
for (const member of union.types) {
|
||||
if (member.type === "TSUnionType") {
|
||||
if (!allStringLiterals(member)) return false;
|
||||
} else if (member.type === "TSLiteralType") {
|
||||
const literal = member.literal;
|
||||
if (!(literal.type === "Literal" && typeof literal.value === "string")) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description:
|
||||
"Ban string-literal union type aliases in favor of an `as const` object plus `(typeof X)[keyof typeof X]`.",
|
||||
category: "TypeScript conventions",
|
||||
recommended: true,
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
stringUnion:
|
||||
"Prefer one `as const` object over the string-literal union alias '{{name}}' — literal sets duplicated across types or files drift apart. Derive the type instead: `const {{constName}} = {...} as const; type {{name}} = (typeof {{constName}})[keyof typeof {{constName}}]`.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
TSTypeAliasDeclaration(node) {
|
||||
const annotation = node.typeAnnotation;
|
||||
if (!annotation || annotation.type !== "TSUnionType") return;
|
||||
if (!allStringLiterals(annotation)) return;
|
||||
context.report({
|
||||
node: node.id,
|
||||
messageId: "stringUnion",
|
||||
data: {
|
||||
name: node.id.name,
|
||||
constName: node.id.name.toUpperCase(),
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
// Shared helpers for the conventions plugin (web/eslint-plugins/conventions).
|
||||
|
||||
/**
|
||||
* True when the node sits somewhere under a Svelte <script> block
|
||||
* (SvelteScriptElement). Used to limit $props() checks to script code.
|
||||
* @param {any} node
|
||||
*/
|
||||
export function isInsideScriptElement(node) {
|
||||
let cursor = node.parent;
|
||||
while (cursor && cursor.type !== "Program") {
|
||||
if (cursor.type === "SvelteScriptElement") return true;
|
||||
cursor = cursor.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user