Lint: simplify ESLint config and add oxlint configuration (#6443)

* Simplify ESLint config and add oxlint configuration

- Switch from typescript-eslint recommended to base config
- Remove redundant TypeScript-specific rules that are handled by base config
- Add oxlint configuration with appropriate rules
- Simplify globals configuration using globals package
- Add coverage directory to ESLint ignores
- Clean up various TypeScript rule configurations

* Add release notes for PR #6443

* [autofix.ci] apply automated fixes

* Refactor release note generator and update CLI argument parsing

- Adjusted the release note generator to fix string formatting.
- Refactored CLI argument parsing in migrate/cli.ts to use parseSync for improved clarity and consistency.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Matiss Janis Aboltins
2025-12-18 23:53:46 +00:00
committed by GitHub
parent a0850eab17
commit 05735eb55d
28 changed files with 187 additions and 195 deletions

View File

@@ -1,6 +1,17 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "import", "jsx-a11y"],
"env": {
"browser": true,
"jest": true,
"node": true
},
"globals": {
"vi": "readonly",
"backend": "readonly",
"importScripts": "readonly",
"FS": "readonly" // TODO: remove this
},
"rules": {
// TODO fix all these and re-enable
"jsx-a11y/click-events-have-key-events": "off",
@@ -11,7 +22,45 @@
"no-var": "warn",
// JSX A11y rules
"jsx-a11y/no-autofocus": ["warn", { "ignoreNonDOM": true }],
"jsx-a11y/no-autofocus": [
"warn",
{
"ignoreNonDOM": true
}
],
// Typescript rules
"typescript/ban-ts-comment": [
"warn",
{
// TODO: remove this
"ts-ignore": "allow-with-description"
}
],
"typescript/consistent-type-definitions": ["warn", "type"],
"typescript/consistent-type-imports": [
"warn",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/no-explicit-any": "warn",
"typescript/no-restricted-types": [
"warn",
{
"types": {
// forbid FC as superfluous
"FunctionComponent": {
"message": "Type the props argument and let TS infer or use ComponentType for a component prop"
},
"FC": {
"message": "Type the props argument and let TS infer or use ComponentType for a component prop"
}
}
}
],
"typescript/no-var-requires": "warn",
// Import rules
"import/first": "error",
@@ -65,6 +114,14 @@
"react/style-prop-object": "warn",
// ESLint rules
"eslint/default-case": [
"warn",
{
"commentPattern": "^no default$"
}
],
"eslint/no-array-constructor": "warn",
// "eslint/no-empty-function": "warn", // TODO: enable this
"eslint/no-redeclare": "warn",
"eslint/no-regex-spaces": "warn",
"eslint/no-restricted-globals": [
@@ -175,7 +232,10 @@
}
]
}
]
],
"eslint/no-useless-constructor": "warn",
"eslint/no-undef": "warn",
"eslint/no-unused-expressions": "warn"
},
"overrides": [
{
@@ -230,7 +290,6 @@
"react/exhaustive-deps": "off"
}
},
{
"files": [
"packages/api/migrations/*",
@@ -241,6 +300,58 @@
"rules": {
"import/no-default-export": "off"
}
},
// TODO: enable these
{
"files": [
"packages/api/**",
"packages/component-library/**",
"packages/crdt/**",
"packages/desktop-electron/**",
"packages/desktop-client/vite.config.mts",
"packages/loot-core/**",
"packages/sync-server/**"
],
"rules": {
"typescript/consistent-type-imports": "off"
}
},
{
"files": [
"packages/api/**",
"packages/component-library/**",
"packages/loot-core/src/server/**",
"packages/loot-core/src/types/**",
"packages/sync-server/**",
"packages/loot-core/typings/window.ts"
],
"rules": {
"typescript/consistent-type-definitions": "off"
}
},
{
"files": [
"packages/desktop-client/src/components/budget/envelope/budgetsummary/BudgetMonthMenu.tsx",
"packages/desktop-client/src/components/budget/tracking/budgetsummary/BudgetMonthMenu.tsx",
"packages/desktop-client/src/components/HelpMenu.tsx",
"packages/desktop-client/src/components/mobile/budget/CategoryGroupActionMenu.tsx",
"packages/desktop-client/src/components/modals/SelectLinkedAccountsModal.tsx",
"packages/desktop-client/src/components/reports/Overview.tsx",
"packages/desktop-client/src/components/reports/reports/CustomReport.tsx",
"packages/desktop-client/src/components/reports/reports/CustomReportListCards.tsx",
"packages/desktop-client/src/components/reports/reports/MissingReportCard.tsx",
"packages/desktop-client/src/components/reports/util.ts",
"packages/desktop-client/src/components/sidebar/Account.tsx",
"packages/desktop-client/src/hooks/useSplitsExpanded.tsx",
"packages/loot-core/src/server/budget/category-template-context.ts",
"packages/loot-core/src/server/budget/schedule-template.ts",
"packages/loot-core/src/server/importers/ynab5.ts",
"packages/loot-core/src/server/rules/action.ts",
"packages/loot-core/src/shared/schedules.ts"
],
"rules": {
"eslint/default-case": "off"
}
}
]
}

