diff --git a/.github/workflows/oxlint.yml b/.github/workflows/oxlint.yml new file mode 100644 index 000000000000..2de009114c09 --- /dev/null +++ b/.github/workflows/oxlint.yml @@ -0,0 +1,72 @@ +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 splits them into 4 shards on this 8 vCPU runner. + # While oxlint is a non-blocking check, the post-merge run both adds and removes entries from the oxlint.seatbelt.tsv. + # Remove this once oxlint becomes the blocking linter. + env: + SEATBELT_INCREASE: ${{ github.event_name == 'push' && github.event.head_commit.author.name != 'OSBotify' && 'all' || '' }} + run: npm run lint -- --linter=oxlint --shards=4 + + - name: Check for an updated oxlint-seatbelt baseline + id: seatbelt_diff + if: github.event_name == 'push' + run: | + if git diff --quiet config/oxlint/oxlint.seatbelt.tsv; then + echo "updated=false" >> "$GITHUB_OUTPUT" + else + echo "updated=true" >> "$GITHUB_OUTPUT" + fi + + - name: Report the Oxlint result without failing the job + run: | + if [[ "${{ steps.oxlint.outcome }}" != "success" ]]; then + echo "::warning::Oxlint found errors this PR adds (see the step log). This check is temporarily non-blocking while oxlint runs next to ESLint." + elif [[ "${{ steps.seatbelt_diff.outputs.updated }}" == "true" ]]; then + echo "::notice::This merge changed the list of known oxlint errors (config/oxlint/oxlint.seatbelt.tsv). OSBotify commits the update. Temporary while oxlint runs next to ESLint." + else + echo "::notice::Oxlint found no new errors." + fi + + - name: Setup git for OSBotify + if: steps.seatbelt_diff.outputs.updated == '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 updated oxlint-seatbelt baseline + if: steps.seatbelt_diff.outputs.updated == 'true' + continue-on-error: true + run: | + git add config/oxlint/oxlint.seatbelt.tsv + git commit -m "Auto-update oxlint-seatbelt baseline" + git push origin main diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index 0c09e86b3098..2846145b97db 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -14,6 +14,11 @@ jobs: uses: ./.github/workflows/lint.yml secrets: inherit + # Left out of confirmPassingBuild's `needs` on purpose: Oxlint is a shadow gate that runs here to auto-tighten its baseline. + oxlint: + uses: ./.github/workflows/oxlint.yml + secrets: inherit + oxfmt: uses: ./.github/workflows/oxfmt.yml diff --git a/.gitignore b/.gitignore index 8c6d0691fdb9..338f6b390d19 100644 --- a/.gitignore +++ b/.gitignore @@ -185,3 +185,19 @@ 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 + +# Per-run Oxlint configs written by scripts/lint/oxlint/OxlintLinter.ts +.oxlintrc.js-plugins.*.json +.oxlintrc.type-aware.*.json diff --git a/.npmrc b/.npmrc index 2a14ea859d36..487c5e8c89be 100644 --- a/.npmrc +++ b/.npmrc @@ -10,3 +10,10 @@ 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 +# List each native binding scope too, or npm silently skips the optional dependency. +min-release-age-exclude[]=oxlint +min-release-age-exclude[]=@oxlint/* +min-release-age-exclude[]=oxc-transform-react +min-release-age-exclude[]=@oxc-transform-react/* +min-release-age-exclude[]=oxlint-tsgolint +min-release-age-exclude[]=@oxlint-tsgolint/* 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 1759e8d8c46c..b0ebc1037f18 100644 --- a/config/eslint/eslint.config.mjs +++ b/config/eslint/eslint.config.mjs @@ -837,6 +837,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/**', '.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..e0f5de6a59ac --- /dev/null +++ b/config/oxlint/eslintDirectives.mjs @@ -0,0 +1,176 @@ +/** + * oxlint runs some ESLint rules as JS plugins under a new name. For example, + * `@typescript-eslint/naming-convention` becomes `hosted/naming-convention`. oxlint doesn't know the + * two are the same rule, so an existing `eslint-disable-next-line @typescript-eslint/naming-convention` + * comment wouldn't hide its report. The wrappers here read those comments and drop what ESLint would hide. + */ + +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(); + +/** + * Turns a comment like `eslint-disable-next-line rule-a, rule-b -- reason` into its kind and the rules + * it names. A directive that names no rule applies to every rule. + * @param {{type: 'Line' | 'Block', value: string, loc?: {start: {line: number, column: number}, end: {line: number, column: number}}}} comment + * @returns {{kind: string, ruleIds: string[]} | null} null when the comment is not a directive + */ +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]}; +} + +/** + * Collects a file's disable comments in a form that is quick to check for each report. A + * `-line`/`-next-line` comment becomes the line it covers. `eslint-disable`/`eslint-enable` comments + * stay in source order, because the last one before a report decides. + * @param {Array[0]>} comments + * @returns {{lines: Map>, blocks: Array<{line: number, column: number, ruleId: string, disable: boolean}>}} + */ +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; + } + // ESLint reads `eslint-disable` and `eslint-enable` only from block comments. + const isBlockKind = parsed.kind === 'eslint-disable' || parsed.kind === 'eslint-enable'; + if (isBlockKind && comment.type !== 'Block') { + continue; + } + for (const ruleId of parsed.ruleIds) { + if (parsed.kind === 'eslint-disable-next-line' || parsed.kind === 'eslint-disable-line') { + // ESLint ignores a multi-line `-line` directive but applies a multi-line `-next-line` + // to the line after the comment ends. + 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, column: comment.loc.start.column ?? 0, ruleId, disable: parsed.kind === 'eslint-disable'}); + } + } + } + + return {lines, blocks}; +} + +/** + * Every wrapped rule checks the same file's comments, so they are parsed once per file. + * @param {string | undefined} filename + * @param {Array[0]>} comments + * @returns {ReturnType} + */ +function directivesFor(filename, comments) { + const key = filename ?? ''; + if (!cache.has(key)) { + cache.set(key, parseDirectives(comments)); + } + return cache.get(key); +} + +/** + * Tells whether ESLint would hide a report of `eslintRuleId` at `position`. As in ESLint, the last + * `eslint-disable`/`eslint-enable` before the report, by line and then column, that names the rule + * (or no rule) wins. + * @param {ReturnType} directives + * @param {string} eslintRuleId + * @param {{line: number, column: number} | null} position + * @returns {boolean} + */ +function isSuppressed(directives, eslintRuleId, position) { + if (position == null) { + return false; + } + const {line, column} = position; + 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) { + const isAfterReport = block.line > line || (block.line === line && block.column > column); + if (isAfterReport || (block.ruleId !== ALL_RULES && block.ruleId !== eslintRuleId)) { + continue; + } + disabled = block.disable; + } + return disabled; +} + +/** + * Finds the line and column a report points at, to check it against the disable comments. Rules pass + * either a descriptor with `loc` or `node`, or a node in the legacy `report(node, message)` form. + * @param {unknown[]} args + * @returns {{line: number, column: number} | null} + */ +function reportPosition(args) { + const first = args[0]; + const candidates = [first?.loc?.start, first?.loc, first?.node?.loc?.start, first?.node?.loc]; + const point = candidates.find((candidate) => typeof candidate?.line === 'number'); + return point ? {line: point.line, column: typeof point.column === 'number' ? point.column : 0} : null; +} + +/** + * Makes an oxlint JS plugin rule respect `eslint-disable` comments written for the ESLint rule it + * copies. Without it, `// eslint-disable-next-line react-hooks/exhaustive-deps` would not hide the + * oxlint copy's report, because oxlint calls that copy `hosted/exhaustive-deps`. + * @param {import('eslint').Rule.RuleModule} rule + * @param {string} eslintRuleId the ID ESLint reports this rule under, like `react-hooks/exhaustive-deps` + * @returns {import('eslint').Rule.RuleModule} + */ +function withEslintDirectiveIDs(rule, eslintRuleId) { + return { + ...rule, + create(context) { + const filteredContext = Object.create(context, { + report: { + // Keeps every argument: ESLint also accepts the legacy `report(node, message)` form. + value(...args) { + const position = reportPosition(args); + if (position != null) { + const sourceCode = context.sourceCode ?? context.getSourceCode(); + const filename = context.filename ?? context.getFilename(); + if (isSuppressed(directivesFor(filename, sourceCode.getAllComments()), eslintRuleId, position)) { + return; + } + } + return context.report(...args); + }, + }, + }); + return rule.create(filteredContext); + }, + }; +} + +/** + * withEslintDirectiveIDs for a whole plugin at once. `toEslintId` gives the ESLint name of each rule, + * like `naming-convention` to `@typescript-eslint/naming-convention`. + * @param {Record} rules + * @param {(name: string) => string} toEslintId maps an oxlint rule name to its ESLint ID + * @returns {Record} + */ +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..cd1dd08bf5a1 --- /dev/null +++ b/config/oxlint/noDeprecatedSelfClosingJsx.mjs @@ -0,0 +1,197 @@ +/** + * Flags `` and other self-closing tags of components that are deprecated through their + * call signature. tsgolint's no-deprecated skips the signature check on self-closing JSX, so it misses + * these. JS plugins don't get type info, so the components are listed by hand in the rule options in + * oxlint.config.mts. Delete this rule once tsgolint catches these itself, or every finding shows up twice. + */ + +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)), '../..'); + +/** + * Reads tsconfig.json, which JSON.parse cannot read because it can have comments and trailing commas. + * @param {string} file + * @returns {any} + */ +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; + +/** + * Reads the import aliases (`@components/*`, `@libs/*`, ...) from tsconfig.json, so an import through + * an alias can be matched to the deprecated component's file. Read once, on first use. + * @returns {Array<[string, string]>} pairs of alias prefix and the absolute directory it points to + */ +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; +} + +/** + * Turns an import path into the file it points to, so `@components/MenuItem` and `../MenuItem` both + * match the `module` set in the rule options. + * @param {string} source + * @param {string} filename the importing file + * @returns {string | null} null for package imports + */ +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, + }, + ], +}; + +/** + * Tells which export an import takes: `import MenuItem from` takes `default`, `import {X as Y} from` takes `X`. + * @param {{type: 'ImportSpecifier' | 'ImportDefaultSpecifier' | 'ImportNamespaceSpecifier', imported?: {type: string, name?: string, value?: string}}} specifier + * @returns {string | null} null for a namespace import + */ +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; +} + +/** + * Finds which identifiers a variable may hold, so a deprecated component renamed through + * `const Item = isNew ? MenuItem : OldItem` is still caught when `` is used. + * @param {{type: string} | null | undefined} node the expression a variable is initialized with + * @returns {string[]} + */ +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 []; + } +} + +/** + * Collects the deprecated imports, their aliases and every `` tag while walking the file, and + * reports at the end, so it does not matter whether an alias is declared before or after its use. + * @param {import('eslint').Rule.RuleContext} context + * @returns {import('eslint').Rule.RuleListener} + */ +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..f747cb0fee82 --- /dev/null +++ b/config/oxlint/oxlint.seatbelt.tsv @@ -0,0 +1,2468 @@ +# 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/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-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/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/Attachments/AttachmentCarousel/Pager/index.tsx" "react-hooks/set-state-in-effect" 1 +"../../src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts" "no-restricted-syntax" 1 +"../../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/useChartLabelFormats.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/components/Charts/hooks/useChartLabelMeasurements.ts" "rulesdir/no-raw-typography" 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.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" "react-hooks/refs" 3 +"../../src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx" "react-hooks/refs" 2 +"../../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/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/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/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/MapViewImpl.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/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/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" "no-restricted-syntax" 1 +"../../src/components/MultiGestureCanvas/useCanvasPanGesture.ts" "no-restricted-syntax" 1 +"../../src/components/MultiGestureCanvas/useCanvasPinchGesture.ts" "react-hooks/set-state-in-effect" 1 +"../../src/components/MultiGestureCanvas/useCanvasTapGestures.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/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" 2 +"../../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/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/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/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/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/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/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/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" "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/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/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/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/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/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-unnecessary-type-assertion" 1 +"../../src/hooks/useResponsiveLayout/index.native.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/hooks/useResponsiveLayout/index.ts" "@typescript-eslint/no-unnecessary-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" "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" "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" "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/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" "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" 1 +"../../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" 10 +"../../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-misused-promises" 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/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-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/dynamicRoutesUtils/stripDynamicRouteSuffixFromPath.ts" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/libs/Navigation/helpers/enableGlobalReimbursementsNavigationUtils.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../src/libs/Navigation/helpers/findFocusedRouteWithOnyxTabGuard.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../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/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/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/helpers/wideTabPreMount.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../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" 16 +"../../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" 3 +"../../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/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/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/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/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" "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" 2 +"../../src/pages/iou/request/step/IOURequestStepConfirmation/index.tsx" "no-restricted-imports" 1 +"../../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" "react-hooks/set-state-in-effect" 1 +"../../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" "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/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/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/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-unnecessary-type-assertion" 1 +"../../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" 1 +"../../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" "react-hooks/purity" 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" "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" 2 +"../../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" "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/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/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/RequestMoneyTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/actions/IOU/RequestMoneyTest.ts" "no-restricted-imports" 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" 6 +"../../tests/navigation/getStateFromPathTests.ts" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../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" 2 +"../../tests/perf-local/OptionsListChatPreview.perf-local.ts" "@typescript-eslint/no-unnecessary-type-assertion" 5 +"../../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/MoneyRequestReportTransactionRowWrites.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/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/AssignCardAssigneeStepTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/BaseModalTest.tsx" "react-hooks/globals" 1 +"../../tests/ui/CategorySelectorTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 8 +"../../tests/ui/ChatActionableButtonsTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/ui/ChatTransactionPreviewTest.tsx" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../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/ImportedMembersPageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../tests/ui/InitialSettingsPageTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/IOURequestStepDistanceTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../tests/ui/IssueNewCardAssigneeStepTest.tsx" "@typescript-eslint/no-unnecessary-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" 11 +"../../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" 2 +"../../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/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/WorkspaceMembersChangeWorkArrangementPageTest.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/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/addUtilsToWindowTest.ts" "@typescript-eslint/no-deprecated/originalMessage" 6 +"../../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/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" 7 +"../../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/DistanceRateTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 2 +"../../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/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/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/useDismissOnMoneyRequestReportRemoval.test.ts" "@typescript-eslint/no-unnecessary-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/useShouldAddOrReplaceReceipt.test.ts" "@typescript-eslint/no-deprecated/originalMessage" 1 +"../../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/getLastRouteTest.ts" "@typescript-eslint/no-unnecessary-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/Navigation/wideTabPreMountTest.ts" "@typescript-eslint/no-unnecessary-type-assertion" 4 +"../../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" "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/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" 69 +"../../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" 34 +"../../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/LazyGroupSelectionTest.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 +"../../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/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/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/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..466451ec9b6c --- /dev/null +++ b/config/oxlint/plugins/core-rules.mjs @@ -0,0 +1,31 @@ +/** + * ESLint core rules that oxlint has no native version of. They run inside oxlint as a JS + * plugin under the `core/` prefix, using ESLint's own rule code, so each one behaves as it does + * under ESLint. + * + * This file only exposes the rules. Which files each one runs on is set in oxlint.config.mts, and + * the scopes there copy ESLint: + * + * - dot-notation, no-return-await, no-dupe-args: JS files only. On TS files ESLint turns these off + * and runs the typed @typescript-eslint versions instead, which oxlint has natively. + * - lines-between-class-members: TS files only, as in ESLint. + * - no-restricted-syntax, no-octal, no-octal-escape, no-undef-init, strict: all files, as in ESLint. + * + * no-invalid-this is left out even though ESLint enables it. It calls sourceCode.getJSDocComment, + * which oxlint's plugin bridge does not support. + */ +import {builtinRules} from 'eslint/use-at-your-own-risk'; + +import {withEslintDirectiveIDsFor} from '../eslintDirectives.mjs'; + +const NAMES = ['no-restricted-syntax', '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..2611ab96d110 --- /dev/null +++ b/config/oxlint/plugins/expensify-rules.mjs @@ -0,0 +1,44 @@ +/** + * Loads every rule from eslint-plugin-expensify and eslint-plugin-local-rules into oxlint, under the + * `rulesdir` name ESLint uses. Two rules change: no-inline-useOnyx-selector stays quiet in files React + * Compiler memoizes, and prefer-locale-compare-from-context is swapped for a copy that needs no type info. + */ +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 in create(), which throws in a jsPlugin. + '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..f62e75940ebd --- /dev/null +++ b/config/oxlint/plugins/hosted-rules.mjs @@ -0,0 +1,128 @@ +/** + * Runs rules from ESLint plugins (react, import, jsdoc, storybook, react-hooks, typescript-eslint) + * inside oxlint, using the plugins' own code. Each one is here because oxlint has no native version or + * its native one reports differently. oxlint reserves most of these plugin names for its own ports, + * so all of them load under `hosted`. + */ +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)), '../../..'); + +/** + * Loads the rules of an installed ESLint plugin, so oxlint runs the same rule code as ESLint. + * @param {string} packageName an ESLint plugin package, or an absolute path to one + * @returns {Record} + */ +function rulesOf(packageName) { + let mod; + try { + mod = require(path.resolve(repoRoot, 'node_modules', packageName)); + } catch { + // A directory require ignores `exports`, so a package with no `main` (typescript-eslint) needs + // the 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; ESLint loads the copy nested under +// eslint-config-expensify. +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 requires parser services, which oxlint's JS plugins lack. +// getParserServices(context, true) tolerates a missing `program` but fails on missing node maps and +// reads context.languageOptions first, so both are stubbed. This repo's selectors never reach the +// type checker (no `types` selector). +const STUB_PARSER_SERVICES = { + esTreeNodeToTSNodeMap: new Map(), + tsNodeToESTreeNodeMap: new Map(), +}; + +/** + * Lets typescript-eslint's naming-convention run in oxlint. The rule fails without TypeScript parser + * services, which oxlint's JS plugins do not have, so it gets STUB_PARSER_SERVICES instead. + * @param {import('eslint').Rule.RuleModule} rule + * @returns {import('eslint').Rule.RuleModule} + */ +function withStubbedParserServices(rule) { + return { + ...rule, + create(context) { + const sourceCode = context.sourceCode ?? context.getSourceCode(); + // A Proxy cannot do this: oxlint defines `parserServices` as a read-only, non-configurable + // property, and a get trap may not return a different value for it. + 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); + }, + }; +} + +/** + * Loads `names` from an ESLint plugin as hosted rules, and makes `eslint-disable` comments that use the + * ESLint name (`/`) apply to them. + * @param {Record} rules + * @param {string} eslintPrefix + * @param {string[]} names + * @returns {Record} + */ +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']); + +/** + * Lists the hosted rules from one ESLint plugin that React Compiler does not gate. The SEPARATELY_GATED + * ones are added one by one at the end of the file. + * @param {string} eslintPrefix + * @returns {string[]} + */ +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..0533a486e346 --- /dev/null +++ b/config/oxlint/plugins/rc-rules.mjs @@ -0,0 +1,51 @@ +/** + * The React Compiler rules from eslint-plugin-react-hooks (react-hooks/refs, react-hooks/purity, ...), + * rebuilt for oxlint under the `rc` name. They all share one compile per file, done in reactCompilerRust.mjs. + */ +import {withEslintDirectiveIDs} from '../eslintDirectives.mjs'; +import {RULE_BY_CATEGORY, reactCompilerDiagnostics} from '../reactCompilerRust.mjs'; + +/** + * Builds the oxlint stand-in for one React Compiler rule of eslint-plugin-react-hooks, like + * `react-hooks/refs`. It reports only that rule's findings. The compile is shared (see reactCompilerDiagnostics). + * @param {string} ruleName a react-hooks rule name, like `refs` + * @returns {import('eslint').Rule.RuleModule} + */ +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..a8c9badfc1a7 --- /dev/null +++ b/config/oxlint/preferLocaleCompareFromContext.mjs @@ -0,0 +1,65 @@ +/** + * oxlint version of rulesdir/prefer-locale-compare-from-context. The ESLint rule uses type info to check + * that `localeCompare` is called on a string. oxlint's JS plugins don't get type info, so this copy + * skips that check. Only strings have `localeCompare`, so the one difference is an object with its own + * `localeCompare`, which this copy also reports. + */ +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'); + +/** + * Loads a module from eslint-plugin-expensify, so this copy uses the same message as the original rule. + * `require` of these ESM modules returns the namespace, so unwrap `default`. + * @param {string} relative path inside eslint-plugin-expensify + * @returns {any} + */ +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: [], +}; + +/** + * Reports every `x.localeCompare(...)` outside tests. + * @param {import('eslint').Rule.RuleContext} context + * @returns {import('eslint').Rule.RuleListener} + */ +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..b648000070d8 --- /dev/null +++ b/config/oxlint/reactCompilerGate.mjs @@ -0,0 +1,93 @@ +/** + * In files React Compiler already memoizes, lint shouldn't tell people to add useCallback or useMemo. + * For ESLint, scripts/lint's ReactCompilerFilter drops those reports. oxlint has no processors and + * can't filter its native rules, so here the JS plugin rule itself is wrapped and drops them. + */ +import {isFileMemoized} from '../reactCompiler/checkWithOxc.mjs'; + +const memoizationCache = new Map(); + +/** + * The lint pipeline's memoization verdict, remembered per file because compiling is slow. + * @param {string} filename + * @param {string} sourceText + * @returns {boolean} + */ +function isFileMemoizedOnce(filename, sourceText) { + if (!memoizationCache.has(filename)) { + memoizationCache.set(filename, isFileMemoized(sourceText, filename)); + } + return memoizationCache.get(filename); +} + +/** + * Gets the final text of a report, so withMessageGating can match it. Rules pass either a `message` or + * a `messageId` into `meta.messages`, both with `{{placeholders}}`. + * @param {import('eslint').Rule.RuleModule} rule + * @param {import('eslint').Rule.ReportDescriptor | undefined} descriptor + * @returns {string} + */ +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); +} + +/** + * Drops the rule's reports in files React Compiler memoizes, where the fix they suggest + * (useCallback/useMemo) is banned. Doing it here also runs the compile inside the parallel JS-plugin + * shards, so under scripts/lint ReactCompilerFilter only sees these reports in files that are not memoized. + * @param {import('eslint').Rule.RuleModule} rule + * @param {(rule: import('eslint').Rule.RuleModule, descriptor: import('eslint').Rule.ReportDescriptor) => boolean} shouldGate picks the reports to drop + * @returns {import('eslint').Rule.RuleModule} + */ +function withGating(rule, shouldGate) { + return { + ...rule, + create(context) { + const gatedContext = Object.create(context, { + report: { + // Keeps every argument: ESLint also accepts the legacy `report(node, message)` form. + value(...args) { + if (shouldGate(rule, args[0])) { + const filename = context.filename ?? context.getFilename(); + const sourceText = (context.sourceCode ?? context.getSourceCode()).text; + if (isFileMemoizedOnce(filename, sourceText)) { + return; + } + } + return context.report(...args); + }, + }, + }); + return rule.create(gatedContext); + }, + }; +} + +/** + * For rules whose every report asks for manual memoization, like jsx-no-constructed-context-values. + * @param {import('eslint').Rule.RuleModule} rule + * @returns {import('eslint').Rule.RuleModule} + */ +function withFullGating(rule) { + return withGating(rule, () => true); +} + +/** + * For rules where only some reports ask for manual memoization. exhaustive-deps also reports missing + * dependencies, which still apply, so only reports whose message matches `pattern` are hidden. + * @param {import('eslint').Rule.RuleModule} rule + * @param {RegExp} pattern + * @returns {import('eslint').Rule.RuleModule} + */ +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..2dc6818275eb --- /dev/null +++ b/config/oxlint/reactCompilerRust.mjs @@ -0,0 +1,258 @@ +/** + * Runs oxc's Rust port of React Compiler on a file and turns what it finds into the same react-hooks/* + * reports eslint-plugin-react-hooks gives. plugins/rc-rules.mjs reports them. We use the Rust port + * because the JavaScript compiler in eslint-plugin-react-hooks would take up most of the oxlint run. + */ +import path from 'node:path'; +import {transformSync} from 'oxc-transform-react'; + +import oxcReactCompilerConfig from '../babel/oxcReactCompilerConfig.js'; + +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 compiler options are wrong; ignoring it would silence every rule. +const CONFIG_CATEGORY = 'Config'; + +// ESLint does not enable the rules behind these categories. +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']); + +// The validations eslint-plugin-react-hooks turns on that oxc does not. The rest are already oxc +// defaults or land in IGNORED_CATEGORIES. +const ENVIRONMENT = { + validateNoSetStateInEffects: true, + // The plugin spells this `validateNoJSXInTryStatements`; oxc spells it with a lowercase `sx`. + validateNoJsxInTryStatements: true, + validateNoImpureFunctionsInRender: true, + validateStaticComponents: true, +}; + +// The category is only in the formatted `codeframe` string, which is not a public API of +// oxc-transform-react. That is why package.json pins the exact version and an unmapped category throws. +const CATEGORY_PATTERN = /react-compiler\(([^)]+)\)/; + +const cache = new Map(); + +/** + * Picks the parser oxc-transform-react uses for the file. Plain JS files are parsed as JSX. + * @param {string} ext file extension without the dot + * @returns {'tsx' | 'ts' | 'jsx'} the `lang` oxc-transform-react parses the file as + */ +function getLang(ext) { + if (ext === 'tsx') { + return 'tsx'; + } + if (ext === 'ts' || ext === 'mts' || ext === 'cts') { + return 'ts'; + } + return 'jsx'; +} + +/** + * Byte width of a character in UTF-8, used by buildOffsetToPoint to turn the compiler's byte offsets into columns. + * @param {number} code a code point + * @returns {number} how many UTF-8 bytes it takes + */ +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, while JavaScript strings index UTF-16 code units. + * Using them directly drifts in any non-ASCII file, anchoring diagnostics on the wrong line where + * `eslint-disable-next-line` no longer suppresses them. This maps a byte offset to the line and UTF-16 + * column oxlint's `loc` expects. + * @param {string} source + * @returns {(offset: number) => {line: number, column: number}} + */ +function buildOffsetToPoint(source) { + 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); + } + } + + /** + * @param {number} offset UTF-8 byte offset into `source` + * @returns {{line: number, column: number}} 1-based line and 0-based UTF-16 column + */ + return function offsetToPoint(offset) { + 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; + } + } + + 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]}; + }; +} + +/** + * Formats the message the way eslint-plugin-react-hooks does, so both linters report the same text. + * @param {string} category + * @param {{message?: string, helpMessage?: string}} error + * @returns {string} + */ +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; +} + +/** + * Runs the compiler on one file and returns its findings as react-hooks rule reports. Throws on a + * category it does not know, so an oxc-transform-react bump that changes the categories fails the run. + * @param {string} filename + * @param {string} sourceText + * @param {Record} [environment] the lint-only validations; the harness passes variants + * @returns {Array<{ruleName: string, loc: {start: {line: number, column: number}, end: {line: number, column: number}}, message: string}>} + */ +function analyze(filename, sourceText, environment = ENVIRONMENT) { + let result; + try { + const buildOptions = oxcReactCompilerConfig(); + result = transformSync(filename, sourceText, { + lang: getLang(path.extname(filename).slice(1)), + reactCompiler: { + ...buildOptions, + outputMode: 'lint', + // Without it the binding returns only fatal diagnostics. + reportDiagnostics: true, + flowSuppressions: false, + environment: {...buildOptions.environment, ...environment}, + }, + }); + } catch (error) { + // The binding throws on malformed options instead of reporting a diagnostic. + 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 ?? []; + + 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 with no `react-compiler(...)` category is a parse failure, which oxlint's own + // parser already reports. + 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]; + // eslint-plugin-react-hooks skips a diagnostic with no location; reporting it at 1:0 would be oxlint-only. + if (!label) { + continue; + } + diagnostics.push({ + ruleName, + loc: { + start: offsetToPoint(label.start), + end: offsetToPoint(label.end ?? label.start), + }, + message: buildMessage(category, error), + }); + } + return diagnostics; +} + +/** + * Each rc/* rule (one per react-hooks rule) needs the same compiler output, so the compile runs once + * per file and the result is cached. + * @param {string} filename + * @param {string} sourceText + * @returns {ReturnType} + */ +function reactCompilerDiagnostics(filename, sourceText) { + if (!cache.has(filename)) { + cache.set(filename, analyze(filename, sourceText)); + } + return cache.get(filename); +} + +export {analyze, CONFIG_CATEGORY, ENVIRONMENT, IGNORED_CATEGORIES, 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..1985a56f24cd --- /dev/null +++ b/config/oxlint/ruleNames.mjs @@ -0,0 +1,110 @@ +/** + * oxlint and ESLint name some rules differently. This maps between the two, so oxlint findings show up + * under ESLint's rule IDs and hosted-rules.mjs knows which ESLint plugin each hosted rule comes from. + * It's plain `.mjs` because oxlint's JS plugins can't load TypeScript. + */ +const OXLINT_RULE_RENAMES = { + 'no-object-constructor': 'no-new-object', + 'no-new-native-nonconstructor': 'no-new-symbol', +}; + +// oxlint reserves native plugin prefixes (`react`, `import`, `react-hooks`, `@typescript-eslint`), so +// hosted rules register under `hosted` and this maps each back to its ESLint plugin. +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', +}; + +const HOSTED_RULE_ESLINT_ID = { + 'no-deprecated-self-closing-jsx': '@typescript-eslint/no-deprecated', +}; + +const OXLINT_DIAGNOSTIC_CODE = /^([\w@/.-]+)\((.+)\)$/; + +/** + * Lists the hosted rules copied from one ESLint plugin, so hosted-rules.mjs knows which rules to load + * from that package. + * @param {string} eslintPrefix an ESLint plugin name, like `react` + * @returns {string[]} + */ +function hostedRuleNames(eslintPrefix) { + return Object.keys(HOSTED_RULE_ORIGIN).filter((name) => HOSTED_RULE_ORIGIN[name] === eslintPrefix); +} + +/** + * Converts oxlint's name for a finding, like `typescript(no-explicit-any)`, to ESLint's rule ID, like + * `@typescript-eslint/no-explicit-any`, so the lint output and the oxlint seatbelt baseline use the + * same rule names as ESLint. A code that does not look like `plugin(rule)` comes back unchanged. + * @param {string} code + * @returns {string} + */ +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') { + 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/checkWithOxc.d.mts b/config/reactCompiler/checkWithOxc.d.mts index 48c2f6b3155e..507fb4104ff9 100644 --- a/config/reactCompiler/checkWithOxc.d.mts +++ b/config/reactCompiler/checkWithOxc.d.mts @@ -16,6 +16,9 @@ type CompilationResult = { }; declare function checkReactCompilerWithOxc(source: string, filename: string): CompilationResult; +declare function shouldSkipReactCompiler(filename: string): boolean; +declare function isFileMemoized(source: string, filename: string): boolean; export type {CompilationResult, CompilerError}; +export {isFileMemoized, shouldSkipReactCompiler}; export default checkReactCompilerWithOxc; diff --git a/config/reactCompiler/checkWithOxc.mjs b/config/reactCompiler/checkWithOxc.mjs index 6056c39878aa..5c196ac8cdf3 100644 --- a/config/reactCompiler/checkWithOxc.mjs +++ b/config/reactCompiler/checkWithOxc.mjs @@ -137,4 +137,27 @@ function checkReactCompilerWithOxc(source, filename) { } } +/** + * Paths lint never compiles: tests and node_modules. Shared by the lint pipeline, its worker and the + * oxlint gate so every caller skips the same files. + * @param {string} filename + * @returns {boolean} + */ +function shouldSkipReactCompiler(filename) { + const normalized = filename.split(path.sep).join('/'); + return normalized.includes('/tests/') || normalized.startsWith('tests/') || normalized.includes('node_modules/'); +} + +/** + * The one memoization verdict lint uses: false for skipped paths, otherwise whether the compiler + * emitted a memo cache for the whole file. + * @param {string} source + * @param {string} filename + * @returns {boolean} + */ +function isFileMemoized(source, filename) { + return !shouldSkipReactCompiler(filename) && checkReactCompilerWithOxc(source, filename).memoized; +} + +export {isFileMemoized, shouldSkipReactCompiler}; export default checkReactCompilerWithOxc; 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..3f15e9dd6e6f --- /dev/null +++ b/config/reactCompiler/suppressedRules.mjs @@ -0,0 +1,10 @@ +/** + * Lint reports that tell you to memoize by hand. Manual memoization is banned in files React Compiler + * already memoizes, so ReactCompilerFilter and the oxlint plugins drop these reports in those files. + * It's plain `.mjs` because oxlint's JS plugins can't load TypeScript. + */ +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/; + +export {EXHAUSTIVE_DEPS_USECALLBACK_USEMEMO_PATTERN, RULES_SUPPRESSED_BY_REACT_COMPILER}; diff --git a/cspell.json b/cspell.json index a8ba6acd0e15..a7514e30e541 100644 --- a/cspell.json +++ b/cspell.json @@ -141,7 +141,14 @@ "Ephermeral", "Erste", "Español", + "codemod", "detekt", + "oxfmtrc", + "oxlint", + "oxlintrc", + "tsgo", + "tsgolint", + "ungated", "Fabrikam", "fillyjonk", "hemulen", @@ -952,6 +959,7 @@ "proguard", "purchaseamount", "purchasecurrency", + "purgeable", "QAPR", "QBWC", "qrcode", @@ -1020,6 +1028,8 @@ "socklen", "syscalls", "serveo", + "nonconstructor", + "setstate", "setuptools", "sharee", "shareeEmail", @@ -1295,6 +1305,9 @@ "tests/unit/hooks/useLetterAvatars.test.tsx", "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", "evals/proposalPolice/fixtures/**" 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 d21797f34977..ab25f78a6629 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", @@ -82,6 +83,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 +94,8 @@ "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..479aa2464eca --- /dev/null +++ b/oxlint-migration/README.md @@ -0,0 +1,126 @@ +# 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` | +| `checkOnyxReadAndMemoRules.py` | `no-unsafe-onyx-read`, `no-onyx-get-snapshot-key` and `no-manual-memoization`, line by line, through the lint pipeline and raw: the Onyx rules depend on where a file sits in `src/`, and the memoization rule on a pipeline stage | `npm run oxlint-onyx-read-and-memo-rules` | +| `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 2026-10-27, from the seatbelt totals after a full run of each linter: + +| rule | ESLint | oxlint | | +| --- | --- | --- | --- | +| `set-state-in-effect` | 117 | 118 | one extra in `src/components/MapView/MapViewImpl.web.tsx`, not yet explained | +| `refs` | 202 | 183 | bridge collapses ESLint's duplicate reports (see below) | +| `purity` | 0 | 29 | all `Date`-during-render; ESLint's compiler build misses them, kept on purpose | +| `preserve-manual-memoization` | 2 | 2 | same two findings; ESLint anchors on the `useMemo` callback, the bridge on its dependency array | +| `immutability` | 6 | 7 | one extra | +| `globals` | 0 | 2 | two 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..916301b70e18 --- /dev/null +++ b/oxlint-migration/checkConfigDrift.py @@ -0,0 +1,216 @@ +"""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. (3) The OnyxUtils message says "a short-lived Onyx connection without a view" on the oxlint side instead of naming Onyx.connectWithoutView: the reviewer bot in .github/workflows/onyxConnectWithoutViewReviewers.yml matches `Onyx.connectWithoutView(` on added lines and requested reviewers for the message string. ESLint keeps the method name; the advice is the same'), + '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`. + '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). 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..36b459032874 --- /dev/null +++ b/oxlint-migration/checkEslintDirectives.mjs @@ -0,0 +1,123 @@ +#!/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;'], + 'a line-comment eslint-disable, which ESLint ignores': ['// eslint-disable eqeqeq', 'a == 1;'], + 'a line-comment eslint-enable, which ESLint ignores': ['/* eslint-disable eqeqeq */', '// eslint-enable eqeqeq', 'a == 1;'], + 'a block disable after the problem on the same line': ['a == 1; /* eslint-disable eqeqeq */', 'b == 2;'], + 'a problem after a block disable on the same line': ['/* eslint-disable eqeqeq */ a == 1;', 'b == 2;'], + 'a block enable between two problems on one line': ['/* eslint-disable eqeqeq */', 'a == 1; /* eslint-enable eqeqeq */ b == 2;', 'c == 3;'], +}; + +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), + }); + + // At the operator's column, where eqeqeq reports, so directives on the same line are placed as ESLint sees them. + for (const [index, text] of source.split('\n').entries()) { + for (const match of text.matchAll(/==/g)) { + innerContext.report({loc: {line: index + 1, column: match.index}, 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/checkOnyxReadAndMemoRules.py b/oxlint-migration/checkOnyxReadAndMemoRules.py new file mode 100644 index 000000000000..3dfe020c9732 --- /dev/null +++ b/oxlint-migration/checkOnyxReadAndMemoRules.py @@ -0,0 +1,229 @@ +"""Do oxlint and ESLint agree on no-unsafe-onyx-read, no-onyx-get-snapshot-key and no-manual-memoization? + + python3 oxlint-migration/checkOnyxReadAndMemoRules.py + +Neither of the usual harnesses can test these three rules: + + - The rule-tester replays `.test.js` RuleTester files. Their upstream tests are TypeScript Jest + suites (tests/unit/NoUnsafeOnyxReadRuleTest.ts, tests/unit/NoOnyxGetSnapshotKeyRuleTest.ts) and a + bun suite (tests/tooling/noManualMemoization.test.ts), which it cannot import. + - Both Onyx rules decide by where the file sits in the repo (src/components, src/pages, src/hooks + and tests may read; src/libs may not), and the fixture and rule-tester trees live under + oxlint-migration/, where every read is simply "outside the allowed path". + - no-manual-memoization is reported by the rule everywhere and then kept only in files both React + Compilers memoize. That second half is ReactCompilerFilter, a lint pipeline stage, so raw tool + output says nothing about what a developer sees from `npm run lint`. + +So the probe files go into src/ at the paths the rules care about, and each tool runs twice: + + pipeline scripts/lint/index.ts --linter=, the gate CI and `npm run lint` run + raw the tool alone, which is what an editor integration shows + +Every line that should report carries an `// expect: ` marker. The run fails if the two tools +disagree on any (file, line, rule), or if either tool misses a marker or reports an unmarked line, so +a rule that silently stopped running fails as loudly as a bridge difference. The raw run covers only +the opted-out memoization file, where the pipeline drops what the raw tools report. + +The files are deleted in a finally. +""" + +import json +import os +import re +import shutil +import subprocess +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PROBE_DIR_NAME = '__oxlintOnyxReadAndMemoProbe' +PROBE_DIRS = [os.path.join(ROOT, 'src', parent, PROBE_DIR_NAME) for parent in ('components', 'libs')] + +RULES = ('rulesdir/no-unsafe-onyx-read', 'rulesdir/no-onyx-get-snapshot-key', 'rulesdir/no-manual-memoization') +MARKER = re.compile(r'//\s*expect:\s*(.+)$') + +COMPONENT_READS = """import React, {useEffect} from 'react'; +import {View} from 'react-native'; +import Onyx from 'react-native-onyx'; + +import ONYXKEYS from '@src/ONYXKEYS'; + +const sessionPromise = Onyx.get(ONYXKEYS.SESSION); // expect: rulesdir/no-unsafe-onyx-read + +function readSession() { + return Onyx.get(ONYXKEYS.SESSION); // expect: rulesdir/no-unsafe-onyx-read +} + +function OnyxReads({method}: {method: 'get'}) { + Onyx.get(ONYXKEYS.SESSION).then(() => {}); // expect: rulesdir/no-unsafe-onyx-read + useEffect(() => { + Onyx.get(ONYXKEYS.ACCOUNT).then(() => {}); // expect: rulesdir/no-unsafe-onyx-read + }, []); + const onPress = () => Onyx.get(ONYXKEYS.SESSION); + const onReportPress = () => Onyx.get(ONYXKEYS.COLLECTION.REPORT); // expect: rulesdir/no-onyx-get-snapshot-key + const onMethodPress = () => Onyx[method](ONYXKEYS.SESSION); // expect: rulesdir/no-unsafe-onyx-read + return ( + + ); +} + +export default OnyxReads; +export {readSession, sessionPromise}; +""" + +LIBS_READ = """import Onyx from 'react-native-onyx'; + +import ONYXKEYS from '@src/ONYXKEYS'; + +async function submit() { + return Onyx.get(ONYXKEYS.SESSION); // expect: rulesdir/no-unsafe-onyx-read +} + +export default submit; +""" + +MEMO_TEMPLATE = """import {{useFocusEffect}} from '@react-navigation/native'; +import React, {{memo, useCallback, useMemo}} from 'react'; +import {{View}} from 'react-native'; + +function ManualMemo({{items, onSelect}}: {{items: string[]; onSelect: (item: string) => void}}) {{ +{directive} const count = useMemo(() => items.length, [items]); // expect: {expect} + const select = useCallback(() => onSelect(items.at(0) ?? ''), [items, onSelect]); // expect: {expect} + const onFocus = useCallback(() => {{}}, []); + useFocusEffect(onFocus); + return ( + + ); +}} + +export default memo(ManualMemo); // expect: {expect} +""" + +# file name -> (directory, source, also compared raw) +PROBES = { + 'OnyxReads.tsx': ('components', COMPONENT_READS, False), + 'libRead.ts': ('libs', LIBS_READ, False), + # React Compiler memoizes this one, so the pipeline keeps the three manual memoization reports. + 'Memoized.tsx': ('components', MEMO_TEMPLATE.format(directive='', expect='rulesdir/no-manual-memoization'), False), + # 'use no memo' stops React Compiler, so the pipeline drops what the raw tools report. + 'OptedOut.tsx': ('components', MEMO_TEMPLATE.format(directive=" 'use no memo';\n", expect='raw-only rulesdir/no-manual-memoization'), True), +} + + +def write_probes(): + paths = {} + for name, (parent, source, _) in PROBES.items(): + directory = os.path.join(ROOT, 'src', parent, PROBE_DIR_NAME) + os.makedirs(directory, exist_ok=True) + path = os.path.join(directory, name) + with open(path, 'w') as handle: + handle.write(source) + paths[name] = os.path.relpath(path, ROOT) + return paths + + +def expected_findings(paths, mode): + """(file, line, rule) for every marker. `raw-only` markers count only in the raw run.""" + findings = set() + for name, path in paths.items(): + for number, line in enumerate(PROBES[name][1].splitlines(), start=1): + match = MARKER.search(line) + if not match: + continue + for rule in match.group(1).split(','): + rule = rule.strip() + if rule.startswith('raw-only '): + if mode == 'raw': + findings.add((path, number, rule.removeprefix('raw-only '))) + elif mode == 'pipeline': + findings.add((path, number, rule)) + return findings + + +def run_json(command, label): + out = subprocess.run( + command, + capture_output=True, + text=True, + cwd=ROOT, + env={**os.environ, 'NODE_OPTIONS': '--max-old-space-size=8192', 'SEATBELT_DISABLE': '1'}, + ) + start = out.stdout.find('{') if label != 'raw eslint' else out.stdout.find('[') + try: + return json.JSONDecoder().raw_decode(out.stdout[start:])[0] + except (json.JSONDecodeError, ValueError): + sys.exit(f'{label} run failed (exit {out.returncode}):\n{out.stdout[:800]}{out.stderr[:800]}') + + +def pipeline_findings(linter, paths): + report = run_json(['bun', os.path.join('scripts', 'lint', 'index.ts'), f'--linter={linter}', '--format=json', *paths], f'{linter} pipeline') + return {(os.path.relpath(m['filePath'], ROOT), m['line'], m['ruleID']) for m in report['messages'] if m.get('ruleID') in RULES} + + +def raw_eslint_findings(paths): + report = run_json(['npx', 'eslint', '--no-cache', '--format', 'json', *paths], 'raw eslint') + return {(os.path.relpath(f['filePath'], ROOT), m['line'], m['ruleId']) for f in report for m in f['messages'] if m.get('ruleId') in RULES} + + +def raw_oxlint_findings(paths): + report = run_json(['npx', 'oxlint', '--format', 'json', *paths], 'raw oxlint') + findings = set() + for diagnostic in report['diagnostics']: + match = re.match(r'^([\w@/.-]+)\((.+)\)$', diagnostic.get('code') or '') + rule = f'{match.group(1)}/{match.group(2)}' if match else diagnostic.get('code') + if rule in RULES: + for label in diagnostic.get('labels', [])[:1]: + findings.add((os.path.relpath(os.path.join(ROOT, diagnostic['filename']), ROOT), label['span']['line'], rule)) + return findings + + +def compare(mode, expected, eslint, oxlint): + failures = [] + for finding in sorted(expected | eslint | oxlint): + in_eslint, in_oxlint, wanted = finding in eslint, finding in oxlint, finding in expected + if in_eslint != in_oxlint: + verdict = 'FAIL: the two tools disagree' + elif in_eslint != wanted: + verdict = 'FAIL: both tools ' + ('miss the marker' if wanted else 'report an unmarked line') + else: + verdict = 'agree' + path, line, rule = finding + print(f'{mode:9} {os.path.basename(path)}:{line:<4} {rule:40} {str(in_eslint):>6} {str(in_oxlint):>6} {verdict}') + if verdict != 'agree': + failures.append(f'{mode} {path}:{line} {rule}: {verdict}') + return failures + + +def main(): + paths = write_probes() + raw_paths = [path for name, path in paths.items() if PROBES[name][2]] + try: + pipeline_eslint = pipeline_findings('eslint', list(paths.values())) + pipeline_oxlint = pipeline_findings('oxlint', list(paths.values())) + raw_eslint = raw_eslint_findings(raw_paths) + raw_oxlint = raw_oxlint_findings(raw_paths) + finally: + for directory in PROBE_DIRS: + shutil.rmtree(directory, ignore_errors=True) + + print(f'{"mode":9} {"location":20} {"rule":40} {"eslint":>6} {"oxlint":>6} verdict') + failures = compare('pipeline', expected_findings(paths, 'pipeline'), pipeline_eslint, pipeline_oxlint) + failures += compare('raw', expected_findings({name: path for name, path in paths.items() if PROBES[name][2]}, 'raw'), raw_eslint, raw_oxlint) + + print() + if failures: + print(f'{len(failures)} check(s) FAILED:') + for failure in failures: + print(f' {failure}') + sys.exit(1) + print('Both tools report the same lines for all three rules, through the lint pipeline and raw.') + + +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..666aadd81a4b --- /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 React Compiler memoizes. 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 React Compiler memoizes 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 React Compiler memoizes, live where it does not.') + + +if __name__ == '__main__': + main() diff --git a/oxlint-migration/checkReactCompilerRust.mjs b/oxlint-migration/checkReactCompilerRust.mjs new file mode 100644 index 000000000000..969ede2eb113 --- /dev/null +++ b/oxlint-migration/checkReactCompilerRust.mjs @@ -0,0 +1,186 @@ +#!/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 {analyze, CONFIG_CATEGORY, ENVIRONMENT, 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('\n10. ENVIRONMENT holds exactly the plugin validations that change what the bridge reports'); +// eslint-plugin-react-hooks lints with its own environment table (COMPILER_OPTIONS in the bundle). The +// bridge lists only the entries whose absence changes a report. Proving that in both directions here +// means a flipped oxc default, or a plugin bump that turns on another validation, fails this run. +const optionsBlock = bundle.match(/const COMPILER_OPTIONS = \{[\s\S]*?environment: \{([\s\S]*?)\n\s*\}/)?.[1] ?? ''; +const pluginValue = (text) => { + const alias = bundle.match(new RegExp(`(?:const|var|let) ${text} = ([^;]+);`)); + return JSON.parse((alias?.[1] ?? text).replaceAll("'", '"')); +}; +const pluginEnvironment = Object.fromEntries([...optionsBlock.matchAll(/^\s*(\w+):\s*(.+?),?\s*$/gm)].map(([, key, value]) => [key.replace('JSX', 'Jsx'), pluginValue(value)])); +check(Object.keys(pluginEnvironment).length > 0, 'the plugin bundle still exposes COMPILER_OPTIONS.environment', `${Object.keys(pluginEnvironment).length} entries`); +const environmentProbeFiles = [...FIXTURES.map(([fixture]) => path.join(FIXTURE_DIR, fixture)), ...fs.readdirSync(PROBE_DIR).filter((name) => name.endsWith('.tsx') && !name.includes('.probe.')).map((name) => path.join(PROBE_DIR, name))]; +const reportsWith = (environment) => JSON.stringify(environmentProbeFiles.map((file) => analyze(file, fs.readFileSync(file, 'utf8'), environment))); +const baselineReports = reportsWith(ENVIRONMENT); +const disagreeing = Object.keys(ENVIRONMENT).filter((key) => JSON.stringify(pluginEnvironment[key]) !== JSON.stringify(ENVIRONMENT[key])); +check(disagreeing.length === 0, 'every ENVIRONMENT entry has the value the plugin uses', disagreeing.length ? `differs: ${disagreeing.join(', ')}` : `${Object.keys(ENVIRONMENT).length} entries`); +const inert = Object.keys(ENVIRONMENT).filter((key) => { + const without = {...ENVIRONMENT}; + delete without[key]; + return reportsWith(without) === baselineReports; +}); +check(inert.length === 0, 'omitting any ENVIRONMENT entry changes a fixture report', inert.length ? `inert, drop: ${inert.join(', ')}` : `${Object.keys(ENVIRONMENT).length} of ${Object.keys(ENVIRONMENT).length}`); +const missing = Object.keys(pluginEnvironment).filter((key) => !(key in ENVIRONMENT) && reportsWith({...ENVIRONMENT, [key]: pluginEnvironment[key]}) !== baselineReports); +check(missing.length === 0, 'no other plugin validation changes a fixture report', missing.length ? `add: ${missing.join(', ')}` : `${Object.keys(pluginEnvironment).length - Object.keys(ENVIRONMENT).length} entries already match oxc or feed ignored categories`); + +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..7f35008da7c4 --- /dev/null +++ b/oxlint-migration/checkSidecarCoverage.py @@ -0,0 +1,183 @@ +"""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', + ), + 'rulesdir/no-unsafe-onyx-read': ( + 'npm run oxlint-onyx-read-and-memo-rules', + 'the rule decides by where the file sits in src/, which the fixture and replay trees under oxlint-migration/ cannot reproduce, and its upstream tests are a TypeScript Jest suite the replay cannot import', + ), + 'rulesdir/no-onyx-get-snapshot-key': ( + 'npm run oxlint-onyx-read-and-memo-rules', + 'same probe; its upstream tests are a TypeScript Jest suite too', + ), + 'rulesdir/no-manual-memoization': ( + 'npm run oxlint-onyx-read-and-memo-rules', + 'what decides whether a finding survives is the ReactCompilerFilter pipeline stage, so the probe runs both tools through the lint pipeline as well as raw', + ), +} + +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..d49d27a13fb7 --- /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 React Compiler memoizes, 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..3a49fd40d0fd --- /dev/null +++ b/oxlint-migration/port-probe/fixtures.manifest.json @@ -0,0 +1,344 @@ +{ + "no-unreachable-loop": { + "fixture": "fixtures/unreachableLoop.ts", + "oxlintRule": "eslint/no-unreachable-loop", + "expected": 1, + "why": "native since 2026-10-21; the false positives that made it hosted (oxc#24251 switch, oxc#25048 catch) are fixed in oxlint 1.89.0" + }, + "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..25797c242d5a --- /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 React Compiler memoizes 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 ( + <> +
+