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#
The file you wrote, byte for byte. Nothing is changed on disk at any point.
export function Cart({ items }: Props) @{
var total = 0;
debugger;
<section class="cart">
@if (items.length > 0) {
@for (const item of items; key item.id) {
<Row item={item} />
}
} @else {
<Empty />
}
</section>
}One byte-oriented pass finds every TSRX control token and records its exact position. This is the real overlay for the file above:
| Token | Bytes |
|---|---|
FunctionBody | 39–40 |
If | 101–102 |
For | 132–133 |
Else | 216–217 |
The TSRX syntax becomes valid TSX placeholders in an in-memory copy; your code is copied verbatim. This is the actual projection:
export function Cart({ items }: Props) /*_t0_0*/{
var total = 0;
debugger;
<section class="cart">
{_t0_W0_({async *_t0_M0_(){/*_t0_N0S__*//*_t0_1*/if (items.length > 0) {
/*_t0_2*/for (const item of _t0_H0_(/*_t0_R0S__*/items/*_t0_R0E__*/,_t0_KH0_(/*_t0_K0S__*/item.id/*_t0_K0E__*/),_t0_HE0_)) {
<Row item={item} />
}
} /*_t0_3*/else {
<Empty />
}/*_t0_N0E__*/}},_t0_E0_)}
</section>
}The real OXC parser and linter run on that copy, exactly once. These are the genuine diagnostics for this file:
eslint(no-unused-vars): Variable 'total' is declared but never used. Unused variables should start with a '_'. at your bytes 48–53eslint(no-debugger): `debugger` statement is not allowed at your bytes 61–70
Every diagnostic is translated to your original bytes. Anything that would point at placeholder code is dropped instead of shown, so errors always land on code you wrote.
Four steps:
- Scan. Read the file once and find the TSRX-only syntax.
- Copy. Build a valid TSX copy in memory, the projection. Your code is copied over unchanged, and only the TSRX controls become placeholders.
- Lint. OXC parses and lints that copy, once.
- 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:
export function Cart({ items }: Props) @{
var total = 0;
debugger;
<section class="cart">
@if (items.length > 0) {
@for (const item of items; key item.id) {
<Row item={item} />
}
} @else {
<Empty />
}
</section>
}export function Cart({ items }: Props) /*_t0_0*/{
var total = 0;
debugger;
<section class="cart">
{_t0_W0_({async *_t0_M0_(){/*_t0_N0S__*//*_t0_1*/if (items.length > 0) {
/*_t0_2*/for (const item of _t0_H0_(/*_t0_R0S__*/items/*_t0_R0E__*/,_t0_KH0_(/*_t0_K0S__*/item.id/*_t0_K0E__*/),_t0_HE0_)) {
<Row item={item} />
}
} /*_t0_3*/else {
<Empty />
}/*_t0_N0E__*/}},_t0_E0_)}
</section>
}Real oxc-tsrx output for this file. Every position points at the authored TSRX on tab 1, never at the scaffolding on tab 2:
eslint(no-unused-vars)(warning): Variable 'total' is declared but never used. Unused variables should start with a '_'. at authored bytes 48–53eslint(no-debugger)(error): `debugger` statement is not allowed at authored bytes 61–70
Usage#
# 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
.jsor.tsconfig 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 before relying on them. - Type-aware lint is opt-in and needs exactly
oxlint-tsgolint0.24.0. A missing or mismatched version fails loudly instead of quietly switching itself off.
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.