Switch from eslint to oxlint

This commit is contained in:
Johannes Marbach
2026-06-18 16:09:26 +02:00
parent ded3d0ab81
commit 1cd2764eb2
15 changed files with 518 additions and 2359 deletions

View File

@@ -1,120 +0,0 @@
const COPYRIGHT_HEADER = `/*
Copyright %%CURRENT_YEAR%% New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
`;
module.exports = {
plugins: ["matrix-org", "rxjs", "jsdoc", "element-call"],
extends: [
"plugin:matrix-org/react",
"plugin:matrix-org/a11y",
"plugin:matrix-org/typescript",
"prettier",
"plugin:rxjs/recommended",
"plugin:storybook/recommended",
],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: ["./tsconfig.json"],
},
env: {
browser: true,
node: true,
},
rules: {
"element-call/no-observablescope-leak": "error",
"jsdoc/no-types": "error",
"jsdoc/empty-tags": "error",
"jsdoc/check-property-names": "error",
"jsdoc/check-values": "error",
"jsdoc/check-param-names": "warn",
// "jsdoc/require-param": "warn",
"jsdoc/require-param-description": "warn",
"matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER],
"jsx-a11y/media-has-caption": "off",
"react/display-name": "error",
// Encourage proper usage of Promises:
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-await": "error",
"@typescript-eslint/await-thenable": "error",
// To help ensure that we get proper vite/rollup lazy loading (e.g. for matrix-js-sdk):
"@typescript-eslint/consistent-type-imports": [
"error",
{ fixStyle: "inline-type-imports" },
],
// To encourage good usage of RxJS:
"rxjs/no-exposed-subjects": "error",
"rxjs/finnish": ["error", { names: { "^this$": false } }],
"no-restricted-imports": [
"error",
{
paths: ["matrix-widget-api", "matrix-js-sdk"].flatMap((lib) =>
["src", "src/", "src/index", "lib", "lib/", "lib/index"]
.map((path) => `${lib}/${path}`)
.map((name) => ({ name, message: `Please use ${lib} instead` })),
),
patterns: [
...["matrix-widget-api"].map((lib) => ({
group: ["src", "src/", "src/**", "lib", "lib/", "lib/**"].map(
(path) => `${lib}/${path}`,
),
message: `Please use ${lib} instead`,
})),
// XXX: We use /lib in lots of places, so allow for now.
...["matrix-js-sdk"].map((lib) => ({
group: ["src", "src/", "src/**"].map((path) => `${lib}/${path}`),
message: `Please use ${lib} instead`,
})),
],
},
],
},
overrides: [
{
files: ["src/*/**"],
rules: {
// In application code we should use the js-sdk logger, never console directly.
"no-console": ["error"],
},
},
{
files: [
"**/*.test.ts",
"**/*.test.tsx",
"**/test.ts",
"**/test.tsx",
"**/test-**",
],
rules: {
// Tests often initialize an ObservableScope in an outer scope in
// beforeEach, which is not actually a problem
"element-call/no-observablescope-leak": "off",
"jsdoc/no-types": "off",
"jsdoc/empty-tags": "off",
"jsdoc/check-property-names": "off",
"jsdoc/check-values": "off",
"jsdoc/check-param-names": "off",
"jsdoc/require-param-description": "off",
},
},
{
files: ["playwright/**"],
rules: {
// Playwright as a `use` function that has nothing to do with React hooks.
"react-hooks/rules-of-hooks": "off",
},
},
],
settings: {
react: {
version: "detect",
},
},
};

View File

@@ -24,8 +24,8 @@ jobs:
run: "pnpm run format:check"
- name: i18n
run: "pnpm run i18n:check"
- name: ESLint
run: "pnpm run lint:eslint"
- name: Lint
run: "pnpm run lint:oxlint"
- name: Type check
run: "pnpm run lint:types"
- name: Dead code analysis

197
.oxlintrc.json Normal file
View File

