This commit is contained in:
+138
-40
@@ -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" },
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user