Replace NodeJS dict aliases with maps and named field reads #24
+542
-7
@@ -1,20 +1,29 @@
|
||||
/**
|
||||
* `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`.
|
||||
* `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, RuleDefinition, RuleVisitor, TextSourceCode } from "@eslint/core";
|
||||
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. */
|
||||
/** 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. */
|
||||
@@ -38,7 +47,7 @@ const DIRECTIVE_PATTERN: RegExp = /^\s*(?:eslint|oxlint)-(?:disable|enable)/u;
|
||||
const RULE_NAME: string = "no-disable-comments";
|
||||
|
||||
/**
|
||||
* `no-disable-comments` — rejects every `eslint-disable*`, `eslint-enable*`,
|
||||
* `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.
|
||||
@@ -68,6 +77,501 @@ const noDisableCommentsRule: ProjectRule = {
|
||||
},
|
||||
};
|
||||
|
||||
/** 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<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") {
|
||||
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<string> = new Set<string>([
|
||||
"parent",
|
||||
"range",
|
||||
"loc",
|
||||
"start",
|
||||
"end",
|
||||
]);
|
||||
|
||||
/** Node types whose member lists mean the same in any order. */
|
||||
const UNORDERED_TYPES: ReadonlySet<string> = new Set<string>([
|
||||
"TSInterfaceBody",
|
||||
"TSTypeLiteral",
|
||||
"TSUnionType",
|
||||
"TSIntersectionType",
|
||||
]);
|
||||
|
||||
/** Type annotations too simple for an alias of their own (`no-useless-type`). */
|
||||
const SIMPLE_TYPES: ReadonlySet<string> = new Set<string>([
|
||||
"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<T extends TSESTree.Node>(
|
||||
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<T>(nodes: T[], keyOf: (node: T) => string): T[][] {
|
||||
const groups: Map<string, T[]> = 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;
|
||||
|
||||
@@ -104,7 +608,7 @@ function isAssetSource(source: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* `no-asset-imports` — rejects importing anything but a script module: a
|
||||
* `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`),
|
||||
@@ -203,6 +707,17 @@ const rules: DummyRuleMap = {
|
||||
// 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",
|
||||
@@ -314,6 +829,16 @@ export const projectOverrides: OxlintOverride[] = [
|
||||
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" },
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -325,6 +850,16 @@ const plugin = {
|
||||
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 } },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user