@@ -0,0 +1,197 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [
"eslint",
"import",
"jsdoc",
"jsx-a11y",
"promise",
"react",
"typescript",
"unicorn",
"vitest"
],
"jsPlugins": [
"eslint-plugin-storybook",
"eslint-plugin-element-call"
// TODO: Re-enable once oxlint supports lint rules that rely on TypeScript type-awareness.
// "eslint-plugin-rxjs"
],
"categories": {
"correctness": "error",
"perf": "error"
},
"options": {
"denyWarnings": true,
"typeAware": true
},
"env": {
"builtin": true
},
"rules": {
"element-call/no-observablescope-leak": "error",
"jsdoc/empty-tags": "error",
"jsdoc/check-property-names": "error",
"jsdoc/require-param-description": "warn",
"react/display-name": "error",
// TODO: Re-enable once oxlint supports lint rules that rely on TypeScript type-awareness.
// "rxjs/no-exposed-subjects": "error",
// "rxjs/finnish": [
// "error",
// {
// "names": {
// "^this$": false
// }
// }
// ],
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "matrix-widget-api/src",
"message": "Please use matrix-widget-api instead"
},
{
"name": "matrix-widget-api/src/",
"message": "Please use matrix-widget-api instead"
},
{
"name": "matrix-widget-api/src/index",
"message": "Please use matrix-widget-api instead"
},
{
"name": "matrix-widget-api/lib",
"message": "Please use matrix-widget-api instead"
},
{
"name": "matrix-widget-api/lib/",
"message": "Please use matrix-widget-api instead"
},
{
"name": "matrix-widget-api/lib/index",
"message": "Please use matrix-widget-api instead"
},
{
"name": "matrix-js-sdk/src",
"message": "Please use matrix-js-sdk instead"
},
{
"name": "matrix-js-sdk/src/",
"message": "Please use matrix-js-sdk instead"
},
{
"name": "matrix-js-sdk/src/index",
"message": "Please use matrix-js-sdk instead"
},
{
"name": "matrix-js-sdk/lib",
"message": "Please use matrix-js-sdk instead"
},
{
"name": "matrix-js-sdk/lib/",
"message": "Please use matrix-js-sdk instead"
},
{
"name": "matrix-js-sdk/lib/index",
"message": "Please use matrix-js-sdk instead"
}
],
"patterns": [
{
"group": [
"matrix-widget-api/src",
"matrix-widget-api/src/",
"matrix-widget-api/src/**",
"matrix-widget-api/lib",
"matrix-widget-api/lib/",
"matrix-widget-api/lib/**"
],
"message": "Please use matrix-widget-api instead"
},
{
"group": [
// We use /lib in lots of places, so allow for now.
"matrix-js-sdk/src",
"matrix-js-sdk/src/",
"matrix-js-sdk/src/**"
],
"message": "Please use matrix-js-sdk instead"
}
]
}
],
"typescript/no-floating-promises": "error",
"typescript/no-misused-promises": "error",
"typescript/promise-function-async": "error",
"typescript/require-await": "error",
"typescript/await-thenable": "error",
// To help ensure that we get proper vite/rollup lazy loading (e.g. for matrix-js-sdk).
"typescript/consistent-type-imports": [
"error",
{
"fixStyle": "inline-type-imports"
}
],
// TODO: These had to be disabled in the eslint -> oxlint migration. Would be nice to
// enable them in future or at least document why we're disabling them.
"eslint/no-await-in-loop": "off",
"eslint/no-unused-vars": ["error", { "args": "none" }],
"import/default": "off",
"jsdoc/check-tag-names": "off",
"jsx-a11y/prefer-tag-over-role": "off",
"promise/no-callback-in-promise": "off",
"react/jsx-key": "off",
"react/jsx-no-constructed-context-values": "off",
"react/no-array-index-key": "off",
"react/no-children-prop": "off",
"react/no-object-type-as-default-prop": "off",
"typescript/no-misused-spread": "off",
"typescript/no-useless-default-assignment": "off",
"typescript/restrict-template-expressions": "off",
"typescript/unbound-method": "off",
"vitest/expect-expect": "off",
"vitest/no-conditional-expect": "off",
"vitest/no-disabled-tests": "off",
"vitest/require-mock-type-parameters": "off",
"vitest/require-to-throw-message": "off"
},
"overrides": [
{
"files": ["src/*/**"],
"rules": {
// In application code we should use the js-sdk logger, never console directly.
"no-console": "error"
}
},
{
"files": [
"**/*.test.ts",
"**/*.test.tsx",
"**/test.ts",
"**/test.tsx",
"**/test-**"
],
"rules": {
// Tests often initialize an ObservableScope in an outer scope in
// beforeEach, which is not actually a problem
"element-call/no-observablescope-leak": "off",
"jsdoc/empty-tags": "off",
"jsdoc/check-property-names": "off",
"jsdoc/require-param-description": "off",
"jsx-a11y/media-has-caption": "off"
// TODO: Enable once oxlint supports them.
// "jsdoc/check-values": "off",
// "jsdoc/check-param-names": "off",
// "jsdoc/no-types": "off",
}
},
{
"files": ["playwright/**"],
"rules": {
// Playwright as a `use` function that has nothing to do with React hooks.
"react-hooks/rules-of-hooks": "off"
}
}
]
}

