Apply batch 4 lint decisions and deno fmt settings
Check / check (pull_request) Successful in 56s

This commit is contained in:
Danny Kim
2026-10-20 09:27:50 +09:00
parent ca619888b2
commit a429e6ffd0
26 changed files with 937 additions and 430 deletions
+138 -40
View File
@@ -62,13 +62,18 @@ const noDisableCommentsRule: ProjectRule = {
type: "problem",
docs: { description: "Disallow lint disable and enable comments" },
schema: [],
messages: { directive: "Lint disable comments are not allowed; fix the source." },
messages: {
directive: "Lint disable comments are not allowed; fix the source.",
},
},
create(context) {
return {
Program(_node: TSESTree.Program): void {
for (const comment of context.sourceCode.getAllComments()) {
if (DIRECTIVE_PATTERN.test(comment.value) && !comment.value.includes(RULE_NAME)) {
if (
DIRECTIVE_PATTERN.test(comment.value) &&
!comment.value.includes(RULE_NAME)
) {
context.report({ loc: comment.loc, messageId: "directive" });
}
}
@@ -90,7 +95,8 @@ const preferAsciiRule: ProjectRule = {
docs: { description: "Require the source to be ASCII" },
schema: [],
messages: {
nonAscii: "Non-ASCII character '{{character}}' (U+{{codePoint}}) is not allowed.",
nonAscii:
"Non-ASCII character '{{character}}' (U+{{codePoint}}) is not allowed.",
},
},
create(context) {
@@ -107,7 +113,13 @@ const preferAsciiRule: ProjectRule = {
end: sourceCode.getLocFromIndex(index + character.length),
},
messageId: "nonAscii",
data: { character, codePoint: codePoint.toString(16).toUpperCase().padStart(4, "0") },
data: {
character,
codePoint: codePoint
.toString(16)
.toUpperCase()
.padStart(4, "0"),
},
});
}
index += character.length;
@@ -123,7 +135,10 @@ const noDuplicateTypeofsRule: ProjectRule = {
type: "problem",
docs: { description: "Disallow `typeof` applied twice in a row" },
schema: [],
messages: { doubled: 'Multiple `typeof`s always return `"string"`, possibly not intended.' },
messages: {
doubled:
'Multiple `typeof`s always return `"string"`, possibly not intended.',
},
},
create(context) {
return {
@@ -146,7 +161,9 @@ const noEmptyYieldRule: ProjectRule = {
type: "problem",
docs: { description: "Disallow `yield` without an expression" },
schema: [],
messages: { empty: "Empty `yield` is not allowed, possibly missing the expression." },
messages: {
empty: "Empty `yield` is not allowed, possibly missing the expression.",
},
},
create(context) {
return {
@@ -160,14 +177,21 @@ const noEmptyYieldRule: ProjectRule = {
};
/** The globals `no-nan` sees through, as in `globalThis.Number.NaN`. */
const GLOBAL_NAMES: ReadonlySet<string> = new Set<string>(["globalThis", "self", "window"]);
const GLOBAL_NAMES: ReadonlySet<string> = new Set<string>([
"globalThis",
"self",
"window",
]);
/** The name a member expression's property spells, computed or not. */
function propertyName(node: TSESTree.MemberExpression): string | null {
if (node.property.type === AST_NODE_TYPES.Identifier && !node.computed) {
return node.property.name;
}
if (node.property.type === AST_NODE_TYPES.Literal && typeof node.property.value === "string") {
if (
node.property.type === AST_NODE_TYPES.Literal &&
typeof node.property.value === "string"
) {
return node.property.value;
}
return null;
@@ -211,12 +235,17 @@ const noNanRule: ProjectRule = {
type: "problem",
docs: { description: "Disallow `NaN` as a value" },
schema: [],
messages: { nan: "Using `NaN` as a value is usually an error and not intended." },
messages: {
nan: "Using `NaN` as a value is usually an error and not intended.",
},
},
create(context) {
return {
Identifier(node: TSESTree.Identifier): void {
if (node.name === "NaN" && node.parent.type !== AST_NODE_TYPES.MemberExpression) {
if (
node.name === "NaN" &&
node.parent.type !== AST_NODE_TYPES.MemberExpression
) {
context.report({ node, messageId: "nan" });
}
},
@@ -233,7 +262,9 @@ const noNanRule: ProjectRule = {
const noUnsafeNumberRule: ProjectRule = {
meta: {
type: "problem",
docs: { description: "Disallow number literals outside the safe integer range" },
docs: {
description: "Disallow number literals outside the safe integer range",
},
schema: [],
messages: {
unsafe:
@@ -245,7 +276,8 @@ const noUnsafeNumberRule: ProjectRule = {
Literal(node: TSESTree.Literal): void {
if (
typeof node.value === "number" &&
(node.value < Number.MIN_SAFE_INTEGER || node.value > Number.MAX_SAFE_INTEGER)
(node.value < Number.MIN_SAFE_INTEGER ||
node.value > Number.MAX_SAFE_INTEGER)
) {
context.report({ node, messageId: "unsafe" });
}
@@ -275,11 +307,15 @@ interface TrailingContinue {
function trailingContinue(node: LoopStatement): TrailingContinue | null {
if (node.body.type === AST_NODE_TYPES.BlockStatement) {
const last = node.body.body.at(-1);
return last?.type === AST_NODE_TYPES.ContinueStatement && last.label === null
return last?.type === AST_NODE_TYPES.ContinueStatement &&
last.label === null
? { statement: last, removable: true }
: null;
}
if (node.body.type === AST_NODE_TYPES.ContinueStatement && node.body.label === null) {
if (
node.body.type === AST_NODE_TYPES.ContinueStatement &&
node.body.label === null
) {
return { statement: node.body, removable: false };
}
return null;
@@ -293,11 +329,14 @@ function trailingContinue(node: LoopStatement): TrailingContinue | null {
const noUselessContinueRule: ProjectRule = {
meta: {
type: "suggestion",
docs: { description: "Disallow an unlabeled `continue` at the end of a loop" },
docs: {
description: "Disallow an unlabeled `continue` at the end of a loop",
},
fixable: "code",
schema: [],
messages: {
useless: "The statement `continue` at the end of the loop and without a label is useless.",
useless:
"The statement `continue` at the end of the loop and without a label is useless.",
},
},
create(context) {
@@ -372,7 +411,8 @@ function serialize(value: unknown): string {
if (typeof value !== "object" || value === null) {
return JSON.stringify(value) ?? "undefined";
}
const unordered: boolean = "type" in value && UNORDERED_TYPES.has(String(value.type));
const unordered: boolean =
"type" in value && UNORDERED_TYPES.has(String(value.type));
const parts: string[] = Object.entries(value)
.filter(([key]: [string, unknown]) => !POSITION_KEYS.has(key))
.map(([key, child]: [string, unknown]) => {
@@ -396,7 +436,9 @@ function declarationsOf<T extends TSESTree.Node>(
const found: T[] = [];
for (const statement of statements) {
const declaration: TSESTree.Node | null =
statement.type === AST_NODE_TYPES.ExportNamedDeclaration ? statement.declaration : statement;
statement.type === AST_NODE_TYPES.ExportNamedDeclaration
? statement.declaration
: statement;
if (declaration !== null && matches(declaration)) {
found.push(declaration);
}
@@ -405,12 +447,16 @@ function declarationsOf<T extends TSESTree.Node>(
}
/** Whether a node is an interface declaration. */
function isInterface(node: TSESTree.Node): node is TSESTree.TSInterfaceDeclaration {
function isInterface(
node: TSESTree.Node,
): node is TSESTree.TSInterfaceDeclaration {
return node.type === AST_NODE_TYPES.TSInterfaceDeclaration;
}
/** Whether a node is a type alias declaration. */
function isTypeAlias(node: TSESTree.Node): node is TSESTree.TSTypeAliasDeclaration {
function isTypeAlias(
node: TSESTree.Node,
): node is TSESTree.TSTypeAliasDeclaration {
return node.type === AST_NODE_TYPES.TSTypeAliasDeclaration;
}
@@ -442,21 +488,33 @@ function typeAliasShape(node: TSESTree.TSTypeAliasDeclaration): string {
const noDuplicateInterfacesRule: ProjectRule = {
meta: {
type: "suggestion",
docs: { description: "Disallow interfaces with the same name or shape in one block" },
docs: {
description:
"Disallow interfaces with the same name or shape in one block",
},
schema: [],
messages: {
sameShape: "Found multiple interfaces with the same shape, possibly mergeable.",
sameName: "Found multiple interfaces named '{{name}}', possibly not intended.",
sameShape:
"Found multiple interfaces with the same shape, possibly mergeable.",
sameName:
"Found multiple interfaces named '{{name}}', possibly not intended.",
},
},
create(context) {
function check(statements: TSESTree.Node[]): void {
const nodes: TSESTree.TSInterfaceDeclaration[] = declarationsOf(statements, isInterface);
const nodes: TSESTree.TSInterfaceDeclaration[] = declarationsOf(
statements,
isInterface,
);
for (const node of duplicateGroups(nodes, interfaceShape).flat()) {
context.report({ node, messageId: "sameShape" });
}
for (const node of duplicateGroups(nodes, interfaceName).flat()) {
context.report({ node, messageId: "sameName", data: { name: node.id.name } });
context.report({
node,
messageId: "sameName",
data: { name: node.id.name },
});
}
}
return {
@@ -474,13 +532,21 @@ const noDuplicateInterfacesRule: ProjectRule = {
const noDuplicateTypesRule: ProjectRule = {
meta: {
type: "suggestion",
docs: { description: "Disallow type aliases with the same type in one block" },
docs: {
description: "Disallow type aliases with the same type in one block",
},
schema: [],
messages: { sameShape: "Found multiple types with the same shape, possibly mergeable." },
messages: {
sameShape:
"Found multiple types with the same shape, possibly mergeable.",
},
},
create(context) {
function check(statements: TSESTree.Node[]): void {
const nodes: TSESTree.TSTypeAliasDeclaration[] = declarationsOf(statements, isTypeAlias);
const nodes: TSESTree.TSTypeAliasDeclaration[] = declarationsOf(
statements,
isTypeAlias,
);
for (const node of duplicateGroups(nodes, typeAliasShape).flat()) {
context.report({ node, messageId: "sameShape" });
}
@@ -516,12 +582,15 @@ const noModifierPrivateRule: ProjectRule = {
type: "suggestion",
docs: { description: "Disallow the private modifier" },
schema: [],
messages: { private: "Use a # private name instead of the private modifier." },
messages: {
private: "Use a # private name instead of the private modifier.",
},
},
create(context) {
function check(node: AccessibilityCarrier): void {
const isConstructor: boolean =
node.type === AST_NODE_TYPES.MethodDefinition && node.kind === "constructor";
node.type === AST_NODE_TYPES.MethodDefinition &&
node.kind === "constructor";
if (node.accessibility === "private" && !isConstructor) {
context.report({ node, messageId: "private" });
}
@@ -580,7 +649,12 @@ const PATH_SPECIFIER_PATTERN: RegExp = /^[./#]/u;
* `.mts`, or `.mjs` package subpaths. `.cts`, `.cjs`, `.tsx`, and `.jsx` are
* not among them.
*/
const SCRIPT_EXTENSIONS: ReadonlySet<string> = new Set([".ts", ".js", ".mts", ".mjs"]);
const SCRIPT_EXTENSIONS: ReadonlySet<string> = new Set([
".ts",
".js",
".mts",
".mjs",
]);
/**
* Whether `source` names an asset rather than a script module: a query string
@@ -600,7 +674,11 @@ function isAssetSource(source: string): boolean {
}
}
const lastSegment: string = segments.at(-1) ?? "";
if (lastSegment === "." || lastSegment === ".." || !lastSegment.includes(".")) {
if (
lastSegment === "." ||
lastSegment === ".." ||
!lastSegment.includes(".")
) {
return false;
}
const extension: string = lastSegment.slice(lastSegment.lastIndexOf("."));
@@ -621,7 +699,10 @@ function isAssetSource(source: string): boolean {
const noAssetImportsRule: ProjectRule = {
meta: {
type: "problem",
docs: { description: "Disallow importing assets instead of .ts, .js, .mts, or .mjs modules" },
docs: {
description:
"Disallow importing assets instead of .ts, .js, .mts, or .mjs modules",
},
schema: [],
messages: {
extension: "Only .ts, .js, .mts and .mjs modules may be imported.",
@@ -653,7 +734,10 @@ const noAssetImportsRule: ProjectRule = {
ExportNamedDeclaration: checkDeclaration,
ExportAllDeclaration: checkDeclaration,
ImportExpression(node: TSESTree.ImportExpression): void {
if (node.source.type === AST_NODE_TYPES.Literal && typeof node.source.value === "string") {
if (
node.source.type === AST_NODE_TYPES.Literal &&
typeof node.source.value === "string"
) {
checkSource(node.source);
}
if (node.options !== null) {
@@ -676,7 +760,12 @@ const rules: DummyRuleMap = {
"id-match": [
"error",
"^_*[^_]*_*$|^[^a-z]*$",
{ properties: false, classFields: false, onlyDeclarations: false, ignoreDestructuring: true },
{
properties: false,
classFields: false,
onlyDeclarations: false,
ignoreDestructuring: true,
},
],
// On Node, these come from an explicit `node:` import instead of the global scope.
"no-restricted-globals": [
@@ -694,12 +783,14 @@ const rules: DummyRuleMap = {
"one-var": ["error", "never"],
"no-use-before-define": ["error", { functions: false }],
"func-style": ["error", "declaration", { allowArrowFunctions: true }],
"unicorn/import-style": ["error", { styles: { "node:path": { named: true } } }],
// Each limit is the smallest value the current code passes.
complexity: ["error", 26],
"max-depth": ["error", 6],
"max-lines": ["error", { max: 2048, skipBlankLines: false, skipComments: false }],
"max-lines-per-function": ["error", 98],
"max-lines": [
"error",
{ max: 2048, skipBlankLines: false, skipComments: false },
],
"max-lines-per-function": ["error", 124],
"max-params": ["error", 4],
"max-statements": ["error", 32],
"import/max-dependencies": ["error", { max: 13 }],
@@ -757,8 +848,8 @@ const rules: DummyRuleMap = {
"import/no-named-export": "off",
// Collides with `typescript/consistent-type-imports`.
"import/consistent-type-specifier-style": "off",
// Collides with literal Gitea Actions `${{ }}` expressions in workflow strings.
"no-template-curly-in-string": "off",
// Collides with Oxfmt, which lowercases hex digits back.
"unicorn/number-literal-case": "off",
// Node code, everywhere: this action runs on Node.
// Node built-ins are the action's runtime.
"import/no-nodejs-modules": "off",
@@ -770,6 +861,8 @@ const rules: DummyRuleMap = {
"no-await-in-loop": "off",
// Not type-aware; TypeScript's `noImplicitReturns` covers it.
"consistent-return": "off",
// Misfires on complete switches, like `consistent-return`; TypeScript covers it.
"typescript/consistent-return": "off",
// TypeScript's type checking covers exhaustive switches.
"typescript/switch-exhaustiveness-check": "off",
// TypeScript checks completeness; a forced `default:` hides missing cases.
@@ -833,6 +926,11 @@ export const projectOverrides: OxlintOverride[] = [
],
rules: { "project/prefer-ascii": "off" },
},
// The workflow test checks literal Gitea Actions `${{ }}` expressions.
{
files: ["tests/workflow.test.ts"],
rules: { "no-template-curly-in-string": "off" },
},
];
/**