Linting
oxc-tsrx lints .tsrx files (and ordinary JS/TS files) with OXC's real lint
rules. Authored descendants use the canonical rule engine; diagnostics or
fixes that would touch TSRX projection scaffolding are suppressed rather than
presented as equivalent Oxlint behavior.
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.
Step by step:
- The file is scanned once to find the TSRX-only syntax.
- A valid-TSX copy is built in memory (the projection). Your code is copied into it unchanged; only the TSRX control syntax is replaced with TSX placeholders. The tool records which byte ranges in the copy correspond to which byte ranges in your file.
- OXC parses and lints that copy, once.
- Every diagnostic is translated back through those recorded ranges, so the
error you see points at the right line and column in your
.tsrxfile.
Ordinary .js, .jsx, .ts, and .tsx files skip steps 1–2 entirely: the
file goes straight to OXC, exactly like running oxlint yourself.
See the projection for yourself#
The tabs below show one real file at each stage. The projected TSX is the
actual output of the projection engine, and the diagnostics are actual
oxc-tsrx output. Notice how the @-controls become scaffold comments and
wrappers in tab 2, while your code (like var total and debugger;) is
byte-for-byte identical, which is what makes exact mapping possible:
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 placeholders in the projected copy are code too, and sometimes a lint rule fires on them instead of on your code. When that happens, the whole diagnostic is dropped (and counted in metadata) rather than shown. The rule is simple: a diagnostic must land entirely inside bytes you wrote, or you don't see it. No error will ever point into invisible scaffolding.
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#
oxc-tsrx finds one .oxlintrc.json or .oxlintrc.jsonc by searching from
your working directory upward, or takes an explicit --config/-c path. The
usual Oxlint fields work: built-in rules and plugins with options, env,
globals, settings, extends, overrides, ignorePatterns, and warning
policy.
JavaScript lint plugins and direct-native JS/TS config files are rejected up
front instead of half-working. Type-aware lint is supported only through an
explicit flag and the exact verified oxlint-tsgolint 0.24.0 executable; a
missing or mismatched tool fails without silently falling back. See
Configuration for the exact support matrix
and Custom JavaScript plugins for the
tested parser adapters and remaining host boundary.
What is tested#
The retained test suite proves no-debugger and no-unused-vars report at
the correct original positions, and that no-var fixes apply correctly,
across all the control-flow forms (@if, @for, @switch, @try). It does
not claim every OXC rule behaves identically around the placeholders; that
broader claim is deliberately left unclaimed until proven. A separate matrix
proves authored type-aware labels, explicit .tsrx imports, one type process
per batch, and identity-safe fixes without changing the default one-OXC-parse
path.