334 lines
13 KiB
TypeScript
334 lines
13 KiB
TypeScript
/**
|
|
* `project` — this repository's changes to the Oxlint categories in
|
|
* `vite.config.ts`. Its rules of its own are `no-disable-comments` and
|
|
* `no-asset-imports`.
|
|
*
|
|
* Loaded by `vite.config.ts` as an Oxlint `jsPlugins` entry.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import type { LanguageOptions, RuleDefinition, 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. */
|
|
interface ProjectSourceCode extends TextSourceCode {
|
|
getAllComments: () => TSESTree.Comment[];
|
|
}
|
|
|
|
/** 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<ProjectRuleTypes>;
|
|
|
|
/** 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" });
|
|
}
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
/** 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<string> = 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 }],
|
|
"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-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",
|
|
// 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 literal Gitea Actions `${{ }}` expressions in workflow strings.
|
|
"no-template-curly-in-string": "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",
|
|
// TypeScript's type checking covers exhaustive switches.
|
|
"typescript/switch-exhaustiveness-check": "off",
|
|
// PENDING USER: union members are values in profiles.json, the Gitea API,
|
|
// and posted comments; renaming them changes behavior.
|
|
"explicit-types/upper-snake-string-unions": "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",
|
|
// Node code: tooling imports Node's built-ins.
|
|
"import/no-nodejs-modules": "off",
|
|
// Node code: a CLI exits with a status.
|
|
"unicorn/no-process-exit": "off",
|
|
// Node code: a CLI reports through the console.
|
|
"no-console": "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" },
|
|
},
|
|
];
|
|
|
|
/**
|
|
* The repo-local plugin, namespace `project`: its own rules, enabled as
|
|
* `project/<rule>`, and `configs.project`, the rule list above.
|
|
*/
|
|
const plugin = {
|
|
meta: { name: "project" },
|
|
rules: {
|
|
"no-disable-comments": noDisableCommentsRule,
|
|
"no-asset-imports": noAssetImportsRule,
|
|
},
|
|
configs: { project: { rules } },
|
|
};
|
|
|
|
export default plugin;
|