Skip to content

fix(conformance): close ~170 hybrid gaps (parserServices gating, console global, type-param constraints)#7

Open
ericsssan wants to merge 7 commits into
mainfrom
fix/conformance-gap-grind
Open

fix(conformance): close ~170 hybrid gaps (parserServices gating, console global, type-param constraints)#7
ericsssan wants to merge 7 commits into
mainfrom
fix/conformance-gap-grind

Conversation

@ericsssan

@ericsssan ericsssan commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Summary

Closes a large batch of differential-conformance gaps in hybrid mode (EZ_RUN_NATIVE=1): 322 → 153 hybrid gaps, zero regressions. Each step was validated against the full corpus, and the per-rule regression gate stayed clean.

Changes

parserServices type-info gating (js/eslint-runner.js) — the biggest single win. The light parserServices was exposing a non-null program and the TS node maps unconditionally, so type-aware plugin rules (sonarjs/*, eslint-plugin-n) ran and fired on cases where the oracle — which has no projectService/project configured — bails via isRequiredParserServices and produces nothing. Now gated on a new _typeInfoRequested flag:

  • program is null when no type info was requested → type-aware rules bail like the oracle.
  • TS node maps are suppressed only for plain-JS files without type info (espree parity). TypeScript files keep them (matching @typescript-eslint/parser, which always supplies the syntactic maps with a null program), so rules like naming-convention are unaffected.
  • undefined flag (LSP / non-differential entry points) preserves prior behavior.

This cleared the sonarjs FP cluster: unused-named-groups, new-operator-misuse, post-message, no-alphabetical-sort, non-number-in-arithmetic-expression, in-operator-type-error. Two regressions it surfaced (n/no-sync, naming-convention) were caught by the regression gate and fixed via the JS-language condition. Promoted @typescript-eslint facade rules are unaffected (their fixtures are 100% projectService-configured).

no-confusing-void-expression 52/107 → 107/107. console was wrongly listed in BUILTIN_ES2015_GLOBALS — it's a host/env global, not an ECMAScript builtin (the JS side already has it in _ENV_GLOBALS). Pre-declaring it as an implicit-global symbol shadowed ez-checker's curated Console type, so console.log(...) typed as any instead of void. Removed it; unresolved console still reports as a global ref via isGlobalReference.

strict-boolean-expressions 185/212 → 208/212. walkType now follows a generic type parameter's constraint (T extends boolean → boolean) instead of treating it as any, matching getConstrainedTypeAtLocation.

no-unnecessary-condition — 16 noOverlapBooleanExpression false negatives closed. involvesIndeterminateType / typesCanOverlap now resolve type params to their constraint, so <T extends object>(a: T) compared against null/undefined fires correctly.

consistent-type-imports 16 → 4 gaps. Detect export type { X } (es-parser emits read, not type_read, for those specifiers) via symbolRefsAllInTypeExports. Suppress the JSX-factory false positive (es-parser omits the implicit factory reference) only for the precise candidate: a default import whose name matches the JSX pragma (default React; honors settings.react.pragma) in a file that actually contains JSX elements — so a genuinely type-only default import, or any import in a JSX-free file, is still reported.

no-unnecessary-type-conversion — type-param constraint handling in typeIsBigIntish.

Upstream blockers filed

Several remaining gaps are blocked on the sibling repos and are tracked there: ez-checker #21/#22/#23 (implicit-global shadowing, Infinity/NaN, type-param constraints) and es-parser #33/#34/#49/#53 (export-type refs, JSX-factory ref, arrow returnType on the ESTree node, parameter-type-annotation scoping). The Ez-side workarounds in this PR cover what can be fixed without those.

Validation

  • Hybrid corpus: 322 → 153 gaps, 0 regressions (per-rule native + hybrid delta gate clean).
  • zig build napi builds clean; baseline updated.

Note: the full zig build CLI target is broken at main by an unrelated, in-progress es-parser v0.2.1 migration in main.zig/parallel.zig. Those WIP files are intentionally not part of this PR. All changes here are validated through the napi target and the differential runner.

Test plan

EZ_RUN_NATIVE=1 bun tests/differential/run.js

Expect: Corpus hybrid: 81123/81276 pass (99.8%), 153 gaps … No regressions.


Update — additional commit: no-duplicate-type-constituents 79/82 → 82/82. type T = A | A with undeclared A was wrongly flagged; an undeclared name is the TS error type (two are non-equivalent). typeNodeIsError now treats a type_reference whose name is neither a known type nor an in-scope type parameter as the error type — type params (f<T>(): T | T) and lib/user types still flag. Hybrid now 150 gaps.

Remaining gaps are the hard tail: indent (45 FP, whitespace), object-shorthand (es-parser #49), no-unsafe-call / restrict-plus-operands (ez-checker #21 / #24), no-misleading-character-class (Unicode + const-string resolution), no-shadow (scope-manager FE/CE-name representation), and CFG-based rules.

Update — additional commit: no-misleading-character-class 165/174 → 168/174. Resolve const-string pattern/flags variables (const p = "[👍]"; new RegExp(p), const flags = ""; new RegExp("[\u{1f44d}]", flags)) via constInitializerOf. CallSourceMap gains an override span so diagnostics report on the argument node (ESLint can't map char offsets through a resolved variable). Hybrid now 147 gaps. Remaining no-misleading gaps are unrelated sub-issues (surrogate-pair ranges, ZWJ-sequence off-by-one, template-substitution evaluation, v-flag nested classes).

Update — additional commit: no-misleading-character-class 168/174 → 170/174 — correct ZWJ-sequence diagnostics. ESLint's zwj runs on UTF-16 code units, so /[👨‍👩‍👦]/ is two joins (one per ZWJ), not one coalesced sequence. Both code paths now branch on the u/v flag and model the surrogate boundary at the emoji's UTF-8 midpoint (literal path) or via the source map (escaped path); _offsetToLineCol counts one unit for a span endpoint inside a 4-byte char (inert for existing rules). Hybrid now 141 gaps.

Update — indent Cat A + fidelity harness: Built tests/differential/estree-fidelity.js (per-node type+range diff vs @typescript-eslint/parser) and used it to root-cause the indent gaps. The TS-class cases (abstract class + public) weren't an indent-algorithm problem at all — the runner's TS-syntax-in-.js re-parse only upgraded jsts, never jsxtsx, so jsx-enabled cases linted against a malformed (ErrorNode) AST. Extended the upgrade to jsx→tsx. indent 1079→1085/1090; hybrid now 135 gaps. Remaining indent: Cat C (es-parser let-ASI #59) and Cat B (JSX fidelity).

Update — checked es-parser #49 (object-shorthand): #49 (arrow returnType not exposed) is already resolved — the adapter exposes ArrowFunctionExpression.returnType. The actual avoidExplicitReturnArrows FNs were two other things: (1) the js→ts re-parse only adopted the TS AST when fully clean, but the code comment says "strictly fewer ErrorNodes" — fixed (object-shorthand 18→6 FN); (2) the remaining 6 are a parenthesized arrow return type ((): (void) =>) becoming an ErrorNode, filed as es-parser #62.

JS parses skip stashLastParse but still update tl_tagged_gen. With the
old conditional-invalidate, a prior TS parse's stash stayed live: the
next openReuseImpl for a JS gen found a tag match but loaded the wrong
(TS) AST, passing stale parent_indices to Checker.init() → infinite
loop in enclosingScopeIdx (no bounds check in ReleaseFast).

Fix: unconditionally call invalidateReuseStash() at the start of every
parse so JS parses leave tl_last_ast=null. openReuseImpl then falls
back to the fresh-parse path as intended.

Also removes the _activeRuleId allowlist gate from the `program` getter
so any rule that accesses parserServices.program gets the real facade,
and updates the baseline to match the resulting coverage changes.
…sonarjs FP cluster

Differential hybrid gaps reduced substantially with zero regressions, validated
via the full corpus run each step.

parserServices type-info gating (js/eslint-runner.js) — the single biggest win:
the light parserServices exposed a non-null `program` and TS node maps
unconditionally, so type-aware plugin rules (sonarjs/*, eslint-plugin-n) ran and
fired where the oracle (no projectService/project) bails via
isRequiredParserServices. Now gated on `_typeInfoRequested`:
  - `program` is null when no type info was requested → rules bail like the oracle.
  - TS node maps suppressed only for plain-JS files w/o type info (espree parity);
    TS files keep them (matching @typescript-eslint/parser: maps + null program),
    so naming-convention etc. are unaffected.
  - undefined flag (LSP / non-differential entry points) preserves prior behavior.
Cleared the sonarjs FP cluster (unused-named-groups, new-operator-misuse,
post-message, no-alphabetical-sort, non-number-in-arithmetic-expression,
in-operator-type-error). Facade rules verified unchanged.

no-confusing-void-expression 52/107 -> 107/107: `console` was wrongly in
BUILTIN_ES2015_GLOBALS (it's a host/env global, not an ECMAScript builtin). The
pre-declared implicit-global symbol shadowed ez-checker's curated `Console`
type, so console.log(...) typed as `any` not `void`. Removed it; unresolved
`console` still reports as a global ref via isGlobalReference.

strict-boolean-expressions 185/212 -> 208/212: walkType now follows a generic
type param's constraint (`T extends boolean` -> boolean) instead of treating it
as `any`, matching getConstrainedTypeAtLocation.

no-unnecessary-condition: 16 noOverlapBooleanExpression FNs closed —
involvesIndeterminateType / typesCanOverlap now resolve type params to their
constraint (`<T extends object>(a: T)` vs null/undefined fires correctly).

consistent-type-imports 16 -> 4 gaps: detect `export type { X }` (es-parser emits
`read` not `type_read` for those specifiers, #33) via symbolRefsAllInTypeExports.
Suppress the JSX-factory FP (es-parser omits the implicit factory ref, #34) only
for the precise candidate: a default import whose name matches the JSX pragma
(default React; honours settings.react.pragma) in a file that actually contains
JSX elements — so a genuinely type-only default import, or any import in a
JSX-free file, is still reported.

no-unnecessary-type-conversion: type_param constraint handling in typeIsBigIntish.
…erences

`type T = A | A` where `A` is undeclared was flagged as a duplicate (3 FPs).
TypeScript types an undeclared name as the error type, and two error types are
non-equivalent, so TSe does not report. The checker can't distinguish an
undeclared name from a type parameter (both resolve as "unresolved") and
surfaces undeclared names as a named type_ref/any rather than the error type, so
`typeNodeIsError` now also treats a type_reference whose name is neither a known
type NOR an in-scope type parameter as the error type. Type parameters
(`f<T>(): T | T`) and lib/user types still flag correctly.

no-duplicate-type-constituents 79/82 -> 82/82. Hybrid 153 -> 150 gaps, 0 regressions.
…s variables

`const p = "[👍]"; new RegExp(p)` and `const flags = ""; new RegExp("[\u{1f44d}]", flags)`
were skipped because the pattern/flags arguments weren't string/template literals.
Now resolve an effective-const identifier to its string-literal initializer via
constInitializerOf, for both the pattern and the flags argument.

When the pattern comes from a resolved constant, char offsets can't be mapped
back into the original source, so ESLint reports on the whole argument node —
CallSourceMap gains an `override` span that collapses every reported span to the
identifier. allowEscape is also disabled on that path (escape forms in the
variable's literal aren't "visible" at the usage site), matching ESLint.

no-misleading-character-class 165/174 -> 168/174. Hybrid 150 -> 147 gaps, 0 regressions.
…(surrogate-aware)

ESLint's `zwj` generator runs on UTF-16 code units: without u/v a supplementary
emoji is two surrogates, so a ZWJ join is [low-of-before, ZWJ, high-of-after],
and consecutive joins coalesce only across a single BMP unit — NOT across a
supplementary emoji (whose two surrogates differ). We keep emoji as one
Character, so `/[👨‍👩‍👦]/` was reported as one joined sequence instead of two.

Both code paths now branch on the u/v flag:
- without u/v: emit one zwj per join when separated by a supplementary char; the
  literal path reports at the emoji's UTF-8 midpoint (start+2) to model the
  surrogate boundary, and the call path lets the source map collapse those onto
  the original escape.
- with u/v: every char is one code point, so consecutive joins coalesce as before.

_offsetToLineCol now counts one UTF-16 unit when a span endpoint lands inside a
4-byte char (the surrogate-midpoint offsets above). No existing rule reports
mid-codepoint, so this is inert elsewhere.

no-misleading-character-class 168/174 -> 170/174 (cases 95 literal, 143 escaped).
Hybrid 147 -> 141 gaps, 0 regressions.
…ty harness

The runner compensates for TS syntax in mis-marked `.js` files by re-parsing as
`ts` when the `js` parse errors and `ts` is clean — but only for `parseLang ===
"js"`. Cases with `ecmaFeatures.jsx` get `parseLang === "jsx"`, so `abstract
class`/`public` (whose ESLint-test oracle used @typescript-eslint/parser)
produced a malformed jsx AST with ErrorNodes, and `indent` (the most
AST-sensitive rule) false-positived across the whole method body. Extend the
upgrade to jsx→tsx so the runner parses these the way the oracle did.

indent 1079/1090 -> 1085/1090 (6 cases: the abstract-class/accessibility-modifier
group). Hybrid 141 -> 135 gaps, 0 regressions.

Also adds tests/differential/estree-fidelity.js — a diff harness comparing ez's
ESTree (type + range, per node) against @typescript-eslint/parser for a snippet.
It pinned this down (identical AST; only the malformed-vs-clean parse differed)
and is reusable for future parser-fidelity work (the remaining indent JSX cases).
…ly fully-clean)

The TS-syntax-in-`.js` re-parse only adopted the TS AST when it had ZERO
ErrorNodes, but the code comment documents the intent as "strictly fewer
ErrorNodes than JS" — and computed `_jsErrCount` was then unused. Cases where TS
is much cleaner but not perfect (e.g. object-shorthand's avoidExplicitReturnArrows
test: 69 JS ErrorNodes vs 1 TS ErrorNode from a `(void)` parenthesized return
type) kept the malformed 69-error JS AST, so the rule saw garbage.

Adopt the TS AST when `_tsErrCount < _jsErrCount`, matching the comment and the
parser the oracle used (@typescript-eslint/parser).

object-shorthand 18 FN -> 6 FN (the 6 remaining are the `(void)` rows, filed as
es-parser #62 — a parenthesized arrow return type becomes an ErrorNode). Full
corpus: 135 gaps unchanged at the case level, 0 regressions.

Context: while checking es-parser #49 (arrow returnType) — that adapter fix is
already in (ArrowFunctionExpression.returnType is exposed); the real object-shorthand
blocker was this upgrade condition plus the #62 parse gap.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant