/** * oxlint config for `npm run lint` (see contributingGuides/LINTING.md). Rules oxlint can't run * natively come from the JS plugins in config/oxlint/plugins, which load the ESLint plugins' own * rule code. */ /* eslint-disable @typescript-eslint/naming-convention -- the rule names in this file are quoted object keys */ /* eslint-disable import/no-extraneous-dependencies -- oxlint is a devDependency; this file only runs while linting */ import {defineConfig} from 'oxlint'; // An oxlint override replaces a rule's options instead of merging them, so every block that sets // `no-restricted-imports` restates the full list, built from these. const restrictedImportPaths = [ { name: 'react-native', importNames: [ 'useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', 'Text', 'ScrollView', 'ActivityIndicator', 'Animated', 'findNodeHandle', 'InteractionManager', ], message: "\nFor 'useWindowDimensions', please use '@src/hooks/useWindowDimensions' instead.\nFor 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from '@components/Pressable' instead.\nFor 'StatusBar', please use '@libs/StatusBar' instead.\nFor 'Text', please use '@components/Text' instead.\nFor 'ScrollView', please use '@components/ScrollView' instead.\nFor 'ActivityIndicator', please use '@components/ActivityIndicator' instead.\nFor 'Animated', please use 'Animated' from 'react-native-reanimated' instead.\nFor 'InteractionManager', please use afterTransition callbacks on Navigation/KeyboardUtils or other alternatives. See contributingGuides/INTERACTION_MANAGER.md.", }, { name: 'react-native-gesture-handler', importNames: ['TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight'], message: "Please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from '@components/Pressable' instead.", }, { // ESLint bans this through @typescript-eslint/no-restricted-imports, which oxlint lacks; its // core rule accepts allowTypeImports instead. name: 'react-native-onyx/dist/OnyxUtils', message: 'OnyxUtils is not a sanctioned way to read Onyx data. Use useOnyx() from @hooks/useOnyx in render paths, or a short-lived Onyx connection without a view for non-render logic. Type-only imports are still allowed.', allowTypeImports: true, }, {name: 'awesome-phonenumber', importNames: ['parsePhoneNumber'], message: "Please use '@libs/PhoneNumber' instead."}, { name: 'react-native-safe-area-context', importNames: ['useSafeAreaInsets', 'SafeAreaConsumer', 'SafeAreaInsetsContext'], message: "Please use 'useSafeAreaInsets' from '@src/hooks/useSafeAreaInset' and/or 'SafeAreaConsumer' from '@components/SafeAreaConsumer' instead.", }, {name: 'react', importNames: ['CSSProperties'], message: "Please use 'ViewStyle', 'TextStyle', 'ImageStyle' from 'react-native' instead."}, {name: 'react', importNames: ['forwardRef'], message: 'forwardRef is deprecated. Please use ref as a prop instead. See: contributingGuides/STYLE.md#forwarding-refs'}, {name: '@styles/index', importNames: ['default', 'defaultStyles'], message: 'Do not import styles directly. Please use the `useThemeStyles` hook instead.'}, {name: '@styles/utils', importNames: ['default', 'DefaultStyleUtils'], message: 'Do not import StyleUtils directly. Please use the `useStyleUtils` hook instead.'}, {name: '@styles/theme', importNames: ['default', 'defaultTheme'], message: 'Do not import themes directly. Please use the `useTheme` hook instead.'}, {name: '@styles/theme/illustrations', message: 'Do not import theme illustrations directly. Please use the `useThemeIllustrations` hook instead.'}, {name: 'date-fns/locale', message: "Do not import 'date-fns/locale' directly. Please use the submodule import instead, like 'date-fns/locale/en-US'."}, { name: 'expensify-common', importNames: ['Device', 'ExpensiMark'], message: "\nFor 'Device', do not import it directly, it's known to make VSCode's IntelliSense crash. Please import the desired module from `expensify-common/Device` instead.\nFor 'ExpensiMark', please use '@libs/Parser' instead.", }, {name: 'lodash/memoize', message: "Please use '@src/libs/memoize' instead."}, {name: 'lodash', importNames: ['memoize'], message: "Please use '@src/libs/memoize' instead."}, {name: 'lodash/isEqual', message: "Please use 'deepEqual' from 'fast-equals' instead."}, {name: 'lodash', importNames: ['isEqual'], message: "Please use 'deepEqual' from 'fast-equals' instead."}, {name: 'react-native-onyx', importNames: ['useOnyx'], message: "Please use '@hooks/useOnyx' instead."}, {name: '@src/utils/findNodeHandle', message: "Do not use 'findNodeHandle' as it is no longer supported on web."}, ]; const restrictedImportPatterns = [ {group: ['**/assets/animations/**/*.json'], message: "Do not import animations directly. Please use the '@components/LottieAnimations' import instead."}, {group: ['@styles/theme/themes/**'], message: 'Do not import themes directly. Please use the `useTheme` hook instead.'}, {group: ['@styles/utils/**', '!@styles/utils/FontUtils', '!@styles/utils/types'], message: 'Do not import style util functions directly. Please use the `useStyleUtils` hook instead.'}, {group: ['@styles/theme/illustrations/themes/**'], message: 'Do not import theme illustrations directly. Please use the `useThemeIllustrations` hook instead.'}, { group: ['**/NumberWithSymbolForm'], importNames: ['default'], message: 'NumberWithSymbolForm is deprecated. Use AmountFormField or KeypadAmountEditor instead. See contributingGuides/NUMBER_WITH_SYMBOL_FORM_MIGRATION.md.', }, { group: ['**/MoneyRequestAmountInput'], importNames: ['default'], message: 'MoneyRequestAmountInput is deprecated. Use AmountFormField or KeypadAmountEditor instead. See contributingGuides/NUMBER_WITH_SYMBOL_FORM_MIGRATION.md.', }, ]; const typescriptRestrictedImportPatterns = [ ...restrictedImportPatterns, { group: ['**/computeReportName', '**/libs/computeReportName'], message: 'Do not import computeReportName. Use getReportName instead, which properly uses derived report attributes.', }, { group: ['**/PolicyUtils', '**/libs/PolicyUtils'], importNames: ['isPaidGroupPolicy'], message: 'isPaidGroupPolicy is billing/paid-only (Collect/Control). For workspace feature gating use isGroupPolicy so free group plans like Submit are not excluded. If this is genuinely a billing/paid-only check, keep it and disable this line with a reason.', }, { group: ['**/ReportUtils', '**/libs/ReportUtils'], importNames: ['isPaidGroupPolicy', 'isPaidGroupPolicyExpenseReport'], message: 'isPaidGroupPolicy / isPaidGroupPolicyExpenseReport are billing/paid-only. For feature gating use isReportInGroupPolicy / isGroupPolicyExpenseReport so Submit workspaces are not excluded. If this is genuinely billing/paid-only, keep it and disable this line with a reason.', }, ]; // Two patterns where ESLint has one: ESLint's gitignore semantics let a directory name cover its // contents, but oxlint matches the path exactly, so the second catches `.../HeaderWithBackButton/types`. const restrictedHeaderImportPatterns = [ {group: ['**/HeaderWithBackButton'], message: 'HeaderWithBackButton is being migrated to the composed Header. Please use `@components/Header` instead for new usages.'}, {group: ['**/HeaderWithBackButton/**'], message: 'HeaderWithBackButton is being migrated to the composed Header. Please use `@components/Header` instead for new usages.'}, ]; // Headless CLI cannot use useTheme; email charts always render with the light theme. const victoryChartRendererRestrictedImportPaths = restrictedImportPaths.filter((entry) => entry.name !== '@styles/theme'); const victoryChartRendererRestrictedImportPatterns = typescriptRestrictedImportPatterns.filter((entry) => !entry.group.includes('@styles/theme/themes/**')); /** * Pressables and the components that forward their `sentryLabel` prop to one, so the `no-restricted-syntax` rules below can require the label. * The lists come from every JSX component whose props accept `sentryLabel`. Inputs (e.g. TextInput) and wrappers that fall back to a default * label (e.g. FormProvider, RadioButton) are left out. Don't add new components here: give them a required `sentryLabel` prop instead, which * TypeScript checks at every usage. Once a component's usages all have a label, make its prop required and remove it from these lists. * Names of compound components are written with dots, e.g. `MenuItem.Root`. */ const SENTRY_LABEL_PRESSABLES = [ 'PressableWithoutFeedback', 'PressableWithFeedback', 'PressableWithDelayToggle', 'PressableWithoutFocus', 'PressableWithSecondaryInteraction', 'AnimatedPressableWithoutFeedback', 'AnimatedPressableWithSecondaryInteraction', ]; /** Wrappers that only need a label when they get a press handler, since without one they can render read-only (e.g. a MenuItem without onPress). */ const SENTRY_LABEL_PRESS_HANDLER_WRAPPERS = [ 'AvatarButtonWithIcon', 'BaseMiniContextMenuItem', 'Button', 'ButtonDisabledWhenOffline', 'ButtonWithDropdownMenu', 'CardDetailsActionButton', 'CardSectionButton', 'Checkbox', 'ConfirmationPage.PrimaryButton', 'ConfirmationPage.SecondaryButton', 'ConfirmationPageButtonBase', 'ConfirmationPagePrimaryButton', 'ContextMenuItem', 'FeatureTraining.HelpButton', 'FocusableMenuItem', 'FormAlertWithSubmitButton', 'HighlightableMenuItem', 'MenuItem', 'MenuItem.Root', 'MenuItemAction', 'MenuItemAvatarNavigation', 'MenuItemField', 'MenuItemFieldHTML', 'MenuItemNavigation', 'MenuItemRoot', 'MenuItemSectionRoot', 'MenuItemWithLabel', 'MenuItemWithTopDescription', 'PopoverMenu.Item', 'PopoverMenu.RadioItem', 'RowMenuItem', 'SelectionButton', 'Table.Row', 'TableRow', 'TabSelectorItem', 'ToggleDistanceUnitButton', 'ViewButton', ]; /** Wrappers that handle the press themselves (e.g. ThreeDotsMenu opens its menu), so they always need a label. */ const SENTRY_LABEL_SELF_PRESS_WRAPPERS = ['AvatarWithImagePicker', 'BookTravelButton', 'FeatureTraining.ConfirmButton', 'PopoverMenu.Sub.Trigger', 'ThreeDotsMenu']; /** Builds an esquery selector list matching JSX elements named e.g. `Button` or `MenuItem.Root`. */ function jsxElementNamesSelector(names: string[]): string { return names .map((name) => { const parts = name.split('.'); if (parts.length === 1) { return `[name.name="${name}"]`; } return parts .map((part, index) => { const path = ['name', ...Array(parts.length - 1 - index).fill('object')].join('.'); return index === 0 ? `[${path}.name="${part}"]` : `[${path}.property.name="${part}"]`; }) .join(''); }) .join(', '); } /** `no-restricted-syntax` entries for all code. */ const restrictedSyntax = [ {selector: 'TSEnumDeclaration', message: "Please don't declare enums, use union types instead."}, { selector: 'CallExpression[callee.object.name="React"][callee.property.name="forwardRef"]', message: 'forwardRef is deprecated. Please use ref as a prop instead. See: contributingGuides/STYLE.md#forwarding-refs', }, { selector: 'ImportNamespaceSpecifier[parent.source.value=/^@libs/]', message: 'Namespace imports from @libs are not allowed. Use named imports instead. Example: import { method } from "@libs/module"', }, { selector: 'ImportNamespaceSpecifier[parent.source.value=/^@userActions/]', message: 'Namespace imports from @userActions are not allowed. Use named imports instead. Example: import { action } from "@userActions/module"', }, { selector: 'ImportNamespaceSpecifier[parent.source.value=/^\\.\\./]', message: 'Namespace imports from parent directories are not allowed. Use named imports instead. Example: import { method } from "../libs/module"', }, { selector: 'ImportNamespaceSpecifier[parent.source.value=/^\\./]', message: 'Namespace imports from sibling modules are not allowed. Use named imports instead. Example: import { method } from "./libs/module"', }, {selector: 'LabeledStatement', message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.'}, {selector: 'WithStatement', message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize. It is also deprecated.'}, ]; /** `no-restricted-syntax` entries that require a `sentryLabel` on pressables and pressable wrappers. */ const sentryLabelRestrictedSyntax = [ { selector: `JSXOpeningElement:matches(${jsxElementNamesSelector(SENTRY_LABEL_PRESSABLES)}):not(:has(> JSXAttribute[name.name="sentryLabel"]))`, message: 'Pressable components must have a sentryLabel from CONST.SENTRY_LABEL. See contributingGuides/SENTRY_LABEL.md.', }, { // Spread props may carry the label, which a selector can't see, so those are skipped. selector: `JSXOpeningElement:matches(${jsxElementNamesSelector(SENTRY_LABEL_PRESS_HANDLER_WRAPPERS)}):has(> JSXAttribute[name.name=/^on(Press|LongPress|SecondaryInteraction|Select|Submit)$/]):not(:has(> JSXAttribute[name.name="sentryLabel"])):not(:has(> JSXSpreadAttribute))`, message: 'Button, MenuItem and other pressable wrappers that handle a press must have a sentryLabel from CONST.SENTRY_LABEL. See contributingGuides/SENTRY_LABEL.md.', }, { // Spread props may carry the label, which a selector can't see, so those are skipped. selector: `JSXOpeningElement:matches(${jsxElementNamesSelector(SENTRY_LABEL_SELF_PRESS_WRAPPERS)}):not(:has(> JSXAttribute[name.name="sentryLabel"])):not(:has(> JSXSpreadAttribute))`, message: 'ThreeDotsMenu, DropdownButton and other wrappers that handle their own press must have a sentryLabel from CONST.SENTRY_LABEL. See contributingGuides/SENTRY_LABEL.md.', }, ]; export default defineConfig({ plugins: ['typescript', 'import', 'jsx-a11y', 'unicorn', 'react'], // oxlint reserves `react`, `import` and `jsdoc` for its native ports, so jsPlugins use these aliases. jsPlugins: [ {name: 'rulesdir', specifier: './config/oxlint/plugins/expensify-rules.mjs'}, {name: 'report-name-utils', specifier: './config/oxlint/plugins/report-name-utils.mjs'}, {name: 'core', specifier: './config/oxlint/plugins/core-rules.mjs'}, {name: 'hosted', specifier: './config/oxlint/plugins/hosted-rules.mjs'}, {name: 'rc', specifier: './config/oxlint/plugins/rc-rules.mjs'}, ], // Every rule is listed explicitly; no category turns rules on by default. categories: {correctness: 'off'}, options: {typeAware: true}, env: {builtin: true, es2026: true, node: true, browser: true, audioworklet: true, jest: true}, globals: {__DEV__: 'readonly'}, settings: { react: {version: '19.2.3'}, propWrapperFunctions: ['forbidExtraProps', 'exact', 'Object.freeze'], // Read by the hosted eslint-plugin-import rules; without them `no-useless-path-segments` and // `no-relative-packages` cannot resolve .ts/.tsx targets and stay silent. Same values as ESLint's. 'import/resolver': {node: {extensions: ['.mjs', '.js', '.jsx', '.json', '.ts', '.tsx', '.d.ts', '.cts', '.mts']}}, 'import/extensions': ['.js', '.mjs', '.jsx', '.ts', '.tsx', '.d.ts', '.cts', '.mts'], }, ignorePatterns: [ '!**/.storybook', '!**/.github', '.github/actions/**/index.js', '**/*.config.js', '**/*.config.mjs', '**/node_modules/**/*', '**/dist/**/*', 'server/**/dist/**', 'server/victory-chart-renderer/.dev/**', '.lint-reports/**/*', 'android/**/build/**/*', 'docs/vendor/**/*', 'docs/assets/**/*', 'web/gtm.js', '**/.expo/**/*', '**/.rock/**/*', '**/.yalc/**/*', 'src/libs/SearchParser/searchParser.js', 'src/libs/SearchParser/autocompleteParser.js', 'help/_scripts/**/*', 'modules/ExpensifyNitroUtils/nitrogen/**/*', 'Mobile-Expensify/**/*', '**/vendor', 'modules/group-ib-fp/**/*', 'web/snippets/gib.js', 'web/cloudflare-worker/**/*', 'src/languages/de.ts', 'src/languages/el.ts', 'src/languages/es.ts', 'src/languages/fr.ts', 'src/languages/it.ts', 'src/languages/ja.ts', 'src/languages/nl.ts', 'src/languages/pl.ts', 'src/languages/pt-BR.ts', 'src/languages/zh-hans.ts', ], rules: { 'array-callback-return': ['error', {allowImplicit: true}], 'block-scoped-var': 'error', curly: ['error', 'all'], 'default-case': ['error', {commentPattern: '^no default$'}], 'default-case-last': 'error', 'default-param-last': 'error', eqeqeq: ['error', 'always', {null: 'ignore'}], 'grouped-accessor-pairs': 'error', 'guard-for-in': 'error', 'max-classes-per-file': ['error', 1], 'no-caller': 'error', 'no-case-declarations': 'error', 'no-constructor-return': 'error', 'no-else-return': ['error', {allowElseIf: false}], 'no-empty-function': ['error', {allow: ['arrowFunctions', 'functions', 'methods']}], 'no-empty-pattern': 'error', 'no-eval': 'error', 'no-extend-native': 'error', 'no-extra-bind': 'error', 'no-extra-label': 'error', 'no-fallthrough': 'error', 'no-global-assign': ['error', {exceptions: []}], 'no-implied-eval': 'error', 'no-iterator': 'error', 'no-labels': ['error', {allowLoop: false, allowSwitch: false}], 'no-lone-blocks': 'error', 'no-loop-func': 'error', 'no-multi-str': 'error', 'no-new': 'error', 'no-new-func': 'error', 'no-new-wrappers': 'error', 'no-nonoctal-decimal-escape': 'error', 'no-param-reassign': [ 'error', {props: true, ignorePropertyModificationsFor: ['acc', 'accumulator', 'e', 'ctx', 'context', 'req', 'request', 'res', 'response', '$scope', 'staticContext']}, ], 'no-proto': 'error', 'no-restricted-properties': [ 'error', {object: 'arguments', property: 'callee', message: 'arguments.callee is deprecated'}, {object: 'global', property: 'isFinite', message: 'Please use Number.isFinite instead'}, {object: 'self', property: 'isFinite', message: 'Please use Number.isFinite instead'}, {object: 'window', property: 'isFinite', message: 'Please use Number.isFinite instead'}, {object: 'global', property: 'isNaN', message: 'Please use Number.isNaN instead'}, {object: 'self', property: 'isNaN', message: 'Please use Number.isNaN instead'}, {object: 'window', property: 'isNaN', message: 'Please use Number.isNaN instead'}, {property: '__defineGetter__', message: 'Please use Object.defineProperty instead.'}, {property: '__defineSetter__', message: 'Please use Object.defineProperty instead.'}, {object: 'Math', property: 'pow', message: 'Use the exponentiation operator (**) instead.'}, ], 'no-script-url': 'error', 'no-self-assign': ['error', {props: true}], 'no-self-compare': 'error', 'no-sequences': 'error', 'no-throw-literal': 'error', 'no-unused-expressions': ['error', {allowShortCircuit: false, allowTernary: false, allowTaggedTemplates: false}], 'no-unused-labels': 'error', 'no-useless-catch': 'error', 'no-useless-concat': 'error', 'no-useless-escape': 'error', 'no-useless-return': 'error', 'no-void': 'error', 'no-with': 'error', 'prefer-promise-reject-errors': ['error', {allowEmptyReject: true}], 'prefer-regex-literals': ['error', {disallowRedundantWrapping: true}], radix: 'error', yoda: 'error', 'for-direction': 'error', 'getter-return': ['error', {allowImplicit: true}], 'no-async-promise-executor': 'error', 'no-await-in-loop': 'error', 'no-compare-neg-zero': 'error', 'no-cond-assign': ['error', 'always'], 'no-console': ['error', {allow: ['debug', 'error']}], 'no-constant-condition': 'error', 'no-control-regex': 'error', 'no-debugger': 'error', 'no-dupe-else-if': 'error', 'no-dupe-keys': 'error', 'no-duplicate-case': 'error', 'no-empty': 'error', 'no-empty-character-class': 'error', 'no-ex-assign': 'error', 'no-extra-boolean-cast': 'error', 'no-func-assign': 'error', 'no-import-assign': 'error', 'no-inner-declarations': 'error', 'no-invalid-regexp': 'error', 'no-irregular-whitespace': 'error', 'no-loss-of-precision': 'error', 'no-misleading-character-class': 'error', 'no-obj-calls': 'error', 'no-promise-executor-return': 'error', 'no-prototype-builtins': 'error', 'no-regex-spaces': 'error', 'no-setter-return': 'error', 'no-sparse-arrays': 'error', 'no-template-curly-in-string': 'error', 'no-unexpected-multiline': 'error', 'no-unreachable': 'error', 'no-unsafe-finally': 'error', 'no-unsafe-negation': 'error', 'no-unsafe-optional-chaining': ['error', {disallowArithmeticOperators: true}], 'no-useless-backreference': 'error', 'use-isnan': 'error', 'valid-typeof': ['error', {requireStringLiterals: true}], 'arrow-body-style': ['error', 'as-needed', {requireReturnForObjectLiteral: false}], 'constructor-super': 'error', 'no-class-assign': 'error', 'no-const-assign': 'error', 'no-dupe-class-members': 'error', 'no-restricted-imports': [ 'error', { paths: [ {name: 'react-native', importNames: ['Button', 'Text', 'TextInput', 'Picker'], message: 'Please use an Expensify component from src/components/ instead.'}, {name: 'react-native', importNames: ['SafeAreaView'], message: 'Please use SafeAreaView from react-native-safe-area-context'}, ], }, ], 'no-this-before-super': 'error', 'no-useless-computed-key': 'error', 'no-useless-constructor': 'error', 'no-useless-rename': ['error', {ignoreDestructuring: false, ignoreImport: false, ignoreExport: false}], 'no-var': 'error', 'object-shorthand': ['error', 'always', {ignoreConstructors: false, avoidQuotes: true}], 'prefer-arrow-callback': ['error', {allowNamedFunctions: false, allowUnboundThis: true}], 'prefer-const': ['error', {destructuring: 'any', ignoreReadBeforeAssign: true}], 'prefer-numeric-literals': 'error', 'prefer-rest-params': 'error', 'prefer-spread': 'error', 'prefer-template': 'error', 'require-yield': 'error', 'symbol-description': 'error', 'import/no-named-as-default': 'error', 'import/no-named-as-default-member': 'error', 'import/no-mutable-exports': 'error', 'import/no-amd': 'error', 'import/first': 'error', 'import/no-duplicates': 'error', 'import/extensions': ['error', 'ignorePackages', {js: 'never', mjs: 'never', jsx: 'never', ts: 'never', tsx: 'never', cts: 'never', mts: 'never'}], 'import/newline-after-import': 'error', // The native port anchors on a different line than ESLint's; `hosted/prefer-default-export` runs instead. 'import/prefer-default-export': 'off', 'import/no-absolute-path': 'error', 'import/no-webpack-loader-syntax': 'error', 'import/no-named-default': 'error', 'import/no-self-import': 'error', 'import/no-cycle': 'error', 'rulesdir/context-provider-split-values': 'error', 'rulesdir/no-acc-spread-in-reduce': 'error', 'rulesdir/no-api-in-views': 'error', 'rulesdir/no-direct-personal-details-list': 'error', 'rulesdir/no-api-side-effects-method': 'error', 'rulesdir/no-beta-handler': 'error', 'rulesdir/no-call-actions-from-actions': 'error', 'rulesdir/no-deep-equal-in-memo': 'error', 'rulesdir/no-default-id-values': 'error', 'rulesdir/no-default-props': 'error', 'rulesdir/no-direct-pre-insert-fullscreen-under-rhp': 'error', 'rulesdir/no-inline-named-export': 'error', 'rulesdir/no-multiple-api-calls': 'error', 'rulesdir/no-multiple-onyx-in-file': 'error', 'rulesdir/no-negated-variables': 'error', 'rulesdir/no-onyx-connect': 'error', 'rulesdir/no-raw-typography': 'error', 'rulesdir/no-layout-spacing-conditional': 'error', 'rulesdir/no-set-or-map-return-in-useOnyx-selector': 'error', 'rulesdir/no-thenable-actions-in-views': 'error', 'rulesdir/no-unstable-hook-defaults': 'error', 'rulesdir/no-use-state-initializer-functions': 'error', 'rulesdir/no-useless-compose': 'error', 'rulesdir/prefer-actions-set-data': 'error', 'rulesdir/prefer-early-return': 'error', 'rulesdir/prefer-localization': 'error', 'rulesdir/prefer-narrow-hook-dependencies': ['error', {stableObjectPatterns: ['[Ss]tyles?$', '^theme', '[Ii]cons?$']}], 'rulesdir/prefer-onyx-connect-in-libs': 'error', 'rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth': 'error', 'rulesdir/prefer-type-fest': 'error', 'rulesdir/require-a11y-disable-justification': 'error', 'rulesdir/require-live-region-for-status-updates': 'error', 'rulesdir/no-onyx-loading-status': 'error', 'rulesdir/require-locale-for-localized-date-format': 'error', 'rulesdir/no-unsafe-onyx-read': 'error', 'rulesdir/no-onyx-get-snapshot-key': 'error', 'rulesdir/use-double-negation-instead-of-boolean': 'error', // Needs the type of the `&&` left operand, and jsPlugins get no type info. 'rulesdir/boolean-conditional-rendering': 'off', // Needs typeChecker.isArrayType; `unicorn/prefer-at` stands in. 'rulesdir/prefer-at': 'off', // ESLint's copy needs the type checker; a type-free rewrite runs for src/**/*.ts(x) below. 'rulesdir/prefer-locale-compare-from-context': 'off', // config/oxlint/reactCompilerGate.mjs drops its reports in files React Compiler memoizes. 'rulesdir/no-inline-useOnyx-selector': 'error', 'no-array-constructor': 'error', 'no-bitwise': 'error', 'no-lonely-if': 'error', 'no-multi-assign': ['error'], 'no-nested-ternary': 'error', 'no-underscore-dangle': ['error', {allow: ['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'], allowAfterThis: false, allowAfterSuper: false, enforceInMethodNames: true}], 'no-unneeded-ternary': ['error', {defaultAssignment: false}], 'operator-assignment': ['error', 'always'], 'prefer-exponentiation-operator': 'error', 'prefer-object-spread': 'error', 'unicorn/prefer-set-has': 'error', 'unicorn/no-array-for-each': 'error', 'unicorn/prefer-array-find': 'error', 'unicorn/prefer-set-size': 'error', 'unicorn/prefer-string-replace-all': 'error', // Stands in for `rulesdir/prefer-at`. Default options only: `checkAllIndexAccess` makes it unusable. 'unicorn/prefer-at': 'error', 'no-delete-var': 'error', 'no-label-var': 'error', 'no-restricted-globals': [ 'error', {name: 'isFinite', message: 'Use Number.isFinite instead https://github.com/airbnb/javascript#standard-library--isfinite'}, {name: 'isNaN', message: 'Use Number.isNaN instead https://github.com/airbnb/javascript#standard-library--isnan'}, 'addEventListener', 'blur', 'close', 'closed', 'confirm', 'defaultStatus', 'defaultstatus', 'event', 'external', 'find', 'focus', 'frameElement', 'frames', 'history', 'innerHeight', 'innerWidth', 'length', 'location', 'locationbar', 'menubar', 'moveBy', 'moveTo', 'name', 'onblur', 'onerror', 'onfocus', 'onload', 'onresize', 'onunload', 'open', 'opener', 'opera', 'outerHeight', 'outerWidth', 'pageXOffset', 'pageYOffset', 'parent', 'print', 'removeEventListener', 'resizeBy', 'resizeTo', 'screen', 'screenLeft', 'screenTop', 'screenX', 'screenY', 'scroll', 'scrollbars', 'scrollBy', 'scrollTo', 'scrollX', 'scrollY', 'self', 'status', 'statusbar', 'stop', 'toolbar', 'top', ], 'no-shadow': 'error', 'no-shadow-restricted-names': 'error', 'no-unused-vars': ['error', {vars: 'all', args: 'after-used', ignoreRestSiblings: true}], 'no-use-before-define': ['error', {functions: true, classes: true, variables: true}], 'react/jsx-boolean-value': ['error', 'never', {always: []}], 'react/jsx-no-duplicate-props': 'error', 'react/jsx-no-undef': 'error', 'react/jsx-pascal-case': ['error', {allowAllCaps: true, ignore: []}], 'react/no-danger': 'error', 'react/no-did-update-set-state': 'error', 'react/no-will-update-set-state': 'error', 'react/no-is-mounted': 'error', 'react/no-string-refs': 'error', 'react/no-unknown-property': 'error', 'react/prefer-es6-class': ['error', 'always'], 'react/self-closing-comp': 'error', 'react/jsx-no-target-blank': ['error', {enforceDynamicLinks: 'always'}], 'react/jsx-filename-extension': 'off', 'react/jsx-no-comment-textnodes': 'error', 'react/no-render-return-value': 'error', 'react/no-find-dom-node': 'error', 'react/no-array-index-key': 'error', 'react/void-dom-elements-no-children': 'error', 'react/no-redundant-should-component-update': 'error', 'react/button-has-type': ['error', {button: true, submit: true, reset: false}], 'react/no-this-in-sfc': 'error', 'react/no-unsafe': ['error', {checkAliases: true}], 'react/jsx-fragments': ['error', 'syntax'], 'react/state-in-constructor': ['error', 'always'], 'react/jsx-no-script-url': ['error', [{name: 'Link', props: ['to']}]], 'react/jsx-no-useless-fragment': 'error', // The native port also fires inside Jest mock factories; `hosted/function-component-definition` runs instead. 'react/function-component-definition': 'off', // ESLint's own rule runs instead, as `hosted/jsx-no-constructed-context-values`. oxlint's version // reports on the JSX provider rather than the value's declaration, and the React Compiler gate // only works on JS plugin rules. 'react/jsx-no-constructed-context-values': 'off', 'react/no-unstable-nested-components': 'error', 'react/no-namespace': 'error', 'react/style-prop-object': 'error', 'react/no-unescaped-entities': 'error', 'react/no-children-prop': 'error', 'jsx-a11y/alt-text': ['error', {elements: ['img', 'object', 'area', 'input[type="image"]'], img: [], object: [], area: [], 'input[type="image"]': []}], 'jsx-a11y/anchor-has-content': 'error', 'jsx-a11y/anchor-is-valid': ['error', {components: ['Link'], specialLink: ['to'], aspects: ['noHref', 'invalidHref', 'preferButton']}], 'jsx-a11y/aria-activedescendant-has-tabindex': 'error', 'jsx-a11y/aria-props': 'error', 'jsx-a11y/aria-proptypes': 'error', 'jsx-a11y/aria-role': ['error', {ignoreNonDOM: false}], 'jsx-a11y/aria-unsupported-elements': 'error', 'jsx-a11y/control-has-associated-label': [ 'error', { labelAttributes: ['label'], controlComponents: [], ignoreElements: ['audio', 'canvas', 'embed', 'input', 'textarea', 'tr', 'video'], ignoreRoles: ['grid', 'listbox', 'menu', 'menubar', 'radiogroup', 'row', 'tablist', 'toolbar', 'tree', 'treegrid'], depth: 5, }, ], 'jsx-a11y/heading-has-content': ['error', {components: ['']}], 'jsx-a11y/html-has-lang': 'error', 'jsx-a11y/iframe-has-title': 'error', 'jsx-a11y/img-redundant-alt': 'error', 'jsx-a11y/interactive-supports-focus': 'error', 'jsx-a11y/label-has-associated-control': ['error', {labelComponents: [], labelAttributes: [], controlComponents: [], assert: 'either', depth: 25}], 'jsx-a11y/lang': 'error', 'jsx-a11y/media-has-caption': ['error', {audio: [], video: [], track: []}], 'jsx-a11y/mouse-events-have-key-events': 'error', 'jsx-a11y/no-access-key': 'error', 'jsx-a11y/no-autofocus': ['error', {ignoreNonDOM: true}], 'jsx-a11y/no-distracting-elements': ['error', {elements: ['marquee', 'blink']}], 'jsx-a11y/no-interactive-element-to-noninteractive-role': ['error', {tr: ['none', 'presentation']}], 'jsx-a11y/no-noninteractive-element-interactions': ['error', {handlers: ['onClick', 'onMouseDown', 'onMouseUp', 'onKeyPress', 'onKeyDown', 'onKeyUp']}], 'jsx-a11y/no-noninteractive-element-to-interactive-role': [ 'error', { ul: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'], ol: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'], li: ['menuitem', 'option', 'row', 'tab', 'treeitem'], table: ['grid'], td: ['gridcell'], }, ], 'jsx-a11y/no-noninteractive-tabindex': ['error', {tags: [], roles: ['tabpanel']}], 'jsx-a11y/no-redundant-roles': 'error', 'jsx-a11y/no-static-element-interactions': ['error', {handlers: ['onClick', 'onMouseDown', 'onMouseUp', 'onKeyPress', 'onKeyDown', 'onKeyUp']}], 'jsx-a11y/role-has-required-aria-props': 'error', 'jsx-a11y/role-supports-aria-props': 'error', 'jsx-a11y/scope': 'error', 'jsx-a11y/tabindex-no-positive': 'error', 'react/rules-of-hooks': 'error', 'typescript/dot-notation': ['error', {allowKeywords: true}], 'typescript/no-unsafe-assignment': 'error', 'typescript/no-unsafe-argument': 'error', 'typescript/no-unsafe-return': 'error', 'typescript/no-unsafe-call': 'error', 'typescript/no-unsafe-member-access': 'error', 'react/jsx-key': 'error', 'core/no-restricted-syntax': ['error', ...restrictedSyntax, ...sentryLabelRestrictedSyntax], // ESLint's own rule, not oxlint's: the React Compiler gate spots the "wrap it in useCallback/useMemo" // reports by ESLint's wording, which oxlint's version does not use, and it only works on JS plugin rules. 'hosted/exhaustive-deps': 'error', // Same React Compiler checks as ESLint's react-hooks/* rules, reported under those ids, but run through // oxc's Rust port of the compiler (config/oxlint/reactCompilerRust.mjs) instead of the JavaScript one in // eslint-plugin-react-hooks, which would take up most of the oxlint run. oxlint's built-in versions // (react/refs, react/purity, ...) are not used: an eslint-disable for react/exhaustive-deps or // react/rules-of-hooks also hides every compiler error in that component // (https://github.com/oxc-project/oxc/issues/26277). 'rc/refs': 'error', 'rc/set-state-in-effect': 'error', 'rc/set-state-in-render': 'error', 'rc/preserve-manual-memoization': 'error', 'rc/immutability': 'error', 'rc/static-components': 'error', 'rc/use-memo': 'error', 'rc/globals': 'error', 'rc/error-boundaries': 'error', 'rc/purity': 'error', 'rc/incompatible-library': 'error', 'rc/unsupported-syntax': 'error', 'no-restricted-exports': ['error', {restrictedNamedExports: ['default', 'then']}], 'import/export': 'error', 'react/require-render-return': 'error', 'no-unreachable-loop': ['error', {ignore: []}], 'hosted/jsx-no-bind': ['error', {ignoreRefs: true, allowArrowFunctions: true, allowFunctions: false, allowBind: false, ignoreDOMComponents: true}], 'hosted/function-component-definition': ['error', {namedComponents: 'function-declaration', unnamedComponents: 'arrow-function'}], 'hosted/jsx-no-constructed-context-values': 'error', 'hosted/prefer-default-export': 'error', 'hosted/order': ['error', {groups: [['builtin', 'external', 'internal']], distinctGroup: true, sortTypesGroup: false, named: false, warnOnUnassignedImports: false}], 'hosted/no-types': 'error', 'core/no-octal': 'error', 'core/no-octal-escape': 'error', 'core/no-undef-init': 'error', 'core/strict': ['error', 'never'], 'one-var': ['error', 'never'], // oxlint only has the new name of ESLint's deprecated `no-new-object`. It also flags bare // `Object()` but not `new Object(arg)`. 'no-object-constructor': 'error', 'hosted/no-import-module-exports': ['error', {exceptions: []}], 'hosted/no-relative-packages': 'error', 'hosted/no-useless-path-segments': ['error', {commonjs: true}], 'hosted/default-props-match-prop-types': ['error', {allowRequiredDefaults: false}], 'hosted/forbid-foreign-prop-types': ['error', {allowInPropTypes: true}], 'hosted/forbid-prop-types': ['error', {forbid: ['any', 'array', 'object'], checkContextTypes: true, checkChildContextTypes: true}], 'hosted/jsx-uses-react': 'error', 'hosted/jsx-uses-vars': 'error', 'hosted/no-access-state-in-setstate': 'error', 'hosted/no-arrow-function-lifecycle': 'error', 'hosted/no-deprecated': 'error', 'hosted/no-invalid-html-attribute': 'error', 'hosted/no-typos': 'error', 'hosted/no-unused-class-component-methods': 'error', 'hosted/no-unused-prop-types': ['error', {customValidators: [], skipShapeProps: true}], 'hosted/no-unused-state': 'error', // Inert on both tools until settings.propWrapperFunctions marks a wrapper `{exact: true}`. 'hosted/prefer-exact-props': 'error', 'hosted/prefer-stateless-function': ['error', {ignorePureComponents: true}], 'hosted/sort-comp': [ 'error', { order: [ 'static-variables', 'static-methods', 'instance-variables', 'lifecycle', '/^handle.+$/', '/^on.+$/', 'getters', 'setters', '/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/', 'instance-methods', 'everything-else', 'rendering', ], groups: { lifecycle: [ 'displayName', 'propTypes', 'contextTypes', 'childContextTypes', 'mixins', 'statics', 'defaultProps', 'constructor', 'getDefaultProps', 'getInitialState', 'state', 'getChildContext', 'getDerivedStateFromProps', 'componentWillMount', 'UNSAFE_componentWillMount', 'componentDidMount', 'componentWillReceiveProps', 'UNSAFE_componentWillReceiveProps', 'shouldComponentUpdate', 'componentWillUpdate', 'UNSAFE_componentWillUpdate', 'getSnapshotBeforeUpdate', 'componentDidUpdate', 'componentDidCatch', 'componentWillUnmount', ], rendering: ['/^render.+$/', 'render'], }, }, ], 'hosted/static-property-placement': ['error', 'property assignment'], }, overrides: [ { files: ['**/*.{js,jsx,mjs,cjs}'], rules: { 'no-redeclare': ['error', {builtinGlobals: false}], 'jsdoc/require-param': 'error', 'jsdoc/require-param-type': 'error', 'jsdoc/check-tag-names': 'error', 'import/named': 'error', 'no-undef': ['error', {typeof: false}], // oxlint's name for ESLint's deprecated `no-new-symbol`; see OXLINT_RULE_RENAMES. 'no-new-native-nonconstructor': 'error', // `typescript/dot-notation` needs type information, so it is off for JS files and ESLint's rule runs here. 'core/dot-notation': ['error', {allowKeywords: true, allowPattern: ''}], 'core/no-dupe-args': 'error', 'core/no-return-await': 'error', }, plugins: ['jsdoc'], }, { files: ['**/*.{ts,tsx,cts,mts}'], rules: { 'no-class-assign': 'off', 'no-new-native-nonconstructor': 'off', 'no-with': 'off', 'typescript/await-thenable': 'error', 'typescript/ban-ts-comment': 'error', 'typescript/no-array-delete': 'error', 'typescript/no-base-to-string': 'error', 'typescript/no-duplicate-enum-values': 'error', 'typescript/no-empty-object-type': 'error', 'typescript/no-extra-non-null-assertion': 'error', 'typescript/no-for-in-array': 'error', 'typescript/no-misused-new': 'error', 'typescript/no-misused-promises': 'error', 'typescript/no-namespace': 'error', 'typescript/no-non-null-asserted-optional-chain': 'error', 'typescript/no-redundant-type-constituents': 'error', 'typescript/no-this-alias': 'error', // ESLint reports 0 here: tsgolint's TS7 inference disagrees with the repo's TS 6.0.2 // about which assertions are unnecessary. 'typescript/no-unnecessary-type-assertion': 'error', 'typescript/no-unnecessary-type-constraint': 'error', 'typescript/no-unsafe-declaration-merging': 'error', 'typescript/no-unsafe-enum-comparison': 'error', 'typescript/no-unsafe-function-type': 'error', 'typescript/no-unsafe-unary-minus': 'error', 'typescript/no-wrapper-object-types': 'error', 'typescript/prefer-as-const': 'error', 'typescript/prefer-namespace-keyword': 'error', 'typescript/restrict-plus-operands': 'error', 'typescript/restrict-template-expressions': 'error', 'typescript/triple-slash-reference': 'error', 'typescript/unbound-method': 'error', // Hosted because tsgolint has not implemented it. 'hosted/naming-convention': [ 'error', {selector: ['variable', 'property'], format: null, filter: {regex: '^__esModule$', match: true}}, {selector: ['variable', 'property'], format: ['camelCase', 'UPPER_CASE', 'PascalCase'], filter: {regex: '^private_[a-z][a-zA-Z0-9]*$', match: false}}, {selector: 'function', format: ['camelCase', 'PascalCase']}, {selector: ['typeLike', 'enumMember'], format: ['PascalCase']}, {selector: ['parameter', 'method'], format: ['camelCase', 'PascalCase'], leadingUnderscore: 'allow'}, ], 'core/lines-between-class-members': ['error', 'always', {exceptAfterSingleLine: false}], 'typescript/adjacent-overload-signatures': 'error', 'typescript/array-type': ['error', {default: 'array-simple'}], 'typescript/ban-tslint-comment': 'error', 'typescript/class-literal-property-style': 'error', 'typescript/consistent-generic-constructors': 'error', 'typescript/consistent-indexed-object-style': 'error', 'typescript/consistent-type-assertions': 'error', 'typescript/consistent-type-definitions': ['error', 'type'], 'typescript/no-confusing-non-null-assertion': 'error', 'typescript/no-inferrable-types': 'error', 'typescript/non-nullable-type-assertion-style': 'error', 'typescript/prefer-for-of': 'error', 'typescript/prefer-function-type': 'error', 'typescript/prefer-nullish-coalescing': ['error', {ignoreIfStatements: true, ignorePrimitives: true, ignoreTernaryTests: true}], 'typescript/prefer-string-starts-ends-with': 'error', 'no-implied-eval': 'off', 'no-new-func': 'off', 'no-redeclare': ['error', {builtinGlobals: false}], 'no-unused-vars': ['error', {vars: 'all', args: 'after-used', caughtErrors: 'none', ignoreRestSiblings: true}], 'no-use-before-define': ['error', {functions: false}], 'constructor-super': 'off', 'getter-return': 'off', 'no-const-assign': 'off', 'no-dupe-keys': 'off', 'no-func-assign': 'off', 'no-import-assign': 'off', 'no-obj-calls': 'off', 'no-setter-return': 'off', 'no-this-before-super': 'off', 'no-unreachable': 'off', 'no-unsafe-negation': 'off', 'valid-typeof': 'off', 'import/no-named-as-default-member': 'off', 'jsdoc/require-param': 'off', 'max-params': ['error', {max: 10}], 'typescript/no-implied-eval': 'error', 'typescript/return-await': ['error', 'in-try-catch'], 'typescript/no-unsafe-type-assertion': 'error', 'typescript/switch-exhaustiveness-check': ['error', {considerDefaultExhaustiveForUnions: true}], 'typescript/no-restricted-types': ['error', {types: {object: "Use 'Record' instead."}}], 'typescript/consistent-type-imports': ['error', {prefer: 'type-imports', fixStyle: 'separate-type-imports'}], 'typescript/consistent-type-exports': ['error', {fixMixedExportsWithInlineTypeSpecifier: false}], 'typescript/no-explicit-any': 'error', 'typescript/no-non-null-assertion': 'error', 'typescript/prefer-enum-initializers': 'error', 'typescript/no-import-type-side-effects': 'error', // oxlint compares constituents syntactically, so `{a: string} & {a: string}` passes where // typescript-eslint reports it. 'typescript/no-duplicate-type-constituents': ['error', {ignoreUnions: true}], }, plugins: ['jsdoc'], }, { files: ['**/scripts/**', '**/server/**', '**/.github/**'], rules: { 'no-console': 'off', }, }, { files: [ 'tests/**', 'test/**', 'spec/**', '**/__tests__/**', '**/__mocks__/**', 'test.{js,jsx}', 'test-*.{js,jsx}', '**/*.{test,spec}.{js,jsx,mjs,cjs}', '**/*.{test,spec}.{ts,tsx,cts,mts}', '**/*_test.{js,jsx,ts,tsx}', '**/jest.config.{js,ts,mts}', '**/jest.setup.{js,ts}', 'jestSetup.js', ], rules: { 'no-console': 'off', 'no-await-in-loop': 'off', 'no-import-assign': 'off', 'jsdoc/check-tag-names': ['error', {definedTags: ['jest-environment']}], }, plugins: ['jsdoc'], }, { files: ['**/*.{ts,tsx,js,jsx,mjs,cjs}'], rules: { 'react-native-a11y/has-accessibility-props': 'error', 'react-native-a11y/has-valid-accessibility-actions': 'error', 'react-native-a11y/has-valid-accessibility-component-type': 'error', 'react-native-a11y/has-valid-accessibility-descriptors': ['error', {touchables: ['PressableWithoutFeedback', 'PressableWithFeedback']}], 'react-native-a11y/has-valid-accessibility-role': 'error', 'react-native-a11y/has-valid-accessibility-state': 'error', 'react-native-a11y/has-valid-accessibility-states': 'error', 'react-native-a11y/has-valid-accessibility-traits': 'error', 'react-native-a11y/has-valid-accessibility-value': 'error', 'react-native-a11y/no-nested-touchables': 'error', 'react-native-a11y/has-valid-accessibility-ignores-invert-colors': 'error', 'react-native-a11y/has-valid-accessibility-live-region': 'error', 'react-native-a11y/has-valid-important-for-accessibility': 'error', '@dword-design/import-alias/prefer-alias': [ 'error', { alias: { '@assets': './assets', '@components': './src/components', '@hooks': './src/hooks', '@userActions': './src/libs/actions', '@libs': './src/libs', '@navigation': './src/libs/Navigation', '@pages': './src/pages', '@prompts': './prompts', '@styles': './src/styles', '@src': './src', '@github': './.github', }, }, ], 'no-unexpected-multiline': 'off', 'arrow-body-style': 'off', 'no-empty': ['error', {allowEmptyCatch: true}], 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], 'react/forbid-component-props': [ 'error', { forbid: [ { propName: 'fsClass', allowedFor: ['View', 'Animated.View', 'Text', 'Animated.Text', 'Pressable'], message: "The 'fsClass' prop doesn't work for custom components, only RN's View, Text and Pressable (plain or Animated).\nType the component's props with 'ForwardedFSClassProps' and pass 'forwardedFSClass' down to one of those, or wrap the content in 'FSClassView' so text inputs inside follow its class.", }, ], }, ], 'no-restricted-properties': [ 'error', {object: 'Image', property: 'getSize', message: 'Usage of Image.getSize is restricted. Please use the `react-native-image-size`.'}, {object: 'HybridAppModule', property: 'isHybridApp', message: 'Use CONFIG.IS_HYBRID_APP instead.'}, {object: 'HybridAppModule', property: 'closeReactNativeApp', message: 'Use `closeReactNativeApp` from `@userActions/HybridApp` instead.'}, ], 'no-restricted-imports': ['error', {paths: restrictedImportPaths, patterns: restrictedImportPatterns}], 'lodash/import-scope': ['error', 'method'], 'prefer-regex-literals': 'off', 'jsdoc/require-param': 'off', 'jsdoc/require-param-type': 'off', 'jsdoc/check-tag-names': 'off', }, jsPlugins: ['eslint-plugin-react-native-a11y', '@dword-design/eslint-plugin-import-alias', 'eslint-plugin-lodash'], plugins: ['jsdoc'], }, { files: ['**/*.{js,jsx,mjs,cjs}'], rules: { 'prefer-arrow-callback': 'off', 'lodash/import-scope': 'off', }, jsPlugins: ['eslint-plugin-lodash'], }, { files: ['**/*.{mjs,cjs}', '.storybook/**/*.{ts,tsx}', 'config/rsbuild/**/*.ts'], rules: { 'import/extensions': 'off', }, }, { files: ['**/*.{ts,tsx}'], rules: { 'you-dont-need-lodash-underscore/concat': 'error', 'you-dont-need-lodash-underscore/drop': 'error', 'you-dont-need-lodash-underscore/drop-right': 'error', 'you-dont-need-lodash-underscore/index-of': 'error', 'you-dont-need-lodash-underscore/join': 'error', 'you-dont-need-lodash-underscore/last': 'error', 'you-dont-need-lodash-underscore/last-index-of': 'error', 'you-dont-need-lodash-underscore/reverse': 'error', 'you-dont-need-lodash-underscore/fill': 'error', 'you-dont-need-lodash-underscore/find': 'error', 'you-dont-need-lodash-underscore/detect': 'error', 'you-dont-need-lodash-underscore/first': 'error', 'you-dont-need-lodash-underscore/find-index': 'error', 'you-dont-need-lodash-underscore/is-array': 'error', 'you-dont-need-lodash-underscore/each': 'error', 'you-dont-need-lodash-underscore/for-each': 'error', 'you-dont-need-lodash-underscore/every': 'error', 'you-dont-need-lodash-underscore/all': 'error', 'you-dont-need-lodash-underscore/filter': 'error', 'you-dont-need-lodash-underscore/select': 'error', 'you-dont-need-lodash-underscore/map': 'error', 'you-dont-need-lodash-underscore/collect': 'error', 'you-dont-need-lodash-underscore/reduce': 'error', 'you-dont-need-lodash-underscore/inject': 'error', 'you-dont-need-lodash-underscore/foldl': 'error', 'you-dont-need-lodash-underscore/reduce-right': 'error', 'you-dont-need-lodash-underscore/foldr': 'error', 'you-dont-need-lodash-underscore/size': 'error', 'you-dont-need-lodash-underscore/some': 'error', 'you-dont-need-lodash-underscore/any': 'error', 'you-dont-need-lodash-underscore/includes': 'error', 'you-dont-need-lodash-underscore/contains': 'error', 'you-dont-need-lodash-underscore/slice': 'error', 'you-dont-need-lodash-underscore/take-right': 'error', 'you-dont-need-lodash-underscore/bind': 'error', 'you-dont-need-lodash-underscore/is-finite': 'error', 'you-dont-need-lodash-underscore/is-integer': 'error', 'you-dont-need-lodash-underscore/is-nan': 'error', 'you-dont-need-lodash-underscore/is-nil': 'error', 'you-dont-need-lodash-underscore/is-null': 'error', 'you-dont-need-lodash-underscore/is-undefined': 'error', 'you-dont-need-lodash-underscore/keys': 'error', 'you-dont-need-lodash-underscore/extend-own': 'error', 'you-dont-need-lodash-underscore/assign': 'error', 'you-dont-need-lodash-underscore/values': 'error', 'you-dont-need-lodash-underscore/entries': 'error', 'you-dont-need-lodash-underscore/to-pairs': 'error', 'you-dont-need-lodash-underscore/pairs': 'error', 'you-dont-need-lodash-underscore/get': 'error', 'you-dont-need-lodash-underscore/split': 'error', 'you-dont-need-lodash-underscore/starts-with': 'error', 'you-dont-need-lodash-underscore/ends-with': 'error', 'you-dont-need-lodash-underscore/to-lower': 'error', 'you-dont-need-lodash-underscore/to-upper': 'error', 'you-dont-need-lodash-underscore/trim': 'error', 'you-dont-need-lodash-underscore/pad-start': 'error', 'you-dont-need-lodash-underscore/pad-end': 'error', 'you-dont-need-lodash-underscore/repeat': 'error', 'you-dont-need-lodash-underscore/uniq': 'error', 'you-dont-need-lodash-underscore/replace': 'error', 'you-dont-need-lodash-underscore/omit': 'error', 'you-dont-need-lodash-underscore/flatten': 'error', 'you-dont-need-lodash-underscore/throttle': 'off', 'you-dont-need-lodash-underscore/is-string': 'error', 'you-dont-need-lodash-underscore/cast-array': 'error', // structuredClone, the suggested clone-deep replacement, is not supported in Hermes: https://github.com/facebook/hermes/issues/684 'you-dont-need-lodash-underscore/clone-deep': 'off', 'you-dont-need-lodash-underscore/is-function': 'error', 'you-dont-need-lodash-underscore/union-by': 'error', 'you-dont-need-lodash-underscore/capitalize': 'error', 'you-dont-need-lodash-underscore/is-date': 'error', 'you-dont-need-lodash-underscore/defaults': 'error', 'you-dont-need-lodash-underscore/is-array-buffer': 'error', 'you-dont-need-lodash-underscore/head': 'error', }, jsPlugins: ['eslint-plugin-you-dont-need-lodash-underscore'], }, { files: ['.github/**/*', 'scripts/**/*', 'server/**/*', 'tests/**/*', '.claude/skills/*/scripts/**/*'], rules: { 'no-await-in-loop': 'off', 'core/no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], }, }, { files: ['.github/**/*'], rules: { 'no-restricted-imports': ['error', {patterns: [{group: ['@src/**'], message: 'Do not import files from src/ directory as they can break the GH Actions build script.'}]}], }, }, { // Like ESLint's block, this replaces the root's list, so the airbnb browser globals are not banned here. files: ['.github/actions/**/*.ts', '.github/libs/**/*.ts'], rules: { 'no-restricted-globals': [ 'error', {name: 'module', message: 'This file is bundled as ESM and runs on Node 24. For an entry-point guard use `import.meta.main` instead of `require.main === module`.'}, {name: '__dirname', message: 'This file is bundled as ESM. Use `import.meta.dirname` instead of `__dirname`.'}, {name: '__filename', message: 'This file is bundled as ESM. Use `import.meta.filename` instead of `__filename`.'}, ], }, }, { files: ['tests/**/*'], rules: { 'import/extensions': 'off', 'no-import-assign': 'off', '@dword-design/import-alias/prefer-alias': ['off'], 'testing-library/await-async-queries': 'error', 'testing-library/await-async-utils': 'error', 'testing-library/no-debugging-utils': 'error', 'testing-library/no-manual-cleanup': 'error', 'testing-library/no-unnecessary-act': 'error', 'testing-library/prefer-find-by': 'error', 'testing-library/prefer-presence-queries': 'error', 'testing-library/prefer-screen-queries': 'error', }, jsPlugins: ['@dword-design/eslint-plugin-import-alias', 'eslint-plugin-testing-library'], }, { files: ['**/*.{ts,tsx}'], rules: { 'no-restricted-imports': ['error', {paths: restrictedImportPaths, patterns: [...typescriptRestrictedImportPatterns, ...restrictedHeaderImportPatterns]}], // Overrides are last-wins: keep this block above the later ones that turn this rule off // or re-set `no-restricted-imports`. 'typescript/no-deprecated': ['error', {allow: ['translateFn']}], // tsgolint misses ``; see config/oxlint/noDeprecatedSelfClosingJsx.mjs. 'hosted/no-deprecated-self-closing-jsx': [ 'error', { components: [ { module: 'src/components/MenuItem', importName: 'default', reason: 'Compose `MenuItem.Root` with its leaves, or use a preset from `@components/MenuItem/presets/`, for new usages.', }, ], }, ], }, }, {files: ['src/**/*'], rules: {'max-lines': ['error', 4000]}}, { files: ['modules/ExpensifyNitroUtils/src/**/*'], rules: { 'typescript/consistent-type-definitions': 'off', }, }, { files: ['server/victory-chart-renderer/**/*.{ts,tsx}'], rules: {'no-restricted-imports': ['error', {paths: victoryChartRendererRestrictedImportPaths, patterns: victoryChartRendererRestrictedImportPatterns}]}, }, { files: ['src/**/*.{ts,tsx}'], rules: { // The lint pipeline keeps this diagnostic only when React Compiler memoizes the file. 'rulesdir/no-manual-memoization': 'error', 'rulesdir/no-object-keys-includes': 'error', 'rulesdir/prefer-locale-compare-from-context': 'error', }, }, { files: ['src/styles/typography.ts', 'src/styles/variables.ts'], rules: { // The typography token files are where raw font sizes and line heights are defined. 'rulesdir/no-raw-typography': 'off', }, }, {files: ['src/styles/**'], excludeFiles: ['src/styles/typography.ts', 'src/styles/variables.ts'], rules: {'rulesdir/no-raw-typography': ['error', {allowVariablesReferences: true}]}}, {files: ['**/en.ts', '**/es.ts'], rules: {'rulesdir/use-periods-for-error-messages': 'error'}}, {files: ['src/libs/ReportNameUtils.ts'], rules: {'report-name-utils/no-function-call-in-get-report-name': 'error'}}, // reportAttributes.ts computes report names, so the TypeScript-only bans do not apply. { files: ['src/libs/actions/OnyxDerived/configs/reportAttributes.ts'], rules: {'no-restricted-imports': ['error', {paths: restrictedImportPaths, patterns: restrictedImportPatterns}]}, }, { files: ['src/languages/**', 'src/CONST/index.ts', 'src/NAICS.ts'], rules: { 'max-lines': 'off', }, }, { files: ['scripts/**', '.github/**'], rules: { // tsgolint's no-unsafe-* port is stricter than typescript-eslint's, and this loosely typed // Actions glue is where it shows: hundreds of findings ESLint does not report. 'typescript/no-unsafe-assignment': 'off', 'typescript/no-unsafe-argument': 'off', 'typescript/no-unsafe-return': 'off', 'typescript/no-unsafe-call': 'off', 'typescript/no-unsafe-member-access': 'off', 'no-throw-literal': 'off', }, }, { files: ['src/libs/Navigation/types.ts', 'src/libs/Navigation/paramLists/**/*.ts'], rules: { 'core/no-restricted-syntax': [ 'error', { selector: 'TSPropertySignature[key.name="backTo"]', message: 'The `backTo` route param is deprecated. Do not add new `backTo` properties to screen param lists. Please look into the `How to remove backTo from URL` section in contributingGuides/NAVIGATION.md. and use alternative routing methods instead.', }, ], }, }, { files: [ '.github/actions/javascript/checkAndroidStatus/checkAndroidStatus.ts', '.github/actions/javascript/getAndroidRolloutPercentage/getAndroidRolloutPercentage.ts', '__mocks__/reportData/actions.ts', 'config/rsbuild/rsbuild.common.ts', 'src/hooks/useAccessibilityAnnouncement/index.ts', 'src/libs/Parser.ts', 'src/libs/actions/Chronos.ts', 'src/libs/actions/IOU/DeleteMoneyRequest.ts', 'src/libs/actions/IOU/Duplicate.ts', 'src/libs/actions/IOU/Hold.ts', 'src/libs/actions/IOU/RejectMoneyRequest.ts', 'src/libs/actions/IOU/TrackExpense.ts', 'src/libs/actions/IOU/UpdateMoneyRequest.ts', 'src/libs/actions/Policy/Member.ts', 'src/libs/actions/Transaction.ts', 'src/pages/inbox/conciergeDraftState.ts', 'src/pages/inbox/report/ReportActionsListItemRenderer.tsx', 'src/utils/createDummySharedValue.ts', 'tests/actions/IOU/CreateDraftTransactionTest.ts', 'tests/actions/IOU/RequestMoneyTest.ts', 'tests/actions/IOUTest/BulkEditTest.ts', 'tests/actions/IOUTest/DeleteMoneyRequestTest.ts', 'tests/actions/IOUTest/DuplicateTest.ts', 'tests/actions/IOUTest/PayMoneyRequestTest.ts', 'tests/actions/IOUTest/SplitTest.ts', 'tests/actions/IOUTest/TrackExpenseTest.ts', 'tests/actions/IOUTest/UpdateMoneyRequestTest.ts', 'tests/actions/MergeTransactionTest.ts', 'tests/actions/ReplaceOptimisticReportWithActualReportTest.ts', 'tests/actions/ReportTest.ts', 'tests/actions/TransactionTest.ts', 'tests/ui/ChatActionableButtonsTest.tsx', 'tests/ui/DynamicReportDetailsPageTest.tsx', 'tests/ui/LHNItemsPresence.tsx', 'tests/ui/MoneyRequestReportViewTest.tsx', 'tests/ui/RecentlyAddedSectionTest.tsx', 'tests/ui/ReportActionItemTest.tsx', 'tests/ui/ReportActionsListTest.tsx', 'tests/ui/ReportAttachments.tsx', 'tests/ui/components/BaseReportActionContextMenuTest.tsx', 'tests/ui/components/LHNOptionsListTest.tsx', 'tests/unit/AgentRuleChangeLogUtilsTest.ts', 'tests/unit/DebugUtilsTest.ts', 'tests/unit/DeleteReportTest.ts', 'tests/unit/LHNAvatarPipelineTest.ts', 'tests/unit/MoneyRequestReportUtilsTest.ts', 'tests/unit/NextStepUtilsTest.ts', 'tests/unit/OnyxDerived/computeForReportTest.ts', 'tests/unit/OnyxDerivedTest.tsx', 'tests/unit/OptionsListUtilsTest.tsx', 'tests/unit/PolicyChangeLogContentTest.tsx', 'tests/unit/ReportActionsUtilsTest.ts', 'tests/unit/ReportPrimaryActionUtilsTest.ts', 'tests/unit/ReportSecondaryActionUtilsTest.ts', 'tests/unit/ReportUtilsTest.ts', 'tests/unit/Search/SearchUIUtilsTest.ts', 'tests/unit/SidebarUtilsTest.ts', 'tests/unit/SpendRuleChangeLogUtilsTest.ts', 'tests/unit/TransactionTest.ts', 'tests/unit/WhisperContentMentionContextTest.tsx', 'tests/unit/components/MoneyReportTransactionThreadContext.test.tsx', 'tests/unit/getReportPreviewSenderIDTest.ts', 'tests/unit/getSendMessageListWeightTest.ts', 'tests/unit/getSendMessageSourceTest.ts', 'tests/unit/hooks/useDeleteTransactionsInvoiceOfflineTest.tsx', 'tests/unit/hooks/useExpenseActionsInvoiceDeleteTest.tsx', 'tests/unit/hooks/useExpenseSubmission.test.ts', 'tests/unit/hooks/useOriginalReportID.test.tsx', 'tests/unit/hooks/useSelectedTransactionsActions.test.ts', 'tests/unit/hooks/useShowNotFoundPageInIOUStep.test.ts', 'tests/unit/hooks/useTransactionThreadReportID.test.ts', 'tests/unit/inlineEditing/TransactionInlineEdit.test.ts', 'tests/unit/isConsecutiveActionMadeByPreviousActorTest.ts', 'tests/unit/libs/MoneyRequestReportUtils.ts', 'tests/unit/pages/inbox/ConciergeDraftContext.test.tsx', 'tests/unit/selectors/ReportActionTest.ts', 'tests/unit/useGetExpensifyCardFromReportActionTest.ts', 'tests/unit/useOneTransactionThreadReportIDTest.ts', 'tests/unit/useReportActionsVisibilityTest.tsx', 'tests/utils/LHNTestUtils.tsx', 'tests/utils/ReportTestUtils.ts', 'tests/utils/collections/reportActions.ts', 'tests/utils/createSharedValueMock.ts', ], rules: { // tsgolint also reports writes to deprecated properties (typescript-eslint#10643), which // floods these files. Costs a few read-site findings ESLint reports here. 'typescript/no-deprecated': 'off', 'hosted/no-deprecated-self-closing-jsx': 'off', }, }, { files: ['**/*.{js,jsx,mjs,cjs}'], rules: { 'typescript/await-thenable': 'off', 'typescript/consistent-type-exports': 'off', 'typescript/dot-notation': 'off', 'typescript/no-array-delete': 'off', 'typescript/no-base-to-string': 'off', 'typescript/no-deprecated': 'off', 'typescript/no-duplicate-type-constituents': 'off', 'typescript/no-for-in-array': 'off', 'typescript/no-implied-eval': 'off', 'typescript/no-misused-promises': 'off', 'typescript/no-redundant-type-constituents': 'off', 'typescript/no-unnecessary-type-assertion': 'off', 'typescript/no-unsafe-enum-comparison': 'off', 'typescript/no-unsafe-type-assertion': 'off', 'typescript/no-unsafe-unary-minus': 'off', 'typescript/non-nullable-type-assertion-style': 'off', 'typescript/prefer-nullish-coalescing': 'off', 'typescript/prefer-string-starts-ends-with': 'off', 'typescript/restrict-plus-operands': 'off', 'typescript/restrict-template-expressions': 'off', 'typescript/return-await': 'off', 'typescript/switch-exhaustiveness-check': 'off', 'typescript/unbound-method': 'off', 'typescript/no-unsafe-assignment': 'off', 'typescript/no-unsafe-argument': 'off', 'typescript/no-unsafe-return': 'off', 'typescript/no-unsafe-call': 'off', 'typescript/no-unsafe-member-access': 'off', 'rulesdir/no-default-props': 'off', 'hosted/no-types': 'off', }, }, { // bun-types declares `expect(...).resolves`/`.rejects` as returning `void` even though Bun // requires awaiting them. See https://github.com/oven-sh/bun/pull/23425. files: ['tests/tooling/**/*.ts'], rules: {'typescript/await-thenable': 'off'}, }, { // oxlint has no storybook plugin, so this restates the story-file scope and severities of // `plugin:storybook/recommended`. files: ['**/*.{stories,story}.{ts,tsx,js,jsx,mjs,cjs}'], rules: { // The storybook preset turns this off for stories. 'react/rules-of-hooks': 'off', // Stories never send anything to Sentry, so they don't need a sentryLabel. 'core/no-restricted-syntax': ['error', ...restrictedSyntax], 'hosted/await-interactions': 'error', 'hosted/context-in-play-function': 'error', 'hosted/default-exports': 'error', 'hosted/hierarchy-separator': 'warn', 'hosted/no-redundant-story-name': 'warn', 'hosted/no-renderer-packages': 'error', 'hosted/prefer-pascal-case': 'warn', 'hosted/story-exports': 'error', 'hosted/use-storybook-expect': 'error', 'hosted/use-storybook-testing-library': 'error', }, }, { files: ['.storybook/main.{js,cjs,mjs,ts}'], rules: {'hosted/no-uninstalled-addons': 'error'}, }, {files: ['src/types/onyx/**/*.ts'], rules: {'hosted/require-jsdoc': ['error', {contexts: ['TSInterfaceDeclaration', 'TSTypeAliasDeclaration']}]}}, { files: [ 'src/ONYXKEYS.ts', 'src/hooks/usePersonalDetails.ts', 'src/libs/PersonalDetailsStore.ts', 'src/libs/PersonalDetailsUtils.ts', 'src/components/OnyxListItemProvider.tsx', 'src/libs/ExportOnyxState/masking.ts', 'tests/**/*.{ts,tsx}', 'jest/**/*.{ts,tsx}', '__mocks__/**/*.{ts,tsx}', 'src/**/__mocks__/**/*.{ts,tsx}', ], rules: {'rulesdir/no-direct-personal-details-list': 'off'}, }, // LocaleContextProvider and HybridAppHandler mount before `Onyx.init` finishes, so they keep relying on the startup // loading status. Tests and mocks follow the code they cover and get cleaned up together with it. { files: [ 'src/components/LocaleContextProvider.tsx', 'src/HybridAppHandler.tsx', 'tests/**/*.{ts,tsx}', 'jest/**/*.{ts,tsx}', '__mocks__/**/*.{ts,tsx}', 'src/**/__mocks__/**/*.{ts,tsx}', ], rules: {'rulesdir/no-onyx-loading-status': 'off'}, }, ], });