Switch from eslint to oxlint (#34166)

* Fix type imports

* Fix jsdoc

* Fixup types

* Fix stray awaits on non-thenables

* Fixup imports

* Fix splats

* Fix this-context on callbacks

* Memoise react contexts

* Prefer find/flatMap

* Make oxlint happier about our React keys

* Avoid unsafe default function params

* Fixup jsdoc

* Fixup contexts

* Switch from eslint to oxlint

* Some oxlint-related tweaks

* Iterate

* Partial revert to defer some changes and shrink diff

* Iterate

* Add eslint-plugin-element-call and enable the copyright rule

* Set strictStorePkgContentCheck

* Iterate

* Make sonar happy

* Fix new lints
This commit is contained in:
Michael Telatynski
2026-07-10 09:47:41 +00:00
committed by GitHub
parent 549dd1b7a8
commit 15c85ec50f
176 changed files with 1294 additions and 2876 deletions
-99
View File
@@ -1,99 +0,0 @@
/*
Copyright 2025 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
module.exports = {
root: true,
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
extends: [
"plugin:matrix-org/react",
"plugin:matrix-org/a11y",
"plugin:matrix-org/typescript",
"plugin:matrix-org/react",
"plugin:storybook/recommended",
],
parserOptions: {
project: ["./tsconfig.json"],
tsconfigRootDir: __dirname,
},
env: {
browser: true,
node: true,
},
rules: {
// Bind or arrow functions in props causes performance issues (but we
// currently use them in some places).
// It's disabled here, but we should using it sparingly.
"react/jsx-no-bind": "off",
"react/jsx-key": ["error"],
"matrix-org/require-copyright-header": "error",
"react-compiler/react-compiler": "error",
"no-restricted-imports": [
"error",
{
paths: [
{
name: "react",
importNames: ["act"],
message: "Please use @test-utils instead.",
},
],
},
],
"@typescript-eslint/unbound-method": ["error", { ignoreStatic: true }],
"@typescript-eslint/explicit-function-return-type": [
"error",
{
allowExpressions: true,
},
],
// We're okay being explicit at the moment
// "@typescript-eslint/no-empty-interface": "off",
// We'd rather not do this but we do
// "@typescript-eslint/ban-ts-comment": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-empty-object-type": [
"error",
{
// We do this sometimes to brand interfaces
allowInterfaces: "with-single-extends",
},
],
"storybook/meta-satisfies-type": "error",
"react/forbid-elements": [
"error",
{
forbid: [
{ element: "h1", message: "Use Compound <Heading> instead" },
{ element: "h2", message: "Use Compound <Heading> instead" },
{ element: "h3", message: "Use Compound <Heading> instead" },
{ element: "h4", message: "Use Compound <Heading> instead" },
{ element: "h5", message: "Use Compound <Heading> instead" },
{ element: "h6", message: "Use Compound <Heading> instead" },
],
},
],
},
overrides: [
{
files: ["src/**/*.test.{ts,tsx}", "src/**/*.stories.tsx"],
rules: {
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/no-explicit-any": "off",
"react/forbid-elements": "off",
},
},
],
settings: {
react: {
version: "detect",
},
},
};
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import { Addon, types, useGlobals } from "storybook/manager-api";
import { type Addon, types, useGlobals } from "storybook/manager-api";
import { WithTooltip, IconButton, TooltipLinkList } from "storybook/internal/components";
import React from "react";
import { GlobeIcon } from "@storybook/icons";
@@ -19,7 +19,7 @@ import { TooltipProvider } from "@vector-im/compound-web";
import { EventPresentationProvider, type EventDensity, type EventLayout, I18nApi, I18nContext } from "../src";
import { setLanguage } from "../src/core/i18n/i18n";
import { StoryContext } from "storybook/internal/csf";
import { type StoryContext } from "storybook/internal/csf";
import { DragDropProvider } from "@dnd-kit/react";
import { PointerActivationConstraints, PointerSensor } from "@dnd-kit/dom";
+1 -16
View File
@@ -58,8 +58,7 @@
"storybook": "storybook dev -p 6007",
"build:storybook": "nx storybook",
"build:doc": "nx typedoc",
"lint": "pnpm lint:types && pnpm lint:js",
"lint:js": "eslint --max-warnings 0 src",
"lint": "pnpm lint:types",
"lint:types": "nx lint:types"
},
"dependencies": {
@@ -98,7 +97,6 @@
"@storybook/addon-vitest": "^10.1.11",
"@storybook/icons": "^2.0.0",
"@storybook/react-vite": "^10.0.7",
"@stylistic/eslint-plugin": "^5.7.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
@@ -106,22 +104,9 @@
"@types/lodash": "^4.17.20",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@typescript-eslint/eslint-plugin": "^8.53.1",
"@typescript-eslint/parser": "^8.53.1",
"@vector-im/compound-web": "catalog:",
"@vitejs/plugin-react": "catalog:",
"@vitest/browser-playwright": "catalog:",
"eslint": "8",
"eslint-config-google": "^0.14.0",
"eslint-plugin-deprecate": "^0.9.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-matrix-org": "^3.0.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-storybook": "^10.0.7",
"eslint-plugin-unicorn": "^56.0.0",
"oxfmt": "0.56.0",
"storybook": "^10.0.7",
"storybook-addon-vis": "^4.0.0",
@@ -18,7 +18,7 @@ const I18N_BASE_PATH = "./src/i18n/strings/";
const I18N_DEST = "storybook-static/i18n/";
// List of languages to include
const INCLUDE_LANGS = [...new Set([...fs.readdirSync(I18N_BASE_PATH)])]
const INCLUDE_LANGS = [...new Set(fs.readdirSync(I18N_BASE_PATH))]
.filter((fn) => fn.endsWith(".json"))
.map((f) => f.slice(0, -5));
@@ -68,7 +68,7 @@ export interface VirtualizedListProps<Item, Context> extends Omit<
/**
* Function to get the key to use for focusing an item.
* @param item - The item to get the key for
* @return The key to use for focusing the item
* @returns The key to use for focusing the item
*/
getItemKey: (item: Item) => string;
@@ -19,7 +19,7 @@
* you will end up with literal "<a>" in your output, rather than HTML. Note that you can also use variable
* substitution to insert React components, but you can't use it to translate text between tags.
*
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
* @returns a React <span> component if any non-strings were used in substitutions, otherwise a string
*/
import React from "react";
import { KEY_SEPARATOR } from "matrix-web-i18n";
@@ -204,7 +204,7 @@ export function lookupString(key: TranslationKey): string {
* @param {object} variables Variable substitutions, e.g { foo: 'bar' }
* @param {object} tags Tag substitutions e.g. { 'a': (sub) => <a>{sub}</a> }
*
* @return a React <span> component if any non-strings were used in substitutions
* @returns a React <span> component if any non-strings were used in substitutions
* or translation used a fallback locale, otherwise a string
*/
// eslint-next-line @typescript-eslint/naming-convention
@@ -243,7 +243,7 @@ export function sanitizeForTranslation(text: string): string {
* the substitution (e.g. return a React component). In case of a tag replacement, the function receives as
* the argument the text inside the element corresponding to the tag.
*
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
* @returns a React <span> component if any non-strings were used in substitutions, otherwise a string
*/
export function substitute(text: string, variables?: StringVariables): string;
export function substitute(text: string, variables?: RichVariables): React.ReactNode;
@@ -277,7 +277,7 @@ export function substitute(text: string, variables?: IVariables, tags?: Tags): s
* function which will receive as the argument the capture groups defined in the regexp. E.g.
* { 'Hello (.?) World': (sub) => sub.toUpperCase() }
*
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
* @returns a React <span> component if any non-strings were used in substitutions, otherwise a string
*/
export function replaceByRegexes(text: string, mapping: IVariables): string;
export function replaceByRegexes(text: string, mapping: Tags): React.ReactNode;
@@ -25,7 +25,7 @@ const Button = (props: HTMLAttributes<HTMLButtonElement>): React.JSX.Element =>
return <button {...props} onFocus={onFocus} tabIndex={isActive ? 0 : -1} ref={ref} />;
};
const checkTabIndexes = (buttons: NodeListOf<HTMLElement>, expectations: number[]): void => {
const expectTabIndexes = (buttons: NodeListOf<HTMLElement>, expectations: number[]): void => {
expect([...buttons].map((b) => b.tabIndex)).toStrictEqual(expectations);
};
@@ -89,16 +89,16 @@ describe("RovingTabIndex", () => {
</RovingTabIndexProvider>,
);
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
expectTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
act(() => container.querySelectorAll("button")[2].focus());
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
expectTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
act(() => container.querySelectorAll("button")[1].focus());
checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
expectTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
act(() => container.querySelectorAll("button")[1].blur());
checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
expectTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
rerender(
<RovingTabIndexProvider>
@@ -112,7 +112,7 @@ describe("RovingTabIndex", () => {
)}
</RovingTabIndexProvider>,
);
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0, -1]);
expectTabIndexes(container.querySelectorAll("button"), [-1, -1, 0, -1]);
rerender(
<RovingTabIndexProvider>
@@ -125,7 +125,7 @@ describe("RovingTabIndex", () => {
)}
</RovingTabIndexProvider>,
);
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
expectTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
});
it("provides a ref to the dom element", () => {
@@ -165,10 +165,10 @@ describe("RovingTabIndex", () => {
</RovingTabIndexProvider>,
);
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
expectTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
act(() => container.querySelectorAll("button")[2].focus());
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
expectTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
});
describe("reducer functions as expected", () => {
@@ -390,22 +390,22 @@ describe("RovingTabIndex", () => {
);
act(() => container.querySelectorAll("button")[0].focus());
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
expectTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
await userEvent.keyboard("[ArrowDown]");
checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
expectTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
await userEvent.keyboard("[ArrowDown]");
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
expectTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
await userEvent.keyboard("[ArrowUp]");
checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
expectTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
await userEvent.keyboard("[ArrowUp]");
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
expectTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
await userEvent.keyboard("[ArrowUp]");
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
expectTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
});
it("handles left/right arrow keys when handleLeftRight=true", async () => {
@@ -420,10 +420,10 @@ describe("RovingTabIndex", () => {
act(() => container.querySelectorAll("button")[0].focus());
await userEvent.keyboard("[ArrowRight]");
checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
expectTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
await userEvent.keyboard("[ArrowLeft]");
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
expectTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
});
it("moves between multiple toolbar containers in one roving group", async () => {
@@ -446,15 +446,15 @@ describe("RovingTabIndex", () => {
const buttons = container.querySelectorAll("button");
act(() => buttons[1].focus());
checkTabIndexes(buttons, [-1, 0, -1, -1]);
expectTabIndexes(buttons, [-1, 0, -1, -1]);
await userEvent.keyboard("[ArrowRight]");
expect(buttons[2]).toHaveFocus();
checkTabIndexes(buttons, [-1, -1, 0, -1]);
expectTabIndexes(buttons, [-1, -1, 0, -1]);
await userEvent.keyboard("[ArrowLeft]");
expect(buttons[1]).toHaveFocus();
checkTabIndexes(buttons, [-1, 0, -1, -1]);
expectTabIndexes(buttons, [-1, 0, -1, -1]);
});
it("handles Home, End, and loop across multiple toolbar containers", async () => {
@@ -477,19 +477,19 @@ describe("RovingTabIndex", () => {
const buttons = container.querySelectorAll("button");
act(() => buttons[2].focus());
checkTabIndexes(buttons, [-1, -1, 0, -1]);
expectTabIndexes(buttons, [-1, -1, 0, -1]);
await userEvent.keyboard("[End]");
expect(buttons[3]).toHaveFocus();
checkTabIndexes(buttons, [-1, -1, -1, 0]);
expectTabIndexes(buttons, [-1, -1, -1, 0]);
await userEvent.keyboard("[ArrowRight]");
expect(buttons[0]).toHaveFocus();
checkTabIndexes(buttons, [0, -1, -1, -1]);
expectTabIndexes(buttons, [0, -1, -1, -1]);
await userEvent.keyboard("[Home]");
expect(buttons[0]).toHaveFocus();
checkTabIndexes(buttons, [0, -1, -1, -1]);
expectTabIndexes(buttons, [0, -1, -1, -1]);
});
it("handles Home and End when handleHomeEnd=true", async () => {
@@ -503,13 +503,13 @@ describe("RovingTabIndex", () => {
);
act(() => container.querySelectorAll("button")[1].focus());
checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
expectTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
await userEvent.keyboard("[End]");
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
expectTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
await userEvent.keyboard("[Home]");
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
expectTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
});
it("loops when handleLoop=true", async () => {
@@ -524,10 +524,10 @@ describe("RovingTabIndex", () => {
act(() => container.querySelectorAll("button")[2].focus());
await userEvent.keyboard("[ArrowDown]");
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
expectTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
await userEvent.keyboard("[ArrowUp]");
checkTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
expectTabIndexes(container.querySelectorAll("button"), [-1, -1, 0]);
});
it("uses a custom getAction mapper", async () => {
@@ -552,7 +552,7 @@ describe("RovingTabIndex", () => {
await userEvent.keyboard("j");
expect(getAction).toHaveBeenCalled();
checkTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
expectTabIndexes(container.querySelectorAll("button"), [-1, 0, -1]);
});
it("handles input fields when handleInputFields=true", () => {
@@ -569,7 +569,7 @@ describe("RovingTabIndex", () => {
const input = getByRole("textbox", { name: "Search input" });
fireEvent.keyDown(input, { key: "ArrowDown" });
checkTabIndexes(container.querySelectorAll("button"), [-1, 0]);
expectTabIndexes(container.querySelectorAll("button"), [-1, 0]);
});
it("moves from an input field with Tab when handleInputFields=false", () => {
@@ -586,7 +586,7 @@ describe("RovingTabIndex", () => {
act(() => (input as HTMLElement).focus());
fireEvent.keyDown(input, { key: "Tab" });
checkTabIndexes(container.querySelectorAll("button"), [-1, 0]);
expectTabIndexes(container.querySelectorAll("button"), [-1, 0]);
});
it("stops provider processing when onKeyDown prevents default", () => {
@@ -606,7 +606,7 @@ describe("RovingTabIndex", () => {
fireEvent.keyDown(container.querySelector('[role="toolbar"]')!, { key: "ArrowDown" });
expect(onKeyDown).toHaveBeenCalled();
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
expectTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
});
it("calls scrollIntoView if specified", async () => {
@@ -620,7 +620,7 @@ describe("RovingTabIndex", () => {
);
act(() => container.querySelectorAll("button")[0].focus());
checkTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
expectTabIndexes(container.querySelectorAll("button"), [0, -1, -1]);
const button = container.querySelectorAll("button")[1];
const mock = vi.spyOn(button, "scrollIntoView");
@@ -153,7 +153,7 @@ export interface LinkedTextOptions {
* Generates a linkifyjs options object that is reasonably paired down
* to just the essentials required for an Element client.
*
* @return A `linkifyjs` `Opts` object. Used by `linkifyString` and `linkifyHtml
* @returns A `linkifyjs` `Opts` object. Used by `linkifyString` and `linkifyHtml
* @see {@link linkifyHtml}
* @see {@link linkifyString}
*/
@@ -9,8 +9,12 @@ import React, { type JSX, useEffect } from "react";
import { fn } from "storybook/test";
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { Room } from "./RoomListItemView";
import { RoomListItemView, type RoomListItemViewSnapshot, type RoomListItemViewActions } from "./RoomListItemView";
import {
type Room,
RoomListItemView,
type RoomListItemViewSnapshot,
type RoomListItemViewActions,
} from "./RoomListItemView";
import { useMockedViewModel } from "../../../../core/viewmodel";
import { withViewDocs } from "../../../../../.storybook/withViewDocs";
import { defaultSnapshot } from "./default-snapshot";
@@ -17,9 +17,10 @@ import {
FlatVirtualizedList,
getContainerAccessibleProps,
type VirtualizedListContext,
GroupedVirtualizedList,
type GroupedVirtualizedListProps,
} from "../../core/VirtualizedList";
import type { RoomListViewSnapshot, RoomListViewModel } from "../RoomListView";
import { GroupedVirtualizedList, type GroupedVirtualizedListProps } from "../../core/VirtualizedList";
import { RoomListSectionHeaderView, RoomListStickySectionHeaderView } from "./RoomListSectionHeaderView";
import { RoomListSectionHeaderDragOverlayView } from "./RoomListSectionHeaderDragOverlayView";
import { RoomListItemWrapper } from "./RoomListItemWrapper";
@@ -26,7 +26,7 @@ export interface UploadButtonViewSnapshot {
}
export interface UploadButtonViewActions {
onUploadOptionSelected(type: string): void;
onUploadOptionSelected(this: void, type: string): void;
}
/**
@@ -40,7 +40,7 @@ export interface RoomAvatarEventViewActions {
/**
* Invoked when the user opens the avatar image.
*/
onAvatarClick(): void;
onAvatarClick(this: void): void;
}
export type RoomAvatarEventViewModel = ViewModel<RoomAvatarEventViewSnapshot, RoomAvatarEventViewActions>;
@@ -19,7 +19,6 @@ import {
type ImageBodyViewSnapshot,
} from "./ImageBodyView";
import imageSrc from "../../../../../../static/image-body/install-spinner.png";
import thumbnailSrc from "../../../../../../static/image-body/install-spinner.png";
import animatedGifSrc from "../../../../../../static/image-body/install-spinner.gif";
const demoBlurhash = "LEHV6nWB2yk8pyo0adR*.7kCMdnj";
const imageBodyViewStateOptions = [ImageBodyViewState.ERROR, ImageBodyViewState.HIDDEN, ImageBodyViewState.READY];
@@ -29,6 +28,8 @@ const imageBodyViewPlaceholderOptions = [
ImageBodyViewPlaceholder.BLURHASH,
];
const thumbnailSrc = imageSrc;
type ImageBodyViewProps = ImageBodyViewSnapshot &
ImageBodyViewActions & {
className?: string;
@@ -28,6 +28,8 @@ type SharedRenderOptions = RenderOptions & {
presentation?: Partial<EventPresentation>;
};
const i18nApi = new I18nApi();
const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"], presentation?: Partial<EventPresentation>) => {
return ({ children }: { children: React.ReactNode }) => {
const resolvedPresentation: EventPresentation | undefined = presentation
@@ -43,12 +45,12 @@ const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"], presentation
if (Wrapper) {
return (
<I18nContext.Provider value={new I18nApi()}>
<I18nContext.Provider value={i18nApi}>
<Wrapper>{content}</Wrapper>
</I18nContext.Provider>
);
} else {
return <I18nContext.Provider value={new I18nApi()}>{content}</I18nContext.Provider>;
return <I18nContext.Provider value={i18nApi}>{content}</I18nContext.Provider>;
}
};
};
+2 -1
View File
@@ -18,7 +18,8 @@
"strict": true,
"paths": {
"@test-utils": ["./src/test/utils/index"]
}
},
"noEmit": true
},
"include": ["./src/**/*.ts", "./src/**/*.tsx", ".storybook/*.ts", ".storybook/*.tsx"],
"references": [{ "path": "./tsconfig.node.json" }]
+1 -1
View File
@@ -10,7 +10,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
import { storybookVis } from "storybook-addon-vis/vitest-plugin";
import { playwright, PlaywrightProviderOptions } from "@vitest/browser-playwright";
import { playwright, type PlaywrightProviderOptions } from "@vitest/browser-playwright";
import { nodePolyfills } from "vite-plugin-node-polyfills";
import rootConfig from "../../vitest.config";