View File

@@ -40,19 +40,23 @@ const rule = ESLintUtils.RuleCreator(
node.parent?.type === "MemberExpression" &&
node.parent.object === node &&
node.parent.property.type === "Identifier" &&
!safeScopeMethods.includes(node.parent.property.name)
!safeScopeMethods.includes(node.parent.property.name) &&
node.name.endsWith("cope")
) {
// Verify that the variable is actually of type ObservableScope
// (expensive, so we check this last)
const services = ESLintUtils.getParserServices(context);
const type = services.getTypeAtLocation(node);
if (type.symbol?.name === "ObservableScope")
// This ObservableScope method call may be causing resource leaks.
context.report({
messageId: "scopeLeak",
loc: node.loc,
node,
});
// TODO: Once oxlint supports lint rules that rely on TypeScript type-awareness,
// Verify that the variable is actually of type ObservableScope rather than just
// checking its name. This is expensive so we should do this last.
//
// const services = ESLintUtils.getParserServices(context);
// const type = services.getTypeAtLocation(node);
// if (type.symbol?.name === "ObservableScope") { ... }
// This ObservableScope method call may be causing resource leaks.
context.report({
messageId: "scopeLeak",
loc: node.loc,
node,
});
}
},
};

View File

@@ -32,6 +32,9 @@ export default {
// https://github.com/webpro-nl/knip/issues/766
"@vector-im/compound-web",
"matrix-widget-api",
// Used by oxlint
"eslint-plugin-element-call",
"eslint-plugin-storybook",
],
ignoreExportsUsedInFile: true,
} satisfies KnipConfig;

View File

@@ -19,9 +19,9 @@
"serve": "vite preview",
"format": "oxfmt",
"format:check": "oxfmt --check; rc=$?; [[ $rc -ne 0 ]] && printf '\\033[46;30m INFO \\033[0m To fix, run: pnpm format\\n' >&2; exit $rc",
"lint": "pnpm lint:types && pnpm lint:eslint && pnpm lint:knip",
"lint:eslint": "eslint --max-warnings 0 src playwright",
"lint:eslint-fix": "eslint --max-warnings 0 src playwright --fix",
"lint": "pnpm lint:types && pnpm lint:oxlint && pnpm lint:knip",
"lint:oxlint": "oxlint src playwright",
"lint:oxlint-fix": "oxlint --fix src playwright",
"lint:knip": "knip",
"lint:types": "tsc",
"i18n": "npx i18next-cli extract",
@@ -60,7 +60,6 @@
"@storybook/addon-docs": "^10.3.6",
"@storybook/addon-vitest": "^10.3.6",
"@storybook/react-vite": "^10.3.6",
"@stylistic/eslint-plugin": "^3.0.0",
"@testing-library/dom": "^10.1.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.0",
@@ -75,8 +74,6 @@
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/sdp-transform": "^2.4.5",
"@typescript-eslint/eslint-plugin": "^8.31.0",
"@typescript-eslint/parser": "^8.31.0",
"@typescript-eslint/utils": "^8.61.0",
"@use-gesture/react": "^10.2.11",
"@vector-im/compound-design-tokens": "^10.0.0",
@@ -87,20 +84,8 @@
"@vitest/ui": "4.1.7",
"classnames": "^2.3.1",
"copy-to-clipboard": "^3.3.3",
"eslint": "^8.14.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-deprecate": "^0.9.0",
"eslint-plugin-element-call": "link:eslint",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^61.5.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-matrix-org": "2.1.0",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-rxjs": "^5.0.3",
"eslint-plugin-storybook": "^10.3.6",
"eslint-plugin-unicorn": "^56.0.0",
"fetch-mock": "11.1.5",
"global-jsdom": "^26.0.0",
"i18next": "^25.0.0",
@@ -117,6 +102,8 @@
"normalize.css": "^8.0.1",
"observable-hooks": "^4.2.3",
"oxfmt": "^0.55.0",
"oxlint": "^1.70.0",
"oxlint-tsgolint": "^0.23.0",
"pako": "^2.0.4",
"postcss": "^8.4.41",
"postcss-preset-env": "^10.0.0",
@@ -131,7 +118,6 @@
"sass": "^1.42.1",
"storybook": "^10.3.6",
"typescript": "^5.8.3",
"typescript-eslint-language-service": "^5.0.5",
"unique-names-generator": "^4.6.0",
"uuid": "^14.0.0",
"vaul": "^1.0.0",

