
# Linting

`oxc-tsrx` lints `.tsrx` files, and ordinary JS/TS files too, with OXC's real
lint rules. The code you wrote goes through the same rule engine Oxlint uses. If
a diagnostic or a fix would land on the placeholder code the tool generates
instead of on yours, it is dropped rather than shown.

## How a lint run works

<!-- pipeline:lint -->

Four steps:

1. **Scan.** Read the file once and find the TSRX-only syntax.
2. **Copy.** Build a valid TSX copy in memory, the *projection*. Your code is
   copied over unchanged, and only the TSRX controls become placeholders.
3. **Lint.** OXC parses and lints that copy, once.
4. **Map back.** Move every error onto your file, so the line and column point
   at what you wrote.

Step 2 also records which piece of the copy came from which piece of your file,
and that record is what makes step 4 exact.

Ordinary `.js`, `.jsx`, `.ts`, and `.tsx` files skip steps 1 and 2 entirely.
They go straight to OXC, exactly like running `oxlint` yourself.

Here is one real file at each of those stages, with the actual copy the tool
built and the actual diagnostics it returned:

<!-- projection-explorer -->

## Usage

Real output, captured at build time. The sample src/Counter.tsrx has a debugger statement, a var declaration, an unawaited promise, and a wrong type annotation; src/View.tsx has an unused variable.

```text
# The report is one line of JSON; jq shows the diagnostics readably
$ oxc-tsrx --format=json src/Counter.tsrx src/View.tsx \
  | jq '.diagnostics'
[
  {
    "filename": "src/Counter.tsrx",
    "rule": "no-debugger",
    "code": "eslint(no-debugger)",
    "severity": "warning",
    "message": "`debugger` statement is not allowed",
    "labels": [
      {
        "span": {
          "offset": 217,
          "length": 9
        }
      }
    ]
  },
  {
    "filename": "src/View.tsx",
    "rule": "no-unused-vars",
    "code": "eslint(no-unused-vars)",
    "severity": "warning",
    "message": "Variable 'seen' is declared but never used. Unused variables should start with a '_'.",
    "labels": [
      {
        "span": {
          "offset": 59,
          "length": 4
        },
        "message": "'seen' is declared here"
      }
    ]
  }
]

# Explicit configuration plus per-rule severity from the CLI
$ oxc-tsrx --format=json --config config/lint.json \
  --warn no-console --deny no-debugger src/Counter.tsrx | jq '.diagnostics'
[
  {
    "filename": "src/Counter.tsrx",
    "rule": "no-console",
    "code": "eslint(no-console)",
    "severity": "warning",
    "message": "Unexpected console statement.",
    "labels": [
      {
        "span": {
          "offset": 191,
          "length": 11
        }
      }
    ]
  },
  {
    "filename": "src/Counter.tsrx",
    "rule": "no-debugger",
    "code": "eslint(no-debugger)",
    "severity": "error",
    "message": "`debugger` statement is not allowed",
    "labels": [
      {
        "span": {
          "offset": 217,
          "length": 9
        }
      }
    ]
  }
]

# Apply safe fixes; here no-var rewrites var to const
$ oxc-tsrx --format=json --deny no-var --fix src/Counter.tsrx | jq '.oxcTsrx.fixes'
{
  "applied": 1,
  "rejected": 0
}

# Opt into the official TypeScript-Go rules
$ oxc-tsrx --format=json --type-aware src/Counter.tsrx | jq '.diagnostics'
[
  {
    "filename": "src/Counter.tsrx",
    "rule": "no-debugger",
    "code": "eslint(no-debugger)",
    "severity": "warning",
    "message": "`debugger` statement is not allowed",
    "labels": [
      {
        "span": {
          "offset": 219,
          "length": 9
        }
      }
    ]
  },
  {
    "filename": "src/Counter.tsrx",
    "rule": "no-floating-promises",
    "code": "typescript(no-floating-promises)",
    "severity": "warning",
    "message": "Promises must be awaited, add void operator to ignore.",
    "labels": [
      {
        "span": {
          "offset": 231,
          "length": 10
        }
      }
    ]
  }
]

# Or add full TypeScript compiler diagnostics on top
$ oxc-tsrx --format=json --type-check src/Counter.tsrx | jq '.diagnostics'
[
  {
    "filename": "src/Counter.tsrx",
    "rule": "no-debugger",
    "code": "eslint(no-debugger)",
    "severity": "warning",
    "message": "`debugger` statement is not allowed",
    "labels": [
      {
        "span": {
          "offset": 219,
          "length": 9
        }
      }
    ]
  },
  {
    "filename": "src/Counter.tsrx",
    "rule": "parse-error",
    "code": "typescript(TS2322)",
    "severity": "error",
    "message": "Type 'number' is not assignable to type 'string'.",
    "labels": [
      {
        "span": {
          "offset": 168,
          "length": 5
        }
      }
    ]
  },
  {
    "filename": "src/Counter.tsrx",
    "rule": "no-floating-promises",
    "code": "typescript(no-floating-promises)",
    "severity": "warning",
    "message": "Promises must be awaited, add void operator to ignore.",
    "labels": [
      {
        "span": {
          "offset": 231,
          "length": 10
        }
      }
    ]
  }
]
```

CLI severity flags (`--allow`/`-A`, `--warn`/`-W`, `--deny`/`-D`) override
whatever the config file says for that rule. Exit codes: `0` clean, `1` when
there are errors (or the configured warning policy fails), `2` for usage or
engine errors.

## Why you never see errors in code you didn't write

The copy OXC reads contains placeholder code you never typed, and now and then a
rule fires on a placeholder instead of on your code.

When that happens the diagnostic is dropped rather than shown, and the run
counts it so nothing disappears silently. The rule is simple: if an error does
not sit entirely inside code you wrote, you never see it.

## Safe fixes

`--fix` applies fixes directly to your original TSRX file, but only fixes
that touch purely your own code. After applying, the tool re-scans and
re-parses the result to confirm it's still valid before writing anything. A
fix that would touch the TSRX control syntax or span a placeholder boundary
is rejected.

## Configuration

Your `.oxlintrc.json` works as usual. `oxc-tsrx` searches upward from the
current directory to find it, or takes a `--config` path, and the ordinary
Oxlint fields all apply: rules, plugins, `env`, `globals`, `settings`,
`extends`, `overrides`, and `ignorePatterns`.

Three things behave differently here:

- **The config has to be JSON or JSONC.** A `.js` or `.ts` config is rejected up
  front rather than half-working.
- **Your own JavaScript lint plugins do run on `.tsrx`**, but they see the TSX
  copy rather than the code you wrote. Read
  [Custom JavaScript plugins](/integrations/custom-js-plugins) before relying on
  them.
- **Type-aware lint is opt-in** and needs exactly `oxlint-tsgolint` 0.24.0. A
  missing or mismatched version fails loudly instead of quietly switching
  itself off.

[Configuration](/integrations/configuration) lists every supported field.

## What is tested

Tests prove that `no-debugger` and `no-unused-vars` report at the right line and
column in your file, and that `no-var` fixes apply correctly, inside every
control-flow form (`@if`, `@for`, `@switch`, `@try`). Type-aware linting has its
own suite covering the same ground.

What is *not* claimed: that every OXC rule behaves identically around the
placeholders. That stays unclaimed until it is proven.
