Port the hugoalh rules, prefer-ascii and no-nan into the project lint plugin
This commit is contained in:
+542
-7
@@ -1,20 +1,29 @@
|
|||||||
/**
|
/**
|
||||||
* `project` — this repository's changes to the Oxlint categories in
|
* `project` - this repository's changes to the Oxlint categories in
|
||||||
* `vite.config.ts`. Its rules of its own are `no-disable-comments` and
|
* `vite.config.ts`. Its rules of its own are `no-disable-comments`,
|
||||||
* `no-asset-imports`.
|
* `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.
|
* Loaded by `vite.config.ts` as an Oxlint `jsPlugins` entry.
|
||||||
*
|
*
|
||||||
* @module
|
* @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 { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/types";
|
||||||
import type { DummyRuleMap, OxlintOverride } from "vite-plus/lint";
|
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 {
|
interface ProjectSourceCode extends TextSourceCode {
|
||||||
getAllComments: () => TSESTree.Comment[];
|
getAllComments: () => TSESTree.Comment[];
|
||||||
|
getLocFromIndex: (index: number) => Position;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ESLint's default rule types, reporting TypeScript-ESTree nodes. */
|
/** 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";
|
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
|
* `oxlint-disable*`, and `oxlint-enable*` comment, line or block. Oxlint has
|
||||||
* no `noInlineConfig` and no `eslint-comments` rules, so this repo bans them
|
* no `noInlineConfig` and no `eslint-comments` rules, so this repo bans them
|
||||||
* itself: when a rule fires, fix the source.
|
* 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. */
|
/** A module specifier read as a path: relative, absolute, or a `#` alias. */
|
||||||
const PATH_SPECIFIER_PATTERN: RegExp = /^[./#]/u;
|
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
|
* source path, static or a dynamic `import()` with a string literal, ending in
|
||||||
* an extension other than `.ts`, `.js`, `.mts`, or `.mjs` (`.css`, `.json`,
|
* an extension other than `.ts`, `.js`, `.mts`, or `.mjs` (`.css`, `.json`,
|
||||||
* `.svg`, `.jsx`, `.tsx`, `.cjs`) or carrying a query string (`?raw`, `?url`),
|
* `.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.
|
// No lint suppression and only `.ts` imports, as in the plugin template.
|
||||||
"project/no-disable-comments": "error",
|
"project/no-disable-comments": "error",
|
||||||
"project/no-asset-imports": "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:
|
// Off, collides with a repo or plugin rule, Oxfmt, or another category rule:
|
||||||
// Collides with `#private`: members are public unless marked so.
|
// Collides with `#private`: members are public unless marked so.
|
||||||
"typescript/explicit-member-accessibility": "off",
|
"typescript/explicit-member-accessibility": "off",
|
||||||
@@ -314,6 +829,16 @@ export const projectOverrides: OxlintOverride[] = [
|
|||||||
files: ["src/processes/child.ts"],
|
files: ["src/processes/child.ts"],
|
||||||
rules: { "typescript/promise-function-async": "off" },
|
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: {
|
rules: {
|
||||||
"no-disable-comments": noDisableCommentsRule,
|
"no-disable-comments": noDisableCommentsRule,
|
||||||
"no-asset-imports": noAssetImportsRule,
|
"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 } },
|
configs: { project: { rules } },
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user