/* Copyright 2026 Element Creations Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import { defineConfig } from "oxlint"; function buildRestrictedPropertiesOptions( properties: string[], message: string, ): { object?: string; property: string; message: string }[] { return properties.map((prop) => { const [object, property] = prop.split("."); return { object: object === "*" ? undefined : object, property, message, }; }); } const defaultRestrictedProperties = [ { object: "window", property: "setImmediate", message: "Use setTimeout instead" }, ...buildRestrictedPropertiesOptions(["React.forwardRef", "*.forwardRef", "forwardRef"], "Use ref props instead."), ] as const; const defaultRestrictedGlobals = [ { name: "setImmediate", message: "Use setTimeout instead.", }, ]; export default defineConfig({ $schema: "./node_modules/oxlint/configuration_schema.json", plugins: [ "eslint", "typescript", "unicorn", "import", "jsdoc", "node", "promise", "vitest", "react", "react-perf", "jsx-a11y", ], jsPlugins: ["eslint-plugin-element-call"], categories: { correctness: "error", perf: "error", suspicious: "error", restriction: "warn", }, options: { typeAware: true, reportUnusedDisableDirectives: "warn", maxWarnings: 0, denyWarnings: true, }, env: { es6: true, }, ignorePatterns: [ "**/lib", "**/dist", "**/node_modules", "**/coverage", "apps/web/src/vector/modernizr.cjs", // Legacy skinning file that some people might still have "apps/web/src/component-index.js", // Auto-generated files "apps/web/src/modules.ts", "apps/web/src/modules.js", // Test result files "**/test-results", "**/html-report", // Shared components generated files "/packages/shared-components/dist/", "/packages/shared-components/src/i18n/i18nKeys.d.ts", "/packages/shared-components/typedoc/", ], settings: { "jsdoc": { tagNamePreference: { remark: "remarks", privateRemarks: "privateRemarks", experimental: "experimental", deprecated: "deprecated", typeParam: "typeParam", defaultValue: "defaultValue", packageDocumentation: "packageDocumentation", alpha: "alpha", knipignore: "knipignore", resolves: "resolves", }, }, "vitest": { typecheck: true, }, "jsx-a11y": { components: { Button: "button", IconButton: "button", AccessibleButton: "button", RovingAccessibleButton: "button", ContextMenuButton: "button", ContextMenuTooltipButton: "button", }, // polymorphicPropName: "as", // Would be good to enable in the future }, "react": { componentWrapperFunctions: ["withMatrixClientHOC"], }, }, rules: { "no-constant-condition": ["error", { checkLoops: "allExceptWhileTrue" }], "typescript/unbound-method": ["error", { ignoreStatic: true }], "typescript/no-empty-object-type": [ "error", { allowInterfaces: "with-single-extends", }, ], "prefer-const": ["error", { destructuring: "all" }], "import/first": "error", "new-cap": "error", "typescript/no-unsafe-function-type": "error", "react/rules-of-hooks": "error", "no-extend-native": "error", "no-inner-declarations": "error", "typescript/no-unnecessary-type-constraint": "error", "jsx-filename-extension": ["error", { allow: "as-needed", extensions: ["tsx"] }], // Tune restriction ruleset "no-undefined": "off", "typescript/use-unknown-in-catch-callback-variable": "off", "typescript/promise-function-async": "off", "typescript/no-non-null-assertion": "off", "typescript/no-invalid-void-type": "off", "typescript/no-explicit-any": "off", "typescript/no-import-type-side-effects": "off", "typescript/no-dynamic-delete": "off", "typescript/explicit-module-boundary-types": "off", "no-param-reassign": "off", "no-use-before-define": "off", "class-methods-use-this": "off", "no-plusplus": "off", "no-default-export": "off", "no-console": "off", "complexity": "off", "no-void": "off", "no-empty-function": "off", "default-case": "off", "no-implicit-globals": "off", "no-bitwise": "off", "no-empty": "off", "no-eq-null": "off", "promise/catch-or-return": "off", "node/no-process-env": "off", // We enable this for src in overrides "unicorn/no-array-reduce": "off", "unicorn/no-anonymous-default-export": "off", "import/no-relative-parent-imports": "off", "import/unambiguous": "off", "import/no-cycle": "off", "jsdoc/empty-tags": "off", "vitest/require-test-timeout": "off", "react/jsx-no-literals": "off", "react/prefer-function-component": "off", "react/forbid-component-props": "off", "react/no-multi-comp": "off", "react/no-danger": "off", "react/only-export-components": "off", "react/no-react-children": "off", "react/no-clone-element": "off", "unicorn/no-instanceof-array": "error", "no-restricted-globals": ["error", ...defaultRestrictedGlobals], "no-restricted-properties": ["error", ...defaultRestrictedProperties], "import/no-duplicates": ["error"], "element-call/copyright-header": [ "error", "/*\nCopyright %%CURRENT_YEAR%% Element Creations Ltd.\n\nSPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial\nPlease see LICENSE in the repository root for full details.\n*/\n\n", ], // Allow the use of underscore to show args are not used. // This is helpful for seeing that a function implements // an interface but won't be using one of it's arguments. "no-unused-vars": ["error", { args: "none", ignoreRestSiblings: true }], // Require method signatures to be explicit to help make signature changes more obvious in review "typescript/explicit-function-return-type": [ "error", { allowExpressions: true, }, ], // Prevent invalid non-type re-exports of types, these can cause downstream build failures "typescript/consistent-type-exports": ["error"], // Prevent unnecessary runtime dependencies between files "typescript/consistent-type-imports": ["error", { fixStyle: "inline-type-imports" }], "jsx-a11y/control-has-associated-label": [ "error", { labelAttributes: ["label", "value"], depth: 3, }, ], // Disable some perf rules "no-await-in-loop": "off", // Disable some opinionated rules "unicorn/switch-case-braces": "off", "sort-keys": "off", "typescript/require-array-sort-compare": "off", "no-extra-boolean-cast": "off", // These would be nice to enable at some point "unicorn/prefer-set-has": "off", "unicorn/prefer-string-slice": "off", "unicorn/prefer-number-properties": "off", "unicorn/prefer-at": "off", "unicorn/no-new-array": "off", "unicorn/no-array-for-each": "off", "unicorn/explicit-length-check": "off", "unicorn/catch-error-name": "off", "require-unicode-regexp": "off", "typescript/restrict-template-expressions": "off", "typescript/no-redundant-type-constituents": "off", "typescript/no-useless-default-assignment": "off", "typescript/no-duplicate-type-constituents": "off", "typescript/no-floating-promises": "off", "typescript/no-implied-eval": "off", "typescript/no-misused-spread": "off", "promise/valid-params": "off", "react-perf/jsx-no-new-function-as-prop": "off", "react-perf/jsx-no-new-object-as-prop": "off", "react-perf/jsx-no-jsx-as-prop": "off", "jsx-a11y/prefer-tag-over-role": "off", "jsx-a11y/no-autofocus": "off", "react/no-children-prop": "off", "react-perf/jsx-no-new-array-as-prop": "off", "react/no-did-update-set-state": "off", "react/no-did-mount-set-state": "off", "jsx-a11y/no-static-element-interactions": "off", "jsx-a11y/no-noninteractive-element-interactions": "off", "jsx-a11y/media-has-caption": "off", "jsx-a11y/no-noninteractive-element-to-interactive-role": "off", "jsx-a11y/aria-activedescendant-has-tabindex": "off", // Rules within `suspicious` we do not yet comply with but probably should "typescript/no-unsafe-type-assertion": "off", "no-shadow": "off", "unicorn/consistent-function-scoping": "off", "typescript/consistent-return": "off", "typescript/no-unsafe-enum-comparison": "off", "typescript/no-unnecessary-type-conversion": "off", "typescript/no-unnecessary-type-parameters": "off", "typescript/no-unnecessary-boolean-literal-compare": "off", "react/no-unstable-nested-components": "off", "unicorn/no-array-sort": "off", "unicorn/no-array-reverse": "off", "unicorn/prefer-add-event-listener": "off", "no-underscore-dangle": "off", "import/no-named-as-default": "off", "import/no-unassigned-import": "off", "import/no-named-as-default-member": "off", "promise/always-return": "off", "preserve-caught-error": "off", "react/react-in-jsx-scope": "off", "unicorn/require-post-message-target-origin": "off", }, overrides: [ { files: ["apps/web/src/**/*", "{packages,modules}/*/src/**/*"], rules: { "no-restricted-globals": [ "error", defaultRestrictedGlobals, { name: "Buffer", message: "Buffer is not available in the web.", }, ], "node/no-process-env": "error", "unicorn/prefer-node-protocol": "off", }, }, { files: ["{packages,apps,modules}/*/src/**/*"], rules: { // Enable this in the future, it has a lot of false positives right now // "react/react-compiler": "error", }, }, { files: ["packages/shared-components/**/*"], rules: { "no-restricted-imports": [ "error", { paths: [ { name: "react", importNames: ["act"], message: "Please use @test-utils instead.", }, { name: "@testing-library/react", message: "Please use @test-utils instead", }, ], }, ], // This would be good to apply globally in the future "react/forbid-elements": [ "error", { forbid: [ { element: "h1", message: "Use Compound instead" }, { element: "h2", message: "Use Compound instead" }, { element: "h3", message: "Use Compound instead" }, { element: "h4", message: "Use Compound instead" }, { element: "h5", message: "Use Compound instead" }, { element: "h6", message: "Use Compound instead" }, ], }, ], }, }, { files: [ "apps/desktop/src/**/*", "packages/playwright-common/src/**/*", "**/scripts/**/*", "apps/web/module_system/**/*", "apps/web/webpack.config.ts", ], rules: { "no-restricted-globals": "off", "unicorn/prefer-node-protocol": "error", // These files can use envvars "node/no-process-env": "off", // They do not depend on js-sdk for access to TypedEventEmitter so disable this rule "no-restricted-imports": "off", // They can use process.exit "unicorn/no-process-exit": "off", // They can use top level await "node/no-top-level-await": "off", }, }, { files: ["apps/web/**/*"], rules: { "no-restricted-properties": [ "error", ...defaultRestrictedProperties, ...buildRestrictedPropertiesOptions( ["window.innerHeight", "window.innerWidth", "window.visualViewport"], "Use UIStore to access window dimensions instead.", ), ...buildRestrictedPropertiesOptions( ["*.mxcUrlToHttp", "*.getHttpUriForMxc"], "Use Media helper instead to centralise access for customisation.", ), ], // Ban matrix-js-sdk/src imports in favour of matrix-js-sdk/src/matrix imports to prevent unleashing hell. // Ban compound-design-tokens raw svg imports in favour of their React component counterparts "no-restricted-imports": [ "error", { paths: [ { name: "events", message: "Please use TypedEventEmitter instead", }, { name: "react", importNames: ["forwardRef"], message: "Use ref props instead.", }, { name: "@testing-library/react", message: "Please use jest-matrix-react instead", }, { name: "matrix-js-sdk", message: "Please use matrix-js-sdk/src/matrix instead", }, { name: "matrix-js-sdk/", message: "Please use matrix-js-sdk/src/matrix instead", }, { name: "matrix-js-sdk/src", message: "Please use matrix-js-sdk/src/matrix instead", }, { name: "matrix-js-sdk/src/", message: "Please use matrix-js-sdk/src/matrix instead", }, { name: "matrix-js-sdk/src/index", message: "Please use matrix-js-sdk/src/matrix instead", }, { name: "emojibase-regex", message: "This regex doesn't actually test for emoji. See the docs at https://emojibase.dev/docs/regex/ and prefer our own EMOJI_REGEX from HtmlUtils.", }, ], patterns: [ { group: [ "matrix-js-sdk/src/**", "!matrix-js-sdk/src/matrix", "!matrix-js-sdk/src/crypto-api", "!matrix-js-sdk/src/types", "!matrix-js-sdk/src/testing", "!matrix-js-sdk/src/utils/**", "matrix-js-sdk/src/utils/internal/**", "matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**", // XXX: Temporarily allow these as they are not available via the main export "!matrix-js-sdk/src/logger", "!matrix-js-sdk/src/errors", "!matrix-js-sdk/src/utils", "!matrix-js-sdk/src/version-support", "!matrix-js-sdk/src/randomstring", "!matrix-js-sdk/src/sliding-sync", "!matrix-js-sdk/src/browser-index", "!matrix-js-sdk/src/feature", "!matrix-js-sdk/src/NamespacedValue", "!matrix-js-sdk/src/ReEmitter", "!matrix-js-sdk/src/event-mapper", "!matrix-js-sdk/src/interactive-auth", "!matrix-js-sdk/src/secret-storage", "!matrix-js-sdk/src/room-hierarchy", "!matrix-js-sdk/src/rendezvous", "!matrix-js-sdk/src/indexeddb-worker", "!matrix-js-sdk/src/pushprocessor", "!matrix-js-sdk/src/extensible_events_v1", "!matrix-js-sdk/src/extensible_events_v1/PollStartEvent", "!matrix-js-sdk/src/extensible_events_v1/PollResponseEvent", "!matrix-js-sdk/src/extensible_events_v1/PollEndEvent", "!matrix-js-sdk/src/extensible_events_v1/InvalidEventError", "!matrix-js-sdk/src/oidc", "!matrix-js-sdk/src/oidc/discovery", "!matrix-js-sdk/src/oidc/authorize", "!matrix-js-sdk/src/oidc/validate", "!matrix-js-sdk/src/oidc/error", "!matrix-js-sdk/src/oidc/register", "!matrix-js-sdk/src/webrtc", "!matrix-js-sdk/src/webrtc/call", "!matrix-js-sdk/src/webrtc/callFeed", "!matrix-js-sdk/src/webrtc/mediaHandler", "!matrix-js-sdk/src/webrtc/callEventTypes", "!matrix-js-sdk/src/webrtc/callEventHandler", "!matrix-js-sdk/src/webrtc/groupCallEventHandler", "!matrix-js-sdk/src/models", "!matrix-js-sdk/src/models/read-receipt", "!matrix-js-sdk/src/models/relations-container", "!matrix-js-sdk/src/models/related-relations", "!matrix-js-sdk/src/matrixrtc", ], message: "Please use matrix-js-sdk/src/matrix instead", }, { group: ["emojibase-regex/emoji*"], message: "This regex doesn't actually test for emoji. See the docs at https://emojibase.dev/docs/regex/ and prefer our own EMOJI_REGEX from HtmlUtils.", }, { group: ["@vector-im/compound-design-tokens/icons/*"], message: "Please use @vector-im/compound-design-tokens/assets/web/icons/* instead", }, { group: ["**/packages/shared-components/**", "../packages/shared-components/**"], message: "Please use @element-hq/web-shared-components", }, ], }, ], }, }, { files: [ "apps/*/playwright/**/*", "packages/playwright-common/**/*", "modules/*/e2e/**/*", "modules/playwright/**/*", ], rules: { // This is a common pattern for Playwright fixtures "no-empty-pattern": "off", // Playwright has a `use` method for fixtures which confuses this rule "react-hooks/rules-of-hooks": "off", }, }, { files: [ "{packages,apps,modules}/*/src/**/*.{test,stories}.{ts,tsx}", "{packages,apps,modules}/*/src/{tests,test}/*.{ts,tsx}", "{packages,apps,modules}/*/src/**/__mocks__/*.{ts,tsx}", "{packages,apps,modules}/*/{test,playwright,e2e}/**/*", "{packages,apps,modules}/*/playwright.config.ts", "{packages,apps,modules}/*/.storybook/**/*", "{packages,apps,modules}/*/__mocks__/**/*", "packages/playwright-common/src/**/*", ], rules: { // Tests can be linted a little more flexibly // We don't need super strict typing in test utilities "no-import-assign": "off", "no-unsafe-optional-chaining": "off", "typescript/no-empty-object-type": "off", "typescript/unbound-method": "off", "typescript/no-floating-promises": "off", "vitest/require-mock-type-parameters": "off", "vitest/no-disabled-tests": "off", "vitest/no-conditional-expect": "off", "vitest/warn-todo": "off", "vitest/require-to-throw-message": "off", "vitest/prefer-snapshot-hint": "off", "vitest/no-standalone-expect": [ "error", { additionalTestBlockFunctions: ["beforeAll", "beforeEach"], }, ], "vitest/expect-expect": [ "error", { assertFunctionNames: ["expect*", "*Test", "assert*", "test*Factory"], }, ], "jsdoc/check-tag-names": "off", "typescript/explicit-function-return-type": "off", "typescript/explicit-module-boundary-types": "off", "typescript/explicit-member-accessibility": "off", "no-proto": "off", // Disable a11y rules for components in tests "jsx-a11y/role-has-required-aria-props": "off", "react/button-has-type": "off", "jsx-a11y/interactive-supports-focus": "off", "jsx-a11y/no-static-element-interactions": "off", "jsx-a11y/anchor-ambiguous-text": "off", "jsx-a11y/click-events-have-key-events": "off", "jsx-a11y/media-has-caption": "off", "jsx-a11y/no-noninteractive-element-to-interactive-role": "off", "jsx-a11y/role-supports-aria-props": "off", "react/jsx-no-constructed-context-values": "off", "react/no-array-index-key": "off", "react/forbid-elements": "off", "typescript/no-extraneous-class": "off", "no-new": "off", "react/iframe-missing-sandbox": "off", "promise/no-promise-in-callback": "off", // This would be good to enable in the future "typescript/await-thenable": "off", "promise/no-callback-in-promise": "off", // This rule requires strictNullChecks enabled "typescript/no-unnecessary-boolean-literal-compare": "off", "typescript/no-unnecessary-type-assertion": "off", }, }, { files: ["{packages,apps,modules}/*/src/**/*.stories.{ts,tsx}"], jsPlugins: ["eslint-plugin-element-call", "eslint-plugin-storybook"], rules: { "storybook/meta-satisfies-type": "error", "storybook/default-exports": "error", "storybook/hierarchy-separator": "error", "storybook/no-redundant-story-name": "error", "storybook/no-renderer-packages": "error", "storybook/no-stories-of": "error", "storybook/story-exports": "error", "storybook/use-storybook-expect": "error", "storybook/use-storybook-testing-library": "error", "storybook/no-uninstalled-addons": "error", "jsx-filename-extension": ["error", { allow: "always", extensions: ["tsx"] }], }, }, { files: ["**/*.{cjs,js}"], rules: { "typescript/no-require-imports": "off", "import/no-commonjs": "off", "unicorn/prefer-module": "off", }, }, { files: ["apps/web/test/**/*-test.*"], rules: { // Jest is still CommonJS "unicorn/prefer-module": "off", }, }, { files: ["**/*.d.ts"], rules: { "unicorn/require-module-specifiers": "off", }, }, ], });