Enable more oxlint rules (#34189)

* 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

* Enable forwardRef oxlint rule

* Enable no-unused-vars oxlint rule

* Enable no-implied-eval oxlint rule

* Enable no-duplicate-type-constituents oxlint rule

* Enable explicit-length-check oxlint rule

* Enable prefer-number-properties oxlint rule

* Enable no-callback-in-promise oxlint rule

* Enable no-require-imports oxlint rule

* Remove disablement of most unicorn oxlint rules

* Enable no-conditional-tests oxlint rule

* Enable promise-valid-params oxlint rule

* Enable require-unicode-regexp oxlint rule

* Remove max-len comments as we use oxfmt for formatting

* Enable majority of oxlint `suspicious` rules

* Iterate

* Fix oxlint type-aware lint running without dependencies built
This commit is contained in:
Michael Telatynski
2026-07-29 08:26:07 +00:00
committed by GitHub
parent e8848e4746
commit bbec915fb0
178 changed files with 405 additions and 403 deletions
@@ -27,6 +27,7 @@ import styles from "./GroupedVirtualizedList.module.css";
* scrolled out of its render window, a tall section's header eventually unmounts and stops sticking;
* the pinned overlay (rendered outside the list) backstops that gap.
*/
// oxlint-disable-next-line no-restricted-properties
const StickyRowItem: Components["Item"] = React.forwardRef(function StickyRowItem(
// `item` and `context` are Virtuoso-injected props, not DOM attributes — pull them out so they
// aren't spread onto the div (`context` would otherwise render as `context="[object Object]"`).
@@ -52,7 +52,7 @@ function renderItemElement(
const isFocused = context.tabIndexKey === itemKey;
return (
<div className="mx_item" data-testid={`row-${index}`} tabIndex={isFocused ? 0 : -1} role="gridcell">
{item === SEPARATOR_ITEM ? "---" : (item as TestItem).name}
{item === SEPARATOR_ITEM ? "---" : item.name}
</div>
);
}
@@ -81,7 +81,7 @@ function renderClickableItemElement(
}}
onFocus={(e) => onFocus(item, e)}
>
{item === SEPARATOR_ITEM ? "---" : (item as TestItem).name}
{item === SEPARATOR_ITEM ? "---" : item.name}
</div>
);
}
@@ -448,7 +448,7 @@ describe.each<ListTestVariant>([flatVariant, groupedVariant])("$name", (variant)
mockIsItemFocusable.mockImplementation((item: TestItemWithSeparator) => {
if (item === SEPARATOR_ITEM) return false;
return (item as TestItem).isFocusable !== false;
return item.isFocusable !== false;
});
renderListWithHeight({ items: mixedItems });
@@ -476,7 +476,7 @@ describe.each<ListTestVariant>([flatVariant, groupedVariant])("$name", (variant)
mockIsItemFocusable.mockImplementation((item: TestItemWithSeparator) => {
if (item === SEPARATOR_ITEM) return false;
return (item as TestItem).isFocusable !== false;
return item.isFocusable !== false;
});
renderListWithHeight({ items: mixedItems });
@@ -709,7 +709,7 @@ describe.each<ListTestVariant>([flatVariant, groupedVariant])("$name", (variant)
style={{ height: `${ITEM_HEIGHT}px`, display: "block", width: "100%" }}
onFocus={(e) => onFocus(item, e)}
>
{item === SEPARATOR_ITEM ? "---" : (item as TestItem).name}
{item === SEPARATOR_ITEM ? "---" : item.name}
</button>
);
},
@@ -31,9 +31,9 @@ export class I18nApi implements II18nApi {
const langs: Record<string, Record<string, string>> = {};
for (const key in translations) {
for (const lang in translations[key as keyof Translations]) {
for (const lang in translations[key]) {
langs[lang] = langs[lang] || {};
langs[lang][key] = translations[key as keyof Translations]![lang];
langs[lang][key] = translations[key][lang];
}
}
@@ -325,6 +325,7 @@ export function replaceByRegexes(text: string, mapping: IVariables | Tags): stri
let replaced: SubstitutionValue;
// If substitution is a function, call it
// oxlint-disable-next-line unicorn/no-instanceof-builtins
if (mapping[regexpString] instanceof Function) {
replaced = ((mapping as Tags)[regexpString] as (...subs: string[]) => string)(...capturedGroups);
} else {
@@ -7,7 +7,7 @@
import React, { type HTMLAttributes } from "react";
import userEvent from "@testing-library/user-event";
import { act, fireEvent, render } from "@test-utils";
import { act, fireEvent, render, type RenderResult } from "@test-utils";
import { describe, expect, it, vi } from "vitest";
import {
@@ -38,7 +38,7 @@ const createButtonElement = (text: string): HTMLButtonElement => {
const renderToolbar = (
ui: React.ReactNode,
props: Partial<React.ComponentProps<typeof RovingTabIndexProvider>> = {},
): ReturnType<typeof render> => {
): RenderResult => {
return render(
<RovingTabIndexProvider {...props}>
{({ onKeyDownHandler }) => (
@@ -583,7 +583,7 @@ describe("RovingTabIndex", () => {
act(() => container.querySelectorAll("button")[0].focus());
const input = getByRole("textbox", { name: "Search input" });
act(() => (input as HTMLElement).focus());
act(() => input.focus());
fireEvent.keyDown(input, { key: "Tab" });
expectTabIndexes(container.querySelectorAll("button"), [-1, 0]);
@@ -30,8 +30,11 @@ import React, {
* @param el - The element being evaluated for native input behaviour.
* @returns `true` when the element should keep its own arrow-key handling.
*/
export function checkInputableElement(el: HTMLElement): boolean {
return el.matches('input:not([type="radio"]):not([type="checkbox"]), textarea, select, [contenteditable=true]');
export function checkInputableElement(el: EventTarget): boolean {
return (
el instanceof Element &&
el.matches('input:not([type="radio"]):not([type="checkbox"]), textarea, select, [contenteditable=true]')
);
}
/**
@@ -498,7 +501,7 @@ export const RovingTabIndexProvider: React.FC<RovingTabIndexProviderProps> = ({
const action = getAction(ev);
// Don't interfere with input default keydown behaviour
// but allow people to move focus from it with Tab.
const isInputTarget = !handleInputFields && checkInputableElement(ev.target as HTMLElement);
const isInputTarget = !handleInputFields && checkInputableElement(ev.target);
const { handled, focusNode } = isInputTarget
? getInputNavigationResult(action, context.state.nodes, context.state.activeNode, ev.shiftKey)
: getStandardNavigationResult(
@@ -121,7 +121,7 @@ describe("linkify-matrix", () => {
});
});
it("properly parses " + char + "_foonetic_xkcd:matrix.org", () => {
const test = "" + char + "_foonetic_xkcd:matrix.org";
const test = char + "_foonetic_xkcd:matrix.org";
expect(isLinkable(test)).toEqual(true);
const found = findLinksInString(test);
expect(found).toEqual([
@@ -166,7 +166,7 @@ describe("linkify-matrix", () => {
]);
});
it("accept " + char + "foo:bar.com", () => {
const test = "" + char + "foo:bar.com";
const test = char + "foo:bar.com";
expect(isLinkable(test)).toEqual(true);
const found = findLinksInString(test);
expect(found).toEqual([
@@ -181,7 +181,7 @@ describe("linkify-matrix", () => {
]);
});
it("accept " + char + "foo:com (mostly for (TLD|DOMAIN)+ mixing)", () => {
const test = "" + char + "foo:com";
const test = char + "foo:com";
expect(isLinkable(test)).toEqual(true);
const found = findLinksInString(test);
expect(found).toEqual([
@@ -196,7 +196,7 @@ describe("linkify-matrix", () => {
]);
});
it("accept repeated TLDs (e.g .org.uk)", () => {
const test = "" + char + "foo:bar.org.uk";
const test = char + "foo:bar.org.uk";
expect(isLinkable(test)).toEqual(true);
const found = findLinksInString(test);
expect(found).toEqual([
@@ -211,7 +211,7 @@ describe("linkify-matrix", () => {
]);
});
it("accept hyphens in name " + char + "foo-bar:server.com", () => {
const test = "" + char + "foo-bar:server.com";
const test = char + "foo-bar:server.com";
expect(isLinkable(test)).toEqual(true);
const found = findLinksInString(test);
expect(found).toEqual([
@@ -226,7 +226,7 @@ describe("linkify-matrix", () => {
]);
});
it("ignores trailing `:`", () => {
const test = "" + char + "foo:bar.com:";
const test = char + "foo:bar.com:";
expect(isLinkable(test)).toEqual(false);
const found = findLinksInString(test);
expect(found).toEqual([
@@ -241,7 +241,7 @@ describe("linkify-matrix", () => {
]);
});
it("accept :NUM (port specifier)", () => {
const test = "" + char + "foo:bar.com:2225";
const test = char + "foo:bar.com:2225";
expect(isLinkable(test)).toEqual(true);
const found = findLinksInString(test);
expect(found).toEqual([
@@ -256,7 +256,7 @@ describe("linkify-matrix", () => {
]);
});
it("ignores duplicate :NUM (double port specifier)", () => {
const test = "" + char + "foo:bar.com:2225:1234";
const test = char + "foo:bar.com:2225:1234";
expect(isLinkable(test)).toEqual(false);
const found = findLinksInString(test);
expect(found).toEqual([
@@ -271,7 +271,7 @@ describe("linkify-matrix", () => {
]);
});
it("ignores all the trailing :", () => {
const test = "" + char + "foo:bar.com::::";
const test = char + "foo:bar.com::::";
expect(isLinkable(test)).toEqual(false);
const found = findLinksInString(test);
expect(found).toEqual([
@@ -286,7 +286,7 @@ describe("linkify-matrix", () => {
]);
});
it("properly parses room alias with dots in name", () => {
const test = "" + char + "foo.asdf:bar.com::::";
const test = char + "foo.asdf:bar.com::::";
expect(isLinkable(test)).toEqual(false);
const found = findLinksInString(test);
expect(found).toEqual([
@@ -301,7 +301,7 @@ describe("linkify-matrix", () => {
]);
});
it("does not parse room alias with too many separators", () => {
const test = "" + char + "foo:::bar.com";
const test = char + "foo:::bar.com";
expect(isLinkable(test)).toEqual(false);
const found = findLinksInString(test);
expect(found).toEqual([
@@ -316,7 +316,7 @@ describe("linkify-matrix", () => {
]);
});
it("properly parses room alias with hyphen in domain part", () => {
const test = "" + char + "foo:bar.com-baz.com";
const test = char + "foo:bar.com-baz.com";
expect(isLinkable(test)).toEqual(true);
const found = findLinksInString(test);
expect(found).toEqual([
@@ -14,7 +14,7 @@ import { type ViewModel } from "./ViewModel";
* @param vm The view model to use
* @returns The current snapshot
*/
export function useViewModel<T>(vm: ViewModel<T, unknown>): T {
export function useViewModel<T>(vm: ViewModel<T>): T {
// We need to pass the same getSnapshot function as getServerSnapshot as this
// is used when making the HTML chat export.
return useSyncExternalStore(vm.subscribe, vm.getSnapshot, vm.getSnapshot);
@@ -190,14 +190,14 @@ export const RoomListItemView = memo(function RoomListItemView({
useEffect(() => {
if (isFocused) {
internalRef.current?.focus({ preventScroll: true } as FocusOptions);
internalRef.current?.focus({ preventScroll: true });
}
}, [isFocused]);
const onItemFocus = (e: React.FocusEvent<HTMLButtonElement>): void => {
onFocus(item.id, e);
// Only when focus enters the row from outside via the keyboard.
if (!e.currentTarget.contains(e.relatedTarget as Node | null) && e.currentTarget.matches(":focus-visible")) {
if (!e.currentTarget.contains(e.relatedTarget) && e.currentTarget.matches(":focus-visible")) {
setKeyboardActive(true);
}
};
@@ -207,10 +207,7 @@ export const RoomListItemView = memo(function RoomListItemView({
// (focus is then in the portaled popover, outside the row). The latter means that when the
// menu closes with Escape, the trigger is still revealed, so the popover's own focus
// restoration lands on it instead of dropping to <body>. Clear once focus leaves for good.
if (
!e.currentTarget.contains(e.relatedTarget as Node | null) &&
!e.currentTarget.querySelector('[data-state="open"]')
) {
if (!e.currentTarget.contains(e.relatedTarget) && !e.currentTarget.querySelector('[data-state="open"]')) {
setKeyboardActive(false);
}
};
@@ -174,7 +174,7 @@ export const RoomListSectionHeaderView = memo(function RoomListSectionHeaderView
const onHeaderFocus = (e: React.FocusEvent<HTMLButtonElement>): void => {
onFocus(id, e);
if (!e.currentTarget.contains(e.relatedTarget as Node | null) && e.currentTarget.matches(":focus-visible")) {
if (!e.currentTarget.contains(e.relatedTarget) && e.currentTarget.matches(":focus-visible")) {
setKeyboardActive(true);
}
};
@@ -183,10 +183,7 @@ export const RoomListSectionHeaderView = memo(function RoomListSectionHeaderView
// Keep it revealed while focus is on the menu button, and while the menu is open (focus is
// then in the portaled popover, outside the header). That way closing with Escape restores
// focus to the still-revealed trigger instead of dropping to <body>. Clear once focus leaves.
if (
!e.currentTarget.contains(e.relatedTarget as Node | null) &&
!e.currentTarget.querySelector('[data-state="open"]')
) {
if (!e.currentTarget.contains(e.relatedTarget) && !e.currentTarget.querySelector('[data-state="open"]')) {
setKeyboardActive(false);
}
};
@@ -19,7 +19,7 @@ export interface WidgetPipViewActions {
* The view model will handle navigating back to the associated room.
* @param ev The mouse event that triggered the back click.
*/
onBackClick: (ev: React.MouseEvent<Element, MouseEvent>) => void;
onBackClick: (ev: React.MouseEvent) => void;
/**
* The view model exposes the `<PersistentApp />` component via this action.
* `PersistentApp` is not available in shared components.
@@ -34,7 +34,7 @@ export interface WidgetPipViewActions {
* Action that needs to be called when the pip view starts to get dragged.
* @param ev The mouse event that triggered the drag start.
*/
onStartMoving: (ev: React.MouseEvent<Element, MouseEvent>) => void;
onStartMoving: (ev: React.MouseEvent) => void;
}
export interface WidgetPipViewSnapshot {
@@ -87,7 +87,7 @@ export function RoomAvatarEventView({
}
return (
<span className={classes} ref={ref as Ref<HTMLSpanElement>} {...eventPresentationAttributes}>
<span className={classes} ref={ref} {...eventPresentationAttributes}>
{_t(
"timeline|m.room.avatar|changed_img",
{ senderDisplayName: snapshot.senderDisplayName },
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX } from "react";
import React, { type JSX, type Ref, type HTMLAttributes } from "react";
import { IconButton, Text } from "@vector-im/compound-web";
import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
@@ -16,13 +16,13 @@ import styles from "./StatusPillView.module.css";
* Displays a user's status message in a pill format with a button that can be used
* to clear the status.
*/
export const StatusPillView = React.forwardRef<
HTMLDivElement,
export const StatusPillView: React.FC<
{
status: UserStatus;
clearStatus: () => void;
} & React.HTMLAttributes<HTMLDivElement>
>(function StatusPillView({ status, clearStatus, ...props }, ref): JSX.Element {
ref?: Ref<HTMLDivElement>;
} & HTMLAttributes<HTMLDivElement>
> = ({ status, clearStatus, ref, ...props }): JSX.Element => {
return (
<div ref={ref} {...props} className={styles.statusPill}>
<Text as="span" className={styles.menuStatusEmoji}>
@@ -41,4 +41,4 @@ export const StatusPillView = React.forwardRef<
</IconButton>
</div>
);
});
};
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX } from "react";
import React, { type JSX, type FC } from "react";
import { Text } from "@vector-im/compound-web";
import { type UserStatus } from "..";
@@ -14,12 +14,12 @@ import styles from "./StatusTextView.module.css";
/**
* Displays a user's status message and emoji in simple text format
*/
export const StatusTextView = React.forwardRef<
HTMLDivElement,
export const StatusTextView: FC<
{
status: UserStatus;
ref?: React.Ref<HTMLDivElement>;
} & React.HTMLAttributes<HTMLDivElement>
>(function StatusTextView({ status, ...props }, ref): JSX.Element {
> = function StatusTextView({ status, ref, ...props }): JSX.Element {
return (
<div ref={ref} {...props} className={styles.statusText}>
<Text as="span" className={styles.menuStatusEmoji}>
@@ -30,4 +30,4 @@ export const StatusTextView = React.forwardRef<
</Text>
</div>
);
});
};
@@ -61,7 +61,7 @@ const customRender = (ui: ReactElement, options: SharedRenderOptions = {}): Retu
return render(ui, {
...renderOptions,
wrapper: wrapWithTooltipProvider(wrapper, presentation) as RenderOptions["wrapper"],
}) as ReturnType<typeof render>;
});
};
// eslint-disable-next-line no-restricted-imports