/** * `project` - this repository's changes to the Oxlint categories in * `vite.config.ts`. Its rules of its own are `no-disable-comments`, * `no-asset-imports`, `prefer-ascii`, and ports of the hugoalh `recommended` * rules Oxlint lacks, as in eslint-plugin-import-structure's own lint. * * Loaded by `vite.config.ts` as an Oxlint `jsPlugins` entry. * * @module */ import type { LanguageOptions, Position, RuleDefinition, RuleTextEdit, RuleVisitor, TextSourceCode, } from "@eslint/core"; import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/types"; import type { DummyRuleMap, OxlintOverride } from "vite-plus/lint"; /** The source code both hosts hand a rule, with the comment list and index lookup. */ interface ProjectSourceCode extends TextSourceCode { getAllComments: () => TSESTree.Comment[]; getLocFromIndex: (index: number) => Position; } /** ESLint's default rule types, reporting TypeScript-ESTree nodes. */ interface ProjectRuleTypes { LangOptions: LanguageOptions; Code: ProjectSourceCode; RuleOptions: []; Visitor: RuleVisitor; Node: TSESTree.Node; MessageIds: string; ExtRuleDocs: unknown; } /** A repo-local rule, in ESLint's standard `create` form. */ type ProjectRule = RuleDefinition; /** A comment that turns lint rules off or back on, in ESLint or Oxlint form. */ const DIRECTIVE_PATTERN: RegExp = /^\s*(?:eslint|oxlint)-(?:disable|enable)/u; /** This rule's own name, which a directive may not use to silence it. */ const RULE_NAME: string = "no-disable-comments"; /** * `no-disable-comments` - rejects every `eslint-disable*`, `eslint-enable*`, * `oxlint-disable*`, and `oxlint-enable*` comment, line or block. Oxlint has * no `noInlineConfig` and no `eslint-comments` rules, so this repo bans them * itself: when a rule fires, fix the source. * * A directive can silence the report on itself, so two built-in checks close * the gaps: `unicorn/no-abusive-eslint-disable` fails one that names no rule, * and this rule skips one that names it, which then silences nothing and fails * `reportUnusedDisableDirectives` in `vite.config.ts`. */ const noDisableCommentsRule: ProjectRule = { meta: { type: "problem", docs: { description: "Disallow lint disable and enable comments" }, schema: [], 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) ) { context.report({ loc: comment.loc, messageId: "directive" }); } } }, }; }, }; /** The highest code point `prefer-ascii` lets through. */ const MAX_ASCII_CODE_POINT: number = 127; /** * `prefer-ascii` - every character of the source is ASCII. A file is exempted * only through an entry in `projectOverrides`. */ const preferAsciiRule: ProjectRule = { meta: { type: "suggestion", docs: { description: "Require the source to be ASCII" }, schema: [], messages: { nonAscii: "Non-ASCII character '{{character}}' (U+{{codePoint}}) is not allowed.", }, }, create(context) { return { Program(_node: TSESTree.Program): void { const { sourceCode } = context; let index: number = 0; for (const character of sourceCode.text) { const codePoint: number = character.codePointAt(0) ?? 0; if (codePoint > MAX_ASCII_CODE_POINT) { context.report({ loc: { start: sourceCode.getLocFromIndex(index), end: sourceCode.getLocFromIndex(index + character.length), }, messageId: "nonAscii", data: { character, codePoint: codePoint .toString(16) .toUpperCase() .padStart(4, "0"), }, }); } index += character.length; } }, }; }, }; /** `no-duplicate-typeofs` - `typeof typeof x` is always `"string"`. */ const noDuplicateTypeofsRule: ProjectRule = { meta: { type: "problem", docs: { description: "Disallow `typeof` applied twice in a row" }, schema: [], messages: { doubled: 'Multiple `typeof`s always return `"string"`, possibly not intended.', }, }, create(context) { return { UnaryExpression(node: TSESTree.UnaryExpression): void { if ( node.operator === "typeof" && node.argument.type === AST_NODE_TYPES.UnaryExpression && node.argument.operator === "typeof" ) { context.report({ node, messageId: "doubled" }); } }, }; }, }; /** `no-empty-yield` - a `yield` with no expression, likely missing one. */ const noEmptyYieldRule: ProjectRule = { meta: { type: "problem", docs: { description: "Disallow `yield` without an expression" }, schema: [], messages: { empty: "Empty `yield` is not allowed, possibly missing the expression.", }, }, create(context) { return { YieldExpression(node: TSESTree.YieldExpression): void { if (node.argument === null) { context.report({ node, messageId: "empty" }); } }, }; }, }; /** The globals `no-nan` sees through, as in `globalThis.Number.NaN`. */ const GLOBAL_NAMES: ReadonlySet = new Set([ "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" ) { return node.property.value; } return null; } /** Whether `node` is a global object, such as `globalThis` or `globalThis.self`. */ function isGlobalChain(node: TSESTree.Expression): boolean { if (node.type === AST_NODE_TYPES.Identifier) { return GLOBAL_NAMES.has(node.name); } return ( node.type === AST_NODE_TYPES.MemberExpression && GLOBAL_NAMES.has(propertyName(node) ?? "") && isGlobalChain(node.object) ); } /** * Whether a member expression reads the global `NaN`: `Number.NaN`, * `globalThis.NaN`, or `globalThis.Number.NaN`. */ function isNanMember(node: TSESTree.MemberExpression): boolean { if (propertyName(node) !== "NaN") { return false; } const { object } = node; if (object.type === AST_NODE_TYPES.Identifier && object.name === "Number") { return true; } return ( isGlobalChain(object) || (object.type === AST_NODE_TYPES.MemberExpression && propertyName(object) === "Number" && isGlobalChain(object.object)) ); } /** `no-nan` - a `NaN` value is usually an error; use `null` for "no number". */ const noNanRule: ProjectRule = { meta: { type: "problem", docs: { description: "Disallow `NaN` as a value" }, schema: [], 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 ) { context.report({ node, messageId: "nan" }); } }, MemberExpression(node: TSESTree.MemberExpression): void { if (isNanMember(node)) { context.report({ node, messageId: "nan" }); } }, }; }, }; /** `no-unsafe-number` - a number literal outside the safe integer range. */ const noUnsafeNumberRule: ProjectRule = { meta: { type: "problem", docs: { description: "Disallow number literals outside the safe integer range", }, schema: [], messages: { unsafe: "Number literals with values equal to 2^53 or greater are too large to be represented accurately.", }, }, create(context) { return { Literal(node: TSESTree.Literal): void { if ( typeof node.value === "number" && (node.value < Number.MIN_SAFE_INTEGER || node.value > Number.MAX_SAFE_INTEGER) ) { context.report({ node, messageId: "unsafe" }); } }, }; }, }; /** A loop statement, whose last `continue` `no-useless-continue` checks. */ type LoopStatement = | TSESTree.DoWhileStatement | TSESTree.ForInStatement | TSESTree.ForOfStatement | TSESTree.ForStatement | TSESTree.WhileStatement; /** A loop's trailing unlabeled `continue`, and whether removing it is a safe fix. */ interface TrailingContinue { statement: TSESTree.ContinueStatement; removable: boolean; } /** * The unlabeled `continue` a loop ends with, and whether removing it is a * safe fix: it is in a block, rather than the loop's whole body. */ 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 ? { statement: last, removable: true } : null; } if ( node.body.type === AST_NODE_TYPES.ContinueStatement && node.body.label === null ) { return { statement: node.body, removable: false }; } return null; } /** * `no-useless-continue` - an unlabeled `continue` that ends a loop body. A * trailing `continue;` in a block is removed by the fix; a loop whose whole * body is `continue;` is only reported. */ const noUselessContinueRule: ProjectRule = { meta: { type: "suggestion", 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.", }, }, create(context) { function check(node: LoopStatement): void { const found: TrailingContinue | null = trailingContinue(node); if (found === null) { return; } if (found.removable) { context.report({ node: found.statement, messageId: "useless", fix: (fixer): RuleTextEdit => fixer.remove(found.statement), }); } else { context.report({ node: found.statement, messageId: "useless" }); } } return { DoWhileStatement: check, ForInStatement: check, ForOfStatement: check, ForStatement: check, WhileStatement: check, }; }, }; /** Node keys that say where a node is, not what it is, in ESLint and Oxlint. */ const POSITION_KEYS: ReadonlySet = new Set([ "parent", "range", "loc", "start", "end", ]); /** Node types whose member lists mean the same in any order. */ const UNORDERED_TYPES: ReadonlySet = new Set([ "TSInterfaceBody", "TSTypeLiteral", "TSUnionType", "TSIntersectionType", ]); /** Type annotations too simple for an alias of their own (`no-useless-type`). */ const SIMPLE_TYPES: ReadonlySet = new Set([ "TSAnyKeyword", "TSBigIntKeyword", "TSBooleanKeyword", "TSIntrinsicKeyword", "TSNeverKeyword", "TSNullKeyword", "TSNumberKeyword", "TSObjectKeyword", "TSStringKeyword", "TSSymbolKeyword", "TSUndefinedKeyword", "TSUnknownKeyword", "TSVoidKeyword", ]); /** * A node's shape as text, without positions, so two declarations compare * alike wherever they sit. Members of a type literal, interface body, * union, or intersection are sorted, since their order means nothing. */ function serialize(value: unknown): string { if (Array.isArray(value)) { return `[${value.map((item: unknown) => serialize(item)).join(",")}]`; } if (typeof value !== "object" || value === null) { return JSON.stringify(value) ?? "undefined"; } 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]) => { const text: string = unordered && Array.isArray(child) ? `[${child .map((item: unknown) => serialize(item)) .toSorted() .join(",")}]` : serialize(child); return `${key}:${text}`; }); return `{${parts.join(",")}}`; } /** The top-level declarations in a block, exported or not, that `matches`. */ function declarationsOf( statements: TSESTree.Node[], matches: (node: TSESTree.Node) => node is T, ): T[] { const found: T[] = []; for (const statement of statements) { const declaration: TSESTree.Node | null = statement.type === AST_NODE_TYPES.ExportNamedDeclaration ? statement.declaration : statement; if (declaration !== null && matches(declaration)) { found.push(declaration); } } return found; } /** Whether a node is an interface declaration. */ 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 { return node.type === AST_NODE_TYPES.TSTypeAliasDeclaration; } /** Groups nodes by a key and returns the groups with more than one node. */ function duplicateGroups(nodes: T[], keyOf: (node: T) => string): T[][] { const groups: Map = Map.groupBy(nodes, keyOf); return [...groups.values()].filter((group: T[]) => group.length > 1); } /** What an interface declares, its `extends` list included. */ function interfaceShape(node: TSESTree.TSInterfaceDeclaration): string { return `${serialize(node.extends)}&${serialize(node.body)}`; } /** An interface's name. */ function interfaceName(node: TSESTree.TSInterfaceDeclaration): string { return node.id.name; } /** What a type alias stands for. */ function typeAliasShape(node: TSESTree.TSTypeAliasDeclaration): string { return serialize(node.typeAnnotation); } /** * `no-duplicate-interfaces` - two interfaces in one block with the same * name, which merge silently, or with the same shape, which could be one. */ const noDuplicateInterfacesRule: ProjectRule = { meta: { type: "suggestion", 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.", }, }, create(context) { function check(statements: TSESTree.Node[]): void { 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 }, }); } } return { BlockStatement(node: TSESTree.BlockStatement): void { check(node.body); }, Program(node: TSESTree.Program): void { check(node.body); }, }; }, }; /** `no-duplicate-types` - two type aliases in one block with the same type. */ const noDuplicateTypesRule: ProjectRule = { meta: { type: "suggestion", docs: { description: "Disallow type aliases with the same type in one block", }, schema: [], 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, ); for (const node of duplicateGroups(nodes, typeAliasShape).flat()) { context.report({ node, messageId: "sameShape" }); } } return { BlockStatement(node: TSESTree.BlockStatement): void { check(node.body); }, Program(node: TSESTree.Program): void { check(node.body); }, }; }, }; /** A class member that can carry the `private` keyword. */ type AccessibilityCarrier = | TSESTree.AccessorProperty | TSESTree.MethodDefinition | TSESTree.PropertyDefinition | TSESTree.TSAbstractAccessorProperty | TSESTree.TSAbstractMethodDefinition | TSESTree.TSAbstractPropertyDefinition | TSESTree.TSParameterProperty; /** * `no-modifier-private` - class members are made private with `#`, never the * `private` keyword, which only TypeScript enforces. A `private constructor` * has no `#` form and stays allowed. */ const noModifierPrivateRule: ProjectRule = { meta: { type: "suggestion", docs: { description: "Disallow the private modifier" }, schema: [], 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"; if (node.accessibility === "private" && !isConstructor) { context.report({ node, messageId: "private" }); } } return { AccessorProperty: check, MethodDefinition: check, PropertyDefinition: check, TSAbstractAccessorProperty: check, TSAbstractMethodDefinition: check, TSAbstractPropertyDefinition: check, TSParameterProperty: check, }; }, }; /** Whether a type is too simple to need an alias: a keyword, literal, or bare name. */ function isSimpleType(node: TSESTree.TypeNode): boolean { if (SIMPLE_TYPES.has(node.type)) { return true; } if (node.type === AST_NODE_TYPES.TSLiteralType) { return node.literal.type === AST_NODE_TYPES.Literal; } return ( node.type === AST_NODE_TYPES.TSTypeReference && node.typeName.type === AST_NODE_TYPES.Identifier && node.typeArguments === undefined ); } /** `no-useless-type` - a type alias for a keyword, literal, or bare type name. */ const noUselessTypeRule: ProjectRule = { meta: { type: "suggestion", docs: { description: "Disallow type aliases too simple to need a name" }, schema: [], messages: { simple: "The type is too simple to need an alias." }, }, create(context) { return { TSTypeAliasDeclaration(node: TSESTree.TSTypeAliasDeclaration): void { if (isSimpleType(node.typeAnnotation)) { context.report({ node, messageId: "simple" }); } }, }; }, }; /** A module specifier read as a path: relative, absolute, or a `#` alias. */ const PATH_SPECIFIER_PATTERN: RegExp = /^[./#]/u; /** * Script module extensions an import may name: our `.ts` files and `.js`, * `.mts`, or `.mjs` package subpaths. `.cts`, `.cjs`, `.tsx`, and `.jsx` are * not among them. */ const SCRIPT_EXTENSIONS: ReadonlySet = new Set([ ".ts", ".js", ".mts", ".mjs", ]); /** * Whether `source` names an asset rather than a script module: a query string * such as `?raw`, or a final path segment whose extension, from its last dot, * is not `.ts`, `.js`, `.mts`, or `.mjs`. Bare package names are left alone, * dots and all, as are extensionless paths. */ function isAssetSource(source: string): boolean { if (source.includes("?")) { return true; } const segments: string[] = source.split("/"); if (!PATH_SPECIFIER_PATTERN.test(source)) { const nameLength: number = source.startsWith("@") ? 2 : 1; if (segments.length <= nameLength) { return false; } } const lastSegment: string = segments.at(-1) ?? ""; if ( lastSegment === "." || lastSegment === ".." || !lastSegment.includes(".") ) { return false; } const extension: string = lastSegment.slice(lastSegment.lastIndexOf(".")); return !SCRIPT_EXTENSIONS.has(extension); } /** * `no-asset-imports` - rejects importing anything but a script module: a * source path, static or a dynamic `import()` with a string literal, ending in * an extension other than `.ts`, `.js`, `.mts`, or `.mjs` (`.css`, `.json`, * `.svg`, `.jsx`, `.tsx`, `.cjs`) or carrying a query string (`?raw`, `?url`), * and any import or export with import attributes. `.js`, `.mts`, and `.mjs` * cover package subpaths such as `lit/decorators.js` or * `pdfjs-dist/legacy/build/pdf.mjs`; our own files are still `.ts` only. Bare * package names, `#` aliases, and `node:` builtins * without an extension pass; package subpaths are checked like paths. */ const noAssetImportsRule: ProjectRule = { meta: { type: "problem", 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.", attributes: "Import attributes are not allowed.", }, }, create(context) { function checkSource(source: TSESTree.StringLiteral): void { if (isAssetSource(source.value)) { context.report({ node: source, messageId: "extension" }); } } function checkDeclaration( node: | TSESTree.ImportDeclaration | TSESTree.ExportNamedDeclaration | TSESTree.ExportAllDeclaration, ): void { if (node.source === null) { return; } checkSource(node.source); if (node.attributes.length > 0) { context.report({ node, messageId: "attributes" }); } } return { ImportDeclaration: checkDeclaration, ExportNamedDeclaration: checkDeclaration, ExportAllDeclaration: checkDeclaration, ImportExpression(node: TSESTree.ImportExpression): void { if ( node.source.type === AST_NODE_TYPES.Literal && typeof node.source.value === "string" ) { checkSource(node.source); } if (node.options !== null) { context.report({ node, messageId: "attributes" }); } }, }; }, }; /** * This repository's changes to the Oxlint categories in `vite.config.ts`, * which turn every non-nursery category on: its own rules, the category * rules it turns off, and options that differ from a rule's defaults. */ const rules: DummyRuleMap = { // The explicit-types template's `camelcase` stand-in: no `_` inside a name // once leading and trailing ones are trimmed, unless it has no lowercase // ASCII letter (UPPER_SNAKE). "id-match": [ "error", "^_*[^_]*_*$|^[^a-z]*$", { 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": [ "error", "window", "process", "Buffer", "global", "setImmediate", "clearImmediate", ], // Single capital type parameters, which explicit-types requires, may be one letter. "id-length": ["error", { exceptionPatterns: ["^[A-Z]$"] }], // Option tweaks. "one-var": ["error", "never"], "no-use-before-define": ["error", { functions: false }], "func-style": ["error", "declaration", { allowArrowFunctions: 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", 124], "max-params": ["error", 4], "max-statements": ["error", 32], "import/max-dependencies": ["error", { max: 13 }], "unicorn/max-nested-calls": ["error", { max: 4 }], // No lint suppression and only `.ts` imports, as in the plugin template. "project/no-disable-comments": "error", "project/no-asset-imports": "error", // Ports of the hugoalh `recommended` rules Oxlint lacks, and ASCII-only source. "project/prefer-ascii": "error", "project/no-duplicate-typeofs": "error", "project/no-empty-yield": "error", "project/no-nan": "error", "project/no-unsafe-number": "error", "project/no-useless-continue": "error", "project/no-duplicate-interfaces": "error", "project/no-duplicate-types": "error", "project/no-modifier-private": "error", "project/no-useless-type": "error", // Off, collides with a repo or plugin rule, Oxfmt, or another category rule: // Collides with `#private`: members are public unless marked so. "typescript/explicit-member-accessibility": "off", // Collides with `typescript/prefer-readonly-parameter-types` being off. "typescript/prefer-readonly": "off", // Collides with Node's and the web's mutable types (`Response`, `Stats`). "typescript/prefer-readonly-parameter-types": "off", // Collides with explicit-types `no-utility-types`, which bans `Record`. "typescript/consistent-indexed-object-style": "off", // Collides with explicitly annotated declarations. "typescript/no-inferrable-types": "off", // Collides with import-structure `enforce-import-order`. "sort-imports": "off", // Collides with import-structure `enforce-mod-file` barrels. "oxc/no-barrel-file": "off", // Collides with exporting each declaration where it stands. "import/group-exports": "off", // Collides with exporting each declaration where it stands. "import/exports-last": "off", // Collides with `import/no-default-export`. "import/prefer-default-export": "off", // Collides with `unicorn/prefer-ternary`. "no-ternary": "off", // Collides with `require-await` and `promise-function-async`. "oxc/no-async-await": "off", // Collides with `typescript/prefer-optional-chain`. "oxc/no-optional-chaining": "off", // Collides with file mode checks such as `mode & 0o077`. "no-bitwise": "off", // Collides with explicit-types `no-optional-null`, which makes `undefined` the absent value. "no-undefined": "off", // Collides with `typescript/no-floating-promises`, which `void` satisfies. "no-void": "off", // Collides with JSON and Node APIs, which return `null`. "unicorn/no-null": "off", // Collides with `import/no-default-export`. "import/no-named-export": "off", // Collides with `typescript/consistent-type-imports`. "import/consistent-type-specifier-style": "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", // Command-line entry points set their exit status. "unicorn/no-process-exit": "off", // The action reports to the job log through the console. "no-console": "off", // Sequential awaits are intentional; workarounds cost more than they save. "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. "default-case": "off", // Off, too much churn: // Literal numbers read better than a constant per use. "no-magic-numbers": "off", // Keys follow meaning, not the alphabet. "sort-keys": "off", // Comments may start with code or lowercase identifiers. "capitalized-comments": "off", // explicit-types `require-return-type` covers return types. "typescript/explicit-function-return-type": "off", // explicit-types `require-return-type` covers return types. "typescript/explicit-module-boundary-types": "off", // Empty callbacks are legitimate no-ops. "no-empty-function": "off", // Methods may not need `this`. "class-methods-use-this": "off", // Braces only where a case declares. "unicorn/switch-case-braces": "off", // Plain property reads are fine. "prefer-destructuring": "off", // Object rest and spread are idiomatic. "oxc/no-rest-spread-properties": "off", // Short numbers need no separators. "unicorn/numeric-separators-style": "off", // `continue` keeps loop bodies flat. "no-continue": "off", }; /** Node code (`vite.config.ts` and `scripts/`): the first `projectOverrides` entry. */ const nodeRules: DummyRuleMap = { // Vite and the Oxlint plugin host read a default export. "import/no-default-export": "off", }; /** * The whole `overrides` array of `vite.config.ts`: the Node code entry first, * then each per-file exception, each with a comment saying why. Later entries * win over earlier ones, and all of them over `rules`. */ export const projectOverrides: OxlintOverride[] = [ { files: ["vite.config.ts", "scripts/**"], rules: nodeRules, }, // exitCode must return the promise it marks handled; an async wrapper's // promise would reject unhandled before callers await it. { files: ["src/processes/child.ts"], rules: { "typescript/promise-function-async": "off" }, }, // Posted Gitea text shows emoji verdict marks and a middle dot. { files: [ "src/post/post.ts", "src/post/post.test.ts", "src/profiles/profiles.ts", "src/profiles/profiles.test.ts", ], 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" }, }, ]; /** * The repo-local plugin, namespace `project`: its own rules, enabled as * `project/`, and `configs.project`, the rule list above. */ const plugin = { meta: { name: "project" }, rules: { "no-disable-comments": noDisableCommentsRule, "no-asset-imports": noAssetImportsRule, "prefer-ascii": preferAsciiRule, "no-duplicate-typeofs": noDuplicateTypeofsRule, "no-empty-yield": noEmptyYieldRule, "no-nan": noNanRule, "no-unsafe-number": noUnsafeNumberRule, "no-useless-continue": noUselessContinueRule, "no-duplicate-interfaces": noDuplicateInterfacesRule, "no-duplicate-types": noDuplicateTypesRule, "no-modifier-private": noModifierPrivateRule, "no-useless-type": noUselessTypeRule, }, configs: { project: { rules } }, }; export default plugin;