From b77f9592f2f4a7f674e03a12c73c07aaed36eb39 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Wed, 21 Oct 2026 12:30:42 +0200 Subject: [PATCH 01/32] Add oxlint config and lint pipeline --- .github/workflows/oxlint.yml | 70 + .github/workflows/preDeploy.yml | 6 + .gitignore | 4 + .npmrc | 5 + config/eslint/eslint.config.mjs | 3 + config/oxlint/eslintDirectives.mjs | 126 + config/oxlint/noDeprecatedSelfClosingJsx.mjs | 162 + config/oxlint/oxlint.seatbelt.tsv | 2595 +++++++++++++++++ config/oxlint/plugins/core-rules.mjs | 29 + config/oxlint/plugins/expensify-rules.mjs | 40 + config/oxlint/plugins/hosted-rules.mjs | 97 + config/oxlint/plugins/rc-rules.mjs | 41 + .../oxlint/preferLocaleCompareFromContext.mjs | 53 + config/oxlint/reactCompilerGate.mjs | 62 + config/oxlint/reactCompilerRust.mjs | 236 ++ config/oxlint/ruleNames.d.mts | 7 + config/oxlint/ruleNames.mjs | 98 + config/reactCompiler/suppressedRules.d.mts | 4 + config/reactCompiler/suppressedRules.mjs | 8 + cspell.json | 10 + knip.json | 7 +- oxlint.config.mts | 1327 +++++++++ package-lock.json | 684 ++++- package.json | 6 +- scripts/lint/formatters/JSONFormatter.ts | 26 + scripts/lint/index.ts | 74 +- scripts/lint/oxlint/OxlintLinter.ts | 498 ++++ .../lint/processors/ReactCompilerFilter.ts | 4 +- scripts/lint/processors/Seatbelt.ts | 27 +- tests/tooling/lintPipeline.test.ts | 465 ++- tsconfig.node.json | 2 +- 31 files changed, 6664 insertions(+), 112 deletions(-) create mode 100644 .github/workflows/oxlint.yml create mode 100644 config/oxlint/eslintDirectives.mjs create mode 100644 config/oxlint/noDeprecatedSelfClosingJsx.mjs create mode 100644 config/oxlint/oxlint.seatbelt.tsv create mode 100644 config/oxlint/plugins/core-rules.mjs create mode 100644 config/oxlint/plugins/expensify-rules.mjs create mode 100644 config/oxlint/plugins/hosted-rules.mjs create mode 100644 config/oxlint/plugins/rc-rules.mjs create mode 100644 config/oxlint/preferLocaleCompareFromContext.mjs create mode 100644 config/oxlint/reactCompilerGate.mjs create mode 100644 config/oxlint/reactCompilerRust.mjs create mode 100644 config/oxlint/ruleNames.d.mts create mode 100644 config/oxlint/ruleNames.mjs create mode 100644 config/reactCompiler/suppressedRules.d.mts create mode 100644 config/reactCompiler/suppressedRules.mjs create mode 100644 oxlint.config.mts create mode 100644 scripts/lint/formatters/JSONFormatter.ts create mode 100644 scripts/lint/oxlint/OxlintLinter.ts diff --git a/.github/workflows/oxlint.yml b/.github/workflows/oxlint.yml new file mode 100644 index 000000000000..b7af5f9e0b88 --- /dev/null +++ b/.github/workflows/oxlint.yml @@ -0,0 +1,70 @@ +name: Oxlint check (non-blocking) + +on: + workflow_call: + pull_request: + types: [opened, synchronize] + branches-ignore: [staging, production] + paths: ['**.js', '**.ts', '**.tsx', '**.json', '**.mjs', '**.cjs', 'oxlint.config.mts', 'config/oxlint/**', 'scripts/lint/**', '.github/workflows/oxlint.yml'] + +concurrency: + group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-oxlint + cancel-in-progress: true + +jobs: + oxlint: + name: Oxlint check (non-blocking) + if: ${{ github.event.head_commit.author.name != 'OSBotify' || github.event_name == 'push' }} + runs-on: blacksmith-8vcpu-ubuntu-2404 + steps: + - name: Checkout + uses: useblacksmith/checkout@0647fdbab2614a5eb86d2971070e325060d91056 # v1.7.0 + with: + token: ${{ github.event_name == 'push' && secrets.OS_BOTIFY_COMMIT_TOKEN || github.token }} + + - name: Setup Node + uses: ./.github/actions/composite/setupNode + + - name: Lint JavaScript and TypeScript with Oxlint + id: oxlint + continue-on-error: true + # oxlint runs JS plugins on one thread (oxc#26621), so OxlintLinter fans them across 4 + # single-threaded shards and runs the type-aware rules in one multi-threaded process: 8 vCPU. + env: + OXLINT_SHARDS: 4 + run: npm run lint -- --linter=oxlint + + - name: Report the Oxlint result without failing the job + run: | + if [[ "${{ steps.oxlint.outcome }}" == "success" ]]; then + echo "::notice::Oxlint found no errors above its baseline." + else + echo "::warning::Oxlint reported errors above its baseline. Non-blocking during the migration; see the step log." + fi + + - name: Check for tightened oxlint-seatbelt baseline + id: seatbelt_diff + if: github.event_name == 'push' + run: | + if git diff --quiet config/oxlint/oxlint.seatbelt.tsv; then + echo "tightened=false" >> "$GITHUB_OUTPUT" + echo "::notice::oxlint.seatbelt.tsv unchanged; skipping auto-commit." + else + echo "tightened=true" >> "$GITHUB_OUTPUT" + fi + + - name: Setup git for OSBotify + if: steps.seatbelt_diff.outputs.tightened == 'true' + uses: Expensify/GitHub-Actions/setupGitForOSBotify@main + with: + OP_VAULT: ${{ vars.OP_VAULT }} + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + SETUP_AS_APP: false + + - name: Auto-commit tightened oxlint-seatbelt baseline + if: steps.seatbelt_diff.outputs.tightened == 'true' + continue-on-error: true + run: | + git add config/oxlint/oxlint.seatbelt.tsv + git commit -m "Auto-tighten oxlint-seatbelt baseline" + git push origin main diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index 0c09e86b3098..c6f5a41a72d3 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -14,6 +14,12 @@ jobs: uses: ./.github/workflows/lint.yml secrets: inherit + # Deliberately absent from confirmPassingBuild's `needs`: the Oxlint run is a shadow gate, here + # only so the post-merge job can auto-tighten its own baseline. + oxlint: + uses: ./.github/workflows/oxlint.yml + secrets: inherit + oxfmt: uses: ./.github/workflows/oxfmt.yml diff --git a/.gitignore b/.gitignore index 8c6d0691fdb9..d82481e269f0 100644 --- a/.gitignore +++ b/.gitignore @@ -185,3 +185,7 @@ server/victory-chart-renderer/.dev/ # Bun lockfiles (VCR uses npm workspaces + package-lock.json; bun is runtime-only) bun.lock + +# Oxlint leg configs derived from oxlint.config.mts for one sharded run (scripts/lint/oxlint/OxlintLinter.ts) +.oxlintrc.js-plugins.*.json +.oxlintrc.type-aware.*.json diff --git a/.npmrc b/.npmrc index 2a14ea859d36..eb8b3f295df6 100644 --- a/.npmrc +++ b/.npmrc @@ -10,3 +10,8 @@ min-release-age-exclude[]=@expensify/react-native-wallet min-release-age-exclude[]=react-fast-pdf min-release-age-exclude[]=react-native-key-command min-release-age-exclude[]=react-native-qrcode-svg +# Omitting a binding scope makes npm silently skip the optional dependency instead of erroring. +min-release-age-exclude[]=oxlint +min-release-age-exclude[]=@oxlint/* +min-release-age-exclude[]=oxc-transform-react +min-release-age-exclude[]=@oxc-transform-react/* diff --git a/config/eslint/eslint.config.mjs b/config/eslint/eslint.config.mjs index 6696ee50f090..50387071594e 100644 --- a/config/eslint/eslint.config.mjs +++ b/config/eslint/eslint.config.mjs @@ -824,6 +824,9 @@ const config = defineConfig([ globalIgnores([ '!**/.storybook', '!**/.github', + // Oxlint's own config. Its rule names are quoted object keys, which naming-convention flags by the + // hundred; `**/*.config.mjs` below is ignored for the same kind of reason. + 'oxlint.config.mts', '.github/actions/**/index.js', '**/*.config.js', '**/*.config.mjs', diff --git a/config/oxlint/eslintDirectives.mjs b/config/oxlint/eslintDirectives.mjs new file mode 100644 index 000000000000..9cd448334fb2 --- /dev/null +++ b/config/oxlint/eslintDirectives.mjs @@ -0,0 +1,126 @@ +// A rule hosted through oxlint's `jsPlugins` cannot take a native plugin's prefix (`react-hooks`, +// `@typescript-eslint`, `react`, `import` are reserved), so an existing `eslint-disable-next-line +// @typescript-eslint/naming-convention` does not suppress `hosted/naming-convention`. This makes a +// hosted rule answer to the id ESLint uses, by dropping the reports ESLint would have dropped. + +const JUSTIFICATION = /\s-{2,}\s/; +const KINDS = ['eslint-disable-next-line', 'eslint-disable-line', 'eslint-disable', 'eslint-enable']; +const ALL_RULES = ' all'; + +const cache = new Map(); + +function parseComment(comment) { + const text = String(comment.value ?? '').trim(); + const kind = KINDS.find((candidate) => text === candidate || (text.startsWith(candidate) && /^\s/.test(text.slice(candidate.length)))); + if (!kind) { + return null; + } + const body = text.slice(kind.length).split(JUSTIFICATION)[0]; + const ruleIds = body + .split(',') + .map((id) => id.trim()) + .filter(Boolean); + return {kind, ruleIds: ruleIds.length ? ruleIds : [ALL_RULES]}; +} + +function parseDirectives(comments) { + const lines = new Map(); + const blocks = []; + + for (const comment of comments) { + const parsed = parseComment(comment); + const start = comment.loc?.start?.line; + const end = comment.loc?.end?.line; + if (!parsed || start == null || end == null) { + continue; + } + for (const ruleId of parsed.ruleIds) { + if (parsed.kind === 'eslint-disable-next-line' || parsed.kind === 'eslint-disable-line') { + // ESLint rejects only a multi-line `-line` directive; a multi-line `-next-line` is + // applied to the line after the comment closes, which is why the target is `end`. + if (parsed.kind === 'eslint-disable-line' && start !== end) { + continue; + } + if (!lines.has(ruleId)) { + lines.set(ruleId, new Set()); + } + lines.get(ruleId).add(parsed.kind === 'eslint-disable-line' ? start : end + 1); + } else { + blocks.push({line: start, ruleId, disable: parsed.kind === 'eslint-disable'}); + } + } + } + + return {lines, blocks}; +} + +function directivesFor(filename, comments) { + const key = filename ?? ''; + if (!cache.has(key)) { + cache.set(key, parseDirectives(comments)); + } + return cache.get(key); +} + +// Mirrors ESLint: the last block directive at or before the line that names the rule (or no rule) +// decides. A bare `eslint-disable` followed by `eslint-enable foo` therefore re-enables `foo` alone, +// and a bare `eslint-enable` re-enables every rule. +function isSuppressed(directives, eslintRuleId, line) { + if (line == null) { + return false; + } + if (directives.lines.get(eslintRuleId)?.has(line) || directives.lines.get(ALL_RULES)?.has(line)) { + return true; + } + let disabled = false; + for (const block of directives.blocks) { + if (block.line > line || (block.ruleId !== ALL_RULES && block.ruleId !== eslintRuleId)) { + continue; + } + // A report on the `eslint-enable` line itself stays suppressed, as with the previous inclusive range. + if (block.disable) { + disabled = true; + } else if (block.line < line) { + disabled = false; + } + } + return disabled; +} + +function reportLine(args) { + const first = args[0]; + const candidates = [first?.loc?.start?.line, first?.loc?.line, first?.node?.loc?.start?.line, first?.node?.loc?.line]; + return candidates.find((line) => typeof line === 'number') ?? null; +} + +function withEslintDirectiveIds(rule, eslintRuleId) { + return { + ...rule, + create(context) { + const filteredContext = Object.create(context, { + report: { + // Forwarded as-is rather than as one descriptor: ESLint also accepts the legacy + // `report(node, message)` form, and dropping the extra arguments would change the message. + value(...args) { + const line = reportLine(args); + if (line != null) { + const sourceCode = context.sourceCode ?? context.getSourceCode(); + const filename = context.filename ?? context.getFilename(); + if (isSuppressed(directivesFor(filename, sourceCode.getAllComments()), eslintRuleId, line)) { + return; + } + } + return context.report(...args); + }, + }, + }); + return rule.create(filteredContext); + }, + }; +} + +function withEslintDirectiveIdsFor(rules, toEslintId) { + return Object.fromEntries(Object.entries(rules).map(([name, rule]) => [name, withEslintDirectiveIds(rule, toEslintId(name))])); +} + +export {withEslintDirectiveIds, withEslintDirectiveIdsFor}; diff --git a/config/oxlint/noDeprecatedSelfClosingJsx.mjs b/config/oxlint/noDeprecatedSelfClosingJsx.mjs new file mode 100644 index 000000000000..74723fcbb1be --- /dev/null +++ b/config/oxlint/noDeprecatedSelfClosingJsx.mjs @@ -0,0 +1,162 @@ +// tsgolint's no-deprecated skips the signature check on self-closing JSX (``, not ``), so a +// component deprecated only through its signature, like `MenuItem` wrapped in `Object.assign`, goes +// unreported. jsPlugins get no types, so the components are listed in the options. Delete this rule once +// tsgolint reports these itself: the findings will double. + +import fs from 'node:fs'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); + +// tsconfig.json allows comments and trailing commas. +function readJsonc(file) { + const text = fs + .readFileSync(file, 'utf8') + .replaceAll(/("(?:\\.|[^"\\])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (match, string) => string ?? '') + .replaceAll(/,(\s*[}\]])/g, '$1'); + return JSON.parse(text); +} + +let aliasPrefixes; + +function getAliasPrefixes() { + if (!aliasPrefixes) { + const paths = readJsonc(path.join(repoRoot, 'tsconfig.json')).compilerOptions?.paths ?? {}; + aliasPrefixes = Object.entries(paths) + .filter(([alias, targets]) => alias.endsWith('/*') && targets.at(0)?.endsWith('/*')) + .map(([alias, targets]) => [alias.slice(0, -1), path.resolve(repoRoot, targets.at(0).slice(0, -1))]); + } + return aliasPrefixes; +} + +function resolveModule(source, filename) { + let resolved = null; + if (source.startsWith('.')) { + resolved = path.resolve(path.dirname(filename), source); + } else { + const match = getAliasPrefixes().find(([prefix]) => source.startsWith(prefix)); + resolved = match ? path.join(match[1], source.slice(match[0].length)) : null; + } + return resolved?.replace(/\.(tsx?|jsx?|mjs)$/, '').replace(/\/index$/, '') ?? null; +} + +const meta = { + type: 'problem', + docs: { + description: 'Report self-closing JSX of a component deprecated through its call signature, which tsgolint misses', + }, + schema: [ + { + type: 'object', + properties: { + components: { + type: 'array', + items: { + type: 'object', + properties: { + module: {type: 'string'}, + importName: {type: 'string'}, + reason: {type: 'string'}, + }, + required: ['module', 'importName'], + additionalProperties: false, + }, + }, + }, + additionalProperties: false, + }, + ], +}; + +function importedName(specifier) { + if (specifier.type === 'ImportDefaultSpecifier') { + return 'default'; + } + if (specifier.type === 'ImportSpecifier') { + return specifier.imported.type === 'Identifier' ? specifier.imported.name : specifier.imported.value; + } + return null; +} + +function initializerIdentifiers(node) { + if (!node) { + return []; + } + switch (node.type) { + case 'Identifier': + return [node.name]; + case 'ConditionalExpression': + return [...initializerIdentifiers(node.consequent), ...initializerIdentifiers(node.alternate)]; + case 'LogicalExpression': + return [...initializerIdentifiers(node.left), ...initializerIdentifiers(node.right)]; + case 'TSAsExpression': + case 'TSSatisfiesExpression': + case 'TSNonNullExpression': + return initializerIdentifiers(node.expression); + default: + return []; + } +} + +function create(context) { + const components = context.options[0]?.components ?? []; + if (components.length === 0) { + return {}; + } + + const deprecatedBindings = new Map(); + const aliases = []; + const selfClosingTags = []; + + return { + ImportDeclaration(node) { + const resolved = resolveModule(String(node.source.value), context.filename ?? context.getFilename()); + for (const component of components) { + if (resolved === null || resolved !== path.resolve(repoRoot, component.module)) { + continue; + } + for (const specifier of node.specifiers) { + if (importedName(specifier) === component.importName) { + deprecatedBindings.set(specifier.local.name, component); + } + } + } + }, + VariableDeclarator(node) { + if (node.id.type !== 'Identifier') { + return; + } + aliases.push({name: node.id.name, targets: initializerIdentifiers(node.init)}); + }, + JSXOpeningElement(node) { + if (!node.selfClosing || node.name.type !== 'JSXIdentifier') { + return; + } + selfClosingTags.push(node.name); + }, + 'Program:exit': function () { + if (deprecatedBindings.size === 0) { + return; + } + for (const alias of aliases) { + const target = alias.targets.find((name) => deprecatedBindings.has(name)); + if (target && !deprecatedBindings.has(alias.name)) { + deprecatedBindings.set(alias.name, deprecatedBindings.get(target)); + } + } + for (const name of selfClosingTags) { + const component = deprecatedBindings.get(name.name); + if (!component) { + continue; + } + context.report({ + node: name, + message: component.reason ? `\`${name.name}\` is deprecated. ${component.reason}` : `\`${name.name}\` is deprecated.`, + }); + } + }, + }; +} + +export {meta, create}; diff --git a/config/oxlint/oxlint.seatbelt.tsv b/config/oxlint/oxlint.seatbelt.tsv new file mode 100644 index 000000000000..4831d1a255ae --- /dev/null +++ b/config/oxlint/oxlint.seatbelt.tsv @@ -0,0 +1,2595 @@ +# eslint-seatbelt temporarily allowed errors +# docs: https://github.com/justjake/eslint-seatbelt#readme + +"../../__mocks__/@pusher/pusher-websocket-react-native/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../__mocks__/@react-native-camera-roll/camera-roll.ts" "@typescript-eslint/no-deprecated/CameraRoll.save" 1 +"../../__mocks__/@react-native-community/netinfo.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../__mocks__/@react-navigation/native/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../__mocks__/@react-navigation/native/index.ts" "import/export" 1 +"../../__mocks__/expo-video.tsx" "react-hooks/refs" 2 +"../../__mocks__/openai.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../__mocks__/react-freeze.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../__mocks__/react-native-onyx.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../__mocks__/react-native-safe-area-context.tsx" "react-hooks/refs" 2 +"../../__mocks__/react-native.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../.github/actions/javascript/failureNotifier/failureNotifier.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../.github/actions/javascript/formatCodeCovComment/formatCodeCovComment.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../.github/actions/javascript/getAndroidRolloutPercentage/getAndroidRolloutPercentage.ts" "rulesdir/no-default-id-values" 1 +"../../.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../.github/actions/javascript/getPullRequestDetails/getPullRequestDetails.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../.github/actions/javascript/getPullRequestIncrementalChanges/getPullRequestIncrementalChanges.ts" "@typescript-eslint/no-unsafe-type-assertion" 5 +"../../.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts" "no-restricted-imports" 1 +"../../.github/actions/javascript/postOrReplaceComment/postOrReplaceComment.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../.github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../.github/actions/javascript/reviewerChecklist/reviewerChecklist.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../.github/actions/javascript/validateReassureOutput/validateReassureOutput.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../.github/libs/DeployChecklistUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../.github/libs/GithubUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 5 +"../../.github/libs/sanitizeJSONStringValues.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../.github/scripts/createDocsRoutes.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../.storybook/mocks/react-navigation-native.ts" "import/export" 1 +"../../.storybook/mocks/react-navigation-stack.ts" "import/export" 1 +"../../assets/emojis/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../jest/setup.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../jest/setup.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../jest/setupAfterEnv.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../jest/setupAfterEnv.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../modules/ExpensifyNitroUtils/src/index.ts" "no-restricted-syntax" 1 +"../../scripts/aggregateGitHubDataFromUpwork.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../scripts/bumpVersion.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../scripts/combine-web-sourcemaps.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../scripts/compareKnipReports.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../scripts/compareKnipReports.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../scripts/createOrUpdateDeployChecklist.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../scripts/generateTranslations.ts" "@typescript-eslint/no-unsafe-type-assertion" 8 +"../../scripts/react-compiler-compliance-check.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../scripts/react-compiler-compliance-check.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../scripts/sentry/release-adoption-older-than.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../scripts/utils/Git.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../scripts/utils/OpenAIUtils.ts" "@typescript-eslint/no-deprecated/prompt_cache_retention" 1 +"../../scripts/utils/OpenAIUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../scripts/utils/Translator/TranslationPromptUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../scripts/utils/TSCompilerUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../server/stubs/react-native-reanimated.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../server/stubs/react-native/index.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../server/victory-chart-renderer/src/initSkiaForCli.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../server/victory-chart-renderer/tests/log.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/ActionSheetAwareScrollView/useActionSheetAwareScrollViewRef.ts" "react-hooks/immutability" 1 +"../../src/components/ActionSheetAwareScrollView/useActionSheetKeyboardSpacing.ts" "@typescript-eslint/no-deprecated/useScrollViewOffset" 1 +"../../src/components/AddPlaidBankAccount.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/AddressForm.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/AddressForm.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/AddressSearch/CurrentLocationButton.tsx" "no-restricted-syntax" 1 +"../../src/components/AddressSearch/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/AddressSearch/isCurrentTargetInsideContainer.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/AgentRules/AgentRulesList.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/AmountTextInput.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx" "import/no-cycle" 1 +"../../src/components/AnchorForAttachmentsOnly/index.tsx" "import/no-cycle" 1 +"../../src/components/AnchorForAttachmentsOnly/index.tsx" "no-restricted-syntax" 1 +"../../src/components/AnchorForCommentsOnly/index.tsx" "no-restricted-syntax" 1 +"../../src/components/AnimatedFlatListWithCellRenderer.tsx" "@typescript-eslint/no-deprecated/Animated.createAnimatedComponent" 1 +"../../src/components/AnimatedFlatListWithCellRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/AnimatedSubmitButton/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/AnimatedSubmitButton/index.tsx" "react-hooks/refs" 2 +"../../src/components/AnimatedSubmitButton/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/ApprovalWorkflowSection.tsx" "@typescript-eslint/no-deprecated/MenuItem" 2 +"../../src/components/ApproverSelectionList.tsx" "no-restricted-imports" 1 +"../../src/components/AttachmentPicker/index.native.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/AttachmentPicker/index.native.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/AttachmentPicker/index.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/AttachmentPicker/index.native.tsx" "react-hooks/refs" 1 +"../../src/components/AttachmentPicker/index.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/AttachmentPicker/index.tsx" "react-hooks/refs" 1 +"../../src/components/AttachmentPreview.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx" "@typescript-eslint/no-deprecated/GestureType" 3 +"../../src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx" "@typescript-eslint/no-deprecated/LegacyComposedGesture" 1 +"../../src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx" "react-hooks/refs" 3 +"../../src/components/Attachments/AttachmentCarousel/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/Attachments/AttachmentCarousel/Pager/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Native" 2 +"../../src/components/Attachments/AttachmentCarousel/Pager/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/Attachments/AttachmentCarousel/Pager/types.ts" "@typescript-eslint/no-deprecated/GestureType" 2 +"../../src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts" "no-restricted-syntax" 1 +"../../src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx" "import/no-cycle" 1 +"../../src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx" "import/no-cycle" 1 +"../../src/components/Attachments/AttachmentView/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/components/Attachments/AttachmentView/index.tsx" "import/no-cycle" 3 +"../../src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx" "no-restricted-syntax" 1 +"../../src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay.tsx" "react-hooks/refs" 1 +"../../src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/AutoCompleteSuggestions/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/AutoUpdateTime.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/AutoUpdateTime.tsx" "react-hooks/purity" 1 +"../../src/components/AvatarCropModal/AvatarCropView.tsx" "no-restricted-imports" 1 +"../../src/components/AvatarCropModal/ImageCropView.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/AvatarCropModal/Slider.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/AvatarSelector.tsx" "no-restricted-syntax" 2 +"../../src/components/AvatarWithImagePicker.tsx" "react-hooks/set-state-in-effect" 2 +"../../src/components/BaseVacationDelegateSelectionComponent.tsx" "no-restricted-imports" 1 +"../../src/components/BlockingViews/FullPageNotFoundView.tsx" "no-restricted-imports" 1 +"../../src/components/BookTravelButton.tsx" "no-restricted-imports" 1 +"../../src/components/ButtonWithDropdownMenu/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/CategoryPicker/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Charts/hooks/useChartInteractions.ts" "@typescript-eslint/no-deprecated/Gesture.Hover" 2 +"../../src/components/Charts/hooks/useChartInteractions.ts" "@typescript-eslint/no-deprecated/Gesture.Race" 2 +"../../src/components/Charts/hooks/useChartInteractions.ts" "@typescript-eslint/no-deprecated/Gesture.Tap" 2 +"../../src/components/Charts/hooks/useChartLabelFormats.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Charts/hooks/useChartLabelMeasurements.ts" "rulesdir/no-raw-typography" 1 +"../../src/components/Charts/PieChart/PieChartContent.tsx" "@typescript-eslint/no-deprecated/Gesture.Hover" 2 +"../../src/components/Charts/PieChart/PieChartContent.tsx" "@typescript-eslint/no-deprecated/Gesture.Race" 2 +"../../src/components/Charts/PieChart/PieChartContent.tsx" "@typescript-eslint/no-deprecated/Gesture.Tap" 2 +"../../src/components/Charts/utils/chartFontAssets.native.ts" "@typescript-eslint/no-unsafe-type-assertion" 13 +"../../src/components/Charts/utils/chartFontAssets.ts" "@typescript-eslint/no-unsafe-type-assertion" 13 +"../../src/components/Charts/utils/chartFontsCache.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/Charts/utils/chartWebFont.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Charts/utils/getChartSkiaTypeface.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/CheckboxWithLabel.tsx" "no-restricted-syntax" 1 +"../../src/components/ColumnsSettingsList.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/ColumnsSettingsList.tsx" "no-restricted-imports" 1 +"../../src/components/Composer/implementation/index.native.tsx" "react-hooks/preserve-manual-memoization" 1 +"../../src/components/Composer/implementation/index.tsx" "react-hooks/set-state-in-effect" 2 +"../../src/components/ConfirmModal.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 +"../../src/components/ConnectionLayout.tsx" "no-restricted-imports" 1 +"../../src/components/ConnectToMergeFlow/index.ios.tsx" "no-restricted-imports" 1 +"../../src/components/ConnectToMergeFlow/index.native.tsx" "no-restricted-imports" 1 +"../../src/components/ConnectToNetSuiteFlow/index.tsx" "react-hooks/refs" 5 +"../../src/components/ConnectToNetSuiteFlow/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/ConnectToXeroFlow/index.native.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/ConnectToXeroFlow/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/CountryPicker/CountrySelectorModal.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/CountryPicker/CountrySelectorModal.tsx" "no-restricted-imports" 1 +"../../src/components/CountryPicker/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/CountrySelector.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/createOnyxContext.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/CurrencyPicker.tsx" "no-restricted-imports" 1 +"../../src/components/CurrencySelector.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/CustomUnitDefaultCategorySelector/index.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/DatePicker/CalendarPicker/index.tsx" "react-hooks/purity" 2 +"../../src/components/DatePicker/CalendarPicker/MonthPickerModal.tsx" "no-restricted-imports" 1 +"../../src/components/DatePicker/CalendarPicker/MonthPickerModal.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/DatePicker/CalendarPicker/YearPickerModal.tsx" "no-restricted-imports" 1 +"../../src/components/DatePicker/CalendarPicker/YearPickerModal.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/DatePicker/DatePickerModal.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/DatePicker/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/DatePicker/index.tsx" "react-hooks/refs" 1 +"../../src/components/DatePicker/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/DialogLabelContext.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/DisplayNames/DisplayNamesWithTooltip.tsx" "react-hooks/refs" 6 +"../../src/components/DistanceEReceiptPanel.tsx" "import/no-cycle" 1 +"../../src/components/DistanceRequest/DistanceRequestRenderItem.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/DistanceRequest/DistanceRequestRenderItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/DotIndicatorMessage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/DotIndicatorMessage.tsx" "import/no-cycle" 1 +"../../src/components/DragAndDrop/NoDropZone/index.tsx" "react-hooks/refs" 1 +"../../src/components/DragAndDrop/Provider/index.tsx" "react-hooks/refs" 4 +"../../src/components/DraggableList/SortableItem.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/DraggableList/SortableItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/DropZone/DropZoneWrapper.tsx" "react-hooks/refs" 1 +"../../src/components/EditableCell/usePopoverEditState.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/EmbeddedDemo/index.native.tsx" "import/no-named-as-default" 1 +"../../src/components/EmojiPicker/EmojiPicker.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/EmojiPicker/EmojiPicker.tsx" "react-hooks/refs" 3 +"../../src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx" "react-hooks/preserve-manual-memoization" 1 +"../../src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx" "react-hooks/refs" 2 +"../../src/components/EmojiPicker/EmojiPickerMenu/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/EmojiPicker/EmojiPickerMenu/index.tsx" "react-hooks/preserve-manual-memoization" 1 +"../../src/components/EmojiPicker/EmojiPickerMenu/index.tsx" "react-hooks/refs" 2 +"../../src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts" "react-hooks/set-state-in-effect" 2 +"../../src/components/EmojiPicker/EmojiSkinToneList.tsx" "react-hooks/set-state-in-effect" 2 +"../../src/components/EmptySelectionListContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/EReceiptWithSizeCalculation.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/ErrorMessageRow.tsx" "import/no-cycle" 1 +"../../src/components/FeatureList.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/FeatureTraining/FeatureTrainingCarousel.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/FeedbackSurvey.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/FilePicker/index.native.tsx" "react-hooks/refs" 1 +"../../src/components/FilePicker/index.tsx" "react-hooks/refs" 1 +"../../src/components/FlatList/hooks/useFlatListHandle.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/FocusableMenuItem.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/FocusTrap/FocusTrapContainerElement/index.web.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/FocusTrap/FocusTrapForScreen/index.web.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Form/FormProvider.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/Form/FormProvider.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/Form/FormProvider.tsx" "react-hooks/immutability" 2 +"../../src/components/Form/FormProvider.tsx" "react-hooks/refs" 2 +"../../src/components/Form/InputWrapper.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/Form/InputWrapper.tsx" "react-hooks/refs" 1 +"../../src/components/FormHelpMessage.tsx" "import/no-cycle" 1 +"../../src/components/HeaderPageLayout.tsx" "no-restricted-imports" 2 +"../../src/components/HeaderWithBackButton/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/HighlightableMenuItem.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/HighlightableMenuItemWithTopDescription.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/Hoverable/ActiveHoverable.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Hoverable/ActiveHoverable.tsx" "react-hooks/refs" 3 +"../../src/components/Hoverable/ActiveHoverable.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/Hoverable/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Hoverable/index.tsx" "react-hooks/refs" 1 +"../../src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx" "@typescript-eslint/no-unsafe-type-assertion" 5 +"../../src/components/HTMLEngineProvider/HTMLRenderers/AccountManagerLinkRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx" "import/no-cycle" 1 +"../../src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.tsx" "@typescript-eslint/no-deprecated/fontSize" 3 +"../../src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/HTMLEngineProvider/HTMLRenderers/HTMLTableScroll/index.native.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/HTMLEngineProvider/HTMLRenderers/index.ts" "import/no-cycle" 2 +"../../src/components/HTMLEngineProvider/HTMLRenderers/MentionHereRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/HTMLEngineProvider/HTMLRenderers/RBRRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/HTMLEngineProvider/HTMLRenderers/ShortMentionRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/HTMLEngineProvider/HTMLRenderers/TransactionHistoryLinkRenderer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getSkiaLineMetrics.ts" "rulesdir/no-raw-typography" 1 +"../../src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx" "import/no-cycle" 1 +"../../src/components/HTMLEngineProvider/index.tsx" "no-restricted-syntax" 1 +"../../src/components/Icon/BankIcons/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Icon/BankIconsUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 45 +"../../src/components/Icon/ExpensifyIconLoader.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Icon/IllustrationLoader.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Image/BaseImage.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/components/Image/BaseImage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/components/Image/index.tsx" "react-hooks/preserve-manual-memoization" 2 +"../../src/components/Image/index.tsx" "react-hooks/purity" 1 +"../../src/components/Image/index.tsx" "react-hooks/refs" 1 +"../../src/components/ImageSVG/index.android.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/ImageSVG/index.ios.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/ImageSVG/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/ImportOnyxState/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/ImportSpreadsheet.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/components/ImportSpreadsheet.tsx" "no-restricted-imports" 1 +"../../src/components/ImportSpreadsheet.tsx" "react-hooks/refs" 1 +"../../src/components/InitialURLContextProvider.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/InteractiveStepWrapper.tsx" "no-restricted-imports" 1 +"../../src/components/KYCWall/BaseKYCWall.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/KYCWall/BaseKYCWall.tsx" "react-hooks/preserve-manual-memoization" 1 +"../../src/components/KYCWall/BaseKYCWall.tsx" "react-hooks/refs" 2 +"../../src/components/LHNOptionsList/LHNEmptyState.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/LHNOptionsList/OptionRowLHN/OptionRow/Pressable.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/LocaleContextProvider.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/LocaleContextProvider.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/Lottie/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/MapView/MapView.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/MapView/MapView.web.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/MapView/ToggleDistanceUnitButton/index.android.tsx" "no-restricted-syntax" 1 +"../../src/components/MenuItem/index.ts" "import/no-cycle" 4 +"../../src/components/MenuItem/leaves/content/MenuItemFieldValueHTML.tsx" "import/no-cycle" 1 +"../../src/components/MenuItem/leaves/helpText/BaseMenuItemHelpText.tsx" "import/no-cycle" 1 +"../../src/components/MenuItem/leaves/helpText/MenuItemHelpText.tsx" "import/no-cycle" 1 +"../../src/components/MenuItem/leaves/helpText/MenuItemHelpTextHTML.tsx" "import/no-cycle" 2 +"../../src/components/MenuItem/MenuItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/MenuItem/MenuItem.tsx" "import/no-cycle" 2 +"../../src/components/MenuItemList.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/MessagesRow.tsx" "import/no-cycle" 1 +"../../src/components/Modal/BaseModal.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/Modal/Global/ConfirmModalWrapper.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 +"../../src/components/Modal/Global/ModalContext.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/Modal/ReanimatedModal/Container/GestureHandler.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/Modal/ReanimatedModal/Container/GestureHandler.tsx" "@typescript-eslint/no-deprecated/GestureType" 1 +"../../src/components/Modal/ReanimatedModal/Container/index.web.tsx" "react-hooks/refs" 1 +"../../src/components/MoneyReportHeader.tsx" "no-restricted-imports" 1 +"../../src/components/MoneyReportHeaderActions/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/MoneyRequestAmountInput.tsx" "react-hooks/immutability" 2 +"../../src/components/MoneyRequestConfirmationList/ConfirmationFooterContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MoneyRequestConfirmationList/hooks/useDistanceRequestState.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MoneyRequestConfirmationList/hooks/useTransactionReportForConfirmation.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MoneyRequestConfirmationList/sections/AmountField.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/MoneyRequestConfirmationList/sections/AmountField.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MoneyRequestConfirmationList/sections/AttendeeField.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MoneyRequestConfirmationList/sections/CategoryField.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/MoneyRequestConfirmationList/sections/DateField.tsx" "react-hooks/purity" 2 +"../../src/components/MoneyRequestConfirmationList/sections/RateField.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/MoneyRequestConfirmationList/sections/selectors.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/MoneyRequestConfirmationList/sections/TaxFields.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MoneyRequestConfirmationListFooter/ConfirmationReceiptThumbnail.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MoneyRequestConfirmationListFooter/hooks/useReceiptThumbnailSource.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MoneyRequestConfirmationListFooter/sections/DistanceMapSection.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MoneyRequestConfirmationListFooter/sections/ReceiptSection.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MoneyRequestHeader.tsx" "no-restricted-imports" 1 +"../../src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MultifactorAuthentication/components/Modals/createCancelConfirmModal.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 +"../../src/components/MultifactorAuthentication/components/OutcomeScreen/OutcomeScreenBase.tsx" "no-restricted-imports" 1 +"../../src/components/MultifactorAuthentication/config/scenarios/ChangePIN.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MultifactorAuthentication/config/scenarios/DefaultUserInterface.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MultifactorAuthentication/Context/stateReducer.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MultifactorAuthentication/Context/usePromptContent.ts" "react-hooks/refs" 6 +"../../src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx" "no-restricted-syntax" 1 +"../../src/components/MultiGestureCanvas/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/MultiGestureCanvas/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Race" 2 +"../../src/components/MultiGestureCanvas/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Simultaneous" 2 +"../../src/components/MultiGestureCanvas/index.tsx" "@typescript-eslint/no-deprecated/GestureRef" 1 +"../../src/components/MultiGestureCanvas/index.tsx" "@typescript-eslint/no-deprecated/GestureType" 2 +"../../src/components/MultiGestureCanvas/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/MultiGestureCanvas/index.tsx" "no-restricted-syntax" 1 +"../../src/components/MultiGestureCanvas/index.tsx" "react-hooks/preserve-manual-memoization" 1 +"../../src/components/MultiGestureCanvas/index.tsx" "react-hooks/refs" 2 +"../../src/components/MultiGestureCanvas/usePanGesture.ts" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/MultiGestureCanvas/usePanGesture.ts" "@typescript-eslint/no-deprecated/LegacyPanGesture" 1 +"../../src/components/MultiGestureCanvas/usePanGesture.ts" "no-restricted-syntax" 1 +"../../src/components/MultiGestureCanvas/usePinchGesture.ts" "@typescript-eslint/no-deprecated/Gesture.Pinch" 2 +"../../src/components/MultiGestureCanvas/usePinchGesture.ts" "@typescript-eslint/no-deprecated/LegacyPinchGesture" 1 +"../../src/components/MultiGestureCanvas/usePinchGesture.ts" "react-hooks/set-state-in-effect" 1 +"../../src/components/MultiGestureCanvas/useTapGestures.ts" "@typescript-eslint/no-deprecated/Gesture.Tap" 4 +"../../src/components/MultiGestureCanvas/useTapGestures.ts" "@typescript-eslint/no-deprecated/LegacyTapGesture" 2 +"../../src/components/MultiGestureCanvas/useTapGestures.ts" "no-restricted-syntax" 1 +"../../src/components/Navigation/QuickCreationActionsBar/index.tsx" "no-restricted-imports" 1 +"../../src/components/NumberWithSymbolForm.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/NumberWithSymbolForm.tsx" "react-hooks/set-state-in-effect" 2 +"../../src/components/OfflineWithFeedback.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/OfflineWithFeedback.tsx" "import/no-cycle" 1 +"../../src/components/Onfido/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/OptionRow.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/OptionRow.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/ParentNavigationSubtitle.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/ParticipantPicker/BaseParticipantPicker.tsx" "no-restricted-imports" 1 +"../../src/components/PDFView/index.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/PDFView/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/PDFView/index.tsx" "import/no-cycle" 1 +"../../src/components/PDFView/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/PDFView/PDFPasswordForm.tsx" "import/no-cycle" 1 +"../../src/components/Picker/BasePicker.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/Picker/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/PopoverMenu/index.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/PopoverMenu/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/PopoverMenu/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/PopoverMenu/index.tsx" "unicorn/prefer-at" 1 +"../../src/components/PopoverMenu/v2/index.tsx" "import/no-cycle" 2 +"../../src/components/PopoverMenu/v2/rows/index.ts" "import/no-cycle" 3 +"../../src/components/PopoverMenu/v2/rows/Item.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/PopoverMenu/v2/rows/Item.tsx" "import/no-cycle" 2 +"../../src/components/PopoverMenu/v2/rows/Label.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/PopoverMenu/v2/rows/Label.tsx" "import/no-cycle" 1 +"../../src/components/PopoverMenu/v2/rows/RadioItem.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/PopoverMenu/v2/rows/RadioItem.tsx" "import/no-cycle" 2 +"../../src/components/PopoverMenu/v2/sub/index.ts" "import/no-cycle" 2 +"../../src/components/PopoverMenu/v2/sub/SubBackButton.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/PopoverMenu/v2/sub/SubBackButton.tsx" "import/no-cycle" 1 +"../../src/components/PopoverMenu/v2/sub/SubTrigger.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/PopoverMenu/v2/sub/SubTrigger.tsx" "import/no-cycle" 1 +"../../src/components/PopoverProvider/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx" "react-hooks/refs" 1 +"../../src/components/Pressable/GenericPressable/implementation/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/Pressable/PressableWithDelayToggle.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Pressable/PressResponder/useResponderRef.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/PressableWithSecondaryInteraction/index.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/PressableWithSecondaryInteraction/index.native.tsx" "no-restricted-syntax" 1 +"../../src/components/PressableWithSecondaryInteraction/index.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/PressableWithSecondaryInteraction/index.tsx" "no-restricted-syntax" 1 +"../../src/components/PromotedActionsBar.tsx" "no-restricted-imports" 1 +"../../src/components/PushRowWithModal/index.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/PushRowWithModal/PushRowModal.tsx" "no-restricted-imports" 1 +"../../src/components/RadioButtonWithLabel.tsx" "no-restricted-syntax" 1 +"../../src/components/Reactions/AddReactionBubble.tsx" "react-hooks/refs" 1 +"../../src/components/Reactions/MiniQuickEmojiReactions.tsx" "react-hooks/refs" 1 +"../../src/components/ReceiptAlternativeMethods.tsx" "import/no-cycle" 1 +"../../src/components/ReceiptCropView/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/ReceiptEmptyState.tsx" "import/no-cycle" 1 +"../../src/components/ReceiptHoverZoom/useReceiptHoverZoom.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/ReceiptImage/index.tsx" "import/no-cycle" 1 +"../../src/components/ReceiptImage/index.tsx" "react-hooks/purity" 1 +"../../src/components/ReimbursementAccountLoadingIndicator.tsx" "no-restricted-imports" 1 +"../../src/components/RenderHTML.tsx" "import/no-cycle" 1 +"../../src/components/ReportActionAvatars/index.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/ReportActionAvatars/useReportPreviewSenderID.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/ReportActionItem/ExportWithDropdownMenu.tsx" "react-hooks/preserve-manual-memoization" 3 +"../../src/components/ReportActionItem/MoneyReportView.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/ReportActionItem/MoneyRequestReceiptView.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/ReportActionItem/MoneyRequestReceiptView.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/ReportActionItem/MoneyRequestView.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 5 +"../../src/components/ReportActionItem/ReportActionItemImage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/components/ReportActionItem/SupportTicketView.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/ReportActionItem/TaskPreview.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/ReportActionItem/TaskView.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/ReportActionItem/TaskView.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 2 +"../../src/components/ReportActionItem/TaskView.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx" "@typescript-eslint/no-deprecated/fontSize" 2 +"../../src/components/ReportActionItem/TripDetailsView.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/ReportActionItem/TripRoomPreview.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/RNMarkdownTextInput.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/RNMaskedTextInput.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/RoomHeaderAvatars.tsx" "no-restricted-syntax" 2 +"../../src/components/Rule/RuleBooleanBase.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Rule/RuleBooleanBase.tsx" "no-restricted-imports" 1 +"../../src/components/Rule/RuleSelectionBase.tsx" "no-restricted-imports" 1 +"../../src/components/Rule/RuleTextBase.tsx" "no-restricted-imports" 1 +"../../src/components/Rule/TextBase.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/SAMLLoadingIndicator.tsx" "@typescript-eslint/no-deprecated/fontSize" 1 +"../../src/components/ScaledDistanceEReceipt.tsx" "import/no-cycle" 1 +"../../src/components/ScreenWrapper/ScreenWrapperContainer.tsx" "react-hooks/refs" 2 +"../../src/components/Search/chartGroupByConfig.ts" "@typescript-eslint/no-unsafe-type-assertion" 20 +"../../src/components/Search/FilterComponents/AdvancedFilters/DateFilterContent.tsx" "no-restricted-imports" 1 +"../../src/components/Search/FilterComponents/DateFilterBase.tsx" "no-restricted-imports" 1 +"../../src/components/Search/FilterComponents/DateFilterBase.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/Search/FilterComponents/DatePresetFilterBase.tsx" "@typescript-eslint/no-deprecated/MenuItem" 2 +"../../src/components/Search/FilterComponents/DatePresetFilterBase.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/Search/FilterComponents/RangeDatePicker.tsx" "react-hooks/purity" 2 +"../../src/components/Search/FilterComponents/ReportField/index.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/Search/FilterComponents/ReportField/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/FilterComponents/ReportField/index.tsx" "no-restricted-imports" 1 +"../../src/components/Search/FilterComponents/SingleSelect.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/FilterComponents/TypeSelector.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/FilterDropdowns/AmountPopup.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/components/Search/FilterDropdowns/BasePopup.tsx" "no-restricted-imports" 1 +"../../src/components/Search/FilterDropdowns/DateSelectPopup/SelectedDateModifierHeader.tsx" "no-restricted-imports" 1 +"../../src/components/Search/FilterDropdowns/SearchFooterPopup.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 3 +"../../src/components/Search/FilterDropdowns/SortByPopup.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/hooks/useOptimisticSearchTracking.ts" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/components/Search/hooks/useSearchSnapshot.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/Search/hooks/useStableOptimisticSortedData.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/components/Search/index.tsx" "react-hooks/preserve-manual-memoization" 5 +"../../src/components/Search/index.tsx" "react-hooks/refs" 5 +"../../src/components/Search/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/Search/SearchAutocompleteList.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/components/Search/SearchBulkActionsButton.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/Search/SearchChartView.tsx" "react-hooks/purity" 1 +"../../src/components/Search/SearchContextDefinitions.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/BaseListItemHeader.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/BaseListItemHeader.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/CardListItemHeader.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/CardListItemHeader.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/components/Search/SearchList/ListItem/ChatListItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/GroupChildrenContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/Search/SearchList/ListItem/MemberListItemHeader.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/MemberListItemHeader.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/components/Search/SearchList/ListItem/SearchMergeReportsListItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/TaskListItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/TaskListItemRow.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/TransactionGroupListItem.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/TransactionGroupListItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 18 +"../../src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemNarrow.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/TransactionListItem/TransactionListItemWide.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/WithdrawalIDListItemHeader.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/Search/SearchList/ListItem/WithdrawalIDListItemHeader.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/Search/SearchPageHeader/DatePickerFilterPopup.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/Search/SearchPageHeader/SearchFilterBar.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/Search/SearchRouter/buildSubstitutionsMap.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/SearchRouter/getAutocompleteSelectionSubstitutionKey.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/SearchRouter/getQueryWithSubstitutions.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/SearchRouter/getUpdatedSubstitutionsMap.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/SearchRouter/SearchRouter.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Search/SearchRouter/SearchRouter.tsx" "no-restricted-imports" 1 +"../../src/components/Search/SearchSingleSelectionPicker.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/Search/SearchTableHeader.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/components/SelectionButton.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/SelectionList/BaseSelectionList.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/SelectionList/components/Footer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/SelectionList/ListItem/SplitListItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/SelectionList/utils/getListboxRole/index.web.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/SelectionScreen.tsx" "no-restricted-imports" 1 +"../../src/components/SettlementButton/AnimatedSettlementButton.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/SettlementButton/index.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/components/SettlementButton/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 10 +"../../src/components/SettlementButton/index.tsx" "no-restricted-imports" 1 +"../../src/components/SidePanel/SidePanelOverlay.tsx" "no-restricted-syntax" 1 +"../../src/components/SidePanel/SidePanelReport/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/SignInButtons/AppleAuthWrapper/index.ios.tsx" "import/no-named-as-default" 1 +"../../src/components/SignInButtons/AppleSignIn/index.ios.tsx" "import/no-named-as-default" 1 +"../../src/components/SignInButtons/AppleSignIn/index.tsx" "import/no-named-as-default" 1 +"../../src/components/SignInButtons/IconButton.tsx" "no-restricted-syntax" 1 +"../../src/components/SkeletonViewContentLoader/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/SpendRules/configuration/SpendRuleCategoryBase.tsx" "no-restricted-imports" 1 +"../../src/components/SpendRules/configuration/SpendRuleMaxAmountBase.tsx" "no-restricted-imports" 1 +"../../src/components/SpendRules/configuration/SpendRuleMerchantEditBase.tsx" "no-restricted-imports" 1 +"../../src/components/SpendRules/configuration/SpendRuleMerchantsBase.tsx" "no-restricted-imports" 1 +"../../src/components/SpendRules/configuration/SpendRulesCurrencyBase.tsx" "no-restricted-imports" 1 +"../../src/components/StatePicker/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/StatePicker/StateSelectorModal.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/StatePicker/StateSelectorModal.tsx" "no-restricted-imports" 1 +"../../src/components/StateSelector.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/SubStepForms/AddressStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/SubStepForms/ConfirmationStep.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/SubStepForms/CountryFullStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/SubStepForms/DateOfBirthStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/SubStepForms/DateOfBirthStep.tsx" "react-hooks/purity" 2 +"../../src/components/SubStepForms/DocusignFullStep.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/components/SubStepForms/DocusignFullStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/SubStepForms/FullNameStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/SubStepForms/PushRowFieldsStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/SubStepForms/RegistrationNumberStep.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/SubStepForms/RegistrationNumberStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/SwipeableView/index.native.tsx" "react-hooks/refs" 2 +"../../src/components/SymbolButton.tsx" "no-restricted-syntax" 1 +"../../src/components/Table/middlewares/filtering.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Table/Table.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/components/Table/TableBody.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/Table/TableContext.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/Tables/WorkspaceCategoryRulesTable/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Tables/WorkspaceListTable/TransferOwnershipFlow.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/TabSelector/getBackground.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/TagPicker/index.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/TagPicker/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/TaxPicker.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Text.tsx" "@typescript-eslint/no-deprecated/fontSize" 1 +"../../src/components/TextInput/BaseTextInput/implementation/index.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/TextInput/BaseTextInput/implementation/index.native.tsx" "react-hooks/refs" 1 +"../../src/components/TextInput/BaseTextInput/implementation/index.native.tsx" "react-hooks/static-components" 1 +"../../src/components/TextInput/BaseTextInput/implementation/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/TextInput/BaseTextInput/implementation/index.tsx" "import/no-cycle" 1 +"../../src/components/TextInput/BaseTextInput/implementation/index.tsx" "react-hooks/refs" 1 +"../../src/components/TextInput/BaseTextInput/implementation/index.tsx" "react-hooks/static-components" 1 +"../../src/components/TextInput/BaseTextInput/implementations.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/TextInput/BaseTextInput/index.tsx" "import/no-cycle" 1 +"../../src/components/TextInput/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/TextInput/index.tsx" "import/no-cycle" 1 +"../../src/components/TextInput/index.tsx" "no-restricted-syntax" 1 +"../../src/components/TextInput/TextInputMeasurement/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/TextLinkBlock.tsx" "no-restricted-syntax" 1 +"../../src/components/TextPicker/index.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/TextPicker/TextSelectorModal.tsx" "no-restricted-imports" 1 +"../../src/components/TextPicker/TextSelectorModal.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/TextWithTooltip/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/ThreeDotsMenu/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/ThreeDotsMenu/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/TimeModalPicker.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/TimeModalPicker.tsx" "no-restricted-imports" 1 +"../../src/components/TimePicker/TimePicker.tsx" "@typescript-eslint/no-unsafe-type-assertion" 6 +"../../src/components/TimePicker/TimePicker.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/Tooltip/BaseGenericTooltip/index.native.tsx" "react-hooks/preserve-manual-memoization" 1 +"../../src/components/Tooltip/BaseGenericTooltip/index.native.tsx" "react-hooks/refs" 2 +"../../src/components/Tooltip/BaseGenericTooltip/index.tsx" "react-hooks/immutability" 1 +"../../src/components/Tooltip/BaseGenericTooltip/index.tsx" "react-hooks/refs" 2 +"../../src/components/Tooltip/BaseTooltip/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Tooltip/GenericTooltip.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/Tooltip/PopoverAnchorTooltip.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Tooltip/PopoverAnchorTooltip.tsx" "react-hooks/refs" 7 +"../../src/components/TransactionItemRow/DataCells/ChatBubbleCell.tsx" "rulesdir/no-raw-typography" 2 +"../../src/components/TransactionItemRow/DataCells/MerchantCell.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/UnitPicker.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/components/UploadFile.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/UserInitialsAvatar.tsx" "@typescript-eslint/no-deprecated/fontSize" 1 +"../../src/components/ValidateCodeInput.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/components/ValidateCodeInput.tsx" "react-hooks/set-state-in-effect" 3 +"../../src/components/ValuePicker/index.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/components/ValuePicker/ValueSelectorModal.tsx" "no-restricted-imports" 1 +"../../src/components/VideoPlayer/BaseVideoPlayer.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../src/components/VideoPlayer/BaseVideoPlayer.tsx" "@typescript-eslint/no-unsafe-type-assertion" 7 +"../../src/components/VideoPlayer/BaseVideoPlayer.tsx" "import/no-cycle" 3 +"../../src/components/VideoPlayer/BaseVideoPlayer.tsx" "react-hooks/refs" 13 +"../../src/components/VideoPlayer/BaseVideoPlayer.tsx" "react-hooks/set-state-in-effect" 2 +"../../src/components/VideoPlayer/index.tsx" "import/no-cycle" 2 +"../../src/components/VideoPlayer/useHandleNativeVideoControls/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/VideoPlayer/VideoPlayerControls/index.tsx" "import/no-cycle" 1 +"../../src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx" "@typescript-eslint/no-deprecated/PanGestureChangeEventPayload" 1 +"../../src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx" "@typescript-eslint/no-deprecated/PanGestureChangeEventPayload" 1 +"../../src/components/VideoPlayerContexts/PlaybackContext/playbackContextReportIDUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/VideoPlayerPreview/index.tsx" "import/no-cycle" 1 +"../../src/components/VideoPopoverMenu/index.tsx" "import/no-cycle" 1 +"../../src/components/WalletStatementModal/index.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/WideRHPContextProvider/useShouldRenderOverlay.ts" "react-hooks/set-state-in-effect" 1 +"../../src/components/withCurrentUserPersonalDetails.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/withNavigationFallback.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/components/withNavigationTransitionEnd.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/withToggleVisibilityView.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/withViewportOffsetTop.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/WorkspaceConfirmationForm.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 2 +"../../src/components/WorkspaceConfirmationForm.tsx" "no-restricted-imports" 1 +"../../src/components/WorkspaceMemberRoleList.tsx" "no-restricted-imports" 1 +"../../src/components/ZeroWidthView/index.tsx" "no-restricted-syntax" 2 +"../../src/CONFIG.ts" "import/no-named-as-default" 1 +"../../src/CONST/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/CONST/runtimeConfigured.ts" "import/no-named-as-default" 1 +"../../src/CONST/runtimeConfigured.ts" "rulesdir/no-direct-personal-details-list" 1 +"../../src/DeepLinkHandler.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/DeepLinkHandler.tsx" "no-restricted-syntax" 1 +"../../src/Expensify.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/Expensify.tsx" "no-restricted-syntax" 1 +"../../src/Expensify.tsx" "react-hooks/refs" 1 +"../../src/Expensify.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/GlobalModals.tsx" "no-restricted-syntax" 2 +"../../src/hooks/useAccountIndicatorChecks.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/hooks/useAdvancedSearchFilters.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useAnimatedHighlightStyle/index.ts" "react-hooks/set-state-in-effect" 2 +"../../src/hooks/useAssignCard.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useAutocompleteSuggestions.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useAutoCreateTrackWorkspace.ts" "no-restricted-imports" 1 +"../../src/hooks/useAutoUpdateTimezone.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useBulkDuplicateAction.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/hooks/useBulkDuplicateAction.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useBulkPayOptions.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/hooks/useCachedImageSource.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useCachedImageSource.ts" "react-hooks/set-state-in-effect" 1 +"../../src/hooks/useCancellationType.ts" "react-hooks/refs" 2 +"../../src/hooks/useCancellationType.ts" "react-hooks/set-state-in-effect" 1 +"../../src/hooks/useCardFeedsForActivePolicies.ts" "no-restricted-imports" 1 +"../../src/hooks/useCardFeedsForDisplay.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useCardFeedsForDisplay.ts" "no-restricted-imports" 1 +"../../src/hooks/useChangeTransactionsReportReports.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/hooks/useCurrentReportID.tsx" "react-hooks/preserve-manual-memoization" 1 +"../../src/hooks/useDebounce.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useDebouncedValue.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useDebounceNonReactive.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useDragAndDrop/types.ts" "@typescript-eslint/no-deprecated/React.MutableRefObject" 1 +"../../src/hooks/useDynamicBackPath.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/hooks/useDynamicBackPath.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useDynamicForwardPath.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/hooks/useExportedToFilterOptions.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useFilterFormValues.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/hooks/useHtmlPaste/index.ts" "react-hooks/preserve-manual-memoization" 1 +"../../src/hooks/useHydrateReportsFromSnapshot.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useImportSpreadsheetConfirmModal.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useInitial.ts" "react-hooks/refs" 2 +"../../src/hooks/useInitialSelection.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useIsBlockedToAddFeed.ts" "react-hooks/set-state-in-effect" 1 +"../../src/hooks/useIsOwnWorkspaceChatRef.ts" "react-hooks/refs" 2 +"../../src/hooks/useIsPaidPolicyAdmin.ts" "no-restricted-imports" 1 +"../../src/hooks/useKeyboardShortcut.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useLazyAsset.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/hooks/useLazyAsset.ts" "react-hooks/set-state-in-effect" 1 +"../../src/hooks/useMarkdownStyle.ts" "rulesdir/no-raw-typography" 3 +"../../src/hooks/useNewTransactions.ts" "react-hooks/preserve-manual-memoization" 3 +"../../src/hooks/useNewTransactions.ts" "react-hooks/refs" 2 +"../../src/hooks/useOnyx.ts" "@typescript-eslint/no-unsafe-type-assertion" 10 +"../../src/hooks/useOptimisticDraftTransactions.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useOtherFeedsForFeedSelector.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/hooks/usePageRefresh/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useParticipantSubmission.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/usePaymentOptions.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/hooks/usePaymentOptions.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/hooks/usePaymentOptions.ts" "react-hooks/preserve-manual-memoization" 6 +"../../src/hooks/usePaymentOptions.ts" "react-hooks/refs" 1 +"../../src/hooks/usePersonalPolicy.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/usePolicyData/index.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/hooks/usePolicyData/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/usePreferredCurrency.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/usePrevious.ts" "react-hooks/refs" 1 +"../../src/hooks/useReportScrollManager/index.native.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useResetBankAccountModal.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useResponsiveLayout/index.native.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useResponsiveLayout/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useRestoreWorkspacesTabOnNavigate.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useReviewDuplicatesNavigation.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useReviewDuplicatesNavigation.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/hooks/useSearchBulkActions.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/hooks/useSearchBulkActions.ts" "@typescript-eslint/no-unsafe-type-assertion" 9 +"../../src/hooks/useSidebarOrderedReports.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useSidebarOrderedReports.tsx" "react-hooks/preserve-manual-memoization" 5 +"../../src/hooks/useSidebarOrderedReports.tsx" "react-hooks/refs" 5 +"../../src/hooks/useSidebarOrderedReports.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/hooks/useSingleExecution/index.native.ts" "react-hooks/refs" 1 +"../../src/hooks/useStableIndexedHandler.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/hooks/useStableIndexedHandler.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useStepFormSubmit.ts" "no-restricted-syntax" 1 +"../../src/hooks/useSubPage/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/hooks/useThemePreference.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/hooks/useTransactionViolationOfWorkspace.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useViolations.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useWindowDimensions/index.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/hooks/useWorkletStateMachine/index.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/hooks/useWorkletStateMachine/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/languages/flattenObject.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/AccountingUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/__mocks__/App.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/__mocks__/App.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/__mocks__/OnyxUpdates.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/App.ts" "import/no-cycle" 1 +"../../src/libs/actions/App.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/applyOnyxUpdatesReliably.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/BankAccounts.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/BankAccounts.ts" "import/no-cycle" 1 +"../../src/libs/actions/BankAccounts.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/CanvasSize.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/Card.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/Card.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/Chronos.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/clearOnyxAndSeedFullReconnect.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/ClearReportActionErrors.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/CompanyCards.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/actions/CompanyCards.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/CompanyCards.ts" "no-restricted-imports" 1 +"../../src/libs/actions/CompanyCards.ts" "no-restricted-syntax" 4 +"../../src/libs/actions/connections/index.ts" "import/no-cycle" 1 +"../../src/libs/actions/connections/index.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/connections/merge/index.ts" "import/no-cycle" 1 +"../../src/libs/actions/connections/NetSuiteCommands.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/connections/QuickbooksDesktop.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/connections/QuickbooksOnline.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/connections/SageIntacct.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/connections/Xero.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/Delegate.ts" "no-restricted-syntax" 4 +"../../src/libs/actions/Delegate.ts" "rulesdir/no-direct-personal-details-list" 1 +"../../src/libs/actions/Domain.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/ExitSurvey.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/getCompanyCardBankConnection/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/libs/actions/getCompanyCardBankConnection/index.tsx" "no-restricted-imports" 1 +"../../src/libs/actions/getCompanyCardBankConnection/index.tsx" "no-restricted-syntax" 2 +"../../src/libs/actions/Help.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/HybridApp/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/ImportTransactions.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/actions/ImportTransactions.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/ImportTransactions.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/InputFocus/index.web.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/IOU/BulkEdit.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/IOU/BulkEdit.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/IOU/DeleteMoneyRequest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/IOU/DeleteMoneyRequest.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/IOU/Duplicate.ts" "@typescript-eslint/no-unsafe-type-assertion" 7 +"../../src/libs/actions/IOU/Duplicate.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/IOU/Hold.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../src/libs/actions/IOU/Hold.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/IOU/index.ts" "rulesdir/no-onyx-connect" 7 +"../../src/libs/actions/IOU/MoneyRequest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/IOU/MoneyRequestBuilder.ts" "@typescript-eslint/no-deprecated/getReportTransactions" 2 +"../../src/libs/actions/IOU/MoneyRequestBuilder.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../src/libs/actions/IOU/MoneyRequestBuilder.ts" "@typescript-eslint/no-deprecated/translateLocal" 1 +"../../src/libs/actions/IOU/MoneyRequestBuilder.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/IOU/PayMoneyRequest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/actions/IOU/PayMoneyRequest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/IOU/PayMoneyRequest.ts" "no-restricted-imports" 1 +"../../src/libs/actions/IOU/PayMoneyRequest.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/IOU/PerDiem.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/actions/IOU/PerDiem.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/IOU/Receipt.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/IOU/RejectMoneyRequest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/IOU/RejectMoneyRequest.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/IOU/ReportWorkflow.ts" "@typescript-eslint/no-deprecated/getReportTransactions" 2 +"../../src/libs/actions/IOU/ReportWorkflow.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/IOU/SearchUpdate.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../src/libs/actions/IOU/SendInvoice.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/IOU/SendInvoice.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/IOU/SendMoney.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/IOU/SendMoney.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/IOU/Split.ts" "@typescript-eslint/no-deprecated/Localize.translateLocal" 2 +"../../src/libs/actions/IOU/Split.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/IOU/Split.ts" "no-restricted-syntax" 3 +"../../src/libs/actions/IOU/SplitExpenseItems.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/IOU/SplitTransactionUpdate.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../src/libs/actions/IOU/SplitTransactionUpdate.ts" "@typescript-eslint/no-unsafe-type-assertion" 20 +"../../src/libs/actions/IOU/StartExpenseFlows.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/IOU/TrackExpense.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../src/libs/actions/IOU/TrackExpense.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/IOU/TrackExpense.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/IOU/UpdateMoneyRequest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/libs/actions/IOU/UpdateMoneyRequest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/IOU/UpdateMoneyRequest.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/Link.ts" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/libs/actions/Link.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/MapboxToken.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/MergeAccounts.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/MergeTransaction.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/MergeTransaction.ts" "no-restricted-imports" 1 +"../../src/libs/actions/MergeTransaction.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/OdometerTransactionUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/OnyxDerived/configs/cardFeedErrors.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/actions/OnyxDerived/configs/guideAccountIDs.ts" "rulesdir/no-direct-personal-details-list" 1 +"../../src/libs/actions/OnyxDerived/configs/loginToAccountIDMap.ts" "rulesdir/no-direct-personal-details-list" 1 +"../../src/libs/actions/OnyxDerived/configs/reportAttributes.ts" "rulesdir/no-direct-personal-details-list" 2 +"../../src/libs/actions/OnyxDerived/index.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/libs/actions/OnyxDerived/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/OnyxUpdateManager/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/OnyxUpdateManager/utils/applyUpdates.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/OnyxUpdateManager/utils/DeferredOnyxUpdates.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/OnyxUpdateManager/utils/DeferredOnyxUpdates.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/OnyxUpdates.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/PaymentMethods.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/PersistedRequests.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/libs/actions/PersonalCards.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/actions/PersonalCards.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/PersonalDetails.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/PersonalDetails.ts" "no-restricted-syntax" 5 +"../../src/libs/actions/Plaid.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/Policy/Category.ts" "no-restricted-syntax" 5 +"../../src/libs/actions/Policy/DistanceRate.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../src/libs/actions/Policy/DistanceRate.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/Policy/Member.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/Policy/Member.ts" "no-restricted-imports" 1 +"../../src/libs/actions/Policy/Member.ts" "no-restricted-syntax" 7 +"../../src/libs/actions/Policy/Member.ts" "rulesdir/no-direct-personal-details-list" 1 +"../../src/libs/actions/Policy/Member.ts" "rulesdir/no-onyx-connect" 1 +"../../src/libs/actions/Policy/PerDiem.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/Policy/Plan.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/Policy/Policy.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/libs/actions/Policy/Policy.ts" "@typescript-eslint/no-unsafe-type-assertion" 8 +"../../src/libs/actions/Policy/Policy.ts" "no-restricted-imports" 2 +"../../src/libs/actions/Policy/Policy.ts" "no-restricted-syntax" 9 +"../../src/libs/actions/Policy/ReportField.ts" "no-restricted-imports" 1 +"../../src/libs/actions/Policy/ReportField.ts" "no-restricted-syntax" 4 +"../../src/libs/actions/Policy/Rules.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/Policy/Tag.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/Policy/Tag.ts" "no-restricted-imports" 1 +"../../src/libs/actions/Policy/Tag.ts" "no-restricted-syntax" 4 +"../../src/libs/actions/Policy/Travel.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/PolicyConnections.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/QueuedOnyxUpdates.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/ReimbursementAccount/resetNonUSDBankAccount.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/replaceOptimisticReportWithActualReport.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/Report/index.ts" "@typescript-eslint/no-deprecated/getReportTransactions" 1 +"../../src/libs/actions/Report/index.ts" "@typescript-eslint/no-deprecated/originalMessage" 11 +"../../src/libs/actions/Report/index.ts" "@typescript-eslint/no-deprecated/reportAction.originalMessage" 1 +"../../src/libs/actions/Report/index.ts" "@typescript-eslint/no-deprecated/videoAttributeCache" 1 +"../../src/libs/actions/Report/index.ts" "@typescript-eslint/no-unnecessary-type-assertion" 5 +"../../src/libs/actions/Report/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 19 +"../../src/libs/actions/Report/index.ts" "no-restricted-syntax" 7 +"../../src/libs/actions/Report/index.ts" "rulesdir/no-onyx-connect" 2 +"../../src/libs/actions/Report/index.ts" "unicorn/prefer-string-replace-all" 1 +"../../src/libs/actions/Report/MarkAllMessageAsRead.tsx" "no-restricted-syntax" 1 +"../../src/libs/actions/ReportLayout.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/ReportLayout.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/RequestConflictUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/ScheduleCall.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/Search.ts" "@typescript-eslint/no-misused-promises" 1 +"../../src/libs/actions/Search.ts" "@typescript-eslint/no-unnecessary-type-assertion" 6 +"../../src/libs/actions/Search.ts" "@typescript-eslint/no-unsafe-type-assertion" 16 +"../../src/libs/actions/Search.ts" "import/no-cycle" 1 +"../../src/libs/actions/Session/index.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/actions/Session/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/libs/actions/Session/index.ts" "no-restricted-syntax" 9 +"../../src/libs/actions/Session/index.ts" "rulesdir/no-onyx-connect" 2 +"../../src/libs/actions/SplitExpenses.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/StatsCounter.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/StoreReview/index.native.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/libs/actions/Subscription.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/Subscription.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/Tab.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/Task.ts" "@typescript-eslint/no-unnecessary-type-assertion" 8 +"../../src/libs/actions/Task.ts" "import/no-cycle" 6 +"../../src/libs/actions/Task.ts" "no-restricted-imports" 1 +"../../src/libs/actions/Task.ts" "no-restricted-syntax" 5 +"../../src/libs/actions/TaskDeletion.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/actions/TaxRate.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/TeachersUnite.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/Transaction.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/actions/Transaction.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/Transaction.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/TransactionEdit.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/actions/Travel.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/TravelBilling.ts" "no-restricted-syntax" 3 +"../../src/libs/actions/UnreportedExpenses.tsx" "no-restricted-syntax" 1 +"../../src/libs/actions/User.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/libs/actions/User.ts" "no-restricted-imports" 1 +"../../src/libs/actions/User.ts" "no-restricted-syntax" 7 +"../../src/libs/actions/VacationDelegate.ts" "no-restricted-syntax" 2 +"../../src/libs/actions/Wallet.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/libs/actions/Wallet.ts" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/libs/actions/Wallet.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/Welcome/index.ts" "import/no-cycle" 2 +"../../src/libs/actions/Welcome/index.ts" "no-restricted-syntax" 1 +"../../src/libs/actions/Welcome/OnboardingFlow.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/actions/Welcome/OnboardingFlow.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/actions/Welcome/OnboardingFlow.ts" "import/no-cycle" 2 +"../../src/libs/actions/Workflow.ts" "import/no-cycle" 3 +"../../src/libs/API/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/API/makeRequest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/AttendeeUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/libs/BankAccountUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/CardFeedUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 7 +"../../src/libs/CardFeedUtils.ts" "import/no-cycle" 2 +"../../src/libs/CardUtils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../src/libs/CardUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 12 +"../../src/libs/CategoryOptionListUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/ChronosUtils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/clearSelectedTextIfComposerBlurred/clearSelectedTextIfComposerBlurred.web.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Clipboard/index.ts" "@typescript-eslint/no-deprecated/document.execCommand" 1 +"../../src/libs/Clipboard/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Clipboard/index.ts" "no-restricted-syntax" 1 +"../../src/libs/ComposerFocusManager.ts" "@typescript-eslint/no-deprecated/TextInput.State.currentlyFocusedField" 1 +"../../src/libs/ComposerFocusManager.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/ComposerUtils/index.ts" "no-restricted-syntax" 1 +"../../src/libs/CountryUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/CurrencyUtils.ts" "rulesdir/no-onyx-connect" 1 +"../../src/libs/DateUtils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/DateUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/libs/DebugUtils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/DebugUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 20 +"../../src/libs/DistanceRequestUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/DistanceRequestUtils.ts" "import/no-cycle" 2 +"../../src/libs/DomUtils/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/EmojiUtils.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Environment/Environment.ts" "import/no-named-as-default" 1 +"../../src/libs/Environment/getEnvironment/index.native.ts" "import/no-named-as-default" 1 +"../../src/libs/Environment/getEnvironment/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Environment/getEnvironment/index.ts" "import/no-named-as-default" 1 +"../../src/libs/ErrorUtils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/libs/ErrorUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/libs/ExpenseRuleUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/ExpensifyCardFeedSelectorUtils.ts" "import/no-cycle" 1 +"../../src/libs/ExportOnyxState/masking.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/ExportOnyxState/masking.ts" "@typescript-eslint/no-unsafe-type-assertion" 14 +"../../src/libs/fileDownload/DownloadUtils.ts" "no-restricted-syntax" 1 +"../../src/libs/fileDownload/FileUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/libs/fileDownload/getImageManipulator/index.native.ts" "@typescript-eslint/no-deprecated/manipulateAsync" 1 +"../../src/libs/fileDownload/getImageManipulator/index.ts" "@typescript-eslint/no-deprecated/manipulateAsync" 1 +"../../src/libs/fileDownload/getImageResolution.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/fileDownload/heicConverter/index.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/libs/fileDownload/index.android.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/fileDownload/index.ios.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/filterArrayByMatch.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/focusComposerWithDelay/index.ts" "no-restricted-syntax" 1 +"../../src/libs/focusComposerWithDelay/setTextInputSelection.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/Formula.ts" "@typescript-eslint/no-deprecated/getReportTransactions" 1 +"../../src/libs/Fullstory/common.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/getPlatform/index.ts" "no-restricted-syntax" 1 +"../../src/libs/getPolicyFromSearchData.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/getReturnValue.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/hadTabNavigation.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/HttpUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/libs/ImportOnyxStateUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 7 +"../../src/libs/interceptAnonymousUser.ts" "no-restricted-syntax" 1 +"../../src/libs/KeyboardShortcut/bindHandlerToKeydownEvent/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/KeyboardShortcut/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/libs/KeyboardShortcut/isEnterWhileComposition.ts" "@typescript-eslint/no-deprecated/event.keyCode" 1 +"../../src/libs/KeyboardShortcut/isEnterWhileComposition.ts" "no-restricted-syntax" 1 +"../../src/libs/LocalePhoneNumber.ts" "rulesdir/no-onyx-connect" 1 +"../../src/libs/Localize/index.ts" "@typescript-eslint/no-deprecated/translateLocal" 1 +"../../src/libs/Localize/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/Log.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/LoginUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/mapChildrenFlat.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/memoize/cache/ArrayCache.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/memoize/index.ts" "@typescript-eslint/no-unsafe-return" 3 +"../../src/libs/memoize/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 8 +"../../src/libs/memoize/utils.ts" "@typescript-eslint/no-unsafe-type-assertion" 7 +"../../src/libs/merge/HRUtils.ts" "import/no-cycle" 1 +"../../src/libs/MergeTransactionUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 5 +"../../src/libs/Middleware/HandleUnusedOptimisticID.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/Middleware/HandleUnusedOptimisticID.ts" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/libs/Middleware/HandleUnusedOptimisticID.ts" "no-restricted-syntax" 1 +"../../src/libs/Middleware/HandleUnusedOptimisticID.ts" "rulesdir/no-direct-personal-details-list" 1 +"../../src/libs/Middleware/Logging.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Middleware/Pagination.ts" "@typescript-eslint/no-unsafe-type-assertion" 5 +"../../src/libs/Middleware/Reauthentication.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/Middleware/RecordFullReconnectTime.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/Middleware/SaveResponseInOnyx.ts" "no-restricted-syntax" 1 +"../../src/libs/models/BankAccount.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/ModifiedExpenseMessage.ts" "@typescript-eslint/no-unsafe-type-assertion" 6 +"../../src/libs/ModifiedExpenseMessage.ts" "import/no-cycle" 3 +"../../src/libs/MoneyRequestReportUtils.ts" "no-restricted-imports" 1 +"../../src/libs/MoneyRequestUtils.ts" "import/no-cycle" 2 +"../../src/libs/Navigation/AppNavigator/AuthScreens.tsx" "no-restricted-syntax" 1 +"../../src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx" "no-restricted-syntax" 6 +"../../src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx" "react-hooks/refs" 1 +"../../src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts" "@typescript-eslint/no-unsafe-type-assertion" 13 +"../../src/libs/Navigation/AppNavigator/createRootStackNavigator/useCustomRootStackNavigatorState/restoreTabNavigatorRoutes.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/AppNavigator/createWorkspaceNavigator/WorkspaceRouter.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/AppNavigator/enhanceCardStyleInterpolator.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../src/libs/Navigation/AppNavigator/FreezeWrapper/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/libs/Navigation/AppNavigator/KeyboardShortcutsHandler/EscapeHandler.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/Navigation/AppNavigator/KeyboardShortcutsHandler/NewChatHandler.tsx" "no-restricted-syntax" 2 +"../../src/libs/Navigation/AppNavigator/KeyboardShortcutsHandler/SearchHandler.tsx" "no-restricted-syntax" 1 +"../../src/libs/Navigation/AppNavigator/KeyboardShortcutsHandler/ShortcutsOverviewHandler.tsx" "no-restricted-syntax" 1 +"../../src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 5 +"../../src/libs/Navigation/AppNavigator/Navigators/Overlay/BaseOverlay.tsx" "no-restricted-syntax" 2 +"../../src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx" "no-restricted-syntax" 1 +"../../src/libs/Navigation/AppNavigator/Navigators/SearchFullscreenNavigator.tsx" "no-restricted-syntax" 1 +"../../src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/AppNavigator/Navigators/TestToolsModalNavigator.tsx" "no-restricted-syntax" 1 +"../../src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/AppNavigator/PublicScreens.tsx" "import/no-named-as-default" 1 +"../../src/libs/Navigation/AppNavigator/routerExtensions/addPushParamsRouterExtension.ts" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/libs/Navigation/AppNavigator/routerExtensions/addRootHistoryRouterExtension.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/AppNavigator/routerExtensions/addRootHistoryRouterExtensionUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/DebugTabNavigator.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/guards/MigratedUserWelcomeModalGuard.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/guards/OnboardingGuard.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/libs/Navigation/guards/OnboardingGuard.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/Navigation/helpers/buildTabNavigatorNestedState.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/Navigation/helpers/createNormalizedConfigs.ts" "@typescript-eslint/no-deprecated/escape" 1 +"../../src/libs/Navigation/helpers/createNormalizedConfigs.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/Navigation/helpers/dynamicRoutesUtils/getDynamicRouteAdaptedState.ts" "@typescript-eslint/no-unnecessary-type-assertion" 5 +"../../src/libs/Navigation/helpers/dynamicRoutesUtils/getDynamicRouteAdaptedState.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/dynamicRoutesUtils/getDynamicRouteQueryParams.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/dynamicRoutesUtils/getPathWithoutDynamicSuffix.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/findFocusedRouteWithOnyxTabGuard.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/Navigation/helpers/getAdaptedStateFromPath.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../src/libs/Navigation/helpers/getAdaptedStateFromPath.ts" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/libs/Navigation/helpers/getAdaptedStateFromPath.ts" "import/no-cycle" 1 +"../../src/libs/Navigation/helpers/getOnboardingRouteFromScreen.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/Navigation/helpers/getParamsFromRoute.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/Navigation/helpers/getPathFromState.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/Navigation/helpers/getPathFromState.ts" "@typescript-eslint/no-unsafe-type-assertion" 6 +"../../src/libs/Navigation/helpers/getStateFromPath.ts" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/libs/Navigation/helpers/getTopmostFullScreenRoute.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/getTopmostReportParams.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/goBackFromWorkspaceSettingPages.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/isNavigatorName.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/isRHPOnSearchMoneyRequestReportPage.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/isTabRouteAtRoot.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/lastVisitedTabPathUtils/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/linkTo/index.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/libs/Navigation/helpers/linkTo/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 15 +"../../src/libs/Navigation/helpers/navigateToWorkspacesPage.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/Navigation/helpers/navigateToWorkspacesPage.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/popReportsSplitNavigatorToReport.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/replaceCompanyCardsRoute.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/swapBackgroundTabForRHPTarget.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/Navigation/helpers/swapBackgroundTabForRHPTarget.ts" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/libs/Navigation/helpers/tabNavigatorUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/Navigation/helpers/useIsSidebarRouteActive.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/linkingConfig/config.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/Navigation/linkingConfig/config.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/Navigation/linkingConfig/RELATIONS/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/libs/Navigation/Navigation.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/Navigation/Navigation.ts" "@typescript-eslint/no-unsafe-type-assertion" 10 +"../../src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.native.tsx" "@typescript-eslint/no-deprecated/NavigationContent" 1 +"../../src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx" "@typescript-eslint/no-deprecated/NavigationContent" 1 +"../../src/libs/Navigation/PlatformStackNavigation/navigationOptions/animation/withAnimation.ts" "@typescript-eslint/no-unsafe-type-assertion" 6 +"../../src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/NetSuiteUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Network/enhanceParameters.ts" "no-restricted-syntax" 1 +"../../src/libs/Network/index.ts" "no-restricted-syntax" 1 +"../../src/libs/Network/LoadTestState.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Network/SequentialQueue.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Notification/LocalNotification/BrowserNotifications.ts" "@typescript-eslint/no-deprecated/translateLocal" 1 +"../../src/libs/Notification/LocalNotification/BrowserNotifications.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Notification/LocalNotification/BrowserNotifications.ts" "no-restricted-imports" 1 +"../../src/libs/Notification/LocalNotification/BrowserNotifications.ts" "no-restricted-syntax" 1 +"../../src/libs/Notification/PushNotification/parsePushNotificationPayload.ts" "@typescript-eslint/no-unsafe-type-assertion" 5 +"../../src/libs/Notification/PushNotification/shouldShowPushNotification.ts" "no-restricted-syntax" 2 +"../../src/libs/Notification/PushNotification/subscribeToPushNotifications.ts" "no-restricted-syntax" 1 +"../../src/libs/ObjectUtils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/ObjectUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/OptionsListUtils/index.ts" "@typescript-eslint/no-deprecated/translateLocal" 2 +"../../src/libs/OptionsListUtils/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/libs/PageHTMLCapture/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Parser.ts" "rulesdir/no-onyx-connect" 1 +"../../src/libs/ParsingUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/PaymentUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/PersonalDetailsUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/PersonalDetailsUtils.ts" "unicorn/prefer-at" 1 +"../../src/libs/PhoneNumber.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/PolicyUtils/approvals.ts" "import/no-cycle" 3 +"../../src/libs/PolicyUtils/connections.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/PolicyUtils/connections.ts" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/libs/PolicyUtils/connections.ts" "import/no-cycle" 1 +"../../src/libs/PolicyUtils/domains.ts" "import/no-cycle" 1 +"../../src/libs/PolicyUtils/errors.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/PolicyUtils/errors.ts" "import/no-cycle" 3 +"../../src/libs/PolicyUtils/features.ts" "import/no-cycle" 2 +"../../src/libs/PolicyUtils/index.ts" "import/no-cycle" 9 +"../../src/libs/PolicyUtils/members.ts" "import/no-cycle" 2 +"../../src/libs/PolicyUtils/policy.ts" "import/no-cycle" 2 +"../../src/libs/PolicyUtils/rules.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/PolicyUtils/tag.ts" "import/no-cycle" 1 +"../../src/libs/PolicyUtils/travel.ts" "import/no-cycle" 2 +"../../src/libs/prepareRequestPayload/index.native.ts" "@typescript-eslint/no-unsafe-type-assertion" 5 +"../../src/libs/prepareRequestPayload/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Pusher/index.native.ts" "@typescript-eslint/no-unsafe-type-assertion" 6 +"../../src/libs/Pusher/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/QuickActionUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/ReceiptUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/ReportActionMessageUtils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/ReportActionsUtils.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../src/libs/ReportActionsUtils.ts" "@typescript-eslint/no-deprecated/reportAction.sequenceNumber" 1 +"../../src/libs/ReportActionsUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 109 +"../../src/libs/ReportActionsUtils.ts" "import/no-cycle" 1 +"../../src/libs/ReportActionsUtils.ts" "rulesdir/no-onyx-connect" 2 +"../../src/libs/ReportAlternateTextUtils.ts" "@typescript-eslint/no-deprecated/getReportTransactions" 1 +"../../src/libs/ReportNameUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/ReportNameUtils.ts" "import/no-cycle" 4 +"../../src/libs/ReportPrimaryActionUtils.ts" "no-restricted-imports" 1 +"../../src/libs/ReportTitleUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/ReportUtils.ts" "@typescript-eslint/no-deprecated/getPolicy" 27 +"../../src/libs/ReportUtils.ts" "@typescript-eslint/no-deprecated/getReportTransactions" 16 +"../../src/libs/ReportUtils.ts" "@typescript-eslint/no-deprecated/originalMessage" 23 +"../../src/libs/ReportUtils.ts" "@typescript-eslint/no-deprecated/translateLocal" 7 +"../../src/libs/ReportUtils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 15 +"../../src/libs/ReportUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 31 +"../../src/libs/ReportUtils.ts" "import/no-cycle" 5 +"../../src/libs/ReportUtils.ts" "rulesdir/no-onyx-connect" 13 +"../../src/libs/sanitizeLogParams.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/SearchQueryUtils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/libs/SearchQueryUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 48 +"../../src/libs/SearchQueryUtils.ts" "import/no-cycle" 4 +"../../src/libs/SearchQueryUtils.ts" "unicorn/prefer-string-replace-all" 1 +"../../src/libs/SearchUIUtils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../src/libs/SearchUIUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 71 +"../../src/libs/SearchUIUtils.ts" "import/no-cycle" 1 +"../../src/libs/SearchUIUtils.ts" "no-restricted-imports" 1 +"../../src/libs/ShortcutManager/index.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/SidebarUtils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/SidebarUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Sound/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/libs/SpendRulesUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/libs/StatusBar/index.android.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/StatusBar/index.web.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/StatusBar/types.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/StringUtils/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/telemetry/forwardLogsToSentry.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/telemetry/TelemetrySynchronizer.ts" "rulesdir/no-direct-personal-details-list" 1 +"../../src/libs/TransactionPreviewUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/TransactionUtils/buildOptimisticTransaction.ts" "import/no-cycle" 1 +"../../src/libs/TransactionUtils/duplicates.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/libs/TransactionUtils/duplicates.ts" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/libs/TransactionUtils/duplicates.ts" "import/no-cycle" 2 +"../../src/libs/TransactionUtils/getUpdatedTransaction.ts" "@typescript-eslint/no-deprecated/translateLocal" 2 +"../../src/libs/TransactionUtils/getUpdatedTransaction.ts" "import/no-cycle" 2 +"../../src/libs/TransactionUtils/index.ts" "@typescript-eslint/no-deprecated/getReportTransactions" 1 +"../../src/libs/TransactionUtils/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/libs/TransactionUtils/index.ts" "import/no-cycle" 7 +"../../src/libs/TransactionUtils/tax.ts" "@typescript-eslint/no-deprecated/translateLocal" 1 +"../../src/libs/TransactionUtils/tax.ts" "import/no-cycle" 2 +"../../src/libs/TransactionUtils/violations.ts" "import/no-cycle" 2 +"../../src/libs/UnreadIndicatorUpdater/index.ts" "no-restricted-imports" 1 +"../../src/libs/UnreadIndicatorUpdater/index.ts" "no-restricted-syntax" 1 +"../../src/libs/updateMultilineInputRange/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Url.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/UserAvatarUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/libs/ValidationUtils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/ValidationUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/Violations/ViolationsUtils.ts" "import/no-cycle" 4 +"../../src/libs/Violations/ViolationsUtils.ts" "no-restricted-syntax" 1 +"../../src/libs/Wallet/index.ios.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/WorkflowUtils.ts" "import/no-cycle" 2 +"../../src/libs/WorkspaceReportFieldUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/libs/WorkspacesSettingsUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/ONYXKEYS.ts" "no-restricted-syntax" 2 +"../../src/pages/AddExistingExpense.tsx" "no-restricted-imports" 1 +"../../src/pages/AddPersonalBankAccountPage/substeps/AddressStep.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/AddPersonalBankAccountPage/substeps/AddressStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 6 +"../../src/pages/AddPersonalBankAccountPage/substeps/AddressStep.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/AddressPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/AddressPage.tsx" "no-restricted-imports" 1 +"../../src/pages/AddressPage.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/ChronosScheduleOOOPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/ChronosScheduleOOOPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Debug/ConstantPicker.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/Debug/ConstantSelector.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/Debug/DateTimeSelector.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/Debug/DebugDetails.tsx" "@typescript-eslint/no-unsafe-type-assertion" 8 +"../../src/pages/Debug/DynamicDebugDetailsConstantPickerPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Debug/DynamicDebugDetailsDateTimePickerPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Debug/Report/DebugReportPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Debug/ReportAction/DebugReportActionCreatePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/Debug/ReportAction/DebugReportActionCreatePage.tsx" "no-restricted-imports" 1 +"../../src/pages/Debug/ReportAction/DebugReportActionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Debug/ReportAction/DebugReportActionPreview.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/Debug/Transaction/DebugTransactionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Debug/Transaction/DebugTransactionViolations.tsx" "no-restricted-syntax" 1 +"../../src/pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage.tsx" "no-restricted-imports" 1 +"../../src/pages/Debug/TransactionViolation/DebugTransactionViolationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/Debug/TransactionViolation/DebugTransactionViolationPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/AddDomainPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/Admins/DomainAddAdminPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/Admins/DomainAddPrimaryContactPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/Admins/DomainAdminsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/BaseDomainMemberDetailsComponent.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/BaseDomainMembersPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/BaseDomainRequireTwoFactorAuthPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/BaseDomainSettingsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/BaseDomainVerifiedPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/BaseVerifyDomainPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/DomainAccessRestrictedPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/DomainAddedPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/DomainAlreadyExistsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/DomainInitialPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/DomainResetPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/DomainSamlPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/Groups/BaseDomainGroupPreferredWorkspacePage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/Groups/DomainGroupCreatePage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/Groups/DomainGroupDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/Groups/DomainGroupEditNamePage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/Groups/DomainGroupsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/Members/DomainAddMemberPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/Members/DomainMembersPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/Members/MoveUserBetweenGroupsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/domain/Members/MoveUsersBetweenGroupsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/DynamicEditReportFieldPage.tsx" "no-restricted-imports" 1 +"../../src/pages/DynamicFlagCommentPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/DynamicFlagCommentPage.tsx" "no-restricted-imports" 1 +"../../src/pages/DynamicNewReportWorkspaceSelectionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/DynamicReferralDetailsPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/DynamicReportChangeApproverPage.tsx" "no-restricted-imports" 1 +"../../src/pages/DynamicReportChangeWorkspacePage.tsx" "no-restricted-imports" 1 +"../../src/pages/DynamicReportDetailsPage/ReportDetailsContent.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 2 +"../../src/pages/DynamicReportDetailsPage/ReportDetailsMenuDebugItem.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/DynamicReportDetailsPage/ReportDetailsMenuGoToRoomItem.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/DynamicReportDetailsPage/ReportDetailsMenuGoToWorkspaceItem.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/DynamicReportDetailsPage/ReportDetailsMenuLeaveItem.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/DynamicReportDetailsPage/ReportDetailsMenuMarkAsIncompleteItem.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/DynamicReportDetailsPage/ReportDetailsMenuMembersOrInviteItem.tsx" "@typescript-eslint/no-deprecated/MenuItem" 2 +"../../src/pages/DynamicReportDetailsPage/ReportDetailsMenuPrivateNotesItem.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/DynamicReportDetailsPage/ReportDetailsMenuSettingsItem.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/DynamicReportDetailsPage/ReportDetailsMenuTrackExpenseItems.tsx" "@typescript-eslint/no-deprecated/MenuItem" 4 +"../../src/pages/DynamicReportDetailsPage/ReportDetailsTitleSection.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/DynamicReportParticipantDetailsPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/DynamicReportParticipantDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/DynamicReportParticipantRoleSelectionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/DynamicReportParticipantsInvitePage.tsx" "no-restricted-imports" 1 +"../../src/pages/DynamicReportParticipantsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/DynamicRoomInvitePage.tsx" "no-restricted-imports" 1 +"../../src/pages/DynamicRoomMemberDetailsPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/DynamicRoomMemberDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/DynamicRoomMembersPage.tsx" "no-restricted-imports" 1 +"../../src/pages/DynamicRoomMembersPage.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/DynamicWorkspaceCurrencySelectionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/EnablePayments/Pay/Activate/ActivateStep.tsx" "no-restricted-imports" 1 +"../../src/pages/EnablePayments/Pay/AdditionalDetails/AdditionalDetailsStep.tsx" "no-restricted-imports" 1 +"../../src/pages/EnablePayments/Pay/AdditionalDetails/AdditionalDetailsStep.tsx" "react-hooks/purity" 1 +"../../src/pages/EnablePayments/Pay/EnablePaymentsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/EnablePayments/Pay/Onfido/OnfidoStep.tsx" "no-restricted-imports" 1 +"../../src/pages/EnablePayments/Pay/Terms/TermsStep.tsx" "no-restricted-imports" 1 +"../../src/pages/EnablePayments/shared/IdologyQuestions.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/EnablePayments/Wallet/AddBankAccount/AddBankAccount.tsx" "no-restricted-imports" 1 +"../../src/pages/EnablePayments/Wallet/EnablePayments.tsx" "no-restricted-imports" 1 +"../../src/pages/EnablePayments/Wallet/PersonalInfo/substeps/DateOfBirthStep.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/EnablePayments/Wallet/PersonalInfo/substeps/DateOfBirthStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/EnablePayments/Wallet/utils/getSubstepValues.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/EnablePayments/Wallet/VerifyIdentity/VerifyIdentity.tsx" "no-restricted-imports" 1 +"../../src/pages/GroupChatNameEditPage.tsx" "no-restricted-imports" 1 +"../../src/pages/home/ForYouSection/ConciergePromptBox.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts" "no-restricted-imports" 1 +"../../src/pages/home/TimeSensitiveSection/items/FixCompanyCardConnection.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/home/TimeSensitiveSection/items/FixCompanyCardConnection.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/home/UpcomingTravelSection/UpcomingTravelItem.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/home/UpcomingTravelSection/useUpcomingTravelReservations.ts" "react-hooks/purity" 1 +"../../src/pages/home/YourSpendSection/CardRow.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/home/YourSpendSection/SpendSummaryRow.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/home/YourSpendSection/useYourSpendData.ts" "no-restricted-imports" 1 +"../../src/pages/inbox/cleanStaleReportActionBackToParam.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/inbox/conciergeDraftState.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/inbox/hooks/useReportWasDeleted.ts" "react-hooks/set-state-in-effect" 1 +"../../src/pages/inbox/ReactionListWrapper.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/report/actionContents/ActionContentRouter.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/inbox/report/actionContents/JoinRequestContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/report/actionContents/ReimbursementDeQueuedContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/report/comment/TextCommentFragment.tsx" "@typescript-eslint/no-deprecated/fontSize" 1 +"../../src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx" "react-hooks/refs" 1 +"../../src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1 +"../../src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx" "react-hooks/refs" 25 +"../../src/pages/inbox/report/DynamicReportDetailsExportPage.tsx" "no-restricted-imports" 1 +"../../src/pages/inbox/report/LinkPreviewer.tsx" "@typescript-eslint/no-deprecated/fontSize" 3 +"../../src/pages/inbox/report/ReactionList/HeaderReactionList.tsx" "no-restricted-syntax" 1 +"../../src/pages/inbox/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx" "react-hooks/refs" 7 +"../../src/pages/inbox/report/ReportActionCompose/SuggestionEmoji.tsx" "react-hooks/refs" 1 +"../../src/pages/inbox/report/ReportActionCompose/SuggestionMention.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/report/ReportActionCompose/SuggestionMention.tsx" "react-hooks/refs" 3 +"../../src/pages/inbox/report/ReportActionCompose/SuggestionMention.tsx" "unicorn/prefer-at" 2 +"../../src/pages/inbox/report/ReportActionCompose/useAttachmentPicker.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/report/ReportActionCompose/useComposerFocus.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/report/ReportActionCompose/useReceiptDrop.ts" "@typescript-eslint/no-unsafe-type-assertion" 5 +"../../src/pages/inbox/report/ReportActionItem.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/inbox/report/ReportActionItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/inbox/report/ReportActionItem.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "no-restricted-syntax" 1 +"../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "react-hooks/refs" 5 +"../../src/pages/inbox/report/ReportActionItemMessageEdit.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/inbox/report/ReportActionsListItemRenderer.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/inbox/report/SearchActionHeader.tsx" "@typescript-eslint/no-deprecated/fontSize" 1 +"../../src/pages/inbox/report/shouldUseEmojiPickerSelection/index.web.ts" "no-restricted-syntax" 1 +"../../src/pages/inbox/report/TripSummary.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/inbox/report/useDebouncedSaveDraft.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/report/useReportUnreadMessageScrollTracking.ts" "react-hooks/preserve-manual-memoization" 1 +"../../src/pages/inbox/report/WhisperBanner.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/report/withReportAndPrivateNotesOrNotFound.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/report/withReportAndPrivateNotesOrNotFound.tsx" "no-restricted-imports" 1 +"../../src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/report/withReportOrNotFound.tsx" "react-hooks/refs" 3 +"../../src/pages/inbox/ReportHeader.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/inbox/ReportNavigateAwayHandler.tsx" "react-hooks/exhaustive-deps" 1 +"../../src/pages/inbox/sidebar/FABPopoverContent/FABFocusableMenuItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/sidebar/FABPopoverContent/menuItems/InvoiceMenuItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/inbox/sidebar/FABPopoverContent/menuItems/NewWorkspaceMenuItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/inbox/sidebar/FABPopoverContent/menuItems/TravelMenuItem.tsx" "no-restricted-imports" 1 +"../../src/pages/inbox/sidebar/FABPopoverContent/useScanActions.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/iou/DynamicSplitBillDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/DynamicSplitExpenseCreateDateRangePage.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/DynamicSplitExpenseEditPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/iou/DynamicSplitExpenseEditPage.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/DynamicSplitExpensePage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 2 +"../../src/pages/iou/DynamicSplitExpensePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/iou/DynamicSplitExpensePage.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/DynamicSplitExpensePage.tsx" "react-hooks/set-state-in-effect" 3 +"../../src/pages/iou/HoldReasonFormView.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/MoneyRequestAmountForm.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/iou/RejectReasonFormView.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/request/DistanceRequestStartPage.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/request/IOURequestStartPage.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/request/ParticipantSearchResults.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/pages/iou/request/step/DistanceManualTabContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/iou/request/step/DynamicIOURequestEditReport.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/iou/request/step/DynamicIOURequestStepDistance.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../src/pages/iou/request/step/DynamicIOURequestStepDistance.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/iou/request/step/DynamicIOURequestStepReport.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/iou/request/step/DynamicIOURequestStepSubrate.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/iou/request/step/DynamicIOURequestStepSubrate.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/request/step/DynamicIOURequestStepSubrate.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/iou/request/step/DynamicIOURequestStepUpgrade.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/iou/request/step/DynamicIOURequestStepUpgrade.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/request/step/DynamicIOURequestStepWaypoint.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/iou/request/step/DynamicIOURequestStepWaypoint.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/request/step/IOURequestEditReportCommon.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/iou/request/step/IOURequestEditReportCommon.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/iou/request/step/IOURequestStepAmount.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/iou/request/step/IOURequestStepAmount.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/iou/request/step/IOURequestStepConfirmation/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/iou/request/step/IOURequestStepConfirmation/index.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/request/step/IOURequestStepConfirmation/index.tsx" "react-hooks/preserve-manual-memoization" 2 +"../../src/pages/iou/request/step/IOURequestStepCurrencyModal.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/request/step/IOURequestStepDistanceGPS/Waypoints/index.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 2 +"../../src/pages/iou/request/step/IOURequestStepDistanceMap.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../src/pages/iou/request/step/IOURequestStepDistanceMap.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/iou/request/step/IOURequestStepGPSTripEdit/Slider.tsx" "@typescript-eslint/no-deprecated/Gesture.Pan" 2 +"../../src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/iou/request/step/IOURequestStepScan/components/NavigationAwareCamera/WebCamera.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/iou/request/step/IOURequestStepScan/components/ScanEditReceipt.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/iou/request/step/IOURequestStepScan/DynamicReceiptView/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/iou/request/step/IOURequestStepScan/DynamicReceiptView/index.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/request/step/IOURequestStepScan/DynamicReceiptView/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/iou/request/step/IOURequestStepScan/hooks/useScanRouteParams.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/iou/request/step/IOURequestStepScan/utils/buildReceiptFiles.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/iou/request/step/IOURequestStepScan/utils/getFileSource.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/iou/request/step/StepScreenDragAndDropWrapper.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/request/step/StepScreenWrapper.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/iou/request/step/StepScreenWrapper.tsx" "no-restricted-imports" 1 +"../../src/pages/iou/request/step/withFullTransactionOrNotFound.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/iou/request/step/withWritableReportOrNotFound.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/LoadingPage.tsx" "no-restricted-imports" 1 +"../../src/pages/LogInWithShortLivedAuthTokenPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/LogOutPreviousUserPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx" "no-restricted-imports" 1 +"../../src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx" "react-hooks/set-state-in-effect" 3 +"../../src/pages/media/AttachmentModalScreen/AttachmentModalContext.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/media/AttachmentModalScreen/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 16 +"../../src/pages/media/AttachmentModalScreen/routes/hooks/useReportAttachmentModalType.ts" "react-hooks/set-state-in-effect" 1 +"../../src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 7 +"../../src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx" "react-hooks/set-state-in-effect" 2 +"../../src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx" "no-restricted-imports" 1 +"../../src/pages/MissingPersonalDetails/MissingPersonalDetailsValidateCodePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/MissingPersonalDetails/subPages/Address.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/pages/MissingPersonalDetails/subPages/Address.tsx" "@typescript-eslint/no-unsafe-type-assertion" 5 +"../../src/pages/MissingPersonalDetails/subPages/Address.tsx" "react-hooks/refs" 4 +"../../src/pages/MultifactorAuthentication/AuthorizeTransactionPage/AuthorizeCardTransactionPreview.tsx" "@typescript-eslint/no-deprecated/fontSize" 2 +"../../src/pages/MultifactorAuthentication/AuthorizeTransactionPage/index.tsx" "no-restricted-imports" 1 +"../../src/pages/MultifactorAuthentication/PromptPage.tsx" "no-restricted-imports" 1 +"../../src/pages/MultifactorAuthentication/RevokePage.tsx" "no-restricted-imports" 1 +"../../src/pages/MultifactorAuthentication/ValidateCodePage.tsx" "no-restricted-imports" 1 +"../../src/pages/NewChatConfirmPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/NewChatConfirmPage.tsx" "no-restricted-imports" 1 +"../../src/pages/NewChatSelectorPage.tsx" "no-restricted-imports" 1 +"../../src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/OnboardingPersonalTrackGoal/BaseOnboardingPersonalTrackGoal.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/OnboardingPrivateDomain/BaseOnboardingPrivateDomain.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/PrivateNotes/PrivateNotesEditPage.tsx" "no-restricted-imports" 1 +"../../src/pages/PrivateNotes/PrivateNotesListPage.tsx" "no-restricted-imports" 1 +"../../src/pages/ProfilePage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/ProfilePage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 5 +"../../src/pages/ProfilePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ProfilePage.tsx" "no-restricted-imports" 1 +"../../src/pages/ReimbursementAccount/AddressFormFields.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx" "no-restricted-imports" 1 +"../../src/pages/ReimbursementAccount/EnterSignerInfo/subSteps/Address.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ReimbursementAccount/NonUSD/BankInfo/BankInfo.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/AccountHolderDetails.tsx" "@typescript-eslint/no-unsafe-type-assertion" 7 +"../../src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/BankAccountDetails.tsx" "@typescript-eslint/no-unsafe-type-assertion" 7 +"../../src/pages/ReimbursementAccount/NonUSD/BankInfo/subSteps/Confirmation.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Nationality.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/Confirmation.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/ContactInformation.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/IncorporationLocation.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/ReimbursementAccount/NonUSD/BusinessInfo/subSteps/Website.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ReimbursementAccount/NonUSD/Finish/index.tsx" "no-restricted-imports" 1 +"../../src/pages/ReimbursementAccount/NonUSD/SignerInfo/HangTight.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/ReimbursementAccount/NonUSD/SignerInfo/subSteps/Address.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ReimbursementAccount/NonUSD/utils/getBankInfoStepValues.ts" "@typescript-eslint/no-unsafe-type-assertion" 11 +"../../src/pages/ReimbursementAccount/NonUSD/utils/getInitialSubStepForBankInfoStep.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ReimbursementAccount/NonUSD/utils/getInitialSubStepForBusinessInfoStep.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/ReimbursementAccount/NonUSD/utils/getInputKeysForBankInfoStep.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ReimbursementAccount/NonUSD/utils/getValuesForBeneficialOwner.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ReimbursementAccount/NonUSD/utils/getValuesForSignerInfo.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx" "no-restricted-imports" 1 +"../../src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx" "react-hooks/refs" 3 +"../../src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx" "react-hooks/set-state-in-effect" 4 +"../../src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/BeneficialOwnersStep.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/ReimbursementAccount/USD/BeneficialOwnerInfo/subSteps/BeneficialOwnerDetailsFormSubSteps/LegalNameUBO.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/ConfirmationBusiness.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IncorporationDateBusiness.tsx" "react-hooks/purity" 1 +"../../src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/IndustryCode/IndustryCodeSelector.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/TypeBusiness/BusinessTypePicker/BusinessTypeSelectorModal.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/TypeBusiness/BusinessTypePicker/BusinessTypeSelectorModal.tsx" "no-restricted-imports" 1 +"../../src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/TypeBusiness/BusinessTypePicker/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ReimbursementAccount/USD/BusinessInfo/subSteps/WebsiteBusiness.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ReimbursementAccount/USD/ConnectBankAccount/components/BankAccountValidationForm.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/ReimbursementAccount/USD/ConnectBankAccount/components/BankAccountValidationForm.tsx" "unicorn/prefer-string-replace-all" 1 +"../../src/pages/ReimbursementAccount/USD/ConnectBankAccount/components/Enable2FACard.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/ReimbursementAccount/USD/ConnectBankAccount/ConnectBankAccount.tsx" "no-restricted-imports" 1 +"../../src/pages/ReimbursementAccount/utils/getSubStepValues.ts" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx" "no-restricted-imports" 1 +"../../src/pages/RejectExpenseReportPage.tsx" "no-restricted-imports" 1 +"../../src/pages/ReportDescriptionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/ReportDescriptionPage.tsx" "no-restricted-syntax" 1 +"../../src/pages/RestrictedAction/Workspace/WorkspaceAdminRestrictedAction.tsx" "no-restricted-imports" 1 +"../../src/pages/RestrictedAction/Workspace/WorkspaceOwnerRestrictedAction/index.native.tsx" "no-restricted-imports" 1 +"../../src/pages/RestrictedAction/Workspace/WorkspaceOwnerRestrictedAction/index.tsx" "no-restricted-imports" 1 +"../../src/pages/RestrictedAction/Workspace/WorkspaceUserRestrictedAction.tsx" "no-restricted-imports" 1 +"../../src/pages/RoomDescriptionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/ScheduleCall/ScheduleCallConfirmationPage.tsx" "no-restricted-imports" 1 +"../../src/pages/ScheduleCall/ScheduleCallPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/ScheduleCall/ScheduleCallPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SavedSearchRenamePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/Search/SavedSearchRenamePage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchAdvancedFiltersContentPage/SearchAdvancedFiltersContentBase.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchAdvancedFiltersPage/SearchAdvancedFiltersBase.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchChangeApproverPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/Search/SearchChangeApproverPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchEditMultiple/SearchEditMultipleAmountPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchEditMultiple/SearchEditMultipleAttendeesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchEditMultiple/SearchEditMultipleBooleanPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchEditMultiple/SearchEditMultipleCategoryPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchEditMultiple/SearchEditMultipleDatePage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchEditMultiple/SearchEditMultipleDescriptionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchEditMultiple/SearchEditMultipleMerchantPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchEditMultiple/SearchEditMultiplePage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchEditMultiple/SearchEditMultipleTagPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/Search/SearchEditMultiple/SearchEditMultipleTagPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchEditMultiple/SearchEditMultipleTaxPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchEditMultiple/SearchEditMultipleUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/Search/SearchMoneyRequestReportPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/Search/SearchPage.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/Search/SearchPageNarrow/index.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchSavePage.tsx" "no-restricted-imports" 1 +"../../src/pages/Search/SearchTransactionsChangeReport.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/SetDefaultWorkspacePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/AboutPage/AboutPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/AboutPage/AboutPage.tsx" "react-hooks/preserve-manual-memoization" 9 +"../../src/pages/settings/Agents/AgentsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Agents/DynamicAddAgentPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Agents/DynamicEditAgentPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Agents/DynamicNewAgentPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Agents/Fields/DynamicEditAgentAvatarPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Agents/Fields/DynamicEditNamePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Agents/Fields/DynamicEditPromptPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/AppDownloadLinks.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Copilot/CopilotPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/settings/Copilot/CopilotPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/DynamicKeyboardShortcutsPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/settings/DynamicKeyboardShortcutsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/ExitSurvey/DynamicExitSurveyConfirmPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/ExitSurvey/DynamicExitSurveyConfirmPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/ExitSurvey/DynamicExitSurveyReasonPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/HelpPage/HelpPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Preferences/LanguagePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Preferences/PaymentCurrencyPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Preferences/PreferencesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Preferences/PriorityModePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Preferences/ThemePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/AgentAIPromptSection.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Profile/Avatar/UserProfileAvatar.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/Contacts/DynamicContactMethodDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/Contacts/DynamicContactMethodDetailsPage.tsx" "react-hooks/refs" 3 +"../../src/pages/settings/Profile/Contacts/DynamicContactMethodDetailsPage.tsx" "react-hooks/set-state-in-effect" 3 +"../../src/pages/settings/Profile/Contacts/DynamicContactMethodsPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/settings/Profile/Contacts/DynamicContactMethodsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/Contacts/DynamicNewContactMethodPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/Contacts/DynamicNewContactMethodPage.tsx" "react-hooks/preserve-manual-memoization" 1 +"../../src/pages/settings/Profile/CustomStatus/SetDatePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/CustomStatus/SetDatePage.tsx" "react-hooks/purity" 1 +"../../src/pages/settings/Profile/CustomStatus/SetTimePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 2 +"../../src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/settings/Profile/CustomStatus/StatusPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/settings/Profile/CustomStatus/StatusPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/CustomStatus/StatusPage.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/settings/Profile/CustomStatus/VacationDelegateMissingWorkspacesPage/index.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/DisplayNamePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/PersonalDetails/BaseLegalNamePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/PersonalDetails/DynamicCountrySelectionPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Profile/PersonalDetails/DynamicCountrySelectionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/PersonalDetails/DynamicStateSelectionPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/settings/Profile/PersonalDetails/DynamicStateSelectionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/PersonalDetails/PersonalAddressPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsConfirmValidateCodePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 8 +"../../src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/PersonalDetails/PrivatePersonalDetailsPage.tsx" "react-hooks/purity" 2 +"../../src/pages/settings/Profile/ProfilePage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/settings/Profile/ProfilePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Profile/ProfilePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/PronounsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/PronounsPage.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/settings/Profile/TimezoneInitialPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Profile/TimezoneInitialPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Profile/TimezoneSelectPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Profile/TimezoneSelectPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Report/DynamicNotificationPreferencePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Report/DynamicReportSettingsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Report/DynamicVisibilityPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Report/DynamicWriteCapabilityPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Report/RoomNamePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Rules/ExpenseRulesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Rules/RulePageBase.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Security/AddDelegate/ConfirmDelegatePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Security/AddDelegate/ConfirmDelegateValidateCodePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Security/AddDelegate/SelectDelegateRolePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateRolePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateValidateCodePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Security/CloseAccount/CloseAccountPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Security/DeviceManagementPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Security/LockAccount/FailedToLockAccountPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Security/LockAccount/UnlockAccountPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Security/MergeAccounts/AccountDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Security/MergeAccounts/AccountValidatePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Security/MergeAccounts/MergeResultPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Security/SecuritySettingsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Security/TwoFactorAuth/EnabledPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/settings/Security/TwoFactorAuth/EnabledPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthWrapper.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/SettingsMenuItem.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/settings/Subscription/CancelSubscriptionPage/index.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Subscription/CardSection/BillingBanner/TrialEndedBillingBanner.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/settings/Subscription/DisableAutoRenewSurveyPage/index.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Subscription/ExpensifyCodePage/index.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Subscription/PaymentCard/ChangeBillingCurrency/index.tsx" "no-restricted-syntax" 1 +"../../src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Subscription/PaymentCard/DynamicPaymentCardCurrencySelectorPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Subscription/PaymentCard/index.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/settings/Subscription/PaymentCard/index.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Subscription/SubscriptionPlan/ComparePlansModal.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Subscription/SubscriptionPlan/ComparePlansModal.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/settings/Subscription/SubscriptionPlan/DynamicSubscriptionPlanDowngradeBlockedPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCardActionButton.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/settings/Subscription/SubscriptionSettings/index.native.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Subscription/SubscriptionSettings/index.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Subscription/SubscriptionSettingsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Subscription/SubscriptionSize/index.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Troubleshoot/DynamicBetaOverridesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Troubleshoot/ServerSelector.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Troubleshoot/TroubleshootPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Troubleshoot/TroubleshootPage.tsx" "react-hooks/purity" 1 +"../../src/pages/settings/useInitialSettingsPageMenuData.ts" "react-hooks/purity" 1 +"../../src/pages/settings/VerifyAccountPageBase.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/AddCardToDigitalWalletPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/AddDebitCardPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/BankAccountPurposePage/index.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/BankAccountPurposePage/substeps/BankAccountPurpose.tsx" "@typescript-eslint/no-deprecated/MenuItem" 2 +"../../src/pages/settings/Wallet/BankAccountPurposePage/substeps/CountrySelection.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Wallet/CardAddedToWalletPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/ChooseTransferAccountPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Wallet/ChooseTransferAccountPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/CountrySelectionList.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Wallet/EnableGlobalReimbursements/EnableGlobalReimbursementsAgreementsPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/settings/Wallet/EnableGlobalReimbursements/EnableGlobalReimbursementsBusinessPage/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Wallet/ExpensifyCardPage/ChangePINPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/ExpensifyCardPage/index.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 2 +"../../src/pages/settings/Wallet/ExpensifyCardPage/index.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/ImportTransactionsCardNamePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/ImportTransactionsCurrencyPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/ImportTransactionsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/InternationalDepositAccount/InternationalDepositAccountContent.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/InternationalDepositAccount/subPages/AccountFlowEntryPoint.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/InternationalDepositAccount/subPages/Confirmation.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/settings/Wallet/InternationalDepositAccount/utils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/settings/Wallet/InternationalDepositAccount/utils.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/settings/Wallet/PaymentMethodList.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/settings/Wallet/PaymentMethodList.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../src/pages/settings/Wallet/PaymentMethodList.tsx" "@typescript-eslint/no-unsafe-type-assertion" 7 +"../../src/pages/settings/Wallet/PaymentMethodListItem.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/settings/Wallet/PersonalCardDetailsHeaderMenu.tsx" "@typescript-eslint/no-deprecated/MenuItem" 2 +"../../src/pages/settings/Wallet/PersonalCardDetailsPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/settings/Wallet/PersonalCardDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/PersonalCardEditNamePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/PersonalCardEditTransactionStartDatePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/PersonalCards/FixPersonalCardConnectionPage/index.native.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/PersonalCards/FixPersonalCardConnectionPage/index.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/PersonalCards/FixPersonalCardConnectionPage/useFixPersonalCardConnection.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/pages/settings/Wallet/PersonalCards/FixPersonalCardConnectionPage/useFixPersonalCardConnection.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/settings/Wallet/PersonalCards/steps/BankConnection/index.native.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/PersonalCards/steps/BankConnection/index.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/PersonalCards/steps/PlaidConnectionStep.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/PersonalCards/steps/SelectBankStep.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/PersonalCards/steps/SelectCountryStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Wallet/PersonalCards/steps/SelectCountryStep.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/PersonalCards/steps/SelectCountryStep.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/settings/Wallet/PersonalCards/steps/SuccessStep.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/PersonalCards/upgrade/PersonalCardUpgradePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/ReportCardLostConfirmValidateCodePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/ReportCardLostPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/ReportVirtualCardFraudConfirmationPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/ShareBankAccount/ShareBankAccount.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/ShareBankAccount/ShareBankAccount.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/settings/Wallet/TransactionsImportedPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/TransferBalancePage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 2 +"../../src/pages/settings/Wallet/TransferBalancePage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/TravelCVVPage/TravelCVVPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/settings/Wallet/UpdatePersonalBankAccountPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Wallet/WalletPage/CardDetails.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 4 +"../../src/pages/settings/Wallet/WalletPage/index.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/settings/Wallet/WalletPage/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/settings/Wallet/WalletPage/index.tsx" "no-restricted-imports" 2 +"../../src/pages/settings/Wallet/WalletPage/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/Share/ShareDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Share/ShareRootPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/Share/ShareRootPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Share/ShareTab.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/Share/SubmitDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/ShareCodePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/ShareCodePage.tsx" "no-restricted-imports" 1 +"../../src/pages/signin/SAMLSignInPage/index.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/signin/SAMLSignInPage/index.native.tsx" "no-restricted-imports" 1 +"../../src/pages/signin/SignInModal.tsx" "no-restricted-imports" 1 +"../../src/pages/signin/SignInPage.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/signin/SignInPageLayout/BackgroundImage/index.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/signin/SignInPageLayout/FooterRow/index.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/signin/SignInPageLayout/SignInHeroCopy.tsx" "rulesdir/no-raw-typography" 2 +"../../src/pages/signin/SignInPageLayout/SignInPageContent.tsx" "rulesdir/no-raw-typography" 2 +"../../src/pages/signin/SMSDeliveryFailurePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx" "react-hooks/set-state-in-effect" 4 +"../../src/pages/tasks/DynamicNewTaskDescriptionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/tasks/DynamicNewTaskDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/tasks/DynamicNewTaskPage.tsx" "no-restricted-imports" 1 +"../../src/pages/tasks/DynamicNewTaskTitlePage.tsx" "no-restricted-imports" 1 +"../../src/pages/tasks/DynamicTaskAssigneeSelectorModal.tsx" "no-restricted-imports" 1 +"../../src/pages/tasks/DynamicTaskShareDestinationSelectorModal.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/tasks/DynamicTaskShareDestinationSelectorModal.tsx" "no-restricted-imports" 1 +"../../src/pages/tasks/TaskDescriptionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/tasks/TaskTitlePage.tsx" "no-restricted-imports" 1 +"../../src/pages/TeachersUnite/ImTeacherUpdateEmailPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TeachersUnite/IntroSchoolPrincipalPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TeachersUnite/KnowATeacherPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TeachersUnite/SaveTheWorldPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/TeachersUnite/SaveTheWorldPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/TransactionDuplicate/DynamicConfirmationPage.tsx" "@typescript-eslint/no-deprecated/fontSize" 1 +"../../src/pages/TransactionDuplicate/DynamicConfirmationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/TransactionDuplicate/DynamicConfirmationPage.tsx" "no-restricted-imports" 2 +"../../src/pages/TransactionDuplicate/DynamicConfirmationPage.tsx" "react-hooks/refs" 7 +"../../src/pages/TransactionDuplicate/DynamicReviewBillablePage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionDuplicate/DynamicReviewCategoryPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionDuplicate/DynamicReviewDescriptionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionDuplicate/DynamicReviewMerchantPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionDuplicate/DynamicReviewPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionDuplicate/DynamicReviewReimbursablePage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionDuplicate/DynamicReviewTagPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionDuplicate/DynamicReviewTaxCodePage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionDuplicate/ReviewFields.tsx" "@typescript-eslint/no-deprecated/fontSize" 1 +"../../src/pages/TransactionMerge/DynamicConfirmationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/TransactionMerge/DynamicConfirmationPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionMerge/DynamicDetailsReviewPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/TransactionMerge/DynamicDetailsReviewPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/TransactionMerge/DynamicDetailsReviewPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionMerge/DynamicMergeTransactionsListPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionMerge/DynamicReceiptReviewPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionMerge/MergeTransactionItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/TransactionMerge/MergeTransactionsListContent.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/TransactionMerge/MergeTransactionsListContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/Travel/CarTripDetails.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 2 +"../../src/pages/Travel/DynamicDomainPermissionInfoPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Travel/DynamicPublicDomainErrorPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Travel/DynamicTravelUpgrade.tsx" "no-restricted-imports" 2 +"../../src/pages/Travel/DynamicTripDetailsPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 2 +"../../src/pages/Travel/DynamicTripSummaryPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Travel/DynamicWorkspaceConfirmationForTravelPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/Travel/EnableTravel/EnableTravelContent.tsx" "no-restricted-imports" 1 +"../../src/pages/Travel/HotelTripDetails.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 3 +"../../src/pages/Travel/MyTripsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/Travel/TravelDotLinkWebview.tsx" "import/no-named-as-default" 1 +"../../src/pages/Travel/TravelDotLinkWebview.tsx" "no-restricted-imports" 1 +"../../src/pages/TripChatNameEditPage.tsx" "no-restricted-imports" 1 +"../../src/pages/UnreportedExpenseListItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/ValidateLoginPage/index.web.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/wallet/WalletStatementPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/wallet/WalletStatementPage.tsx" "react-hooks/purity" 2 +"../../src/pages/workspace/accounting/AccountingContext/default.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/AccountingSetupWebViewPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/businessCentral/BusinessCentralPrerequisitesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/campfire/advanced/CampfireAdvancedPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 5 +"../../src/pages/workspace/accounting/campfire/CampfireExistingConnectionsPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/accounting/campfire/CampfireExistingConnectionsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/campfire/export/CampfireExportPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 6 +"../../src/pages/workspace/accounting/campfire/export/CampfireExportPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/certinia/advanced/CertiniaAdvancedPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/accounting/certinia/CertiniaExistingConnectionsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/certinia/export/CertiniaExportPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/accounting/certinia/import/CertiniaDimensionMappingPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/ClaimOfferPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/ClaimOfferPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/dualentry/DualEntryExistingConnectionsPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/accounting/dualentry/DualEntryExistingConnectionsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/dualentry/export/DualEntryExportPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/intacct/advanced/DynamicSageIntacctAccountingMethodPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/intacct/DynamicSageIntacctPrerequisitesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/intacct/EnterSageIntacctCredentialsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/intacct/ExistingConnectionsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/intacct/export/DynamicSageIntacctNonReimbursableExpensesDestinationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesDestinationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/intacct/import/SageIntacctAddUserDimensionPage.tsx" "no-restricted-syntax" 1 +"../../src/pages/workspace/accounting/intacct/import/SageIntacctAddUserDimensionPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/intacct/import/SageIntacctMappingsTypePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/workspace/accounting/netsuite/advanced/DynamicNetSuiteAccountingMethodPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/advanced/DynamicNetSuiteAutoSyncPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx" "react/jsx-key" 1 +"../../src/pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/advanced/NetSuiteExpenseReportApprovalLevelSelectPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/advanced/NetSuiteJournalEntryApprovalLevelSelectPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/advanced/NetSuiteVendorBillApprovalLevelSelectPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteDateSelectPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteExportExpensesDestinationSelectPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/export/NetSuiteTravelBillingJournalPostingPreferenceSelectPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomersOrProjectSelectPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomersOrProjectsPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomersOrProjectsPage.tsx" "no-restricted-syntax" 2 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomersOrProjectsPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldEdit.tsx" "@typescript-eslint/no-unsafe-type-assertion" 12 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListSelectorPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomSegmentContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/subPages/ChooseSegmentTypeStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/subPages/ConfirmCustomSegmentList.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/subPages/CustomListMappingStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/subPages/CustomSegmentMappingStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldView.tsx" "@typescript-eslint/no-unsafe-type-assertion" 7 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportMappingPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/accounting/netsuite/import/NetSuiteImportPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/subPages/NetSuiteTokenSetupContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/accounting/PolicyAccountingPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/accounting/PolicyAccountingPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/accounting/PolicyAccountingPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/qbd/advanced/QuickbooksDesktopAccountingMethodPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/qbd/export/DynamicQuickbooksDesktopCompanyCardExpenseAccountSelectCardPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/accounting/qbd/export/DynamicQuickbooksDesktopCompanyCardExpenseAccountSelectCardPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/qbd/export/DynamicQuickbooksDesktopOutOfPocketExpenseEntitySelectPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopChartOfAccountsPage.tsx" "no-restricted-syntax" 2 +"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopChartOfAccountsPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx" "no-restricted-syntax" 3 +"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx" "no-restricted-syntax" 3 +"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx" "no-restricted-syntax" 1 +"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopImportPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopItemsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopItemsPage.tsx" "no-restricted-syntax" 3 +"../../src/pages/workspace/accounting/qbd/import/QuickbooksDesktopItemsPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/qbd/QuickbooksDesktopExistingConnectionsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupFlowSyncPage.tsx" "rulesdir/no-default-id-values" 2 +"../../src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/qbd/QuickBooksDesktopSetupPage.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/workspace/accounting/qbd/RequireQuickBooksDesktopPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/qbo/advanced/DynamicQuickbooksOnlineAccountingMethodPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/qbo/advanced/DynamicQuickbooksOnlineAutoSyncPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/qbo/export/DynamicQuickbooksCompanyCardExpenseAccountPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/accounting/qbo/export/DynamicQuickbooksCompanyCardExpenseAccountSelectCardPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/qbo/export/DynamicQuickbooksOutOfPocketExpenseEntitySelectPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/qbo/import/QuickbooksChartOfAccountsPage.tsx" "no-restricted-syntax" 2 +"../../src/pages/workspace/accounting/qbo/import/QuickbooksChartOfAccountsPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/qbo/import/QuickbooksClassesPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/qbo/import/QuickbooksCustomersDisplayedAsPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/accounting/qbo/import/QuickbooksCustomersPage.tsx" "no-restricted-syntax" 2 +"../../src/pages/workspace/accounting/qbo/import/QuickbooksCustomersPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx" "no-restricted-syntax" 2 +"../../src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/qbo/import/QuickbooksLocationsDisplayedAsPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/accounting/qbo/import/QuickbooksTaxesPage.tsx" "no-restricted-syntax" 2 +"../../src/pages/workspace/accounting/qbo/import/QuickbooksTaxesPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/rillet/export/RilletExportPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/rillet/RilletExistingConnectionsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/utils.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/accounting/utils.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/xero/advanced/DynamicXeroAccountingMethodPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/xero/advanced/DynamicXeroAutoSyncPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/xero/export/DynamicXeroExportConfigurationPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/accounting/xero/export/DynamicXeroPurchaseBillDateSelectPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/xero/export/DynamicXeroPurchaseBillStatusSelectorPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/accounting/xero/import/XeroChartOfAccountsPage.tsx" "@typescript-eslint/no-deprecated/fontSize" 1 +"../../src/pages/workspace/accounting/xero/import/XeroChartOfAccountsPage.tsx" "no-restricted-syntax" 2 +"../../src/pages/workspace/accounting/xero/import/XeroChartOfAccountsPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx" "no-restricted-syntax" 3 +"../../src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/xero/XeroImportPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/xero/XeroImportPage.tsx" "no-restricted-syntax" 1 +"../../src/pages/workspace/accounting/xero/XeroImportPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/xero/XeroMapTrackingCategoryConfigurationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/accounting/xero/XeroTaxesConfigurationPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/accounting/xero/XeroTaxesConfigurationPage.tsx" "no-restricted-syntax" 4 +"../../src/pages/workspace/accounting/xero/XeroTaxesConfigurationPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/categories/CategoryGLCodePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/CategoryPayrollCodePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/CategorySettingsPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/categories/CategorySettingsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/DynamicCategoryApproverPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/DynamicCategoryDefaultTaxRatePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/DynamicCategoryDescriptionHintPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/DynamicCategoryFlagAmountsOverPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/categories/DynamicCategoryFlagAmountsOverPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/DynamicCategoryRequiredFieldsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/DynamicCategoryRequireItemizedReceiptsOverPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/categories/DynamicCategoryRequireItemizedReceiptsOverPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/DynamicCategoryRequireReceiptsOverPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/categories/DynamicCategoryRequireReceiptsOverPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/DynamicCreateCategoryPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/DynamicDefaultCategorySelectorPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/DynamicEditCategoryPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/DynamicExpenseLimitTypeSelectorPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/ImportedCategoriesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/WorkspaceCategoriesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/categories/WorkspaceCategoriesPage.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/workspace/companyCards/addNew/AmexCustomFeed.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/addNew/CardInstructionsStep.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/addNew/CardNameStep.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/addNew/CardTypeStep.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/addNew/CompanyCardLayoutNamePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/addNew/CompanyCardsImportedPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/addNew/DetailsStep.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/addNew/ImportFromFileStep.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/addNew/SelectBankStep.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/addNew/SelectCountryStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/companyCards/addNew/SelectCountryStep.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/addNew/SelectFeedType.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/assignCard/CardNameStep.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx" "react-hooks/purity" 1 +"../../src/pages/workspace/companyCards/BankConnection/index.native.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/BankConnection/index.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/BankConnection/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/workspace/companyCards/DynamicWorkspaceCompanyCardDetailsPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/companyCards/DynamicWorkspaceCompanyCardDetailsPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/companyCards/DynamicWorkspaceCompanyCardDetailsPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/companyCards/DynamicWorkspaceCompanyCardDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/WorkspaceCompanyCardAddWorkEmailPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/companyCards/WorkspaceCompanyCardAddWorkEmailPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/WorkspaceCompanyCardEditCardNamePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/WorkspaceCompanyCardPageEmptyState.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/companyCards/WorkspaceCompanyCardsErrorConfirmation.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsFeedNamePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/WorkspaceCompanyCardStatementCloseDateSelectionList/index.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/companyCards/WorkspaceCompanyCardStatementCloseDateSelectionList/index.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/companyCards/WorkspaceVerifyWorkAccountPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/companyCards/WorkspaceVerifyWorkAccountPage.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/connections/ConnectionCard.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/connections/ConnectionsAccountingPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/connections/ConnectionsMergeProviderCard.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/connections/ConnectionsMergeProviderCard.tsx" "@typescript-eslint/no-deprecated/RowMenuItem" 1 +"../../src/pages/workspace/connections/ConnectionsReceiptPartnersPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/copyPolicySettings/CopyPolicySettingsConfirmPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectWorkspacesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/copyPolicySettings/CopyPolicySettingsUpgradePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/distanceRates/PolicyCommuterExclusionsPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/distanceRates/PolicyCommuterExclusionsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/distanceRates/PolicyDistanceRateDateEditPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/distanceRates/PolicyDistanceRateNameEditPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 3 +"../../src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/distanceRates/PolicyDistanceRateTaxRateEditPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/distanceRates/PolicyDistanceRateTaxReclaimableEditPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/distanceRates/PolicyDistanceRateUnitPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/downgrade/DynamicPayAndDowngradePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/downgrade/DynamicWorkspaceDowngradePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/workspace/DynamicWorkspaceConfirmationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/DynamicWorkspaceInvitePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/workspace/DynamicWorkspacePlanTypeSelectionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/expensifyCard/DynamicExpensifyCardLimitPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/expensifyCard/DynamicExpensifyCardLimitTypePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/expensifyCard/DynamicExpensifyCardLimitTypePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/expensifyCard/DynamicExpensifyCardLimitTypePage.tsx" "react-hooks/purity" 2 +"../../src/pages/workspace/expensifyCard/DynamicExpensifyCardNamePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/expensifyCard/DynamicWorkspaceExpensifyCardDetailsPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/expensifyCard/DynamicWorkspaceExpensifyCardDetailsPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/expensifyCard/DynamicWorkspaceExpensifyCardDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/expensifyCard/DynamicWorkspaceSettlementAccountPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/workspace/expensifyCard/DynamicWorkspaceSettlementAccountPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/expensifyCard/issueNew/CardTypeStep.tsx" "@typescript-eslint/no-deprecated/MenuItem" 2 +"../../src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx" "react-hooks/purity" 2 +"../../src/pages/workspace/expensifyCard/issueNew/spendRules/SpendRuleSelectionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/expensifyCard/WorkspaceCardSettingsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/expensifyCard/WorkspaceCardsListLabel.tsx" "react-hooks/purity" 1 +"../../src/pages/workspace/expensifyCard/WorkspaceExpensifyCardAddWorkEmailPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/expensifyCard/WorkspaceExpensifyCardFeedSelectorPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/fields/CreateFieldsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/fields/FieldsAddListValuePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/fields/FieldsEditValuePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/fields/FieldsInitialListValuePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/fields/FieldsInitialValuePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/fields/FieldsListValuesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/fields/FieldsSettingsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/fields/FieldsTypeSelectorPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/fields/FieldsValueSettingsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/fields/WorkspaceFieldsSection.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/hr/HRApprovalModePageBase.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/hr/merge/MergeHRGroupsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/hr/utils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/invoices/WorkspaceInvoiceBalanceSection.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/invoices/WorkspaceInvoicingDetailsName.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/invoices/WorkspaceInvoicingDetailsSection.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 2 +"../../src/pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/mcp/WorkspaceMCPPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/mcp/WorkspaceMCPPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/members/DynamicWorkspaceOwnerChangeErrorPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/members/DynamicWorkspaceOwnerChangeErrorPage.tsx" "react-hooks/refs" 2 +"../../src/pages/workspace/members/DynamicWorkspaceOwnerChangeSuccessPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/members/DynamicWorkspaceOwnerChangeSuccessPage.tsx" "react-hooks/refs" 2 +"../../src/pages/workspace/members/DynamicWorkspaceOwnerChangeWrapperPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/members/ImportedMembersConfirmationPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/members/ImportedMembersConfirmationPage.tsx" "react-hooks/refs" 7 +"../../src/pages/workspace/members/ImportedMembersPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/members/ImportMembersPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/members/ImportMembersPage.tsx" "no-restricted-syntax" 1 +"../../src/pages/workspace/members/ImportMembersPage.tsx" "rulesdir/no-default-id-values" 1 +"../../src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/workspace/members/WorkspaceMemberCustomFieldPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 2 +"../../src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/members/WorkspaceOwnerChangeCheck.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/workspace/members/WorkspaceOwnerPaymentCardForm.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/members/WorkspaceOwnerPaymentCardForm.tsx" "react-hooks/set-state-in-effect" 2 +"../../src/pages/workspace/merge/MergeConnectionsPageBase.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/merge/MergeProviderCard.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/merge/MergeProviderCard.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/merge/MergeProviderCard.tsx" "@typescript-eslint/no-deprecated/RowMenuItem" 1 +"../../src/pages/workspace/perDiem/EditPerDiemAmountPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/perDiem/EditPerDiemCurrencyPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/perDiem/EditPerDiemDestinationPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/perDiem/EditPerDiemSubratePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/perDiem/ImportedPerDiemPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/perDiem/WorkspacePerDiemDetailsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/receiptPartners/ChangeReceiptBillingAccountPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/receiptPartners/DynamicEditInviteReceiptPartnerPolicyPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/receiptPartners/DynamicEditInviteReceiptPartnerPolicyPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/receiptPartners/DynamicEditInviteReceiptPartnerPolicyPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/receiptPartners/DynamicInviteReceiptPartnerPolicyPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/receiptPartners/WorkspaceReceiptPartnersPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/recruiting/approver/MergeATSApprovalModePage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/recruiting/merge/filters/MergeATSFiltersPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/reports/InitialListValueSelector/index.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/reports/ReportsDefaultTitle.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/reports/TypeSelector/index.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/reports/TypeSelector/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/reports/WorkspaceReportsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/rooms/WorkspaceRoomsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/rules/AgentRules/AddAgentRulePage.tsx" "react/no-unstable-nested-components" 1 +"../../src/pages/workspace/rules/ExpenseReportRulesSection.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 3 +"../../src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleAmountPageBase.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRulePageBase.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/rules/IndividualExpenseRulesSectionRevamp.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/rules/MerchantRules/ExpenseDefaultTypePage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/rules/MerchantRules/MerchantRulePageBase.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/rules/MerchantTypeRules/MerchantTypeRulePageBase.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/rules/RulesNewPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/rules/SpendRules/SpendRulePageBase.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/tags/DynamicEditTagPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/tags/DynamicTagApproverPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/tags/DynamicTagGLCodePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/tags/DynamicTagSettingsPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/tags/DynamicTagSettingsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/tags/DynamicWorkspaceEditTagsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/tags/DynamicWorkspaceViewTagsPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/tags/DynamicWorkspaceViewTagsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/tags/ImportedMultiLevelTagsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/tags/ImportedTagsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/tags/ImportMultiLevelTagsSettingsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/tags/ImportTagsOptionsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/tags/WorkspaceCreateTagPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/tags/WorkspaceTagsPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/workspace/tags/WorkspaceTagsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/tags/WorkspaceTagsPage.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/workspace/taxes/NamePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/taxes/ValuePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/taxes/WorkspaceCreateTaxValuePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/taxes/WorkspaceTaxCodePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/taxes/WorkspaceTaxesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/taxes/WorkspaceTaxesPage.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/timeTracking/WorkspaceTimeTrackingDefaultRatePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/travel/PolicyTravelPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/travel/WorkspaceTravelBillingExportPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/travel/WorkspaceTravelBillingFeedSelectorPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/travel/WorkspaceTravelBillingMonthlyLimitPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/travel/WorkspaceTravelBillingSection.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../src/pages/workspace/travel/WorkspaceTravelBillingSettlementAccountPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/travel/WorkspaceTravelBillingSettlementAccountPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/travel/WorkspaceTravelBillingSettlementFrequencyPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/upgrade/GenericFeaturesView.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/upgrade/UpgradeIntro.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx" "no-restricted-imports" 2 +"../../src/pages/workspace/vendors/WorkspaceVendorsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/withPolicy.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/withPolicy.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/workspace/withPolicyAndFullscreenLoading.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/withPolicyConnections.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 2 +"../../src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 2 +"../../src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/workflows/WorkspaceWorkflowsCurrencyConversionFeesPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/WorkspaceConfirmationOwnerSelectorPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/WorkspaceConfirmationSuccessPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/WorkspaceInitialPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/WorkspaceMembersPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/WorkspaceNamePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/WorkspaceNewRoomPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/pages/workspace/WorkspaceNewRoomPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/WorkspaceNewRoomPage.tsx" "react-hooks/set-state-in-effect" 3 +"../../src/pages/workspace/WorkspaceOverviewClientIDPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/WorkspaceOverviewCurrencyPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/WorkspaceOverviewDescriptionPage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/WorkspaceOverviewPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 3 +"../../src/pages/workspace/WorkspaceOverviewPage.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/pages/workspace/WorkspaceOverviewPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/pages/workspace/WorkspaceOverviewSharePage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/workspace/WorkspaceOverviewSharePage.tsx" "no-restricted-imports" 1 +"../../src/pages/workspace/WorkspacePageWithSections.tsx" "no-restricted-imports" 2 +"../../src/pages/workspace/WorkspacePageWithSections.tsx" "react-hooks/preserve-manual-memoization" 2 +"../../src/polyfills/requestIdleCallback.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/ROUTES.ts" "@typescript-eslint/no-deprecated/getUrlWithBackToParam" 53 +"../../src/ROUTES.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../src/selectors/AdvancedSearchFiltersForm.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/selectors/Domain.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/selectors/Policy.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/setup/addUtilsToWindow.ts" "@typescript-eslint/no-unsafe-type-assertion" 20 +"../../src/setup/index.ts" "import/no-named-as-default" 1 +"../../src/setup/moduleInitPolyfill.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/setup/platformSetup/index.native.ts" "no-empty-function" 1 +"../../src/setup/telemetry/index.ts" "no-empty-function" 1 +"../../src/setup/telemetry/index.web.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/stories/AddressSearch.stories.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/stories/ButtonWithDropdownMenu.stories.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/stories/DragAndDrop.stories.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/stories/EReceipt.stories.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/stories/Form.stories.tsx" "@typescript-eslint/no-unsafe-type-assertion" 10 +"../../src/stories/HeaderWithBackButton.stories.tsx" "no-restricted-imports" 2 +"../../src/stories/MenuItem.stories.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/stories/MenuItemComparison.stories.tsx" "@typescript-eslint/no-deprecated/MenuItem" 29 +"../../src/stories/MenuItemComparison.stories.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 22 +"../../src/stories/PopoverMenu.stories.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 +"../../src/stories/ReportActionItemImages.stories.tsx" "no-restricted-syntax" 1 +"../../src/stories/TextInput.stories.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/styles/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/styles/index.ts" "rulesdir/no-raw-typography" 27 +"../../src/styles/theme/utils.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/styles/utils/autoCompleteSuggestion/index.web.ts" "no-restricted-syntax" 1 +"../../src/styles/utils/cursor/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 13 +"../../src/styles/utils/FontUtils/fontFamily/multiFontFamily/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/styles/utils/generators/TooltipStyleUtils/isOverlappingAtTop/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/styles/utils/index.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../src/styles/utils/index.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/styles/utils/index.ts" "rulesdir/no-raw-typography" 4 +"../../src/TIMEZONES.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/types/expo.d.ts" "no-redeclare" 1 +"../../src/types/form/SpendRuleForm.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/types/onyx/Account.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/AccountData.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/BankAccount.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/CancellationDetails.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/CardFeeds.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/Domain.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/DomainErrors.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/DomainPendingActions.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/DuplicateWorkspace.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/ExpenseRule.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/ExpensifyCardSettings.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/ExportTemplate.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/Fund.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/Login.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/PendingContactAction.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/PersonalBankAccount.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/PersonalCard.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/PersonalDetails.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/PlaidData.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/Policy.ts" "no-restricted-syntax" 2 +"../../src/types/onyx/PolicyCategory.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/PolicyEmployee.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/PolicyJoinMember.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/PolicyTag.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/PrivatePersonalDetails.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/PrivateSubscription.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/RecentWaypoint.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/ReimbursementAccount.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/Report.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/ReportAction.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/ReportActionReactions.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/ReportMetadata.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/ReportNameValuePairs.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/SaveSearch.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/ShareBankAccount.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/Transaction.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/UnshareBankAccount.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/UserWallet.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/VacationDelegate.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/ValidateDomainTwoFactorCode.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/VerifyValidateCodeAction.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/WalletAdditionalDetails.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/WalletOnfido.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/WalletTerms.ts" "no-restricted-syntax" 1 +"../../src/types/onyx/WalletTransfer.ts" "no-restricted-syntax" 1 +"../../src/types/utils/asMutable.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/types/utils/callOrReturn.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/types/utils/CollectionDataSet.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/types/utils/EmptyObject.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/types/utils/getEmptyArray.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/types/utils/htmlDivElementRef.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/types/utils/ObjectUtils.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../src/types/utils/textRef.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/types/utils/viewRef.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/utils/createDummySharedValue.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/utils/times.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/actions/AppTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/actions/BankAccountsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/actions/EnforceActionExportRestrictions.ts" "no-restricted-imports" 1 +"../../tests/actions/IOU/BuildOnyxDataForMoneyRequestTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 6 +"../../tests/actions/IOU/CreateDraftTransactionTest.ts" "no-restricted-imports" 1 +"../../tests/actions/IOU/MoneyRequestBuilderTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 2 +"../../tests/actions/IOU/MoneyRequestBuilderTest.ts" "no-restricted-imports" 1 +"../../tests/actions/IOU/MoneyRequestSettersTest.ts" "no-restricted-imports" 1 +"../../tests/actions/IOU/MoneyRequestTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/actions/IOU/MoneyRequestTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/actions/IOU/RequestMoneyTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/actions/IOU/RequestMoneyTest.ts" "no-restricted-imports" 1 +"../../tests/actions/IOU/SearchUpdateTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/actions/IOU/SearchUpdateTest.ts" "no-restricted-imports" 1 +"../../tests/actions/IOU/SplitExpenseItemsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 6 +"../../tests/actions/IOU/SplitReportTotalsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 10 +"../../tests/actions/IOU/SplitReportTotalsTest.ts" "no-restricted-imports" 1 +"../../tests/actions/IOUTest/GetUpdateMoneyRequestParamsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../tests/actions/IOUTest/HoldTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/actions/IOUTest/HoldTest.ts" "no-restricted-imports" 1 +"../../tests/actions/IOUTest/PayMoneyRequestTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/actions/IOUTest/PayMoneyRequestTest.ts" "no-restricted-imports" 1 +"../../tests/actions/IOUTest/ReceiptTest.ts" "no-restricted-imports" 1 +"../../tests/actions/IOUTest/SplitSelfDMTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/actions/IOUTest/SplitTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/actions/IOUTest/TrackExpenseTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../tests/actions/IOUTest/TrackExpenseTest.ts" "no-restricted-imports" 1 +"../../tests/actions/IOUTest/UpdateMoneyRequestTest.ts" "no-restricted-imports" 1 +"../../tests/actions/IOUTest/UpdateMoneyRequestVendorTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/actions/MergeTransactionTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 5 +"../../tests/actions/OdometerTransactionUtilsTest.ts" "no-restricted-imports" 1 +"../../tests/actions/OnyxUpdateManagerTest.ts" "no-restricted-imports" 1 +"../../tests/actions/PolicyCategoryTest.ts" "no-restricted-imports" 1 +"../../tests/actions/PolicyProfileTest.ts" "no-restricted-imports" 1 +"../../tests/actions/PolicyTagTest.ts" "no-restricted-imports" 1 +"../../tests/actions/PolicyTaxTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 8 +"../../tests/actions/PolicyTaxTest.ts" "no-unsafe-optional-chaining" 1 +"../../tests/actions/ReportFieldTest.ts" "no-restricted-imports" 1 +"../../tests/actions/ReportPreviewActionUtilsTest.ts" "no-restricted-imports" 2 +"../../tests/actions/ReportTest.ts" "no-restricted-imports" 2 +"../../tests/actions/TaskTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/actions/TaskTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/actions/TaskTest.ts" "no-restricted-imports" 1 +"../../tests/actions/TransactionTest.ts" "no-restricted-imports" 1 +"../../tests/navigation/addCardToDigitalWalletUrlTests.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/navigation/cleanStaleReportActionBackToParamTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/navigation/ensureTabNavigatorRoutesTests.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/navigation/findFocusedRouteWithOnyxTabGuardTests.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/navigation/getPathFromStateTests.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/navigation/LinkedActionNotFoundGuardTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/navigation/OneTransactionThreadRedirectHandlerOnyxTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/navigation/splitExpenseDynamicRouteTests.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/perf-local/OptionsListChatPreview.perf-local.ts" "@typescript-eslint/no-unnecessary-type-assertion" 5 +"../../tests/perf-local/OptionsListChatPreview.perf-local.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/perf-test/DynamicReportDetailsPage.perf-test.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/perf-test/DynamicReportDetailsPage.perf-test.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/perf-test/MoneyRequestReportActionsList.perf-test.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/perf-test/ReportActionCompose.perf-test.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/AddAgentRulePageTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/ui/AgentRulesSectionTest.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../tests/ui/AgentRulesSectionTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/ui/AgentsPromoBannersTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/AncestorReportActionItemTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/ui/ApproverSelectionListTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/ui/BaseModalTest.tsx" "react-hooks/globals" 1 +"../../tests/ui/CategorySelectorTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 8 +"../../tests/ui/ChangeReceiptBillingAccountPageTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/ui/ChatActionableButtonsTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/ui/ChronosTimerHeaderButton.test.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/ClearReportActionErrorsUITest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/ui/components/CompactMenuContextTest.tsx" "@typescript-eslint/no-deprecated/MenuItem" 6 +"../../tests/ui/components/HeaderViewTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/ui/components/IOURequestStepConfirmationPageTest.tsx" "no-restricted-imports" 1 +"../../tests/ui/components/MenuItemTest.tsx" "@typescript-eslint/no-deprecated/MenuItem" 7 +"../../tests/ui/components/MoneyReportHeaderActionsPlacementTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/ui/components/MoneyReportHeaderFocusSelectionModeTest.tsx" "no-restricted-imports" 1 +"../../tests/ui/components/MoneyReportHeaderSelectionModeTest.tsx" "no-restricted-imports" 1 +"../../tests/ui/components/MoneyRequestHeaderSecondaryActionsTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/ui/components/SettlementButtonTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 7 +"../../tests/ui/ComposerImperativeRefTest.tsx" "@typescript-eslint/no-deprecated/keyCode" 1 +"../../tests/ui/ComposerTest.tsx" "rulesdir/no-raw-typography" 1 +"../../tests/ui/DomainNameOrNotFoundWrapperTest.tsx" "@typescript-eslint/no-unsafe-member-access" 1 +"../../tests/ui/DynamicCategoryDefaultTaxRatePageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/DynamicReportDetailsPageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/ui/DynamicWorkspaceWorkflowsApprovalsExpensesFromPageFastEditSaveTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/DynamicWorkspaceWorkflowsApprovalsExpensesFromPageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/ExpensifyCodePageTest.tsx" "@typescript-eslint/no-unsafe-return" 1 +"../../tests/ui/ImportedMembersPageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/ui/InitialSettingsPageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/IOURequestStepDistanceManualTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/ui/IOURequestStepDistanceOdometerBackupTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/ui/IOURequestStepDistanceOdometerDiscardGuardTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/ui/IOURequestStepDistanceOdometerNextSyncTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/ui/IOURequestStepDistanceOdometerSflResumeTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/ui/IOURequestStepDistanceRateTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../tests/ui/IOURequestStepDistanceTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/IssueNewCardAssigneeStepTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/ui/IssueNewCardConfirmValidateCodePageTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/ui/IssueNewCardConfirmValidateCodePageTest.tsx" "no-unsafe-optional-chaining" 1 +"../../tests/ui/MoneyReportViewTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 2 +"../../tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 10 +"../../tests/ui/MoneyRequestReportPreview.test.tsx" "@typescript-eslint/no-deprecated/originalMessage" 5 +"../../tests/ui/MoneyRequestReportPreview.test.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../tests/ui/MoneyRequestReportPreview.test.tsx" "no-restricted-imports" 1 +"../../tests/ui/MoneyRequestViewReceiptTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/ui/MoneyRequestViewTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/ui/MyTripsPageTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/ui/NumericBigNumberPadTest.tsx" "no-inner-declarations" 1 +"../../tests/ui/ProfilePageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 5 +"../../tests/ui/PublicProfilePageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../tests/ui/PushRowWithModalTest.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 +"../../tests/ui/RecentlyAddedSectionTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/ReportActionItemTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/ui/ReportActionItemTest.tsx" "react-hooks/globals" 1 +"../../tests/ui/ReportActionsListTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/ReportActionsListTest.tsx" "no-unsafe-optional-chaining" 2 +"../../tests/ui/RulesNewPageDescribeRuleTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/ui/SearchEditMultiplePageTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/ui/SearchMoneyRequestReportPageTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 2 +"../../tests/ui/SearchMoneyRequestReportPageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/ui/SearchPageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 9 +"../../tests/ui/SearchRouterContextualSuggestionTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/ServerSelectionTest.tsx" "no-restricted-imports" 1 +"../../tests/ui/ShareSubmitScanFieldsTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/SubmitDetailsPageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/TableTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/ui/TagSelectorTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 5 +"../../tests/ui/TransactionPreviewContentTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/ui/TransactionReceiptOdometerReplaceTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/ui/UnreadIndicatorsTest.tsx" "no-restricted-imports" 1 +"../../tests/ui/WorkArrangementPageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/WorkspaceInviteMessageDEWApproverRowTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/WorkspaceInviteMessageFastEditSaveTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/WorkspaceInvoiceVBASectionTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/WorkspaceMoreFeaturesPageTest.tsx" "no-restricted-imports" 1 +"../../tests/ui/WorkspacePageWithSectionsTest.tsx" "@typescript-eslint/no-unsafe-member-access" 1 +"../../tests/ui/WorkspaceWorkflowsApprovalsCreatePageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/WorkspaceWorkflowsApprovalsEditPageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/WorkspaceWorkflowsDEWHidePeopleTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/WorkspaceWorkflowsLoadMoreTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/WorkspaceWorkflowsLockedBankAccountTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/WorkspaceWorkflowsPayerPageTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../tests/ui/WorkspaceWorkflowsPayerRowTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/WorkspaceWorkflowsSmartLimitApprovalsTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/AccountSwitcherSkeletonViewTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/activeSpansSweepOrderTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/AddExistingExpenseTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/ApiUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../tests/unit/APIWriteWhenReadyTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/unit/backgroundLocationTrackingTaskTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/BankAccountUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 13 +"../../tests/unit/calculateSuperWideRHPWidthTest.ts" "@typescript-eslint/no-unsafe-return" 3 +"../../tests/unit/canEditFieldOfMoneyRequestTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/canEditFieldOfMoneyRequestTest.ts" "@typescript-eslint/no-deprecated/randomReportAction.originalMessage" 3 +"../../tests/unit/canEditFieldOfMoneyRequestTest.ts" "no-restricted-imports" 1 +"../../tests/unit/CategoryUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 5 +"../../tests/unit/ChronosTimerActionTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 2 +"../../tests/unit/ChronosTimerActionTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/cleanupPreMountedDraftReportsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/CollectDepositAccountUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/CompanyCardsRBRTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/components/Avatar/connected/ChatThreadAvatarTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 6 +"../../tests/unit/components/Form/FormProvider.test.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/components/Form/FormProvider.test.tsx" "import/no-duplicates" 1 +"../../tests/unit/components/HTMLEngineProvider/VictoryChartAttributesParsers.test.ts" "rulesdir/no-raw-typography" 1 +"../../tests/unit/components/MoneyReportTransactionThreadContext.test.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/components/reportDetails/ReportDetailsMenuItemsTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/ComposerLocalTimeTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/ContextMenuActionsCopyMessageTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 6 +"../../tests/unit/ContextMenuActionsDeleteTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/CopyPolicySettingsConfirmTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/CopyPolicySettingsConfirmTest.tsx" "react-hooks/immutability" 1 +"../../tests/unit/CopyPolicySettingsNavigationTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/CopyPolicySettingsUpgradeTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/CopyPolicySettingsUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/CreateReportMenuItemTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/DateUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/DateUtilsTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 3 +"../../tests/unit/DistanceRateTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/DistanceRequestUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/ExpenseDefaultRuleUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/FlaggedExpensesTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../tests/unit/FormulaTest.ts" "no-restricted-imports" 1 +"../../tests/unit/getCentralPaneReportIDTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/unit/getDefaultCardFeedTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../tests/unit/getReportSubmitViolationSummaryTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/unit/getSendMessageListWeightTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/GlobalReimbursementPayErrorTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/unit/goBackFromWorkspaceSettingPagesTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 5 +"../../tests/unit/GPSDraftDetailsActionsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/GPSDraftDetailsUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/HandleMovedScanFailedExpensesTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/HandleMovedScanFailedExpensesTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/unit/HomePage/RecentlyAddedSection/useRecentlyAddedDataTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../tests/unit/HomePage/YourSpendSection/useYourSpendDataTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 6 +"../../tests/unit/HomePage/YourSpendSection/useYourSpendDataTest.ts" "no-restricted-imports" 1 +"../../tests/unit/HomePage/YourSpendSection/YourSpendSectionTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useAppReturnCount.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useAutocompleteSuggestions.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/hooks/useBlurOnKeyboardHide.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useBrokenDirectCompanyCardFeedsForAdmin.test.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/unit/hooks/useChangeTransactionsReportReports.test.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/hooks/useConfirmationSections.test.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useConfirmationValidation.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useDeleteTransactionsInvoiceOfflineTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 4 +"../../tests/unit/hooks/useDismissOnMoneyRequestReportRemoval.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/hooks/useExpenseActionsInvoiceDeleteTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../tests/unit/hooks/useExpenseSubmission.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 6 +"../../tests/unit/hooks/useFeedKeysWithAssignedCards.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 5 +"../../tests/unit/hooks/useFilesValidation.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../tests/unit/hooks/useFilterPendingDeleteReports.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 9 +"../../tests/unit/hooks/useFormErrorManagement.test.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useHoldRejectActionsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useHydrateReportsFromSnapshot.test.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 6 +"../../tests/unit/hooks/useInsightsFilters.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useNonPersonalCardList.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/unit/hooks/useOdometerReceiptStitcherTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/usePayChatReportActions.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useReportWasDeleted.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 14 +"../../tests/unit/hooks/useSearchBulkActionsDeleteTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/hooks/useSearchBulkActionsDownloadReceiptsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useSearchBulkActionsDuplicateTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useSearchBulkActionsExportTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useSeedMyExpensesSearch.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useSelectedTransactionsActions.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../tests/unit/hooks/useSelectionModeReportActions.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/hooks/useSplitEffectivePolicy.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useSplitParticipants.test.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useTimeSensitiveAddBankAccount.test.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/hooks/useTimeSensitiveAddBankAccount.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useTimeSensitiveLockedBankAccount.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/hooks/useTransactionThreadReportID.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/unit/hooks/useTransactionThreadReportIDs.test.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/hooks/useUpcomingTravelReservations.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/ImportedMerchantRulesPageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/IOUAmountSubmissionTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/IOUUtilsTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/IOUUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 5 +"../../tests/unit/IOUUtilsTest.ts" "no-restricted-imports" 1 +"../../tests/unit/LastVisitedPathOnBackgroundTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/LHNAvatarPipelineTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 7 +"../../tests/unit/LhnSearchPreviewParityTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 25 +"../../tests/unit/LhnSearchPreviewParityTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/libs/receiptStorageTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/libs/TransactionThreadNavigationUtilsTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/libs/TransactionThreadNavigationUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/logSubmittedReceiptMilestoneTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/MerchantRuleSuggestionUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/MergeTransactionUtilsTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/ModifiedExpenseMessageTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 2 +"../../tests/unit/ModifiedExpenseMessageTest.ts" "no-restricted-imports" 1 +"../../tests/unit/navigateAfterOnboardingTest.ts" "no-restricted-imports" 1 +"../../tests/unit/navigateToWorkspacesPageTest.ts" "no-restricted-imports" 1 +"../../tests/unit/Navigation/createRootStackNavigator/handleToggleModalWithHistoryAction.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/unit/Navigation/getIsScreenBlurredWebTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/Navigation/getIsScreenBlurredWebTest.ts" "@typescript-eslint/no-unsafe-return" 1 +"../../tests/unit/Navigation/getLastRouteTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/Navigation/getTabStateWithFocusedTargetTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/unit/Navigation/hasDifferentSplitScope.test.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/unit/Navigation/preMountBufferTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 8 +"../../tests/unit/NextStepUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 6 +"../../tests/unit/NumberFormatUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/OnboardingFlowTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/OnyxDerived/computeForReportTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../tests/unit/OnyxDerived/visibleReportActionsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/onyxDerivedComputeSpanTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/OnyxDerivedSelfHealTest.ts" "no-restricted-imports" 1 +"../../tests/unit/OnyxDerivedTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/unit/OnyxDerivedTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/unit/OnyxDerivedTest.tsx" "no-restricted-imports" 1 +"../../tests/unit/OptionsListUtilsTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 14 +"../../tests/unit/pages/DynamicReportDetailsPage/useReportDetailsDeleteModalTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/pages/DynamicReportDetailsPage/useReportDetailsReportNameTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/pages/DynamicReportDetailsPage/useReportDetailsRequestDataTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/pages/DynamicReportDetailsPage/useReportDetailsRequestDataTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/pages/inbox/ConciergeDraftContext.test.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/pages/inbox/report/HomeAddressRequiredContent.test.tsx" "@typescript-eslint/no-deprecated/originalMessage" 2 +"../../tests/unit/pages/inbox/report/ReportActionEditMessageContext.test.tsx" "no-restricted-imports" 1 +"../../tests/unit/pages/Search/SearchChangeApproverPageTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/pages/settings/AddAgentPageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/PaymentUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/performPostBatchCleanupTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/PersistedRequests.ts" "no-restricted-imports" 1 +"../../tests/unit/PolicyUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/unit/preMountDraftReportTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/ProductMarketingWindowTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/QuickActionUtilsTest.ts" "no-restricted-imports" 2 +"../../tests/unit/ReceiptUploadRetryHandlerTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/ReceiptUploadRetryHandlerTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../tests/unit/RenameLegacySageIntacctExportDateTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/unit/ReportActionItemSingleTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/ReportActionsUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 38 +"../../tests/unit/ReportAlternateTextParityTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 15 +"../../tests/unit/ReportAlternateTextParityTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/ReportAlternateTextUtilsTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 67 +"../../tests/unit/ReportAlternateTextUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/reportAttributesTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 2 +"../../tests/unit/ReportNameUtilsTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 33 +"../../tests/unit/ReportPrimaryActionUtilsTest.ts" "no-restricted-imports" 1 +"../../tests/unit/ReportSecondaryActionUtilsTest.ts" "no-restricted-imports" 2 +"../../tests/unit/ReportSelectorTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 6 +"../../tests/unit/ReportTitleUtilsTest.ts" "no-restricted-imports" 1 +"../../tests/unit/ReportUtilsCanShowReportRecipientLocalTimeTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/ReportUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 7 +"../../tests/unit/ReportUtilsTest.ts" "no-restricted-imports" 1 +"../../tests/unit/ReportUtilsThreadSummaryTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/resolveTransactionThreadReportTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/Search/applyShiftRangeBatchToSelectionTest.ts" "@typescript-eslint/consistent-generic-constructors" 2 +"../../tests/unit/Search/ChatSearchViewTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/unit/Search/ExpenseFlatSearchViewTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/unit/Search/ExpenseGroupedSearchViewSplitTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/unit/Search/ExpenseGroupedSearchViewTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/unit/Search/ExpenseReportSearchViewTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/unit/Search/FooterTotalSelectionRetentionTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/unit/Search/LazyGroupSelectionTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/Search/LazyGroupSelectionTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 5 +"../../tests/unit/Search/SearchColumnsPageTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../tests/unit/Search/SearchQueryUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/unit/Search/SearchUIUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../tests/unit/Search/seedMyExpensesSearchTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../tests/unit/Search/TaskSearchViewTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/unit/Search/useOptimisticSearchTrackingTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../tests/unit/Search/useSearchLoadingStateTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/unit/Search/useSearchSnapshotTest.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/unit/Search/yourSpendApplicabilityTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/SearchActionsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 5 +"../../tests/unit/SearchAutocompleteListTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 3 +"../../tests/unit/SearchAutocompleteListTest.tsx" "import/no-duplicates" 1 +"../../tests/unit/SearchAutocompleteListTest.tsx" "no-restricted-imports" 1 +"../../tests/unit/SearchEditMultipleUtilsTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/SequentialQueueTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/showReportActionNotificationTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/unit/SidebarLinksTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/SidebarLinksTest.tsx" "react/button-has-type" 1 +"../../tests/unit/SidebarTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 2 +"../../tests/unit/SidebarUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/SidebarUtilsTest.ts" "no-restricted-imports" 1 +"../../tests/unit/StartupDataSpansTest.ts" "@typescript-eslint/no-misused-promises" 1 +"../../tests/unit/submitDismissStrategiesTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/submitWithDismissFirstTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/TodosUtilsTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/TodosUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/TransactionNavigationUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/TransactionPreviewUtils.test.ts" "@typescript-eslint/no-deprecated/originalMessage" 2 +"../../tests/unit/TransactionTest.ts" "no-restricted-imports" 1 +"../../tests/unit/TransactionUtilsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/useAccountIDToNameMapTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/useGPSWaypointMarkersTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/useIsDomainUsingCardTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/useMarkAsReadTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/useMoneyRequestReportDataTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/useMoneyRequestReportPaginationTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/useMoneyRequestReportScrollTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/useMoneyRequestReportSortedTransactionsTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/useMoneyRequestReportVisibleActionsTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/usePersonalDetailSearchSelectorTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/usePreMountDestinationTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/useReceiptHoverZoomTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/useReportIDToNameMapTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/useScreenInitialFocusTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/useSearchPageSetupTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 +"../../tests/unit/useSelectionListShortcutsTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/useSendMessageSpanMarksTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/useSidebarOrderedReportsTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/useSubPageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 19 +"../../tests/unit/useTodoCountsTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../tests/unit/useTodoCountsTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/useTransactionViolationOfWorkspaceTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/useTwoFactorAuthRouteTests.ts" "@typescript-eslint/no-unnecessary-type-assertion" 3 +"../../tests/unit/ViolationFixParityTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/unit/WhisperContentMentionContextTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/unit/WorkspacesSettingsUtilsTest.ts" "no-restricted-imports" 1 +"../../tests/utils/collections/card.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/utils/createMock.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/utils/createMockScreenNavigation.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../tests/utils/LHNTestUtils.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/utils/ReportTestUtils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../wdyr.ts" "import/no-named-as-default" 1 +"../rsbuild/CustomVersionFilePlugin.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../rsbuild/ModuleInitTimingPlugin.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../rsbuild/rsbuild.common.ts" "@typescript-eslint/no-unsafe-argument" 1 +"../rsbuild/rsbuild.common.ts" "@typescript-eslint/no-unsafe-assignment" 1 +"../rsbuild/rsbuild.common.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 diff --git a/config/oxlint/plugins/core-rules.mjs b/config/oxlint/plugins/core-rules.mjs new file mode 100644 index 000000000000..8684a2253672 --- /dev/null +++ b/config/oxlint/plugins/core-rules.mjs @@ -0,0 +1,29 @@ +// no-invalid-this is deliberately absent, even though ESLint enables it: it reaches +// sourceCode.getJSDocComment, which oxlint's bridge answers with `throw Error("...not supported at +// present (and deprecated)")` at node_modules/oxlint/dist/lint.js:5765, erroring out 36 files. +import {builtinRules} from 'eslint/use-at-your-own-risk'; + +import {withEslintDirectiveIdsFor} from '../eslintDirectives.mjs'; + +const NAMES = [ + 'no-restricted-syntax', + 'no-unreachable-loop', + 'lines-between-class-members', + 'no-octal', + 'no-octal-escape', + 'no-undef-init', + 'strict', + 'dot-notation', + 'no-dupe-args', + 'no-return-await', +]; + +const plugin = { + meta: { + name: 'core', + version: '0.0.1', + }, + rules: withEslintDirectiveIdsFor(Object.fromEntries(NAMES.map((name) => [name, builtinRules.get(name)])), (name) => name), +}; + +export default plugin; diff --git a/config/oxlint/plugins/expensify-rules.mjs b/config/oxlint/plugins/expensify-rules.mjs new file mode 100644 index 000000000000..44972fde89f2 --- /dev/null +++ b/config/oxlint/plugins/expensify-rules.mjs @@ -0,0 +1,40 @@ +import fs from 'node:fs'; +import {createRequire} from 'node:module'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +import {create as preferLocaleCompareCreate, meta as preferLocaleCompareMeta} from '../preferLocaleCompareFromContext.mjs'; +import {withFullGating} from '../reactCompilerGate.mjs'; + +const require = createRequire(import.meta.url); +const pluginDir = path.dirname(fileURLToPath(import.meta.url)); + +const RULE_DIRS = [path.resolve(pluginDir, '../../../node_modules/eslint-config-expensify/eslint-plugin-expensify'), path.resolve(pluginDir, '../../../eslint-plugin-local-rules')]; + +const HELPER_FILES = new Set(['CONST.js']); + +const rules = {}; +for (const dir of RULE_DIRS) { + for (const file of fs.readdirSync(dir)) { + if (!file.endsWith('.js') || HELPER_FILES.has(file)) { + continue; + } + rules[file.replace(/\.js$/, '')] = require(path.join(dir, file)); + } +} + +const plugin = { + meta: { + name: 'rulesdir', + version: '0.0.1', + }, + rules: { + ...rules, + 'no-inline-useOnyx-selector': withFullGating(rules['no-inline-useOnyx-selector']), + // ESLint's copy calls getParserServices at create() time, which throws in a jsPlugin, so a + // type-free rewrite stands in for it. + 'prefer-locale-compare-from-context': {create: preferLocaleCompareCreate, meta: preferLocaleCompareMeta}, + }, +}; + +export default plugin; diff --git a/config/oxlint/plugins/hosted-rules.mjs b/config/oxlint/plugins/hosted-rules.mjs new file mode 100644 index 000000000000..61d48962187f --- /dev/null +++ b/config/oxlint/plugins/hosted-rules.mjs @@ -0,0 +1,97 @@ +import {createRequire} from 'node:module'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +import {EXHAUSTIVE_DEPS_USECALLBACK_USEMEMO_PATTERN} from '../../reactCompiler/suppressedRules.mjs'; +import {withEslintDirectiveIds, withEslintDirectiveIdsFor} from '../eslintDirectives.mjs'; +import {create as noDeprecatedSelfClosingJsxCreate, meta as noDeprecatedSelfClosingJsxMeta} from '../noDeprecatedSelfClosingJsx.mjs'; +import {withFullGating, withMessageGating} from '../reactCompilerGate.mjs'; +import {hostedRuleNames} from '../ruleNames.mjs'; + +const require = createRequire(import.meta.url); +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..'); + +function rulesOf(packageName) { + let mod; + try { + mod = require(path.resolve(repoRoot, 'node_modules', packageName)); + } catch { + // A directory require ignores the package's `exports` map, so a package that ships no `main` + // (typescript-eslint) resolves only by bare specifier. + mod = require(packageName); + } + return mod?.rules ?? mod?.default?.rules ?? {}; +} + +const react = rulesOf('eslint-plugin-react'); +const importPlugin = rulesOf('eslint-plugin-import'); +const jsdoc = rulesOf('eslint-plugin-jsdoc'); +const storybook = rulesOf('eslint-plugin-storybook'); +const typescriptEslint = rulesOf('@typescript-eslint/eslint-plugin'); + +// eslint-plugin-react-hooks is not a root dependency: it arrives nested under eslint-config-expensify, +// and that nested copy is the one ESLint loads. Resolving by bare specifier would run a second install. +const reactHooks = (() => { + const configEntry = require.resolve('eslint-config-expensify', {paths: [repoRoot]}); + return rulesOf(require.resolve('eslint-plugin-react-hooks', {paths: [path.dirname(configEntry)]})); +})(); + +// naming-convention demands parser services at startup, which oxlint's JS plugins cannot provide. +// getParserServices(context, true) tolerates a missing `program` but hard-fails on missing node maps +// and reads context.languageOptions before any check, so both are stubbed. With this repo's selector +// groups the rule never reaches the type checker anyway (no `types` selector). +const STUB_PARSER_SERVICES = { + esTreeNodeToTSNodeMap: new Map(), + tsNodeToESTreeNodeMap: new Map(), +}; + +function withStubbedParserServices(rule) { + return { + ...rule, + create(context) { + const sourceCode = context.sourceCode ?? context.getSourceCode(); + // Prototype shadowing, not a Proxy: oxlint defines `parserServices` as a read-only, + // non-configurable data property, which a Proxy get trap may not report a different value for. + const stubbedSourceCode = Object.create(sourceCode, { + parserServices: {value: STUB_PARSER_SERVICES}, + }); + const stubbedContext = Object.create(context, { + sourceCode: {value: stubbedSourceCode}, + getSourceCode: {value: () => stubbedSourceCode}, + languageOptions: { + value: context.languageOptions ?? {parser: {meta: {name: 'oxlint-js-plugin'}}}, + }, + }); + return rule.create(stubbedContext); + }, + }; +} + +function hostRules(rules, eslintPrefix, names) { + return withEslintDirectiveIdsFor(Object.fromEntries(names.map((name) => [name, rules[name]])), (name) => `${eslintPrefix}/${name}`); +} + +const SEPARATELY_GATED = new Set(['jsx-no-constructed-context-values', 'exhaustive-deps']); + +function plainlyHosted(eslintPrefix) { + return hostedRuleNames(eslintPrefix).filter((name) => !SEPARATELY_GATED.has(name)); +} + +const plugin = { + meta: { + name: 'hosted', + version: '0.0.1', + }, + rules: { + ...hostRules(react, 'react', plainlyHosted('react')), + ...hostRules(importPlugin, 'import', plainlyHosted('import')), + ...hostRules(jsdoc, 'jsdoc', plainlyHosted('jsdoc')), + ...hostRules(storybook, 'storybook', plainlyHosted('storybook')), + ...hostRules({'naming-convention': withStubbedParserServices(typescriptEslint['naming-convention'])}, '@typescript-eslint', plainlyHosted('@typescript-eslint')), + 'jsx-no-constructed-context-values': withFullGating(withEslintDirectiveIds(react['jsx-no-constructed-context-values'], 'react/jsx-no-constructed-context-values')), + 'no-deprecated-self-closing-jsx': withEslintDirectiveIds({create: noDeprecatedSelfClosingJsxCreate, meta: noDeprecatedSelfClosingJsxMeta}, '@typescript-eslint/no-deprecated'), + 'exhaustive-deps': withMessageGating(withEslintDirectiveIds(reactHooks['exhaustive-deps'], 'react-hooks/exhaustive-deps'), EXHAUSTIVE_DEPS_USECALLBACK_USEMEMO_PATTERN), + }, +}; + +export default plugin; diff --git a/config/oxlint/plugins/rc-rules.mjs b/config/oxlint/plugins/rc-rules.mjs new file mode 100644 index 000000000000..48a8801f3bb0 --- /dev/null +++ b/config/oxlint/plugins/rc-rules.mjs @@ -0,0 +1,41 @@ +import {withEslintDirectiveIds} from '../eslintDirectives.mjs'; +import {RULE_BY_CATEGORY, reactCompilerDiagnostics} from '../reactCompilerRust.mjs'; + +function buildRule(ruleName) { + return { + meta: { + type: 'problem', + docs: { + description: `React Compiler: ${ruleName}`, + url: `https://react.dev/reference/eslint-plugin-react-hooks/lints/${ruleName}`, + }, + schema: [], + }, + create(context) { + return { + Program() { + const filename = context.filename ?? context.getFilename(); + const sourceCode = context.sourceCode ?? context.getSourceCode(); + for (const diagnostic of reactCompilerDiagnostics(filename, sourceCode.text)) { + if (diagnostic.ruleName !== ruleName) { + continue; + } + context.report({loc: diagnostic.loc, message: diagnostic.message}); + } + }, + }; + }, + }; +} + +const rules = Object.fromEntries(Object.values(RULE_BY_CATEGORY).map((ruleName) => [ruleName, withEslintDirectiveIds(buildRule(ruleName), `react-hooks/${ruleName}`)])); + +const plugin = { + meta: { + name: 'rc', + version: '0.0.1', + }, + rules, +}; + +export default plugin; diff --git a/config/oxlint/preferLocaleCompareFromContext.mjs b/config/oxlint/preferLocaleCompareFromContext.mjs new file mode 100644 index 000000000000..c870968d854d --- /dev/null +++ b/config/oxlint/preferLocaleCompareFromContext.mjs @@ -0,0 +1,53 @@ +// oxlint's JS plugins get no type information, so the type query ESLint's +// rulesdir/prefer-locale-compare-from-context makes (`isString(getTypeAtLocation(...))`) cannot be made +// here. `localeCompare` exists on exactly one built-in prototype, so the receiver check is dropped +// rather than approximated. The resulting divergence is an object with its own `localeCompare`. +import {createRequire} from 'node:module'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +const require = createRequire(import.meta.url); +const pluginDir = path.dirname(fileURLToPath(import.meta.url)); +const expensifyPlugin = path.resolve(pluginDir, '../../node_modules/eslint-config-expensify/eslint-plugin-expensify'); + +/** These modules are ESM; `require` hands back the namespace, so unwrap a default if there is one. */ +function load(relative) { + const module = require(path.join(expensifyPlugin, relative)); + return module?.default ?? module; +} + +const CONST = load('CONST.js'); +const {isInTestFile} = require(path.join(expensifyPlugin, 'utils/index.js')); + +const message = CONST.MESSAGE.PREFER_LOCALE_COMPARE_FROM_CONTEXT; + +const meta = { + type: 'problem', + docs: { + description: message, + }, + schema: [], +}; + +function create(context) { + return { + CallExpression(node) { + const filename = context.filename ?? context.getFilename(); + if (isInTestFile(filename)) { + return; + } + + if (node.callee?.type !== 'MemberExpression' || node.callee.property?.name !== 'localeCompare') { + return; + } + + if (!node.callee.object) { + return; + } + + context.report({node, message}); + }, + }; +} + +export {meta, create}; diff --git a/config/oxlint/reactCompilerGate.mjs b/config/oxlint/reactCompilerGate.mjs new file mode 100644 index 000000000000..38311b1b96ba --- /dev/null +++ b/config/oxlint/reactCompilerGate.mjs @@ -0,0 +1,62 @@ +// oxlint has no processor concept and no way to filter a native Rust rule's diagnostics, so the +// ESLint react-compiler-compat processor is replicated here by wrapping `context.report` in a JS plugin. +import path from 'node:path'; + +import {didBothCompilersMemoizeFile} from '../reactCompiler/checkBoth.mjs'; + +const memoizationCache = new Map(); + +function isFileMemoizedByBothCompilers(filename, sourceText) { + if (!memoizationCache.has(filename)) { + const normalized = filename.split(path.sep).join('/'); + const skipped = normalized.includes('/tests/') || normalized.startsWith('tests/') || normalized.includes('node_modules/'); + memoizationCache.set(filename, skipped ? false : didBothCompilersMemoizeFile(sourceText, filename)); + } + return memoizationCache.get(filename); +} + +function resolveMessage(rule, descriptor) { + if (typeof descriptor?.message === 'string') { + return descriptor.message.replaceAll(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => descriptor.data?.[key] ?? match); + } + const template = rule.meta?.messages?.[descriptor?.messageId]; + if (typeof template !== 'string') { + return ''; + } + return template.replaceAll(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => descriptor.data?.[key] ?? match); +} + +function withGating(rule, shouldGate) { + return { + ...rule, + create(context) { + const gatedContext = Object.create(context, { + report: { + // Forwarded as-is rather than as one descriptor: ESLint also accepts the legacy + // `report(node, message)` form, and dropping the extra arguments would change the message. + value(...args) { + if (shouldGate(rule, args[0])) { + const filename = context.filename ?? context.getFilename(); + const sourceText = (context.sourceCode ?? context.getSourceCode()).text; + if (isFileMemoizedByBothCompilers(filename, sourceText)) { + return; + } + } + return context.report(...args); + }, + }, + }); + return rule.create(gatedContext); + }, + }; +} + +function withFullGating(rule) { + return withGating(rule, () => true); +} + +function withMessageGating(rule, pattern) { + return withGating(rule, (target, descriptor) => pattern.test(resolveMessage(target, descriptor))); +} + +export {withFullGating, withMessageGating}; diff --git a/config/oxlint/reactCompilerRust.mjs b/config/oxlint/reactCompilerRust.mjs new file mode 100644 index 000000000000..8fe042c6bfa4 --- /dev/null +++ b/config/oxlint/reactCompilerRust.mjs @@ -0,0 +1,236 @@ +import path from 'node:path'; +import {transformSync} from 'oxc-transform-react'; + +const RULE_BY_CATEGORY = { + Refs: 'refs', + EffectSetState: 'set-state-in-effect', + RenderSetState: 'set-state-in-render', + PreserveManualMemo: 'preserve-manual-memoization', + Immutability: 'immutability', + StaticComponents: 'static-components', + UseMemo: 'use-memo', + Globals: 'globals', + ErrorBoundaries: 'error-boundaries', + Purity: 'purity', + IncompatibleLibrary: 'incompatible-library', + UnsupportedSyntax: 'unsupported-syntax', +}; + +// A `Config` diagnostic means the options handed to the compiler are wrong, so it is never +// ignorable: a broken ENVIRONMENT would otherwise become twelve rules that silently report nothing. +const CONFIG_CATEGORY = 'Config'; + +// ESLint does not enable the rule behind each of these, so surfacing them would be oxlint-only +// noise. +const IGNORED_CATEGORIES = new Set([ + 'CapitalizedCalls', + 'EffectDependencies', + 'EffectDerivationsOfState', + 'EffectExhaustiveDependencies', + 'FBT', + 'Gating', + 'Hooks', + 'Invariant', + 'MemoDependencies', + 'Suppression', + 'Syntax', + 'Todo', + 'VoidUseMemo', +]); + +const COMPILATION_SKIPPED_CATEGORIES = new Set(['EffectDependencies', 'IncompatibleLibrary', 'PreserveManualMemo', 'UnsupportedSyntax']); + +const ENVIRONMENT = { + validateRefAccessDuringRender: true, + validateNoSetStateInRender: true, + validateNoSetStateInEffects: true, + // The plugin spells this `validateNoJSXInTryStatements`; oxc spells it with a lowercase `sx`. + validateNoJsxInTryStatements: true, + validateNoImpureFunctionsInRender: true, + validateStaticComponents: true, + validateNoFreezingKnownMutableFunctions: true, + validateNoVoidUseMemo: true, + validateNoCapitalizedCalls: [], + validateHooksUsage: true, + validateNoDerivedComputationsInEffects: true, + enableUseKeyedState: false, + enableVerboseNoSetStateInEffect: false, + validateExhaustiveEffectDependencies: 'off', + enableTreatRefLikeIdentifiersAsRefs: true, +}; + +// Categories are only available inside the formatted `codeframe` string, which is not a public API of +// oxc-transform-react. Hence the exact version pin in package.json and the throw on an unmapped +// category: an upstream rename has to fail loudly. +const CATEGORY_PATTERN = /react-compiler\(([^)]+)\)/; + +const cache = new Map(); + +function getLang(ext) { + if (ext === 'tsx') { + return 'tsx'; + } + if (ext === 'ts') { + return 'ts'; + } + return 'jsx'; +} + +function utf8Length(code) { + if (code < 0x80) { + return 1; + } + if (code < 0x800) { + return 2; + } + // A lone surrogate is three bytes too: it is replaced with U+FFFD on the way into Rust, so the + // source oxc measured is the same width either way. + if (code < 0x10000) { + return 3; + } + return 4; +} + +// `label.start`/`label.end` are UTF-8 *byte* offsets into the source oxc parsed, while a JavaScript +// string is indexed in UTF-16 code units. Indexing `sourceText` with one directly overshoots by one +// unit for every extra byte, which drifts across newlines in any file holding non-ASCII (281 of +// src/'s .tsx files do), so a diagnostic would anchor on the wrong line and an +// `eslint-disable-next-line` on the right line would stop suppressing it. This maps a byte offset +// back to the line and UTF-16 column oxlint's `loc` expects. Built once per file rather than per +// diagnostic, hence the closure. +function buildOffsetToPoint(source) { + // Byte and UTF-16 offset of the start of each line, both indexed by line - 1. + const lineBytes = [0]; + const lineUnits = [0]; + let byte = 0; + for (let unit = 0; unit < source.length; ) { + const code = source.codePointAt(unit); + byte += utf8Length(code); + unit += code > 0xffff ? 2 : 1; + if (code === 0x0a) { + lineBytes.push(byte); + lineUnits.push(unit); + } + } + + return function offsetToPoint(offset) { + // The last line starting at or before `offset`. An offset past the end of the source clamps + // to the final line rather than running off it. + let low = 0; + let high = lineBytes.length - 1; + while (low < high) { + const mid = Math.ceil((low + high) / 2); + if (lineBytes[mid] <= offset) { + low = mid; + } else { + high = mid - 1; + } + } + + // Walked rather than subtracted: a column is UTF-16 code units, not bytes. + let byteInLine = lineBytes[low]; + let unit = lineUnits[low]; + while (byteInLine < offset && unit < source.length) { + const code = source.codePointAt(unit); + byteInLine += utf8Length(code); + unit += code > 0xffff ? 2 : 1; + } + return {line: low + 1, column: unit - lineUnits[low]}; + }; +} + +function buildMessage(category, error) { + const heading = COMPILATION_SKIPPED_CATEGORIES.has(category) ? 'Compilation Skipped' : 'Error'; + const summary = `${heading}: ${error.message ?? 'Unknown compiler error'}`; + return error.helpMessage ? `${summary}\n\n${error.helpMessage}.` : summary; +} + +function analyze(filename, sourceText) { + let result; + try { + result = transformSync(filename, sourceText, { + lang: getLang(path.extname(filename).slice(1)), + reactCompiler: { + target: '19', + outputMode: 'lint', + // Without it the binding returns only fatal diagnostics, so a non-blocking problem + // reports nothing and the rule looks clean. + reportDiagnostics: true, + flowSuppressions: false, + // Without it the compiler skips every function under an + // `eslint-disable-next-line react-hooks/exhaustive-deps` comment, and the repo has + // 228. Lint-only: setting it in the build configs would change what those comments + // make the build memoize, i.e. what ships. + eslintSuppressionRules: [], + environment: ENVIRONMENT, + }, + }); + } catch (error) { + // Malformed options are rejected at the binding rather than reported as a diagnostic; + // swallowing it would make one bad edit to ENVIRONMENT look like a clean repo. + const message = error instanceof Error ? error.message : String(error); + if (/is none of these types|ReactCompilerOptions/.test(message)) { + throw new Error(`oxc-transform-react rejected this module's React Compiler options: ${message}`); + } + return []; + } + + const errors = result.errors ?? []; + + // A config oxc rejects is a bug in this module, not a finding about the file. Fail loudly, the + // same way an unmapped category does. + const rejected = errors.find((error) => (error.message ?? '').startsWith('Invalid React Compiler')); + if (rejected) { + throw new Error(`oxc-transform-react rejected this module's React Compiler options: ${rejected.message}`); + } + + // A fatal result carrying no `react-compiler(...)` category means the compiler never got as far + // as analyzing: a parse failure aborts that way. oxlint's own parser reports the syntax error, so + // a rule here stays quiet rather than throwing on it. + if (result.fatal && !errors.some((error) => CATEGORY_PATTERN.test(error.codeframe ?? ''))) { + return []; + } + + const diagnostics = []; + const offsetToPoint = buildOffsetToPoint(sourceText); + for (const error of errors) { + const match = CATEGORY_PATTERN.exec(error.codeframe ?? ''); + if (!match) { + throw new Error(`React Compiler diagnostic with no category in its codeframe (${filename}): ${error.message ?? ''}`); + } + const category = match[1]; + if (category === CONFIG_CATEGORY) { + throw new Error( + `oxc-transform-react rejected this module's React Compiler options while linting ${filename}: ${error.message ?? ''}. Fix ENVIRONMENT or the reactCompiler options in config/oxlint/reactCompilerRust.mjs.`, + ); + } + if (IGNORED_CATEGORIES.has(category)) { + continue; + } + const ruleName = RULE_BY_CATEGORY[category]; + if (!ruleName) { + throw new Error( + `Unknown React Compiler category '${category}' in ${filename}. oxc-transform-react likely renamed or added one: map it in RULE_BY_CATEGORY or IGNORED_CATEGORIES in config/oxlint/reactCompilerRust.mjs.`, + ); + } + const label = error.labels?.[0]; + diagnostics.push({ + ruleName, + loc: { + start: offsetToPoint(label?.start ?? 0), + end: offsetToPoint(label?.end ?? label?.start ?? 0), + }, + message: buildMessage(category, error), + }); + } + return diagnostics; +} + +function reactCompilerDiagnostics(filename, sourceText) { + if (!cache.has(filename)) { + cache.set(filename, analyze(filename, sourceText)); + } + return cache.get(filename); +} + +export {RULE_BY_CATEGORY, reactCompilerDiagnostics}; diff --git a/config/oxlint/ruleNames.d.mts b/config/oxlint/ruleNames.d.mts new file mode 100644 index 000000000000..21853846a690 --- /dev/null +++ b/config/oxlint/ruleNames.d.mts @@ -0,0 +1,7 @@ +declare const OXLINT_RULE_RENAMES: Record; +declare const HOSTED_RULE_ESLINT_ID: Record; +declare const HOSTED_RULE_ORIGIN: Record; +declare function hostedRuleNames(eslintPrefix: string): string[]; +declare function oxlintCodeToESLintRuleID(code: string): string; + +export {HOSTED_RULE_ESLINT_ID, HOSTED_RULE_ORIGIN, OXLINT_RULE_RENAMES, hostedRuleNames, oxlintCodeToESLintRuleID}; diff --git a/config/oxlint/ruleNames.mjs b/config/oxlint/ruleNames.mjs new file mode 100644 index 000000000000..ad72ded53b59 --- /dev/null +++ b/config/oxlint/ruleNames.mjs @@ -0,0 +1,98 @@ +// Plain .mjs so the bun lint pipeline and oxlint's JS plugin runtime can both read one copy; only the +// first of those can load TypeScript. +const OXLINT_RULE_RENAMES = { + 'no-object-constructor': 'no-new-object', + 'no-new-native-nonconstructor': 'no-new-symbol', +}; + +// A rule hosted through oxlint's `jsPlugins` cannot take a native plugin's prefix (`react`, +// `import`, `react-hooks` and `@typescript-eslint` are reserved), so it is registered bare under +// `hosted` and this table is what gives it back its ESLint identity. +const HOSTED_RULE_ORIGIN = { + 'jsx-no-bind': 'react', + 'function-component-definition': 'react', + 'jsx-no-constructed-context-values': 'react', + 'exhaustive-deps': 'react-hooks', + 'prefer-default-export': 'import', + order: 'import', + 'no-types': 'jsdoc', + 'naming-convention': '@typescript-eslint', + 'no-import-module-exports': 'import', + 'no-relative-packages': 'import', + 'no-useless-path-segments': 'import', + 'default-props-match-prop-types': 'react', + 'forbid-foreign-prop-types': 'react', + 'forbid-prop-types': 'react', + 'jsx-uses-react': 'react', + 'jsx-uses-vars': 'react', + 'no-access-state-in-setstate': 'react', + 'no-arrow-function-lifecycle': 'react', + 'no-deprecated': 'react', + 'no-invalid-html-attribute': 'react', + 'no-typos': 'react', + 'no-unused-class-component-methods': 'react', + 'no-unused-prop-types': 'react', + 'no-unused-state': 'react', + 'prefer-exact-props': 'react', + 'prefer-stateless-function': 'react', + 'sort-comp': 'react', + 'static-property-placement': 'react', + 'require-jsdoc': 'jsdoc', + 'await-interactions': 'storybook', + 'context-in-play-function': 'storybook', + 'default-exports': 'storybook', + 'hierarchy-separator': 'storybook', + 'no-redundant-story-name': 'storybook', + 'no-renderer-packages': 'storybook', + 'no-uninstalled-addons': 'storybook', + 'prefer-pascal-case': 'storybook', + 'story-exports': 'storybook', + 'use-storybook-expect': 'storybook', + 'use-storybook-testing-library': 'storybook', +}; + +// Repo-written hosted rules that report under an existing ESLint rule id. +const HOSTED_RULE_ESLINT_ID = { + 'no-deprecated-self-closing-jsx': '@typescript-eslint/no-deprecated', +}; + +const REACT_HOOKS_RULES_UNDER_REACT = new Set(['exhaustive-deps', 'rules-of-hooks']); + +const OXLINT_DIAGNOSTIC_CODE = /^([\w@/.-]+)\((.+)\)$/; + +function hostedRuleNames(eslintPrefix) { + return Object.keys(HOSTED_RULE_ORIGIN).filter((name) => HOSTED_RULE_ORIGIN[name] === eslintPrefix); +} + +function oxlintCodeToESLintRuleID(code) { + const match = OXLINT_DIAGNOSTIC_CODE.exec(code); + if (!match) { + return code; + } + const [, plugin, rule] = match; + if (plugin === 'eslint' || plugin === 'core') { + return OXLINT_RULE_RENAMES[rule] ?? rule; + } + if (plugin === 'typescript') { + return `@typescript-eslint/${rule}`; + } + if (plugin === 'rc' || (plugin === 'react' && REACT_HOOKS_RULES_UNDER_REACT.has(rule))) { + return `react-hooks/${rule}`; + } + if (plugin === 'hosted') { + if (HOSTED_RULE_ESLINT_ID[rule]) { + return HOSTED_RULE_ESLINT_ID[rule]; + } + const origin = HOSTED_RULE_ORIGIN[rule]; + if (!origin) { + throw new Error(`No HOSTED_RULE_ORIGIN entry for hosted rule "${rule}". Add it to config/oxlint/ruleNames.mjs.`); + } + return `${origin}/${rule}`; + } + if (plugin === 'jsx_a11y') { + return `jsx-a11y/${rule}`; + } + return `${plugin}/${rule}`; +} + +export {HOSTED_RULE_ESLINT_ID, HOSTED_RULE_ORIGIN, OXLINT_RULE_RENAMES, hostedRuleNames, oxlintCodeToESLintRuleID}; diff --git a/config/reactCompiler/suppressedRules.d.mts b/config/reactCompiler/suppressedRules.d.mts new file mode 100644 index 000000000000..21d60d31f411 --- /dev/null +++ b/config/reactCompiler/suppressedRules.d.mts @@ -0,0 +1,4 @@ +declare const RULES_SUPPRESSED_BY_REACT_COMPILER: Set; +declare const EXHAUSTIVE_DEPS_USECALLBACK_USEMEMO_PATTERN: RegExp; + +export {EXHAUSTIVE_DEPS_USECALLBACK_USEMEMO_PATTERN, RULES_SUPPRESSED_BY_REACT_COMPILER}; diff --git a/config/reactCompiler/suppressedRules.mjs b/config/reactCompiler/suppressedRules.mjs new file mode 100644 index 000000000000..6c1664e644d5 --- /dev/null +++ b/config/reactCompiler/suppressedRules.mjs @@ -0,0 +1,8 @@ +// Plain `.mjs`: oxlint's JS plugin runtime cannot load TypeScript. +const RULES_SUPPRESSED_BY_REACT_COMPILER = new Set(['react/jsx-no-constructed-context-values', 'rulesdir/no-inline-useOnyx-selector']); + +// Only the "wrap in useCallback/useMemo" suggestions are false positives in compiled files; +// warnings about genuinely missing dependencies are not suppressed. +const EXHAUSTIVE_DEPS_USECALLBACK_USEMEMO_PATTERN = /\buseCallback\(\) Hook\b|\buseMemo\(\) Hook\b/; + +export {EXHAUSTIVE_DEPS_USECALLBACK_USEMEMO_PATTERN, RULES_SUPPRESSED_BY_REACT_COMPILER}; diff --git a/cspell.json b/cspell.json index 07b8cebe6eb5..496f73e485ff 100644 --- a/cspell.json +++ b/cspell.json @@ -141,6 +141,11 @@ "Erste", "Español", "detekt", + "oxfmtrc", + "oxlint", + "oxlintrc", + "tsgo", + "tsgolint", "Fabrikam", "fillyjonk", "hemulen", @@ -947,6 +952,7 @@ "proguard", "purchaseamount", "purchasecurrency", + "purgeable", "QAPR", "QBWC", "qrcode", @@ -1014,6 +1020,8 @@ "socklen", "syscalls", "serveo", + "nonconstructor", + "setstate", "setuptools", "sharee", "shareeEmail", @@ -1285,6 +1293,8 @@ "tests/unit/hooks/useLetterAvatars.test.tsx", "tests/unit/usePersonalDetailSearchSelectorTest.tsx", "config/eslint/eslint.seatbelt.tsv", + "config/oxlint/oxlint.seatbelt.tsv", + "oxlint.config.mts", "tests/unit/BankAccountUtilsTest.ts", "tests/unit/__snapshots__/ReportAlternateTextParityTest.ts.snap", "evals/proposalPolice/fixtures/**" diff --git a/knip.json b/knip.json index 730d221fe72e..986a13935cf6 100644 --- a/knip.json +++ b/knip.json @@ -82,6 +82,8 @@ "html-entities", "@fullstory/babel-plugin-react-native", "@fullstory/babel-plugin-annotate-react", + "eslint-config-airbnb-typescript", + "eslint-plugin-react-hooks", "eslint-config-prettier", "eslint-plugin-storybook", "@dword-design/eslint-plugin-import-alias", @@ -91,5 +93,8 @@ "diff-so-fancy", "@babel/plugin-proposal-class-properties" ], - "ignoreBinaries": ["metro-symbolicate", "mkcert", "openssl"] + "ignoreBinaries": ["metro-symbolicate", "mkcert", "openssl"], + "ignoreIssues": { + "config/oxlint/ruleNames.mjs": ["exports"] + } } diff --git a/oxlint.config.mts b/oxlint.config.mts new file mode 100644 index 000000000000..b1be3774326b --- /dev/null +++ b/oxlint.config.mts @@ -0,0 +1,1327 @@ +import {defineConfig} from 'oxlint'; + +// Every block that sets `no-restricted-imports` has to restate the whole option object, because an +// oxlint override replaces it rather than merging into it. Building each variant from these keeps the +// copies identical, the way `restrictedImportPaths` does in config/eslint/eslint.config.mjs. +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 carries this under @typescript-eslint/no-restricted-imports, which oxlint does + // not implement; its core rule accepts allowTypeImports, so the path lands in every + // block that re-sets no-restricted-imports, because an oxlint override replaces the + // option object rather than merging into it. + 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.connectWithoutView() 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-GB'."}, + { + 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.'}, +]; + +// TypeScript files also ban three helpers that have safer replacements. +const typescriptRestrictedImportPatterns = [ + ...restrictedImportPatterns, + { + group: ['**/ReportNameUtils', '**/libs/ReportNameUtils'], + importNames: ['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.', + }, +]; + +// `HeaderWithBackButton` is being migrated to the composed `Header` (`@components/Header`). +// Two patterns for what ESLint writes as one: ESLint matches with gitignore semantics, where a bare +// directory name also covers everything under it, and oxlint matches the path exactly. The second +// entry is what 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/**')); + +export default defineConfig({ + plugins: ['typescript', 'import', 'jsx-a11y', 'unicorn', 'react'], + // Aliases are declared here because oxlint reserves `react`, `import` and `jsdoc` for its native + // ports, and a jsPlugin rule id always carries a prefix. + // prefix ESLint uses, so declaring it would only add noise. + jsPlugins: [ + {name: 'rulesdir', specifier: './config/oxlint/plugins/expensify-rules.mjs'}, + {name: 'report-name-utils', specifier: './config/eslint/plugins/eslint-plugin-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'}, + ], + // Categories off: every rule is listed explicitly to mirror eslint.config.mjs. + 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'], + progress: {hide: false}, + // `eslint-seatbelt/configure` is off: eslint-seatbelt suppresses through an ESLint processor, + // which oxlint has no concept of. + }, + ignorePatterns: [ + 'oxlint.config.mts', + '!**/.storybook', + '!**/.github', + '.github/actions/**/index.js', + '**/*.config.js', + '**/*.config.mjs', + '**/node_modules/**/*', + '**/dist/**/*', + 'server/**/dist/**', + 'server/victory-chart-renderer/.dev/**', + '.eslint-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', + // OFF, shadowed not dropped: oxlint's native port anchors its reports on a different line + // than ESLint's, so ESLint's own rule runs instead as `hosted/prefer-default-export`. + '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/require-locale-for-localized-date-format': 'error', + 'rulesdir/use-double-negation-instead-of-boolean': 'error', + // OFF, blocked upstream: needs the type of the `&&` left operand; jsPlugins get no type info. + 'rulesdir/boolean-conditional-rendering': 'off', + // OFF, superseded: needs typeChecker.isArrayType, and jsPlugins get no type info. + // `unicorn/prefer-at` at default options stands in for the x[x.length - N] family. + 'rulesdir/prefer-at': 'off', + // OFF here, ON for src/**/*.ts(x): ESLint's copy needs the type checker, so oxlint runs a + // type-free rewrite (config/oxlint/preferLocaleCompareFromContext.mjs) instead. + 'rulesdir/prefer-locale-compare-from-context': 'off', + // Hosted behind the React Compiler gate (config/oxlint/reactCompilerGate.mjs): ESLint's + // processor drops this rule's messages in a dual-memoized file, and the gate mirrors that. + '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', + // ON with no ESLint counterpart: stands in for `rulesdir/prefer-at`, which needs the type + // checker. Default options only; `checkAllIndexAccess` is what 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'}], + // Dead on both tools: after the JS override below only .ts/.mts/.cts stay in scope, and JSX does not parse there. + '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', + // OFF, shadowed not dropped: oxlint's native port also fires inside Jest mock factories, + // where ESLint's does not. ESLint's own rule runs as `hosted/function-component-definition`. + 'react/function-component-definition': 'off', + // OFF, ESLint's own rule runs as `hosted/jsx-no-constructed-context-values` behind the React + // Compiler gate, which a native Rust rule cannot have. The native port diverges too: it + // anchors on the JSX provider rather than the declaration. + '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', + {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: 'JSXElement[openingElement.name.name=/^Pressable(WithoutFeedback|WithFeedback|WithDelayToggle|WithoutFocus)$/]:not(:has(JSXAttribute[name.name="sentryLabel"]))', + message: 'All Pressable components must include sentryLabel prop for Sentry tracking. Example: ', + }, + {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.'}, + ], + // NOT oxlint's native react/exhaustive-deps: it is a Rust rule, and the React Compiler + // message gate can only wrap a JS plugin. See config/oxlint/plugins/hosted-rules.mjs. + 'hosted/exhaustive-deps': 'error', + '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', + 'core/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', + // no-invalid-this is off: ESLint's implementation reaches sourceCode.getJSDocComment, which + // oxlint's JS bridge answers with a throw. + 'core/no-octal': 'error', + 'core/no-octal-escape': 'error', + 'core/no-undef-init': 'error', + 'core/strict': ['error', 'never'], + 'one-var': ['error', 'never'], + // Stands in for ESLint's deprecated `no-new-object`: oxlint only implements the new name, + // which also flags bare `Object()` while not flagging `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}], + // Stands in for ESLint's `no-new-symbol`, deprecated in 9.x for this rule. Same + // rename mechanism as no-new-object -> no-object-constructor, see OXLINT_RENAMES. + 'no-new-native-nonconstructor': 'error', + // No native oxlint rule of any name; hosted from ESLint core through `core/`. + '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: { + // Core rules the TypeScript compiler enforces; off as typescript-eslint's eslint-recommended does for TS. + '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', + // ON although ESLint reports 0: 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; oxlint rejects `@typescript-eslint` + // as a JS-plugin name, so it reports under the `hosted/` alias. + '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}], + // Every "off" in this block mirrors ESLint's resolved config for TS files: TypeScript-covered core rules, + // and typed rules eslint-config-expensify leaves off. Parity, not coverage gaps. + '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: {}, 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', + // Accepted gap: oxlint compares constituents syntactically, so `{a: string} & {a: string}` + // passes where typescript-eslint's type-checker comparison reports it. + 'typescript/no-duplicate-type-constituents': ['error', {ignoreUnions: true}], + }, + plugins: ['jsdoc'], + }, + { + files: ['**/scripts/**', '**/server/**', '**/.github/**'], + rules: { + // CLI output; ESLint resolves no-console off for these directories too. + '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: { + // Mirrors ESLint's test-file block. + '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: { + // Every "off" in this block is off in eslint.config.mjs for the same files. + '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', 'Pressable'], + message: + "The 'fsClass' prop doesn't work for custom components, only RN's View, Text and Pressable.\nPlease use the 'ForwardedFSClassProps' or 'MultipleFSClassProps' types to pass down the desired 'fsClass' value to the allowed components.", + }, + ], + }, + ], + '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: { + // Mirrors ESLint's plain-JS block. + 'prefer-arrow-callback': 'off', + 'lodash/import-scope': 'off', + }, + jsPlugins: ['eslint-plugin-lodash'], + }, + { + files: ['**/*.{mjs,cjs}', '.storybook/**/*.{ts,tsx}', 'config/rsbuild/**/*.ts'], + rules: { + // Node ESM, Storybook and the Rsbuild config need explicit extensions on relative imports; off in ESLint too. + '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', + // throttle and clone-deep stay off as in ESLint; structuredClone, the suggested clone-deep replacement, is + // unsupported in Hermes (facebook/hermes#68). + 'you-dont-need-lodash-underscore/throttle': 'off', + 'you-dont-need-lodash-underscore/is-string': 'error', + 'you-dont-need-lodash-underscore/cast-array': 'error', + '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/**/*'], + rules: { + // Sequential awaits are fine in scripts and tests; off in eslint.config.mjs for the same directories. + '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.'}]}], + }, + }, + { + // An oxlint override replaces the rule value rather than merging, so the airbnb browser + // list from the root is deliberately not repeated 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: { + // Mirrors ESLint's tests block. + '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, so this block's position is load-bearing twice: this rule + // has to stay above the 83-file block near the end of `overrides` that switches it + // off, and `no-restricted-imports` above has to stay above the two blocks that re-set + // it for 24 files between them. + '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: { + // Off in eslint.config.mjs for the same files. + '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: {'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 it keeps the base list without the TypeScript-only bans. + { + 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: { + // ESLint applies max-lines to src/** with these files in its ignores list. + 'max-lines': 'off', + }, + }, + { + files: ['scripts/**', '.github/**'], + rules: { + // Deliberate difference, not a mirror: tsgolint's no-unsafe-* port is stricter than typescript-eslint's and this + // loosely typed Actions glue is where it shows. Removing this block adds 695 findings ESLint reports 0 of. + '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 reports writes to deprecated properties, typescript-eslint does not (typescript-eslint#10643); off + // where that would flood. Costs 5 read-site findings ESLint reports in these files. + 'typescript/no-deprecated': 'off', + 'hosted/no-deprecated-self-closing-jsx': 'off', + }, + }, + { + files: ['**/*.{js,jsx,mjs,cjs}'], + rules: { + // Typed rules off for plain JS, as typescript-eslint does; parity, not coverage gaps. + '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', + }, + }, + { + files: ['**/*.d.{ts,tsx}'], + rules: { + // ESLint's code-path analyzer throws on declaration files under oxlint's plugin bridge + // (`Cannot read properties of undefined (reading 'parent')`), and a thrown plugin fails the run. + 'core/no-unreachable-loop': 'off', + }, + }, + { + // Mirrors config/eslint/eslint.config.mjs: bun-types declares + // `expect(...).resolves` / `.rejects` as returning `void` even though Bun's own docs + // recommend (and its runtime requires) awaiting them, so the rule reports every correct + // use of that pattern here. See https://github.com/oven-sh/bun/pull/23425. Without this + // block oxlint reports 30 findings across 7 files that ESLint never shows. + files: ['tests/tooling/**/*.ts'], + rules: {'typescript/await-thenable': 'off'}, + }, + { + // oxlint has no storybook plugin, so the story-file scope `plugin:storybook/recommended` + // applies internally is restated here, with the upstream severities. + files: ['**/*.{stories,story}.{ts,tsx,js,jsx,mjs,cjs}'], + rules: { + // The storybook preset turns this rule off for story globs; oxlint enabled it + // everywhere, so this mirrors the preset's scope. + 'react/rules-of-hooks': 'off', + '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', + }, + }, + { + // The one storybook rule upstream scopes to the config file rather than to stories: it + // reads the addon list off disk and checks each one is installed. + 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']}]}}, + { + // Mirrors eslint.config.mjs: the personal-details wrappers, the key declaration and test setup are exempt. + 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'}, + }, + ], +}); diff --git a/package-lock.json b/package-lock.json index 6cca7f501815..e4f160c81406 100644 --- a/package-lock.json +++ b/package-lock.json @@ -272,8 +272,10 @@ "nitrogen": "0.37.1", "onchange": "^7.1.0", "openai": "^7.0.0", - "oxc-transform-react": "0.148.0", + "oxc-transform-react": "0.155.0", "oxfmt": "^0.55.0", + "oxlint": "1.89.0", + "oxlint-tsgolint": "7.0.2003", "patch-package": "^8.1.0-canary.1", "peggy": "^4.0.3", "portfinder": "^1.0.34", @@ -13237,9 +13239,9 @@ ] }, "node_modules/@oxc-transform-react/binding-android-arm-eabi": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-android-arm-eabi/-/binding-android-arm-eabi-0.148.0.tgz", - "integrity": "sha512-W+elZaL7gMDaRC6OtkYO2gOelNNcCDBO7EAZYGSkPW2WXoCTkILVibnpDkiMFlwBnKQRwqdq7SPk/KwwFyMtPQ==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-android-arm-eabi/-/binding-android-arm-eabi-0.155.0.tgz", + "integrity": "sha512-X0gdaAcnVLdTUduUdsJ7pRPDpTwhiHRcq8wOC37HXMA1L5XaHbu/zQkeCmb84CChiIvRiQqHF6WkO8DLl62yeA==", "cpu": [ "arm" ], @@ -13254,9 +13256,9 @@ } }, "node_modules/@oxc-transform-react/binding-android-arm64": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-android-arm64/-/binding-android-arm64-0.148.0.tgz", - "integrity": "sha512-qHDAUwJOfsIAL6s3smScVtCVbkrKYAF2GhNSb2jB3fYvdbaIk96W0BSXvBoye7eee8sIVTjznrb0fSYZxmmdGQ==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-android-arm64/-/binding-android-arm64-0.155.0.tgz", + "integrity": "sha512-OtYUiF+jniic0JVMoyZPABnSckQUjDJwIyawDCjq8dM5f0bTDvURKgbDJrL7tR+Z86xvdxkvzwtqb0Xb+HlrBA==", "cpu": [ "arm64" ], @@ -13271,9 +13273,9 @@ } }, "node_modules/@oxc-transform-react/binding-darwin-arm64": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-darwin-arm64/-/binding-darwin-arm64-0.148.0.tgz", - "integrity": "sha512-vkhY+l8u8A3+hfsxQMK+xP7ejiKWR5hbRsnDbg5fNa4tCNNzpeJ8xqmlZ6+MKGhXMP03ZtjXlXGyRaGgCFoAgQ==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-darwin-arm64/-/binding-darwin-arm64-0.155.0.tgz", + "integrity": "sha512-6EjLyB0XcCaXDWS8/q7QfMdmEGkeI11L9dtKu7Ju3Pl0dRKLcwXVdzdjGP3itzHy4RDbEAuVlCwO6lfFRpkiWg==", "cpu": [ "arm64" ], @@ -13288,9 +13290,9 @@ } }, "node_modules/@oxc-transform-react/binding-darwin-x64": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-darwin-x64/-/binding-darwin-x64-0.148.0.tgz", - "integrity": "sha512-Ehu33kg2NocBwrVxtlLkGpOzWhzFXRwo7CYxpLFIgmQT24iMYzjgsDjRUW9tHgC1q12UF1A7mUGhZV0wO2sUQQ==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-darwin-x64/-/binding-darwin-x64-0.155.0.tgz", + "integrity": "sha512-n80yhAy3DNm3u5OqJDpNf+S7gaNpWyICSCoiTt6+oO9YC9oWkm/ywc12Qwrg7wCFtvniR64w+hWjkxKjETdscw==", "cpu": [ "x64" ], @@ -13305,9 +13307,9 @@ } }, "node_modules/@oxc-transform-react/binding-freebsd-x64": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-freebsd-x64/-/binding-freebsd-x64-0.148.0.tgz", - "integrity": "sha512-5xxrqOGqO+gX6a+kyRtgIQsTByDcQayclyBKlgjyEiz2CXvvQ5SZn9/kGw6rbL7E+gwQQFzpVYLdYUM7zJlcXA==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-freebsd-x64/-/binding-freebsd-x64-0.155.0.tgz", + "integrity": "sha512-zZH3IX7fFrQA+zmy/amDrAe1OxqJl3vBmh+yROvsfPPaUWSn8+0Z451F9tMGhQ1iL6miBcDUHChBOVjfaJXBUg==", "cpu": [ "x64" ], @@ -13322,9 +13324,9 @@ } }, "node_modules/@oxc-transform-react/binding-linux-arm-gnueabihf": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.148.0.tgz", - "integrity": "sha512-2wDoBJ0aOo3ygoTu6TAV6UCIzlCTO8eEMWYVpLbX0iq/29WA6M3tjrRItWrE0suLP0uXcBDmWrsUu4NeqsM5og==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.155.0.tgz", + "integrity": "sha512-/WnW+j/lVYwNHzaFK6uONgbC9M1S7nTWV+ONe7F9pDz3xCfvhTeWTDrqN84/vrPbxT7Xh9j3PXVnU2cw+L2NHQ==", "cpu": [ "arm" ], @@ -13339,9 +13341,9 @@ } }, "node_modules/@oxc-transform-react/binding-linux-arm-musleabihf": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.148.0.tgz", - "integrity": "sha512-Lh73KZTuC2rh4Pff1XpGh4WkQCkWdtM04adrvDGZbxGTYWXaF6uWih5WdO5Pr+ZbkceBbHgKgEQL8Lp3M1TbxQ==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.155.0.tgz", + "integrity": "sha512-VzH6NX11/y0S4nhvYRYWP54G7VPhTy7MoHiAp1nOsRIOaGssUATn1/fLglgz2EgJmxr5AHy1SK9dDd/PUmW5iQ==", "cpu": [ "arm" ], @@ -13356,13 +13358,16 @@ } }, "node_modules/@oxc-transform-react/binding-linux-arm64-gnu": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.148.0.tgz", - "integrity": "sha512-HglSrS/bA2pKL20xCPgVlHvhhVycUC7QsSmBWJH5NqWVLPA2RdcvGyEtCWDKKTH0jbYJoHGrnz/aTq3KabwrnQ==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.155.0.tgz", + "integrity": "sha512-mLF5v1yiJghvDG5wUXWv9P0iEItM3EuFgSFfqdyY134fUsU5f7t5B7nxLr4T7DrEUWhs1+SwKNWZA+Mv4HSFiw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -13373,13 +13378,16 @@ } }, "node_modules/@oxc-transform-react/binding-linux-arm64-musl": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.148.0.tgz", - "integrity": "sha512-kM0JCd+Mym4QIILv9a2CxqnriNozIz8KQjlw3kRrCdbEVEc2fIop4tvlcwYTRa+vKCWecZx627DsHTL7Ad9pzA==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.155.0.tgz", + "integrity": "sha512-EkBHEZKR4kDTU7+/mdleIu6VREm0zsQwDzQXAsMvbaRsmy8RkD8tOiXPNh6vyyqtahkLhEdj4vBs9c9h9iO66A==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -13390,13 +13398,16 @@ } }, "node_modules/@oxc-transform-react/binding-linux-ppc64-gnu": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.148.0.tgz", - "integrity": "sha512-7C8iUK/nw1wos9IJvW/RxROOz4TIj6g22a1B8ydPA5rBz8qMFNJNOPy3/UwhAd01F1h28LZlSeu6gewthGSL2A==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.155.0.tgz", + "integrity": "sha512-Iz26Sm7q5tO2h+da6leks3y62raTyZp32n3FX6WydxsecbFNzKpm+rKNPoti9Syd3u03rmkYlS8q7tPQMLLjUw==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -13407,13 +13418,16 @@ } }, "node_modules/@oxc-transform-react/binding-linux-riscv64-gnu": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.148.0.tgz", - "integrity": "sha512-0ZN6WTRcLxhPns7MYYKy7MQGTGf0jpEDACmdXEUquhPOizQ+7AsOcTnYR9Uss1dgG9G+2XAAd759i7jQXh0+2A==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.155.0.tgz", + "integrity": "sha512-CvKMO4MNKLdmy8Q75r1rwBz+VBcSYDObCyQaJOLWAwJnsWXiV7I788N4p2iWUYPWPmfvUd6d0UrrRz5ma5CCnw==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -13424,13 +13438,16 @@ } }, "node_modules/@oxc-transform-react/binding-linux-riscv64-musl": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.148.0.tgz", - "integrity": "sha512-dcd2RAANPoKmHIKJlFCnvRlYJ7s268fswMgO7kmY6m5g35XMRlv71jZ/easkMW/RPr9vUPZyUZPHq0pTyMgXUg==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.155.0.tgz", + "integrity": "sha512-OfYtP9jEMZwdnjPLVrrMvy3CQSqwdQJw6Q6YU23fObDECt1lEe2o+TJ6nFgDVAKhTl7pc9dca3vGXRgNfiBfoA==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -13441,13 +13458,16 @@ } }, "node_modules/@oxc-transform-react/binding-linux-s390x-gnu": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.148.0.tgz", - "integrity": "sha512-JdyMRM7bjODyVsDkyyJQfRCeM4MXmOzEu25RuPdz9dvPPKNb4gT9BrcnrDQiceQ2MLeLo9z7jawuCs4kjCALzw==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.155.0.tgz", + "integrity": "sha512-u5USLM5psVaNX2KEzTwfcKlzgGI16EY7Iod/Z/7Fw9FwBQiN+DUyhC99NL63aJ/Qyq0N5HIG4Y95DIEekc+EfQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -13458,13 +13478,16 @@ } }, "node_modules/@oxc-transform-react/binding-linux-x64-gnu": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.148.0.tgz", - "integrity": "sha512-UlpQs3EHU9p6oVJe/UifVer7L3OnNJULFaAmPBVz8p9wsFBHA2cBzOuD4cBTo5i+QmQMbSsrJ8qsGtPg+OCO2Q==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.155.0.tgz", + "integrity": "sha512-Kbddg5Eu/B+l6xW0CS5kaNNRK79IWu7o4NsDkn8SMz7k0N8CHV5watP0pscPIUuxcknBZkQxGqQ+b+qV7B18vA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -13475,13 +13498,16 @@ } }, "node_modules/@oxc-transform-react/binding-linux-x64-musl": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-x64-musl/-/binding-linux-x64-musl-0.148.0.tgz", - "integrity": "sha512-+ZTnNcXOeUazfT1KNAmkmYayS82c/Z06AuZUaITbZKn+Hbp99SAI5HVE+HZXumwKGF89r0JOSQviIXfuyOLpSw==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-linux-x64-musl/-/binding-linux-x64-musl-0.155.0.tgz", + "integrity": "sha512-hFZ69Mh6rZ1XeqSv4njgAB5WgKGBSTgH596EHusayzH+jtgczsMPK81k4SWlguKxrrzIqezLC8pyzhgruaNQGw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -13492,9 +13518,9 @@ } }, "node_modules/@oxc-transform-react/binding-openharmony-arm64": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-openharmony-arm64/-/binding-openharmony-arm64-0.148.0.tgz", - "integrity": "sha512-QsbwZEBvJhAvdgUDekEi5B5m0H7aZE5f32M0OLmpUd2o94mA1gm2vIHP0YgaJ96SMHlBJwvCA+lvLzEiq7nyjQ==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-openharmony-arm64/-/binding-openharmony-arm64-0.155.0.tgz", + "integrity": "sha512-XikOLTZAchqLWNep6ptHZnA47EGlV0s7zpCA7iPRuiM/V9xfgqBbolryFu9/lNjoN6twmt8k0aRuW0frxSTVNQ==", "cpu": [ "arm64" ], @@ -13509,9 +13535,9 @@ } }, "node_modules/@oxc-transform-react/binding-win32-arm64-msvc": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.148.0.tgz", - "integrity": "sha512-OxXPOxtEn05uPpVjozZDaRq9jLWNLjldTZtYwuphGEx7F+c0s49Sj+NfB3TXBa2siDIcWHNnXK4crg1RiG5W7Q==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.155.0.tgz", + "integrity": "sha512-3aJKnNR9dBcSTfrrB3PxaYP5N1Myua8DZSHhe4SEp8M/SusvxdaK6bf9h6h801UsQB66S75fk4iTyEShLHZztA==", "cpu": [ "arm64" ], @@ -13526,9 +13552,9 @@ } }, "node_modules/@oxc-transform-react/binding-win32-ia32-msvc": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.148.0.tgz", - "integrity": "sha512-I7hP5FSEuUD+8Qle97YqC9BtvS69ERDu5afbGVpL0Wy9WtO5LtUnwwVj/u+75gEFMBYyh/eCyuah1J8eCYjWWg==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.155.0.tgz", + "integrity": "sha512-i2ZSZ79dSgVvC+Ksxju3/4p9+6vXzbeY7iOoBreL4mNOCjQziYChdv5Z921V9AZ4E4LVnD/NoHpez6M8T0Ga3A==", "cpu": [ "ia32" ], @@ -13543,9 +13569,9 @@ } }, "node_modules/@oxc-transform-react/binding-win32-x64-msvc": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.148.0.tgz", - "integrity": "sha512-nUv3W/fIaMuyQN479OZcJAA4uuuL/YzdQI490OgrBrzP69vXyMYnIVivptDNJF/gu37AmT2vW2+8ZWx33ghjdQ==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-transform-react/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.155.0.tgz", + "integrity": "sha512-lrPIzcUHw/hB/LuT/G639+IrPCvvMlthF3xCE9grz1IbsSk+22sb5zyCxTvmpBxZR7/E1ckwZdnw7AmpAd+7Jg==", "cpu": [ "x64" ], @@ -13882,6 +13908,437 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@oxlint-tsgolint/darwin-arm64": { + "version": "7.0.2003", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2003.tgz", + "integrity": "sha512-TgV33rXr6ueXBwvc+0nssUkTBSXHJxv77I8p4RCjDjCnvexHtmoPiudVRDfj3S3puMDdeQdHW6jdUgUPy3nr/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/darwin-x64": { + "version": "7.0.2003", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2003.tgz", + "integrity": "sha512-hY3FMAjIaPDdK3FNxyRXRfHPOFeoBn6dmnLyKZgQ2IbTTD1adXhl6PfCIqHhCPvurigv7nf7mYuWZZ19MmJzmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/linux-arm64": { + "version": "7.0.2003", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2003.tgz", + "integrity": "sha512-eAET4JpyfBbg8SO0K74o4R55tEjVC292pIyxGOw2XGf8x/HrPNyxwQ478jMYOM54FIVOHc8sJ6VRHxluy6lucw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/linux-x64": { + "version": "7.0.2003", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2003.tgz", + "integrity": "sha512-GXdyO/XyqDJ3s/llR/oOktLsYNjZtWSQBy0JMc+/0gsNPvTcseKPhn9c6KcFyWmnr6H4vljYALbujonqSzzEGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/win32-arm64": { + "version": "7.0.2003", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2003.tgz", + "integrity": "sha512-TWauXnPfet0VgpmrstoAK58eJ4gbuwPUuUTQmYQAzE/RG1CpnxXtrKUJULf6lyerPE4KN3gM+DflIA1hOH+38Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint-tsgolint/win32-x64": { + "version": "7.0.2003", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2003.tgz", + "integrity": "sha512-DoRmfe7j8VqNlukp8liRVW45GQDhzRccNenjD/pdzelgtffW47pCMd1xbJLkPaPbKnTwID3onn3VZlL7JbebEA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.89.0.tgz", + "integrity": "sha512-Pflpba5F3gRAQ12sWglRxvWTHEdAusNDvmi4Y5N1T5SY1AGkM7AixOnET8qkrnLJ/F32qAlNokO6eCknT8tmnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.89.0.tgz", + "integrity": "sha512-dYmyD1x7LaPlb79vZWUcpTShk8dI9z1I+IubtqrBxWNJaDhQdPHwcsvg7q5StTU4xWyd9AwPfLaTYFZY8NZgtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.89.0.tgz", + "integrity": "sha512-Z/oxOPiIqiS7fItRdxaaOEC+6GD3OpGi0IxoR0dMefzQHolsKLzScw2DNcu/5gUPok3tv+G9mslvYB5bo3i0qg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.89.0.tgz", + "integrity": "sha512-eAjTkHSjAxrdCPJxO6y5sPQL5pYLKb2kvxZz9UU7tXnOZk9I/HLtkvx9S7GHY883k+AmL/eC7mqFt+w7WCwk0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.89.0.tgz", + "integrity": "sha512-0lNCA9Tl1n1FKpqy2/9ckp0kIuSrtJ/l9bMRgaEhOtM7TsOHshFuwOcvW8oA9teXLMOJT61LjEKWiaHDFGxF/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.89.0.tgz", + "integrity": "sha512-OTFEvi9W+JczaZD1Caj17SGCoW8H7TE+fvx0D7ATEWa1Lohn91UMxFQ6r145SiGy/yCxeIwptMys/RAM7gSVSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.89.0.tgz", + "integrity": "sha512-fRxgNHvBLswj6MrhW2uBq5UDhPRQlCbHeq/Nf8yuG9zOxhDsXqX/mi2PX1axgsQxMm88el7HLiJAzglAnflYxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.89.0.tgz", + "integrity": "sha512-v9ENzHvwrQoYSWTtviH1pOYc7R9UovDxVofnck50cNmPihRRTcrg5X0UlRbLOZ7IVtxPOlh2hb7kDO+FpE8SfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.89.0.tgz", + "integrity": "sha512-vfffe8mQ484AzVadlUOFpgL+JDwhvs9hrzxTgQP1FVfi4ZEUUYeffXlzC9dFV7Dgnu5s/KrNKvfmG6HxBd/ISQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.89.0.tgz", + "integrity": "sha512-Buonlp4hVS7RGNP5M7bPpGNQrenyzA2C64JcO/oz9C1c6dGOXJ7VEAnaiAMY7voou0JYyCJdvDVlFQc5DZX3DQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.89.0.tgz", + "integrity": "sha512-1eVwJJrJ3lYIPT+cm7BcBBZZXY6IQZP/oyonaruTor/MrVyWOpvnwt1oD5CLdsHg5aVBIxXodQSvt+1gK6ck5w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.89.0.tgz", + "integrity": "sha512-fRQQsKUGShdEL5Fl9o9uhj3grkZbeXKLv0icQ5tspUCNYSDy8wzzEcP1WC2bKdTQ0rSszbKlO3iiHORxV22DLg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.89.0.tgz", + "integrity": "sha512-dEtYjIy6+fHcjKklsmtmMkZRhwHr2UbzjmpvuPtQYSeN9awQ2hx5JLms5Wvri+8P/NZtD8v5hfgHALUql+/Ycg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.89.0.tgz", + "integrity": "sha512-hqjwIj5KOYM8Wjef/p7IruNi1iByLhSzbMBk62y22P/E8q7Jj0Pp+DeUTKTRYyZYCDtU1wEGlnWOnttHbyNIww==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.89.0.tgz", + "integrity": "sha512-iAQN3e6o9UgUCcdGwqRx/jYcKbG3HK7ur3rH6lTqVcQeNGZ85AeWIg6cgn/edwFQ0ofEZZItMwkzyCVTahnvEA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.89.0.tgz", + "integrity": "sha512-MNxjEByz3N/7Q4qFQuPWvc14TpN9LoD3pECEDsJTZbP/wyB7U+hduUXEl7QAbbgVezFwsb9sWtNUO6XVBndOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.89.0.tgz", + "integrity": "sha512-xxe+i9wfuFso/VnoL4mfj0UlzE+cfCFiUB06vkXCMyQdgVVmItF5FfAF4tATAktSU6uHruWR+FjqvAT9cHZZzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.89.0.tgz", + "integrity": "sha512-saFy07cO94E664glwgLmKLI40HgTL1PY86y4T6+ud4niX4H0Kdqjcmqmjps7mQNdBytV5guGbXDdyQtRfeLbLA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.89.0.tgz", + "integrity": "sha512-hLFJ/o0f/dTJh/EW0vmcbREYSQO9tYgfiAAlsOkiLFiJuRRWvYtFELUXN5o2ijpSo/tlO1n+YlWN58tZwPG18A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@peggyjs/from-mem": { "version": "1.3.0", "dev": true, @@ -35371,9 +35828,9 @@ } }, "node_modules/oxc-transform-react": { - "version": "0.148.0", - "resolved": "https://registry.npmjs.org/oxc-transform-react/-/oxc-transform-react-0.148.0.tgz", - "integrity": "sha512-0EreEjngNalcw26tFo2UPiD3tZm8KqumYFZRDb5weWdz3ENLea7+GV0kzLaSCdaq3uF4rQ0jw1GAgW41HLn90g==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/oxc-transform-react/-/oxc-transform-react-0.155.0.tgz", + "integrity": "sha512-JsZb0yduU1JdSj1RrNov2Ysio68LW9VKa10vkAJehy3NsE6UZrfcjmTLrStBRVF40YKxN30I+Cq8zJxIXzLqHQ==", "dev": true, "license": "MIT", "engines": { @@ -35383,25 +35840,25 @@ "url": "https://github.com/sponsors/oxc-project" }, "optionalDependencies": { - "@oxc-transform-react/binding-android-arm-eabi": "0.148.0", - "@oxc-transform-react/binding-android-arm64": "0.148.0", - "@oxc-transform-react/binding-darwin-arm64": "0.148.0", - "@oxc-transform-react/binding-darwin-x64": "0.148.0", - "@oxc-transform-react/binding-freebsd-x64": "0.148.0", - "@oxc-transform-react/binding-linux-arm-gnueabihf": "0.148.0", - "@oxc-transform-react/binding-linux-arm-musleabihf": "0.148.0", - "@oxc-transform-react/binding-linux-arm64-gnu": "0.148.0", - "@oxc-transform-react/binding-linux-arm64-musl": "0.148.0", - "@oxc-transform-react/binding-linux-ppc64-gnu": "0.148.0", - "@oxc-transform-react/binding-linux-riscv64-gnu": "0.148.0", - "@oxc-transform-react/binding-linux-riscv64-musl": "0.148.0", - "@oxc-transform-react/binding-linux-s390x-gnu": "0.148.0", - "@oxc-transform-react/binding-linux-x64-gnu": "0.148.0", - "@oxc-transform-react/binding-linux-x64-musl": "0.148.0", - "@oxc-transform-react/binding-openharmony-arm64": "0.148.0", - "@oxc-transform-react/binding-win32-arm64-msvc": "0.148.0", - "@oxc-transform-react/binding-win32-ia32-msvc": "0.148.0", - "@oxc-transform-react/binding-win32-x64-msvc": "0.148.0" + "@oxc-transform-react/binding-android-arm-eabi": "0.155.0", + "@oxc-transform-react/binding-android-arm64": "0.155.0", + "@oxc-transform-react/binding-darwin-arm64": "0.155.0", + "@oxc-transform-react/binding-darwin-x64": "0.155.0", + "@oxc-transform-react/binding-freebsd-x64": "0.155.0", + "@oxc-transform-react/binding-linux-arm-gnueabihf": "0.155.0", + "@oxc-transform-react/binding-linux-arm-musleabihf": "0.155.0", + "@oxc-transform-react/binding-linux-arm64-gnu": "0.155.0", + "@oxc-transform-react/binding-linux-arm64-musl": "0.155.0", + "@oxc-transform-react/binding-linux-ppc64-gnu": "0.155.0", + "@oxc-transform-react/binding-linux-riscv64-gnu": "0.155.0", + "@oxc-transform-react/binding-linux-riscv64-musl": "0.155.0", + "@oxc-transform-react/binding-linux-s390x-gnu": "0.155.0", + "@oxc-transform-react/binding-linux-x64-gnu": "0.155.0", + "@oxc-transform-react/binding-linux-x64-musl": "0.155.0", + "@oxc-transform-react/binding-openharmony-arm64": "0.155.0", + "@oxc-transform-react/binding-win32-arm64-msvc": "0.155.0", + "@oxc-transform-react/binding-win32-ia32-msvc": "0.155.0", + "@oxc-transform-react/binding-win32-x64-msvc": "0.155.0" } }, "node_modules/oxfmt": { @@ -35456,6 +35913,73 @@ } } }, + "node_modules/oxlint": { + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.89.0.tgz", + "integrity": "sha512-a25hkfo61okGPvJz432fRIPoxlKtNnOVy9w4rzrmGgNm4iVTPwpSQlmdqNWnp6p9QyyPVfAZRgnf5XvSBZFk/A==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.89.0", + "@oxlint/binding-android-arm64": "1.89.0", + "@oxlint/binding-darwin-arm64": "1.89.0", + "@oxlint/binding-darwin-x64": "1.89.0", + "@oxlint/binding-freebsd-x64": "1.89.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.89.0", + "@oxlint/binding-linux-arm-musleabihf": "1.89.0", + "@oxlint/binding-linux-arm64-gnu": "1.89.0", + "@oxlint/binding-linux-arm64-musl": "1.89.0", + "@oxlint/binding-linux-ppc64-gnu": "1.89.0", + "@oxlint/binding-linux-riscv64-gnu": "1.89.0", + "@oxlint/binding-linux-riscv64-musl": "1.89.0", + "@oxlint/binding-linux-s390x-gnu": "1.89.0", + "@oxlint/binding-linux-x64-gnu": "1.89.0", + "@oxlint/binding-linux-x64-musl": "1.89.0", + "@oxlint/binding-openharmony-arm64": "1.89.0", + "@oxlint/binding-win32-arm64-msvc": "1.89.0", + "@oxlint/binding-win32-ia32-msvc": "1.89.0", + "@oxlint/binding-win32-x64-msvc": "1.89.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2003", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/oxlint-tsgolint": { + "version": "7.0.2003", + "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2003.tgz", + "integrity": "sha512-VnK4zlqgmgq/7ZcjzCk/WpN8kKFsYGcB85Io9qT3wL4K8Un3RKmJkp793crNETieMusPMKOA4a+Mw2wbteI6TQ==", + "dev": true, + "license": "MIT", + "bin": { + "tsgolint": "bin/tsgolint.js" + }, + "optionalDependencies": { + "@oxlint-tsgolint/darwin-arm64": "7.0.2003", + "@oxlint-tsgolint/darwin-x64": "7.0.2003", + "@oxlint-tsgolint/linux-arm64": "7.0.2003", + "@oxlint-tsgolint/linux-x64": "7.0.2003", + "@oxlint-tsgolint/win32-arm64": "7.0.2003", + "@oxlint-tsgolint/win32-x64": "7.0.2003" + } + }, "node_modules/p-limit": { "version": "3.1.0", "license": "MIT", diff --git a/package.json b/package.json index 2023e9cdc908..aa24ddbfb0b4 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "perf-local": "NODE_OPTIONS=--experimental-vm-modules npx reassure --testMatch '/tests/perf-local/**/*.perf-local.ts'", "typecheck": "bun scripts/typecheck.ts", "lint": "bun scripts/lint/index.ts", + "lint-oxlint": "oxlint .", "lint-changed": "./scripts/lintChanged.sh", "lint-watch": "onchange '**/*.{js,jsx,ts,tsx,mjs,cjs}' -- bun scripts/lint/index.ts {{changed}}", "eslint-report": "bun scripts/eslint-report.ts", @@ -349,8 +350,10 @@ "nitrogen": "0.37.1", "onchange": "^7.1.0", "openai": "^7.0.0", - "oxc-transform-react": "0.148.0", + "oxc-transform-react": "0.155.0", "oxfmt": "^0.55.0", + "oxlint": "1.89.0", + "oxlint-tsgolint": "7.0.2003", "patch-package": "^8.1.0-canary.1", "peggy": "^4.0.3", "portfinder": "^1.0.34", @@ -373,6 +376,7 @@ }, "overrides": { "typescript": "$typescript", + "vite-plus": "^0.1.15", "@typescript-eslint/eslint-plugin": "8.58.1", "@typescript-eslint/parser": "8.58.1", "@typescript-eslint/typescript-estree": "8.61.0", diff --git a/scripts/lint/formatters/JSONFormatter.ts b/scripts/lint/formatters/JSONFormatter.ts new file mode 100644 index 000000000000..c00dff3f84d9 --- /dev/null +++ b/scripts/lint/formatters/JSONFormatter.ts @@ -0,0 +1,26 @@ +import type {FormatterResult, LintMessage} from '../types'; + +import Formatter from '../Formatter'; +import {LINT_SEVERITY} from '../types'; + +type JSONReport = { + messages: LintMessage[]; + errorCount: number; + warningCount: number; +}; + +class JSONFormatter extends Formatter { + readonly name = 'json'; + + format(messages: LintMessage[]): FormatterResult { + const report: JSONReport = { + messages, + errorCount: messages.filter((message) => message.severity === LINT_SEVERITY.ERROR).length, + warningCount: messages.filter((message) => message.severity === LINT_SEVERITY.WARNING).length, + }; + return {text: JSON.stringify(report), errorCount: report.errorCount, warningCount: report.warningCount}; + } +} + +export default JSONFormatter; +export type {JSONReport}; diff --git a/scripts/lint/index.ts b/scripts/lint/index.ts index ca7fe1fb25a0..f8df570bd40a 100644 --- a/scripts/lint/index.ts +++ b/scripts/lint/index.ts @@ -9,25 +9,59 @@ * bun scripts/lint/index.ts --timings -> print per-stage wall times */ +import type {TupleToUnion} from 'type-fest'; + import CLI from 'expensify-common/CLI'; +import type Linter from './Linter'; + import checkOnyxConnectBypass from '../checkOnyxConnectBypass'; import Bench from '../utils/Bench'; import ESLintLinter from './eslint/ESLintLinter'; +import JSONFormatter from './formatters/JSONFormatter'; import StylishFormatter from './formatters/StylishFormatter'; import Pipeline from './LintPipeline'; +import OxlintLinter from './oxlint/OxlintLinter'; import ReactCompilerFilter from './processors/ReactCompilerFilter'; -import Seatbelt, {resolveSeatbeltOptions} from './processors/Seatbelt'; +import Seatbelt, {SEATBELT_TSV_BY_LINTER, resolveSeatbeltOptions} from './processors/Seatbelt'; import StratifyNoDeprecated from './processors/StratifyNoDeprecated'; const projectRoot = `${import.meta.dir}/../..`; +const LINTER_NAMES = ['eslint', 'oxlint'] as const; +type LinterName = TupleToUnion; + +function isLinterName(value: string): value is LinterName { + return LINTER_NAMES.some((name) => name === value); +} + +function parseLinterName(value: string): LinterName { + if (!isLinterName(value)) { + throw new Error(`Unknown linter "${value}". Expected one of: ${LINTER_NAMES.join(', ')}`); + } + return value; +} + +const FORMAT_NAMES = ['stylish', 'json'] as const; +type FormatName = TupleToUnion; + +function isFormatName(value: string): value is FormatName { + return FORMAT_NAMES.some((name) => name === value); +} + +function parseFormatName(value: string): FormatName { + if (!isFormatName(value)) { + throw new Error(`Unknown format "${value}". Expected one of: ${FORMAT_NAMES.join(', ')}`); + } + return value; +} + /* CLI argv uses kebab-case for flags documented in help */ /* eslint-disable @typescript-eslint/naming-convention */ const cli = new CLI({ flags: { 'no-cache': { - description: 'Disable the ESLint content cache', + description: 'Disable the ESLint content cache (no-op under Oxlint, which has none)', }, 'show-warnings': { description: 'Include grandfathered seatbelt warnings in the report', @@ -39,6 +73,23 @@ const cli = new CLI({ description: 'Print per-stage wall times', }, }, + namedArgs: { + linter: { + description: `Which linter produces the diagnostics (${LINTER_NAMES.join(' | ')})`, + default: 'eslint', + parse: parseLinterName, + }, + format: { + description: `How to print the report (${FORMAT_NAMES.join(' | ')})`, + default: 'stylish', + parse: parseFormatName, + }, + shards: { + description: 'Oxlint only: processes to fan the JS plugins across (0 = auto by cores and free memory; 1 = single process)', + default: 0, + parse: (value: string) => Number.parseInt(value, 10), + }, + }, positionalArgs: [ { name: 'targets', @@ -54,15 +105,22 @@ const lintTargets = cli.positionalArgs.targets.length > 0 ? cli.positionalArgs.t const showTimings = cli.flags.timings || process.env.LINT_TIMINGS === '1'; const bench = new Bench(); -const pipeline = new Pipeline( - projectRoot, - new ESLintLinter({ +function makeLinter(name: LinterName): Linter { + if (name === 'oxlint') { + return new OxlintLinter({projectRoot, fix: cli.flags.fix, shards: cli.namedArgs.shards > 0 ? String(cli.namedArgs.shards) : undefined}); + } + return new ESLintLinter({ projectRoot, useCache: !cli.flags['no-cache'], fix: cli.flags.fix, - }), - [new ReactCompilerFilter(), new StratifyNoDeprecated(), new Seatbelt(resolveSeatbeltOptions(projectRoot))], - new StylishFormatter(projectRoot, cli.flags['show-warnings']), + }); +} + +const pipeline = new Pipeline( + projectRoot, + makeLinter(cli.namedArgs.linter), + [new ReactCompilerFilter(), new StratifyNoDeprecated(), new Seatbelt(resolveSeatbeltOptions(projectRoot, process.env, SEATBELT_TSV_BY_LINTER[cli.namedArgs.linter]))], + cli.namedArgs.format === 'json' ? new JSONFormatter() : new StylishFormatter(projectRoot, cli.flags['show-warnings']), bench, ); diff --git a/scripts/lint/oxlint/OxlintLinter.ts b/scripts/lint/oxlint/OxlintLinter.ts new file mode 100644 index 000000000000..5af5c65ac3a4 --- /dev/null +++ b/scripts/lint/oxlint/OxlintLinter.ts @@ -0,0 +1,498 @@ +import {$} from 'bun'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import type {LintFileResult, LintMessage, LintSeverity, LinterResult} from '../types'; + +// eslint-disable-next-line import/extensions +import {oxlintCodeToESLintRuleID} from '../../../config/oxlint/ruleNames.mjs'; +import Linter from '../Linter'; +import {LINT_SEVERITY} from '../types'; + +const OXLINT_FILE_COUNT_KEY = 'number_of_files' as const; +const OXLINT_CONFIG_FILE = 'oxlint.config.mts'; +// Not `npx`: it passes the command to `sh -c` as one string, and Linux caps a single argv element at +// 128 KB, so a shard's file list fails to exec with E2BIG (exit 249). macOS has no per-element cap. +const OXLINT_BIN = path.join('node_modules', '.bin', 'oxlint'); + +type OxlintSpan = { + offset: number; + length: number; + line?: number; + column?: number; +}; + +type OxlintLabel = { + span?: OxlintSpan; +}; + +type OxlintDiagnostic = { + message: string; + code?: string | null; + severity?: string; + filename: string; + help?: string; + note?: string; + labels?: OxlintLabel[]; +}; + +type OxlintJSONReport = { + diagnostics: OxlintDiagnostic[]; + [OXLINT_FILE_COUNT_KEY]?: number; +}; + +type OxlintJSPlugin = string | {name: string; specifier: string}; + +type OxlintRules = Record; + +type OxlintOverride = { + files?: string[]; + jsPlugins?: OxlintJSPlugin[]; + rules?: OxlintRules; + [key: string]: unknown; +}; + +type OxlintConfig = { + jsPlugins?: OxlintJSPlugin[]; + options?: Record; + rules?: OxlintRules; + overrides?: OxlintOverride[]; + [key: string]: unknown; +}; + +type OxlintLinterOptions = { + projectRoot: string; + fix: boolean; + threads?: string; + shards?: string; +}; + +type OxlintLeg = { + label: string; + args: string[]; + lintedFiles: readonly string[]; +}; + +const FATAL_EXIT_CODE = 2; +const SIGNAL_EXIT_CODE = 128; +const OXLINT_WARNING_SEVERITIES = new Set(['warning', 'advice']); +const GIB = 1073741824; + +// Workaround for oxlint running every JS plugin on one thread (oxc#26621): JS-plugin rules are fanned +// across `--threads=1` processes with type information off, and type-aware rules run once in a separate +// process, because each oxlint process spawns its own multi-GB `oxlint-tsgolint` type program. +const JS_SHARD_MEM_GB = 2; +const TYPE_AWARE_MEM_GB = 10; + +// On macOS `os.freemem()` counts only free pages and ignores the inactive, speculative and purgeable +// pages the kernel reclaims on demand, so it reads a few GB on an idle machine with tens of GB available. +function availableMemoryBytes(): number { + if (process.platform === 'darwin') { + const vmStat = Bun.spawnSync(['vm_stat']); + if (vmStat.success) { + const text = vmStat.stdout.toString(); + const pageSize = Number(/page size of (\d+)/.exec(text)?.[1]); + const pages = (name: string) => Number(new RegExp(`^Pages ${name}:\\s+(\\d+)`, 'm').exec(text)?.[1] ?? 0); + if (pageSize > 0) { + return (pages('free') + pages('inactive') + pages('speculative') + pages('purgeable')) * pageSize; + } + } + } + return os.freemem(); +} + +function defaultShardCount(cpus = os.availableParallelism(), availableBytes = availableMemoryBytes()): number { + const byCpu = Math.floor(cpus / 2); + const byMem = Math.floor((availableBytes / GIB - TYPE_AWARE_MEM_GB) / JS_SHARD_MEM_GB); + return Math.max(1, Math.min(byCpu, byMem)); +} + +function resolveShardCount(override: string | undefined): number { + if (override === undefined || override.trim() === '') { + return defaultShardCount(); + } + const parsed = Number.parseInt(override, 10); + return Number.isFinite(parsed) && parsed >= 1 ? parsed : 1; +} + +function shardFiles(files: readonly string[], count: number): string[][] { + if (count <= 1) { + return [files.slice()]; + } + const shards: string[][] = Array.from({length: Math.min(count, files.length)}, () => []); + for (const [index, file] of files.entries()) { + shards.at(index % shards.length)?.push(file); + } + return shards; +} + +// Mirrors how oxlint names a package plugin: `eslint-plugin-lodash` -> `lodash`, `@scope/eslint-plugin-x` -> `@scope/x`. +function jsPluginName(plugin: OxlintJSPlugin): string { + if (typeof plugin !== 'string') { + return plugin.name; + } + return plugin.replace(/(^|\/)eslint-plugin(-|$)/, '$1').replace(/\/$/, ''); +} + +function withoutRulesFrom(rules: OxlintRules | undefined, pluginNames: ReadonlySet): OxlintRules { + return Object.fromEntries(Object.entries(rules ?? {}).filter(([rule]) => ![...pluginNames].some((name) => rule.startsWith(`${name}/`)))); +} + +function deriveLegConfigs(config: OxlintConfig): { + jsPlugins: OxlintConfig; + typeAware: OxlintConfig; +} { + const pluginNames = new Set([...(config.jsPlugins ?? []), ...(config.overrides ?? []).flatMap((override) => override.jsPlugins ?? [])].map(jsPluginName)); + const typeAware: OxlintConfig = { + ...config, + rules: withoutRulesFrom(config.rules, pluginNames), + overrides: (config.overrides ?? []).map((override) => { + const stripped: OxlintOverride = { + ...override, + rules: withoutRulesFrom(override.rules, pluginNames), + }; + delete stripped.jsPlugins; + return stripped; + }), + }; + delete typeAware.jsPlugins; + return { + jsPlugins: {...config, options: {...config.options, typeAware: false}}, + typeAware, + }; +} + +function messageKey(message: LintMessage): string { + return [message.ruleID, message.severity, message.line, message.column, message.message].join('\u0000'); +} + +function mergeShardResults(results: LinterResult[]): LinterResult { + const fatalShard = results.find((result) => result.exitCode >= FATAL_EXIT_CODE); + if (fatalShard) { + return fatalShard; + } + const byFile = new Map; messages: LintMessage[]}>(); + for (const result of results) { + for (const file of result.files) { + const entry = byFile.get(file.filePath) ?? { + seen: new Set(), + messages: [], + }; + entry.messages.push(...file.messages.filter((message) => !entry.seen.has(messageKey(message)))); + for (const message of file.messages) { + entry.seen.add(messageKey(message)); + } + byFile.set(file.filePath, entry); + } + } + return { + files: [...byFile].map(([filePath, {messages}]) => ({ + filePath, + messages, + })), + exitCode: results.reduce((worst, result) => Math.max(worst, result.exitCode), 0), + stderr: results + .map((result) => result.stderr) + .filter(Boolean) + .join('\n'), + }; +} + +function normalizeSeverity(severity: string | undefined): LintSeverity { + return OXLINT_WARNING_SEVERITIES.has(severity ?? '') ? LINT_SEVERITY.WARNING : LINT_SEVERITY.ERROR; +} + +/** Oxlint splits a finding across `message`, `help` and `note`; ESLint has only `message`. */ +function joinDiagnosticText(diagnostic: OxlintDiagnostic): string { + return [diagnostic.message, diagnostic.help, diagnostic.note].filter(Boolean).join('\n'); +} + +/** Oxlint reports cwd-relative paths. They must be absolute or every seatbelt row silently misses. */ +function toAbsolute(projectRoot: string, filename: string): string { + return path.isAbsolute(filename) ? filename : path.resolve(projectRoot, filename); +} + +function toLintMessage(projectRoot: string, diagnostic: OxlintDiagnostic, ruleID: string): LintMessage { + const span = diagnostic.labels?.at(0)?.span; + return { + filePath: toAbsolute(projectRoot, diagnostic.filename), + ruleID, + severity: normalizeSeverity(diagnostic.severity), + message: joinDiagnosticText(diagnostic), + line: span?.line ?? 0, + column: span?.column ?? 0, + }; +} + +/** + * `lintedFiles` comes from a separate `--debug=files` pass because oxlint's JSON omits every file + * that produced no diagnostic. + */ +function normalizeOxlintDiagnostics(diagnostics: OxlintDiagnostic[], projectRoot: string, lintedFiles: readonly string[]): LintFileResult[] { + const byFile = new Map(); + for (const filePath of lintedFiles) { + byFile.set(toAbsolute(projectRoot, filePath), []); + } + + for (const diagnostic of diagnostics) { + const filePath = toAbsolute(projectRoot, diagnostic.filename); + const messages = byFile.get(filePath) ?? []; + messages.push(toLintMessage(projectRoot, diagnostic, oxlintCodeToESLintRuleID(diagnostic.code ?? ''))); + byFile.set(filePath, messages); + } + + return [...byFile].map(([filePath, messages]) => ({filePath, messages})); +} + +/** Oxlint prints warnings such as "No files found to lint." to stdout, ahead of the JSON. */ +function extractJSONObject(text: string): string | null { + const start = text.indexOf('{'); + const end = text.lastIndexOf('}'); + if (start < 0 || end <= start) { + return null; + } + return text.slice(start, end + 1); +} + +// A killed oxlint writes nothing; a killed `oxlint-tsgolint` makes oxlint print +// `Error running tsgolint: "exit status: exit status: 1"` instead of a report and exit 1. +function producedNoJSON(stdout: string): boolean { + return extractJSONObject(stdout) === null; +} + +function isTransientFailure(stdout: string, exitCode: number): boolean { + return producedNoJSON(stdout) || exitCode >= SIGNAL_EXIT_CODE; +} + +function fatal(reason: string, stdout: string, stderr: string, exitCode: number): LinterResult { + return { + files: [], + exitCode: Math.max(FATAL_EXIT_CODE, exitCode), + stderr: `${stderr}\n${reason}\n${stdout.slice(0, 500)}`.trim(), + }; +} + +function isOxlintConfig(value: unknown): value is OxlintConfig { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +/** The config is a TypeScript module, so it is imported rather than parsed; bun runs `.mts` natively. */ +async function loadOxlintConfig(projectRoot: string): Promise { + const loaded: unknown = await import(path.join(projectRoot, OXLINT_CONFIG_FILE)); + const config = typeof loaded === 'object' && loaded !== null && 'default' in loaded ? loaded.default : undefined; + if (!isOxlintConfig(config)) { + throw new Error(`${OXLINT_CONFIG_FILE} does not default-export a config object`); + } + return config; +} + +function isOxlintReport(value: unknown): value is OxlintJSONReport { + return typeof value === 'object' && value !== null && 'diagnostics' in value && Array.isArray(value.diagnostics); +} + +/** + * Oxlint exits 1 for findings, for no files matched, and for a config that failed to parse. Only + * the first is a normal run, and `LintPipeline` treats everything at or below 1 as one, so the + * other two are promoted here. A config error is recognizable by producing no JSON at all. + */ +function parseOxlintStdout(stdout: string, stderr: string, exitCode: number, projectRoot: string, lintedFiles: readonly string[]): LinterResult { + const jsonText = extractJSONObject(stdout); + if (!jsonText) { + return fatal('Failed to parse Oxlint JSON output.', stdout, stderr, exitCode); + } + + let parsed: unknown; + try { + parsed = JSON.parse(jsonText); + } catch { + return fatal('Failed to parse Oxlint JSON output.', stdout, stderr, exitCode); + } + if (!isOxlintReport(parsed)) { + return fatal('Oxlint JSON output has no `diagnostics` array.', stdout, stderr, exitCode); + } + + // A diagnostic with no `code` is a crashing JS plugin, not a finding. + const codeless = parsed.diagnostics.filter((diagnostic) => !diagnostic.code); + if (codeless.length > 0) { + const sample = codeless + .slice(0, 5) + .map((diagnostic) => ` ${diagnostic.filename}: ${diagnostic.message}`) + .join('\n'); + return fatal(`Oxlint reported ${codeless.length} diagnostic(s) with no rule code, which means a JS plugin threw:\n${sample}`, '', stderr, exitCode); + } + + return { + files: normalizeOxlintDiagnostics(parsed.diagnostics, projectRoot, lintedFiles), + exitCode, + stderr, + }; +} + +/** + * `--type-aware` is not passed: `oxlint.config.mts` already sets `options.typeAware`. + * `--no-error-on-unmatched-pattern` is not passed either, because it turns a mistyped path into a + * silent clean pass; the empty-file-list case is detected explicitly instead. + * Oxlint has no `--cache`, so the pipeline's `--no-cache` flag has no effect here. + */ +class OxlintLinter extends Linter { + readonly name = 'oxlint'; + + constructor(private readonly options: OxlintLinterOptions) { + super(); + } + + private async listLintedFiles(targets: string[]): Promise { + const result = await $`${OXLINT_BIN} --debug=files ${targets}`.cwd(this.options.projectRoot).nothrow().quiet(); + return result.stdout + .toString() + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + } + + private async listLintedFilesWithRetry(targets: string[]): Promise<{files: string[]; retried: boolean}> { + const files = await this.listLintedFiles(targets); + if (files.length > 0) { + return {files, retried: false}; + } + return {files: await this.listLintedFiles(targets), retried: true}; + } + + private runOxlint(args: string[]) { + return $`${OXLINT_BIN} ${args}` + .cwd(this.options.projectRoot) + .env({...process.env, LINT_PIPELINE: '1'}) + .nothrow() + .quiet(); + } + + private async writeLegConfigs(): Promise<{ + jsPluginsConfig: string; + typeAwareConfig: string; + remove: () => void; + }> { + const {jsPlugins, typeAware} = deriveLegConfigs(await loadOxlintConfig(this.options.projectRoot)); + const jsPluginsConfig = `.oxlintrc.js-plugins.${process.pid}.json`; + const typeAwareConfig = `.oxlintrc.type-aware.${process.pid}.json`; + fs.writeFileSync(path.join(this.options.projectRoot, jsPluginsConfig), JSON.stringify(jsPlugins)); + fs.writeFileSync(path.join(this.options.projectRoot, typeAwareConfig), JSON.stringify(typeAware)); + return { + jsPluginsConfig, + typeAwareConfig, + remove: () => { + fs.rmSync(path.join(this.options.projectRoot, jsPluginsConfig), { + force: true, + }); + fs.rmSync(path.join(this.options.projectRoot, typeAwareConfig), { + force: true, + }); + }, + }; + } + + private async runLegs(legs: OxlintLeg[], notes: string[]): Promise { + const outputs = []; + if (this.options.fix) { + for (const leg of legs) { + outputs.push(await this.runOxlint(leg.args)); + } + } else { + outputs.push(...(await Promise.all(legs.map((leg) => this.runOxlint(leg.args))))); + } + + const parsed: LinterResult[] = []; + for (const [index, leg] of legs.entries()) { + let output = outputs.at(index); + if (!output) { + throw new Error(`Missing output for oxlint ${leg.label}`); + } + if (isTransientFailure(output.stdout.toString(), output.exitCode)) { + const how = `exit ${output.exitCode}, ${producedNoJSON(output.stdout.toString()) ? 'no JSON' : 'JSON present'}`; + output = await this.runOxlint(leg.args); + const recovered = !isTransientFailure(output.stdout.toString(), output.exitCode); + notes.push(`Oxlint ${leg.label} died (${how}) and was retried once, serially: ${recovered ? 'recovered' : 'failed again'}.`); + } + const result = parseOxlintStdout(output.stdout.toString(), output.stderr.toString(), output.exitCode, this.options.projectRoot, leg.lintedFiles); + parsed.push(result.exitCode >= FATAL_EXIT_CODE ? {...result, stderr: `Oxlint ${leg.label}:\n${result.stderr}`} : result); + } + return mergeShardResults(parsed); + } + + async run(targets: string[]): Promise { + const {files: lintedFiles, retried: listerRetried} = await this.listLintedFilesWithRetry(targets); + if (lintedFiles.length === 0) { + return fatal(`Oxlint matched no files for: ${targets.join(' ')} (asked twice, in case the first listing died)`, '', '', FATAL_EXIT_CODE); + } + + const notes: string[] = []; + if (listerRetried) { + notes.push(`Oxlint listed no files on the first attempt and ${lintedFiles.length} on the second, so that listing died rather than matching nothing.`); + } + + const baseArgs: string[] = ['--format', 'json']; + if (this.options.fix) { + baseArgs.push('--fix'); + } + + const shardCount = resolveShardCount(this.options.shards ?? process.env.OXLINT_SHARDS); + let plan: string; + let merged: LinterResult; + try { + if (shardCount <= 1) { + const threads = this.options.threads ?? process.env.OXLINT_THREADS; + const args = threads ? [`--threads=${threads}`, ...baseArgs] : baseArgs; + plan = 'Oxlint plan: 1 process, stock config.'; + merged = await this.runLegs([{label: 'process', args: [...args, ...lintedFiles], lintedFiles}], notes); + } else { + const buckets = shardFiles(lintedFiles, shardCount); + plan = `Oxlint plan: ${buckets.length} JS-plugin shards plus 1 type-aware process (cores ${os.availableParallelism()}, available memory ${(availableMemoryBytes() / GIB).toFixed(1)} GB).`; + const configs = await this.writeLegConfigs(); + try { + const legs: OxlintLeg[] = buckets.map((bucket, index) => ({ + label: `JS-plugin shard ${index + 1} of ${buckets.length}`, + args: ['-c', configs.jsPluginsConfig, '--threads=1', ...baseArgs, ...bucket], + lintedFiles: bucket, + })); + legs.push({ + label: 'type-aware process', + args: ['-c', configs.typeAwareConfig, ...baseArgs, ...targets], + lintedFiles, + }); + merged = await this.runLegs(legs, notes); + } finally { + configs.remove(); + } + } + } catch (error) { + return fatal(error instanceof Error ? error.message : String(error), '', '', FATAL_EXIT_CODE); + } + + if (notes.length === 0 && merged.exitCode < FATAL_EXIT_CODE) { + return merged; + } + return { + ...merged, + stderr: [merged.stderr, ...notes, plan].filter(Boolean).join('\n'), + }; + } +} + +export default OxlintLinter; +export { + defaultShardCount, + deriveLegConfigs, + extractJSONObject, + isOxlintConfig, + isTransientFailure, + joinDiagnosticText, + jsPluginName, + loadOxlintConfig, + mergeShardResults, + normalizeOxlintDiagnostics, + parseOxlintStdout, + producedNoJSON, + resolveShardCount, + shardFiles, +}; +export type {OxlintConfig, OxlintDiagnostic, OxlintLinterOptions}; diff --git a/scripts/lint/processors/ReactCompilerFilter.ts b/scripts/lint/processors/ReactCompilerFilter.ts index 334a9ce92a4e..0ba65204cfe1 100644 --- a/scripts/lint/processors/ReactCompilerFilter.ts +++ b/scripts/lint/processors/ReactCompilerFilter.ts @@ -3,11 +3,11 @@ import path from 'node:path'; import type {LintMessage, ProcessorContext} from '../types'; +// eslint-disable-next-line import/extensions +import {EXHAUSTIVE_DEPS_USECALLBACK_USEMEMO_PATTERN, RULES_SUPPRESSED_BY_REACT_COMPILER} from '../../../config/reactCompiler/suppressedRules.mjs'; import WorkerPool from '../../utils/WorkerPool'; import Processor from '../Processor'; -const RULES_SUPPRESSED_BY_REACT_COMPILER = new Set(['react/jsx-no-constructed-context-values', 'rulesdir/no-inline-useOnyx-selector']); -const EXHAUSTIVE_DEPS_USECALLBACK_USEMEMO_PATTERN = /\buseCallback\(\) Hook\b|\buseMemo\(\) Hook\b/; const CACHE_DIR = 'node_modules/.cache/react-compiler'; const REACT_COMPILER_FINGERPRINT_FILES = [ 'babel.config.js', diff --git a/scripts/lint/processors/Seatbelt.ts b/scripts/lint/processors/Seatbelt.ts index 5beddb43b98a..d521d8d1f4df 100644 --- a/scripts/lint/processors/Seatbelt.ts +++ b/scripts/lint/processors/Seatbelt.ts @@ -9,7 +9,12 @@ import Processor from '../Processor'; import {LINT_SEVERITY} from '../types'; const SEATBELT_NAME = 'eslint-seatbelt'; -const SEATBELT_TSV_RELATIVE = 'config/eslint/eslint.seatbelt.tsv'; +const SEATBELT_TSV_BY_LINTER = { + eslint: 'config/eslint/eslint.seatbelt.tsv', + oxlint: 'config/oxlint/oxlint.seatbelt.tsv', +} as const; + +const SEATBELT_TSV_RELATIVE = SEATBELT_TSV_BY_LINTER.eslint; const DEFAULT_FILE_HEADER = `# ${SEATBELT_NAME} temporarily allowed errors # docs: https://github.com/justjake/${SEATBELT_NAME}#readme`; @@ -476,7 +481,8 @@ async function applySeatbelt(messages: LintMessage[], options: SeatbeltOptions, const shouldWrite = anyChanged && !options.frozen && !options.readOnly; if (pruned > 0) { const verb = shouldWrite ? 'removed' : 'would remove'; - console.log(`eslint-seatbelt: ${verb} ${pruned} baseline row(s) for deleted files`); + // stderr, not stdout: --format=json writes the report to stdout and this would corrupt it. + console.error(`eslint-seatbelt: ${verb} ${pruned} baseline row(s) for deleted files`); } if (shouldWrite) { await writeTSVAtomically(options.seatbeltFile, tsv); @@ -519,11 +525,11 @@ function parseRuleSetEnvVar(value: string | undefined): SeatbeltRuleSet | undefi * - `SEATBELT_FROZEN` defaults to false (the wrapper forces `0` so `CI=true` does not freeze). * - `readOnly` defaults to `!CI`; `SEATBELT_INCREASE` forces writes. */ -function resolveSeatbeltOptions(projectRoot: string, env: NodeJS.ProcessEnv = process.env): SeatbeltOptions { +function resolveSeatbeltOptions(projectRoot: string, env: NodeJS.ProcessEnv = process.env, seatbeltRelativePath: string = SEATBELT_TSV_RELATIVE): SeatbeltOptions { const allowIncreaseRules = parseRuleSetEnvVar(env.SEATBELT_INCREASE) ?? new Set(); const isIncreaseSet = allowIncreaseRules === 'all' || allowIncreaseRules.size > 0; return { - seatbeltFile: `${projectRoot}/${SEATBELT_TSV_RELATIVE}`, + seatbeltFile: `${projectRoot}/${seatbeltRelativePath}`, projectRoot, disable: readBooleanEnvVar(env.SEATBELT_DISABLE) ?? false, frozen: readBooleanEnvVar(env.SEATBELT_FROZEN) ?? false, @@ -536,5 +542,16 @@ function resolveSeatbeltOptions(projectRoot: string, env: NodeJS.ProcessEnv = pr } export default Seatbelt; -export {applySeatbelt, canonicalizeMessages, compareMessages, countRuleIDs, parseSeatbeltTSV, resolveSeatbeltOptions, serializeSeatbeltTSV, transformMessages, updateMaxErrors}; +export { + SEATBELT_TSV_BY_LINTER, + applySeatbelt, + canonicalizeMessages, + compareMessages, + countRuleIDs, + parseSeatbeltTSV, + resolveSeatbeltOptions, + serializeSeatbeltTSV, + transformMessages, + updateMaxErrors, +}; export type {SeatbeltApplyResult, SeatbeltFileData}; diff --git a/tests/tooling/lintPipeline.test.ts b/tests/tooling/lintPipeline.test.ts index c8785733fc22..c47c99a1eabe 100644 --- a/tests/tooling/lintPipeline.test.ts +++ b/tests/tooling/lintPipeline.test.ts @@ -1,15 +1,34 @@ import {describe, expect, it} from 'bun:test'; +import fs from 'node:fs'; +import path from 'node:path'; + import type {ESLintJSONResult} from '../../scripts/lint/eslint/ESLintLinter'; +import type {OxlintConfig, OxlintDiagnostic} from '../../scripts/lint/oxlint/OxlintLinter'; import type {LintMessage, LinterResult} from '../../scripts/lint/types'; +import {oxlintCodeToESLintRuleID} from '../../config/oxlint/ruleNames.mjs'; import {normalizeESLintResults, parseESLintStdout} from '../../scripts/lint/eslint/ESLintLinter'; +import JSONFormatter from '../../scripts/lint/formatters/JSONFormatter'; import StylishFormatter from '../../scripts/lint/formatters/StylishFormatter'; import Linter from '../../scripts/lint/Linter'; import Pipeline from '../../scripts/lint/LintPipeline'; +import { + defaultShardCount, + deriveLegConfigs, + isTransientFailure, + jsPluginName, + loadOxlintConfig, + mergeShardResults, + normalizeOxlintDiagnostics, + parseOxlintStdout, + producedNoJSON, + resolveShardCount, + shardFiles, +} from '../../scripts/lint/oxlint/OxlintLinter'; import {filterReactCompilerMessages, shouldPersistCompilerCache} from '../../scripts/lint/processors/ReactCompilerFilter'; -import Seatbelt, {resolveSeatbeltOptions} from '../../scripts/lint/processors/Seatbelt'; -import {stratifyMessages} from '../../scripts/lint/processors/StratifyNoDeprecated'; +import Seatbelt, {SEATBELT_TSV_BY_LINTER, parseSeatbeltTSV, resolveSeatbeltOptions} from '../../scripts/lint/processors/Seatbelt'; +import {NO_DEPRECATED_RULE_ID, stratifyMessages} from '../../scripts/lint/processors/StratifyNoDeprecated'; import {LINT_SEVERITY} from '../../scripts/lint/types'; function makeMessage(overrides: Partial = {}): LintMessage { @@ -51,17 +70,28 @@ describe('resolveSeatbeltOptions', () => { }); it('lets SEATBELT_INCREASE force writes even locally', () => { - const options = resolveSeatbeltOptions(root, {SEATBELT_INCREASE: 'no-console'}); + const options = resolveSeatbeltOptions(root, { + SEATBELT_INCREASE: 'no-console', + }); expect(options.readOnly).toBe(false); expect(options.allowIncreaseRules).toEqual(new Set(['no-console'])); }); it('lets SEATBELT_INCREASE override SEATBELT_READ_ONLY', () => { - const options = resolveSeatbeltOptions(root, {SEATBELT_INCREASE: 'no-console', SEATBELT_READ_ONLY: '1'}); + const options = resolveSeatbeltOptions(root, { + SEATBELT_INCREASE: 'no-console', + SEATBELT_READ_ONLY: '1', + }); expect(options.readOnly).toBe(false); expect(options.allowIncreaseRules).toEqual(new Set(['no-console'])); }); + it('defaults to the ESLint baseline and takes a per-linter override', () => { + expect(resolveSeatbeltOptions(root, {}).seatbeltFile).toBe(`${root}/${SEATBELT_TSV_BY_LINTER.eslint}`); + expect(resolveSeatbeltOptions(root, {}, SEATBELT_TSV_BY_LINTER.oxlint).seatbeltFile).toBe(`${root}/${SEATBELT_TSV_BY_LINTER.oxlint}`); + expect(SEATBELT_TSV_BY_LINTER.eslint).not.toBe(SEATBELT_TSV_BY_LINTER.oxlint); + }); + it('parses SEATBELT_INCREASE=ALL', () => { expect(resolveSeatbeltOptions(root, {SEATBELT_INCREASE: 'ALL'}).allowIncreaseRules).toBe('all'); }); @@ -76,7 +106,18 @@ describe('resolveSeatbeltOptions', () => { describe('extractJSONArray via runESLint stdout', () => { it('normalizes results even when babel logs wrap the JSON array', () => { const wrapped = `babel.config.js\n - running in: undefined\n${JSON.stringify([ - {filePath: '/repo/src/a.ts', messages: [{ruleId: 'no-console', severity: 2, message: 'nope', line: 3, column: 4}]}, + { + filePath: '/repo/src/a.ts', + messages: [ + { + ruleId: 'no-console', + severity: 2, + message: 'nope', + line: 3, + column: 4, + }, + ], + }, ])}\n`; const start = wrapped.indexOf('['); const end = wrapped.lastIndexOf(']'); @@ -98,7 +139,15 @@ describe('normalizeESLintResults', () => { const [result] = normalizeESLintResults([ { filePath: '/repo/src/a.ts', - messages: [{ruleId: 'no-console', severity: 2, message: 'nope', line: 3, column: 4}], + messages: [ + { + ruleId: 'no-console', + severity: 2, + message: 'nope', + line: 3, + column: 4, + }, + ], }, ]); expect(result.filePath).toBe('/repo/src/a.ts'); @@ -134,7 +183,10 @@ describe('filterReactCompilerMessages', () => { it('drops suppressible messages when both compilers memoize the file', async () => { const messages = [ makeMessage({ruleID: 'react/jsx-no-constructed-context-values'}), - makeMessage({ruleID: 'react-hooks/exhaustive-deps', message: 'React Hook useCallback() Hook is missing a dependency'}), + makeMessage({ + ruleID: 'react-hooks/exhaustive-deps', + message: 'React Hook useCallback() Hook is missing a dependency', + }), makeMessage({ruleID: 'no-console'}), ]; const result = await filterReactCompilerMessages(messages, '/tmp', () => true); @@ -148,7 +200,12 @@ describe('filterReactCompilerMessages', () => { }); it('does not suppress genuine exhaustive-deps missing-deps warnings', async () => { - const messages = [makeMessage({ruleID: 'react-hooks/exhaustive-deps', message: 'React Hook useEffect has a missing dependency: "foo"'})]; + const messages = [ + makeMessage({ + ruleID: 'react-hooks/exhaustive-deps', + message: 'React Hook useEffect has a missing dependency: "foo"', + }), + ]; const result = await filterReactCompilerMessages(messages, '/tmp', () => true); expect(result).toEqual(messages); }); @@ -162,15 +219,38 @@ describe('filterReactCompilerMessages', () => { }); it('keeps suppressible messages when the source file cannot be read', async () => { - const messages = [makeMessage({filePath: '/tmp/does-not-exist.tsx', ruleID: 'react/jsx-no-constructed-context-values'})]; + const messages = [ + makeMessage({ + filePath: '/tmp/does-not-exist.tsx', + ruleID: 'react/jsx-no-constructed-context-values', + }), + ]; const result = await filterReactCompilerMessages(messages, '/tmp'); expect(result).toEqual(messages); }); it('does not persist fallback compiler failures to cache', () => { - expect(shouldPersistCompilerCache({filename: 'a.tsx', bothMemoized: false, cacheable: false})).toBe(false); - expect(shouldPersistCompilerCache({filename: 'a.tsx', bothMemoized: false, cacheable: true})).toBe(true); - expect(shouldPersistCompilerCache({filename: 'a.tsx', bothMemoized: true, cacheable: true})).toBe(true); + expect( + shouldPersistCompilerCache({ + filename: 'a.tsx', + bothMemoized: false, + cacheable: false, + }), + ).toBe(false); + expect( + shouldPersistCompilerCache({ + filename: 'a.tsx', + bothMemoized: false, + cacheable: true, + }), + ).toBe(true); + expect( + shouldPersistCompilerCache({ + filename: 'a.tsx', + bothMemoized: true, + cacheable: true, + }), + ).toBe(true); expect(shouldPersistCompilerCache(undefined)).toBe(false); }); }); @@ -178,13 +258,25 @@ describe('filterReactCompilerMessages', () => { describe('stratifyMessages', () => { it('rewrites no-deprecated using the source expression at the lint location', () => { const source = 'const x = StyleSheet.absoluteFillObject;\n'; - const messages = [makeMessage({ruleID: '@typescript-eslint/no-deprecated', message: '`absoluteFillObject` is deprecated.', line: 1, column: 11})]; + const messages = [ + makeMessage({ + ruleID: '@typescript-eslint/no-deprecated', + message: '`absoluteFillObject` is deprecated.', + line: 1, + column: 11, + }), + ]; const result = stratifyMessages(messages, source); expect(result.at(0)?.ruleID).toBe('@typescript-eslint/no-deprecated/StyleSheet.absoluteFillObject'); }); it('falls back to the backtick symbol in the message when there is no source', () => { - const messages = [makeMessage({ruleID: '@typescript-eslint/no-deprecated', message: '`Foo.bar` is deprecated.'})]; + const messages = [ + makeMessage({ + ruleID: '@typescript-eslint/no-deprecated', + message: '`Foo.bar` is deprecated.', + }), + ]; expect(stratifyMessages(messages, null).at(0)?.ruleID).toBe('@typescript-eslint/no-deprecated/Foo.bar'); }); @@ -194,6 +286,351 @@ describe('stratifyMessages', () => { }); }); +function oxlintDiagnostic(overrides: Partial = {}): OxlintDiagnostic { + return { + message: 'nope', + code: 'eslint(no-console)', + severity: 'error', + filename: 'src/a.ts', + labels: [{span: {offset: 42, length: 7, line: 3, column: 4}}], + ...overrides, + }; +} + +function oxlintStdout(diagnostics: OxlintDiagnostic[], numberOfFiles = 1, prefix = ''): string { + return `${prefix}{ "diagnostics": ${JSON.stringify(diagnostics)},\n "number_of_files": ${numberOfFiles},\n "number_of_rules": 475\n}\n`; +} + +describe('normalizeOxlintDiagnostics', () => { + it('maps every plugin prefix the repo produces to its ESLint rule id', () => { + const expected: Array<[string, string]> = [ + ['typescript(no-unsafe-type-assertion)', '@typescript-eslint/no-unsafe-type-assertion'], + ['import(no-cycle)', 'import/no-cycle'], + ['rc(set-state-in-effect)', 'react-hooks/set-state-in-effect'], + ['core(no-restricted-syntax)', 'no-restricted-syntax'], + ['rulesdir(no-onyx-connect)', 'rulesdir/no-onyx-connect'], + ['unicorn(prefer-at)', 'unicorn/prefer-at'], + ['eslint(no-restricted-imports)', 'no-restricted-imports'], + ['react(jsx-key)', 'react/jsx-key'], + ['hosted(naming-convention)', '@typescript-eslint/naming-convention'], + ['eslint(no-object-constructor)', 'no-new-object'], + ['react(exhaustive-deps)', 'react-hooks/exhaustive-deps'], + ]; + const diagnostics = expected.map(([code]) => oxlintDiagnostic({code})); + const [file] = normalizeOxlintDiagnostics(diagnostics, '/repo', ['src/a.ts']); + expect(file.messages.map((message) => message.ruleID)).toEqual(expected.map(([, ruleID]) => ruleID)); + }); + + it('resolves oxlint cwd-relative paths against the project root', () => { + const [file] = normalizeOxlintDiagnostics([oxlintDiagnostic()], '/repo', ['src/a.ts']); + expect(file.filePath).toBe('/repo/src/a.ts'); + expect(file.messages.at(0)?.filePath).toBe('/repo/src/a.ts'); + }); + + it('keeps the linted files that produced no diagnostic, so the seatbelt can ratchet down', () => { + const files = normalizeOxlintDiagnostics([oxlintDiagnostic()], '/repo', ['src/a.ts', 'src/clean.ts']); + expect(files.map((file) => file.filePath)).toEqual(['/repo/src/a.ts', '/repo/src/clean.ts']); + expect(files.at(1)?.messages).toEqual([]); + }); + + it('passes 1-based line and column through, and defaults a label-less diagnostic to 0', () => { + const [file] = normalizeOxlintDiagnostics([oxlintDiagnostic(), oxlintDiagnostic({labels: []})], '/repo', ['src/a.ts']); + expect(file.messages.at(0)).toMatchObject({line: 3, column: 4}); + expect(file.messages.at(1)).toMatchObject({line: 0, column: 0}); + }); + + it('folds help and note into the message, which is the only text field downstream reads', () => { + const diagnostic = oxlintDiagnostic({ + message: 'Dependency cycle detected', + help: 'Refactor to remove the cycle.', + note: 'These paths form a cycle:\nA -> B -> A', + }); + const [file] = normalizeOxlintDiagnostics([diagnostic], '/repo', ['src/a.ts']); + expect(file.messages.at(0)?.message).toBe('Dependency cycle detected\nRefactor to remove the cycle.\nThese paths form a cycle:\nA -> B -> A'); + }); + + it('demotes oxlint warning and advice severities, and treats everything else as an error', () => { + const diagnostics = [oxlintDiagnostic({severity: 'warning'}), oxlintDiagnostic({severity: 'advice'}), oxlintDiagnostic({severity: 'error'})]; + const [file] = normalizeOxlintDiagnostics(diagnostics, '/repo', ['src/a.ts']); + expect(file.messages.map((message) => message.severity)).toEqual([LINT_SEVERITY.WARNING, LINT_SEVERITY.WARNING, LINT_SEVERITY.ERROR]); + }); + + it('throws on a hosted rule with no known ESLint origin rather than emitting an unmatchable id', () => { + expect(() => normalizeOxlintDiagnostics([oxlintDiagnostic({code: 'hosted(not-a-hosted-rule)'})], '/repo', ['src/a.ts'])).toThrow(/not-a-hosted-rule/); + }); +}); + +describe('parseOxlintStdout', () => { + it('parses the report even when oxlint prints a warning to stdout ahead of it', () => { + const parsed = parseOxlintStdout(oxlintStdout([oxlintDiagnostic()], 1, 'No files found to lint. Please check your paths and ignore patterns.\n'), '', 1, '/repo', ['src/a.ts']); + expect(parsed.exitCode).toBe(1); + expect(parsed.files.at(0)?.messages.at(0)?.ruleID).toBe('no-console'); + }); + + it('treats a config that failed to parse as fatal, even though oxlint exits 1', () => { + const configError = "Failed to parse oxlint configuration file.\n\n x Rule 'nope' not found in plugin 'eslint'\n"; + const parsed = parseOxlintStdout(configError, '', 1, '/repo', ['src/a.ts']); + expect(parsed.exitCode).toBe(2); + expect(parsed.files).toEqual([]); + expect(parsed.stderr).toContain('Failed to parse Oxlint JSON output'); + }); + + it('treats a diagnostic with no rule code as a crashing JS plugin, not a finding', () => { + const parsed = parseOxlintStdout(oxlintStdout([oxlintDiagnostic({code: undefined, message: 'plugin blew up'})]), '', 1, '/repo', ['src/a.ts']); + expect(parsed.exitCode).toBe(2); + expect(parsed.files).toEqual([]); + expect(parsed.stderr).toContain('no rule code'); + expect(parsed.stderr).toContain('plugin blew up'); + }); + + it('rejects a JSON object that is not an oxlint report', () => { + expect(parseOxlintStdout('{"oops": true}', '', 1, '/repo', []).exitCode).toBe(2); + }); + + it('preserves an oxlint crash exit code above 2', () => { + expect(parseOxlintStdout('', 'oom', 137, '/repo', []).exitCode).toBe(137); + }); + + it('surfaces an Oxlint fatal through the pipeline the same way an ESLint one does', async () => { + const parsed = parseOxlintStdout('Failed to parse oxlint configuration file.', '', 1, '/repo', []); + const pipeline = new Pipeline('/tmp', new StubLinter(parsed), [new Seatbelt(resolveSeatbeltOptions('/tmp', {SEATBELT_DISABLE: '1'}))], new StylishFormatter('/tmp', false)); + const result = await pipeline.run(['.']); + expect(result.exitCode).toBe(2); + expect(result.reportText).toContain('Failed to parse Oxlint JSON output'); + }); +}); + +describe('oxlint sharding', () => { + function shardResult(filePath: string, exitCode: number, messages: LintMessage[] = []): LinterResult { + return {files: [{filePath, messages}], exitCode, stderr: ''}; + } + + it('interleaves files across shards so adjacent paths land in different buckets', () => { + const files = Array.from({length: 10}, (_, i) => `src/f${i}.ts`); + const shards = shardFiles(files, 3); + expect(shards.map((shard) => shard.length)).toEqual([4, 3, 3]); + expect(shards.at(0)).toEqual(['src/f0.ts', 'src/f3.ts', 'src/f6.ts', 'src/f9.ts']); + expect(shards.flat().sort()).toEqual([...files].sort()); + }); + + it('a shard count of 1 or more than the file count never drops or duplicates a file', () => { + const files = ['a.ts', 'b.ts', 'c.ts']; + expect(shardFiles(files, 1)).toEqual([files]); + expect(shardFiles(files, 99).flat().sort()).toEqual(files); + expect(shardFiles(files, 99)).toHaveLength(3); + }); + + it('auto-shards by machine size and honours explicit overrides', () => { + expect(resolveShardCount(undefined)).toBeGreaterThanOrEqual(1); + expect(resolveShardCount('')).toBeGreaterThanOrEqual(1); + expect(resolveShardCount('6')).toBe(6); + expect(resolveShardCount('0')).toBe(1); + expect(resolveShardCount('not-a-number')).toBe(1); + }); + + it('sizes the JS-plugin shards by half the cores, minus the memory one type-aware process needs', () => { + const gib = 1073741824; + expect(defaultShardCount(14, 48 * gib)).toBe(7); + expect(defaultShardCount(14, 18 * gib)).toBe(4); + expect(defaultShardCount(4, 16 * gib)).toBe(2); + expect(defaultShardCount(4, 4 * gib)).toBe(1); + expect(defaultShardCount(1, 64 * gib)).toBe(1); + }); + + it('derives the rule prefix of a jsPlugins entry the way oxlint does', () => { + expect( + jsPluginName({ + name: 'rulesdir', + specifier: './config/oxlint/plugins/expensify-rules.mjs', + }), + ).toBe('rulesdir'); + expect(jsPluginName('eslint-plugin-lodash')).toBe('lodash'); + expect(jsPluginName('@dword-design/eslint-plugin-import-alias')).toBe('@dword-design/import-alias'); + expect(jsPluginName('@scope/eslint-plugin')).toBe('@scope'); + }); + + /* eslint-disable @typescript-eslint/naming-convention */ + it('splits the config into a JS-plugin leg without type information and a type-aware leg without JS plugins', () => { + const config: OxlintConfig = { + plugins: ['typescript', 'import'], + jsPlugins: [{name: 'rulesdir', specifier: './plugins/rules.mjs'}, 'eslint-plugin-lodash'], + options: {typeAware: true}, + ignorePatterns: ['dist/**'], + rules: { + 'no-console': 'error', + 'typescript/no-floating-promises': 'error', + 'rulesdir/no-onyx-connect': 'error', + 'lodash/import-scope': ['error', 'method'], + }, + overrides: [ + { + files: ['**/*.test.ts'], + jsPlugins: ['@dword-design/eslint-plugin-import-alias'], + rules: { + '@dword-design/import-alias/prefer-alias': 'error', + 'no-console': 'off', + }, + }, + {files: ['**/*.js'], rules: {'rulesdir/no-onyx-connect': 'off'}}, + ], + }; + const {jsPlugins, typeAware} = deriveLegConfigs(config); + + expect(jsPlugins.options).toEqual({typeAware: false}); + expect(jsPlugins.jsPlugins).toEqual(config.jsPlugins); + expect(jsPlugins.rules).toEqual(config.rules); + expect(jsPlugins.overrides).toEqual(config.overrides); + + expect(typeAware.options).toEqual({typeAware: true}); + expect(typeAware).not.toHaveProperty('jsPlugins'); + expect(typeAware.rules).toEqual({ + 'no-console': 'error', + 'typescript/no-floating-promises': 'error', + }); + expect(typeAware.overrides).toEqual([ + {files: ['**/*.test.ts'], rules: {'no-console': 'off'}}, + {files: ['**/*.js'], rules: {}}, + ]); + expect(typeAware.overrides?.at(0)).not.toHaveProperty('jsPlugins'); + + expect(typeAware.ignorePatterns).toEqual(['dist/**']); + expect(typeAware.plugins).toEqual(['typescript', 'import']); + expect(config.jsPlugins).toHaveLength(2); + expect(config.options).toEqual({typeAware: true}); + }); + /* eslint-enable @typescript-eslint/naming-convention */ + + it('drops a finding a later leg repeats for the same file, but keeps one a single leg reports twice', () => { + const native = makeMessage({ruleID: 'no-console', line: 3, column: 4}); + const typeAwareOnly = makeMessage({ + ruleID: '@typescript-eslint/no-floating-promises', + line: 9, + column: 1, + }); + const twiceInOneLeg = makeMessage({ + ruleID: 'react-hooks/refs', + line: 12, + column: 5, + }); + const merged = mergeShardResults([shardResult('/repo/a.ts', 1, [native, twiceInOneLeg, twiceInOneLeg]), shardResult('/repo/a.ts', 1, [native, typeAwareOnly])]); + expect(merged.files).toHaveLength(1); + expect(merged.files.at(0)?.messages).toEqual([native, twiceInOneLeg, twiceInOneLeg, typeAwareOnly]); + }); + + it('retries a leg that died, not one that failed deterministically', () => { + expect(isTransientFailure('', 137)).toBe(true); + expect(isTransientFailure('Error running tsgolint: "exit status: exit status: 1"\n', 1)).toBe(true); + expect(isTransientFailure(oxlintStdout([]), 137)).toBe(true); + expect(isTransientFailure(oxlintStdout([]), 134)).toBe(true); + expect(isTransientFailure(oxlintStdout([oxlintDiagnostic({code: undefined, message: 'plugin blew up'})]), 1)).toBe(false); + expect(isTransientFailure(oxlintStdout([oxlintDiagnostic()], 1), 1)).toBe(false); + expect(isTransientFailure(oxlintStdout([]), 0)).toBe(false); + }); + + it('merges shards by concatenating files and taking the worst exit code', () => { + const merged = mergeShardResults([shardResult('/repo/a.ts', 0, [makeMessage()]), shardResult('/repo/b.ts', 1, [makeMessage()]), shardResult('/repo/c.ts', 0)]); + expect(merged.exitCode).toBe(1); + expect(merged.files.map((file) => file.filePath)).toEqual(['/repo/a.ts', '/repo/b.ts', '/repo/c.ts']); + }); + + it('a crashed JS plugin in one shard is fatal and dominates clean shards', () => { + const clean = parseOxlintStdout(oxlintStdout([oxlintDiagnostic()], 1), '', 1, '/repo', ['src/a.ts']); + const crash = parseOxlintStdout(oxlintStdout([oxlintDiagnostic({code: undefined, message: 'plugin blew up'})]), '', 1, '/repo', ['src/b.ts']); + const merged = mergeShardResults([clean, crash]); + expect(merged.exitCode).toBe(2); + expect(merged.stderr).toContain('plugin blew up'); + }); + + it('only a shard that produced no JSON is worth retrying', () => { + // A killed process writes nothing, and a config error prints its complaint without JSON. + expect(producedNoJSON('')).toBe(true); + expect(producedNoJSON('Failed to parse oxlint configuration file.\n x Rule not found\n')).toBe(true); + + // Deterministic failures are not retried: both of these carry a JSON body. + expect(producedNoJSON(oxlintStdout([oxlintDiagnostic({code: undefined, message: 'plugin blew up'})]))).toBe(false); + expect(producedNoJSON(oxlintStdout([oxlintDiagnostic()], 1))).toBe(false); + expect(producedNoJSON(oxlintStdout([]))).toBe(false); + + // Oxlint prints warnings ahead of the JSON, which must not read as a dead shard. + expect(producedNoJSON(`No files found to lint.\n${oxlintStdout([])}`)).toBe(false); + }); +}); + +describe('oxlint rule names', () => { + it('every rule id in the oxlint seatbelt is one the enabled config still produces through the mapping', async () => { + const root = path.join(import.meta.dir, '..', '..'); + const config = await loadOxlintConfig(root); + const configuredRules = [config.rules, ...(config.overrides ?? []).map((override) => override.rules)].flatMap((rules) => Object.keys(rules ?? {})); + const toDiagnosticCode = (rule: string) => { + const slash = rule.lastIndexOf('/'); + return slash < 0 ? `eslint(${rule})` : `${rule.slice(0, slash).replace('jsx-a11y', 'jsx_a11y')}(${rule.slice(slash + 1)})`; + }; + const reachable = new Set(configuredRules.map((rule) => oxlintCodeToESLintRuleID(toDiagnosticCode(rule)))); + + const {data} = parseSeatbeltTSV(fs.readFileSync(path.join(root, SEATBELT_TSV_BY_LINTER.oxlint), 'utf8')); + const seatbeltRuleIDs = new Set( + [...data.values()].flatMap((file) => file.lines.map((line) => (line.ruleID.startsWith(`${NO_DEPRECATED_RULE_ID}/`) ? NO_DEPRECATED_RULE_ID : line.ruleID))), + ); + + expect(seatbeltRuleIDs.size).toBeGreaterThan(20); + expect([...seatbeltRuleIDs].filter((ruleID) => !reachable.has(ruleID))).toEqual([]); + }); +}); + +function knownOxlintPlugins(schema: unknown): string[] { + if (typeof schema !== 'object' || schema === null || !('definitions' in schema)) { + return []; + } + const definitions: unknown = schema.definitions; + if (typeof definitions !== 'object' || definitions === null || !('LintPluginOptionsSchema' in definitions)) { + return []; + } + const pluginSchema: unknown = definitions.LintPluginOptionsSchema; + if (typeof pluginSchema !== 'object' || pluginSchema === null || !('enum' in pluginSchema) || !Array.isArray(pluginSchema.enum)) { + return []; + } + return pluginSchema.enum.filter((entry): entry is string => typeof entry === 'string'); +} + +describe('oxlint config', () => { + it('names only plugins oxlint knows, at the root and in every override', async () => { + // An unknown name in an override's `plugins` array is not an error: the override's other rules + // keep working while the rules the missing plugin owns report nothing. + const root = path.join(import.meta.dir, '..', '..'); + const known = new Set(knownOxlintPlugins(JSON.parse(fs.readFileSync(path.join(root, 'node_modules/oxlint/configuration_schema.json'), 'utf8')))); + expect(known.size).toBeGreaterThan(5); + + const config = await loadOxlintConfig(root); + const named = [config.plugins, ...(config.overrides ?? []).map((override) => override.plugins)].flatMap((plugins: unknown) => + Array.isArray(plugins) ? plugins.map((entry: unknown) => entry) : [], + ); + expect(named.filter((plugin) => typeof plugin !== 'string' || !known.has(plugin))).toEqual([]); + }); +}); + +describe('JSONFormatter', () => { + it('emits the linter-agnostic message list, warnings included', () => { + const messages = [makeMessage(), makeMessage({severity: LINT_SEVERITY.WARNING, ruleID: 'no-debugger'})]; + const result = new JSONFormatter().format(messages); + expect(JSON.parse(result.text)).toEqual({ + messages, + errorCount: 1, + warningCount: 1, + }); + expect(result.errorCount).toBe(1); + expect(result.warningCount).toBe(1); + }); + + it('emits an empty report that still parses, rather than an empty string', () => { + const result = new JSONFormatter().format([]); + expect(JSON.parse(result.text)).toEqual({ + messages: [], + errorCount: 0, + warningCount: 0, + }); + }); +}); + describe('Pipeline', () => { it('returns the linter exit code when the linter itself crashed', async () => { const pipeline = new Pipeline( diff --git a/tsconfig.node.json b/tsconfig.node.json index 7896af84ad2a..706940424424 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -9,6 +9,6 @@ // `prompts` is deliberately not listed even though the Proposal Police action imports it: the Bun // program owns those files, and listing them in two referenced projects would give editors two // candidate owners for one file. The action's imports still pull them into this program. - "include": [".github/**/*", "web/proxy.ts", "config/**/*", "config/**/*.d.mts"], + "include": [".github/**/*", "web/proxy.ts", "config/**/*", "config/**/*.d.mts", "oxlint.config.mts"], "exclude": ["**/node_modules/*", "**/dist/*", ".github/actions/**/index.js"] } From c2485b3c6a18fb26cda1b58cea497c2ca24c4050 Mon Sep 17 00:00:00 2001 From: Lukasz Modzelewski Date: Wed, 21 Oct 2026 12:31:03 +0200 Subject: [PATCH 02/32] Add oxlint migration harness and probes --- .gitignore | 12 + .oxfmtrc.json | 3 +- config/eslint/eslint.config.mjs | 6 + config/oxlint/reactCompilerRust.mjs | 2 +- cspell.json | 3 + .../no-direct-personal-details-list.test.js | 44 ++ ...ct-pre-insert-fullscreen-under-rhp.test.js | 60 +++ .../no-layout-spacing-conditional.test.js | 93 ++++ ...require-a11y-disable-justification.test.js | 54 ++ ...ire-live-region-for-status-updates.test.js | 53 ++ knip.json | 3 +- oxlint-migration/README.md | 125 +++++ oxlint-migration/checkConfigDrift.py | 217 ++++++++ oxlint-migration/checkEslintDirectives.mjs | 118 +++++ oxlint-migration/checkJsxUsesPort.py | 142 +++++ oxlint-migration/checkLocaleComparePort.py | 162 ++++++ oxlint-migration/checkParityDirection.py | 82 +++ oxlint-migration/checkReactCompilerGate.py | 139 +++++ oxlint-migration/checkReactCompilerRust.mjs | 161 ++++++ oxlint-migration/checkSidecarCoverage.py | 171 ++++++ oxlint-migration/compareFullRepo.py | 159 ++++++ oxlint-migration/compareFullRepo.sh | 40 ++ oxlint-migration/compareNativeCtxValues.py | 229 ++++++++ oxlint-migration/compareWarmCache.sh | 164 ++++++ oxlint-migration/eslint-ctx-values-rule.mjs | 14 + oxlint-migration/eslintEnabledRules.mjs | 39 ++ oxlint-migration/listAllRules.py | 247 +++++++++ .../native-vs-sidecar-probe/Counter.tsx | 24 + .../native-vs-sidecar-probe/NonAscii.tsx | 24 + .../native-vs-sidecar-probe/README.md | 141 +++++ .../native-vs-sidecar-probe/TwoComponents.tsx | 30 ++ .../rhImmutabilityAnchor.tsx | 30 ++ .../parallel-probe/shard-experiment.sh | 82 +++ .../port-probe/compareFixtures.py | 244 +++++++++ .../port-probe/eslint.fixtures.config.mjs | 260 +++++++++ .../port-probe/fixtures.manifest.json | 344 ++++++++++++ .../port-probe/fixtures/a11yNpmBatch.tsx | 132 +++++ .../port-probe/fixtures/coreOctalSyntax.cjs | 9 + .../port-probe/fixtures/corePlainJsRules.cjs | 17 + .../fixtures/coreRestrictedSyntax.tsx | 32 ++ .../port-probe/fixtures/coreStyleRules.ts | 17 + .../fixtures/deprecatedSelfClosingJsx.tsx | 9 + .../fixtures/deprecatedSelfClosingJsxCard.tsx | 12 + .../fixtures/functionComponentDefinition.tsx | 6 + .../port-probe/fixtures/importNativeBatch.ts | 46 ++ .../fixtures/importNativeBatchAbsolute.ts | 14 + .../fixtures/importNativeBatchHelper.ts | 13 + .../fixtures/importNativeBatchJs.js | 22 + .../port-probe/fixtures/importOrder.ts | 9 + .../port-probe/fixtures/importPathRules.ts | 10 + .../fixtures/inlineUseOnyxSelector.ts | 12 + .../port-probe/fixtures/jsdocNoTypes.ts | 11 + .../port-probe/fixtures/jsx11yBatchA.tsx | 206 ++++++++ .../port-probe/fixtures/jsx11yBatchB.tsx | 191 +++++++ .../port-probe/fixtures/jsxNoBind.tsx | 19 + .../jsxNoConstructedContextValues.tsx | 16 + .../port-probe/fixtures/namingConvention.ts | 47 ++ .../port-probe/fixtures/oxonlyAlias.ts | 22 + .../port-probe/fixtures/oxonlyCore.ts | 34 ++ .../port-probe/fixtures/oxonlyJsdoc.ts | 37 ++ .../port-probe/fixtures/oxonlyReportName.ts | 12 + .../port-probe/fixtures/oxonlyTyped.ts | 35 ++ .../fixtures/preferDefaultExport.ts | 2 + .../port-probe/fixtures/rawTypography.ts | 9 + .../fixtures/reactClassComponents.tsx | 37 ++ .../fixtures/reactDeprecatedApis.tsx | 22 + .../port-probe/fixtures/reactExactProps.tsx | 20 + .../port-probe/fixtures/reactNativeA11y.tsx | 9 + .../port-probe/fixtures/reactNativeRulesA.tsx | 133 +++++ .../port-probe/fixtures/reactNativeRulesB.tsx | 91 ++++ .../port-probe/fixtures/reactNativeRulesC.jsx | 110 ++++ .../port-probe/fixtures/reactPropTypes.tsx | 33 ++ .../requireLocaleForLocalizedDateFormat.ts | 7 + .../port-probe/fixtures/rhErrorBoundaries.tsx | 8 + .../port-probe/fixtures/rhExhaustiveDeps.tsx | 12 + .../port-probe/fixtures/rhGating.tsx | 16 + .../port-probe/fixtures/rhGlobals.tsx | 7 + .../port-probe/fixtures/rhImmutability.tsx | 17 + .../fixtures/rhIncompatibleLibrary.tsx | 9 + .../fixtures/rhPreserveManualMemoization.tsx | 21 + .../port-probe/fixtures/rhPurity.tsx | 5 + .../port-probe/fixtures/rhRefs.tsx | 21 + .../fixtures/rhSetStateInEffect.tsx | 10 + .../fixtures/rhSetStateInRender.tsx | 8 + .../fixtures/rhStaticComponents.tsx | 8 + .../fixtures/rhUnsupportedSyntax.tsx | 5 + .../port-probe/fixtures/rhUseMemo.tsx | 8 + .../port-probe/fixtures/sbAwait.stories.tsx | 11 + .../port-probe/fixtures/sbMain.ts | 7 + .../fixtures/sbNoDefault.stories.tsx | 4 + .../fixtures/sbNoStories.stories.tsx | 2 + .../port-probe/fixtures/sbPlay.stories.tsx | 22 + .../fixtures/sbRenderer.stories.tsx | 10 + .../port-probe/fixtures/sbRequireJsdoc.ts | 10 + .../fixtures/sbTitleBatch.stories.tsx | 8 + .../fixtures/testingLibraryDebug.ts | 8 + .../port-probe/fixtures/tlBatchLodash.ts | 8 + .../port-probe/fixtures/tlBatchTest.tsx | 88 ++++ .../port-probe/fixtures/tsBatchA.ts | 274 ++++++++++ .../port-probe/fixtures/tsBatchB.ts | 234 +++++++++ .../port-probe/fixtures/tsconfig.json | 15 + .../port-probe/fixtures/typedSample.ts | 15 + .../port-probe/fixtures/unicornNativeBatch.ts | 57 ++ .../port-probe/fixtures/unicornPreferAt.ts | 19 + .../port-probe/fixtures/unreachableLoop.ts | 9 + .../port-probe/fixtures/ydnlBatch.ts | 154 ++++++ .../port-probe/fixtures/youDontNeedLodash.ts | 4 + .../fragments/a11yNpmBatch.eslint.mjs | 27 + .../fragments/a11yNpmBatch.manifest.json | 74 +++ .../fragments/a11yNpmBatch.oxlint.json | 22 + .../fragments/deprecatedJsx.eslint.mjs | 21 + .../fragments/deprecatedJsx.manifest.json | 8 + .../fragments/deprecatedJsx.oxlint.json | 15 + .../fragments/importNativeBatch.eslint.mjs | 90 ++++ .../fragments/importNativeBatch.manifest.json | 74 +++ .../fragments/importNativeBatch.oxlint.json | 35 ++ .../fragments/jsx11yBatch.eslint.mjs | 114 ++++ .../fragments/jsx11yBatch.manifest.json | 188 +++++++ .../fragments/jsx11yBatch.oxlint.json | 138 +++++ .../port-probe/fragments/oxonly.eslint.mjs | 89 ++++ .../port-probe/fragments/oxonly.manifest.json | 76 +++ .../port-probe/fragments/oxonly.oxlint.json | 59 +++ .../reactNativeRulesBatch.eslint.mjs | 85 +++ .../reactNativeRulesBatch.manifest.json | 199 +++++++ .../reactNativeRulesBatch.oxlint.json | 95 ++++ .../fragments/storybookBatch.eslint.mjs | 66 +++ .../fragments/storybookBatch.manifest.json | 74 +++ .../fragments/storybookBatch.oxlint.json | 66 +++ .../port-probe/fragments/tlBatch.eslint.mjs | 39 ++ .../fragments/tlBatch.manifest.json | 50 ++ .../port-probe/fragments/tlBatch.oxlint.json | 24 + .../port-probe/fragments/tsBatch.eslint.mjs | 76 +++ .../fragments/tsBatch.manifest.json | 334 ++++++++++++ .../port-probe/fragments/tsBatch.oxlint.json | 64 +++ .../fragments/typedSample.eslint.mjs | 21 + .../fragments/typedSample.manifest.json | 8 + .../fragments/typedSample.oxlint.json | 6 + .../fragments/unicornNativeBatch.eslint.mjs | 56 ++ .../unicornNativeBatch.manifest.json | 38 ++ .../fragments/unicornNativeBatch.oxlint.json | 22 + .../port-probe/fragments/ydnlBatch.eslint.mjs | 105 ++++ .../fragments/ydnlBatch.manifest.json | 422 +++++++++++++++ .../fragments/ydnlBatch.oxlint.json | 80 +++ .../port-probe/oxlint.fixtures.json | 222 ++++++++ oxlint-migration/rc-rust-probe.oxlintrc.json | 21 + oxlint-migration/resolveConfigs.mjs | 132 +++++ oxlint-migration/rule-tester/buildTree.mjs | 185 +++++++ .../no-thenable-actions-in-views.test.js | 46 ++ .../rule-tester/compareRuleTester.py | 283 ++++++++++ ...eslint-processor-react-compiler-compat.mjs | 71 +++ .../rule-tester/eslint.ruleTester.config.mjs | 63 +++ oxlint-migration/rule-tester/resolveHook.mjs | 18 + .../rule-tester/ruleTesterStub.mjs | 20 + oxlint-migration/ruleMap.py | 493 ++++++++++++++++++ oxlint.config.mts | 1 + package.json | 14 + scripts/react-compiler-compliance-check.ts | 6 +- 157 files changed, 10829 insertions(+), 4 deletions(-) create mode 100644 eslint-plugin-local-rules/tests/no-direct-personal-details-list.test.js create mode 100644 eslint-plugin-local-rules/tests/no-direct-pre-insert-fullscreen-under-rhp.test.js create mode 100644 eslint-plugin-local-rules/tests/no-layout-spacing-conditional.test.js create mode 100644 eslint-plugin-local-rules/tests/require-a11y-disable-justification.test.js create mode 100644 eslint-plugin-local-rules/tests/require-live-region-for-status-updates.test.js create mode 100644 oxlint-migration/README.md create mode 100644 oxlint-migration/checkConfigDrift.py create mode 100644 oxlint-migration/checkEslintDirectives.mjs create mode 100644 oxlint-migration/checkJsxUsesPort.py create mode 100644 oxlint-migration/checkLocaleComparePort.py create mode 100644 oxlint-migration/checkParityDirection.py create mode 100644 oxlint-migration/checkReactCompilerGate.py create mode 100644 oxlint-migration/checkReactCompilerRust.mjs create mode 100644 oxlint-migration/checkSidecarCoverage.py create mode 100644 oxlint-migration/compareFullRepo.py create mode 100755 oxlint-migration/compareFullRepo.sh create mode 100644 oxlint-migration/compareNativeCtxValues.py create mode 100755 oxlint-migration/compareWarmCache.sh create mode 100644 oxlint-migration/eslint-ctx-values-rule.mjs create mode 100644 oxlint-migration/eslintEnabledRules.mjs create mode 100644 oxlint-migration/listAllRules.py create mode 100644 oxlint-migration/native-vs-sidecar-probe/Counter.tsx create mode 100644 oxlint-migration/native-vs-sidecar-probe/NonAscii.tsx create mode 100644 oxlint-migration/native-vs-sidecar-probe/README.md create mode 100644 oxlint-migration/native-vs-sidecar-probe/TwoComponents.tsx create mode 100644 oxlint-migration/native-vs-sidecar-probe/rhImmutabilityAnchor.tsx create mode 100755 oxlint-migration/parallel-probe/shard-experiment.sh create mode 100644 oxlint-migration/port-probe/compareFixtures.py create mode 100644 oxlint-migration/port-probe/eslint.fixtures.config.mjs create mode 100644 oxlint-migration/port-probe/fixtures.manifest.json create mode 100644 oxlint-migration/port-probe/fixtures/a11yNpmBatch.tsx create mode 100644 oxlint-migration/port-probe/fixtures/coreOctalSyntax.cjs create mode 100644 oxlint-migration/port-probe/fixtures/corePlainJsRules.cjs create mode 100644 oxlint-migration/port-probe/fixtures/coreRestrictedSyntax.tsx create mode 100644 oxlint-migration/port-probe/fixtures/coreStyleRules.ts create mode 100644 oxlint-migration/port-probe/fixtures/deprecatedSelfClosingJsx.tsx create mode 100644 oxlint-migration/port-probe/fixtures/deprecatedSelfClosingJsxCard.tsx create mode 100644 oxlint-migration/port-probe/fixtures/functionComponentDefinition.tsx create mode 100644 oxlint-migration/port-probe/fixtures/importNativeBatch.ts create mode 100644 oxlint-migration/port-probe/fixtures/importNativeBatchAbsolute.ts create mode 100644 oxlint-migration/port-probe/fixtures/importNativeBatchHelper.ts create mode 100644 oxlint-migration/port-probe/fixtures/importNativeBatchJs.js create mode 100644 oxlint-migration/port-probe/fixtures/importOrder.ts create mode 100644 oxlint-migration/port-probe/fixtures/importPathRules.ts create mode 100644 oxlint-migration/port-probe/fixtures/inlineUseOnyxSelector.ts create mode 100644 oxlint-migration/port-probe/fixtures/jsdocNoTypes.ts create mode 100644 oxlint-migration/port-probe/fixtures/jsx11yBatchA.tsx create mode 100644 oxlint-migration/port-probe/fixtures/jsx11yBatchB.tsx create mode 100644 oxlint-migration/port-probe/fixtures/jsxNoBind.tsx create mode 100644 oxlint-migration/port-probe/fixtures/jsxNoConstructedContextValues.tsx create mode 100644 oxlint-migration/port-probe/fixtures/namingConvention.ts create mode 100644 oxlint-migration/port-probe/fixtures/oxonlyAlias.ts create mode 100644 oxlint-migration/port-probe/fixtures/oxonlyCore.ts create mode 100644 oxlint-migration/port-probe/fixtures/oxonlyJsdoc.ts create mode 100644 oxlint-migration/port-probe/fixtures/oxonlyReportName.ts create mode 100644 oxlint-migration/port-probe/fixtures/oxonlyTyped.ts create mode 100644 oxlint-migration/port-probe/fixtures/preferDefaultExport.ts create mode 100644 oxlint-migration/port-probe/fixtures/rawTypography.ts create mode 100644 oxlint-migration/port-probe/fixtures/reactClassComponents.tsx create mode 100644 oxlint-migration/port-probe/fixtures/reactDeprecatedApis.tsx create mode 100644 oxlint-migration/port-probe/fixtures/reactExactProps.tsx create mode 100644 oxlint-migration/port-probe/fixtures/reactNativeA11y.tsx create mode 100644 oxlint-migration/port-probe/fixtures/reactNativeRulesA.tsx create mode 100644 oxlint-migration/port-probe/fixtures/reactNativeRulesB.tsx create mode 100644 oxlint-migration/port-probe/fixtures/reactNativeRulesC.jsx create mode 100644 oxlint-migration/port-probe/fixtures/reactPropTypes.tsx create mode 100644 oxlint-migration/port-probe/fixtures/requireLocaleForLocalizedDateFormat.ts create mode 100644 oxlint-migration/port-probe/fixtures/rhErrorBoundaries.tsx create mode 100644 oxlint-migration/port-probe/fixtures/rhExhaustiveDeps.tsx create mode 100644 oxlint-migration/port-probe/fixtures/rhGating.tsx create mode 100644 oxlint-migration/port-probe/fixtures/rhGlobals.tsx create mode 100644 oxlint-migration/port-probe/fixtures/rhImmutability.tsx create mode 100644 oxlint-migration/port-probe/fixtures/rhIncompatibleLibrary.tsx create mode 100644 oxlint-migration/port-probe/fixtures/rhPreserveManualMemoization.tsx create mode 100644 oxlint-migration/port-probe/fixtures/rhPurity.tsx create mode 100644 oxlint-migration/port-probe/fixtures/rhRefs.tsx create mode 100644 oxlint-migration/port-probe/fixtures/rhSetStateInEffect.tsx create mode 100644 oxlint-migration/port-probe/fixtures/rhSetStateInRender.tsx create mode 100644 oxlint-migration/port-probe/fixtures/rhStaticComponents.tsx create mode 100644 oxlint-migration/port-probe/fixtures/rhUnsupportedSyntax.tsx create mode 100644 oxlint-migration/port-probe/fixtures/rhUseMemo.tsx create mode 100644 oxlint-migration/port-probe/fixtures/sbAwait.stories.tsx create mode 100644 oxlint-migration/port-probe/fixtures/sbMain.ts create mode 100644 oxlint-migration/port-probe/fixtures/sbNoDefault.stories.tsx create mode 100644 oxlint-migration/port-probe/fixtures/sbNoStories.stories.tsx create mode 100644 oxlint-migration/port-probe/fixtures/sbPlay.stories.tsx create mode 100644 oxlint-migration/port-probe/fixtures/sbRenderer.stories.tsx create mode 100644 oxlint-migration/port-probe/fixtures/sbRequireJsdoc.ts create mode 100644 oxlint-migration/port-probe/fixtures/sbTitleBatch.stories.tsx create mode 100644 oxlint-migration/port-probe/fixtures/testingLibraryDebug.ts create mode 100644 oxlint-migration/port-probe/fixtures/tlBatchLodash.ts create mode 100644 oxlint-migration/port-probe/fixtures/tlBatchTest.tsx create mode 100644 oxlint-migration/port-probe/fixtures/tsBatchA.ts create mode 100644 oxlint-migration/port-probe/fixtures/tsBatchB.ts create mode 100644 oxlint-migration/port-probe/fixtures/tsconfig.json create mode 100644 oxlint-migration/port-probe/fixtures/typedSample.ts create mode 100644 oxlint-migration/port-probe/fixtures/unicornNativeBatch.ts create mode 100644 oxlint-migration/port-probe/fixtures/unicornPreferAt.ts create mode 100644 oxlint-migration/port-probe/fixtures/unreachableLoop.ts create mode 100644 oxlint-migration/port-probe/fixtures/ydnlBatch.ts create mode 100644 oxlint-migration/port-probe/fixtures/youDontNeedLodash.ts create mode 100644 oxlint-migration/port-probe/fragments/a11yNpmBatch.eslint.mjs create mode 100644 oxlint-migration/port-probe/fragments/a11yNpmBatch.manifest.json create mode 100644 oxlint-migration/port-probe/fragments/a11yNpmBatch.oxlint.json create mode 100644 oxlint-migration/port-probe/fragments/deprecatedJsx.eslint.mjs create mode 100644 oxlint-migration/port-probe/fragments/deprecatedJsx.manifest.json create mode 100644 oxlint-migration/port-probe/fragments/deprecatedJsx.oxlint.json create mode 100644 oxlint-migration/port-probe/fragments/importNativeBatch.eslint.mjs create mode 100644 oxlint-migration/port-probe/fragments/importNativeBatch.manifest.json create mode 100644 oxlint-migration/port-probe/fragments/importNativeBatch.oxlint.json create mode 100644 oxlint-migration/port-probe/fragments/jsx11yBatch.eslint.mjs create mode 100644 oxlint-migration/port-probe/fragments/jsx11yBatch.manifest.json create mode 100644 oxlint-migration/port-probe/fragments/jsx11yBatch.oxlint.json create mode 100644 oxlint-migration/port-probe/fragments/oxonly.eslint.mjs create mode 100644 oxlint-migration/port-probe/fragments/oxonly.manifest.json create mode 100644 oxlint-migration/port-probe/fragments/oxonly.oxlint.json create mode 100644 oxlint-migration/port-probe/fragments/reactNativeRulesBatch.eslint.mjs create mode 100644 oxlint-migration/port-probe/fragments/reactNativeRulesBatch.manifest.json create mode 100644 oxlint-migration/port-probe/fragments/reactNativeRulesBatch.oxlint.json create mode 100644 oxlint-migration/port-probe/fragments/storybookBatch.eslint.mjs create mode 100644 oxlint-migration/port-probe/fragments/storybookBatch.manifest.json create mode 100644 oxlint-migration/port-probe/fragments/storybookBatch.oxlint.json create mode 100644 oxlint-migration/port-probe/fragments/tlBatch.eslint.mjs create mode 100644 oxlint-migration/port-probe/fragments/tlBatch.manifest.json create mode 100644 oxlint-migration/port-probe/fragments/tlBatch.oxlint.json create mode 100644 oxlint-migration/port-probe/fragments/tsBatch.eslint.mjs create mode 100644 oxlint-migration/port-probe/fragments/tsBatch.manifest.json create mode 100644 oxlint-migration/port-probe/fragments/tsBatch.oxlint.json create mode 100644 oxlint-migration/port-probe/fragments/typedSample.eslint.mjs create mode 100644 oxlint-migration/port-probe/fragments/typedSample.manifest.json create mode 100644 oxlint-migration/port-probe/fragments/typedSample.oxlint.json create mode 100644 oxlint-migration/port-probe/fragments/unicornNativeBatch.eslint.mjs create mode 100644 oxlint-migration/port-probe/fragments/unicornNativeBatch.manifest.json create mode 100644 oxlint-migration/port-probe/fragments/unicornNativeBatch.oxlint.json create mode 100644 oxlint-migration/port-probe/fragments/ydnlBatch.eslint.mjs create mode 100644 oxlint-migration/port-probe/fragments/ydnlBatch.manifest.json create mode 100644 oxlint-migration/port-probe/fragments/ydnlBatch.oxlint.json create mode 100644 oxlint-migration/port-probe/oxlint.fixtures.json create mode 100644 oxlint-migration/rc-rust-probe.oxlintrc.json create mode 100644 oxlint-migration/resolveConfigs.mjs create mode 100644 oxlint-migration/rule-tester/buildTree.mjs create mode 100644 oxlint-migration/rule-tester/cases/no-thenable-actions-in-views.test.js create mode 100644 oxlint-migration/rule-tester/compareRuleTester.py create mode 100644 oxlint-migration/rule-tester/eslint-processor-react-compiler-compat.mjs create mode 100644 oxlint-migration/rule-tester/eslint.ruleTester.config.mjs create mode 100644 oxlint-migration/rule-tester/resolveHook.mjs create mode 100644 oxlint-migration/rule-tester/ruleTesterStub.mjs create mode 100644 oxlint-migration/ruleMap.py diff --git a/.gitignore b/.gitignore index d82481e269f0..1a941c8e55cb 100644 --- a/.gitignore +++ b/.gitignore @@ -186,6 +186,18 @@ server/victory-chart-renderer/.dev/ # Bun lockfiles (VCR uses npm workspaces + package-lock.json; bun is runtime-only) bun.lock +# Oxlint migration investigation: generated rule inventory (npm run oxlint-rule-inventory) +oxlint-migration/rule-inventory.json +oxlint-migration/rule-availability.json + +# Oxlint migration investigation: raw no-deprecated report dumps from the obstacle #4 probe +oxlint-migration/dep-cmp.json +oxlint-migration/dep-only-api.json + +# Python caches from the oxlint-migration comparison scripts +__pycache__/ +oxlint-migration/port-probe/oxlint.fixtures.merged*.json + # Oxlint leg configs derived from oxlint.config.mts for one sharded run (scripts/lint/oxlint/OxlintLinter.ts) .oxlintrc.js-plugins.*.json .oxlintrc.type-aware.*.json diff --git a/.oxfmtrc.json b/.oxfmtrc.json index edc832fe2b5e..eff628de2ad5 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -72,7 +72,8 @@ "src/libs/SearchParser/searchParser.js", "src/libs/SearchParser/autocompleteParser.js", "Mobile-Expensify", - "modules/group-ib-fp" + "modules/group-ib-fp", + "oxlint-migration/port-probe/fixtures" ], "overrides": [ { diff --git a/config/eslint/eslint.config.mjs b/config/eslint/eslint.config.mjs index 50387071594e..99080a080fba 100644 --- a/config/eslint/eslint.config.mjs +++ b/config/eslint/eslint.config.mjs @@ -824,6 +824,12 @@ const config = defineConfig([ globalIgnores([ '!**/.storybook', '!**/.github', + // Oxlint migration harness, deleted with ESLint (see oxlint-migration/README.md). + // config/oxlint/ is deliberately NOT ignored: it is production lint config, not an artifact. + 'oxlint-migration/**', + // Conductor scratch directory, gitignored. Oxlint already skips it, so ESLint has to as + // well or the two legs lint different file sets. + '.context/**', // Oxlint's own config. Its rule names are quoted object keys, which naming-convention flags by the // hundred; `**/*.config.mjs` below is ignored for the same kind of reason. 'oxlint.config.mts', diff --git a/config/oxlint/reactCompilerRust.mjs b/config/oxlint/reactCompilerRust.mjs index 8fe042c6bfa4..6bfd76f1c114 100644 --- a/config/oxlint/reactCompilerRust.mjs +++ b/config/oxlint/reactCompilerRust.mjs @@ -233,4 +233,4 @@ function reactCompilerDiagnostics(filename, sourceText) { return cache.get(filename); } -export {RULE_BY_CATEGORY, reactCompilerDiagnostics}; +export {CONFIG_CATEGORY, IGNORED_CATEGORIES, RULE_BY_CATEGORY, reactCompilerDiagnostics}; diff --git a/cspell.json b/cspell.json index 496f73e485ff..89a6a0551537 100644 --- a/cspell.json +++ b/cspell.json @@ -140,12 +140,14 @@ "Ephermeral", "Erste", "Español", + "codemod", "detekt", "oxfmtrc", "oxlint", "oxlintrc", "tsgo", "tsgolint", + "ungated", "Fabrikam", "fillyjonk", "hemulen", @@ -1294,6 +1296,7 @@ "tests/unit/usePersonalDetailSearchSelectorTest.tsx", "config/eslint/eslint.seatbelt.tsv", "config/oxlint/oxlint.seatbelt.tsv", + "INVESTIGATION DOCS/**", "oxlint.config.mts", "tests/unit/BankAccountUtilsTest.ts", "tests/unit/__snapshots__/ReportAlternateTextParityTest.ts.snap", diff --git a/eslint-plugin-local-rules/tests/no-direct-personal-details-list.test.js b/eslint-plugin-local-rules/tests/no-direct-personal-details-list.test.js new file mode 100644 index 000000000000..4a9dff9c73be --- /dev/null +++ b/eslint-plugin-local-rules/tests/no-direct-personal-details-list.test.js @@ -0,0 +1,44 @@ +import {RuleTester} from 'eslint'; + +// eslint-disable-next-line import/extensions -- Node resolves this file directly when the oxlint-migration harness imports it, so the extension is required +import {create, meta} from '../no-direct-personal-details-list.js'; + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, +}); + +// Mirrors tests/unit/NoDirectPersonalDetailsListRuleTest.ts. oxlint-migration/rule-tester harvests +// this file and replays the cases through both tools, so the rule is proven to report on oxlint's +// bridged AST, not just on ESLint's. + +ruleTester.run( + 'no-direct-personal-details-list', + {create, meta}, + { + valid: [ + 'const [personalDetail] = usePersonalDetail(accountID);', + 'const [personalDetails] = usePersonalDetailsByIDs(accountIDs);', + 'const personalDetail = getPersonalDetail(accountID);', + 'type Update = OnyxUpdate;', + 'type Data = Partial>;', + 'const [metadata] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_METADATA);', + 'const [list] = useOnyx(ONYXKEYS.COLLECTION.REPORT);', + 'const key = SOME_OTHER_KEYS.PERSONAL_DETAILS_LIST;', + 'const key = ONYXKEYS[PERSONAL_DETAILS_LIST];', + ], + invalid: [ + {code: 'const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);', errors: 1}, + {code: 'const [login] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: personalDetailsLoginSelector(accountID)});', errors: 1}, + {code: "const [personalDetails] = useOnyx(ONYXKEYS['PERSONAL_DETAILS_LIST']);", errors: 1}, + {code: 'const [personalDetails] = useOnyx(ONYXKEYS[`PERSONAL_DETAILS_LIST`]);', errors: 1}, + {code: 'Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[accountID]: null});', errors: 1}, + {code: 'const update = {onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, value: personalDetails};', errors: 1}, + {code: 'Onyx.connectWithoutView({key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (value) => value});', errors: 1}, + {code: 'const masks = {[ONYXKEYS.PERSONAL_DETAILS_LIST]: {allowList: []}};', errors: 1}, + {code: 'const [all] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {});', errors: 2}, + ], + }, +); diff --git a/eslint-plugin-local-rules/tests/no-direct-pre-insert-fullscreen-under-rhp.test.js b/eslint-plugin-local-rules/tests/no-direct-pre-insert-fullscreen-under-rhp.test.js new file mode 100644 index 000000000000..f3a69c9dba6e --- /dev/null +++ b/eslint-plugin-local-rules/tests/no-direct-pre-insert-fullscreen-under-rhp.test.js @@ -0,0 +1,60 @@ +import {RuleTester} from 'eslint'; + +// eslint-disable-next-line import/extensions -- Node resolves this file directly when the oxlint-migration harness imports it, so the extension is required +import {create, meta} from '../no-direct-pre-insert-fullscreen-under-rhp.js'; + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, +}); + +const importNavigation = "import Navigation from '@libs/Navigation/Navigation';"; + +ruleTester.run( + 'no-direct-pre-insert-fullscreen-under-rhp', + {create, meta}, + { + valid: [ + { + code: `${importNavigation}\nNavigation.preInsertFullscreenUnderRHP(route);`, + filename: 'src/hooks/usePreMountDestination/index.ts', + }, + { + code: `${importNavigation}\nNavigation.preInsertFullscreenUnderRHP(route);`, + filename: 'src/libs/Navigation/Navigation.ts', + }, + {code: 'const Navigation = {preInsertFullscreenUnderRHP: () => {}};\nNavigation.preInsertFullscreenUnderRHP(route);'}, + {code: `${importNavigation}\nNavigation.navigate(route);`}, + {code: `${importNavigation}\nconst preInsert = Navigation.preInsertFullscreenUnderRHP;`}, + ], + invalid: [ + { + code: `${importNavigation}\nNavigation.preInsertFullscreenUnderRHP(route);`, + errors: [{messageId: 'useHookInstead'}], + }, + { + code: `${importNavigation}\nNavigation['preInsertFullscreenUnderRHP'](route);`, + errors: [{messageId: 'useHookInstead'}], + }, + { + code: `${importNavigation}\nconst {preInsertFullscreenUnderRHP} = Navigation;\npreInsertFullscreenUnderRHP(route);`, + errors: [{messageId: 'useHookInstead'}], + }, + { + code: `${importNavigation}\nconst preInsert = Navigation.preInsertFullscreenUnderRHP;\npreInsert(route);`, + errors: [{messageId: 'useHookInstead'}], + }, + { + code: "import * as Navigation from '@src/libs/Navigation/Navigation';\nNavigation.preInsertFullscreenUnderRHP(route);", + errors: [{messageId: 'useHookInstead'}], + }, + { + code: "import Navigation from '../../libs/Navigation/Navigation';\nNavigation.preInsertFullscreenUnderRHP(route);", + errors: [{messageId: 'useHookInstead'}], + filename: 'src/pages/home/ReportScreen.tsx', + }, + ], + }, +); diff --git a/eslint-plugin-local-rules/tests/no-layout-spacing-conditional.test.js b/eslint-plugin-local-rules/tests/no-layout-spacing-conditional.test.js new file mode 100644 index 000000000000..fe13b27643fd --- /dev/null +++ b/eslint-plugin-local-rules/tests/no-layout-spacing-conditional.test.js @@ -0,0 +1,93 @@ +import {RuleTester} from 'eslint'; + +// eslint-disable-next-line import/extensions -- Node resolves this file directly when the oxlint-migration harness imports it, so the extension is required +import {create, meta} from '../no-layout-spacing-conditional.js'; + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + parserOptions: {ecmaFeatures: {jsx: true}}, + }, +}); + +ruleTester.run( + 'no-layout-spacing-conditional', + {create, meta}, + { + valid: [ + 'const {cardPaddingHorizontal} = useLayoutSpacing(); const style = [styles.flexRow, cardPaddingHorizontal];', + 'const style = shouldUseNarrowLayout ? styles.flexColumn : styles.flexRow;', + 'const style = shouldUseNarrowLayout ? styles.alignItemsCenter : styles.alignItemsStart;', + 'const style = isSelected ? styles.ph5 : styles.ph8;', + 'const style = shouldUseNarrowLayout ? {marginBottom: variables.bottomTabHeight} : undefined;', + 'const jsx = ;', + 'const style = styles.ph5;', + 'const style = shouldUseNarrowLayout ? styles.pb2 : styles.pb5;', + 'const style = shouldUseNarrowLayout ? styles.mt3 : styles.mt5;', + 'const style = shouldUseNarrowLayout ? styles.gap3 : styles.gap5;', + 'const style = shouldUseNarrowLayout ? styles.ph3 : styles.ph2;', + 'const style = shouldUseNarrowLayout ? styles.mh5 : styles.mh2;', + 'const style = isLargeScreenWidth ? styles.mr3 : styles.mr2;', + 'const style = shouldUseNarrowLayout ? styles.p3 : styles.p4;', + 'const style = shouldUseNarrowLayout ? styles.ph5 : styles.mh8;', + 'const style = shouldUseNarrowLayout ? styles.ph5 : undefined;', + 'const style = isLargeScreenWidth ? styles.ph5 : styles.ph8;', + 'const style = isMediumScreenWidth ? styles.ph5 : styles.ph8;', + ], + invalid: [ + { + code: 'const style = shouldUseNarrowLayout ? styles.ph5 : styles.ph8;', + errors: [{messageId: 'layoutSpacingConditionalKnown', data: {test: 'shouldUseNarrowLayout', first: 'ph5', second: 'ph8', replacement: 'cardPaddingHorizontal'}}], + }, + { + code: 'const style = isSmallScreenWidth ? styles.p5 : styles.p8;', + errors: [{messageId: 'layoutSpacingConditionalKnown', data: {test: 'isSmallScreenWidth', first: 'p5', second: 'p8', replacement: 'cardPadding'}}], + }, + { + code: 'const style = shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8;', + errors: [{messageId: 'layoutSpacingConditionalKnown', data: {test: 'shouldUseNarrowLayout', first: 'mhn5', second: 'mhn8', replacement: 'cardEdgeToEdge'}}], + }, + { + code: 'const style = shouldUseNarrowLayout ? [styles.ph5, styles.pb5] : [styles.ph8, styles.pb8];', + errors: [{messageId: 'layoutSpacingConditionalKnown'}], + }, + { + code: 'const style = shouldUseNarrowLayout ? styles.ph5 : [styles.ph8, styles.pt3];', + errors: [{messageId: 'layoutSpacingConditionalKnown'}], + }, + { + code: 'const style = !shouldUseNarrowLayout ? styles.ph8 : styles.ph5;', + errors: [{messageId: 'layoutSpacingConditionalKnown', data: {test: '!shouldUseNarrowLayout', first: 'ph8', second: 'ph5', replacement: 'cardPaddingHorizontal'}}], + }, + { + code: 'const style = isLargeScreenWidth ? styles.ph8 : styles.ph5;', + errors: [{messageId: 'layoutSpacingConditionalKnown', data: {test: 'isLargeScreenWidth', first: 'ph8', second: 'ph5', replacement: 'cardPaddingHorizontal'}}], + }, + { + code: 'const style = layout.shouldUseNarrowLayout ? styles.ph5 : styles.ph8;', + errors: [{messageId: 'layoutSpacingConditionalKnown'}], + }, + { + code: 'const jsx = ;', + errors: [{messageId: 'layoutSpacingConditionalKnown'}], + }, + { + code: 'const style = shouldUseNarrowLayout ? styles.pl5 : styles.pl8;', + errors: [{messageId: 'layoutSpacingConditionalKnown', data: {test: 'shouldUseNarrowLayout', first: 'pl5', second: 'pl8', replacement: 'cardPaddingLeft'}}], + }, + { + code: 'const style = shouldUseNarrowLayout ? styles.mh5 : styles.mh8;', + errors: [{messageId: 'layoutSpacingConditional', data: {test: 'shouldUseNarrowLayout', first: 'mh5', second: 'mh8'}}], + }, + { + code: 'const style = isLargeScreenWidth ? styles.mr8 : styles.mr5;', + errors: [{messageId: 'layoutSpacingConditional'}], + }, + { + code: 'const padding = shouldUseNarrowLayout ? styles.ph5.paddingHorizontal : styles.ph8.paddingHorizontal;', + errors: [{messageId: 'layoutSpacingConditionalKnown'}], + }, + ], + }, +); diff --git a/eslint-plugin-local-rules/tests/require-a11y-disable-justification.test.js b/eslint-plugin-local-rules/tests/require-a11y-disable-justification.test.js new file mode 100644 index 000000000000..081d54eab2ba --- /dev/null +++ b/eslint-plugin-local-rules/tests/require-a11y-disable-justification.test.js @@ -0,0 +1,54 @@ +import {RuleTester} from 'eslint'; + +// eslint-disable-next-line import/extensions -- Node resolves this file directly when the oxlint-migration harness imports it, so the extension is required +import {create, meta} from '../require-a11y-disable-justification.js'; + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + parserOptions: {ecmaFeatures: {jsx: true}}, + }, +}); + +const ISSUE = 'https://github.com/Expensify/App/issues/12345'; + +ruleTester.run( + 'require-a11y-disable-justification', + {create, meta}, + { + valid: [ + {code: `// eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors -- ${ISSUE}\nconst element = ;`}, + { + code: '// eslint-disable-next-line react-native-a11y/has-valid-accessibility-ignores-invalid-hints -- https://github.com/FormidableLabs/eslint-plugin-react-native-a11y/pull/78\nconst element = ;', + }, + { + code: '// eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors -- decorative wrapper, the child owns the label\nconst element = ;', + }, + { + code: '/* eslint-disable react-native-a11y/has-valid-accessibility-descriptors\n * the row below is announced by its parent list item\n */\nconst element = ;', + }, + {code: '// eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors -- https://example.com/1\nconst element = ;'}, + {code: '// eslint-disable-next-line no-console\nconsole.log(1);'}, + {code: 'const element = ;'}, + ], + invalid: [ + { + code: '// eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors\nconst element = ;', + errors: [{messageId: 'missingIssueOrRationale'}], + }, + { + code: '// eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors -- legacy\nconst element = ;', + errors: [{messageId: 'missingIssueOrRationale'}], + }, + { + code: '/* eslint-disable react-native-a11y/has-valid-accessibility-ignores-invalid-hints */\nconst element = ;', + errors: [{messageId: 'missingIssueOrRationale'}], + }, + { + code: '// eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors\nconst first = ;\n// eslint-disable-next-line react-native-a11y/has-valid-accessibility-ignores-invalid-hints\nconst second = ;', + errors: [{messageId: 'missingIssueOrRationale'}, {messageId: 'missingIssueOrRationale'}], + }, + ], + }, +); diff --git a/eslint-plugin-local-rules/tests/require-live-region-for-status-updates.test.js b/eslint-plugin-local-rules/tests/require-live-region-for-status-updates.test.js new file mode 100644 index 000000000000..1b08c3b2fad0 --- /dev/null +++ b/eslint-plugin-local-rules/tests/require-live-region-for-status-updates.test.js @@ -0,0 +1,53 @@ +import {RuleTester} from 'eslint'; + +// eslint-disable-next-line import/extensions -- Node resolves this file directly when the oxlint-migration harness imports it, so the extension is required +import {create, meta} from '../require-live-region-for-status-updates.js'; + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + parserOptions: {ecmaFeatures: {jsx: true}}, + }, +}); + +ruleTester.run( + 'require-live-region-for-status-updates', + {create, meta}, + { + valid: [ + {code: 'const element = ;'}, + {code: 'const element = ;'}, + {code: 'const element = ;'}, + {code: 'const element = ;'}, + {code: 'const element = ;'}, + {code: 'const element = ;'}, + ], + invalid: [ + { + code: 'const element = ;', + errors: [{messageId: 'missingLiveRegion'}], + }, + { + code: 'const element = ;', + errors: [{messageId: 'missingLiveRegion'}], + }, + { + code: 'const element = ;', + errors: [{messageId: 'missingLiveRegion'}], + }, + { + code: 'const element = ;', + errors: [{messageId: 'missingStatusRole'}], + }, + { + code: 'const element = ;', + errors: [{messageId: 'missingStatusRole'}], + }, + { + code: 'const element = ;', + errors: [{messageId: 'invalidLiveRegion'}], + }, + ], + }, +); diff --git a/knip.json b/knip.json index 986a13935cf6..042bac09a277 100644 --- a/knip.json +++ b/knip.json @@ -22,6 +22,7 @@ "metro.config.js", "config/babel/oxcJestTransformer.js", "eslint.changed.config.mjs", + "oxlint-migration/rule-tester/buildTree.mjs", "react-native.config.js", "rock.config.mjs", "rspack.config.mjs", @@ -93,7 +94,7 @@ "diff-so-fancy", "@babel/plugin-proposal-class-properties" ], - "ignoreBinaries": ["metro-symbolicate", "mkcert", "openssl"], + "ignoreBinaries": ["metro-symbolicate", "mkcert", "openssl", "python3"], "ignoreIssues": { "config/oxlint/ruleNames.mjs": ["exports"] } diff --git a/oxlint-migration/README.md b/oxlint-migration/README.md new file mode 100644 index 000000000000..9675e5468139 --- /dev/null +++ b/oxlint-migration/README.md @@ -0,0 +1,125 @@ +# oxlint-migration + +**Temporary. This whole directory is deleted when ESLint is removed.** + +Everything here exists to answer one question: does Oxlint report the same thing ESLint reports? Every +script in here runs both linters and compares them, so none of it has a job once there is only one +linter left. Nothing in the app, in CI's production lint gate, or in `.oxlintrc.json` depends on this +directory. Deleting it is a `git rm -r` plus removing the `oxlint-*` scripts from `package.json`. + +One thing to know before reading any of it: "what ESLint reports" is no longer `npx eslint`. The +React Compiler suppression and the `@typescript-eslint/no-deprecated` stratification used to be +ESLint processors wired into `config/eslint/eslint.config.mjs`; they are now stages in +`scripts/lint/`, which runs them over ESLint's output rather than inside it. Scripts whose rule of +interest one of those stages filters run the repo's own gate, +`bun scripts/lint/index.ts --linter=eslint --format=json`, rather than reimplementing the stages or +reapplying them to a raw report. The rest read raw ESLint and say so at the call site. + +The same move retired this migration's first blocker. The seatbelt baseline used to be a pseudo-rule +contributed by the `eslint-seatbelt` plugin, which only ESLint could run, so switching to Oxlint +needed a baseline mechanism built from scratch. Expensify/App#99259 dropped the dependency for +`scripts/lint/processors/Seatbelt.ts`, a stage over the normalized `LintMessage[]` in +`scripts/lint/types.ts`. It reads no ESLint type, and `scripts/lint/Linter.ts` is already a port with +`ESLintLinter` as one implementation, so the baseline follows whichever linter the pipeline is handed. + +What is *not* here, on purpose: + +| lives in | what | +| --- | --- | +| `.oxlintrc.json` (repo root) | the production Oxlint config | +| `config/oxlint/plugins/` | the four jsPlugin modules the config loads (`core`, `hosted`, `rulesdir`, `rh`) | +| `config/oxlint/reactCompilerGate.mjs` | the React Compiler gate those plugins wrap rules in | +| `config/oxlint/preferLocaleCompareFromContext.mjs` | the type-free rewrite of one custom rule | + +Those are production lint config, they mirror `config/eslint/`, and they stay after this directory is +gone. See `contributingGuides/LINTING.md`. + +## Why `.oxlintrc.json` is at the repo root and not in `config/oxlint/` + +ESLint keeps a one-line re-export at the root and the real config in `config/eslint/`. Oxlint cannot +do the same. Its `extends` resolves *every* relative path against the directory of the file that +declares it, including `ignorePatterns` and `overrides.files`. Measured 2026-08-13: moving the real +config to `config/oxlint/oxlintrc.json` behind a root `extends` stub raised the linted file count from +8218 to 8958, because repo-root-relative ignores such as `web/gtm.js` and `docs/vendor/**` silently +stopped matching. The root file is therefore the real config, and that is deliberate. + +## What is in here + +| path | what it does | run with | +| --- | --- | --- | +| `compareFullRepo.{sh,py}` | whole-repo, finding-by-finding parity between the two tools | `npm run compare-oxlint` | +| `checkParityDirection.py` | splits that report by direction: findings oxlint adds are safe, findings it hides are checks that stop running when the gate flips | `npm run oxlint-parity-direction` | +| `port-probe/` | one fixture per rule that deliberately violates it, so a rule with no findings in this repo is still proven to run | `npm run oxlint-rule-fixtures` | +| `rule-tester/` | harvests the upstream `RuleTester` cases for the custom rules and replays them as real files through both tools | `npm run oxlint-rule-tester` | +| `checkSidecarCoverage.py` | fails if any hand-hosted sidecar rule has no fixture, replayed case or probe | `npm run oxlint-sidecar-coverage` | +| `checkReactCompilerGate.py` | asserts the gate suppresses exactly what the ESLint side suppresses | `npm run oxlint-react-compiler-gate` | +| `checkLocaleComparePort.py` | asserts the type-free rule rewrite matches the type-aware original, receiver shape by receiver shape | `npm run oxlint-locale-compare-port` | +| `checkJsxUsesPort.py` | the two rules that cannot report anything, asserted by outcome instead | `npm run oxlint-jsx-uses-port` | +| `listAllRules.py` | inventory of every rule either tool enables, and why anything is off | `npm run oxlint-rule-inventory`, `npm run oxlint-rule-availability` | +| `ruleMap.py` | the shared rule-id map and `PORT_PLAN`; imported by most of the above | library | +| `compareNativeCtxValues.py`, `eslint-ctx-values-rule.mjs` | reproduction for the two upstream bugs in Oxlint's native `react/jsx-no-constructed-context-values` (wrong anchor line, no component-scope check) | on demand, until both are filed | + +## The React Compiler rules report partially, on purpose + +All twelve `rc/*` rules are on. Some of them report less (or more) than ESLint does, and they are +left on anyway: a rule whose count differs shows up as a number in `npm run compare-oxlint`, while a +rule switched off reads exactly like a clean codebase. + +Measured whole-repo on `oxc-transform-react` 0.152.0 and oxlint 1.86.0: + +| rule | ESLint | oxlint | | +| --- | --- | --- | --- | +| `set-state-in-effect` | 120 | 120 | exact, same locations | +| `refs` | 210 | 189 | bridge collapses ESLint's duplicate reports (see below) | +| `purity` | 0 | 35 | all `Date`-during-render; ESLint's compiler build misses them, kept on purpose | +| `preserve-manual-memoization` | 4 | 54 | over-reports through the bridge | +| `immutability` | 6 | 7 | one extra | +| `globals` | 0 | 1 | one extra | +| `static-components` | 2 | 2 | exact | +| the other five | 0 | 0 | latent in this repo | + +`config/oxlint/reactCompilerRust.mjs` passes `reportDiagnostics: true` to `transformSync`, which is +what lets any of them report the non-fatal categories at all. `oxc-transform-react` 0.148.0 had +narrowed `result.errors` to *fatal* React Compiler diagnostics (oxc-project/oxc#26128, tracked as +#26318), so on the default a component whose only problem is a non-blocking validation like +`set-state-in-effect` returned nothing; the old `panicThreshold: "all_errors"` workaround made every +diagnostic fatal to recover them, at the cost of aborting on the first failing function per file. +`oxc-project/oxc#26624`, shipped in 0.152.0, added the opt-in `reportDiagnostics` that returns the +recoverable diagnostics without forcing them fatal, so the bridge now sees every finding at full +speed. `checkReactCompilerRust.mjs` pins this: `TwoComponents.tsx` reports all its findings in one +pass, as ESLint does, and each of the twelve fixtures reports on its own. + +Why the bridge and not oxlint's native `react/*` compiler rules: oxc-project/oxc#26277, a +`disable-next-line` naming `react/exhaustive-deps` or `react/rules-of-hooks` silences every native +compiler diagnostic in the enclosing component, and the native rules do not expose the compiler's +`eslintSuppressionRules` option to turn that off. This config does not use the native `react/*` rules +for the compiler checks precisely because of that; `config/oxlint/reactCompilerRust.mjs` sets +`eslintSuppressionRules: []` to defeat it on the bridge. #26277 is open with no PR. + +## Files that are records, not inputs + +`migration-details.log` (what `npx @oxlint/migrate` skipped and why) and `dep-override-files.json` +(the write-only deprecation file list a codemod emitted into the config) are kept as provenance for +steps that are already applied. Nothing reads them. They die with this directory. + +Generated reports are gitignored, not committed: `rule-inventory.json`, `rule-availability.json`, +`dep-cmp.json`, `dep-only-api.json`. Regenerate with the scripts above rather than restoring them. + +## The plan documents + +`OXLINT_MIGRATION_INVESTIGATION.md`, `OXLINT_MIGRATION_TLDR.md`, `OXLINT_SIDE_BY_SIDE_PLAN.md`, +`OXLINT_MIGRATION_STEPS.md` and `OXLINT_RULE_TEST_SUITE_PLAN.md` at the repo root carry the +measurements, the decisions and the remaining checklist. Start with `OXLINT_MIGRATION_STEPS.md` if +what you want is the order of work. + +## Deleted 2026-08-25 + +Nine files whose subject no longer exists: `naming-codemod.py` and `wire-codemod.py` (the directive +wrapper removed the need for twin suppression comments, so there is nothing to codemod), +`compareLintResults.ts` (superseded by `compareFullRepo.py`), `compareReactCompiler.mjs` (verified +Oxlint's native `react/*` per-check rules, which `rc/*` replaced), `reactCompilerVariants.py`, +`compareReactCompilerNative.py` and `measureReactCompilerCost.sh` (they measured a sidecar-versus-native +trade whose two subjects are both gone: `rh/` was deleted and 1.79.0 split the aggregate rule), and +`native-vs-sidecar-probe/{native,sidecar}.oxlintrc.json`. None was wired into `package.json`, so no +command changed. The three `.tsx` fixtures in `native-vs-sidecar-probe/` stay: they are live inputs to +`npm run oxlint-react-compiler-rust`. diff --git a/oxlint-migration/checkConfigDrift.py b/oxlint-migration/checkConfigDrift.py new file mode 100644 index 000000000000..53b96027bfd2 --- /dev/null +++ b/oxlint-migration/checkConfigDrift.py @@ -0,0 +1,217 @@ +"""Does oxlint.config.mts still say the same thing as ESLint's config? + + python3 oxlint-migration/checkConfigDrift.py [--json ] [--files ...] + +The mirrored config is maintained by hand, so it drifts silently. The usual way it drifts is not +somebody editing it: it is `eslint-config-expensify` being bumped. That package owns 328 of the 437 +rules ESLint enables here, and a bump can change a severity or an option with no diff anywhere in this +repo. This is the check that catches it, and the moment to run it is the bump. + +How it works: `resolveConfigs.mjs` resolves both configs for the same file from *authored* values, then +this script compares them rule by rule. Every difference has to be in LEDGER below, with a reason. A +difference that is not there fails the run; a LEDGER entry that no longer differs also fails, because a +stale exemption is worse than none. + +Deliberately not `eslint --print-config`: it fills in each rule's schema defaults, so `import/order` +comes back carrying three options nothing authored, and comparing that against a hand-written mirror +reports drift on every rule that has defaults. + +Three statuses, and the difference between them matters: + + equivalent the two configs are spelled differently and behave the same. The reason has to say why, + against the rule's own default, not from memory. + accepted a real difference this repo chose, with the reason it chose it. + open a real difference nobody chose. Printed loudly, does not fail the run, because these + predate the check. Each one needs an owner and then a fix or an `accepted`. +""" + +import argparse +import collections +import json +import os +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +sys.path.insert(0, HERE) +from ruleMap import REPRESENTATIVE_FILES, TS_EXTENSION_RULES, norm_ox_config + +EQUIVALENT, ACCEPTED, OPEN = 'equivalent', 'accepted', 'open' + +LEDGER = { + 'curly': (EQUIVALENT, 'oxlint writes ["error", "all"]; "all" is what curly does with no option (eslint/lib/rules/curly.js only branches on "multi", "multi-line", "multi-or-nest")'), + 'no-undef': (EQUIVALENT, 'oxlint writes {typeof: false}, which is defaultOptions in eslint/lib/rules/no-undef.js:31-35'), + 'dot-notation': (EQUIVALENT, 'oxlint writes {allowKeywords: true, allowPattern: ""}, both defaults in eslint/lib/rules/dot-notation.js:30-31'), + 'import/order': (EQUIVALENT, 'oxlint writes distinctGroup, sortTypesGroup, named and warnOnUnassignedImports; all four are `default` in eslint-plugin-import\'s order schema (true, false, false, false)'), + 'jsx-a11y/anchor-has-content': (EQUIVALENT, 'ESLint writes {components: []}; the rule reads `options.components || []` (eslint-plugin-jsx-a11y/lib/rules/anchor-has-content.js:38), so omitting it is the same'), + 'react/jsx-no-duplicate-props': (ACCEPTED, 'ESLint passes {ignoreCase: true}; oxlint\'s native port accepts no options at all and fails the config with "this rule does not accept configuration options", so the option cannot be mirrored. 0 findings on either tool today'), + 'react/jsx-no-undef': (ACCEPTED, 'same: ESLint passes {allowGlobals: true}, oxlint\'s port accepts no options. 0 findings on either tool today'), + 'import/no-cycle': (ACCEPTED, 'ESLint passes {maxDepth: "\u221e"}; oxlint rejects a string there ("invalid type: string, expected u32") and its default already behaves the same way on this repo, measured 748 = 748 over src with the option omitted and with maxDepth at u32::MAX. Enabled on both, and oxlint is the only one that reports: 534 real cycles against ESLint\'s 0'), + 'rulesdir/prefer-at': (ACCEPTED, 'needs typeChecker.isArrayType to tell arrays from records, which a jsPlugin cannot reach. unicorn/prefer-at at default options is enabled instead, and covers the type-free half'), + 'unicorn/prefer-at': (ACCEPTED, 'oxlint-only on purpose: the type-free stand-in for rulesdir/prefer-at, which cannot be ported. Default options only, so it covers the x[x.length - N] family and not plain arr[i]. Measured 2026-09-10: 2 findings, the same 2 lines ESLint\'s own copy of the rule reports'), + 'hosted/no-deprecated-self-closing-jsx': (ACCEPTED, 'oxlint-only on purpose: a stand-in for the half of @typescript-eslint/no-deprecated tsgolint misses. tsgolint checks a JSX tag\'s resolved signature for but not for self-closing (isNodeCalleeOfParent handles KindJsxOpeningElement only), so MenuItem, deprecated only through the component it wraps with Object.assign, went unreported. It reports under @typescript-eslint/no-deprecated and is scoped exactly like typescript/no-deprecated. Measured 2026-10-14: closes 120 of the 120 MenuItem findings ESLint reports and oxlint hid, at the same (file, line, column)'), + 'rulesdir/boolean-conditional-rendering': (ACCEPTED, 'needs the type of the && left operand, and no syntactic stand-in exists'), + '@typescript-eslint/no-deprecated': (ACCEPTED, 'off for the 91 files in the write-site override. tsgolint reports writes to deprecated properties that typescript-eslint misses (typescript-eslint#10643), and no option separates reads from writes. Measured 2026-09-15 over the full-repo reports, matching on (file, line, column): 225 shared, 174 oxlint-only, 6 ESLint-only. All 174 oxlint-only are write sites -- 164 explicit object-literal properties and 10 shorthand ones, and 172 of the 174 are the single deprecated property originalMessage. All 6 ESLint-only are read sites, calls to the deprecated getReportTransactions (src/libs/ReportUtils.ts:1292-1296), every one of them inside this override: the block that holds back the write-site flood takes the legitimate read findings with it. That is the price of the workaround, 6 lost read-site findings across 91 files'), + 'no-restricted-imports': (ACCEPTED, 'two differences, both about where a ban lives rather than whether it applies. (1) The react-native-onyx/dist/OnyxUtils ban: ESLint keeps it under @typescript-eslint/no-restricted-imports (config/eslint/eslint.config.mjs:727), oxlint has no typescript/no-restricted-imports, and its core rule accepts allowTypeImports, so the path lives there instead. Measured 2026-09-14: 13 findings on both tools, same 13 files, same 13 lines. (2) The HeaderWithBackButton ban (config/eslint/eslint.config.mjs:210) needs two patterns in oxlint where ESLint writes one: ESLint matches with gitignore semantics, so a bare directory name also covers everything below it, and oxlint matches the path exactly. Measured over the full-repo reports on (file, line): 502 findings on both, same 502 locations. Neither ban is visible to this probe -- none of its 46 files imports either path -- so both are verified against compare-oxlint, not here'), + 'no-invalid-this': (ACCEPTED, 'oxlint\'s plugin bridge throws on sourceCode.getJSDocComment, so the rule cannot run there. TS files are largely covered by noImplicitThis'), + 'progress/activate': (ACCEPTED, 'progress-bar plugin; oxlint prints its own progress. No behavior to preserve'), + 'react-hooks/config': (ACCEPTED, 'not a compiler-category rule and not ported. Left behind when the rh/ sidecar was deleted'), + 'react-hooks/gating': (ACCEPTED, 'same'), + 'react-hooks/component-hook-factories': (ACCEPTED, 'ships as a deprecated stub upstream: create() returns {}, so it cannot report on either tool'), + 'react/jsx-filename-extension': (ACCEPTED, 'off in oxlint, warn with [.js, .jsx, .tsx] at ESLint\'s root. Dead config on both: the JS override turns it off for .js/.jsx/.mjs/.cjs, leaving .ts/.mts/.cts as the only live scope, and JSX does not parse there (parse error, not a finding). 0 findings on either tool, no seatbelt row'), + # Found 2026-09-10, the first run after the probe set widened from 8 representative files to every + # rule-carrying block. None of these was visible before, because no representative file was a + # story file, an `.github/actions` source, a `src/types/onyx` type or a `.d.ts`. + 'no-unreachable-loop': (ACCEPTED, 'off for **/*.d.ts on the oxlint side only. ESLint\'s code-path analyzer, which this rule needs, crashes on a `declare module` with no body when fed through oxlint\'s AST bridge (TypeError at CodePathAnalyzer.enterNode). Declaration files contain no loops, so nothing is lost'), + 'prefer-const': (EQUIVALENT, 'not a difference: ESLint authors the identical options in a preset. eslint-config-expensify/configs/private/es6.js:93-96 sets ["error", {destructuring: "any", ignoreReadBeforeAssign: true}], and `eslint --print-config` confirms that is what resolves. resolveConfigs.mjs reads the flat array\'s literal `rules` and never expands an `extends:` key or a legacy preset object, so preset-authored options read as absent'), + 'no-redeclare': (ACCEPTED, 'different rule doing the same job. ESLint turns the core rule off and runs @typescript-eslint/no-redeclare instead, which ignores TypeScript declaration merging (ignoreDeclarationMerge defaults true). oxlint has no typescript/no-redeclare at all, so it runs the core rule with builtinGlobals false, the only lever that stops it firing on TS global augmentation. Measured 2026-09-15: restoring builtinGlobals true adds exactly 2 findings, src/types/global.d.ts:69 and src/types/expo.d.ts:2, both global augmentation that ESLint reports 0 of'), + 'prefer-promise-reject-errors': (EQUIVALENT, 'same as prefer-const: eslint-config-expensify/configs/private/best-practices.js:365 authors ["error", {allowEmptyReject: true}], which is exactly what oxlint mirrors'), + 'no-throw-literal': (ACCEPTED, 'shares the scripts/** and .github/** override with the no-unsafe-* family. ESLint has no matching block, and its seatbelt carries 0 entries for it repo-wide, so nothing is being suppressed that ESLint reports'), + '@typescript-eslint/no-unsafe-assignment': (ACCEPTED, "off for scripts/** and .github/** because oxlint's tsgolint port of this family is stricter than typescript-eslint's, not because the paths are exempt. Measured 2026-09-15: deleting the override adds 695 findings across scripts/ and .github/ that ESLint reports 0 of. Not a type-info problem -- on the same file with the same program, typescript/no-unsafe-type-assertion agrees exactly (1 = 1 on .github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts, 8 = 8 on src/libs/memoize/index.ts), while no-unsafe-return is 3 vs 0 on that same memoize file. The divergence is global; the override just puts it where the loosely-typed GitHub Actions glue lives. ESLint's seatbelt carries 0 entries for all six rules repo-wide"), + '@typescript-eslint/no-unsafe-argument': (ACCEPTED, 'same stricter-tsgolint override as no-unsafe-assignment'), + '@typescript-eslint/no-unsafe-return': (ACCEPTED, 'same stricter-tsgolint override as no-unsafe-assignment; this is the one measurable outside the override too, 3 vs 0 on src/libs/memoize/index.ts'), + '@typescript-eslint/no-unsafe-call': (ACCEPTED, 'same stricter-tsgolint override as no-unsafe-assignment'), + '@typescript-eslint/no-unsafe-member-access': (ACCEPTED, 'same stricter-tsgolint override as no-unsafe-assignment; the largest of the six, 306 of the 695'), +} + + +def severity(value): + raw = value[0] if isinstance(value, list) else value + return {0: 'off', 1: 'warn', 2: 'error'}.get(raw, raw) + + +def options(value): + return value[1:] if isinstance(value, list) else [] + + +def fold_eslint_id(rule_id): + """A typescript-eslint extension rule is oxlint's base rule, which is already TS-aware.""" + if rule_id.startswith('@typescript-eslint/') and rule_id.split('/', 1)[1] in TS_EXTENSION_RULES: + return rule_id.split('/', 1)[1] + return rule_id + + +def resolve(files): + out = subprocess.run(['node', os.path.join(HERE, 'resolveConfigs.mjs'), *files], capture_output=True, text=True, cwd=ROOT) + if out.returncode != 0: + sys.exit(f'resolveConfigs.mjs failed:\n{out.stderr}') + return json.loads(out.stdout) + + +def compare(resolved): + """rule id -> (kind, one example of the difference, files it shows up in).""" + findings = {} + for path, sides in sorted(resolved.items()): + eslint = {fold_eslint_id(rid): val for rid, val in sides['eslint'].items()} + oxlint = {norm_ox_config(rid): val for rid, val in sides['oxlint'].items()} + for rule_id in sorted(set(eslint) | set(oxlint)): + es, ox = eslint.get(rule_id), oxlint.get(rule_id) + kind, detail = None, None + if ox is None: + if severity(es) != 'off': + kind, detail = 'eslint-only', f'ESLint {json.dumps(es)}, absent from oxlint' + elif es is None: + if severity(ox) != 'off': + kind, detail = 'oxlint-only', f'oxlint {json.dumps(ox)}, absent from ESLint' + elif severity(es) != severity(ox): + kind, detail = 'severity', f'ESLint {severity(es)}, oxlint {severity(ox)}' + elif options(es) != options(ox) and severity(es) != 'off': + kind, detail = 'options', f'ESLint {json.dumps(es)} vs oxlint {json.dumps(ox)}' + entry = findings.setdefault(rule_id, [None, None, [], []]) + if kind is None: + entry[3].append(path) + continue + entry[0] = entry[0] or kind + entry[1] = entry[1] or detail + entry[2].append(path) + # A rule that differs on some probed files and agrees on others is a SCOPE mismatch: both configs + # enable it, they just do not enable it over the same files. That is the one shape a findings + # comparison can never see, because a rule switched off for a file reports nothing there and + # nothing is what parity looks like. + for rule_id, entry in list(findings.items()): + if entry[0] is None: + del findings[rule_id] + continue + if entry[3]: + entry[1] = f'{entry[1]} (differs on {len(entry[2])} of {len(entry[2]) + len(entry[3])} probed files)' + entry[0] = 'scope' + return {rid: tuple(entry) for rid, entry in findings.items()} + + +def probe_files(): + """Two files per rule-carrying block in either config, plus the blocks nothing matched. + + Delegated to resolveConfigs.mjs because the ESLint side's `files` entries can be predicate + functions (that is what FlatCompat emits), which only the resolver can evaluate. + """ + out = subprocess.run( + ['node', os.path.join(HERE, 'resolveConfigs.mjs'), '--probe-files'], + cwd=ROOT, capture_output=True, text=True, check=True, + ) + return json.loads(out.stdout) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--json', help='write the full comparison to this path') + parser.add_argument('--files', nargs='*', help='files to resolve (default: the representative set)') + args = parser.parse_args() + + if args.files: + files, unmatched = args.files, [] + else: + probes = probe_files() + files, unmatched = sorted(set(probes['files']) | set(REPRESENTATIVE_FILES)), probes['unmatched'] + missing = [f for f in files if not os.path.exists(os.path.join(ROOT, f))] + if missing: + sys.exit('probe file(s) missing, so the override(s) they stand for are unchecked:\n ' + '\n '.join(missing)) + findings = compare(resolve(files)) + + unlisted = {rid: v for rid, v in findings.items() if rid not in LEDGER} + stale = [rid for rid in LEDGER if rid not in findings] + by_status = collections.defaultdict(list) + for rule_id, value in findings.items(): + if rule_id in LEDGER: + by_status[LEDGER[rule_id][0]].append((rule_id, value)) + + print(f'{len(files)} files, {len(findings)} rules differ\n') + print(f'{len(by_status[EQUIVALENT]):3} spelled differently, same behavior') + print(f'{len(by_status[ACCEPTED]):3} accepted differences') + print(f'{len(by_status[OPEN]):3} open differences, nobody chose these') + + if by_status[OPEN]: + print('\nOPEN, each needs an owner:') + for rule_id, (kind, detail, paths, _same) in sorted(by_status[OPEN]): + print(f' {rule_id} ({kind}, {len(paths)} file(s))') + print(f' {detail}') + print(f' {LEDGER[rule_id][1]}') + + if unlisted: + print(f'\n{len(unlisted)} rule(s) differ and are not in LEDGER. This is what a config bump looks like:') + for rule_id, (kind, detail, paths, _same) in sorted(unlisted.items()): + print(f' {rule_id} ({kind}, {len(paths)} file(s)): {detail}') + + if stale: + print(f'\n{len(stale)} LEDGER entries no longer differ and should be deleted:') + for rule_id in sorted(stale): + print(f' {rule_id}') + + if args.json: + payload = {rid: {'kind': k, 'detail': d, 'differsOn': p, 'agreesOn': q, 'status': LEDGER.get(rid, (None, None))[0], 'reason': LEDGER.get(rid, (None, None))[1]} for rid, (k, d, p, q) in findings.items()} + with open(args.json, 'w') as handle: + json.dump(payload, handle, indent=1, sort_keys=True) + handle.write('\n') + + if unmatched: + print(f'\n{len(unmatched)} config block(s) carry rules but match no file in the repo, so every rule they set is dead:') + for block in unmatched: + print(f' {block["linter"]} overrides[{block["index"]}] files={json.dumps(block["files"])}') + + if unlisted or stale or unmatched: + sys.exit(1) + print('\nNo unlisted drift. Every difference between the two configs is written down.') + + +if __name__ == '__main__': + main() diff --git a/oxlint-migration/checkEslintDirectives.mjs b/oxlint-migration/checkEslintDirectives.mjs new file mode 100644 index 000000000000..ac4f9dac5235 --- /dev/null +++ b/oxlint-migration/checkEslintDirectives.mjs @@ -0,0 +1,118 @@ +#!/usr/bin/env node +import {Linter} from 'eslint'; +// Differential test for config/oxlint/eslintDirectives.mjs. The module re-implements ESLint's +// directive resolution so a hosted rule answers to the id ESLint uses, and the only trustworthy +// oracle for "what would ESLint have done" is ESLint. Every case below is linted twice: once by the +// installed ESLint with `eqeqeq` and its own directive handling, once by the module's wrapper with +// the same directives and the rule id `eqeqeq`. The two line sets have to match. +// +// The module can only drop reports, never restore them, and oxlint's own directive engine already +// drops everything after a bare `/* eslint-disable */` (it does not implement the rule-specific +// enable below). So a divergence here is not visible in `npx oxlint` output today. It is still worth +// holding: the module's whole contract is "decide what ESLint would have decided", and any rule that +// later moves under the `hosted/` plugin starts routing through it. +import path from 'node:path'; +import process from 'node:process'; +import {fileURLToPath} from 'node:url'; + +import {withEslintDirectiveIds} from '../config/oxlint/eslintDirectives.mjs'; + +const ESLINT_RULE_ID = 'eqeqeq'; +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +// `a == 1` on its own line, one per line, so a case is written as a list of lines and the reported +// line numbers are readable straight off it. +const CASES = { + 'no directives': ['a == 1;'], + 'blanket disable': ['/* eslint-disable */', 'a == 1;'], + 'blanket disable, then specific enable': ['/* eslint-disable */', 'a == 1;', '/* eslint-enable eqeqeq */', 'b == 2;', 'c == 3;'], + 'blanket disable, then enable of another rule': ['/* eslint-disable */', 'a == 1;', '/* eslint-enable no-unused-labels */', 'b == 2;'], + 'two blanket disables, then specific enable': ['/* eslint-disable */', '/* eslint-disable */', 'a == 1;', '/* eslint-enable eqeqeq */', 'b == 2;'], + 'blanket disable, specific enable, blanket disable again': ['/* eslint-disable */', 'a == 1;', '/* eslint-enable eqeqeq */', 'b == 2;', '/* eslint-disable */', 'c == 3;'], + 'blanket disable, specific enable, specific disable again': ['/* eslint-disable */', 'a == 1;', '/* eslint-enable eqeqeq */', 'b == 2;', '/* eslint-disable eqeqeq */', 'c == 3;'], + 'specific disable, then blanket enable': ['/* eslint-disable eqeqeq */', 'a == 1;', '/* eslint-enable */', 'b == 2;'], + 'specific disable, then specific enable': ['/* eslint-disable eqeqeq */', 'a == 1;', '/* eslint-enable eqeqeq */', 'b == 2;'], + 'multi-rule disable list, specific enable of one': ['/* eslint-disable eqeqeq, no-unused-labels */', 'a == 1;', '/* eslint-enable eqeqeq */', 'b == 2;'], + 'specific enable with nothing open': ['/* eslint-enable eqeqeq */', 'a == 1;'], + 'specific enable before the disable it names': ['/* eslint-enable eqeqeq */', '/* eslint-disable eqeqeq */', 'a == 1;'], + 'justifications on both directives': ['/* eslint-disable -- because reasons */', 'a == 1;', '/* eslint-enable eqeqeq -- back on */', 'b == 2;'], + 'disable-next-line under a blanket disable': ['/* eslint-disable */', '// eslint-disable-next-line eqeqeq', 'a == 1;', 'b == 2;'], + 'disable-next-line after a specific enable': ['/* eslint-disable */', '/* eslint-enable eqeqeq */', '// eslint-disable-next-line eqeqeq', 'a == 1;', 'b == 2;'], + 'disable-next-line on its own': ['// eslint-disable-next-line eqeqeq', 'a == 1;', 'b == 2;'], + 'disable-line on its own': ['a == 1; // eslint-disable-line eqeqeq', 'b == 2;'], + 'blanket disable-next-line': ['// eslint-disable-next-line', 'a == 1;', 'b == 2;'], + 'a multi-line disable-next-line, which ESLint applies to the line after it closes': ['/* eslint-disable-next-line', ' eqeqeq */', 'a == 1;', 'b == 2;'], + 'a multi-line disable-line, which ESLint rejects': ['a == 1; /* eslint-disable-line', ' eqeqeq */', 'b == 2;'], + 'a multi-line block disable': ['/* eslint-disable', ' eqeqeq */', 'a == 1;'], + 'a multi-line block disable, then a multi-line block enable': ['/* eslint-disable', ' eqeqeq */', 'a == 1;', '/* eslint-enable', ' eqeqeq */', 'b == 2;'], +}; + +const linter = new Linter(); + +// What ESLint reports, directives and all. +function eslintLines(source) { + return linter + .verify(source, {files: ['**/*.js'], rules: {[ESLINT_RULE_ID]: 'error'}, linterOptions: {reportUnusedDisableDirectives: 'off'}}, 'probe.js') + .filter((message) => message.ruleId === ESLINT_RULE_ID) + .map((message) => message.line); +} + +// The comment list the module reads, taken from ESLint's own parse rather than from espree directly: +// espree is only a transitive dependency here, `eslint` is a declared one. A rule still runs under a +// blanket `eslint-disable`, only its reports are dropped, so this sees every comment. +function commentsOf(source) { + let comments = []; + const capture = { + create(context) { + comments = (context.sourceCode ?? context.getSourceCode()).getAllComments(); + return {}; + }, + }; + linter.verify(source, {files: ['**/*.js'], plugins: {probe: {rules: {capture}}}, rules: {'probe/capture': 'error'}}, 'probe.js'); + return comments; +} + +// What the module lets through, asked the way a hosted rule asks it: report on every `==` line and +// see which survive. A fresh filename per case, because the module caches parsed directives by name. +function moduleLines(source, filename) { + const reported = []; + let innerContext; + const rule = withEslintDirectiveIds( + { + meta: {schema: []}, + create(context) { + innerContext = context; + return {}; + }, + }, + ESLINT_RULE_ID, + ); + + const comments = commentsOf(source); + rule.create({ + filename, + sourceCode: {getAllComments: () => comments}, + report: (descriptor) => reported.push(descriptor.loc.line), + }); + + for (const [index, text] of source.split('\n').entries()) { + if (!text.includes('==')) { + continue; + } + innerContext.report({loc: {line: index + 1, column: 0}, message: 'probe'}); + } + return reported; +} + +let failed = false; +for (const [label, lines] of Object.entries(CASES)) { + const source = `${lines.join('\n')}\n`; + const expected = eslintLines(source); + const actual = moduleLines(source, path.join(repoRoot, `probe-${label.replaceAll(/\W+/g, '-')}.js`)); + const ok = JSON.stringify(expected) === JSON.stringify(actual); + failed ||= !ok; + console.log(`${ok ? 'PASS' : 'FAIL'} ${label} eslint [${expected.join(',')}], module [${actual.join(',')}]`); +} + +console.log(failed ? '\nFAILED' : `\nAll ${Object.keys(CASES).length} cases match ESLint ${new Linter().version}.`); +process.exit(failed ? 1 : 0); diff --git a/oxlint-migration/checkJsxUsesPort.py b/oxlint-migration/checkJsxUsesPort.py new file mode 100644 index 000000000000..5d9ed6f16d5b --- /dev/null +++ b/oxlint-migration/checkJsxUsesPort.py @@ -0,0 +1,142 @@ +"""Do react/jsx-uses-react and react/jsx-uses-vars still do their job under oxlint? + + python3 oxlint-migration/checkJsxUsesPort.py + +These two rules cannot report anything. All they do is mark identifiers as used, so that +no-unused-vars stays quiet about a React import and about variables only referenced from JSX. A +fixture is therefore impossible: there is no finding to compare. + +What can be compared is the outcome they exist to produce. This probe puts one file into src/, where +both tools run their real configs, and asserts three things: + + 1. the React import and the JSX-only component are NOT reported as unused, on either tool + 2. a genuinely unused variable IS reported, on both tools, on the same line -- otherwise step 1 + would also pass with no unused-vars rule running at all + 3. switching the two rules off changes nothing, on either tool + +Step 3 is the interesting one, and it is a measurement rather than an assumption: both +@typescript-eslint/parser's scope analysis and oxc's own semantics already count a JSX reference as a +reference, so the pair is redundant on both sides. If that ever stops being true on ESLint's side +while oxlint stays silent, this probe fails and the pair becomes a real coverage gap. +""" + +import json +import os +import shutil +import subprocess +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PROBE_DIR = os.path.join(ROOT, 'src', '__jsxUsesProbe') +PROBE_PATH = os.path.join(PROBE_DIR, 'probe.tsx') +ES_OFF = '{"react/jsx-uses-react": "off", "react/jsx-uses-vars": "off"}' +OX_OFF = ['-A', 'hosted/jsx-uses-react', '-A', 'hosted/jsx-uses-vars'] + +PROBE = """import React from 'react'; + +const UsedOnlyFromJsx = () => ; + +const neverUsedAtAll = 1; + +function JsxUsesProbe() { + return ; +} + +export default JsxUsesProbe; +""" + +CONTROL_LINE = 5 +SILENT_LINES = (1, 3) + + +def unused_var_lines_oxlint(extra_arguments): + out = subprocess.run( + ['npx', 'oxlint', '--format', 'json', *extra_arguments, os.path.relpath(PROBE_PATH, ROOT)], + capture_output=True, text=True, cwd=ROOT, + ) + try: + diagnostics = json.loads(out.stdout)['diagnostics'] + except (json.JSONDecodeError, KeyError): + sys.exit(f'oxlint run failed:\n{out.stdout[:600]}{out.stderr[:600]}') + return { + (d.get('labels') or [{}])[0].get('span', {}).get('line') + for d in diagnostics + if 'no-unused-vars' in (d.get('code') or '') + } + + +def unused_var_lines_eslint(extra_arguments): + # Raw ESLint, not the pipeline: no-unused-vars is not filtered by any pipeline stage, and this needs + # `--rule` passthrough anyway. See the same note in checkLocaleComparePort.py. + out = subprocess.run( + ['npx', 'eslint', '--no-warn-ignored', '--format', 'json', *extra_arguments, os.path.relpath(PROBE_PATH, ROOT)], + capture_output=True, text=True, cwd=ROOT, + env={**os.environ, 'NODE_OPTIONS': '--max-old-space-size=8192'}, + ) + try: + report = json.loads(out.stdout) + except json.JSONDecodeError: + sys.exit(f'eslint run failed:\n{out.stdout[:600]}{out.stderr[:600]}') + return {m['line'] for f in report for m in f['messages'] if 'no-unused-vars' in (m.get('ruleId') or '')} + + +def main(): + os.makedirs(PROBE_DIR, exist_ok=True) + with open(PROBE_PATH, 'w') as handle: + handle.write(PROBE) + try: + es_on = unused_var_lines_eslint([]) + es_off = unused_var_lines_eslint(['--rule', ES_OFF]) + ox_on = unused_var_lines_oxlint([]) + ox_off = unused_var_lines_oxlint(OX_OFF) + finally: + shutil.rmtree(PROBE_DIR, ignore_errors=True) + + failures = [] + print(f'unused-vars lines reported, rules ON : eslint {sorted(es_on)}, oxlint {sorted(ox_on)}') + print(f'unused-vars lines reported, rules OFF: eslint {sorted(es_off)}, oxlint {sorted(ox_off)}') + print() + + if CONTROL_LINE not in es_on or CONTROL_LINE not in ox_on: + failures.append( + f'the genuinely unused variable on line {CONTROL_LINE} was not reported by both tools ' + f'(eslint {sorted(es_on)}, oxlint {sorted(ox_on)}), so this probe can observe nothing' + ) + else: + print(f'control: line {CONTROL_LINE} reported by both tools, so an unused-vars rule really is running') + + for line in SILENT_LINES: + what = 'the React import' if line == 1 else 'the component used only from JSX' + if line in es_on or line in ox_on: + failures.append(f'{what} (line {line}) was reported as unused: eslint={line in es_on}, oxlint={line in ox_on}') + else: + print(f'silent: line {line}, {what}, on both tools') + + if es_on != ox_on: + failures.append(f'the two tools disagree with the rules on: eslint {sorted(es_on)} vs oxlint {sorted(ox_on)}') + + print() + if es_off == es_on and ox_off == ox_on: + print('Switching both rules off changes nothing on either tool: the scope analysis in the') + print('TypeScript parser, and the semantics oxc builds, both already count a JSX reference as a') + print('reference, so the pair is redundant on both sides rather than load-bearing on one.') + elif es_off != es_on and ox_off == ox_on: + failures.append( + f'the pair is load-bearing for ESLint (off: {sorted(es_off)}, on: {sorted(es_on)}) but makes no ' + f'difference in oxlint, so the sidecar copy is not doing the job the ESLint one does' + ) + else: + print(f'Switching both rules off changes what is reported (eslint {sorted(es_off)}, oxlint {sorted(ox_off)}),') + print('which is fine as long as both tools change the same way, asserted above.') + + print() + if failures: + print(f'{len(failures)} check(s) FAILED:') + for failure in failures: + print(f' {failure}') + sys.exit(1) + print('jsx-uses-react and jsx-uses-vars produce the same observable outcome on both tools.') + + +if __name__ == '__main__': + main() diff --git a/oxlint-migration/checkLocaleComparePort.py b/oxlint-migration/checkLocaleComparePort.py new file mode 100644 index 000000000000..f7384f4491ec --- /dev/null +++ b/oxlint-migration/checkLocaleComparePort.py @@ -0,0 +1,162 @@ +"""Does the type-free prefer-locale-compare-from-context match ESLint's type-aware original? + + python3 oxlint-migration/checkLocaleComparePort.py + +ESLint's rule (eslint-config-expensify/eslint-plugin-expensify/prefer-locale-compare-from-context.js) +asks the type checker whether the receiver is a string. oxlint's JS plugins get no types, so +config/oxlint/preferLocaleCompareFromContext.mjs drops the question instead of approximating it: +`localeCompare` lives on exactly one built-in prototype. + +This cannot be checked by the port-probe fixture harness, which parses without a TypeScript program +and would make ESLint's rule throw at create() time. So the probe files go into src/, where the +repo's real ESLint config has full type information, and both tools run their real configs. + +Every receiver shape in the repo is covered, plus the one shape where the two are KNOWN to differ. +That divergence is asserted rather than hidden: if it ever stops being the only difference, or if a +shape that should agree starts diverging, this fails. +""" + +import json +import os +import subprocess +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PROBE_DIR = os.path.join(ROOT, 'src', '__localeCompareProbe') +OX_RULE = 'rulesdir(prefer-locale-compare-from-context)' +ES_RULE = 'rulesdir/prefer-locale-compare-from-context' + +PROBE = """type Row = {created?: string}; + +const LANGUAGE: Record = {en: 'English'}; + +function shapes(a: Row, b: Row, first: string, second: string) { + const memberAccess = (a.created ?? '').localeCompare(b.created ?? ''); + const plainString = first.localeCompare(second); + const indexAccess = LANGUAGE[first].localeCompare(LANGUAGE[second]); + const ownMethod = {localeCompare: (other: string) => other.length}; + const notAString = ownMethod.localeCompare(first); + return [memberAccess, plainString, indexAccess, notAString]; +} + +export default shapes; +""" + +IN_TEST = """function inTest(first: string, second: string) { + return first.localeCompare(second); +} + +export default inTest; +""" + +EXPECTED = { + 6: (True, 'member access on an optional string, as in ReportUtils.ts:9493'), + 7: (True, 'plain string variable'), + 8: (True, 'index access into Record, as in LOCALES.ts:73'), + 10: ('divergent', 'object with its OWN localeCompare method -- ESLint sees a non-string receiver and stays silent; the type-free port cannot, so it reports. No such receiver exists in src/'), +} + + +def write_probes(): + os.makedirs(os.path.join(PROBE_DIR, 'tests'), exist_ok=True) + paths = {} + for name, body, relative in ( + ('shapes', PROBE, os.path.join(PROBE_DIR, 'shapes.ts')), + ('inTest', IN_TEST, os.path.join(PROBE_DIR, 'tests', 'inTest.ts')), + ): + with open(relative, 'w') as handle: + handle.write(body) + paths[name] = os.path.relpath(relative, ROOT) + return paths + + +def remove_probes(paths): + for path in paths.values(): + os.remove(os.path.join(ROOT, path)) + os.rmdir(os.path.join(PROBE_DIR, 'tests')) + os.rmdir(PROBE_DIR) + + +def oxlint_lines(paths): + out = subprocess.run(['npx', 'oxlint', '--format', 'json', *paths], capture_output=True, text=True, cwd=ROOT) + try: + diagnostics = json.loads(out.stdout)['diagnostics'] + except (json.JSONDecodeError, KeyError): + sys.exit(f'oxlint run failed:\n{out.stdout[:600]}{out.stderr[:600]}') + return { + (d['filename'], (d.get('labels') or [{}])[0].get('span', {}).get('line')) + for d in diagnostics + if (d.get('code') or '') == OX_RULE + } + + +def eslint_lines(paths): + # Raw ESLint, not the pipeline: the only rule read here is prefer-locale-compare-from-context, which + # no pipeline stage filters. Only the two rules in RULES_SUPPRESSED_BY_REACT_COMPILER and + # @typescript-eslint/no-deprecated need the pipeline to mean what the repo's gate means. + out = subprocess.run( + ['npx', 'eslint', '--no-warn-ignored', '--format', 'json', *paths], + capture_output=True, text=True, cwd=ROOT, + env={**os.environ, 'NODE_OPTIONS': '--max-old-space-size=8192'}, + ) + try: + report = json.loads(out.stdout) + except json.JSONDecodeError: + sys.exit(f'eslint run failed:\n{out.stdout[:600]}{out.stderr[:600]}') + return { + (os.path.relpath(f['filePath'], ROOT), m['line']) + for f in report + for m in f['messages'] + if m.get('ruleId') == ES_RULE + } + + +def main(): + paths = write_probes() + try: + ox = oxlint_lines(list(paths.values())) + es = eslint_lines(list(paths.values())) + finally: + remove_probes(paths) + + failures = [] + print(f'{"line":>5} {"eslint":>6} {"oxlint":>6} verdict / case') + for line, (expectation, description) in sorted(EXPECTED.items()): + ox_hit = (paths['shapes'], line) in ox + es_hit = (paths['shapes'], line) in es + if expectation == 'divergent': + ok = ox_hit and not es_hit + verdict = 'known divergence, as expected' if ok else 'FAIL: the known divergence changed shape' + else: + ok = ox_hit == es_hit == expectation + verdict = 'agree' if ok else f'FAIL: expected both to report, got eslint={es_hit} oxlint={ox_hit}' + if not ok: + failures.append(f'line {line}: {verdict}') + print(f'{line:>5} {str(es_hit):>6} {str(ox_hit):>6} {verdict}\n {description}') + + # The rule skips anything under tests/. Both tools must be silent, for the same reason. + test_ox = {line for path, line in ox if path == paths['inTest']} + test_es = {line for path, line in es if path == paths['inTest']} + skip_ok = not test_ox and not test_es + print(f'\ntest-file skip: eslint={sorted(test_es)} oxlint={sorted(test_ox)} ' + f'{"both silent, as ESLint does" if skip_ok else "FAIL: one of them reported inside tests/"}') + if not skip_ok: + failures.append('the tests/ skip does not match') + + stray = {(path, line) for path, line in ox | es if line not in EXPECTED and path != paths['inTest']} + if stray: + failures.append(f'findings on unaccounted lines: {sorted(stray)}') + print(f'FAIL: findings on lines this probe does not describe: {sorted(stray)}') + + print() + if failures: + print(f'{len(failures)} check(s) FAILED:') + for failure in failures: + print(f' {failure}') + sys.exit(1) + print('The type-free port matches the type-aware original on every shape in src/, and diverges') + print('only on a receiver that defines its own localeCompare, which src/ does not contain.') + + +if __name__ == '__main__': + main() diff --git a/oxlint-migration/checkParityDirection.py b/oxlint-migration/checkParityDirection.py new file mode 100644 index 000000000000..76f9252b9209 --- /dev/null +++ b/oxlint-migration/checkParityDirection.py @@ -0,0 +1,82 @@ +"""Which way each whole-repo delta points, at the granularity the seatbelt enforces. + + python3 oxlint-migration/checkParityDirection.py [oxlint.json] [eslint.json] + +A finding oxlint adds makes the gate stricter, which the baseline absorbs. A finding ESLint reports +and oxlint does not is a check that stops running the day oxlint goes blocking. The unit is (file, +rule, count) because that is what a seatbelt row is, so a finding anchored at a different column is a +different reading of the same check, not a lost one. Exit is always 0: findings are hidden today, so a +red run would only train people to ignore it. +""" + +import collections +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from compareFullRepo import read_messages, relative +from ruleMap import ROOT, norm_es_folded + +TOP_DETAIL = 6 + + +def per_file_rule(messages): + counts = collections.Counter() + for message in messages: + counts[(relative(message['filePath']), norm_es_folded(message.get('ruleID')))] += 1 + return counts + + +def baseline_total(path): + total = 0 + with open(path) as handle: + for line in handle: + if line.strip() and not line.startswith('#'): + total += int(line.rstrip('\n').split('\t')[-1]) + return total + + +def main(ox_file, es_file): + ox = per_file_rule(read_messages(ox_file, 'oxlint')) + es = per_file_rule(read_messages(es_file, 'eslint')) + + added = collections.Counter() + hidden = collections.Counter() + hidden_files = collections.Counter() + exact = 0 + for key in set(ox) | set(es): + if ox[key] > es[key]: + added[key[1]] += ox[key] - es[key] + elif es[key] > ox[key]: + hidden[key[1]] += es[key] - ox[key] + hidden_files[key[1]] += 1 + else: + exact += 1 + + print(f'(file, rule) pairs identical on both tools: {exact} ({sum(es[k] for k in es if ox[k] == es[k])} findings)') + print(f'findings oxlint adds: {sum(added.values()):6} safe: the gate gets stricter and the baseline absorbs it') + print(f'findings oxlint hides: {sum(hidden.values()):6} each is a check an oxlint gate would not run') + + if hidden: + print('\nhidden, by rule (needs a written reason in OXLINT_MIGRATION_STATE.md section 3.3.1):') + for rule, count in hidden.most_common(): + print(f' {rule:56} {count:5} across {hidden_files[rule]} files') + worst = sorted(((es[k] - ox[k], k[0], es[k], ox[k]) for k in es if k[1] == rule and ox[k] < es[k]), reverse=True) + for lost, path, have, reported in worst[:TOP_DETAIL]: + print(f' {f"-{lost}":>5} {path} eslint {have} -> oxlint {reported}') + if len(worst) > TOP_DETAIL: + print(f' ... {len(worst) - TOP_DETAIL} more files') + + print('\nbaseline totals:') + for linter, live, path in (('oxlint', ox, 'config/oxlint/oxlint.seatbelt.tsv'), ('eslint', es, 'config/eslint/eslint.seatbelt.tsv')): + grandfathered = baseline_total(os.path.join(ROOT, path)) + reported = sum(live.values()) + verdict = 'exact, nothing to tighten' if grandfathered == reported else f'{grandfathered - reported} rows would tighten' + print(f' {linter:7} baseline {grandfathered:5}, live {reported:5} -- {verdict}') + + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else '/tmp/oxlint-full.json', sys.argv[2] if len(sys.argv) > 2 else '/tmp/eslint-full.json')) diff --git a/oxlint-migration/checkReactCompilerGate.py b/oxlint-migration/checkReactCompilerGate.py new file mode 100644 index 000000000000..1c396a0cec35 --- /dev/null +++ b/oxlint-migration/checkReactCompilerGate.py @@ -0,0 +1,139 @@ +"""Does oxlint's React Compiler gate suppress what the ESLint side suppresses? + + python3 oxlint-migration/checkReactCompilerGate.py + +Two rules are suppressed wholesale in files both React compilers memoize. On the ESLint side that is +a pipeline stage, `scripts/lint/processors/ReactCompilerFilter.ts`, which runs over ESLint's output +rather than inside it; oxlint has no processor concept, so config/oxlint/reactCompilerGate.mjs does +it by wrapping `context.report` inside the JS plugin. This checks the two agree, on both answers. + +Because the suppression is no longer wired into the ESLint config, `npx eslint` alone reports the +findings the repo's gate does not. The ESLint side here therefore runs the repo's own gate, +`scripts/lint/index.ts --linter=eslint --format=json`, which applies the production stage. Comparing +against raw ESLint would fail on the memoized variant no matter what oxlint does. + +The per-rule fixtures in oxlint-migration/port-probe cannot check this. They deliberately opt out of +memoization with 'use no memo', because their job is "does the rule run at all" and a gate that +silenced them would look identical to a rule that never loaded. So the gate needs its own test, +and it needs a matched pair: + + memoized both compilers memoize it -> BOTH tools must report nothing + opted out same code + 'use no memo' -> BOTH tools must report both rules + +Only the pair is meaningful. Silence alone proves nothing (a broken plugin is also silent) and +findings alone prove nothing (an absent gate also reports). + +The probe files are written into src/ rather than oxlint-migration/, because the repo's real ESLint +config excludes oxlint-migration from the TypeScript program and its type-aware rules cannot parse a +file outside it. They are deleted in a finally. +""" + +import json +import os +import subprocess +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PROBE_DIR = os.path.join(ROOT, 'src', '__reactCompilerGateProbe') + +TEMPLATE = """import React from 'react'; + +import useOnyx from '@hooks/useOnyx'; + +import ONYXKEYS from '@src/ONYXKEYS'; + +const GateProbeContext = React.createContext<{{count: number}} | undefined>(undefined); + +function GateProbe({{children}}: {{children: React.ReactNode}}) {{ +{directive} const [accountID] = useOnyx(ONYXKEYS.SESSION, {{selector: (session) => session?.accountID}}); + const contextValue = {{count: accountID ?? 0}}; + return {{children}}; +}} + +export default GateProbe; +""" + +GATED_RULES = [ + ('rulesdir(no-inline-useOnyx-selector)', 'rulesdir/no-inline-useOnyx-selector'), + ('hosted(jsx-no-constructed-context-values)', 'react/jsx-no-constructed-context-values'), +] + +VARIANTS = { + 'memoized': ('', False), + 'optedOut': (" 'use no memo';\n", True), +} + + +def write_probes(): + os.makedirs(PROBE_DIR, exist_ok=True) + paths = {} + for name, (directive, _) in VARIANTS.items(): + path = os.path.join(PROBE_DIR, f'{name}.tsx') + with open(path, 'w') as handle: + handle.write(TEMPLATE.format(directive=directive)) + paths[name] = os.path.relpath(path, ROOT) + return paths + + +def oxlint_findings(paths): + out = subprocess.run( + ['npx', 'oxlint', '--format', 'json', *paths], + capture_output=True, text=True, cwd=ROOT, + ) + try: + diagnostics = json.loads(out.stdout)['diagnostics'] + except (json.JSONDecodeError, KeyError): + sys.exit(f'oxlint run failed:\n{out.stdout[:600]}{out.stderr[:600]}') + return {(d['filename'], d.get('code') or '') for d in diagnostics} + + +def eslint_findings(paths): + out = subprocess.run( + ['bun', os.path.join('scripts', 'lint', 'index.ts'), '--linter=eslint', '--format=json', *paths], + capture_output=True, text=True, cwd=ROOT, + env={**os.environ, 'NODE_OPTIONS': '--max-old-space-size=8192', 'SEATBELT_DISABLE': '1'}, + ) + try: + report = json.loads(out.stdout) + except json.JSONDecodeError: + sys.exit(f'eslint run failed:\n{out.stdout[:600]}{out.stderr[:600]}') + return {(os.path.relpath(m['filePath'], ROOT), m.get('ruleID')) for m in report['messages']} + + +def main(): + paths = write_probes() + try: + ox = oxlint_findings(list(paths.values())) + es = eslint_findings(list(paths.values())) + finally: + for path in paths.values(): + os.remove(os.path.join(ROOT, path)) + os.rmdir(PROBE_DIR) + + failures = [] + print(f'{"variant":10} {"rule":42} {"eslint":>7} {"oxlint":>7} verdict') + for name, (_, should_report) in VARIANTS.items(): + for ox_rule, es_rule in GATED_RULES: + ox_hit = (paths[name], ox_rule) in ox + es_hit = (paths[name], es_rule) in es + if ox_hit != es_hit: + verdict = 'FAIL: the two tools disagree' + elif ox_hit != should_report: + verdict = f'FAIL: expected {"a finding" if should_report else "silence"} from both' + else: + verdict = 'agree' + if verdict != 'agree': + failures.append(f'{name}/{es_rule}: {verdict}') + print(f'{name:10} {es_rule:42} {str(es_hit):>7} {str(ox_hit):>7} {verdict}') + + print() + if failures: + print(f'{len(failures)} check(s) FAILED:') + for failure in failures: + print(f' {failure}') + sys.exit(1) + print('The gate matches the ESLint side: silent where both compilers memoize, live where they do not.') + + +if __name__ == '__main__': + main() diff --git a/oxlint-migration/checkReactCompilerRust.mjs b/oxlint-migration/checkReactCompilerRust.mjs new file mode 100644 index 000000000000..b47a206617b6 --- /dev/null +++ b/oxlint-migration/checkReactCompilerRust.mjs @@ -0,0 +1,161 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import {fileURLToPath} from 'node:url'; + +import {CONFIG_CATEGORY, IGNORED_CATEGORIES, RULE_BY_CATEGORY, reactCompilerDiagnostics} from '../config/oxlint/reactCompilerRust.mjs'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const FIXTURE_DIR = path.join(repoRoot, 'oxlint-migration/port-probe/fixtures'); +const PROBE_DIR = path.join(repoRoot, 'oxlint-migration/native-vs-sidecar-probe'); +const PLUGIN_BUNDLE = path.join(repoRoot, 'node_modules/eslint-config-expensify/node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js'); + +const FIXTURES = [ + ['rhRefs.tsx', 'refs', 1], + ['rhSetStateInRender.tsx', 'set-state-in-render', 1], + ['rhPreserveManualMemoization.tsx', 'preserve-manual-memoization', 1], + ['rhImmutability.tsx', 'immutability', 2], + ['rhUseMemo.tsx', 'use-memo', 1], + ['rhGlobals.tsx', 'globals', 1], + ['rhPurity.tsx', 'purity', 1], + ['rhIncompatibleLibrary.tsx', 'incompatible-library', 1], + ['rhUnsupportedSyntax.tsx', 'unsupported-syntax', 1], + ['rhSetStateInEffect.tsx', 'set-state-in-effect', 1], + ['rhStaticComponents.tsx', 'static-components', 1], + ['rhErrorBoundaries.tsx', 'error-boundaries', 1], +]; + +let failed = false; + +function check(ok, label, detail = '') { + failed ||= !ok; + console.log(`${ok ? 'PASS' : 'FAIL'} ${label}${detail ? ` ${detail}` : ''}`); +} + +function diagnose(file) { + return reactCompilerDiagnostics(file, fs.readFileSync(file, 'utf8')); +} + +function countsByRule(diagnostics) { + const counts = new Map(); + for (const {ruleName} of diagnostics) { + counts.set(ruleName, (counts.get(ruleName) ?? 0) + 1); + } + return counts; +} + +console.log("1. one fixture per rule, and no rule reporting another rule's fixture"); +const ALL_RULES = Object.values(RULE_BY_CATEGORY); +for (const [fixture, rule, expected] of FIXTURES) { + const counts = countsByRule(diagnose(path.join(FIXTURE_DIR, fixture))); + const own = counts.get(rule) ?? 0; + const strays = ALL_RULES.filter((other) => other !== rule && (counts.get(other) ?? 0) > 0); + check(own === expected && strays.length === 0, `${fixture} -> rc/${rule}`, `got ${own}, expected ${expected}${strays.length ? `, STRAY ${strays.join(',')}` : ''}`); +} +check(FIXTURES.length === ALL_RULES.length, 'every rule in RULE_BY_CATEGORY has a fixture', `${FIXTURES.length} fixtures for ${ALL_RULES.length} rules`); + +console.log('\n2. the category tables cover the whole ErrorCategory enum in eslint-plugin-react-hooks'); +const bundle = fs.readFileSync(PLUGIN_BUNDLE, 'utf8'); +const enumCategories = [...bundle.matchAll(/ErrorCategory\["(\w+)"] = "\1"/g)].map((match) => match[1]); +// Config is in neither table on purpose: it is thrown on rather than mapped or ignored. +const known = new Set([...Object.keys(RULE_BY_CATEGORY), ...IGNORED_CATEGORIES, CONFIG_CATEGORY]); +const unmapped = enumCategories.filter((category) => !known.has(category)); +const phantom = [...known].filter((category) => !enumCategories.includes(category)); +check(enumCategories.length > 0, 'ErrorCategory enum found in the plugin bundle', `${enumCategories.length} categories`); +check(unmapped.length === 0, 'every upstream category is mapped or explicitly ignored', unmapped.length ? `UNMAPPED ${unmapped.join(',')}` : ''); +check(phantom.length === 0, 'no table entry names a category upstream dropped', phantom.length ? `STALE ${phantom.join(',')}` : ''); + +console.log('\n3. an unrecognized category throws'); +const refsFixture = path.join(FIXTURE_DIR, 'rhRefs.tsx'); +const unmappedCopy = path.join(PROBE_DIR, 'rhRefs.unmapped-probe.tsx'); +fs.copyFileSync(refsFixture, unmappedCopy); +const savedRule = RULE_BY_CATEGORY.Refs; +delete RULE_BY_CATEGORY.Refs; +let threw = false; +try { + diagnose(unmappedCopy); +} catch (error) { + threw = /Unknown React Compiler category 'Refs'/.test(String(error.message)); +} +RULE_BY_CATEGORY.Refs = savedRule; +fs.rmSync(unmappedCopy); +check(threw, 'a category missing from both tables throws'); + +console.log('\n4. a file the compiler cannot parse reports nothing'); +const brokenFile = path.join(PROBE_DIR, 'broken.probe.tsx'); +fs.writeFileSync(brokenFile, 'export function Broken( {\n'); +let broken; +try { + broken = diagnose(brokenFile); +} catch (error) { + broken = `threw: ${error.message}`; +} +fs.rmSync(brokenFile); +check(Array.isArray(broken) && broken.length === 0, 'a syntactically broken file yields []', String(broken)); + +console.log('\n5. the analysis is cached per filename'); +const cachedFirst = diagnose(refsFixture); +const cachedSecond = reactCompilerDiagnostics(refsFixture, 'export const nothing = 1;\n'); +check(cachedSecond === cachedFirst, 'a second call for the same filename returns the cached array', `${cachedFirst.length} diagnostics`); + +console.log('\n6. suppression comments no longer hide the analysis (the reason this module exists)'); +const counter = diagnose(path.join(PROBE_DIR, 'Counter.tsx')); +const counterLines = counter.map((diagnostic) => diagnostic.loc.start.line).sort((first, second) => first - second); +check(JSON.stringify(counterLines) === JSON.stringify([8, 12]), 'Counter.tsx reports the ref read and the setState-in-effect', `lines ${counterLines.join(',') || '(none)'}`); +const twoComponents = diagnose(path.join(PROBE_DIR, 'TwoComponents.tsx')); +const twoLines = twoComponents.map((diagnostic) => diagnostic.loc.start.line).sort((first, second) => first - second); +check( + JSON.stringify(twoLines) === JSON.stringify([7, 21, 24]), + 'TwoComponents.tsx reports both components in one pass, as ESLint does', + `lines ${twoLines.join(',') || '(none)'}, expected 7,21,24`, +); + +console.log('\n7. the one recorded anchor divergence stays where it was measured'); +// ESLint anchors the second immutability finding on the escape site, the `onClick={onSelect}` line +// (measured 2026-08-21, column 29); the Rust compiler has no label there and anchors both on the +// modification site. +const anchorFile = path.join(PROBE_DIR, 'rhImmutabilityAnchor.tsx'); +const anchorLines = fs.readFileSync(anchorFile, 'utf8').split('\n'); +const modificationLine = anchorLines.findLastIndex((line) => line.includes('latest = 1;')) + 1; +const escapeLine = anchorLines.findLastIndex((line) => line.includes('onClick={onSelect}')) + 1; +const anchorPoints = diagnose(anchorFile) + .map((diagnostic) => `${diagnostic.loc.start.line}:${diagnostic.loc.start.column + 1}`) + .sort(); +const expectedPoints = [`${modificationLine}:9`, `${modificationLine}:9`]; +check( + JSON.stringify(anchorPoints) === JSON.stringify(expectedPoints), + 'rhImmutabilityAnchor.tsx reports both immutability findings on the modification site', + `got ${anchorPoints.join(' and ') || 'nothing'}, expected ${expectedPoints.join(' and ') || 'nothing'}; ESLint puts the second one at ${escapeLine}:29`, +); + +console.log('\n8. every rc/* rule is enabled, including the ones that only report part of what ESLint does'); +// A rule that under-reports shows up as a count in `npm run compare-oxlint`. A rule switched off +// reads exactly like a clean codebase, so none of them are switched off. +const oxlintConfig = (await import(path.join(repoRoot, 'oxlint.config.mts'))).default; +const severityOf = (rule) => oxlintConfig.rules?.[`rc/${rule}`]; +const notEnabled = ALL_RULES.filter((rule) => severityOf(rule) !== 'error'); +check( + notEnabled.length === 0, + 'all rc/* rules are "error" in oxlint.config.mts', + notEnabled.length === 0 ? `${ALL_RULES.length} of ${ALL_RULES.length}` : `not "error": ${notEnabled.map((rule) => `rc/${rule} (is ${severityOf(rule) ?? 'missing'})`).join(', ')}`, +); + +console.log('\n9. a byte offset past a non-ASCII character still anchors on the right line'); +// oxc hands back UTF-8 byte offsets; a JavaScript string is indexed in UTF-16 code units. Indexing +// the source with one directly drifts one unit per extra byte, so the diagnostic lands past its own +// line and an `eslint-disable-next-line` on the correct line stops suppressing it. 281 of src/'s +// .tsx files contain non-ASCII, so this is the common case rather than a corner. +const nonAsciiFile = path.join(PROBE_DIR, 'NonAscii.tsx'); +const nonAsciiLines = fs.readFileSync(nonAsciiFile, 'utf8').split('\n'); +const refReadLine = nonAsciiLines.findIndex((line) => line.includes('ref.current * 2')) + 1; +const refReadColumn = nonAsciiLines[refReadLine - 1].indexOf('ref.current'); +const nonAsciiPoints = diagnose(nonAsciiFile).map((diagnostic) => `${diagnostic.loc.start.line}:${diagnostic.loc.start.column}`); +check( + JSON.stringify(nonAsciiPoints) === JSON.stringify([`${refReadLine}:${refReadColumn}`]), + 'NonAscii.tsx reports rc/refs on the ref read itself', + `got ${nonAsciiPoints.join(' and ') || 'nothing'}, expected ${refReadLine}:${refReadColumn}`, +); + +console.log(failed ? '\nFAILED' : '\nAll assertions hold.'); +process.exit(failed ? 1 : 0); diff --git a/oxlint-migration/checkSidecarCoverage.py b/oxlint-migration/checkSidecarCoverage.py new file mode 100644 index 000000000000..1ced86d1b4e4 --- /dev/null +++ b/oxlint-migration/checkSidecarCoverage.py @@ -0,0 +1,171 @@ +"""Every rule oxlint runs in its Node sidecar, and the evidence that it really runs. + + python3 oxlint-migration/checkSidecarCoverage.py [--json ] + +Full-repo parity cannot answer this question. Only 13 of the enabled rules have violations in this +repository, so for every other rule "0 = 0" is indistinguishable from a rule that loads, runs against +a bridged AST it does not understand, and silently reports nothing. That risk is concentrated in the +sidecar rules, because oxlint does not reimplement them: it loads the same JavaScript modules ESLint +loads and feeds them a syntax tree it built itself. + +Evidence is one of three kinds, all of which run both linters and compare: + + fixture a file in oxlint-migration/port-probe that violates the rule + replay a RuleTester case, upstream or repo-owned, materialized as a real file + probe a bespoke script, for the rules neither of those can express + +The run fails if an enabled sidecar rule has none of the three, or if the fixture manifest names a +rule that is not an enabled sidecar rule, since a stale entry proves nothing. +""" + +import argparse +import glob +import json +import os +import shutil +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) + +sys.path.insert(0, HERE) +sys.path.insert(0, os.path.join(HERE, 'rule-tester')) +from compareRuleTester import TREE, build_tree, enabled_custom_rules +from ruleMap import is_on, js_plugin_rules, load_oxlint_config + +MANIFEST = os.path.join(HERE, 'port-probe', 'fixtures.manifest.json') + +HAND_HOSTED = {'core', 'hosted', 'rulesdir'} + +PROBE_EVIDENCE = { + 'rulesdir/prefer-locale-compare-from-context': ( + 'npm run oxlint-locale-compare-port', + 'ESLint runs a type-aware rule here and oxlint runs a type-free rewrite, so the probe asserts them receiver shape by receiver shape inside src/, where types exist', + ), + 'rulesdir/no-inline-useOnyx-selector': ( + 'npm run oxlint-react-compiler-gate', + 'the fixture proves the rule; this proves the React Compiler gate wrapped around it, which is what decides whether a finding survives', + ), + 'hosted/jsx-no-constructed-context-values': ( + 'npm run oxlint-react-compiler-gate', + 'same gate, same reason', + ), + 'hosted/jsx-uses-react': ( + 'npm run oxlint-jsx-uses-port', + 'the rule cannot report: it only marks identifiers as used for no-unused-vars, so the probe asserts the outcome it exists to produce, and measures that switching it off changes nothing on either tool', + ), + 'hosted/jsx-uses-vars': ( + 'npm run oxlint-jsx-uses-port', + 'same, for JSX-only variables rather than the React import', + ), +} + +EXEMPT = {} + + +def enabled_sidecar_rules(): + """oxlint rule id -> alias, for every rule the config turns on under a jsPlugin alias. + + Keyed on the id the config writes, not on "some rule of that name is enabled somewhere": an alias + may host a rule that production runs natively instead, and covering the aliased id would test + something the shipped config never runs. + """ + aliases = set(js_plugin_rules().values()) + config = load_oxlint_config() + scopes = [config.get('rules', {})] + [override.get('rules', {}) for override in config.get('overrides', [])] + rules = {} + for scope in scopes: + for rule_id, value in scope.items(): + alias = rule_id.split('/')[0] if '/' in rule_id else None + if alias in aliases and is_on(value): + rules[rule_id] = alias + return dict(sorted(rules.items())) + + +def fixture_evidence(): + """oxlint rule id -> the fixture that violates it, including the fragments/ shards.""" + manifest = json.load(open(MANIFEST)) + for path in sorted(glob.glob(os.path.join(os.path.dirname(MANIFEST), 'fragments', '*.manifest.json'))): + manifest.update(json.load(open(path))) + return {entry['oxlintRule']: entry['fixture'] for entry in manifest.values()} + + +def replay_evidence(custom_rules): + """Custom rule names with at least one RuleTester case, from a real harvest.""" + summary = build_tree(custom_rules) + shutil.rmtree(TREE, ignore_errors=True) + return {f'rulesdir/{rule}' for rule in custom_rules if rule not in summary['rulesWithoutTests']} + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--json', help='write the coverage table to this path') + args = parser.parse_args() + + sidecar = enabled_sidecar_rules() + fixtures = fixture_evidence() + replays = replay_evidence(enabled_custom_rules()) + + rows = [] + for oxlint_id, alias in sidecar.items(): + evidence = [] + if oxlint_id in fixtures: + evidence.append(('fixture', fixtures[oxlint_id])) + if oxlint_id in replays: + evidence.append(('replay', 'oxlint-migration/rule-tester')) + if oxlint_id in PROBE_EVIDENCE: + evidence.append(('probe', PROBE_EVIDENCE[oxlint_id][0])) + rows.append({'oxlintRule': oxlint_id, 'alias': alias, 'evidence': evidence}) + rows.sort(key=lambda row: (row['alias'], row['oxlintRule'])) + + uncovered = [row for row in rows if not row['evidence'] and row['alias'] in HAND_HOSTED and row['oxlintRule'] not in EXEMPT] + exempted = [row for row in rows if not row['evidence'] and row['oxlintRule'] in EXEMPT] + plugin_hosted = [row for row in rows if not row['evidence'] and row['alias'] not in HAND_HOSTED] + stray = sorted(rule for rule in fixtures if rule.split('/')[0] in set(sidecar.values()) and rule not in sidecar) + + print(f'{"oxlint rule id":48} evidence') + for row in rows: + kinds = ', '.join(f'{kind} ({where})' for kind, where in row['evidence']) or 'NONE' + print(f'{row["oxlintRule"]:48} {kinds}') + + by_alias = {} + for row in rows: + covered, total = by_alias.get(row['alias'], (0, 0)) + by_alias[row['alias']] = (covered + (1 if row['evidence'] else 0), total + 1) + print() + for alias in sorted(alias for alias in by_alias if alias in HAND_HOSTED): + covered, total = by_alias[alias] + print(f'{alias + "/":18} {covered}/{total} covered') + hand_covered = sum(covered for alias, (covered, _) in by_alias.items() if alias in HAND_HOSTED) + hand_total = sum(total for alias, (_, total) in by_alias.items() if alias in HAND_HOSTED) + print(f'{"hand-hosted":18} {hand_covered}/{hand_total} covered') + if plugin_hosted: + print(f'\n{len(plugin_hosted)} rule(s) come from whole npm plugins loaded by package name, where one') + print('fixture per plugin proves the plugin loads and reports. Listed, not required:') + for row in plugin_hosted: + print(f' {row["oxlintRule"]}') + + if exempted: + print(f'\nExempt ({len(exempted)}), each by an explicit decision:') + for row in exempted: + print(f' {row["oxlintRule"]}: {EXEMPT[row["oxlintRule"]]}') + + if args.json: + with open(args.json, 'w') as handle: + json.dump({'rules': rows, 'uncovered': [row['oxlintRule'] for row in uncovered]}, handle, indent=1) + handle.write('\n') + + print() + if stray: + print(f'{len(stray)} fixture manifest entries name a sidecar rule the config does not enable: {", ".join(stray)}') + if uncovered: + print(f'{len(uncovered)} sidecar rule(s) have no evidence they run:') + for row in uncovered: + print(f' {row["oxlintRule"]}') + if stray or uncovered: + sys.exit(1) + print(f'All {len(rows)} enabled sidecar rules are covered by a fixture, a replayed case or a probe.') + + +if __name__ == '__main__': + main() diff --git a/oxlint-migration/compareFullRepo.py b/oxlint-migration/compareFullRepo.py new file mode 100644 index 000000000000..1fd82d382621 --- /dev/null +++ b/oxlint-migration/compareFullRepo.py @@ -0,0 +1,159 @@ +"""Full-repository finding-by-finding comparison between ESLint and Oxlint. + +Usage (see OXLINT_MIGRATION_INVESTIGATION.md appendix for the generation commands): + python3 oxlint-migration/compareFullRepo.py [/tmp/oxlint-full.json] [/tmp/eslint-full.json] + +Both reports come from `scripts/lint/index.ts --format json`, one per linter, so both have +passed the same processors. Running one leg raw would compare a React-Compiler-filtered +report against an unfiltered one and every react-hooks row would be meaningless. + +Checks parity three ways: + + counts per-rule totals, the table + locations the (file, line) set per rule -- equal counts are not equal findings, + and a rule whose port anchors reports one line off would otherwise pass + ruleless messages carrying no rule id. Under the pipeline a crashing JS-plugin rule + never reaches here at all: OxlintLinter promotes a codeless diagnostic to a + fatal exit 2 and writes no report, so the run dies instead of silently + reporting a rule that checked nothing. What is left is ESLint's own + ruleId-less output, such as an unused disable directive. + +Then a config-level coverage check, which catches gaps the findings table cannot see: +a rule enabled in ESLint but missing from the Oxlint config looks like parity until +the first violation is written. Rule metadata and the port plan for every ESLint-only +rule live in oxlint-migration/ruleMap.py; for the full rule inventory (including rules +with no findings) use oxlint-migration/listAllRules.py. +""" + +import collections +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import ruleMap +from ruleMap import PORT_PLAN, ROOT, norm_es_folded + +# What norm_es returns for a message with no ruleId. +RULELESS = '' + + +def read_time(report_path): + try: + return float(open(f'{report_path}.time').read().strip()) + except (OSError, ValueError): + return None + + +def relative(path): + """Both tools name the same file differently: oxlint repo-relative, ESLint absolute.""" + return os.path.relpath(path, ROOT) if os.path.isabs(path) else path + + +def read_messages(report_path, linter): + """The flat LintMessage list `scripts/lint --format json` writes.""" + try: + return json.load(open(report_path))['messages'] + except (json.JSONDecodeError, KeyError): + sys.exit( + f'{report_path} is not a lint report. The {linter} leg exited fatally and wrote nothing ' + f'usable; rerun it on its own to see the error.' + ) + + +def locations(messages): + """rule -> {(file, line)}. Both legs are already normalized to ESLint rule ids.""" + per_rule = collections.defaultdict(set) + for message in messages: + per_rule[norm_es_folded(message.get('ruleID'))].add((relative(message['filePath']), message['line'])) + return per_rule + + +def findings_table(ox_file, es_file): + ox = read_messages(ox_file, 'oxlint') + es = read_messages(es_file, 'eslint') + ox_at, es_at = locations(ox), locations(es) + cox = collections.Counter(norm_es_folded(m.get('ruleID')) for m in ox) + ces = collections.Counter(norm_es_folded(m.get('ruleID')) for m in es) + + print(f'{"rule":62} {"eslint":>7} {"oxlint":>7}') + diffs, misplaced = [], [] + for rule in sorted(set(cox) | set(ces), key=lambda r: -(ces.get(r, 0) + cox.get(r, 0))): + a, b = ces.get(rule, 0), cox.get(rule, 0) + print(f'{rule:62} {a:7} {b:7}') + if a != b: + diffs.append((rule, a, b)) + elif a and es_at[rule] != ox_at[rule]: + misplaced.append((rule, sorted(es_at[rule] - ox_at[rule]), sorted(ox_at[rule] - es_at[rule]))) + print(f'{"totals":62} {sum(ces.values()):7} {sum(cox.values()):7}') + + print() + print('Timing (single cold run each, not a benchmark -- a busy machine moves these by 10%+):') + for name, path in (('eslint', es_file), ('oxlint', ox_file)): + seconds = read_time(path) + if seconds is None: + print(f' {name:7} unknown (cached report or generated outside the wrapper -- rerun with `npm run compare-oxlint -- --fresh`)') + else: + print(f' {name:7} {seconds:8.1f} s') + + print() + ruleless = {'oxlint': ox_at.get(RULELESS, set()), 'eslint': es_at.get(RULELESS, set())} + if any(ruleless.values()): + print('MESSAGES WITH NO RULE ID -- not a rule finding, so no count comparison covers them:') + for linter, where in ruleless.items(): + for path, line in sorted(where)[:5]: + print(f' {linter:7} {path}:{line}') + if len(where) > 5: + print(f' {linter:7} ... and {len(where) - 5} more') + print() + + if not diffs and not misplaced: + print('Differences: none -- full parity on every rule, counts and locations.') + if diffs: + print(f'Differences ({len(diffs)} rules):') + for rule, a, b in diffs: + side = 'ESLINT-ONLY' if b == 0 else ('OXLINT-ONLY' if a == 0 else 'DIFF') + print(f' {rule:60} eslint={a:<6} oxlint={b:<6} {side}') + if misplaced: + print(f'Same count, different locations ({len(misplaced)} rules) -- equal totals hiding a real disagreement:') + for rule, es_only, ox_only in misplaced: + print(f' {rule}') + for where in es_only[:3]: + print(f' eslint only: {where[0]}:{where[1]}') + for where in ox_only[:3]: + print(f' oxlint only: {where[0]}:{where[1]}') + + +def config_coverage_check(): + ox_rules = ruleMap.oxlint_enabled_rules() + es_rules = ruleMap.eslint_enabled_rules() + + gaps = sorted(es_rules - ox_rules) + extras = sorted(ox_rules - es_rules) + print() + print(f'Config coverage (enabled rules, union across scopes): eslint={len(es_rules)}, oxlint={len(ox_rules)}, shared={len(es_rules & ox_rules)}') + print(f' (union taken over every tracked lintable file, {len(ruleMap.tracked_lintable_files())} of them)') + planned = [r for r in gaps if r in PORT_PLAN] + unexplained = [r for r in gaps if r not in PORT_PLAN] + if planned: + print(f' ESLint-only with a port plan ({len(planned)}) -- effort / mechanism:') + for rule in planned: + plan = PORT_PLAN[rule] + print(f' {rule:56} [{plan["effort"]:22}] {plan["mechanism"]}') + if unexplained: + print(f' ESLint-only, UNEXPLAINED ({len(unexplained)}) -- each is a silent coverage gap:') + for rule in unexplained: + print(f' {rule}') + else: + print(' No unexplained ESLint-only rules.') + if extras: + print(f' Oxlint-only extras ({len(extras)}, informational): {", ".join(extras[:10])}{" ..." if len(extras) > 10 else ""}') + + +if __name__ == '__main__': + findings_table( + sys.argv[1] if len(sys.argv) > 1 else '/tmp/oxlint-full.json', + sys.argv[2] if len(sys.argv) > 2 else '/tmp/eslint-full.json', + ) + config_coverage_check() diff --git a/oxlint-migration/compareFullRepo.sh b/oxlint-migration/compareFullRepo.sh new file mode 100755 index 000000000000..cd4bbfa6bdf5 --- /dev/null +++ b/oxlint-migration/compareFullRepo.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -eu + +OXLINT_JSON=/tmp/oxlint-full.json +ESLINT_JSON=/tmp/eslint-full.json + +if [[ "${1:-}" == "--fresh" ]]; then + rm -f "$OXLINT_JSON" "$ESLINT_JSON" "$OXLINT_JSON.time" "$ESLINT_JSON.time" +fi + +if [[ -s "$OXLINT_JSON" ]]; then + echo "Reusing $OXLINT_JSON (pass --fresh to regenerate)" +else + echo "Running oxlint (full repo, type-aware)..." + # Through the pipeline, not `npx oxlint`: both legs have to pass the same processors or the + # react-hooks rows compare a filtered report against an unfiltered one. + # Seatbelt off: its grandfathered debt is what this comparison measures. + SECONDS=0 + SEATBELT_DISABLE=1 \ + bun "$(dirname "$0")/../scripts/lint/index.ts" --linter=oxlint --format=json . \ + >"$OXLINT_JSON" || true + echo "$SECONDS" >"$OXLINT_JSON.time" +fi + +if [[ -s "$ESLINT_JSON" ]]; then + echo "Reusing $ESLINT_JSON (pass --fresh to regenerate)" +else + echo "Running ESLint (full repo, ~10 min)..." + # 16 GB heap + capped concurrency: with ESLINT_CONCURRENCY=auto the ESLint workers OOM + # (ERR_WORKER_OUT_OF_MEMORY) even on a 48 GB machine. + # Seatbelt off: its grandfathered debt is what this comparison measures. + SECONDS=0 + SEATBELT_DISABLE=1 NODE_OPTIONS=--max_old_space_size=16384 ESLINT_CONCURRENCY=2 \ + bun "$(dirname "$0")/../scripts/lint/index.ts" --linter=eslint --format=json --no-cache . \ + >"$ESLINT_JSON" || true + echo "$SECONDS" >"$ESLINT_JSON.time" +fi + +echo +python3 "$(dirname "$0")/compareFullRepo.py" "$OXLINT_JSON" "$ESLINT_JSON" diff --git a/oxlint-migration/compareNativeCtxValues.py b/oxlint-migration/compareNativeCtxValues.py new file mode 100644 index 000000000000..e4e269a3ff23 --- /dev/null +++ b/oxlint-migration/compareNativeCtxValues.py @@ -0,0 +1,229 @@ +"""What is wrong with oxlint's native react/jsx-no-constructed-context-values? + + python3 oxlint-migration/compareNativeCtxValues.py # whole repo, ~2 min + python3 oxlint-migration/compareNativeCtxValues.py src/pages # or a subtree, seconds + +Runs oxlint ONCE with two implementations of the same rule enabled, both switched on in a throwaway +copy of the real config: + + react/jsx-no-constructed-context-values oxlint's native Rust port (off in production) + esr/jsx-no-constructed-context-values ESLint's own rule, hosted through a jsPlugin, ungated + +The production config runs neither of these. It runs `hosted/jsx-no-constructed-context-values`, +ESLint's rule behind the React Compiler gate. The `esr` alias here is deliberately ungated, because +the point is to compare rule behaviour, not to reproduce what ships. + +and pairs their findings so the differences are readable rather than a wall of counts. ESLint's +message states BOTH lines it cares about ("(at line 53) ... (at line 58)"), so pairing is exact: +its second line is the provider, which is where native reports. + +Three things come out of it: + + 1. ANCHOR same finding, different reported line. ESLint points at the declaration and names + the variable; native points at the JSX provider with a generic message. This is + why the existing eslint-disable comments do not suppress native. + 2. EXTRA native reports, ESLint does not. ESLint requires the value to be declared inside + the component that renders it; native does not check the scope. + 3. SUPPRESSED ESLint findings that an existing disable comment already silences. Listed + separately so they are not read as differences. + +Note this rule is NOT at parity even where the two agree: ESLint's processor drops every message +from it in files both React compilers memoize, so ESLint reports 0 repo-wide today. See +"React Compiler checks" in OXLINT_MIGRATION_INVESTIGATION.md. +""" + +import collections +import json +import os +import re +import subprocess +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +CONFIG = os.path.join(ROOT, '.oxlintrc.measure-ctx-values.json') +RULE = 'react/jsx-no-constructed-context-values' +NATIVE_CODE = 'react(jsx-no-constructed-context-values)' +ESLINT_CODE = 'esr(jsx-no-constructed-context-values)' + +# Text patch rather than an `extends` wrapper: oxlint's `extends` does not inherit ignorePatterns, +PLUGIN_ANCHOR = '{"name": "rh", "specifier": "./config/oxlint/plugins/rh-plugin.mjs"}' +PLUGIN_ENTRY = PLUGIN_ANCHOR + ',\n {"name": "esr", "specifier": "./oxlint-migration/eslint-ctx-values-rule.mjs"}' + + +def write_config(): + text = open(os.path.join(ROOT, '.oxlintrc.json')).read() + native_off = f'"{RULE}": "off"' + if text.count(native_off) != 1: + sys.exit(f'expected exactly one `{native_off}` in .oxlintrc.json, found {text.count(native_off)}') + text = text.replace(PLUGIN_ANCHOR, PLUGIN_ENTRY) + text = text.replace(native_off, f'"{RULE}": "error",\n "esr/jsx-no-constructed-context-values": "error"') + open(CONFIG, 'w').write(text) + + +def run_oxlint(targets): + command = ['npx', 'oxlint', '-c', os.path.basename(CONFIG), '--format', 'json', *targets] + result = subprocess.run(command, capture_output=True, text=True, cwd=ROOT) + try: + return json.loads(result.stdout)['diagnostics'] + except (json.JSONDecodeError, KeyError): + sys.exit(f'oxlint run failed:\n{result.stdout[:600]}{result.stderr[:600]}') + + +SOURCE = {} + + +def lines_of(path): + if path not in SOURCE: + try: + SOURCE[path] = open(os.path.join(ROOT, path)).read().splitlines() + except OSError: + SOURCE[path] = [] + return SOURCE[path] + + +def source(path, line): + text = lines_of(path) + return text[line - 1].strip() if 0 < line <= len(text) else '' + + +def is_suppressed(path, line): + """Does an existing eslint-disable comment already silence a finding on this line?""" + text = lines_of(path) + if any(re.search(r'/\*\s*eslint-disable\s+' + re.escape(RULE), one) for one in text): + return True + return line >= 2 and RULE in text[line - 2] and 'eslint-disable' in text[line - 2] + + +def declaration_of(path, provider_line): + """The identifier passed as `value`, and where it is declared. None for inline literals.""" + text = lines_of(path) + window = ' '.join(text[provider_line - 1:provider_line + 2]) + match = re.search(r'value=\{(\w+)\}', window) + if not match: + return None, None + name = match.group(1) + pattern = re.compile(r'\b(?:const|let|var|function)\s+' + re.escape(name) + r'\b') + for index in range(min(provider_line, len(text)) - 1, -1, -1): + if pattern.search(text[index]): + return name, index + 1 + return name, None + + +def enclosing_function(path, line): + """Nearest function header above `line`, as (name, line). Regex, so treat it as a hint.""" + text = lines_of(path) + pattern = re.compile(r'^\s*(?:export\s+(?:default\s+)?)?(?:function\s+(\w+)|const\s+(\w+)\s*=\s*(?:\([^)]*\)|\w+)\s*=>|const\s+(\w+)\s*=\s*function)') + for index in range(min(line, len(text)) - 1, -1, -1): + match = pattern.match(text[index]) + if match: + return next(group for group in match.groups() if group), index + 1 + return None, None + + +def collect(diagnostics): + """native: {(file, provider_line)}; eslint: {(file, provider_line): declaration_line}.""" + native, eslint = set(), {} + for diagnostic in diagnostics: + code = diagnostic.get('code') or '' + labels = diagnostic.get('labels') or [] + line = labels[0]['span']['line'] if labels else 0 + path = diagnostic['filename'] + if code == NATIVE_CODE: + native.add((path, line)) + elif code == ESLINT_CODE: + # ESLint's message cites the object and the provider ("The 'x' object (at line 53) passed + # as the value prop to the Context provider (at line 58)"); one `at line` means an inline + # literal, and then the reported line IS the provider. + cited = [int(value) for value in re.findall(r'\(at line (\d+)\)', diagnostic.get('message', ''))] + provider = cited[-1] if cited else line + eslint[(path, provider)] = line + return native, eslint + + +def main(targets): + write_config() + try: + native, eslint = collect(run_oxlint(targets)) + finally: + os.remove(CONFIG) + + anchor, agree, suppressed, extra = [], [], [], [] + for key in sorted(eslint): + path, provider = key + declaration = eslint[key] + if is_suppressed(path, declaration): + suppressed.append((path, declaration, provider, key not in native)) + elif key not in native: + continue + elif declaration == provider: + agree.append((path, provider)) + else: + anchor.append((path, declaration, provider)) + missed = sorted(key for key in eslint if key not in native and not is_suppressed(key[0], eslint[key])) + extra = sorted(key for key in native if key not in eslint) + + print(f'\nnative {len(native)} findings / {len({f for f, _ in native})} files' + f' ESLint rule {len(eslint)} findings / {len({f for f, _ in eslint})} files\n') + + print('=' * 100) + print(f'1. ANCHOR -- same finding, different line ({len(anchor)})') + print(' ESLint points at the declaration and names the variable. Native points at the JSX') + print(' provider. An eslint-disable comment written above the declaration misses native.') + print('=' * 100) + for path, declaration, provider in anchor: + print(f'\n{path}') + print(f' eslint {declaration:5} | {source(path, declaration)[:110]}') + print(f' native {provider:5} | {source(path, provider)[:110]}') + + print('\n' + '=' * 100) + print(f'2. EXTRA -- native reports, ESLint does not ({len(extra)} findings, ' + f'{len({f for f, _ in extra})} files)') + print(' ESLint only reports a value declared INSIDE the component that renders it.') + print('=' * 100) + for path, provider in extra: + name, declared = declaration_of(path, provider) + function_name, function_line = enclosing_function(path, provider) + print(f'\n{path}') + print(f' native {provider:5} | {source(path, provider)[:110]}') + if name and declared: + where = f'declared at line {declared}' + if function_line and declared < function_line: + where += f', outside `{function_name}` which starts at line {function_line}' + print(f' `{name}` {where}') + print(f' {declared:12} | {source(path, declared)[:110]}') + elif name: + print(f' `{name}` is not declared in this file (imported or a parameter)') + else: + print(' value is an inline literal here -- both rules should agree, so this one is worth a look') + + if missed: + print('\n' + '=' * 100) + print(f'2b. ESLint reports, native does NOT ({len(missed)}) -- there were none when this was written') + print('=' * 100) + for path, provider in missed: + print(f'\n{path}') + print(f' eslint {eslint[(path, provider)]:5} | {source(path, eslint[(path, provider)])[:110]}') + + print('\n' + '=' * 100) + print(f'3. Findings an existing eslint-disable comment already silences ({len(suppressed)})') + print(' "silences native too" is False wherever the comment sits above the declaration but') + print(' native reports on the provider line, which is the anchor difference biting.') + print('=' * 100) + for path, declaration, provider, silences_native in suppressed: + print(f' eslint {declaration:5} native {provider:5} silences native too: {str(silences_native):5} {path}') + + print('\n' + '=' * 100) + print('Summary') + print('=' * 100) + print(f' agree on file AND line : {len(agree)}') + print(f' agree on the finding, differ on line: {len(anchor)}') + print(f' native-only (false positives) : {len(extra)}') + print(f' ESLint-only (native missed) : {len(missed)}') + print(f' already suppressed by a comment : {len(suppressed)}, of which ' + f'{sum(1 for row in suppressed if not row[3])} do NOT suppress native') + by_area = collections.Counter('tests/' if f.startswith('tests/') else f.split('/')[0] + '/' for f, _ in extra) + print(f' native-only by area : {dict(by_area)}') + + +if __name__ == '__main__': + main(sys.argv[1:] or ['.']) diff --git a/oxlint-migration/compareWarmCache.sh b/oxlint-migration/compareWarmCache.sh new file mode 100755 index 000000000000..ec07f95a77b1 --- /dev/null +++ b/oxlint-migration/compareWarmCache.sh @@ -0,0 +1,164 @@ +#!/bin/bash +# Time the required lint check both ways, with ESLint's cache in the same warm state CI sees. +# +# CI restores node_modules/.cache/eslint from an earlier run, so on a normal PR every file is a +# cache hit except the ones the PR touched. This reproduces that by priming the cache with the +# dirty files held back, snapshotting it, then timing a whole-repo run against the restored +# snapshot. oxlint has no cache, so it is timed once as-is. +# +# ./oxlint-migration/compareWarmCache.sh # 10 files changed vs the base branch +# ./oxlint-migration/compareWarmCache.sh src/pages/Foo.tsx # these exact files are dirty +# ./oxlint-migration/compareWarmCache.sh --dirty=0 # every file cached (best case for ESLint) +# ./oxlint-migration/compareWarmCache.sh --dirty=branch # every file changed vs the base branch +# ./oxlint-migration/compareWarmCache.sh --runs=3 # repeat the warm run, report each +# ./oxlint-migration/compareWarmCache.sh --fresh # re-prime even if a snapshot exists +# +# Pick the dirty files deliberately. `--dirty=N` takes the first N paths alphabetically, which on a +# config-heavy branch is all .mjs and skips the TypeScript program the type-aware rules load, so it +# reads faster than a real PR. Name a couple of src/**/*.tsx files to match one. +set -euo pipefail + +ROOT_DIR=$(dirname "$(dirname "$(realpath "${BASH_SOURCE[0]}")")") +cd "$ROOT_DIR" + +CACHE=node_modules/.cache/eslint +SNAPSHOT=node_modules/.cache/eslint.warm-snapshot +STAMP=node_modules/.cache/eslint.warm-stamp +LOG_DIR=/tmp/oxlint-warm-compare +BASE_REF=${BASE_REF:-origin/main} + +DIRTY_SPEC=10 +RUNS=1 +FRESH=0 +EXPLICIT_FILES=() +for arg in "$@"; do + case "$arg" in + --dirty=*) DIRTY_SPEC="${arg#*=}" ;; + --runs=*) RUNS="${arg#*=}" ;; + --fresh) FRESH=1 ;; + --*) + echo "unknown option: $arg" >&2 + exit 1 + ;; + *) + if [[ ! -f "$arg" ]]; then + echo "not a file: $arg" >&2 + exit 1 + fi + EXPLICIT_FILES+=("$arg") + ;; + esac +done +if [[ ! "$RUNS" =~ ^[1-9][0-9]*$ ]]; then + echo "--runs must be a positive integer" >&2 + exit 1 +fi +if [[ ! "$DIRTY_SPEC" =~ ^([0-9]+|branch)$ ]]; then + echo "--dirty must be a number or 'branch'" >&2 + exit 1 +fi + +mkdir -p "$LOG_DIR" + +# The dirty set: the files a hypothetical PR touched, so the ones CI's restored cache would miss. +DIRTY_FILES=() +if [[ ${#EXPLICIT_FILES[@]} -gt 0 ]]; then + DIRTY_FILES=("${EXPLICIT_FILES[@]}") +elif [[ "$DIRTY_SPEC" != "0" ]]; then + if ! git rev-parse --verify --quiet "$BASE_REF" >/dev/null; then + echo "$BASE_REF not found; set BASE_REF to an existing ref" >&2 + exit 1 + fi + CHANGED=$(git diff --name-only --diff-filter=d "$BASE_REF"...HEAD -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.mjs' '*.cjs' | sort) + if [[ -z "$CHANGED" ]]; then + echo "no lintable files changed vs $BASE_REF; use --dirty=0" >&2 + exit 1 + fi + if [[ "$DIRTY_SPEC" != "branch" ]]; then + CHANGED=$(printf '%s\n' "$CHANGED" | head -n "$DIRTY_SPEC") + fi + while IFS= read -r file; do + DIRTY_FILES+=("$file") + done <<<"$CHANGED" +fi + +# Match the flags scripts/lint/eslint/ESLintLinter.ts spawns ESLint with: a flag that changes which +# rules run is a different cache, and the prime is wasted. Notably no `--quiet` -- the pipeline +# deliberately does not pass it (ESLintLinter.ts:111-118), because warnings have to reach the +# formatter for seatbelt to demote grandfathered errors into them first. `--format` is left off as +# the one intentional difference: it changes nothing ESLint computes, and the prime log stays +# readable. +# Concurrency and heap default to what the CI job sets (.github/workflows/lint.yml:83-84) rather than +# what the runner defaults to, both because CI is what this reproduces and because +# `--concurrency=auto` gives every core its own worker at ~12 GB of type-aware program each, which +# the OS kills long before the run finishes. compareFullRepo.sh hit the same wall. +export ESLINT_CONCURRENCY="${ESLINT_CONCURRENCY:-2}" +export NODE_OPTIONS="${NODE_OPTIONS:---max_old_space_size=14336}" +export SEATBELT_FROZEN="${SEATBELT_FROZEN:-0}" +ESLINT_FLAGS=(--cache --cache-location="$CACHE" --cache-strategy content + "--concurrency=$ESLINT_CONCURRENCY" --no-warn-ignored) + +STAMP_VALUE=$(printf '%s\n' ${DIRTY_FILES[@]+"${DIRTY_FILES[@]}"} | shasum | cut -d' ' -f1) +if [[ "$FRESH" == 1 || ! -s "$SNAPSHOT" || $(cat "$STAMP" 2>/dev/null || true) != "$STAMP_VALUE" ]]; then + IGNORE_FLAGS=() + for file in ${DIRTY_FILES[@]+"${DIRTY_FILES[@]}"}; do + IGNORE_FLAGS+=(--ignore-pattern "$file") + done + echo "Priming ESLint cache with ${#DIRTY_FILES[@]} file(s) held back (cold, several minutes)..." + rm -f "$CACHE" + SECONDS=0 + npx eslint . "${ESLINT_FLAGS[@]}" ${IGNORE_FLAGS[@]+"${IGNORE_FLAGS[@]}"} >"$LOG_DIR/prime.log" 2>&1 || true + COLD_SECONDS=$SECONDS + # ESLint exits non-zero on findings, which is fine, but it writes no cache if it was killed + # part-way. Without this check the run below would silently time a cold lint as a warm one. + if [[ ! -s "$CACHE" ]]; then + echo "prime wrote no cache after ${COLD_SECONDS}s; it was killed. See $LOG_DIR/prime.log" >&2 + echo "lower the worker count, e.g. ESLINT_CONCURRENCY=1 $0 $*" >&2 + exit 1 + fi + echo "$COLD_SECONDS" >"$LOG_DIR/prime.time" + cp "$CACHE" "$SNAPSHOT" + echo "$STAMP_VALUE" >"$STAMP" +else + COLD_SECONDS=$(cat "$LOG_DIR/prime.time" 2>/dev/null || echo '?') + echo "Reusing primed cache snapshot (pass --fresh to re-prime)" +fi + +# A timed run writes the dirty files into the cache, so restore the snapshot before each one. +WARM_TIMES=() +for ((i = 1; i <= RUNS; i++)); do + cp "$SNAPSHOT" "$CACHE" + echo "Warm ESLint run $i/$RUNS (npm run lint, whole repo)..." + SECONDS=0 + npm run lint >"$LOG_DIR/warm-$i.log" 2>&1 || true + WARM_TIMES+=("$SECONDS") +done + +# `--quiet` matches how the ESLint side ends up reporting: the pipeline's formatter drops warnings +# unless --show-warnings is passed. It is a no-op while every oxlint finding here is an error, and +# becomes the matching flag if the seatbelt counterpart lands as a downgrade to warning. Set +# OXLINT_SILENT=1 to drop diagnostic output entirely, which is what the check costs once the findings +# are baselined rather than printed 4498 at a time. +OXLINT_FLAGS=(--type-aware --quiet) +if [[ -n "${OXLINT_SILENT:-}" ]]; then + OXLINT_FLAGS+=(--silent) +fi +echo "oxlint run (whole repo, ${OXLINT_FLAGS[*]}, no cache)..." +SECONDS=0 +npx oxlint "${OXLINT_FLAGS[@]}" >"$LOG_DIR/oxlint.log" 2>&1 || true +OXLINT_SECONDS=$SECONDS + +echo +echo "dirty files: ${#DIRTY_FILES[@]} base ref: $BASE_REF logs: $LOG_DIR" +printf 'ESLint cold prime %6ss\n' "$COLD_SECONDS" +for ((i = 1; i <= RUNS; i++)); do + printf 'ESLint warm run %-2s %6ss\n' "$i" "${WARM_TIMES[i - 1]}" +done +printf 'oxlint %6ss\n' "$OXLINT_SECONDS" +if [[ "$OXLINT_SECONDS" -gt 0 ]]; then + BEST_WARM=$(printf '%s\n' "${WARM_TIMES[@]}" | sort -n | head -1) + awk -v w="$BEST_WARM" -v o="$OXLINT_SECONDS" 'BEGIN {printf "warm ESLint / oxlint = %.2fx\n", w / o}' + if [[ "$COLD_SECONDS" != '?' ]]; then + awk -v c="$COLD_SECONDS" -v o="$OXLINT_SECONDS" 'BEGIN {printf "cold ESLint / oxlint = %.2fx\n", c / o}' + fi +fi diff --git a/oxlint-migration/eslint-ctx-values-rule.mjs b/oxlint-migration/eslint-ctx-values-rule.mjs new file mode 100644 index 000000000000..3a08b72211f4 --- /dev/null +++ b/oxlint-migration/eslint-ctx-values-rule.mjs @@ -0,0 +1,14 @@ +import {createRequire} from 'node:module'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +const require = createRequire(import.meta.url); +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +const react = require(path.resolve(repoRoot, 'node_modules/eslint-plugin-react')); +const rules = react?.rules ?? react?.default?.rules ?? {}; + +export default { + meta: {name: 'esr', version: '0.0.1'}, + rules: {'jsx-no-constructed-context-values': rules['jsx-no-constructed-context-values']}, +}; diff --git a/oxlint-migration/eslintEnabledRules.mjs b/oxlint-migration/eslintEnabledRules.mjs new file mode 100644 index 000000000000..37c91bed7d06 --- /dev/null +++ b/oxlint-migration/eslintEnabledRules.mjs @@ -0,0 +1,39 @@ +import {ESLint} from 'eslint'; +/** + * Every rule the real ESLint config enables, over whatever file list arrives on stdin. + * + * One process for the whole repo. `npx eslint --print-config` spends ~1.3s of node startup per + * file, so resolving 9k files that way is a three-hour job; the same work through the ESLint API + * in a single process takes about five seconds. + * + * Reads one path per line on stdin, writes {"files": N, "rules": [...]} on stdout. + */ +import {readFileSync} from 'node:fs'; + +const files = readFileSync(0, 'utf8') + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + +const eslint = new ESLint({cwd: process.cwd()}); +const enabled = new Set(); +let resolved = 0; + +for (const file of files) { + // Returns undefined (older ESLint threw) for a path the config does not cover, which over a + // whole-repo sweep just means the file is not linted. + // eslint-disable-next-line no-await-in-loop + const config = await eslint.calculateConfigForFile(file).catch(() => null); + if (!config) { + continue; + } + resolved++; + for (const [ruleID, value] of Object.entries(config.rules ?? {})) { + const severity = Array.isArray(value) ? value[0] : value; + if (severity !== 'off' && severity !== 0) { + enabled.add(ruleID); + } + } +} + +process.stdout.write(JSON.stringify({files: resolved, rules: [...enabled].sort()})); diff --git a/oxlint-migration/listAllRules.py b/oxlint-migration/listAllRules.py new file mode 100644 index 000000000000..5a3e0f18466e --- /dev/null +++ b/oxlint-migration/listAllRules.py @@ -0,0 +1,247 @@ +"""Inventory of every lint rule this repo runs, on either tool. + + python3 oxlint-migration/listAllRules.py [--json oxlint-migration/rule-inventory.json] + python3 oxlint-migration/listAllRules.py --available [--all] [--json ] + +Default mode: rules this repo *enables*, per tool. Unlike compareFullRepo.py, which can +only see rules that currently produce findings, this walks both configs and lists every +enabled rule -- shared, ESLint-only and oxlint-only -- with the data needed to build a +per-rule fixture suite: + + status shared / eslint-only / oxlint-only + native does oxlint implement the rule itself (vs. hosting ESLint's via jsPlugins) + type-aware does the rule need TypeScript types (tsgolint), and does tsgolint have it + findings current violation counts from the cached full-repo reports, when present + fixture the example file that proves the rule behaves the same on both tools + +`--available` mode answers a different question: for the rules ESLint enforces, does +oxlint even have a rule for it? ESLint's side is read from the real flat config's registered +plugins plus its core rules; oxlint's side is its native catalogue plus everything its +jsPlugins host. Rules ESLint has installed but switched off are hidden -- they need no +migration decision -- so the interesting output is the "available in oxlint but not enabled +there yet" list. Pass `--all` to see everything, including oxlint-only rules. + +The JSON dump is the machine-readable input for the fixture harness in +oxlint-migration/port-probe (see compareFixtures.py). +""" + +import collections +import glob +import json +import os +import sys + +import ruleMap +from ruleMap import ROOT + +FIXTURE_MANIFEST = os.path.join(ROOT, 'oxlint-migration/port-probe/fixtures.manifest.json') + + +def load_findings(): + """Per-rule finding counts from the cached full-repo reports (empty when absent).""" + counts = {} + # Both legs are written by `scripts/lint --format json`, so each report is one flat LintMessage + # list keyed by `ruleID`. OxlintLinter already renames every oxlint code to its ESLint identity, + # so the oxlint side is read with norm_es too, not norm_ox. + for linter, path in (('oxlint', '/tmp/oxlint-full.json'), ('eslint', '/tmp/eslint-full.json')): + if not os.path.exists(path): + continue + report = json.load(open(path)) + counts[linter] = collections.Counter(ruleMap.norm_es(m.get('ruleID')) for m in report['messages']) + return counts + + +def load_fixtures(): + """fixtures.manifest.json plus every fragments/*.manifest.json, the same merge compareFixtures does. + + Reading only the base manifest under-reports coverage by the whole sharded campaign: the batches + land as fragments, so a rule proven by one of them looked uncovered here. + """ + if not os.path.exists(FIXTURE_MANIFEST): + return {} + manifest = json.load(open(FIXTURE_MANIFEST)) + for path in sorted(glob.glob(os.path.join(os.path.dirname(FIXTURE_MANIFEST), 'fragments', '*.manifest.json'))): + manifest.update(json.load(open(path))) + return manifest + + +def dump_json(inventory, default_name='rule-inventory.json'): + """Writes the inventory when --json is passed (with an optional explicit path).""" + if '--json' not in sys.argv: + return + index = sys.argv.index('--json') + candidate = sys.argv[index + 1] if len(sys.argv) > index + 1 else None + out = candidate if candidate and not candidate.startswith('-') else os.path.join(ROOT, 'oxlint-migration', default_name) + json.dump(inventory, open(out, 'w'), indent=2, sort_keys=True) + print(f'Wrote {out}') + + +def build(): + es_rules = ruleMap.eslint_enabled_rules() + ox_rules = ruleMap.oxlint_enabled_rules() + catalogue = ruleMap.oxlint_catalogue() + hosted = ruleMap.js_plugin_rules() + type_aware = ruleMap.tsgolint_rules() + findings = load_findings() + fixtures = load_fixtures() + + inventory = {} + for rule in sorted(es_rules | ox_rules): + in_es, in_ox = rule in es_rules, rule in ox_rules + status = 'shared' if in_es and in_ox else ('eslint-only' if in_es else 'oxlint-only') + inventory[rule] = { + 'status': status, + 'eslint': in_es, + 'oxlint': in_ox, + 'jsPlugin': hosted.get(rule), + 'inOxlintSchema': rule in catalogue, + 'typeAware': rule in type_aware, + 'typeAwareImplemented': type_aware.get(rule), + 'lowValue': rule in ruleMap.KNOWN_NOT_IMPLEMENTED_LOW_VALUE, + 'findingsEslint': findings.get('eslint', {}).get(rule, 0), + 'findingsOxlint': findings.get('oxlint', {}).get(rule, 0), + 'fixture': fixtures.get(rule, {}).get('fixture'), + 'portPlan': ruleMap.PORT_PLAN.get(rule), + } + return inventory + + +def flag(entry): + """How the rule runs on the oxlint side (or what it would take to get it there).""" + if entry['status'] == 'eslint-only': + plan = entry['portPlan'] + return plan['effort'] if plan else ('low-value' if entry['lowValue'] else 'UNPLANNED') + if entry['jsPlugin']: + return f'js:{entry["jsPlugin"]}' + # a schema miss is not proof of absence -- oxlint 1.77's schema omits whole working plugins + return 'native' if entry['inOxlintSchema'] else 'native?' + + +def build_available(): + """Installed-rule inventory: what each tool *has*, independent of the repo's configs.""" + es_installed = ruleMap.eslint_installed_rules() + ox_available = ruleMap.oxlint_available_rules() + es_enabled = ruleMap.eslint_enabled_rules(fold_extension_rules=False) + ox_enabled = ruleMap.oxlint_enabled_rules() + ox_disabled = ruleMap.oxlint_disabled_rules() + + def counterpart(rule): + if rule in ox_available: + return rule + if rule.startswith('@typescript-eslint/'): + base = rule.split('/', 1)[1] + if base in ruleMap.TS_EXTENSION_RULES and base in ox_available: + return base + return None + + inventory = {} + for rule, source in sorted(es_installed.items()): + match = counterpart(rule) + inventory[rule] = { + 'eslintSource': source, + 'oxlintRule': match, + 'oxlintSource': ox_available.get(match) if match else None, + 'eslintEnabled': rule in es_enabled, + 'oxlintEnabled': (match in ox_enabled) if match else False, + 'oxlintOffIn': ox_disabled.get(match) if match else None, + 'portPlan': ruleMap.PORT_PLAN.get(rule), + } + matched = {entry['oxlintRule'] for entry in inventory.values() if entry['oxlintRule']} + for rule, source in sorted(ox_available.items()): + if rule in matched or rule in inventory: + continue + inventory[rule] = { + 'eslintSource': None, + 'oxlintRule': rule, + 'oxlintSource': source, + 'eslintEnabled': False, + 'oxlintEnabled': rule in ox_enabled, + 'oxlintOffIn': ox_disabled.get(rule), + 'portPlan': None, + } + return inventory + + +def print_available(inventory, show_all=False): + """Prints the availability comparison. + + By default only rules ESLint actually enables are listed: a rule ESLint has installed + but switched off needs no migration decision at all. `--all` restores the full list + (adds the switched-off ESLint rules and the oxlint rules ESLint has no equivalent for). + """ + hidden = 0 if show_all else sum(1 for e in inventory.values() if not e['eslintEnabled']) + rows = {rule: entry for rule, entry in sorted(inventory.items()) if show_all or entry['eslintEnabled']} + + header = f'{"rule":58} {"eslint has":14} {"oxlint has":22} {"oxlint enabled":14}' + print(header) + print('-' * len(header)) + for rule, entry in rows.items(): + state = 'yes' if entry['oxlintEnabled'] else ('available' if entry['oxlintSource'] else 'no rule') + print(f'{rule:58} {entry["eslintSource"] or "-":14} {entry["oxlintSource"] or "-":22} {state:14}') + + both = [r for r, e in rows.items() if e['eslintSource'] and e['oxlintSource']] + es_only = [r for r, e in rows.items() if e['eslintSource'] and not e['oxlintSource']] + ox_only = [r for r, e in rows.items() if not e['eslintSource'] and e['oxlintSource']] + print() + scope = 'installed' if show_all else 'rules ESLint enables' + print(f'{scope.capitalize()}: {len(rows)} total -- oxlint has {len(both)}, oxlint has no counterpart for {len(es_only)}') + if ox_only: + print(f' Oxlint-only (oxlint can run, ESLint has no such rule): {len(ox_only)}') + if hidden: + print(f' Hidden: {hidden} rules ESLint has installed but does not enable (pass --all to show them)') + + available_not_on = [r for r, e in rows.items() if e['eslintEnabled'] and e['oxlintSource'] and not e['oxlintEnabled']] + if available_not_on: + print(f' Enabled in ESLint, available in oxlint, NOT enabled there yet ({len(available_not_on)}) -- the actionable list.') + print(' Each is off for a specific reason, never because it currently reports nothing:') + for rule in available_not_on: + entry = inventory[rule] + plan = entry['portPlan'] or {} + marked = f'off in {entry["oxlintOffIn"]}' if entry['oxlintOffIn'] else 'NOT IN CONFIG' + print(f' {rule:50} {entry["oxlintSource"]:12} {marked:16} [{plan.get("effort", "?")}] {plan.get("mechanism", "no PORT_PLAN entry")}') + if es_only: + by_prefix = collections.Counter(r.split('/')[0] if '/' in r else '' for r in es_only) + print(' No oxlint counterpart, grouped by plugin:') + for prefix, count in by_prefix.most_common(): + print(f' {prefix:36} {count}') + return rows + + +def main(): + if '--available' in sys.argv: + inventory = build_available() + rows = print_available(inventory, show_all='--all' in sys.argv) + dump_json(rows, default_name='rule-availability.json') + return + inventory = build() + by_status = collections.Counter(e['status'] for e in inventory.values()) + + header = f'{"rule":58} {"status":12} {"impl":12} {"type?":6} {"eslint":>7} {"oxlint":>7} fixture' + print(header) + print('-' * len(header)) + for rule, entry in inventory.items(): + type_col = '' if not entry['typeAware'] else ('type' if entry['typeAwareImplemented'] else 'type!') + print( + f'{rule:58} {entry["status"]:12} {flag(entry):12} {type_col:6} ' + f'{entry["findingsEslint"]:7} {entry["findingsOxlint"]:7} {entry["fixture"] or ""}' + ) + + print() + print(f'Totals: {len(inventory)} rules -- ' + ', '.join(f'{k}={v}' for k, v in sorted(by_status.items()))) + by_impl = collections.Counter(flag(e) for e in inventory.values() if e['status'] != 'eslint-only') + print('Oxlint side: ' + ', '.join(f'{k}={v}' for k, v in sorted(by_impl.items()))) + covered = sum(1 for e in inventory.values() if e['fixture']) + unproven = sum(1 for e in inventory.values() if flag(e) == 'native?' and not e['fixture']) + print(f'Fixture coverage: {covered}/{len(inventory)} rules have an example file') + print(f"Rules missing from oxlint's schema and without a fixture (unproven, not necessarily broken): {unproven}") + unplanned = [r for r, e in inventory.items() if flag(e) == 'UNPLANNED'] + if unplanned: + print(f'ESLint-only rules with no port plan ({len(unplanned)}): {", ".join(unplanned)}') + else: + print('Every ESLint-only rule has a port plan or is classified low-value.') + + dump_json(inventory) + + +if __name__ == '__main__': + main() diff --git a/oxlint-migration/native-vs-sidecar-probe/Counter.tsx b/oxlint-migration/native-vs-sidecar-probe/Counter.tsx new file mode 100644 index 000000000000..14d7cf990d2a --- /dev/null +++ b/oxlint-migration/native-vs-sidecar-probe/Counter.tsx @@ -0,0 +1,24 @@ +import {useEffect, useRef, useState} from 'react'; + +function Counter({start}: {start: number}) { + const [count, setCount] = useState(start); + const ref = useRef(0); + + // BUG A: reading a ref while rendering. + const doubled = ref.current * 2; + + // BUG B: calling setState straight from an effect. + useEffect(() => { + setCount(start); + }, [start]); + + // Not a bug we care about here. One missing dependency, silenced the normal way. + useEffect(() => { + console.log(count); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return
{doubled}
; +} + +export default Counter; diff --git a/oxlint-migration/native-vs-sidecar-probe/NonAscii.tsx b/oxlint-migration/native-vs-sidecar-probe/NonAscii.tsx new file mode 100644 index 000000000000..83ff9b105e26 --- /dev/null +++ b/oxlint-migration/native-vs-sidecar-probe/NonAscii.tsx @@ -0,0 +1,24 @@ +import {useRef} from 'react'; + +// The point of this fixture is the non-ASCII above the bug, not the copy itself. Every character +// here is more than one UTF-8 byte, so oxc's byte offset for the ref read below is well ahead of +// the same position counted in UTF-16 code units: 😀 is four bytes and two units, é is two bytes +// and one unit, and 日本語 and € are three bytes each. +const LABEL = '😀😀😀😀😀😀😀😀 café 日本語 € ⚠️'; + +function NonAscii() { + const ref = useRef(0); + + // BUG: reading a ref while rendering. This line, and only this line, is what rc/refs has to + // report. Counting the offset above in UTF-16 units instead of bytes lands several lines past it. + const doubled = ref.current * 2; + + return ( +
+ {LABEL} + {doubled} +
+ ); +} + +export default NonAscii; diff --git a/oxlint-migration/native-vs-sidecar-probe/README.md b/oxlint-migration/native-vs-sidecar-probe/README.md new file mode 100644 index 000000000000..45bafee6abb2 --- /dev/null +++ b/oxlint-migration/native-vs-sidecar-probe/README.md @@ -0,0 +1,141 @@ +# Why we do not swap to Oxlint's native React Compiler rules + +**Status, 2026-08-25.** The two configs the commands below use (`native.oxlintrc.json` and +`sidecar.oxlintrc.json`) were deleted: `rh/*` no longer exists, and Oxlint 1.79.0 replaced the aggregate +rule this compared against. The commands are kept as a record of how the measurement was run, not as +something to re-run. To run the 12 React Compiler rules in isolation today, use +`npx oxlint -c oxlint-migration/rc-rust-probe.oxlintrc.json --no-ignore `. The three `.tsx` files +here are still live inputs to `npm run oxlint-react-compiler-rust`. + +Oxlint 1.79.0 ships native Rust ports of the React Compiler checks. Swapping our Node sidecar (`rh/*`) +for them looks like free speed. This directory is the demo of what it actually costs, and of the fact +that the cause is Oxlint's port rather than anything in our setup. + +## The behavior, in two commands + +`Counter.tsx` is one component with two real bugs (a ref read during render on line 8, a `setState` +inside an effect on line 12) and one unrelated `// eslint-disable-next-line react-hooks/exhaustive-deps` +on line 18. + +```bash +# Oxlint's native Rust rules +npx oxlint -c oxlint-migration/native-vs-sidecar-probe/native.oxlintrc.json \ + oxlint-migration/native-vs-sidecar-probe/Counter.tsx + +# The Node sidecar we run in production, i.e. the same rule modules ESLint runs +npx oxlint -c oxlint-migration/native-vs-sidecar-probe/sidecar.oxlintrc.json \ + oxlint-migration/native-vs-sidecar-probe/Counter.tsx +``` + +Native reports one thing, `react/rule-suppression` on line 18, and neither bug. The sidecar reports both +bugs. Delete that one comment and native reports both bugs too, which is the proof that the rules work +and the comment is what silences them: + +```bash +grep -v "eslint-disable-next-line react-hooks/exhaustive-deps" \ + oxlint-migration/native-vs-sidecar-probe/Counter.tsx > /tmp/CounterNoComment.tsx +npx oxlint -c oxlint-migration/native-vs-sidecar-probe/native.oxlintrc.json /tmp/CounterNoComment.tsx +``` + +## The unit is the function, not the file + +`Counter.tsx` cannot tell those apart, because everything in it lives in one component. `TwoComponents.tsx` +can: `Dirty` carries the comment and a bug, `Clean` carries no comment and two bugs. + +```bash +npx oxlint -c oxlint-migration/native-vs-sidecar-probe/native.oxlintrc.json \ + oxlint-migration/native-vs-sidecar-probe/TwoComponents.tsx +``` + +| | Oxlint native | ESLint + eslint-plugin-react-hooks 7.1.1 | +| --- | --- | --- | +| `Dirty`, ref read during render | **not reported** | reported | +| `Clean`, ref read during render | reported | reported | +| `Clean`, `setState` in effect | reported | reported | + +So a suppression comment blinds its **enclosing component**, not the whole file. This matches Oxlint's +[rule doc](https://oxc.rs/docs/guide/usage/linter/rules/react/rule-suppression.html) ("the whole function +loses optimization") and `filter_suppressions_that_affect_function` in +`crates/oxc_react_compiler/src/react_compiler/entrypoint/suppression.rs`. + +It looks file-wide in this repo constantly, because a component is very often the whole file: +`src/components/Search/index.tsx` is a single `function Search({` from line 128 to past line 1060, and all +3 of its suppression comments and all 8 of its compiler findings sit inside it. + +## What the swap costs, measured on the whole repo + +Native `react/*` for the 12 twins enabled everywhere, `rh/*` twins off, `--type-aware`, compared as +`(rule, file, line)` against the sidecar we ship: + +| rule | sidecar | native | lost | native-only | +| --- | --- | --- | --- | --- | +| `refs` | 148 | 108 | 45 | 5 | +| `set-state-in-effect` | 129 | 69 | 60 | 0 | +| `preserve-manual-memoization` | 13 | 6 | 11 | 4 | +| `immutability` | 6 | 3 | 4 | 1 | +| `static-components` | 2 | 1 | 1 | 0 | +| **total** | **298** | **187** | **121** | **10** | + +**121 findings lost across 58 files**, and it breaks parity in both directions: native also reports 10 +findings ESLint never shows. + +## Proof it is Oxlint's Rust port, not our setup + +Everything above runs inside this repo, so it cannot by itself rule out our config, our jsPlugins or our +custom rules. This can. Reproduce from scratch in an empty directory **outside** the repo: + +```bash +mkdir /tmp/oxlint-isolation && cd /tmp/oxlint-isolation +npm init -y && npm install oxlint@1.79.0 # 2 packages, no patches, no plugins of ours +cat > .oxlintrc.json <<'JSON' +{"plugins": ["react"], "categories": {}, + "rules": {"react/refs": "error", "react/set-state-in-effect": "error", "react/rule-suppression": "error"}} +JSON +``` + +Then one file per comment variant, each with the same two real bugs and the comment placed far from both. +Measured 2026-08-21 on Oxlint 1.79.0: + +| comment present in the file | Oxlint output | +| --- | --- | +| none | `react/refs`, `react/set-state-in-effect` | +| `// eslint-disable-next-line react-hooks/exhaustive-deps` | `react/rule-suppression` only | +| `// eslint-disable-next-line react-hooks/rules-of-hooks` | `react/rule-suppression` only | +| `// oxlint-disable-next-line react-hooks/exhaustive-deps` | `react/rule-suppression` only | +| `/* eslint-disable react-hooks/exhaustive-deps */` at the top of the file | `react/rule-suppression` only | +| `// eslint-disable-next-line react-hooks/refs` | `react/refs`, `react/set-state-in-effect` | +| `// eslint-disable-next-line no-console` | `react/refs`, `react/set-state-in-effect` | +| `// @ts-expect-error` | `react/refs`, `react/set-state-in-effect` | + +Then real ESLint, in the same directory, on the same files, with the same plugin version our sidecar +hosts: + +```bash +npm install eslint@9 eslint-plugin-react-hooks@7.1.1 +# eslint.config.mjs enabling only react-hooks/refs and react-hooks/set-state-in-effect +``` + +ESLint reports both bugs in every case. Oxlint reports neither whenever the comment is present. Same +machine, same files, same plugin version, no Expensify config on either side. + +The Rust source matches the table exactly. `crates/oxc_linter/src/utils/react_compiler.rs` hardcodes the +trigger list for the whole rule family as `react-hooks/exhaustive-deps`, `react-hooks/rules-of-hooks`, +`react/exhaustive-deps` and `react/rules-of-hooks`, which is why `react-hooks/refs` changes nothing. And +`find_program_suppressions` matches the `oxlint-` spellings as well as the `eslint-` ones, which is why +row 4 behaves like row 2. That last point matters beyond this table: the twin-comment codemod on +`feat/oxlint-fixed` would have armed the bail-out just as effectively as the ESLint comments do, so +translating the comments was never a way out. They have to be gone. + +## No knob, but a clean thing to ask for + +Every rule in the family is declared `RuleNoConfig` in `apps/oxlint/src-js/package/config.generated.ts`, +so none of them accept options. `options.respectEslintDisableDirectives: false` does not reach it either +(measured: it un-suppresses the plain `react-hooks/exhaustive-deps` rule and the compiler checks stay +silent). + +The mechanism does exist in the crate, though. The standalone `transform-react` package exposes +`eslintSuppressionRules`, and `crates/oxc_react_compiler/tests/transform.rs` has a passing test named +`empty_eslint_suppression_rules_disable_bailouts` proving an empty list disables the bail-outs. So the +request to file is **"expose `eslintSuppressionRules` on the linter rules the way the transform package +already does"**, not "please change your suppression policy". Nobody has asked for it: searched across the +oxc tracker open and closed, the web, Reddit, HN, Stack Overflow and CN/JP/KR blogs. diff --git a/oxlint-migration/native-vs-sidecar-probe/TwoComponents.tsx b/oxlint-migration/native-vs-sidecar-probe/TwoComponents.tsx new file mode 100644 index 000000000000..e3a95ba573d5 --- /dev/null +++ b/oxlint-migration/native-vs-sidecar-probe/TwoComponents.tsx @@ -0,0 +1,30 @@ +import {useEffect, useRef, useState} from 'react'; + +// COMPONENT A: carries the disable comment AND a bug. +function Dirty({start}) { + const [count, setCount] = useState(start); + const refA = useRef(0); + const doubledA = refA.current * 2; // BUG A1: ref read during render + + useEffect(() => { + console.log(count); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return doubledA; +} + +// COMPONENT B: completely clean of comments, but has the same kind of bug. +function Clean({start}) { + const [value, setValue] = useState(start); + const refB = useRef(0); + const doubledB = refB.current * 2; // BUG B1: ref read during render + + useEffect(() => { + setValue(start); // BUG B2: setState in effect + }, [start]); + + return doubledB + value; +} + +export {Dirty, Clean}; diff --git a/oxlint-migration/native-vs-sidecar-probe/rhImmutabilityAnchor.tsx b/oxlint-migration/native-vs-sidecar-probe/rhImmutabilityAnchor.tsx new file mode 100644 index 000000000000..90fc57de80dc --- /dev/null +++ b/oxlint-migration/native-vs-sidecar-probe/rhImmutabilityAnchor.tsx @@ -0,0 +1,30 @@ +// The one anchor divergence the Rust React Compiler port carries, kept as its own probe because the +// fixture it came from could not both stay multi-line and stay at parity. +// +// `react-hooks/immutability` reports this shape TWICE on both tools, and both agree on the first +// finding: "Cannot reassign variable after render completes", on the `latest = 1;` line, column 9. +// The second, "Cannot modify local variables after render completes", is where they part. Measured +// 2026-08-21 on this file, ESLint through oxlint-migration/port-probe/eslint.fixtures.config.mjs and +// oxlint through oxlint-migration/rc-rust-probe.oxlintrc.json (line numbers are quoted as landmarks +// rather than absolutes, because editing this header would move them): +// +// ESLint (JavaScript compiler) the `onClick={onSelect}` line, column 29 -- the reference passed +// as a JSX prop, i.e. the site where the function escapes render +// rc/* (Rust compiler) the `latest = 1;` line, column 9 -- the modification site again, +// carrying the arrow declaration as a secondary label and the escape +// site nowhere at all +// +// So the location ESLint anchors on is not present in the Rust diagnostic at all, which is why this +// is a recorded divergence rather than a mapping bug in config/oxlint/reactCompilerRust.mjs: there is +// no label to anchor on. Repo impact, measured 2026-08-21: 7 rc/immutability findings whole-repo. +// Asserted by `npm run oxlint-react-compiler-rust`, so a future oxc-transform-react bump that +// changes the anchor shows up as a failing assertion rather than as parity drift. +// +// rh/immutability: reassigning a local from a callback, which runs after render has completed. +export function ReassignsAfterRender() { + let latest = 0; + const onSelect = () => { + latest = 1; + }; + return ; +} diff --git a/oxlint-migration/parallel-probe/shard-experiment.sh b/oxlint-migration/parallel-probe/shard-experiment.sh new file mode 100755 index 000000000000..85cf7e2d8d7a --- /dev/null +++ b/oxlint-migration/parallel-probe/shard-experiment.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# oxlint runs JS plugins on one thread, so --threads does nothing for them. This probe fans files +# across several `oxlint --threads=1` processes instead. See the plan doc for what each subcommand shows: +# ./shard-experiment.sh measure [N] baseline vs N contiguous shards +# ./shard-experiment.sh curve scaling curve over several N +# ./shard-experiment.sh verify [N] shards vs a single run, per-rule diff +# macOS ships bash 3.2, so empty array expansions must be guarded. +set -u +REPO=/Users/lukasz.modzelewski/conductor/workspaces/expensify-app/kyoto +cd "$REPO" || exit 1 +LIST=/tmp/oxfiles.txt + +file_list() { npx oxlint --debug=files . 2>/dev/null | sort > "$LIST"; } + +split_contiguous() { + local N="$1" SD="$2" TOTAL LINES + TOTAL=$(wc -l < "$LIST" | tr -d ' ') + rm -rf "$SD"; mkdir -p "$SD" + LINES=$(( (TOTAL + N - 1) / N )) + split -l "$LINES" -d -a 2 "$LIST" "$SD/s_" +} + +run_shards() { + local N="$1" SD="$2"; shift 2 + local EXTRA=() ; for a in "$@"; do EXTRA+=("$a"); done + local START END + START=$(date +%s.%N) + for s in "$SD"/s_*; do + ( npx oxlint --threads=1 ${EXTRA[@]+"${EXTRA[@]}"} $(cat "$s") >/dev/null 2>&1 ) & + done + wait + END=$(date +%s.%N) + echo "$END-$START" | bc +} + +time_baseline() { + local S E; S=$(date +%s.%N) + npx oxlint . >/dev/null 2>&1 || true + E=$(date +%s.%N); echo "$E-$S" | bc +} + +case "${1:-measure}" in + measure) + N="${2:-6}"; file_list + echo "baseline (default threads, 1 proc): $(time_baseline)s" + split_contiguous "$N" /tmp/oxshards + echo "N=$N contiguous shards (threads=1 each): $(run_shards "$N" /tmp/oxshards)s" + ;; + curve) + file_list + echo "baseline: $(time_baseline)s" + for N in 1 3 4 6 7 14; do + if [ "$N" -eq 1 ]; then + S=$(date +%s.%N); npx oxlint --threads=1 . >/dev/null 2>&1 || true; E=$(date +%s.%N) + printf 'N=1 (threads=1) %.1fs\n' "$(echo "$E-$S"|bc)"; continue + fi + split_contiguous "$N" /tmp/oxshards + printf 'N=%s %.1fs\n' "$N" "$(run_shards "$N" /tmp/oxshards)" + done + ;; + verify) + N="${2:-6}"; file_list + echo "single run -> /tmp/single.json" + npx oxlint --format json . >/tmp/single.json 2>/dev/null || true + SD=/tmp/oxjson; rm -rf "$SD"; mkdir -p "$SD" + split_contiguous "$N" "$SD" + for s in "$SD"/s_*; do ( npx oxlint --threads=1 --format json $(cat "$s") >"$s.json" 2>/dev/null ) & done; wait + node -e ' + const fs=require("fs"); + const strip=t=>t.replace(/^[^{]*/,"").replace(/[^}]*$/,""); + const single=JSON.parse(strip(fs.readFileSync("/tmp/single.json","utf8"))); + let merged=[]; + for(const f of fs.readdirSync("/tmp/oxjson").filter(x=>x.endsWith(".json"))) + merged.push(...JSON.parse(strip(fs.readFileSync("/tmp/oxjson/"+f,"utf8"))).diagnostics); + const byCode=a=>{const m={};for(const d of a){const k=d.code||"(none)";m[k]=(m[k]||0)+1;}return m;}; + const A=byCode(single.diagnostics),B=byCode(merged); + let diffs=0; for(const k of new Set([...Object.keys(A),...Object.keys(B)])) if((A[k]||0)!==(B[k]||0)){diffs++;console.log(` ${k}: single=${A[k]||0} sharded=${B[k]||0}`);} + console.log(`TOTAL single=${single.diagnostics.length} sharded=${merged.length} per-rule-diffs=${diffs}`); + ' + ;; + *) echo "unknown subcommand: $1"; exit 2 ;; +esac diff --git a/oxlint-migration/port-probe/compareFixtures.py b/oxlint-migration/port-probe/compareFixtures.py new file mode 100644 index 000000000000..4ca30ed0ff5b --- /dev/null +++ b/oxlint-migration/port-probe/compareFixtures.py @@ -0,0 +1,244 @@ +"""Per-rule parity harness: one example file per rule, linted by both tools. + + python3 oxlint-migration/port-probe/compareFixtures.py [--filter=] [--bless] + +The full-repo comparison (oxlint-migration/compareFullRepo.py) can only prove parity for +rules that currently have violations, and the config coverage check can only prove a +rule is *configured*. Neither can tell whether a configured rule actually runs: oxlint +silently accepts unknown rules inside `overrides`, and its JSON schema omits plugins +the binary does implement. A fixture answers that directly. + +Each entry in fixtures.manifest.json names the rule, the file that violates it, the id +oxlint reports it under, and how many findings to expect. The run fails if a rule is +silent on either side or if the two tools disagree on (file, line, rule). + +Fixtures can also arrive as shards under fragments/: every .manifest.json merges +into the manifest (duplicate keys fail the run), every .eslint.mjs exports an +array of ESLint flat-config objects appended to the probe config, and every +.oxlint.json contributes {"rules", "jsPlugins", "overrides", "flags"} to a +generated oxlint.fixtures.merged.json. Shards are how batches land without two authors +editing one shared config. + +--filter selects manifest entries whose rule id or fixture name contains the substring, +and restricts both tools to just those fixture files. --bless fills an entry whose +"expected" is null with the ESLint finding count and writes the shard that owns it; +expected counts must come from ESLint while ESLint still exists as the oracle. +""" + +import argparse +import glob +import json +import os +import re +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +FRAGMENT_DIR = os.path.join(HERE, 'fragments') +# OXPROBE_TAG lets a concurrent batch write its own merged config instead of racing another batch +# for the shared generated file. The real path (no tag) is what compareFullRepo and CI read. +MERGED_OXLINT_CONFIG = os.path.join(HERE, f"oxlint.fixtures.merged{os.environ.get('OXPROBE_TAG', '')}.json") + +sys.path.insert(0, os.path.join(HERE, '..')) +from ruleMap import load_jsonc # noqa: E402 + + +def load_manifest(): + """fixtures.manifest.json plus every fragments/*.manifest.json, with per-entry origin.""" + manifest, origin = {}, {} + + def absorb(entries, path): + for rule, entry in entries.items(): + if rule in manifest: + sys.exit(f'duplicate manifest entry "{rule}": {origin[rule]} and {path}') + manifest[rule], origin[rule] = entry, path + return entries + + absorb(json.load(open(os.path.join(HERE, 'fixtures.manifest.json'))), os.path.join(HERE, 'fixtures.manifest.json')) + for path in sorted(glob.glob(os.path.join(FRAGMENT_DIR, '*.manifest.json'))): + with open(path) as handle: + absorb(json.load(handle), path) + return manifest, origin + + +def build_oxlint_config(): + """Merge the base probe config with every fragments/*.oxlint.json into one real file.""" + base = load_jsonc(os.path.join(HERE, 'oxlint.fixtures.json')) + base.pop('$schema', None) + flags = set() + for path in sorted(glob.glob(os.path.join(FRAGMENT_DIR, '*.oxlint.json'))): + with open(path) as handle: + fragment = json.load(handle) + base.setdefault('rules', {}).update(fragment.get('rules', {})) + base['jsPlugins'] = base.get('jsPlugins', []) + fragment.get('jsPlugins', []) + base['overrides'] = base.get('overrides', []) + fragment.get('overrides', []) + flags.update(fragment.get('flags', [])) + if '--type-aware' in flags: + base.setdefault('options', {})['typeAware'] = True + with open(MERGED_OXLINT_CONFIG, 'w') as handle: + json.dump(base, handle, indent=4) + handle.write('\n') + return MERGED_OXLINT_CONFIG, sorted(flags) + + +def oxlint_findings(config, flags, targets): + out = subprocess.run( + ['npx', 'oxlint', '-c', os.path.basename(config), *flags, '--no-ignore', '--format', 'json', *targets], + capture_output=True, text=True, cwd=HERE, + ) + try: + diagnostics = json.loads(out.stdout)['diagnostics'] + except (json.JSONDecodeError, KeyError): + sys.exit(f'oxlint run failed:\n{out.stdout[:2000]}\n{out.stderr[:2000]}') + findings = set() + for diagnostic in diagnostics: + match = re.match(r'^([\w@/.-]+)\((.+)\)$', diagnostic.get('code', '')) + rule = f'{match.group(1)}/{match.group(2)}' if match else diagnostic.get('code', '') + line = diagnostic['labels'][0]['span']['line'] if diagnostic.get('labels') else 0 + findings.add((os.path.basename(diagnostic['filename']), line, rule)) + return findings + + +def eslint_findings(targets): + out = subprocess.run( + ['npx', 'eslint', '--no-config-lookup', '-c', 'eslint.fixtures.config.mjs', '--no-ignore', '--format', 'json', *targets], + capture_output=True, text=True, cwd=HERE, + ) + try: + report = json.loads(out.stdout) + except json.JSONDecodeError: + sys.exit(f'eslint run failed:\n{out.stdout[:2000]}\n{out.stderr[:2000]}') + return {(os.path.basename(f['filePath']), m['line'], m['ruleId']) for f in report for m in f['messages']} + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--filter', help='only entries whose rule id or fixture name contains this substring') + parser.add_argument('--bless', action='store_true', help='fill expected=null from the ESLint count and rewrite the owning file') + args = parser.parse_args() + + manifest, origin = load_manifest() + # Strays are judged against every entry, so a --filter run does not flag the other rules a + # shared fixture file trips. + claimed = {e['oxlintRule'] for e in manifest.values()} | set(manifest) + if args.filter: + manifest = {rule: entry for rule, entry in manifest.items() if args.filter in rule or args.filter in entry['fixture']} + if not manifest: + sys.exit(f'no manifest entries selected (filter={args.filter!r}) -- refusing to pass on zero fixtures') + + targets = sorted({f'fixtures/{os.path.basename(entry["fixture"])}' for entry in manifest.values()}) + missing = [t for t in targets if not os.path.isfile(os.path.join(HERE, t))] + if missing: + sys.exit(f'manifest names fixture files that do not exist: {", ".join(missing)}') + + config, flags = build_oxlint_config() + ox = oxlint_findings(config, flags, targets) + es = eslint_findings(targets) + linter_files = {os.path.basename(t) for t in targets} + if not ox and not es and all(entry.get('expected') == 0 for entry in manifest.values()): + print(f'warning: neither tool reported anything across {len(linter_files)} fixture files') + + failures, blessed = [], {} + print(f'{"rule":56} {"eslint":>7} {"oxlint":>7} {"expected":>9} verdict') + for rule, entry in manifest.items(): + fixture = os.path.basename(entry['fixture']) + es_hits = {f for f in es if f[0] == fixture and f[2] == rule} + ox_hits = {f for f in ox if f[0] == fixture and f[2] == entry['oxlintRule']} + expected = entry['expected'] + if expected is None: + if args.bless and not es_hits and not entry.get('oxlintOnly'): + # Blessing 0 would pin "ESLint found nothing" as the oracle, and 0-vs-0 then reads as + # parity forever: the fixture proves the rule runs on neither tool. Every way of ending + # up here is a bug to fix rather than record -- a fixture whose shape no longer violates + # the rule, or a manifest keyed by the oxlint id so the ESLint side matches nothing. + failures.append(rule) + print(f'{rule:56} {len(es_hits):7} {len(ox_hits):7} {"null":>9} FAIL: refusing to bless 0 -- ESLint is silent, so this fixture proves nothing') + continue + if args.bless: + expected = entry['expected'] = len(es_hits) + blessed.setdefault(origin[rule], {})[rule] = entry + else: + failures.append(rule) + print(f'{rule:56} {len(es_hits):7} {len(ox_hits):7} {"null":>9} FAIL: expected is null, run with --bless') + continue + es_lines = sorted(line for _, line, _ in es_hits) + ox_lines = sorted(line for _, line, _ in ox_hits) + accepted_ox_lines = entry.get('oxlintLines') + divergence = entry.get('expectedOxlint') is not None or entry.get('oxlintOnly') + if len(es_hits) != expected: + verdict, ok = f'FAIL: eslint found {len(es_hits)}, fixture claims {expected}', False + elif entry.get('blockedUpstream'): + # The fixture still proves ESLint reports (checked above); oxlint cannot, for a reason + # written down in the entry. Asserted as zero rather than skipped, so this flips to a + # failure the day upstream makes it reportable, which is the signal to act. + if ox_hits: + verdict, ok = f'FAIL: oxlint now reports {ox_lines} -- upstream fixed it, drop blockedUpstream and re-enable the rule', False + else: + verdict, ok = 'blocked upstream, oxlint silent as recorded', True + elif divergence: + # An intentional one-sided rule: oxlintOnly pins ESLint at 0, or expectedOxlint pins any + # count. Deliberate strictness stays a pinned divergence, so drift on EITHER side trips. + if not entry.get('whyOxlintDivergence'): + verdict, ok = 'FAIL: expectedOxlint/oxlintOnly needs a whyOxlintDivergence', False + elif entry.get('oxlintOnly') and entry['expected'] != 0: + verdict, ok = 'FAIL: oxlintOnly means eslint expected 0', False + else: + want = len(es_hits) if entry.get('oxlintOnly') and entry.get('expectedOxlint') is None else entry['expectedOxlint'] + if want is None: + verdict, ok = 'FAIL: oxlintOnly needs expectedOxlint (from the oxlint column of a first run)', False + elif len(ox_hits) != want: + verdict, ok = f'FAIL: oxlint found {len(ox_hits)} ({ox_lines}), fixture claims {want}', False + else: + verdict, ok = f'intentional divergence, oxlint {ox_lines} vs eslint {es_lines}', True + elif accepted_ox_lines is not None and not entry.get('whyOxlintLines'): + verdict, ok = 'FAIL: oxlintLines needs a whyOxlintLines saying why the anchors differ', False + elif accepted_ox_lines is not None: + if ox_lines != sorted(accepted_ox_lines): + verdict, ok = f'FAIL: oxlint {ox_lines}, manifest accepts {sorted(accepted_ox_lines)}', False + else: + verdict, ok = 'parity, anchor differs (eslint {es_lines})'.replace('{es_lines}', str(es_lines)), True + elif es_lines != ox_lines: + verdict, ok = f'FAIL: lines differ (eslint {es_lines} vs oxlint {ox_lines})', False + else: + verdict, ok = 'parity', True + if not ok: + failures.append(rule) + print(f'{rule:56} {len(es_hits):7} {len(ox_hits):7} {expected:9} {verdict}') + + if args.bless and blessed: + for path, entries in blessed.items(): + doc = json.load(open(path)) + doc.update(entries) + with open(path, 'w') as handle: + json.dump(doc, handle, indent=4, sort_keys=False) + handle.write('\n') + count = sum(len(v) for v in blessed.values()) + print(f'\nblessed {count} entries from the ESLint run into {len(blessed)} file(s).') + + print() + stray_ox = {f for f in ox if f[0] in linter_files and f[2] not in claimed} + stray_es = {f for f in es if f[0] in linter_files and f[2] not in claimed} + # A finding no entry claims means the two probe configs enable different rules, which is the + # drift this harness exists to catch, so it fails instead of being counted and ignored. + for tool, strays in (('oxlint', stray_ox), ('eslint', stray_es)): + for file, line, rule in sorted(strays): + print(f'FAIL: {tool} reports {rule} at {file}:{line}, which no manifest entry claims') + if stray_ox or stray_es: + failures.append(f'findings outside the manifest (oxlint={len(stray_ox)}, eslint={len(stray_es)})') + if failures: + print(f'{len(failures)}/{len(manifest)} rules FAILED: {", ".join(failures)}') + sys.exit(1) + blocked = [rule for rule, entry in manifest.items() if entry.get('blockedUpstream')] + divergent = [rule for rule, entry in manifest.items() if entry.get('expectedOxlint') is not None or entry.get('oxlintOnly')] + if blocked or divergent: + print(f'{len(manifest) - len(blocked) - len(divergent)} rules behave identically on both tools.') + if divergent: + print(f'{len(divergent)} pinned intentional divergences: {", ".join(divergent)}') + if blocked: + print(f'{len(blocked)} blocked upstream, oxlint silent by known cause: {", ".join(blocked)}') + else: + print(f'All {len(manifest)} rules behave identically on both tools.') + + +if __name__ == '__main__': + main() diff --git a/oxlint-migration/port-probe/eslint.fixtures.config.mjs b/oxlint-migration/port-probe/eslint.fixtures.config.mjs new file mode 100644 index 000000000000..fa6aa1ce6af8 --- /dev/null +++ b/oxlint-migration/port-probe/eslint.fixtures.config.mjs @@ -0,0 +1,260 @@ +import js from '@eslint/js'; +import fs from 'node:fs'; +import {createRequire} from 'node:module'; +import path from 'node:path'; +import {fileURLToPath, pathToFileURL} from 'node:url'; +import tseslint from 'typescript-eslint'; + +const require = createRequire(import.meta.url); +const here = path.dirname(fileURLToPath(import.meta.url)); +const fromRepo = (relative) => require(path.resolve(here, '../..', relative)); + +// Batches land as shards under fragments/: every .eslint.mjs exports an array of flat-config +// objects appended after the shared ones below, so no two batches ever edit this file. A shard must +// scope its entries to its own fixture files, exactly as the scoped blocks here do. +async function fragmentConfigs() { + const dir = path.join(here, 'fragments'); + if (!fs.existsSync(dir)) { + return []; + } + const shards = fs + .readdirSync(dir) + .filter((name) => name.endsWith('.eslint.mjs')) + .sort(); + const configs = []; + for (const name of shards) { + const mod = await import(pathToFileURL(path.join(dir, name)).href); + configs.push(...(Array.isArray(mod.default) ? mod.default : [mod.default])); + } + return configs; +} + +// Loaded through require() so CJS/ESM interop differences between these plugins do not matter. +const react = fromRepo('node_modules/eslint-plugin-react'); +const importPlugin = fromRepo('node_modules/eslint-plugin-import'); +const jsdoc = fromRepo('node_modules/eslint-plugin-jsdoc'); +const testingLibrary = fromRepo('node_modules/eslint-plugin-testing-library'); +const lodashUnderscore = fromRepo('node_modules/eslint-plugin-you-dont-need-lodash-underscore'); +const reactNativeA11y = fromRepo('node_modules/eslint-plugin-react-native-a11y'); +// Package-relative resolution of this one is blocked by the package's "exports" field. +const noInlineUseOnyxSelector = fromRepo('node_modules/eslint-config-expensify/eslint-plugin-expensify/no-inline-useOnyx-selector.js'); +// require() of an ESM module returns its namespace, which here is {name, meta, create}. +const requireLocaleForLocalizedDateFormat = fromRepo('eslint-plugin-local-rules/require-locale-for-localized-date-format.js'); +const noRawTypography = fromRepo('eslint-plugin-local-rules/no-raw-typography.js'); +// The same nested copy oxlint loads, so both tools run one plugin instance. +const reactHooks = fromRepo('node_modules/eslint-config-expensify/node_modules/eslint-plugin-react-hooks'); + +const plugin = (mod) => ({rules: (mod?.rules ?? mod?.default?.rules) || {}}); + +const shared = [ + js.configs.recommended, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parser: tseslint.parser, + parserOptions: {ecmaFeatures: {jsx: true}}, + }, + plugins: { + '@typescript-eslint': tseslint.plugin, + react: plugin(react), + import: plugin(importPlugin), + jsdoc: plugin(jsdoc), + 'testing-library': plugin(testingLibrary), + 'you-dont-need-lodash-underscore': plugin(lodashUnderscore), + 'react-native-a11y': plugin(reactNativeA11y), + rulesdir: { + rules: { + 'no-inline-useOnyx-selector': noInlineUseOnyxSelector, + 'require-locale-for-localized-date-format': requireLocaleForLocalizedDateFormat, + 'no-raw-typography': noRawTypography, + }, + }, + }, + // The extra object form {property: 'exact', exact: true} is not in the repo config: with only + // the repo's plain strings, react/prefer-exact-props stays inert on both tools. + settings: {react: {version: 'detect'}, propWrapperFunctions: ['forbidExtraProps', 'exact', 'Object.freeze', {property: 'exact', exact: true}]}, + rules: { + // Off on TS files in production too (typescript-eslint's eslint-recommended), so + // js.configs.recommended must not run them on TS fixtures. + 'no-redeclare': 'off', + 'no-unreachable': 'off', + 'no-unreachable-loop': ['error', {ignore: []}], + 'react/jsx-no-bind': ['error', {ignoreRefs: true, allowArrowFunctions: true, allowFunctions: false, allowBind: false, ignoreDOMComponents: true}], + 'react/function-component-definition': ['error', {namedComponents: 'function-declaration', unnamedComponents: 'arrow-function'}], + 'react/jsx-no-constructed-context-values': 'error', + 'import/prefer-default-export': 'error', + 'import/order': ['error', {groups: [['builtin', 'external', 'internal']]}], + 'jsdoc/no-types': 'error', + '@typescript-eslint/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'}, + ], + 'rulesdir/no-inline-useOnyx-selector': 'error', + 'rulesdir/no-raw-typography': 'error', + 'rulesdir/require-locale-for-localized-date-format': 'error', + 'no-restricted-syntax': [ + 'error', + {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: + 'JSXElement[openingElement.name.name=/^Pressable(WithoutFeedback|WithFeedback|WithDelayToggle|WithoutFocus)$/]:not(:has(JSXAttribute[name.name="sentryLabel"]))', + message: 'All Pressable components must include sentryLabel prop for Sentry tracking. Example: ', + }, + {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.', + }, + ], + strict: ['error', 'never'], + 'one-var': ['error', 'never'], + 'no-undef-init': 'error', + 'no-new-object': 'error', + 'lines-between-class-members': ['error', 'always', {exceptAfterSingleLine: false}], + 'import/no-import-module-exports': ['error', {exceptions: []}], + 'import/no-relative-packages': 'error', + 'import/no-useless-path-segments': ['error', {commonjs: true}], + 'react/default-props-match-prop-types': ['error', {allowRequiredDefaults: false}], + 'react/forbid-foreign-prop-types': ['error', {allowInPropTypes: true}], + 'react/forbid-prop-types': ['error', {forbid: ['any', 'array', 'object'], checkContextTypes: true, checkChildContextTypes: true}], + 'react/no-access-state-in-setstate': 'error', + 'react/no-arrow-function-lifecycle': 'error', + 'react/no-deprecated': 'error', + 'react/no-invalid-html-attribute': 'error', + 'react/no-typos': 'error', + 'react/no-unused-class-component-methods': 'error', + 'react/no-unused-prop-types': ['error', {customValidators: [], skipShapeProps: true}], + 'react/no-unused-state': 'error', + 'react/prefer-stateless-function': ['error', {ignorePureComponents: true}], + 'react/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'], + }, + }, + ], + 'react/static-property-placement': ['error', 'property assignment'], + 'you-dont-need-lodash-underscore/uniq': 'error', + 'testing-library/no-debugging-utils': 'error', + 'react-native-a11y/has-valid-accessibility-descriptors': 'error', + 'no-undef': 'off', + 'no-unused-vars': 'off', + }, + }, + { + // Scoped to the rh* fixtures because these rules run the React Compiler over every file they + // see, and the other fixtures hold components that would report shapes no manifest entry claims. + files: ['**/rh*.tsx'], + languageOptions: {parser: tseslint.parser, parserOptions: {sourceType: 'module', ecmaFeatures: {jsx: true}}}, + plugins: {'react-hooks': {rules: reactHooks.rules}}, + rules: { + 'react-hooks/component-hook-factories': 'error', + 'react-hooks/config': 'error', + 'react-hooks/error-boundaries': 'error', + 'react-hooks/exhaustive-deps': 'error', + 'react-hooks/gating': 'error', + 'react-hooks/globals': 'error', + 'react-hooks/immutability': 'error', + 'react-hooks/incompatible-library': 'error', + 'react-hooks/preserve-manual-memoization': 'error', + 'react-hooks/purity': 'error', + 'react-hooks/refs': 'error', + 'react-hooks/set-state-in-effect': 'error', + 'react-hooks/set-state-in-render': 'error', + 'react-hooks/static-components': 'error', + 'react-hooks/unsupported-syntax': 'error', + 'react-hooks/use-memo': 'error', + }, + }, + { + // Scoped to one file because the rule throws on any *read* of `.propTypes` once an exact + // wrapper is configured, and reactPropTypes.tsx contains one deliberately. + files: ['**/reactExactProps.tsx'], + rules: {'react/prefer-exact-props': 'error'}, + }, + { + // no-octal and no-octal-escape can only be violated in script mode: in a module the legacy + // syntax is a parse error on both tools. Both give .cjs a script/commonjs sourceType. + files: ['**/*.cjs'], + rules: { + 'no-octal': 'error', + 'no-octal-escape': 'error', + // typescript-eslint switches all three off for TS, and no-dupe-args also needs sloppy mode. + 'dot-notation': ['error', {allowKeywords: true, allowPattern: ''}], + 'no-dupe-args': 'error', + 'no-return-await': 'error', + // builtinGlobals true is what production resolves for JS; oxlint runs false (checkConfigDrift LEDGER). + 'no-redeclare': ['error', {builtinGlobals: true}], + 'no-undef': 'off', + 'no-unused-vars': 'off', + }, + }, +]; + +export default [...shared, ...(await fragmentConfigs())]; diff --git a/oxlint-migration/port-probe/fixtures.manifest.json b/oxlint-migration/port-probe/fixtures.manifest.json new file mode 100644 index 000000000000..db52e5301b03 --- /dev/null +++ b/oxlint-migration/port-probe/fixtures.manifest.json @@ -0,0 +1,344 @@ +{ + "no-unreachable-loop": { + "fixture": "fixtures/unreachableLoop.ts", + "oxlintRule": "core/no-unreachable-loop", + "expected": 1, + "why": "WIRED 2026-08-10 as core/no-unreachable-loop; oxlint's native port false-positives, so ESLint's core rule is hosted instead" + }, + "react/jsx-no-bind": { + "fixture": "fixtures/jsxNoBind.tsx", + "oxlintRule": "hosted/jsx-no-bind", + "expected": 1, + "why": "WIRED 2026-08-10; not implemented natively in oxlint" + }, + "react/function-component-definition": { + "fixture": "fixtures/functionComponentDefinition.tsx", + "oxlintRule": "hosted/function-component-definition", + "expected": 1, + "why": "WIRED 2026-08-10; oxlint's native port diverges, so the hosted rule shadows it" + }, + "react/jsx-no-constructed-context-values": { + "fixture": "fixtures/jsxNoConstructedContextValues.tsx", + "oxlintRule": "hosted/jsx-no-constructed-context-values", + "expected": 1, + "why": "WIRED 2026-08-12 as hosted/... behind the React Compiler gate. Hosted rather than native for two reasons: only a JS plugin can hold the gate, and oxlint's native port diverges (wrong anchor line, no component-scope check). The fixture opts out of memoization so it tests the rule, not the gate" + }, + "import/prefer-default-export": { + "fixture": "fixtures/preferDefaultExport.ts", + "oxlintRule": "hosted/prefer-default-export", + "expected": 1, + "why": "WIRED 2026-08-10; oxlint's native port diverges, so the hosted rule shadows it" + }, + "import/order": { + "fixture": "fixtures/importOrder.ts", + "oxlintRule": "hosted/order", + "expected": 1, + "why": "WIRED 2026-08-10; not implemented natively (oxfmt also enforces a stricter grouping)" + }, + "jsdoc/no-types": { + "fixture": "fixtures/jsdocNoTypes.ts", + "oxlintRule": "hosted/no-types", + "expected": 2, + "why": "WIRED 2026-08-10; not implemented natively in oxlint" + }, + "@typescript-eslint/naming-convention": { + "fixture": "fixtures/namingConvention.ts", + "oxlintRule": "hosted/naming-convention", + "expected": 6, + "why": "tsgolint lists it unimplemented, but our options never query types -- hosted with stubbed parser services. The 6th finding is the quoted 'foo-bar' property: our options define no requiresQuotes entry, so it is checked like any other property. Both tools agree, which is what proves the stubbed compilerOptions.target is harmless" + }, + "rulesdir/require-locale-for-localized-date-format": { + "fixture": "fixtures/requireLocaleForLocalizedDateFormat.ts", + "oxlintRule": "rulesdir/require-locale-for-localized-date-format", + "expected": 1, + "why": "WIRED 2026-08-21; ESLint enabled it and .oxlintrc.json did not, which no findings comparison could surface because the rule reports 0 on this repo" + }, + "rulesdir/no-inline-useOnyx-selector": { + "fixture": "fixtures/inlineUseOnyxSelector.ts", + "oxlintRule": "rulesdir/no-inline-useOnyx-selector", + "expected": 1, + "why": "WIRED 2026-08-12 behind the same React Compiler gate; ungated it reports 238 findings across 186 files that ESLint's processor drops. Fixture opts out of memoization so it tests the rule, not the gate" + }, + "you-dont-need-lodash-underscore/uniq": { + "fixture": "fixtures/youDontNeedLodash.ts", + "oxlintRule": "you-dont-need-lodash-underscore/uniq", + "expected": 1, + "why": "already enabled on both sides, but missing from oxlint's JSON schema -- fixture is the only proof it runs" + }, + "testing-library/no-debugging-utils": { + "fixture": "fixtures/testingLibraryDebug.ts", + "oxlintRule": "testing-library/no-debugging-utils", + "expected": 1, + "why": "same: enabled on both sides, absent from oxlint's schema" + }, + "react-native-a11y/has-valid-accessibility-descriptors": { + "fixture": "fixtures/reactNativeA11y.tsx", + "oxlintRule": "react-native-a11y/has-valid-accessibility-descriptors", + "expected": 1, + "why": "same: enabled on both sides, absent from oxlint's schema" + }, + "strict": { + "fixture": "fixtures/coreStyleRules.ts", + "oxlintRule": "core/strict", + "expected": 1, + "why": "WIRED 2026-08-11; no native oxlint port" + }, + "one-var": { + "fixture": "fixtures/coreStyleRules.ts", + "oxlintRule": "eslint/one-var", + "expected": 1, + "why": "WIRED 2026-08-11; native as of oxlint 1.78.0 (#24470), so no sidecar cost" + }, + "no-undef-init": { + "fixture": "fixtures/coreStyleRules.ts", + "oxlintRule": "core/no-undef-init", + "expected": 1, + "why": "WIRED 2026-08-11; no native oxlint port (unicorn/no-useless-undefined is broader, so not a stand-in)" + }, + "no-new-object": { + "fixture": "fixtures/coreStyleRules.ts", + "oxlintRule": "eslint/no-object-constructor", + "expected": 1, + "why": "WIRED 2026-08-11 under its post-rename name: ESLint deprecated no-new-object in 8.50 for no-object-constructor, and oxlint only implements the new one. This fixture pins the shape both agree on, `new Object()`. The replacement also flags a bare `Object()` and does not flag `new Object(arg)` -- neither exists in the repo" + }, + "lines-between-class-members": { + "fixture": "fixtures/coreStyleRules.ts", + "oxlintRule": "core/lines-between-class-members", + "expected": 1, + "why": "WIRED 2026-08-11; no native oxlint port. TS-only in both configs, matching eslint-config-expensify" + }, + "no-octal": { + "fixture": "fixtures/coreOctalSyntax.cjs", + "oxlintRule": "core/no-octal", + "expected": 1, + "why": "WIRED 2026-08-11; the fixture must be script-mode, because in a module oxc rejects legacy octal syntax at parse time and no rule is reached" + }, + "no-octal-escape": { + "fixture": "fixtures/coreOctalSyntax.cjs", + "oxlintRule": "core/no-octal-escape", + "expected": 1, + "why": "same as no-octal: only reachable in a script-mode file" + }, + "react-hooks/purity": { + "fixture": "fixtures/rhPurity.tsx", + "oxlintRule": "rc/purity", + "expected": 1, + "why": "covered 2026-08-12; the compiler rules had no fixture at all before, and 10 of the 16 had no evidence of any kind" + }, + "react-hooks/set-state-in-render": { + "fixture": "fixtures/rhSetStateInRender.tsx", + "oxlintRule": "rc/set-state-in-render", + "expected": 1, + "why": "covered 2026-08-12" + }, + "react-hooks/globals": { + "fixture": "fixtures/rhGlobals.tsx", + "oxlintRule": "rc/globals", + "expected": 1, + "why": "covered 2026-08-12" + }, + "react-hooks/error-boundaries": { + "fixture": "fixtures/rhErrorBoundaries.tsx", + "oxlintRule": "rc/error-boundaries", + "expected": 1, + "why": "covered 2026-08-12" + }, + "react-hooks/use-memo": { + "fixture": "fixtures/rhUseMemo.tsx", + "oxlintRule": "rc/use-memo", + "expected": 1, + "why": "covered 2026-08-12" + }, + "react-hooks/unsupported-syntax": { + "fixture": "fixtures/rhUnsupportedSyntax.tsx", + "oxlintRule": "rc/unsupported-syntax", + "expected": 1, + "why": "covered 2026-08-12" + }, + "react-hooks/incompatible-library": { + "fixture": "fixtures/rhIncompatibleLibrary.tsx", + "oxlintRule": "rc/incompatible-library", + "expected": 1, + "why": "covered 2026-08-12; react-hook-form is matched by module name, so the package does not have to be installed" + }, + "react-hooks/refs": { + "fixture": "fixtures/rhRefs.tsx", + "oxlintRule": "rc/refs", + "expected": 1, + "why": "covered 2026-08-12; ported to rc/ 2026-08-21. Doubles as the direct test of the Rust engine's reason for existing: the component also carries an eslint-disable-next-line react-hooks/exhaustive-deps comment, which makes oxlint's native react/refs report nothing (measured 0) while ESLint and rc/refs both report line 20" + }, + "react-hooks/immutability": { + "fixture": "fixtures/rhImmutability.tsx", + "oxlintRule": "rc/immutability", + "expected": 1, + "why": "covered 2026-08-12, ported to rc/ 2026-08-21. Both tools report TWO diagnostics here (the reassignment and the escaping function) and this harness compares a set of (file, line, rule), so the two collapse into one row. Deliberately so: the fixture is one line because the two engines anchor the second diagnostic differently, ESLint on the escape site and the Rust compiler on the modification site. That divergence, and the count of two, are asserted separately by npm run oxlint-react-compiler-rust against oxlint-migration/native-vs-sidecar-probe/rhImmutabilityAnchor.tsx" + }, + "react-hooks/set-state-in-effect": { + "fixture": "fixtures/rhSetStateInEffect.tsx", + "oxlintRule": "rc/set-state-in-effect", + "expected": 1, + "why": "covered 2026-08-12; previously repo parity only" + }, + "react-hooks/static-components": { + "fixture": "fixtures/rhStaticComponents.tsx", + "oxlintRule": "rc/static-components", + "expected": 1, + "why": "covered 2026-08-12; previously repo parity only" + }, + "react-hooks/preserve-manual-memoization": { + "fixture": "fixtures/rhPreserveManualMemoization.tsx", + "oxlintRule": "rc/preserve-manual-memoization", + "expected": 1, + "why": "covered 2026-08-12; previously repo parity only" + }, + "react-hooks/exhaustive-deps": { + "fixture": "fixtures/rhExhaustiveDeps.tsx", + "oxlintRule": "hosted/exhaustive-deps", + "expected": 1, + "why": "covered 2026-08-12; oxlint's native rule 2026-08-24; hosted behind the React Compiler message gate 2026-08-25. A missing dependency on purpose, not a useCallback/useMemo suggestion: both tools run this rule behind the gate, which drops the suggestions and keeps missing dependencies, so a suggestion-shaped fixture would test the gate instead of the rule. Hosting also retires the anchor divergence the native rule needed an oxlintLines exemption for, since the same rule module now reports the same line on both tools" + }, + "no-restricted-syntax": { + "fixture": "fixtures/coreRestrictedSyntax.tsx", + "oxlintRule": "core/no-restricted-syntax", + "expected": 8, + "why": "covered 2026-08-12; it had repo-wide parity before, which stops being evidence the day the repo stops violating it. One violation per configured selector except WithStatement, which is a parse error in a module on both tools. This is also the only test of esquery selector matching through oxlint's bridge" + }, + "dot-notation": { + "fixture": "fixtures/corePlainJsRules.cjs", + "oxlintRule": "core/dot-notation", + "expected": 1, + "why": "WIRED 2026-08-11, covered 2026-08-12. Natively oxlint has only typescript/dot-notation, which the plain-JS override turns off with the rest of the typed rules" + }, + "no-dupe-args": { + "fixture": "fixtures/corePlainJsRules.cjs", + "oxlintRule": "core/no-dupe-args", + "expected": 1, + "why": "WIRED 2026-08-11, covered 2026-08-12. Sloppy mode only: duplicate parameter names are a parse error in a module, so the fixture has to be script-mode .cjs" + }, + "no-return-await": { + "fixture": "fixtures/corePlainJsRules.cjs", + "oxlintRule": "core/no-return-await", + "expected": 1, + "why": "WIRED 2026-08-11, covered 2026-08-12. Natively oxlint has only typescript/return-await, off for plain JS along with the rest of the typed rules" + }, + "no-redeclare": { + "fixture": "fixtures/corePlainJsRules.cjs", + "oxlintRule": "eslint/no-redeclare", + "expected": 1, + "why": "covered 2026-10-05. Plain JS only, as in production: on TS ESLint runs @typescript-eslint/no-redeclare instead. The duplicate parameter in the no-dupe-args fixture is also a redeclaration on both tools" + }, + "import/no-useless-path-segments": { + "fixture": "fixtures/importPathRules.ts", + "oxlintRule": "hosted/no-useless-path-segments", + "expected": 1, + "why": "WIRED 2026-08-11; oxlint's native import plugin has 33 rules and none of these three" + }, + "import/no-relative-packages": { + "fixture": "fixtures/importPathRules.ts", + "oxlintRule": "hosted/no-relative-packages", + "expected": 1, + "why": "WIRED 2026-08-11; no native oxlint port" + }, + "import/no-import-module-exports": { + "fixture": "fixtures/importPathRules.ts", + "oxlintRule": "hosted/no-import-module-exports", + "expected": 2, + "why": "WIRED 2026-08-11; no native oxlint port. Two findings because the rule reports once per import statement" + }, + "react/default-props-match-prop-types": { + "fixture": "fixtures/reactPropTypes.tsx", + "oxlintRule": "hosted/default-props-match-prop-types", + "expected": 1, + "why": "WIRED 2026-08-11; PropTypes era, no native oxlint port" + }, + "react/forbid-foreign-prop-types": { + "fixture": "fixtures/reactPropTypes.tsx", + "oxlintRule": "hosted/forbid-foreign-prop-types", + "expected": 1, + "why": "WIRED 2026-08-11; PropTypes era, no native oxlint port" + }, + "react/forbid-prop-types": { + "fixture": "fixtures/reactPropTypes.tsx", + "oxlintRule": "hosted/forbid-prop-types", + "expected": 1, + "why": "WIRED 2026-08-11; PropTypes era, no native oxlint port" + }, + "react/no-typos": { + "fixture": "fixtures/reactPropTypes.tsx", + "oxlintRule": "hosted/no-typos", + "expected": 1, + "why": "WIRED 2026-08-11; catches `static PropTypes` and friends, no native oxlint port" + }, + "react/no-unused-prop-types": { + "fixture": "fixtures/reactPropTypes.tsx", + "oxlintRule": "hosted/no-unused-prop-types", + "expected": 2, + "why": "WIRED 2026-08-11; the only one of the 17 with live suppression sites in the repo (13), so the alias needed a comment sweep" + }, + "react/prefer-exact-props": { + "fixture": "fixtures/reactExactProps.tsx", + "oxlintRule": "hosted/prefer-exact-props", + "expected": 1, + "why": "WIRED 2026-08-11, covered 2026-08-12. In production this rule is inert on both tools, because settings.propWrapperFunctions lists plain strings and the rule only reports for wrappers marked exact: true. Both fixture configs therefore add the object form, which is the only way to observe the rule at all -- see the settings comment in oxlint.fixtures.json. Its own file, and scoped to it in both configs, because with an exact wrapper configured the rule throws on any read of `.propTypes`" + }, + "react/no-access-state-in-setstate": { + "fixture": "fixtures/reactClassComponents.tsx", + "oxlintRule": "hosted/no-access-state-in-setstate", + "expected": 1, + "why": "WIRED 2026-08-11; class-component era, no native oxlint port" + }, + "react/no-arrow-function-lifecycle": { + "fixture": "fixtures/reactClassComponents.tsx", + "oxlintRule": "hosted/no-arrow-function-lifecycle", + "expected": 1, + "why": "WIRED 2026-08-11; class-component era, no native oxlint port" + }, + "react/no-unused-class-component-methods": { + "fixture": "fixtures/reactClassComponents.tsx", + "oxlintRule": "hosted/no-unused-class-component-methods", + "expected": 1, + "why": "WIRED 2026-08-11; class-component era, no native oxlint port" + }, + "react/no-unused-state": { + "fixture": "fixtures/reactClassComponents.tsx", + "oxlintRule": "hosted/no-unused-state", + "expected": 1, + "why": "WIRED 2026-08-11; class-component era, no native oxlint port" + }, + "react/prefer-stateless-function": { + "fixture": "fixtures/reactClassComponents.tsx", + "oxlintRule": "hosted/prefer-stateless-function", + "expected": 1, + "why": "WIRED 2026-08-11; class-component era, no native oxlint port" + }, + "react/sort-comp": { + "fixture": "fixtures/reactClassComponents.tsx", + "oxlintRule": "hosted/sort-comp", + "expected": 1, + "why": "WIRED 2026-08-11; the full 12-entry order array from eslint-config-expensify is repeated in both fixture configs on purpose -- a rule can run and still disagree if the options are read differently" + }, + "react/static-property-placement": { + "fixture": "fixtures/reactClassComponents.tsx", + "oxlintRule": "hosted/static-property-placement", + "expected": 1, + "why": "WIRED 2026-08-11; class-component era, no native oxlint port" + }, + "react/no-deprecated": { + "fixture": "fixtures/reactDeprecatedApis.tsx", + "oxlintRule": "hosted/no-deprecated", + "expected": 1, + "why": "WIRED 2026-08-11; needs settings.react.version, which is why both fixture configs set it" + }, + "react/no-invalid-html-attribute": { + "fixture": "fixtures/reactDeprecatedApis.tsx", + "oxlintRule": "hosted/no-invalid-html-attribute", + "expected": 1, + "why": "WIRED 2026-08-11; no native oxlint port" + }, + "rulesdir/no-raw-typography": { + "fixture": "fixtures/rawTypography.ts", + "oxlintRule": "rulesdir/no-raw-typography", + "expected": 1, + "why": "WIRED 2026-09-08; ESLint enabled it and .oxlintrc.json did not, which no findings comparison could surface on its own. The rule needs no type information, so the production rulesdir sidecar loads it unchanged" + } +} diff --git a/oxlint-migration/port-probe/fixtures/a11yNpmBatch.tsx b/oxlint-migration/port-probe/fixtures/a11yNpmBatch.tsx new file mode 100644 index 000000000000..0c770cb28630 --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/a11yNpmBatch.tsx @@ -0,0 +1,132 @@ +// One violation per react-native-a11y rule, each followed by the valid form its own docs allow. +// The rules read prop names and literal shapes, so nothing here needs the react-native runtime: +// TouchableOpacity, View, Image and Text are matched by name only. +// Handler and array identities matter: isNodePropExpression treats only Identifier, CallExpression, +// ConditionalExpression and MemberExpression as "value we cannot evaluate", so the valid forms below +// pass objects and functions by name. +const actions = [{name: 'activate'}]; +const onAccessibilityAction = () => undefined; + +// react-native-a11y/has-accessibility-props: accessibilityRole mixed with a deprecated trait +export function HasAccessibilityProps() { + return ( + <> + + + + ); +} + +// react-native-a11y/has-valid-accessibility-actions: list without a handler +export function HasValidAccessibilityActions() { + return ( + <> + + + + ); +} + +// react-native-a11y/has-valid-accessibility-component-type: value outside the enum +export function HasValidAccessibilityComponentType() { + return ( + <> + + + + ); +} + +// react-native-a11y/has-valid-accessibility-ignores-invert-colors: invertable Image without the prop +export function HasValidAccessibilityIgnoresInvertColors() { + return ( + <> + + + + ); +} + +// react-native-a11y/has-valid-accessibility-live-region: value outside the enum +export function HasValidAccessibilityLiveRegion() { + return ( + <> + + + + ); +} + +// react-native-a11y/has-valid-accessibility-role: value outside the enum +export function HasValidAccessibilityRole() { + return ( + <> + + + + ); +} + +// react-native-a11y/has-valid-accessibility-state: object key with a non-boolean literal +export function HasValidAccessibilityState() { + return ( + <> + + + + ); +} + +// react-native-a11y/has-valid-accessibility-states: array member outside the enum +export function HasValidAccessibilityStates() { + return ( + <> + + + + ); +} + +// react-native-a11y/has-valid-accessibility-traits: value outside the enum +export function HasValidAccessibilityTraits() { + return ( + <> + + + + ); +} + +// react-native-a11y/has-valid-accessibility-value: numeric field holding a string +export function HasValidAccessibilityValue() { + return ( + <> + + + + ); +} + +// react-native-a11y/has-valid-important-for-accessibility: value outside the enum +export function HasValidImportantForAccessibility() { + return ( + <> + + + + ); +} + +// react-native-a11y/no-nested-touchables: touchable inside accessible={true} +export function NoNestedTouchables() { + return ( + <> + + undefined} /> + + + Not clickable + + + ); +} diff --git a/oxlint-migration/port-probe/fixtures/coreOctalSyntax.cjs b/oxlint-migration/port-probe/fixtures/coreOctalSyntax.cjs new file mode 100644 index 000000000000..dbdefa954da8 --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/coreOctalSyntax.cjs @@ -0,0 +1,9 @@ +// Fixture for the two rules oxc's parser makes unreachable in ESM: in a module, legacy octal +// syntax is a parse error ("'0'-prefixed octal literals and octal escape sequences are +// deprecated"), so only a script-mode file can hold the violations a linter rule would see. +// That is exactly the coverage these two rules still add, and why they are wired. +const legacyOctalLiteral = 010; // no-octal + +const octalEscapeSequence = 'copyright \251'; // no-octal-escape + +module.exports = {legacyOctalLiteral, octalEscapeSequence}; diff --git a/oxlint-migration/port-probe/fixtures/corePlainJsRules.cjs b/oxlint-migration/port-probe/fixtures/corePlainJsRules.cjs new file mode 100644 index 000000000000..c946c9e4d227 --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/corePlainJsRules.cjs @@ -0,0 +1,17 @@ +// Fixture for the three ESLint core rules that are hosted through the `core/` alias for plain JS +// only, because typescript-eslint's `eslint-recommended` switches them off for TS. The file is .cjs +// for two reasons: it lands inside the plain-JS scope both configs use, and `no-dupe-args` is only +// reachable in sloppy mode -- duplicate parameter names are a parse error in a module on both tools. +const target = {literalKey: 1}; + +const readWithBrackets = target['literalKey']; // dot-notation + +function duplicated(sameName, sameName) { + return sameName; +} // no-dupe-args, no-redeclare + +async function awaitedReturn() { + return await Promise.resolve(1); // no-return-await +} + +module.exports = {awaitedReturn, duplicated, readWithBrackets}; diff --git a/oxlint-migration/port-probe/fixtures/coreRestrictedSyntax.tsx b/oxlint-migration/port-probe/fixtures/coreRestrictedSyntax.tsx new file mode 100644 index 000000000000..aeb5a02c0a8b --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/coreRestrictedSyntax.tsx @@ -0,0 +1,32 @@ +// Fixture for no-restricted-syntax, hosted through `core/` because oxlint has no native port: the +// rule matches esquery selectors, so this is also the only place the bridge's selector engine is +// tested, including the regex attribute matches and the `:not(:has(...))` in the Pressable rule. +// +// One violation per selector the repo configures, except WithStatement: `with` is a parse error in a +// module on both tools, so that selector is unreachable here for the same reason no-octal needs a +// script-mode file. +import * as Parent from '../ParentModule'; +import * as Sibling from './SiblingModule'; +import * as Libs from '@libs/SomeLib'; +import * as ReportActions from '@userActions/Report'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import React from 'react'; + +enum Status { + Active, +} + +const Wrapped = React.forwardRef(() => null); + +function scan(rows: number[]) { + outer: for (const row of rows) { + if (row > 0) { + break outer; + } + } + return [Parent, Sibling, Libs, ReportActions]; +} + +const trigger = {}} />; + +export {scan, Status, trigger, Wrapped}; diff --git a/oxlint-migration/port-probe/fixtures/coreStyleRules.ts b/oxlint-migration/port-probe/fixtures/coreStyleRules.ts new file mode 100644 index 000000000000..4de653bd7c1f --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/coreStyleRules.ts @@ -0,0 +1,17 @@ +// Fixture for the ESLint core rules hosted through the `core/` alias, plus the two oxlint +// implements natively. One deliberate violation per rule; the harness matches on (file, line, rule) +// so the rules are free to share a file. +'use strict'; // strict ['never']: the directive is redundant inside a module + +let firstName, secondName; // one-var ['never'] + +let placeholder = undefined; // no-undef-init + +const wrapper = new Object(); // no-new-object in ESLint, reported by oxlint's no-object-constructor + +class TwoAdjacentMembers { + first() {} + second() {} // lines-between-class-members ['always']: no blank line above +} + +export {firstName, placeholder, secondName, TwoAdjacentMembers, wrapper}; diff --git a/oxlint-migration/port-probe/fixtures/deprecatedSelfClosingJsx.tsx b/oxlint-migration/port-probe/fixtures/deprecatedSelfClosingJsx.tsx new file mode 100644 index 000000000000..cda508b2ec38 --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/deprecatedSelfClosingJsx.tsx @@ -0,0 +1,9 @@ +import Card from './deprecatedSelfClosingJsxCard'; + +const RowCard = Card; + +const plain = ; +const aliased = ; +const compound = ; + +export {aliased, compound, plain}; diff --git a/oxlint-migration/port-probe/fixtures/deprecatedSelfClosingJsxCard.tsx b/oxlint-migration/port-probe/fixtures/deprecatedSelfClosingJsxCard.tsx new file mode 100644 index 000000000000..4baefea459cf --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/deprecatedSelfClosingJsxCard.tsx @@ -0,0 +1,12 @@ +/** @deprecated Compose `Card.Root` instead. */ +function LegacyCard({title}: {title: string}) { + return
{title}
; +} + +function CardRoot() { + return null; +} + +const Card = Object.assign(LegacyCard, {Root: CardRoot}); + +export default Card; diff --git a/oxlint-migration/port-probe/fixtures/functionComponentDefinition.tsx b/oxlint-migration/port-probe/fixtures/functionComponentDefinition.tsx new file mode 100644 index 000000000000..b83e691092de --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/functionComponentDefinition.tsx @@ -0,0 +1,6 @@ +import React from 'react'; + +// react/function-component-definition: named components must be function declarations +const Badge = () => ; + +export default Badge; diff --git a/oxlint-migration/port-probe/fixtures/importNativeBatch.ts b/oxlint-migration/port-probe/fixtures/importNativeBatch.ts new file mode 100644 index 000000000000..bd3199510777 --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/importNativeBatch.ts @@ -0,0 +1,46 @@ +// Fixture for the import/* rules oxlint implements natively and production enables for TypeScript. +// One violation per rule, each on its own line, because the harness matches on (file, line, rule). +// Every import here is a relative path inside the fixtures tree, so both resolvers see a real file. +// import/no-absolute-path is the one rule that cannot live here: importNativeBatchAbsolute.ts says why. + +// no-webpack-loader-syntax: an inline loader prefix, spelled `raw-loader!./x` and not `!raw-loader!./x`. +// The leading `!` makes the specifier unresolvable to eslint-plugin-import's node resolver, and its +// extensions rule adds a "Missing file extension" finding on top for unresolvable specifiers. It comes +// first because the probe's base config also runs import/order, which reads a loader specifier as an +// external package and so wants it ahead of every sibling import below. +import webpackLoaderChain from 'raw-loader!./importNativeBatchHelper'; + +// Control: the correct shape, reported by nothing. +import {sharedConfigValue} from './importNativeBatchHelper'; + +// consistent-type-specifier-style prefer-top-level: an inline `type` specifier. +import {type HelperOptions} from './importNativeBatchHelper'; + +// extensions (ignorePackages + never for ts): the extension is spelled out. +import extensionfulHelper from './importNativeBatchHelper.ts'; + +// no-self-import: this file, by relative path. The file has a default export (see the bottom) so that the +// self-import is a plain default import: oxlint's import plugin comes with default-on rules of its own +// (import/default), which production switches off with categories.correctness but this probe does not. +import selfImportedModule from './importNativeBatch'; + +// no-named-default: `default` pulled in as a named specifier. +import {default as renamedDefault} from './importNativeBatchHelper'; + +// newline-after-import: the statement below has no blank line above it. +import {helperSum} from './importNativeBatchHelper'; +const noBlankLineAbove = helperSum(sharedConfigValue, 1); + +// import/first: the statement above is the first non-import, so this import is out of order. +const somethingElse = [extensionfulHelper, webpackLoaderChain, selfImportedModule, renamedDefault, noBlankLineAbove] as HelperOptions[]; +import {helperSum as lateSum} from './importNativeBatchHelper'; + +// no-amd: a factory call that looks like AMD. +define([], () => lateSum(1, 2)); + +// no-mutable-exports: a live binding that can be reassigned. +export let mutableCounter = 0; + +export const batchMarker = 'importNativeBatch'; +export const batchContents = somethingElse; +export default batchMarker; diff --git a/oxlint-migration/port-probe/fixtures/importNativeBatchAbsolute.ts b/oxlint-migration/port-probe/fixtures/importNativeBatchAbsolute.ts new file mode 100644 index 000000000000..6fe32014ae55 --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/importNativeBatchAbsolute.ts @@ -0,0 +1,14 @@ +// Fixture for import/no-absolute-path, kept out of importNativeBatch.ts on purpose: ESLint's +// import/extensions reports "Missing file extension" for every specifier its resolver cannot resolve, +// which any absolute specifier is, so a file holding both violations gives ESLint two findings where +// oxlint gives one. Production has no such import to collide with -- the rule exists to keep them out. +// Only import/no-absolute-path is configured here, on both tools, at production's bare "error". + +// Control: the same module by relative path, reported by nothing. +import {sharedConfigValue} from './importNativeBatchHelper'; + +// no-absolute-path: an absolute specifier. +import absolutePathModule from '/tmp/importNativeBatchAbsolute'; + +export const absoluteBatchMarker = sharedConfigValue; +export const absoluteBatchImport = absolutePathModule; diff --git a/oxlint-migration/port-probe/fixtures/importNativeBatchHelper.ts b/oxlint-migration/port-probe/fixtures/importNativeBatchHelper.ts new file mode 100644 index 000000000000..7b0f036d2323 --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/importNativeBatchHelper.ts @@ -0,0 +1,13 @@ +// Helper module for importNativeBatch: the shapes import/named and import/no-named-as-default-member need +// are a module whose default export carries a property that is ALSO a named export, the react-native-config +// shape production hits. Plain ESM syntax only, so eslint-plugin-import's ExportMap can parse it. + +export const sharedConfigValue = 1; + +export function helperSum(a: number, b: number): number { + return a + b; +} + +export type HelperOptions = {prefix: string}; + +export default {sharedConfigValue, helperSum}; diff --git a/oxlint-migration/port-probe/fixtures/importNativeBatchJs.js b/oxlint-migration/port-probe/fixtures/importNativeBatchJs.js new file mode 100644 index 000000000000..702c69f63bb0 --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/importNativeBatchJs.js @@ -0,0 +1,22 @@ +// Fixture for the two import/* rules production scopes to plain JS (`.oxlintrc.json`: import/named is in the +// **/*.js|jsx|mjs|cjs override, import/no-named-as-default-member is root-on and switched off for TS) and the +// ESLint config mirrors. Kept out of the .ts fixture so the probe does not run either rule where production +// never runs it. +// +// Two exports at the bottom on purpose: the probe's base config enables prefer-default-export for every +// file, and a single-export file would draw an extra finding from oxlint that ESLint does not see there. + +// Control: the named export exists, and reading it directly is not what no-named-as-default-member is about. +import {sharedConfigValue} from './importNativeBatchHelper'; + +// import/named: no such export. +import {nameThatDoesNotExist} from './importNativeBatchHelper'; + +// no-named-as-default-member: `sharedConfigValue` is a named export, read off the default import -- +// exactly the `import Config from 'react-native-config'` / `Config.SOMETHING` shape from production. +import helperDefault from './importNativeBatchHelper'; + +const memberRead = helperDefault.sharedConfigValue; + +export const jsBatchMarker = [memberRead, nameThatDoesNotExist, sharedConfigValue]; +export const jsBatchDefault = helperDefault; diff --git a/oxlint-migration/port-probe/fixtures/importOrder.ts b/oxlint-migration/port-probe/fixtures/importOrder.ts new file mode 100644 index 000000000000..62529bfadb5d --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/importOrder.ts @@ -0,0 +1,9 @@ +// import/order: the relative import must come after the builtin one +import {basename} from './helper'; +import fs from 'node:fs'; + +function exists() { + return fs.existsSync(basename); +} + +export default exists; diff --git a/oxlint-migration/port-probe/fixtures/importPathRules.ts b/oxlint-migration/port-probe/fixtures/importPathRules.ts new file mode 100644 index 000000000000..876ee3ac7aad --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/importPathRules.ts @@ -0,0 +1,10 @@ +// Fixture for the three eslint-plugin-import rules oxlint has no native port for. All three are +// path rules, so they go in one file: the harness matches on (file, line, rule). + +// no-useless-path-segments: './../fixtures/x' is the long way round to './x' +import * as ordering from './../fixtures/importOrder'; +// no-relative-packages: reaching into another package by relative path instead of by its name +import uniq from '../../../node_modules/lodash/uniq'; + +// no-import-module-exports: ESM imports above, CommonJS export below +module.exports = {ordering, uniq}; diff --git a/oxlint-migration/port-probe/fixtures/inlineUseOnyxSelector.ts b/oxlint-migration/port-probe/fixtures/inlineUseOnyxSelector.ts new file mode 100644 index 000000000000..29450875d95b --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/inlineUseOnyxSelector.ts @@ -0,0 +1,12 @@ +import useOnyx from '@hooks/useOnyx'; + +// rulesdir/no-inline-useOnyx-selector: the selector is an inline arrow +function useAccountID() { + // 'use no memo': same reason as jsxNoConstructedContextValues.tsx -- this rule is gated on the + // React Compiler too, and both compilers memoize this hook. + 'use no memo'; + const [accountID] = useOnyx('session', {selector: (session: {accountID: number}) => session.accountID}); + return accountID; +} + +export default useAccountID; diff --git a/oxlint-migration/port-probe/fixtures/jsdocNoTypes.ts b/oxlint-migration/port-probe/fixtures/jsdocNoTypes.ts new file mode 100644 index 000000000000..92d1fe1c6e33 --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/jsdocNoTypes.ts @@ -0,0 +1,11 @@ +/** + * jsdoc/no-types: types belong in TypeScript, not in the JSDoc block + * + * @param {string} name + * @returns {string} + */ +function greet(name: string) { + return name; +} + +export default greet; diff --git a/oxlint-migration/port-probe/fixtures/jsx11yBatchA.tsx b/oxlint-migration/port-probe/fixtures/jsx11yBatchA.tsx new file mode 100644 index 000000000000..6dde742de2cb --- /dev/null +++ b/oxlint-migration/port-probe/fixtures/jsx11yBatchA.tsx @@ -0,0 +1,206 @@ +import React from 'react'; + +// Plain DOM JSX -- no React Native. Every element sits on its own line so both tools anchor on the +// same line number: ESLint sometimes reports the attribute and oxlint the element, which only stays +// invisible while the whole opening tag is on one line. +// Part A covers the content and ARIA-attribute rules. Role and interaction rules are in jsx11yBatchB.tsx. +// Every component below pairs one violation with a valid control, so a rule that stops firing for a +// reason unrelated to the fixture (a dropped plugin, an option that silently disabled it) cannot pass +// by also silencing the violation. + +// jsx-a11y/alt-text -- with no alt; control carries alt. +function AltText() { + return ( + <> + + Expensify logo + + ); +} + +// jsx-a11y/anchor-has-content -- empty anchor; control has text. +function AnchorHasContent() { + return ( + <> + + Pricing + + ); +} + +// jsx-a11y/anchor-is-valid -- href="#" is the invalidHref aspect; control is a real href, and the +// Link control is what production's {components: ["Link"], specialLink: ["to"]} exists for. +function AnchorIsValid() { + return ( + <> + Learn more + Expenses + Reports + + ); +} + +// jsx-a11y/aria-activedescendant-has-tabindex -- aria-activedescendant without tabIndex; control +// uses -1, which the rule accepts and no-noninteractive-tabindex does not flag. +function AriaActiveDescendantHasTabindex() { + return ( + <> +
+
+ + ); +} + +// jsx-a11y/aria-props -- misspelled ARIA attribute; control is spelled correctly. +function AriaProps() { + return ( + <> +
+
+ + ); +} + +// jsx-a11y/aria-proptypes -- "yes" is not a valid boolean value; control uses "true". +function AriaProptypes() { + return ( + <> +
+