View File

@@ -161,8 +161,7 @@ category: ${type}
authors: [${username}]
---
${summary}
`;
${summary}`;
}
// simple exec that fails silently and returns an empty string on failure

View File

@@ -1,4 +1,3 @@
import tsParser from '@typescript-eslint/parser';
import pluginJSXA11y from 'eslint-plugin-jsx-a11y';
import oxlint from 'eslint-plugin-oxlint';
import pluginPerfectionist from 'eslint-plugin-perfectionist';
@@ -29,6 +28,7 @@ export default defineConfig(
'packages/desktop-electron/client-build/',
'packages/loot-core/**/lib-dist/*',
'packages/loot-core/**/proto/*',
'packages/sync-server/coverage/',
'packages/sync-server/user-files/',
'packages/sync-server/server-files/',
'.yarn/*',
@@ -46,15 +46,8 @@ export default defineConfig(
],
languageOptions: {
globals: {
...globals.jest,
vi: true,
describe: true,
expect: true,
it: true,
beforeAll: true,
beforeEach: true,
afterAll: true,
afterEach: true,
test: true,
},
},
},
@@ -67,8 +60,23 @@ export default defineConfig(
...globals.browser,
...globals.commonjs,
...globals.node,
...globals.jest,
globalThis: false,
vi: true,
RequestInfo: true,
RequestInit: true,
ParentNode: true,
FS: true,
IDBValidKey: true,
NodeJS: true,
Electron: true,
// Worker globals
FetchEvent: true,
ExtendableEvent: true,
ExtendableMessageEvent: true,
ServiceWorkerGlobalScope: true,
},
},
settings: {
@@ -83,7 +91,7 @@ export default defineConfig(
},
},
},
pluginTypescript.configs.recommended,
pluginTypescript.configs.base,
{
plugins: {
actual: pluginActual,
@@ -103,13 +111,6 @@ export default defineConfig(
// http://eslint.org/docs/rules/
'array-callback-return': 'warn',
'default-case': [
'warn',
{
commentPattern: '^no default$',
},
],
curly: ['warn', 'multi-line', 'consistent'],
eqeqeq: ['warn', 'smart'],
'no-array-constructor': 'warn',
@@ -292,18 +293,6 @@ export default defineConfig(
'actual/prefer-if-statement': 'warn',
'actual/prefer-logger-over-console': 'error',
// Note: base rule explicitly disabled in favor of the TS one
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
varsIgnorePattern: '^(_|React)',
argsIgnorePattern: '^(_|React)',
ignoreRestSiblings: true,
caughtErrors: 'none',
},
],
// https://github.com/eslint/eslint/issues/16954
// https://github.com/eslint/eslint/issues/16953
'no-loop-func': 'off',
@@ -326,79 +315,9 @@ export default defineConfig(
},
],
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description',
},
],
// Rules disabled during TS migration
'@typescript-eslint/no-var-requires': 'off',
'prefer-const': 'warn',
'prefer-spread': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-require-imports': 'off',
},
},
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parser: tsParser,
ecmaVersion: 2018,
sourceType: 'module',
parserOptions: {
projectService: true,
ecmaFeatures: {
jsx: true,
},
// typescript-eslint specific options
warnOnUnsupportedTypeScriptVersion: true,
},
},
// If adding a typescript-eslint version of an existing ESLint rule,
// make sure to disable the ESLint rule here.
rules: {
// TypeScript's `noFallthroughCasesInSwitch` option is more robust (#6906)
'default-case': 'off',
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/291)
'no-dupe-class-members': 'off',
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/477)
'no-undef': 'off',
// Add TypeScript specific rules (and turn off ESLint equivalents)
'@typescript-eslint/consistent-type-assertions': 'warn',
'no-array-constructor': 'off',
'@typescript-eslint/no-array-constructor': 'warn',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': [
'warn',
{
functions: false,
classes: false,
variables: false,
typedefs: false,
},
],
'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': [
'error',
{
allowShortCircuit: true,
allowTernary: true,
allowTaggedTemplates: true,
},
],
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'warn',
},
},
{
@@ -414,40 +333,6 @@ export default defineConfig(
'typescript-paths/absolute-import': ['error', { enableAlias: false }],
},
},
{
files: [
'packages/desktop-client/**/*.{ts,tsx}',
'packages/loot-core/src/client/**/*.{ts,tsx}',
],
rules: {
// enforce import type
'@typescript-eslint/consistent-type-imports': [
'warn',
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports',
},
],
'@typescript-eslint/no-restricted-types': [
'warn',
{
types: {
// forbid FC as superfluous
FunctionComponent: {
message:
'Type the props argument and let TS infer or use ComponentType for a component prop',
},
FC: {
message:
'Type the props argument and let TS infer or use ComponentType for a component prop',
},
},
},
],
},
},
{
files: [
'eslint.config.mjs',
@@ -472,17 +357,6 @@ export default defineConfig(
'no-restricted-syntax': 'off',
},
},
{
files: [
'packages/desktop-client/**/*.{ts,tsx}',
'packages/loot-core/src/client/**/*.{ts,tsx}',
],
ignores: ['**/**/globals.d.ts'],
rules: {
// enforce type over interface
'@typescript-eslint/consistent-type-definitions': ['warn', 'type'],
},
},
{
files: ['packages/sync-server/**/*'],
// TODO: fix the issues in these files

View File

@@ -63,7 +63,6 @@
"@octokit/rest": "^22.0.1",
"@types/node": "^22.19.1",
"@types/prompts": "^2.4.9",
"@typescript-eslint/parser": "^8.46.4",
"cross-env": "^10.1.0",
"eslint": "^9.39.2",
"eslint-import-resolver-typescript": "^4.4.4",

View File

@@ -3,7 +3,7 @@ import { keyframes } from '@emotion/css';
import { theme } from './theme';
import { tokens } from './tokens';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
export type CSSProperties = Record<string, any>;
const MOBILE_MIN_HEIGHT = 40;
@@ -12,7 +12,7 @@ const shadowLarge = {
boxShadow: '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)',
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
export const styles: Record<string, any> = {
incomeHeaderHeight: 70,
cardShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',

View File

@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* oxlint-disable typescript/no-explicit-any */
import './proto/sync_pb.js'; // Import for side effects
export {

View File

@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-namespace */
/* oxlint-disable typescript/no-namespace */
// package:
// file: sync.proto

View File

@@ -6,9 +6,9 @@ declare module '*.png';
declare global {
function __resetWorld(): void;
// eslint-disable-next-line @typescript-eslint/no-namespace
// oxlint-disable-next-line typescript/no-namespace
namespace PlaywrightTest {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
// oxlint-disable-next-line typescript/consistent-type-definitions
interface Matchers<R> {
toMatchThemeScreenshots(): Promise<R>;
}

View File

@@ -283,7 +283,7 @@ function ConfigureField<T extends RuleConditionEntity>({
op={op}
options={subfieldToOptions(field, subfield)}
style={{ marginTop: 10 }}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
onChange={(v: any) => {
dispatch({ type: 'set-value', value: v });
}}

View File

@@ -70,7 +70,7 @@ const payees: PayeeEntity[] = [
];
vi.mock('../../hooks/usePayees', async importOriginal => {
const actual =
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
// oxlint-disable-next-line typescript/consistent-type-imports
await importOriginal<typeof import('../../hooks/usePayees')>();
return {
...actual,

View File

@@ -96,7 +96,7 @@ root.render(
);
declare global {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
// oxlint-disable-next-line typescript/consistent-type-definitions
interface Window {
__actionsForMenu: typeof boundActions & {
undo: typeof undo;

View File

@@ -9,7 +9,7 @@ export function sequentialNewlinesPlugin() {
// Adapted from https://codesandbox.io/s/create-react-app-forked-h3rmcy?file=/src/sequentialNewlinePlugin.js:0-774
const data = this.data();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
function add(field: string, value: any) {
const list = data[field] ? data[field] : (data[field] = []);

View File

@@ -221,6 +221,6 @@ export default defineConfig(async ({ mode }) => {
},
maxWorkers: 2,
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
} satisfies UserConfig & { test: any };
});

View File

@@ -105,7 +105,7 @@ export const readFile: T.ReadFile = (
encoding = null;
}
// `any` as cannot refine return with two function overrides
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
return new Promise<any>((resolve, reject) => {
fs.readFile(filepath, encoding, (err, data) => {
if (err) {

View File

@@ -850,7 +850,7 @@ function handleSyncError(
acct: db.DbAccount,
): SyncError {
// TODO: refactor bank sync logic to use BankSyncError properly
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
if (err instanceof BankSyncError || (err as any)?.type === 'BankSyncError') {
const error = err as BankSyncError;

View File

@@ -118,7 +118,7 @@ export async function compileAndRunAqlQuery(
options: RunCompiledAqlQueryOptions,
) {
const { sqlPieces, state } = compileQuery(queryState, schema, schemaConfig);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
const data: any = await runCompiledAqlQuery(
queryState,
sqlPieces,

View File

@@ -306,7 +306,7 @@ export async function upload() {
: null),
...(groupId ? { 'X-ACTUAL-GROUP-ID': groupId } : null),
// TODO: fix me
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
} as any,
body: uploadContent,
});

View File

@@ -197,7 +197,7 @@ export async function select(table, id) {
);
// TODO: In the next phase, we will make this function generic
// and pass the type of the return type to `runQuery`.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
return rows[0] as any;
}
@@ -279,7 +279,7 @@ export async function selectWithSchema(table, sql, params) {
.map(row => convertFromSelect(schema, schemaConfig, table, row))
.filter(Boolean);
// TODO: Make convertFromSelect generic so we don't need this cast
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
return convertedRows as any[];
}

View File

@@ -80,7 +80,7 @@ export class RuleError extends Error {
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
export function APIError(msg: string, meta?: Record<string, any>) {
return { type: 'APIError', message: msg, meta };
}

View File

@@ -3,6 +3,8 @@
import * as fs from 'fs';
import * as path from 'path';
import yargs from 'yargs';
import { logger } from '../../platform/server/log';
import * as sqlite from '../../platform/server/sqlite';
@@ -16,24 +18,26 @@ import {
migrate,
} from './migrations';
const argv = require('yargs').options({
m: {
alias: 'migrationsDir',
requiresArg: true,
type: 'string',
describe: 'Migrations directory',
},
name: {
requiresArg: true,
type: 'string',
describe: 'Name of new migration',
},
db: {
requiresArg: true,
type: 'string',
describe: 'Path to database',
},
}).argv;
const argv = yargs()
.options({
m: {
alias: 'migrationsDir',
requiresArg: true,
type: 'string',
describe: 'Migrations directory',
},
name: {
requiresArg: true,
type: 'string',
describe: 'Name of new migration',
},
db: {
requiresArg: true,
type: 'string',
describe: 'Path to database',
},
})
.parseSync();
function getDatabase() {
return sqlite.openDatabase(argv.db);
@@ -62,7 +66,7 @@ async function list(db) {
const cmd = argv._[0];
withMigrationsDir(argv.migrationsDir || getMigrationsDir(), async () => {
withMigrationsDir(argv.m || getMigrationsDir(), async () => {
switch (cmd) {
case 'reset':
fs.unlinkSync(argv.db);

View File

@@ -94,7 +94,7 @@ const baseTime = 1565374471903;
const clientId1 = '80dd7da215247293';
const clientId2 = '90xU1sd5124329ac';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
function makeGen<T extends Arbitrary<any>>({
table,
row,

View File

@@ -54,7 +54,7 @@ async function importFileWithRealTime(
let transactions = originalTransactions;
if (transactions) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
transactions = (transactions as any[]).map(trans => ({
...trans,
amount: amountToInteger(trans.amount),

View File

@@ -1,4 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
type AnyFunction = (...args: any[]) => any;
export function sequential<T extends AnyFunction>(

View File

@@ -1,4 +1,4 @@
const os = require('os');
import os from 'os';
const isWindows = os.platform() === 'win32';
const isMac = os.platform() === 'darwin';

View File

@@ -9,7 +9,7 @@ export interface ServerHandlers {
applySpecialCases?: boolean;
}) => Promise<{ filters: unknown[] }>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
query: (query: QueryState) => Promise<{ data: any; dependencies: string[] }>;
'get-server-version': () => Promise<

View File

@@ -12,7 +12,7 @@ export type WithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
// Allows use of object literals inside child elements of `Trans` tags
// see https://github.com/i18next/react-i18next/issues/1483
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// oxlint-disable-next-line typescript/no-explicit-any
export type TransObjectLiteral = any;
export type AtLeastOne<T extends Record<string, unknown>> =

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Simplify linting configuration by migrating from ESLint to Oxlint with updated rules.

View File

@@ -9277,7 +9277,7 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:8.46.4, @typescript-eslint/parser@npm:^8.46.4":
"@typescript-eslint/parser@npm:8.46.4":
version: 8.46.4
resolution: "@typescript-eslint/parser@npm:8.46.4"
dependencies:
@@ -10051,7 +10051,6 @@ __metadata:
"@octokit/rest": "npm:^22.0.1"
"@types/node": "npm:^22.19.1"
"@types/prompts": "npm:^2.4.9"
"@typescript-eslint/parser": "npm:^8.46.4"
cross-env: "npm:^10.1.0"
eslint: "npm:^9.39.2"
eslint-import-resolver-typescript: "npm:^4.4.4"