2477
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -64,7 +64,7 @@
}
],
"semanticCommits": "disabled",
"ignoreDeps": ["eslint-plugin-matrix-org"],
"ignoreDeps": [],
"vulnerabilityAlerts": {
"schedule": ["at any time"],
"prHourlyLimit": 0,

View File

@@ -47,7 +47,7 @@ export const FullScreenView: FC<FullScreenViewProps> = ({
};
interface ErrorPageProps {
error: Error | unknown;
error: unknown;
widget: WidgetHelpers | null;
}

View File

@@ -253,7 +253,7 @@ describe("Test mappings", () => {
});
describe("Test select a device", () => {
it(`Switch to correct device `, () => {
it(`Switch to correct device`, () => {
withTestScheduler(({ cold, schedule, expectObservable, flush }) => {
const controlledAudioOutput = new AndroidControlledAudioOutput(
cold("a", { a: FULL_DEVICE_LIST }),

View File

@@ -170,7 +170,7 @@ export const createLocalMembership$ = ({
logger: parentLogger,
muteStates,
matrixRTCSession,
roomId: roomId,
roomId,
}: Props): {
/**
* This request to start audio and video tracks.

View File

@@ -102,7 +102,7 @@ export class RtcTransportAutoDiscovery {
const transportList = await doNetworkOperationWithRetry(async () =>
client._unstable_getRTCTransports(),
);
const first = transportList.filter(isLivekitTransportConfig)[0];
const first = transportList.find(isLivekitTransportConfig);
if (first) {
return first;
} else {

View File

@@ -131,9 +131,9 @@ export class TileStoreBuilder {
private numGridEntries = 0;
// A sparse array of grid entries which should be kept in the same spots as
// which they appeared in the previous grid
private readonly stationaryGridEntries: GridTileData[] = new Array(
this.prevGrid.length,
);
private readonly stationaryGridEntries: GridTileData[] = Array.from({
length: this.prevGrid.length,
});
// Grid entries which should now enter the visible section of the grid
private readonly visibleGridEntries: GridTileData[] = [];
// Grid entries which should now enter the invisible section of the grid

View File

@@ -128,8 +128,8 @@ export function getBasicRTCSession(
/**
* Construct a basic CallViewModel to test components that make use of it.
* @param members
* @param initialRtcMemberships
* @param members - Room members to include in the call.
* @param initialRtcMemberships - RTC memberships to start with.
* @returns
*/
export function getBasicCallViewModelEnvironment(

View File

@@ -38,7 +38,7 @@
"livekit-client/dist/src/proto/livekit_models_pb": [
"./node_modules/@livekit/protocol/src/gen/livekit_models_pb.d.ts"
]
},
}
// TODO: Enable the following options later.
// "forceConsistentCasingInFileNames": true,
@@ -48,8 +48,6 @@
// "noPropertyAccessFromIndexSignature": true,
// "noUncheckedIndexedAccess": true,
// "noUnusedParameters": true,
"plugins": [{ "name": "typescript-eslint-language-service" }]
},
"include": [
"./src/**/*.ts",