mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
chore: add lint rules, fix script\configs
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
// Rule: ASCII-ONLY SOURCE (scoped to test files via eslint.config.js).
|
||||
// Non-ASCII in test names and expectations hurts grep-ability and hides
|
||||
// hardcoded localized strings; intentional glyphs go to the allowChars
|
||||
// option. File-content based (not AST), one report per line.
|
||||
// Details: web/eslint-plugins/README.md.
|
||||
|
||||
const DEFAULT_ALLOW = [];
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description:
|
||||
"Require ASCII-only source in scoped files (tests); exceptions go to the allowChars option.",
|
||||
category: "Conventions",
|
||||
recommended: false,
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowChars: {
|
||||
type: "array",
|
||||
items: { type: "string", minLength: 1, maxLength: 1 },
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
nonAscii:
|
||||
"Non-ASCII character {{chars}} on this line. Tests are written in plain ASCII English; keep intentional glyphs in the rule's allowChars option.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const allowed = new Set(context.options[0]?.allowChars ?? DEFAULT_ALLOW);
|
||||
return {
|
||||
Program() {
|
||||
const sourceCode = context.sourceCode;
|
||||
const text = sourceCode.getText();
|
||||
const reportedLines = new Set();
|
||||
// eslint-disable-next-line no-control-regex -- the printable-ASCII whitelist needs explicit control escapes
|
||||
for (const match of text.matchAll(/[^\x09\x0A\x0D\x20-\x7E]/g)) {
|
||||
const char = match[0];
|
||||
if (allowed.has(char)) continue;
|
||||
const loc = sourceCode.getLocFromIndex(match.index);
|
||||
if (reportedLines.has(loc.line)) continue;
|
||||
reportedLines.add(loc.line);
|
||||
context.report({
|
||||
loc,
|
||||
messageId: "nonAscii",
|
||||
data: {
|
||||
chars: `'${char}' (U+${char.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")})`,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
// Rule: COMMENT FORMAT: bounded blocks and bounded density.
|
||||
// maxBlockLines caps one comment block; maxRatio caps the share of comment
|
||||
// lines in files with minLines+ non-blank lines. Warn-only tripwires:
|
||||
// excess means refactor the code, not squeeze the comment.
|
||||
// Details: web/eslint-plugins/README.md.
|
||||
|
||||
function lineSpan(comment) {
|
||||
return comment.loc.end.line - comment.loc.start.line + 1;
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description:
|
||||
"Cap comment block length and overall comment density per file.",
|
||||
category: "Conventions",
|
||||
recommended: false,
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
maxBlockLines: { type: "integer", minimum: 1 },
|
||||
maxRatio: { type: "number", minimum: 0, maximum: 1 },
|
||||
minLines: { type: "integer", minimum: 0 },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
blockTooLong:
|
||||
"Comment block is {{lines}} lines (limit {{max}}). Explain 'why' briefly; move 'what' into names, structure, or a linked doc.",
|
||||
tooDense:
|
||||
"Comments take {{percent}}% of this file (limit {{max}}%). Prefer extracting functions/modules over narrating the code.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const maxBlockLines = context.options[0]?.maxBlockLines ?? 5;
|
||||
const maxRatio = context.options[0]?.maxRatio ?? 0.15;
|
||||
const minLines = context.options[0]?.minLines ?? 0;
|
||||
|
||||
return {
|
||||
Program(node) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const comments = sourceCode.getAllComments();
|
||||
if (comments.length === 0) return;
|
||||
|
||||
const commentLines = new Set();
|
||||
let blockStart = comments[0];
|
||||
let blockEndLine = comments[0].loc.end.line;
|
||||
let blockLines = lineSpan(comments[0]);
|
||||
|
||||
const flushBlock = () => {
|
||||
if (blockLines > maxBlockLines) {
|
||||
context.report({
|
||||
loc: blockStart.loc,
|
||||
messageId: "blockTooLong",
|
||||
data: { lines: String(blockLines), max: String(maxBlockLines) },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < comments.length; i++) {
|
||||
const comment = comments[i];
|
||||
for (let l = comment.loc.start.line; l <= comment.loc.end.line; l++) {
|
||||
commentLines.add(l);
|
||||
}
|
||||
if (i === 0) continue;
|
||||
// Adjacent `//` lines form one block; anything else starts a new one.
|
||||
if (
|
||||
comment.type === "Line" &&
|
||||
comments[i - 1].type === "Line" &&
|
||||
comment.loc.start.line === blockEndLine + 1
|
||||
) {
|
||||
blockEndLine = comment.loc.end.line;
|
||||
blockLines += 1;
|
||||
} else {
|
||||
flushBlock();
|
||||
blockStart = comment;
|
||||
blockEndLine = comment.loc.end.line;
|
||||
blockLines = lineSpan(comment);
|
||||
}
|
||||
}
|
||||
flushBlock();
|
||||
|
||||
const totalLines = sourceCode.lines.filter(
|
||||
(l) => l.trim() !== "",
|
||||
).length;
|
||||
const ratio = commentLines.size / totalLines;
|
||||
if (totalLines >= minLines && ratio > maxRatio) {
|
||||
context.report({
|
||||
loc: node.loc,
|
||||
messageId: "tooDense",
|
||||
data: {
|
||||
percent: (ratio * 100).toFixed(1),
|
||||
max: (maxRatio * 100).toFixed(1),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
// Rule: COMMENTS IN ENGLISH ONLY. Whitelist, not blacklist: a comment must
|
||||
// be plain ASCII, so Arabic/CJK/Korean are caught just like Cyrillic.
|
||||
// Intentional glyphs go to the allowChars option. String literals are NOT
|
||||
// checked: localized dictionary values legitimately contain other scripts.
|
||||
// Details: web/eslint-plugins/README.md.
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description:
|
||||
"Require comments to be written in English (ASCII whitelist; exceptions via allowChars).",
|
||||
category: "Conventions",
|
||||
recommended: false,
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
allowChars: {
|
||||
type: "array",
|
||||
items: { type: "string", minLength: 1, maxLength: 1 },
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
nonAscii:
|
||||
"Comment contains non-ASCII text ({{chars}}). Write comments in English; localized user-facing strings belong in lib/i18n dictionaries.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const allowed = new Set(context.options[0]?.allowChars ?? []);
|
||||
return {
|
||||
Program() {
|
||||
for (const comment of context.sourceCode.getAllComments()) {
|
||||
const offenders = new Set();
|
||||
for (const match of comment.value.matchAll(
|
||||
// eslint-disable-next-line no-control-regex -- the printable-ASCII whitelist needs explicit control escapes
|
||||
/[^\x09\x0A\x0D\x20-\x7E]/g,
|
||||
)) {
|
||||
if (!allowed.has(match[0])) offenders.add(match[0]);
|
||||
}
|
||||
if (offenders.size === 0) continue;
|
||||
context.report({
|
||||
loc: comment.loc,
|
||||
messageId: "nonAscii",
|
||||
data: {
|
||||
chars: [...offenders]
|
||||
.map(
|
||||
(c) =>
|
||||
`'${c}' (U+${c.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")})`,
|
||||
)
|
||||
.join(", "),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,18 +1,11 @@
|
||||
/**
|
||||
* 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__.
|
||||
* Local ESLint plugin "conventions": cross-cutting code conventions the
|
||||
* recommended rule sets do not enforce. Rule details:
|
||||
* web/eslint-plugins/README.md.
|
||||
*/
|
||||
import asciiOnly from "./ascii-only.js";
|
||||
import commentFormat from "./comment-format.js";
|
||||
import commentsEnglish from "./comments-english.js";
|
||||
import interfaceProps from "./interface-props.js";
|
||||
import noStringUnionAlias from "./no-string-union-alias.js";
|
||||
|
||||
@@ -22,6 +15,9 @@ export default {
|
||||
version: "0.1.0",
|
||||
},
|
||||
rules: {
|
||||
"ascii-only": asciiOnly,
|
||||
"comment-format": commentFormat,
|
||||
"comments-english": commentsEnglish,
|
||||
"interface-props": interfaceProps,
|
||||
"no-string-union-alias": noStringUnionAlias,
|
||||
},
|
||||
|
||||
@@ -1,28 +1,8 @@
|
||||
// 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).
|
||||
// Rule: SVELTE 5 PROPS MUST USE A LOCAL `interface Props` (AGENTS.md).
|
||||
// Banned: the inline generic `$props<T>()`, untyped destructuring, binding
|
||||
// the whole `$props()` object, non-local props type names. Purely syntactic
|
||||
// check; inline import('...') types are already covered by
|
||||
// @typescript-eslint/consistent-type-imports.
|
||||
|
||||
import { isInsideScriptElement } from "./utils.js";
|
||||
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
// 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.
|
||||
// Rule: NO STRING-LITERAL UNION TYPE ALIASES. A `type Kind = 'a' | 'b'`
|
||||
// alias duplicates the literal set across files and derived aliases drift
|
||||
// from the source; prefer one `as const` object + indexed access. Only
|
||||
// TSTypeAliasDeclaration is checked. Details: web/eslint-plugins/README.md.
|
||||
|
||||
/** True when the union and all its (possibly nested) members are string literals. */
|
||||
function allStringLiterals(/** @type {any} */ union) {
|
||||
|
||||
Reference in New Issue
Block a user