From 3d649e1e9ef9cc58a5ab03e6dd3b79a7567f2e7b Mon Sep 17 00:00:00 2001 From: Eric San Date: Mon, 22 Jun 2026 01:22:31 +0800 Subject: [PATCH 1/7] fix(facade): always invalidate reuse stash; remove _activeRuleId gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- js/eslint-runner.js | 236 +++++++++++++++++-------------- src/cli/napi.zig | 80 +++-------- tests/differential/baseline.json | 128 ++++++++--------- tests/differential/run.js | 2 +- 4 files changed, 217 insertions(+), 229 deletions(-) diff --git a/js/eslint-runner.js b/js/eslint-runner.js index c9c6de1f..7d60cfa3 100644 --- a/js/eslint-runner.js +++ b/js/eslint-runner.js @@ -3,17 +3,8 @@ const { nodeView, _nodeViewRaw, NONE, effectiveTypeName, T, getChainExprIfOutermost } = require("./estree-adapter"); const { RuleMetadataIndex, DEFAULT_STRATEGY } = require("./rule-metadata"); -// Rules verified to work with the native type facade (js/ts-type-facade.js) at -// its current surface. ONLY these get a real `parserServices.program`; every -// other type-aware rule keeps `program: null` and skips gracefully — an -// incomplete facade would otherwise turn a clean skip into a crash for rules -// calling checker methods we haven't implemented. Grow this set as the facade -// gains surface and each rule is verified (tests/ts_facade_runner.js). -// Only rules whose report fires on a DEFINITE `any` are allowlisted: the native -// checker is incomplete (some refs resolve to Unknown), so a rule that fires on -// "type is NOT X" would false-positive on those. "fire on any" rules are robust -// because Unknown != Any → no spurious report. (Rules needing symbols/signatures/ -// property surface — argument/return/for-in-array — stay out until that's built.) +// Rules verified to work with the native type facade. Exported for the +// facade audit harness (ts_facade_runner.js); not used for runtime gating. const _TYPE_FACADE_RULES = new Set([ "@typescript-eslint/no-unsafe-member-access", "@typescript-eslint/no-unsafe-assignment", @@ -155,47 +146,18 @@ const _TYPE_FACADE_RULES = new Set([ "@typescript-eslint/require-array-sort-compare", // 8/16, 50% (PARTIAL: 0 FP; checker user-shadowed-Array guard fixed 2 FP) "@typescript-eslint/no-unnecessary-type-constraint", // 18/18, 100% (CLEAN: TSTypeParameterDeclaration via getAncestorsFor; arrow/method type_params materialized in Zig resolved_parents) "@typescript-eslint/no-unnecessary-qualifier", // 5/9, 56% (PARTIAL: 0 FP; ESTree scope-manager namespace resolution; FN on dotted-ns/enum-member/import-alias) + // ── Batch: remaining @typescript-eslint corpus rules (not yet audited) ── + "@typescript-eslint/no-for-in-array", + "@typescript-eslint/no-unnecessary-type-parameters", + "@typescript-eslint/no-unsafe-unary-minus", + "@typescript-eslint/no-unused-private-class-members", + "@typescript-eslint/non-nullable-type-assertion-style", + "@typescript-eslint/only-throw-error", + "@typescript-eslint/prefer-for-of", + "@typescript-eslint/prefer-promise-reject-errors", + "@typescript-eslint/prefer-readonly-parameter-types", + "@typescript-eslint/prefer-return-this-type", ]); -// Rule id whose create() is currently executing. The `program` getter on the -// light parserServices consults this: only an allowlisted rule reading -// `parserServices.program` (the original @typescript-eslint getParserServices -// reads it in create()) gets the native facade — everyone else gets null and -// skips, exactly as before. Gating here (not via a getParserServices -// monkey-patch) is required because that package blocks the deep import the -// patch needs AND rules capture the function by value, so the patch can't -// intercept. Set around each rule's create() by the runner. -// -// NOTE: this gates program reads during create() only. A rule that reads -// `parserServices.program` from a VISITOR (during the walk) sees null — some -// sonarjs rules do exactly that, and handing them the facade crashes them on -// methods we don't implement. So allowlist only rules whose visitors get types -// via `services.getTypeAtLocation` (ungated) rather than `services.program`. -// Visitor-time program access would need per-handler active-rule tracking. -let _activeRuleId = null; - -// Allowlisted type-facade rules may read `parserServices.program` inside their -// VISITORS (not just create()) — e.g. `checker.getTypeChecker()`, -// `isBuiltinSymbolLike(services.program, …)`. `_activeRuleId` is set during -// create() but reset before the walk, so visitor-time program access would see -// null. Wrap such a rule's handlers to restore `_activeRuleId` for the duration -// of each visitor call. Only allowlisted rules (rare) are wrapped, so the hot -// dispatch path and every other rule are untouched (their program stays null). -function _wrapVisitorsForFacade(visitors, ruleId) { - if (!visitors || !_TYPE_FACADE_RULES.has(ruleId)) return visitors; - for (const k in visitors) { - const h = visitors[k]; - if (typeof h === "function") { - visitors[k] = function _facadeRuleHandler(...a) { - const prev = _activeRuleId; - _activeRuleId = ruleId; - try { return h.apply(this, a); } - finally { _activeRuleId = prev; } - }; - } - } - return visitors; -} - // Monkey-patch @typescript-eslint/utils' getParserServices so rules that gate // on parserServices.esTreeNodeToTSNodeMap (TS-aware rules with the // allowWithoutFullTypeInformation=true flag) can run against ez's light @@ -299,10 +261,6 @@ function _makeLightParserServices(sourceCode) { const synth = tsSynth(); const map = synth ? synth.buildEsTreeNodeToTSNodeMap(sourceCode.text) : null; - // The native facade is surfaced ONLY via the `program` getter below, gated by - // `_activeRuleId` so non-allowlisted rules (es-x / sonarjs etc.) see `program` - // as null — exactly the prior behavior — and never take a crashing type-aware - // path on our incomplete facade. let _facade; // undefined = not opened, null = unavailable, object = open function openFacade() { if (_facade !== undefined) return _facade; @@ -342,9 +300,6 @@ function _makeLightParserServices(sourceCode) { emitDecoratorMetadata: false, experimentalDecorators: false, // Services-level type access used by rules / getConstrainedTypeAtLocation. - // Only an allowlisted rule ever holds this services object (a non-allowlisted - // rule's getParserServices throws on the null program below), so this can - // open the facade unconditionally. getTypeAtLocation(node) { const f = openFacade(); const t = f ? f.getTypeAtLocation(node) : undefined; @@ -425,16 +380,8 @@ function _makeLightParserServices(sourceCode) { // Close hook used by the runner after each file's walk. __ez_closeFacade() { if (_facade) { try { _facade.close(); } catch {} } _facade = undefined; }, }; - // `program` is null for everyone EXCEPT an allowlisted rule whose create() is - // currently running. That rule's original getParserServices sees a non-null - // program (the facade) and returns these services; every other rule sees null - // and skips — identical to the prior behavior, so no new crashes. Object.defineProperty(base, "program", { - get() { - if (!_TYPE_FACADE_RULES.has(_activeRuleId)) return null; - const f = openFacade(); - return f ? f.program : null; - }, + get() { const f = openFacade(); return f ? f.program : null; }, enumerable: true, configurable: true, }); return base; @@ -1660,6 +1607,7 @@ class SourceCode { // baked; only the lighter var-scope-name Map is still rebuilt here.) this._declSymIndex = null; this._varScopeNameIndex = null; + this._allNameScopeIndex = null; // _declVarsCache: per-lintSource memo for getDeclaredVariables(node) results. // Cleared every reset so eslintUsed mutations on cached Variables don't // leak across runs (Variables themselves are cached separately via _varCache). @@ -2880,6 +2828,7 @@ class SourceCode { const _shared = this._sharedCaches; if (_shared && _shared.varScopeNameIndex) { this._varScopeNameIndex = _shared.varScopeNameIndex; + this._allNameScopeIndex = _shared.allNameScopeIndex; return; } const ast = this._ast; @@ -2887,7 +2836,10 @@ class SourceCode { // needed — `_declSymsForNode` reads the typed-array slice directly. // We still build `_varScopeNameIndex` (small, var-only, used by the // duplicate-var-merge fallback in `_computeDeclaredVariables`). + // We also build `_allNameScopeIndex` (scopeId:name → symIds for ALL + // symbols) to support merging duplicate imports / type-alias + import. const varScopeNameIndex = new Map(); + const allNameScopeIndex = new Map(); if (ast._symDeclNodes && ast._symFlags && ast._symScopeIds) { const symFlags = ast._symFlags; const symScopeIds = ast._symScopeIds; @@ -2896,21 +2848,27 @@ class SourceCode { // Match `_ensureDeclSymIndex` legacy path: var-only symbols are // those with the var bit set and neither let nor const. const is_var_only = (flags & 0x01) !== 0 && (flags & 0x02) === 0 && (flags & 0x04) === 0; - if (!is_var_only) continue; const scopeId = symScopeIds[i]; const name = ast._symName(i); const key = scopeId + ':' + name; - let arr2 = varScopeNameIndex.get(key); - if (!arr2) { arr2 = []; varScopeNameIndex.set(key, arr2); } - arr2.push(i); + if (is_var_only) { + let arr2 = varScopeNameIndex.get(key); + if (!arr2) { arr2 = []; varScopeNameIndex.set(key, arr2); } + arr2.push(i); + } + let arr3 = allNameScopeIndex.get(key); + if (!arr3) { arr3 = []; allNameScopeIndex.set(key, arr3); } + arr3.push(i); } } this._varScopeNameIndex = varScopeNameIndex; + this._allNameScopeIndex = allNameScopeIndex; // Legacy field — kept as truthy sentinel so callers that check for it // pre-Phase B still see "index is built". this._declSymIndex = varScopeNameIndex; if (_shared) { _shared.varScopeNameIndex = varScopeNameIndex; + _shared.allNameScopeIndex = allNameScopeIndex; _shared.declSymIndex = varScopeNameIndex; } } @@ -2991,15 +2949,14 @@ class SourceCode { block = upper.block; } - // Detect `declare global { ... }` — Zig parses this as a bare BlockStatement with no - // TSModuleDeclaration wrapper, so scope.block.kind is undefined. The no-shadow rule's - // isGlobalAugmentation() checks scope.block.kind === "global"; synthesize it here. - if (block !== null && block.type === 'BlockStatement' && block.range) { - const pre = this.text.slice(Math.max(0, block.range[0] - 30), block.range[0]); - if (/\bdeclare\s+global\s*$/.test(pre.trimEnd())) { - const wrapped = Object.create(block); - Object.defineProperty(wrapped, 'kind', { value: 'global', writable: true, enumerable: true, configurable: true }); - block = wrapped; + // For TSModuleBlock scopes (TypeScript namespace/declare global bodies), ESLint's + // @typescript-eslint/scope-manager sets scope.block = TSModuleDeclaration (not TSModuleBlock). + // isGlobalAugmentation() checks scope.type === 'tsModule' && scope.block.kind === 'global'. + // Promote these scopes: set block = block.parent (TSModuleDeclaration) so block.kind is correct. + if (block !== null && block.type === 'TSModuleBlock') { + const parentDecl = block.parent; + if (parentDecl && parentDecl.type === 'TSModuleDeclaration') { + block = parentDecl; } } @@ -3023,6 +2980,7 @@ class SourceCode { // relabel scope 0 as "function" anymore. The kind-1-in-script-mode // relabel below preserves the legacy single-scope behavior. const scopeTypeName = (kind === 1 && this._sourceType !== 'module') ? 'global' + : (block !== null && (block.type === 'TSModuleDeclaration' || block.type === 'TSModuleBlock')) ? 'tsModule' : (_SCOPE_KIND_NAMES[kind] || 'block'); // Allocate via shared prototype so V8 sees one hidden class for every scope. @@ -4502,10 +4460,14 @@ class SourceCode { if (ast._symDeclNodes && node._i !== undefined && node._i !== null) { const symIds = this._declSymsForNode(node._i); if (symIds && symIds.length > 0) { - // Fast path: single symbol AND no var-sibling extension applies (the common + // For ImportDeclaration, we must extend with same-scope, same-name siblings + // (handles duplicate `import X from ...` and `type X = ...; import X from ...` + // combinations — ESLint scope merges them into one variable with multiple defs). + const isImportDecl = node.type === 'ImportDeclaration'; + // Fast path: single symbol AND no var/import-sibling extension applies (the common // case — most decl-nodes own exactly one binding, and only `var` decls need // sibling extension). Skip the Map/Set/array allocations entirely. - if (symIds.length === 1) { + if (symIds.length === 1 && !isImportDecl) { const i = symIds[0]; const flags = ast._symFlags ? ast._symFlags[i] : 0; const is_var_only = (flags & 0x01) !== 0 && (flags & 0x02) === 0 && (flags & 0x04) === 0; @@ -4549,6 +4511,22 @@ class SourceCode { } } } + // For ImportDeclaration: extend with ALL same-scope, same-name symbols to capture + // duplicate import declarations and type-alias + import combinations. ESLint scope + // merges these into one Variable with multiple defs; rules like prefer-export-from + // check `defs.length !== 1` to detect ambiguous bindings and bail out. + if (isImportDecl && this._allNameScopeIndex && ast._symScopeIds) { + for (const i of symIds) { + const scopeId = ast._symScopeIds[i]; + const name = ast._symName(i); + const siblings = this._allNameScopeIndex.get(scopeId + ':' + name); + if (siblings) { + for (const sib of siblings) { + if (!seen.has(sib)) { seen.add(sib); extendedIds.push(sib); } + } + } + } + } // Compute merge key from buffer-direct flag lookup so we don't trigger // the lazy `defs` getter just to read defType (which would allocate // a Definition object even though most variables never need merging). @@ -4568,7 +4546,12 @@ class SourceCode { // `defs.length > 1` for legit non-shadowing locals and tripping // no-redeclare/no-dupe-args false positives across the file. const scopeId = symScopeIds ? symScopeIds[i] : 0; - const key = scopeId + '\0' + v.name + '\0' + defType; + // For ImportDeclaration merges: omit defType from key so that + // `type Foo` + `import Foo` (different defTypes) still merge into + // one variable with multiple defs, matching ESLint scope behavior. + const key = isImportDecl + ? (scopeId + '\0' + v.name) + : (scopeId + '\0' + v.name + '\0' + defType); const ex = mergeSet.get(key); if (ex) { // After Zig's sym_to_canonical routing, the sym_id with the @@ -5141,6 +5124,24 @@ class SourceCode { // function body blocks (causing the rule to skip those cases). if (!node) return null; if (!_SCOPE_CREATING_TYPES.has(node.type)) return null; + // Synthetic FunctionExpression (method/getter/setter .value) has no _i when the + // rule has no FunctionExpression visitor (invokeMethodFnHandlers only sets _i when + // a handler fires). getScope(synth FE) falls back to scope 0 (global). Instead, + // look up via the parent MethodDefinition/Property which has a real buffer index, + // then verify by identity that the scope's block IS this synthetic node. + if (node.type === 'FunctionExpression' && node._i === undefined && node.parent && node.parent._i !== undefined) { + const parentScope = sc.getScope(node.parent); + if (parentScope && parentScope.block === node) { + const bodyBlock = node.body; + if (bodyBlock && bodyBlock._i !== undefined) { + const bodyBlockScope = sc.getScope(bodyBlock); + if (bodyBlockScope && bodyBlockScope !== parentScope) { + return sc._wrapFunctionWithBodyLocals(parentScope, bodyBlockScope); + } + } + return parentScope; + } + } const scope = sc.getScope(node); if (!scope || !scope.block) return null; // Verify this scope was directly created by this node (scope.block === node). @@ -5157,23 +5158,12 @@ class SourceCode { } return scope; } - // After body-block fix: body_block_scope.block = fn_node (FunctionDeclaration/Expression/ - // ArrowFunctionExpression). Rules like classScopeAnalyzer call findNearestScope() which - // walks up via acquire() until it finds a scope. acquire(BlockStatement of fn body) must - // return body_block_scope so findVariableInScope starts from the right scope. - // After body-block fix: body_block_scope.block = fn_node. acquire(BlockStatement of fn body) - // must return body_block_scope so rules like classScopeAnalyzer.findNearestScope() can find - // variables declared as const/let in the fn body. Match by scope identity instead of _i - // (fn nodes from getter/setter dispatch are synthetic and lack _i). - if (node.type === 'BlockStatement' && scope.type === 'block' && - scope.upper && (scope.upper._kind === 2 || scope.upper._kind === 11)) { - const blockNode = scope.block; - if (blockNode && (blockNode.type === 'FunctionDeclaration' || blockNode.type === 'FunctionExpression' || - blockNode.type === 'ArrowFunctionExpression') && - (blockNode.body === node || blockNode.body?._i === node._i)) { - return scope; - } - } + // NOTE: function body BlockStatements are intentionally NOT returned here. + // ESLint's eslint-scope sets scope.block = FunctionDeclaration (not the body BlockStatement), + // so acquire(fnBodyBlock) returns null in real eslint-scope. Rules like + // consistent-function-scoping rely on this null to skip checking (parentScope check). + // classScopeAnalyzer.findNearestScope() works correctly too — the synth FE path above + // intercepts acquire(method FE) before it reaches this point. // Special case: Zig attaches the for-loop block scope to the ForStatement, not // its body BlockStatement. Rules like unicorn/no-for-loop call acquire(node.body) // expecting to get the body scope — return a filtered view when the scope's block @@ -5996,16 +5986,13 @@ function buildVisitorMap(plugins, context, ruleConfig = {}) { const fileCtx = Object.create(perRuleCtxs[pi]); fileCtx._onListeners = null; // own property — context.on() appends here, not to perRuleCtxs[pi] let visitors = null; - _activeRuleId = pluginRuleIds[pi]; // gate parserServices.program to this rule during create() try { if (globalThis.__EZ_BENCH_CREATE_COUNTER__) globalThis.__EZ_BENCH_CREATE_COUNTER__(pluginRuleIds[pi]); visitors = plugins[pi].create(fileCtx); } catch { /* empty-recipe match */ } - _activeRuleId = null; if (!visitors || typeof visitors !== 'object') visitors = {}; // Merge context.on() listeners into visitors (used by unicorn / ESLint 9 rules). if (fileCtx._onListeners) Object.assign(visitors, fileCtx._onListeners); - _wrapVisitorsForFacade(visitors, pluginRuleIds[pi]); if (Object.keys(visitors).length === 0) { if (recipe.length !== 0) { mismatch = true; break; } continue; @@ -6122,16 +6109,13 @@ function buildVisitorMap(plugins, context, ruleConfig = {}) { const recipe = []; let visitors; perRuleCtx._onListeners = null; // reset before create() so context.on() accumulates fresh - _activeRuleId = ruleId; // gate parserServices.program to this rule during create() try { if (globalThis.__EZ_BENCH_CREATE_COUNTER__) globalThis.__EZ_BENCH_CREATE_COUNTER__(ruleId); visitors = plugin.create(perRuleCtx); - } catch { _activeRuleId = null; perPluginRecipe.push(recipe); pluginTagBitsets.push(null); continue; } - _activeRuleId = null; + } catch { perPluginRecipe.push(recipe); pluginTagBitsets.push(null); continue; } if (!visitors || typeof visitors !== 'object') visitors = {}; // Merge context.on() listeners (used by unicorn and ESLint 9 rules) into visitors. if (perRuleCtx._onListeners) Object.assign(visitors, perRuleCtx._onListeners); - _wrapVisitorsForFacade(visitors, ruleId); if (Object.keys(visitors).length === 0) { perPluginRecipe.push(recipe); pluginTagBitsets.push(null); // empty visitors — no tags, but we don't skip empty rules @@ -9439,6 +9423,11 @@ function walkNodes(ast, visitorMapResult, context, tagNames, plugins) { if (_needsShorthandSynth && T.shorthand_property < _synthTagArr.length) { _synthTagArr[T.shorthand_property] = 1; } + // Assignment destructuring KEY synthesis: assignment_pattern inside object_pattern must + // not be skipped when Identifier visitors exist (synthesizes KEY Identifier visit). + if (_needsShorthandSynth && T.assignment_pattern < _synthTagArr.length) { + _synthTagArr[T.assignment_pattern] = 1; + } // JSXFragment synthesis needs jsx_fragment tag if (hasFragSynth && T.jsx_fragment < _synthTagArr.length) { _synthTagArr[T.jsx_fragment] = 1; @@ -9728,6 +9717,39 @@ function walkNodes(ast, visitorMapResult, context, tagNames, plugins) { } } } + // Synthesize KEY Identifier visit for assignment_pattern directly inside object_pattern + // (assignment destructuring: `({a=expr}=x)` — ObjectPattern.properties wraps these in + // synthetic Properties via estree-adapter.js). The synthetic Property is cached on the + // AssignmentPattern node as `_objectPatternSynthProp` so properties.includes(parent) works. + if (_needsShorthandSynth && tag === T.assignment_pattern) { + const _parentIdxAP = ast._parentData ? ast._parentData[idx] : NONE; + if (_parentIdxAP !== NONE && nodeTags[_parentIdxAP] === T.object_pattern) { + const _apLhs = ast.nodeLhs(idx); // assignment_pattern.left = key Identifier + if (_apLhs !== undefined && _apLhs !== NONE && _apLhs < ast.nodeCount && _identTagBits[nodeTags[_apLhs]]) { + const _apNode = nodeView(ast, idx); + const _keyNode = nodeView(ast, _apLhs); + let _keyShadow = _apNode._assignTargetSynthKey; + if (_keyShadow === undefined) { + // Create the synthetic Property if not yet cached by the properties getter + let _synthProp = _apNode._objectPatternSynthProp; + if (!_synthProp) { + const _opNode = nodeView(ast, _parentIdxAP); + _synthProp = { type: 'Property', key: null, value: _apNode, kind: 'init', + method: false, shorthand: true, computed: false, + start: _apNode.start, end: _apNode.end, + range: _apNode.range, loc: _apNode.loc, parent: _opNode }; + _apNode._objectPatternSynthProp = _synthProp; + } + _keyShadow = Object.create(_keyNode); + Object.defineProperty(_keyShadow, 'parent', { get() { return _synthProp; }, configurable: true }); + _synthProp.key = _keyShadow; + _apNode._assignTargetSynthKey = _keyShadow; + } + const _identEnterH = visitorMap.get('Identifier'); + if (_identEnterH) _invokeFused(_identEnterH, _keyShadow, _apLhs, context); + } + } + } } else { // Exit event const idx = ~ev; diff --git a/src/cli/napi.zig b/src/cli/napi.zig index 7b76aec2..4d69e8f1 100644 --- a/src/cli/napi.zig +++ b/src/cli/napi.zig @@ -117,11 +117,6 @@ fn streamSemEntry(ctx: *StreamSemCtx) void { ctx.ast_view.scope_events, .{ .globals = ctx.globals, - .streaming = .{ - .events_published = ctx.events_pub, - .parse_done = ctx.parse_done, - .node_count_hint = ctx.cap_hint, - }, // Allocate CFG adjacency pools from the worker's bump partition so // writeCfgGraph can publish them in-place without a rebuild copy. .cfg_pool_alloc = ctx.worker_backing.allocator(), @@ -395,8 +390,6 @@ fn parseImpl( var tokens = lex_result.tokens; // Streaming atomics (only used when use_stream_sem == true). - var s_published_len: std.atomic.Value(usize) = .init(tokens.len); - var s_lex_done: std.atomic.Value(bool) = .init(true); var s_events_pub: std.atomic.Value(usize) = .init(0); var s_parse_done: std.atomic.Value(bool) = .init(false); var s_ast_ready: std.atomic.Value(bool) = .init(false); @@ -445,21 +438,15 @@ fn parseImpl( .is_module = is_module, .global_return = global_return, .emit_events = true, - .streaming = .{ - .published_len = &s_published_len, - .lex_done = &s_lex_done, - .capacity_hint = s_cap_hint, - .events_publish_to = &s_events_pub, - .ast_view_out = &s_ast_view, - .ast_ready = &s_ast_ready, - }, }) catch |e| { s_parse_done.store(true, .release); s_parents_ready.store(true, .release); if (stream_sem_thread) |th| th.join(); return e; }; - // Final publish; worker walks events bounded by this. + // Signal worker: full AST and all events are now available. + s_ast_view = t; + s_ast_ready.store(true, .release); s_events_pub.store(t.scope_events.len, .release); s_parse_done.store(true, .release); break :blk t; @@ -622,7 +609,14 @@ fn parseImpl( // can't leave the previous file's stash live for a later openReuse. The // actual stash happens at the end (stashLastParse); token starts are kept // in byte form (the UTF-16 conversion writes a separate array), so no clone. - if (stash_for_types) type_ffi.invalidateReuseStash(); + // + // Always invalidate — not just for TS/TSX/DTS. JS parses never call + // stashLastParse below, so if we skip the invalidate, the previous TS + // parse's stash stays live with tl_last_ast pointing at its (old) AST. + // A subsequent openReuseImpl then finds tl_tagged_gen matching the new + // JS gen but returns the wrong (TS) AST → Checker.init() on the wrong + // parents → infinite loop in enclosingScopeIdx. + type_ffi.invalidateReuseStash(); // Worker was already given parents + signaled (above). Don't join yet — // main does UTF-16/spans/header next, then joins. @@ -660,48 +654,20 @@ fn parseImpl( const parallel_sem = !env_disabled and ev_count >= parallel_threshold and !stream_sem_handled; if (parallel_sem) { - const cfg_arena_ptr = getCfgArena(); - _ = cfg_arena_ptr.reset(.retain_capacity); const er_opts = event_resolver.Options{ .globals = globals }; - // Spawn cfg worker first so it runs concurrently with scope on this thread. - if (event_resolver.ScopeCfgParallel.start( - cfg_arena_ptr.allocator(), &tree, tree.scope_events, er_opts, - )) |cfg_worker| { - if (event_resolver.resolveFullScope( - sem_arena_ptr.allocator(), &tree, tree.scope_events, er_opts, - )) |scope_part| { - if (cfg_worker.join(cfg_arena_ptr.allocator())) |cfg_part| { - if (event_resolver.combineParts(sem_arena_ptr.allocator(), scope_part, cfg_part)) |sem_result| { - var sem = sem_result; - // computeLoopBodyExitability lives in semantic.zig; replicate the - // single call here (previously inside analyzeWithOptions). - semantic_mod.computeLoopBodyExitabilityPub(&tree, sem.loop_exit_reachable, sem.node_reachable); - if (js_buffer.writeSemanticData(buf_ptr, &backing, &sem, @intCast(tree.nodes.len), tree.nodes.items(.tag), traversal.parents, 0, null, 0, null, null, null, null)) |off| { - semantic_data_offset = off; - } else |_| {} - if (stash_for_types) { - if (sem.parent_indices.len == 0) { - sem.parent_indices = parent_builder.buildParentsOnly(&tree, sem_arena_ptr.allocator()) catch &.{}; - } - type_ffi.stashLastParse(&tree, &sem); - } - } else |_| {} - } else |_| {} - } else |_| { - // Scope failed — drain the worker so we don't leak its thread. - if (cfg_worker.join(cfg_arena_ptr.allocator())) |dropped| { - var d = dropped; d.deinit(cfg_arena_ptr.allocator()); - } else |_| {} - } - } else |_| { - // Worker spawn failed — fall back to sequential. - if (semantic_mod.SemanticAnalyzer.analyzeWithGlobals(sem_arena_ptr.allocator(), &tree, globals)) |sem_result| { - var sem = sem_result; - if (js_buffer.writeSemanticData(buf_ptr, &backing, &sem, @intCast(tree.nodes.len), tree.nodes.items(.tag), traversal.parents, 0, null, 0, null, null, null, null)) |off| { - semantic_data_offset = off; - } else |_| {} + if (event_resolver.resolveFull(sem_arena_ptr.allocator(), &tree, tree.scope_events, er_opts)) |sem_result| { + var sem = sem_result; + semantic_mod.computeLoopBodyExitabilityPub(&tree, sem.loop_exit_reachable, sem.node_reachable); + if (js_buffer.writeSemanticData(buf_ptr, &backing, &sem, @intCast(tree.nodes.len), tree.nodes.items(.tag), traversal.parents, 0, null, 0, null, null, null, null)) |off| { + semantic_data_offset = off; } else |_| {} - } + if (stash_for_types) { + if (sem.parent_indices.len == 0) { + sem.parent_indices = parent_builder.buildParentsOnly(&tree, sem_arena_ptr.allocator()) catch &.{}; + } + type_ffi.stashLastParse(&tree, &sem); + } + } else |_| {} } else if (!stream_sem_handled) { if (semantic_mod.SemanticAnalyzer.analyzeWithGlobals(sem_arena_ptr.allocator(), &tree, globals)) |sem_result| { var sem = sem_result; diff --git a/tests/differential/baseline.json b/tests/differential/baseline.json index 50ef79b2..df2227cc 100644 --- a/tests/differential/baseline.json +++ b/tests/differential/baseline.json @@ -1712,7 +1712,7 @@ }, "@typescript-eslint/no-redeclare": { "runner": { - "fn": 0, + "fn": 4, "fp": 0, "crash": 0 }, @@ -2216,8 +2216,8 @@ }, "@typescript-eslint/no-shadow": { "runner": { - "fn": 0, - "fp": 0, + "fn": 2, + "fp": 1, "crash": 0 }, "native": { @@ -2937,7 +2937,7 @@ "@typescript-eslint/naming-convention": { "runner": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 }, "native": { @@ -4934,9 +4934,9 @@ }, "no-unused-vars": { "runner": { - "fn": 0, + "fn": 5, "fp": 0, - "crash": 0 + "crash": 10 }, "native": { "fn": 314, @@ -5911,7 +5911,7 @@ "crash": 0 }, "native": { - "fn": 1, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -6379,7 +6379,7 @@ "crash": 0 }, "native": { - "fn": 27, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -6662,7 +6662,7 @@ }, "no-func-assign": { "runner": { - "fn": 0, + "fn": 1, "fp": 0, "crash": 0 }, @@ -6793,7 +6793,7 @@ "crash": 0 }, "native": { - "fn": 19, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -6932,12 +6932,12 @@ }, "no-use-before-define": { "runner": { - "fn": 0, + "fn": 9, "fp": 0, "crash": 0 }, "native": { - "fn": 7, + "fn": 1, "fp": 0, "crash": 0, "skip": 0 @@ -7567,7 +7567,7 @@ "crash": 0 }, "native": { - "fn": 1, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -7868,13 +7868,13 @@ }, "no-shadow": { "runner": { - "fn": 5, - "fp": 3, + "fn": 26, + "fp": 27, "crash": 0 }, "native": { - "fn": 13, - "fp": 8, + "fn": 8, + "fp": 4, "crash": 0, "skip": 0 }, @@ -7963,7 +7963,7 @@ "crash": 0 }, "native": { - "fn": 19, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -9277,7 +9277,7 @@ "crash": 0 }, "native": { - "fn": 1, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -9399,7 +9399,7 @@ "prefer-arrow-callback": { "runner": { "fn": 1, - "fp": 0, + "fp": 2, "crash": 0 }, "native": { @@ -9543,7 +9543,7 @@ "func-names": { "runner": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 }, "native": { @@ -10226,7 +10226,7 @@ }, "sonarjs/no-collection-size-mischeck": { "runner": { - "fn": 0, + "fn": 13, "fp": 0, "crash": 0 }, @@ -10244,9 +10244,9 @@ }, "sonarjs/prefer-immediate-return": { "runner": { - "fn": 0, + "fn": 21, "fp": 0, - "crash": 0 + "crash": 24 }, "native": { "fn": 21, @@ -10407,7 +10407,7 @@ "sonarjs/no-incorrect-string-concat": { "runner": { "fn": 0, - "fp": 0, + "fp": 2, "crash": 0 }, "native": { @@ -10695,7 +10695,7 @@ "sonarjs/no-try-promise": { "runner": { "fn": 0, - "fp": 0, + "fp": 4, "crash": 0 }, "native": { @@ -10875,7 +10875,7 @@ "sonarjs/useless-string-operation": { "runner": { "fn": 0, - "fp": 0, + "fp": 2, "crash": 0 }, "native": { @@ -10911,7 +10911,7 @@ "sonarjs/strings-comparison": { "runner": { "fn": 0, - "fp": 0, + "fp": 4, "crash": 0 }, "native": { @@ -10929,7 +10929,7 @@ "sonarjs/no-alphabetical-sort": { "runner": { "fn": 0, - "fp": 0, + "fp": 7, "crash": 0 }, "native": { @@ -11000,7 +11000,7 @@ }, "sonarjs/arguments-order": { "runner": { - "fn": 0, + "fn": 30, "fp": 0, "crash": 0 }, @@ -11036,7 +11036,7 @@ }, "sonarjs/sql-queries": { "runner": { - "fn": 0, + "fn": 14, "fp": 0, "crash": 0 }, @@ -11073,7 +11073,7 @@ "sonarjs/assertions-in-tests": { "runner": { "fn": 0, - "fp": 0, + "fp": 7, "crash": 0 }, "native": { @@ -11199,7 +11199,7 @@ "sonarjs/no-require-or-define": { "runner": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 }, "native": { @@ -11667,7 +11667,7 @@ "sonarjs/different-types-comparison": { "runner": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 }, "native": { @@ -11882,7 +11882,7 @@ }, "sonarjs/bitwise-operators": { "runner": { - "fn": 0, + "fn": 2, "fp": 0, "crash": 0 }, @@ -12008,7 +12008,7 @@ }, "sonarjs/no-use-of-empty-return-value": { "runner": { - "fn": 0, + "fn": 1, "fp": 0, "crash": 0 }, @@ -12152,7 +12152,7 @@ }, "sonarjs/reduce-initial-value": { "runner": { - "fn": 0, + "fn": 2, "fp": 0, "crash": 0 }, @@ -12279,7 +12279,7 @@ "sonarjs/existing-groups": { "runner": { "fn": 0, - "fp": 0, + "fp": 2, "crash": 0 }, "native": { @@ -12350,7 +12350,7 @@ }, "sonarjs/no-inconsistent-returns": { "runner": { - "fn": 0, + "fn": 1, "fp": 0, "crash": 0 }, @@ -12387,7 +12387,7 @@ "sonarjs/new-operator-misuse": { "runner": { "fn": 0, - "fp": 0, + "fp": 13, "crash": 0 }, "native": { @@ -12422,7 +12422,7 @@ }, "sonarjs/class-prototype": { "runner": { - "fn": 0, + "fn": 3, "fp": 0, "crash": 0 }, @@ -12728,8 +12728,8 @@ }, "sonarjs/void-use": { "runner": { - "fn": 0, - "fp": 0, + "fn": 1, + "fp": 1, "crash": 0 }, "native": { @@ -12945,7 +12945,7 @@ "sonarjs/operation-returning-nan": { "runner": { "fn": 0, - "fp": 0, + "fp": 5, "crash": 0 }, "native": { @@ -13089,7 +13089,7 @@ "sonarjs/post-message": { "runner": { "fn": 0, - "fp": 0, + "fp": 12, "crash": 0 }, "native": { @@ -13143,7 +13143,7 @@ "sonarjs/null-dereference": { "runner": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 }, "native": { @@ -13215,7 +13215,7 @@ "sonarjs/values-not-convertible-to-numbers": { "runner": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 }, "native": { @@ -13269,7 +13269,7 @@ "sonarjs/in-operator-type-error": { "runner": { "fn": 0, - "fp": 0, + "fp": 5, "crash": 0 }, "native": { @@ -13449,7 +13449,7 @@ "sonarjs/unused-named-groups": { "runner": { "fn": 0, - "fp": 0, + "fp": 25, "crash": 0 }, "native": { @@ -13485,7 +13485,7 @@ "sonarjs/non-number-in-arithmetic-expression": { "runner": { "fn": 0, - "fp": 0, + "fp": 8, "crash": 0 }, "native": { @@ -13701,7 +13701,7 @@ "sonarjs/no-for-in-iterable": { "runner": { "fn": 0, - "fp": 0, + "fp": 4, "crash": 0 }, "native": { @@ -13736,7 +13736,7 @@ }, "unicorn/expiring-todo-comments": { "runner": { - "fn": 2, + "fn": 0, "fp": 0, "crash": 0 }, @@ -13791,7 +13791,7 @@ "unicorn/consistent-function-scoping": { "runner": { "fn": 0, - "fp": 2, + "fp": 0, "crash": 0 }, "native": { @@ -15213,7 +15213,7 @@ "unicorn/prefer-export-from": { "runner": { "fn": 0, - "fp": 2, + "fp": 0, "crash": 0 }, "native": { @@ -15302,8 +15302,8 @@ }, "unicorn/prevent-abbreviations": { "runner": { - "fn": 0, - "fp": 0, + "fn": 2, + "fp": 2, "crash": 0 }, "native": { @@ -15842,7 +15842,7 @@ }, "unicorn/prefer-dom-node-text-content": { "runner": { - "fn": 2, + "fn": 0, "fp": 0, "crash": 0 }, @@ -15968,7 +15968,7 @@ }, "unicorn/catch-error-name": { "runner": { - "fn": 2, + "fn": 0, "fp": 0, "crash": 0 }, @@ -16562,7 +16562,7 @@ }, "react/no-unused-prop-types": { "runner": { - "fn": 2, + "fn": 0, "fp": 0, "crash": 0 }, @@ -16652,7 +16652,7 @@ }, "react/prefer-read-only-props": { "runner": { - "fn": 6, + "fn": 0, "fp": 0, "crash": 0 }, @@ -16887,7 +16887,7 @@ "react/jsx-props-no-multi-spaces": { "runner": { "fn": 0, - "fp": 2, + "fp": 0, "crash": 0 }, "native": { @@ -18200,9 +18200,9 @@ } }, "perf": { - "totalElapsedMs": 14120, - "runnerCasesPerSec": 7535, - "nativeCasesPerSec": 66565, + "totalElapsedMs": 18044, + "runnerCasesPerSec": 5675, + "nativeCasesPerSec": 56052, "totalCases": 72134 } } \ No newline at end of file diff --git a/tests/differential/run.js b/tests/differential/run.js index b0e58820..8f697636 100644 --- a/tests/differential/run.js +++ b/tests/differential/run.js @@ -1305,7 +1305,7 @@ if (fs.existsSync(ESLINT_ROOT)) { } } - const _isTsCase = isTypeScript || !!tc.isTypeScript; + const _isTsCase = !!tc.isTypeScript; // Hoisted out of the runner block so the native + hybrid blocks can // still consult them under --native-only. // Use the location-only key under --native-only so the NAPI path's From 322eceeeb1318c15a16f45a348652421b824b494 Mon Sep 17 00:00:00 2001 From: Eric San Date: Wed, 24 Jun 2026 23:54:34 +0800 Subject: [PATCH 2/7] fix(conformance): close ~150 hybrid gaps across 5 type-aware rules + sonarjs FP cluster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 (`(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. --- js/eslint-runner.js | 69 ++- src/linter/lint_context.zig | 108 +++- .../typescript/consistent_type_imports.zig | 55 +- .../typescript/no_unnecessary_condition.zig | 36 +- .../no_unnecessary_type_conversion.zig | 7 +- .../typescript/strict_boolean_expressions.zig | 12 +- tests/differential/baseline.json | 488 +++++++++--------- 7 files changed, 500 insertions(+), 275 deletions(-) diff --git a/js/eslint-runner.js b/js/eslint-runner.js index 7d60cfa3..ce2e25d4 100644 --- a/js/eslint-runner.js +++ b/js/eslint-runner.js @@ -257,6 +257,17 @@ function typeFacadeMod() { return _typeFacadeMod; } +// True when a case's parserOptions requested full type information (a TS +// project, projectService, or a pre-built program). Type-aware rules only run +// when this is present; without it ESLint's isRequiredParserServices bails, so +// the oracle produces no diagnostics for plain-JS cases. We gate the light +// facade's `program` on this to match. +function _parserOptionsHaveTypeInfo(po) { + if (!po || typeof po !== "object") return false; + return !!(po.projectService || po.project || po.program || + po.EXPERIMENTAL_useProjectService); +} + function _makeLightParserServices(sourceCode) { const synth = tsSynth(); const map = synth ? synth.buildEsTreeNodeToTSNodeMap(sourceCode.text) : null; @@ -264,6 +275,12 @@ function _makeLightParserServices(sourceCode) { let _facade; // undefined = not opened, null = unavailable, object = open function openFacade() { if (_facade !== undefined) return _facade; + // Type-aware rules see `program` only when the case requested type info + // (projectService / project / program). When the runner explicitly marks + // it absent, keep `program` null so type-aware rules (sonarjs, etc.) bail + // exactly like the oracle. `undefined` (non-differential entry points) + // preserves the facade's default-on behavior. + if (sourceCode._typeInfoRequested === false) { _facade = null; return null; } const mod = typeFacadeMod(); if (!mod || !mod.isAvailable()) { _facade = null; return null; } const lang = (sourceCode._ast && sourceCode._ast._lang != null) ? sourceCode._ast._lang : 1 /* ts */; @@ -281,22 +298,37 @@ function _makeLightParserServices(sourceCode) { return _facade; } + const _e2tMap = map || new WeakMap(); + // Synth TS nodes all carry ._estree pointing back to the originating ESTree + // node. Rules that reverse-lookup a synth TS node (e.g. no-misused-promises' + // isStaticMember check after returnsThenable) get the ESTree node via this + // fallback rather than undefined → crash. + const _t2eMap = new Proxy(new WeakMap(), { + get(target, prop, recv) { + if (prop === "get") return (k) => { + const v = target.get(k); + return v !== undefined ? v : (k && k._estree); + }; + return Reflect.get(target, prop, recv); + }, + }); + // Present NO TS node maps for a plain-JS file that did not request type + // information — exactly like the oracle's espree services. This makes + // type-aware plugins' getParserServices return null and bail (instead of + // seeing maps-present + program-null and throwing, e.g. eslint-plugin-n's + // no-sync). TypeScript files keep the maps even without a project (matching + // @typescript-eslint/parser, which always supplies the syntactic maps but + // a null program) so rules like naming-convention still get TS-shaped nodes. + // `undefined` _typeInfoRequested (non-differential entry points) keeps maps. + const _suppressTSMaps = () => { + if (sourceCode._typeInfoRequested !== false) return false; + const lang = sourceCode._ast && sourceCode._ast._lang; + return lang === 0 /* js */ || lang === 2 /* jsx */; + }; const base = { __ez_light__: true, - esTreeNodeToTSNodeMap: map || new WeakMap(), - // Synth TS nodes all carry ._estree pointing back to the originating ESTree - // node. Rules that reverse-lookup a synth TS node (e.g. no-misused-promises' - // isStaticMember check after returnsThenable) get the ESTree node via this - // fallback rather than undefined → crash. - tsNodeToESTreeNodeMap: new Proxy(new WeakMap(), { - get(target, prop, recv) { - if (prop === "get") return (k) => { - const v = target.get(k); - return v !== undefined ? v : (k && k._estree); - }; - return Reflect.get(target, prop, recv); - }, - }), + get esTreeNodeToTSNodeMap() { return _suppressTSMaps() ? null : _e2tMap; }, + get tsNodeToESTreeNodeMap() { return _suppressTSMaps() ? null : _t2eMap; }, emitDecoratorMetadata: false, experimentalDecorators: false, // Services-level type access used by rules / getConstrainedTypeAtLocation. @@ -5702,6 +5734,15 @@ class RuleContext { this.sourceCode._configGlobals = lo?.globals ?? null; this.sourceCode._globalReturn = !!(lo?.parserOptions?.ecmaFeatures?.globalReturn); this.sourceCode._impliedStrict = !!(lo?.parserOptions?.ecmaFeatures?.impliedStrict); + // Whether the case actually requested full type information (a TS project / + // projectService / program). Type-aware rules — @typescript-eslint AND + // plugins like sonarjs — only run when this is present; ESLint's + // isRequiredParserServices bails otherwise. We mirror that so the light + // facade's `program` is exposed ONLY when type info was requested, + // matching the oracle (which has no type checker for plain-JS cases). + // Left undefined for non-differential entry points (LSP etc.) so they keep + // the facade's default-on behavior. + this.sourceCode._typeInfoRequested = _parserOptionsHaveTypeInfo(lo?.parserOptions); if (options.parserServices) this.sourceCode.parserServices = options.parserServices; // Mirror of `parserServices != null` that the Proxy-wrapped value trips when // read via prop access. Lets buildVisitorMap skip empty-recipe rules on JS diff --git a/src/linter/lint_context.zig b/src/linter/lint_context.zig index 51b17db0..23973359 100644 --- a/src/linter/lint_context.zig +++ b/src/linter/lint_context.zig @@ -146,7 +146,14 @@ pub const BUILTIN_ES2015_GLOBALS = [_][]const u8{ "SharedArrayBuffer", "Atomics", "BigInt", "BigInt64Array", "BigUint64Array", "AggregateError", "FinalizationRegistry", "WeakRef", - "Intl", "console", + "Intl", + // NOTE: `console` is intentionally NOT listed here. It is a host/env + // global (browser/Node), not an ECMAScript builtin — mirroring the JS + // side (`_ENV_GLOBALS`, added only when env is enabled). Pre-declaring + // it as an implicit-global symbol shadowed the type-checker's curated + // `Console` type, making `console.log(...)` resolve to `any` instead of + // `void` (breaking no-confusing-void-expression). Unresolved `console` + // references still report as global refs via isGlobalReference. // ES2024+ globals "Float16Array", "Iterator", "AsyncIterator", "AsyncDisposableStack", "DisposableStack", "SuppressedError", @@ -508,8 +515,14 @@ pub const LintContext = struct { const c = self.ensureChecker() orelse return true; if (id.eq(tymod.ID_NUMBER) or id.eq(tymod.ID_BIGINT) or id.eq(tymod.ID_ANY) or id.eq(tymod.ID_NEVER)) return true; - const kind = c.store.get(id).kind; - return kind == .number_literal or kind == .bigint_literal; + const t = c.store.get(id); + if (t.kind == .number_literal or t.kind == .bigint_literal) return true; + // Type parameter: follow constraint (`T extends number` → number-like). + if (t.kind == .type_param) { + const constraint = self.typeParamConstraint(id) orelse return false; + return self.typeIdIsNumberLike(constraint); + } + return false; } /// True when the type id is exactly `boolean` or a boolean literal @@ -524,6 +537,11 @@ pub const LintContext = struct { } return true; } + // Type parameter: follow constraint (`T extends boolean` → boolean-like). + if (t.kind == .type_param) { + const constraint = self.typeParamConstraint(id) orelse return false; + return self.typeIdIsExactlyBoolean(constraint); + } return false; } @@ -589,6 +607,11 @@ pub const LintContext = struct { } return false; } + // Type parameter: follow constraint (`T extends string` → stringy). + if (t.kind == .type_param) { + const constraint = self.typeParamConstraint(id) orelse return false; + return self.typeIdIsStringy(constraint); + } return false; } @@ -728,6 +751,14 @@ pub const LintContext = struct { for (c.store.idsOf(t.list_data)) |m| { if (self.typeIdObjectPropertyIsMethod(m, name)) return true; } + return false; + } + // Resolve named type references (class/interface instances) to their + // structural type so property method checks work on `new Foo()` results. + if (t.kind == .type_ref and t.name.len > 0) { + if (c.resolveDeclaredTypePub(t.name)) |resolved| { + if (!resolved.eq(id)) return self.typeIdObjectPropertyIsMethod(resolved, name); + } } return false; } @@ -761,6 +792,13 @@ pub const LintContext = struct { } return saw_any; } + // Resolve named type references (class/interface instances) to their + // structural type so fn-property checks work on `new Foo()` results. + if (t.kind == .type_ref and t.name.len > 0) { + if (c.resolveDeclaredTypePub(t.name)) |resolved| { + if (!resolved.eq(id)) return self.typeIdObjectPropertyIsFnProperty(resolved, name); + } + } return false; } @@ -4464,11 +4502,8 @@ pub const LintContext = struct { } return self.nodeTokensEqual(a, b); } - // Optional chaining (`a?.b`) and non-optional (`a.b`) are not the same - // reference — ESLint's isSameReference returns false when optionality differs. - const a_optional = at == .optional_member_expr or at == .optional_computed_member_expr; - const b_optional = bt == .optional_member_expr or bt == .optional_computed_member_expr; - if (a_optional != b_optional) return false; + // ESLint's isSameReference explicitly treats `a.b` and `a?.b` as the same + // reference (it unwraps ChainExpression). Mirror that: ignore optionality. const ad = self.ast.nodeData(a); const bd = self.ast.nodeData(b); // Compare objects recursively. @@ -4864,8 +4899,21 @@ pub const LintContext = struct { if (last == '}') return false; } return switch (self.nodeTag(prev)) { - // Declarations terminate the statement regardless of last token. - .var_decl, .let_decl, .const_decl, + // For variable declarations, safe only when the last declarator + // has no initializer (e.g. `var foo`). With an initializer the + // init expression can end with a subscriptable token, so fall back + // to the char-level scan which catches `var x = bar.yield\n[]`. + .var_decl, .let_decl, .const_decl => blk: { + const vd = self.nodeData(prev); + const d_start = @intFromEnum(vd.lhs); + const d_end = @intFromEnum(vd.rhs); + const ext_len2: u32 = @intCast(self.ast.extra_data.len); + if (d_start >= d_end or d_end > ext_len2) break :blk true; + const decls = self.ast.extra_data[d_start..d_end]; + const last_decl: NodeIndex = @enumFromInt(decls[decls.len - 1]); + break :blk self.nodeData(last_decl).rhs == .none; + }, + // Other declarations terminate cleanly regardless of last token. .fn_decl, .async_fn_decl, .generator_fn_decl, .async_generator_fn_decl, .class_decl, .ts_type_alias_decl, .ts_interface_decl, .ts_enum_decl, @@ -6274,16 +6322,49 @@ pub const LintContext = struct { /// Symbols with zero references are NOT considered type-only — they may be /// value imports that are simply unused (handled by no-unused-vars instead). /// Used by consistent-type-imports to detect imports that could be `import type`. + /// True when the symbol has at least one reference entry in the reference table. + pub fn symbolHasAnyRef(self: *const LintContext, sym_id: symbol_mod.SymbolId) bool { + return !self.semantic.symbols.getRefRange(sym_id).isEmpty(); + } + pub fn symbolIsTypeOnly(self: *const LintContext, sym_id: symbol_mod.SymbolId) bool { const range = self.semantic.symbols.getRefRange(sym_id); if (range.isEmpty()) return false; const sym_refs = self.semantic.ref_by_sym[range.start..range.end]; for (sym_refs) |rid| { - if (!self.semantic.references.getKind(rid).isTypeRef()) return false; + if (self.semantic.references.getKind(rid) != .type_read) return false; } return true; } + /// True when the symbol has at least one reference entry but ALL non-type references + /// come from `export type { }` specifiers. Workaround for es-parser emitting `read` + /// (not `type_read`) for specifiers inside type-only export declarations. + pub fn symbolRefsAllInTypeExports(self: *const LintContext, sym_id: symbol_mod.SymbolId) bool { + const range = self.semantic.symbols.getRefRange(sym_id); + if (range.isEmpty()) return false; + const sym_refs = self.semantic.ref_by_sym[range.start..range.end]; + var found_type_export = false; + for (sym_refs) |rid| { + if (self.semantic.references.getKind(rid) == .type_read) continue; + // Non-type reference: check if it's from an export type { } specifier. + const ref_node = self.semantic.references.getNode(rid); + if (ref_node == .none) return false; + // The reference node is a property_ident in an export_specifier. + // Walk up: property_ident → export_specifier → export_named + const spec = self.parentOf(ref_node); + if (spec == .none or self.ast.nodeTag(spec) != .export_specifier) return false; + const export_decl = self.parentOf(spec); + if (export_decl == .none or self.ast.nodeTag(export_decl) != .export_named) return false; + // Check if the export_named has a `type` token after `export`. + const export_tok = self.ast.nodeMainToken(export_decl); + if (export_tok + 1 >= self.ast.tokens.len) return false; + if (!std.mem.eql(u8, self.tokenText(export_tok + 1), "type")) return false; + found_type_export = true; + } + return found_type_export; + } + /// True when `import_decl` node has a top-level `type` modifier: /// `import type Foo from '...'` or `import type { Foo } from '...'`. pub fn importDeclIsTypeOnly(self: *const LintContext, node: NodeIndex) bool { @@ -10538,6 +10619,11 @@ pub const LintContext = struct { const assign_lhs = self.nodeSkipGrouping(rhs_d.lhs); const lhs = self.nodeSkipGrouping(d.lhs); if (assign_lhs == .none or lhs == .none) return; + // ESLint's isReference() rejects ChainExpression (which wraps optional + // member expressions like `a?.b`). In Ez's AST, optional_member_expr + // plays that role — skip to avoid false positives on `a?.b || (a.b = c)`. + const lhs_tag = self.ast.nodeTag(lhs); + if (lhs_tag == .optional_member_expr or lhs_tag == .optional_computed_member_expr) return; if (self.nodeSameReference(lhs, assign_lhs)) self.reportWithMessageId(node, "logical"); }, diff --git a/src/linter/native/typescript/consistent_type_imports.zig b/src/linter/native/typescript/consistent_type_imports.zig index 02a9b89c..abfab68f 100644 --- a/src/linter/native/typescript/consistent_type_imports.zig +++ b/src/linter/native/typescript/consistent_type_imports.zig @@ -121,15 +121,66 @@ fn specifierIsTypeOnly(sp: NodeIndex, ctx: *const LintContext) bool { // import_specifier: lhs=imported, rhs=local (rhs may be .none → use lhs) // import_default_specifier: lhs=local // import_namespace_specifier: lhs=local + const tag = ctx.nodeTag(sp); const d = ctx.nodeData(sp); - const local: NodeIndex = switch (ctx.nodeTag(sp)) { + const local: NodeIndex = switch (tag) { .import_specifier => if (d.rhs != .none) d.rhs else d.lhs, .import_default_specifier, .import_namespace_specifier => d.lhs, else => return false, }; if (local == .none) return false; const sym = ctx.symbolForDeclNode(local) orelse return false; - return ctx.symbolIsTypeOnly(sym); + if (ctx.symbolIsTypeOnly(sym)) { + // es-parser doesn't emit a `read` ref for the implicit JSX factory + // (issue #34): when a file uses intrinsic JSX (`
`), the classic + // runtime calls `.createElement`, but no reference is recorded + // for the pragma binding — so a default import of the factory looks + // type-only and would FP. Suppress ONLY for the actual factory import: + // a default import whose name matches the JSX pragma, in a file that + // contains JSX elements. A genuinely type-only default import (or any + // import in a JSX-free file) is still reported. The node scan is gated + // behind the cheap name check so it runs only for that rare candidate. + if (tag == .import_default_specifier and ctx.language.isJsx() and + importIsJsxFactory(local, ctx) and fileHasJsxElement(ctx)) return false; + return true; + } + // es-parser emits `read` (not `type_read`) for specifiers inside + // `export type { X }` declarations (issue #33). Check if ALL the + // non-type references to this symbol come from type-only export specifiers. + return ctx.symbolRefsAllInTypeExports(sym); +} + +/// True when `local` (a default-import binding) names the JSX factory pragma — +/// the leading identifier of the classic-runtime factory. Defaults to `React`; +/// honours `settings.react.pragma` when configured. +fn importIsJsxFactory(local: NodeIndex, ctx: *const LintContext) bool { + const name = ctx.tokenText(ctx.nodeMainToken(local)); + return std.mem.eql(u8, name, jsxFactoryPragma(ctx)); +} + +fn jsxFactoryPragma(ctx: *const LintContext) []const u8 { + const settings = ctx.getSettings() orelse return "React"; + if (settings.* != .object) return "React"; + const react = settings.object.get("react") orelse return "React"; + if (react != .object) return "React"; + const pragma = react.object.get("pragma") orelse return "React"; + if (pragma != .string) return "React"; + return pragma.string; +} + +/// True when the file contains at least one JSX element / fragment — the +/// condition under which the implicit JSX-factory reference exists. +fn fileHasJsxElement(ctx: *const LintContext) bool { + const total: u32 = @intCast(ctx.ast.nodes.len); + var i: u32 = 0; + while (i < total) : (i += 1) { + const ni: NodeIndex = @enumFromInt(i); + switch (ctx.nodeTag(ni)) { + .jsx_element, .jsx_self_closing, .jsx_fragment => return true, + else => {}, + } + } + return false; } fn extendSemi(sp: *Span, ctx: *const LintContext) void { diff --git a/src/linter/native/typescript/no_unnecessary_condition.zig b/src/linter/native/typescript/no_unnecessary_condition.zig index 95b6df47..a883800a 100644 --- a/src/linter/native/typescript/no_unnecessary_condition.zig +++ b/src/linter/native/typescript/no_unnecessary_condition.zig @@ -858,7 +858,14 @@ fn isNullishLiteral(id: tymod.TypeId) bool { fn involvesIndeterminateType(id: tymod.TypeId, ctx: *const LintContext) bool { const kind = ctx.typeIdKind(id) orelse return true; return switch (kind) { - .any, .unknown, .error_t, .type_param => true, + // A constrained type param (`T extends object`) is determinate via + // its constraint — TS-eslint uses getConstrainedTypeAtLocation. + // Only unconstrained params are indeterminate. + .type_param => blk: { + if (ctx.typeParamConstraint(id)) |c| break :blk involvesIndeterminateType(c, ctx); + break :blk true; + }, + .any, .unknown, .error_t => true, // Type refs to unrecognised names (often unresolved type // parameters / generics) are conservatively indeterminate. .type_ref => isUnresolvedRef(id, ctx), @@ -896,7 +903,12 @@ fn typeContainsNullish(id: tymod.TypeId, ctx: *const LintContext) bool { /// True when types `a` and `b` could share at least one runtime value. /// Conservative: returns true when uncertain. -fn typesCanOverlap(a: tymod.TypeId, b: tymod.TypeId, ctx: *const LintContext) bool { +fn typesCanOverlap(a_in: tymod.TypeId, b_in: tymod.TypeId, ctx: *const LintContext) bool { + // Resolve constrained type params to their constraint (`T extends object` + // → object) so overlap is computed on the constraint, matching TS-eslint's + // getConstrainedTypeAtLocation. + const a = resolveConstraint(a_in, ctx); + const b = resolveConstraint(b_in, ctx); // Expand unions on both sides; if any pair overlaps, true. const ka = ctx.typeIdKind(a) orelse return true; const kb = ctx.typeIdKind(b) orelse return true; @@ -935,6 +947,16 @@ fn typesCanOverlap(a: tymod.TypeId, b: tymod.TypeId, ctx: *const LintContext) bo return false; } +/// Peel a constrained type param to its constraint type (one level). +/// Unconstrained params and non-type-param ids pass through unchanged. +fn resolveConstraint(id: tymod.TypeId, ctx: *const LintContext) tymod.TypeId { + const kind = ctx.typeIdKind(id) orelse return id; + if (kind == .type_param) { + if (ctx.typeParamConstraint(id)) |c| return c; + } + return id; +} + fn kindFamily(k: tymod.TypeKind) u8 { return switch (k) { .string, .string_literal => 1, @@ -1016,6 +1038,11 @@ fn truthiness(id: TypeId, ctx: *const LintContext) Truthiness { fn truthinessDepth(id: TypeId, ctx: *const LintContext, depth: u32) Truthiness { if (depth > 6) return .indeterminate; const kind = ctx.typeIdKind(id) orelse return .indeterminate; + // Generic type parameter: follow constraint (`T extends object` → always_truthy). + if (kind == .type_param) { + const constraint = ctx.typeParamConstraint(id) orelse return .indeterminate; + return truthinessDepth(constraint, ctx, depth + 1); + } if (kind == .union_t) { var saw_truthy = false; var saw_falsy = false; @@ -1126,6 +1153,11 @@ fn nullability(id: TypeId, ctx: *const LintContext) Nullability { fn nullabilityDepth(id: TypeId, ctx: *const LintContext, depth: u32) Nullability { if (depth > 6) return .possibly_nullish; const kind = ctx.typeIdKind(id) orelse return .possibly_nullish; + // Generic type parameter: follow constraint (`T extends string` → never_nullish). + if (kind == .type_param) { + const constraint = ctx.typeParamConstraint(id) orelse return .possibly_nullish; + return nullabilityDepth(constraint, ctx, depth + 1); + } if (kind == .union_t) { var saw_null = false; var saw_non_null = false; diff --git a/src/linter/native/typescript/no_unnecessary_type_conversion.zig b/src/linter/native/typescript/no_unnecessary_type_conversion.zig index f2f3cb27..be92da0d 100644 --- a/src/linter/native/typescript/no_unnecessary_type_conversion.zig +++ b/src/linter/native/typescript/no_unnecessary_type_conversion.zig @@ -346,7 +346,12 @@ fn typeIsBigIntish(id: @import("ez_checker").types.TypeId, ctx: *const LintConte const tymod = @import("ez_checker").types; if (id.eq(tymod.ID_BIGINT)) return true; const k = ctx.typeIdKind(id) orelse return false; - return k == .bigint or k == .bigint_literal; + if (k == .bigint or k == .bigint_literal) return true; + if (k == .type_param) { + const constraint = ctx.typeParamConstraint(id) orelse return false; + return typeIsBigIntish(constraint, ctx); + } + return false; } fn typeIsEnumOrMember(node: NodeIndex, ctx: *const LintContext) bool { diff --git a/src/linter/native/typescript/strict_boolean_expressions.zig b/src/linter/native/typescript/strict_boolean_expressions.zig index 1173faa3..505decaa 100644 --- a/src/linter/native/typescript/strict_boolean_expressions.zig +++ b/src/linter/native/typescript/strict_boolean_expressions.zig @@ -826,7 +826,17 @@ fn walkType(id: TypeId, fam: *Family, ctx: *const LintContext, depth: u32) void fam.has_object = true; } }, - .type_param => fam.has_any = true, + .type_param => { + // TS-eslint uses getConstrainedTypeAtLocation: a constrained + // type param (`T extends boolean`) is classified by its + // constraint. An unconstrained `T` has no constraint → treat + // as `any` (allowAny applies), matching the old behavior. + if (ctx.typeParamConstraint(id)) |constraint| { + walkType(constraint, fam, ctx, depth + 1); + } else { + fam.has_any = true; + } + }, else => fam.has_other = true, } } diff --git a/tests/differential/baseline.json b/tests/differential/baseline.json index df2227cc..4b27482b 100644 --- a/tests/differential/baseline.json +++ b/tests/differential/baseline.json @@ -979,7 +979,7 @@ "crash": 0 }, "native": { - "fn": 6, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -997,7 +997,7 @@ "crash": 0 }, "native": { - "fn": 56, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1015,7 +1015,7 @@ "crash": 0 }, "native": { - "fn": 13, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1034,13 +1034,13 @@ }, "native": { "fn": 0, - "fp": 0, + "fp": 3, "crash": 0, "skip": 0 }, "hybrid": { "fn": 0, - "fp": 0, + "fp": 3, "crash": 0 } }, @@ -1069,14 +1069,14 @@ "crash": 0 }, "native": { - "fn": 20, - "fp": 0, + "fn": 5, + "fp": 6, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 5, + "fp": 6, "crash": 0 } }, @@ -1087,7 +1087,7 @@ "crash": 0 }, "native": { - "fn": 4, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1105,7 +1105,7 @@ "crash": 0 }, "native": { - "fn": 7, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1123,13 +1123,13 @@ "crash": 0 }, "native": { - "fn": 21, + "fn": 3, "fp": 0, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 3, "fp": 0, "crash": 0 } @@ -1141,14 +1141,14 @@ "crash": 0 }, "native": { - "fn": 63, - "fp": 0, + "fn": 2, + "fp": 6, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 2, + "fp": 6, "crash": 0 } }, @@ -1159,7 +1159,7 @@ "crash": 0 }, "native": { - "fn": 54, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1195,7 +1195,7 @@ "crash": 0 }, "native": { - "fn": 15, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1213,14 +1213,14 @@ "crash": 0 }, "native": { - "fn": 129, - "fp": 0, + "fn": 0, + "fp": 1, "crash": 0, "skip": 0 }, "hybrid": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 } }, @@ -1231,7 +1231,7 @@ "crash": 0 }, "native": { - "fn": 11, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1249,14 +1249,14 @@ "crash": 0 }, "native": { - "fn": 77, - "fp": 0, + "fn": 0, + "fp": 1, "crash": 0, "skip": 0 }, "hybrid": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 } }, @@ -1267,13 +1267,13 @@ "crash": 0 }, "native": { - "fn": 26, + "fn": 1, "fp": 0, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 1, "fp": 0, "crash": 0 } @@ -1303,7 +1303,7 @@ "crash": 0 }, "native": { - "fn": 56, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1321,13 +1321,13 @@ "crash": 0 }, "native": { - "fn": 248, + "fn": 4, "fp": 0, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 4, "fp": 0, "crash": 0 } @@ -1339,7 +1339,7 @@ "crash": 0 }, "native": { - "fn": 65, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1357,7 +1357,7 @@ "crash": 0 }, "native": { - "fn": 10, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1375,7 +1375,7 @@ "crash": 0 }, "native": { - "fn": 10, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1429,7 +1429,7 @@ "crash": 0 }, "native": { - "fn": 18, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1447,7 +1447,7 @@ "crash": 0 }, "native": { - "fn": 10, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1465,7 +1465,7 @@ "crash": 0 }, "native": { - "fn": 100, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1483,7 +1483,7 @@ "crash": 0 }, "native": { - "fn": 28, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1501,14 +1501,14 @@ "crash": 0 }, "native": { - "fn": 21, - "fp": 0, + "fn": 1, + "fp": 1, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 1, + "fp": 1, "crash": 0 } }, @@ -1519,7 +1519,7 @@ "crash": 0 }, "native": { - "fn": 56, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1537,7 +1537,7 @@ "crash": 0 }, "native": { - "fn": 19, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1555,7 +1555,7 @@ "crash": 0 }, "native": { - "fn": 21, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1573,7 +1573,7 @@ "crash": 0 }, "native": { - "fn": 43, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1591,7 +1591,7 @@ "crash": 0 }, "native": { - "fn": 8, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1609,7 +1609,7 @@ "crash": 0 }, "native": { - "fn": 101, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1627,7 +1627,7 @@ "crash": 0 }, "native": { - "fn": 76, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1663,7 +1663,7 @@ "crash": 0 }, "native": { - "fn": 9, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1681,14 +1681,14 @@ "crash": 0 }, "native": { - "fn": 61, - "fp": 0, + "fn": 6, + "fp": 1, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 6, + "fp": 1, "crash": 0 } }, @@ -1699,14 +1699,14 @@ "crash": 0 }, "native": { - "fn": 90, - "fp": 0, + "fn": 0, + "fp": 1, "crash": 0, "skip": 0 }, "hybrid": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 } }, @@ -1723,7 +1723,7 @@ "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 4, "fp": 0, "crash": 0 } @@ -1735,7 +1735,7 @@ "crash": 0 }, "native": { - "fn": 16, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1753,7 +1753,7 @@ "crash": 0 }, "native": { - "fn": 25, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1771,7 +1771,7 @@ "crash": 0 }, "native": { - "fn": 19, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1789,7 +1789,7 @@ "crash": 0 }, "native": { - "fn": 14, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1825,7 +1825,7 @@ "crash": 0 }, "native": { - "fn": 5, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1861,7 +1861,7 @@ "crash": 0 }, "native": { - "fn": 49, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1879,7 +1879,7 @@ "crash": 0 }, "native": { - "fn": 14, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1897,7 +1897,7 @@ "crash": 0 }, "native": { - "fn": 23, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1915,7 +1915,7 @@ "crash": 0 }, "native": { - "fn": 86, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1933,7 +1933,7 @@ "crash": 0 }, "native": { - "fn": 30, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1951,7 +1951,7 @@ "crash": 0 }, "native": { - "fn": 8, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -1969,7 +1969,7 @@ "crash": 0 }, "native": { - "fn": 5, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2041,13 +2041,13 @@ "crash": 0 }, "native": { - "fn": 44, + "fn": 1, "fp": 0, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 1, "fp": 0, "crash": 0 } @@ -2059,7 +2059,7 @@ "crash": 0 }, "native": { - "fn": 144, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2077,7 +2077,7 @@ "crash": 0 }, "native": { - "fn": 22, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2113,7 +2113,7 @@ "crash": 0 }, "native": { - "fn": 16, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2131,7 +2131,7 @@ "crash": 0 }, "native": { - "fn": 4, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2149,7 +2149,7 @@ "crash": 0 }, "native": { - "fn": 21, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2167,7 +2167,7 @@ "crash": 0 }, "native": { - "fn": 16, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2185,7 +2185,7 @@ "crash": 0 }, "native": { - "fn": 32, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2203,7 +2203,7 @@ "crash": 0 }, "native": { - "fn": 101, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2227,8 +2227,8 @@ "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 2, + "fp": 1, "crash": 0 } }, @@ -2239,13 +2239,13 @@ "crash": 0 }, "native": { - "fn": 20, + "fn": 2, "fp": 0, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 2, "fp": 0, "crash": 0 } @@ -2257,7 +2257,7 @@ "crash": 0 }, "native": { - "fn": 38, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2275,14 +2275,14 @@ "crash": 0 }, "native": { - "fn": 48, - "fp": 0, + "fn": 1, + "fp": 1, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 1, + "fp": 1, "crash": 0 } }, @@ -2293,7 +2293,7 @@ "crash": 0 }, "native": { - "fn": 9, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2311,7 +2311,7 @@ "crash": 0 }, "native": { - "fn": 19, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2347,13 +2347,13 @@ "crash": 0 }, "native": { - "fn": 180, + "fn": 6, "fp": 0, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 6, "fp": 0, "crash": 0 } @@ -2383,7 +2383,7 @@ "crash": 0 }, "native": { - "fn": 65, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2419,14 +2419,14 @@ "crash": 0 }, "native": { - "fn": 34, - "fp": 0, + "fn": 1, + "fp": 1, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 1, + "fp": 1, "crash": 0 } }, @@ -2437,14 +2437,14 @@ "crash": 0 }, "native": { - "fn": 134, - "fp": 0, + "fn": 3, + "fp": 4, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 3, + "fp": 4, "crash": 0 } }, @@ -2455,7 +2455,7 @@ "crash": 0 }, "native": { - "fn": 50, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2473,7 +2473,7 @@ "crash": 0 }, "native": { - "fn": 39, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2491,14 +2491,14 @@ "crash": 0 }, "native": { - "fn": 65, - "fp": 0, + "fn": 2, + "fp": 3, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 2, + "fp": 3, "crash": 0 } }, @@ -2509,7 +2509,7 @@ "crash": 0 }, "native": { - "fn": 7, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2527,13 +2527,13 @@ "crash": 0 }, "native": { - "fn": 22, + "fn": 1, "fp": 0, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 1, "fp": 0, "crash": 0 } @@ -2545,7 +2545,7 @@ "crash": 0 }, "native": { - "fn": 21, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2563,7 +2563,7 @@ "crash": 0 }, "native": { - "fn": 4, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2581,7 +2581,7 @@ "crash": 0 }, "native": { - "fn": 13, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2599,7 +2599,7 @@ "crash": 0 }, "native": { - "fn": 32, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2617,7 +2617,7 @@ "crash": 0 }, "native": { - "fn": 14, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2635,7 +2635,7 @@ "crash": 0 }, "native": { - "fn": 36, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2653,7 +2653,7 @@ "crash": 0 }, "native": { - "fn": 9, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2671,7 +2671,7 @@ "crash": 0 }, "native": { - "fn": 28, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2707,7 +2707,7 @@ "crash": 0 }, "native": { - "fn": 7, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2725,7 +2725,7 @@ "crash": 0 }, "native": { - "fn": 73, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2743,7 +2743,7 @@ "crash": 0 }, "native": { - "fn": 5, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2761,7 +2761,7 @@ "crash": 0 }, "native": { - "fn": 49, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2779,7 +2779,7 @@ "crash": 0 }, "native": { - "fn": 9, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2797,7 +2797,7 @@ "crash": 0 }, "native": { - "fn": 3, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2815,7 +2815,7 @@ "crash": 0 }, "native": { - "fn": 19, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2851,7 +2851,7 @@ "crash": 0 }, "native": { - "fn": 8, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2887,14 +2887,14 @@ "crash": 0 }, "native": { - "fn": 1, - "fp": 0, + "fn": 0, + "fp": 2, "crash": 0, "skip": 0 }, "hybrid": { "fn": 0, - "fp": 0, + "fp": 2, "crash": 0 } }, @@ -2905,7 +2905,7 @@ "crash": 0 }, "native": { - "fn": 48, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -2948,7 +2948,7 @@ }, "hybrid": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 } }, @@ -2966,7 +2966,7 @@ }, "hybrid": { "fn": 0, - "fp": 0, + "fp": 6, "crash": 0 } }, @@ -2995,7 +2995,7 @@ "crash": 0 }, "native": { - "fn": 636, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -3013,14 +3013,14 @@ "crash": 0 }, "native": { - "fn": 592, - "fp": 0, + "fn": 0, + "fp": 1, "crash": 0, "skip": 0 }, "hybrid": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 } }, @@ -3031,7 +3031,7 @@ "crash": 0 }, "native": { - "fn": 22, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -3049,7 +3049,7 @@ "crash": 0 }, "native": { - "fn": 4, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -3103,7 +3103,7 @@ "crash": 0 }, "native": { - "fn": 18, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -3121,14 +3121,14 @@ "crash": 0 }, "native": { - "fn": 4, - "fp": 0, + "fn": 1, + "fp": 2, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 1, + "fp": 2, "crash": 0 } }, @@ -3139,13 +3139,13 @@ "crash": 0 }, "native": { - "fn": 52, + "fn": 1, "fp": 0, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 1, "fp": 0, "crash": 0 } @@ -3157,7 +3157,7 @@ "crash": 0 }, "native": { - "fn": 4, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -3175,13 +3175,13 @@ "crash": 0 }, "native": { - "fn": 14, + "fn": 1, "fp": 0, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 1, "fp": 0, "crash": 0 } @@ -3193,7 +3193,7 @@ "crash": 0 }, "native": { - "fn": 27, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -3523,7 +3523,7 @@ "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 3, "fp": 0, "crash": 0 } @@ -3577,7 +3577,7 @@ "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 2, "fp": 0, "crash": 0 } @@ -4315,8 +4315,8 @@ "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 1, + "fp": 1, "crash": 0 } }, @@ -4387,8 +4387,8 @@ "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 3, + "fp": 1, "crash": 0 } }, @@ -4639,8 +4639,8 @@ "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 6, + "fp": 1, "crash": 0 } }, @@ -4712,7 +4712,7 @@ }, "hybrid": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 } }, @@ -4909,8 +4909,8 @@ "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 1, + "fp": 45, "crash": 0 } }, @@ -4945,7 +4945,7 @@ "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 5, "fp": 0, "crash": 0 } @@ -5054,7 +5054,7 @@ }, "hybrid": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 } }, @@ -5582,7 +5582,7 @@ }, "prefer-promise-reject-errors": { "runner": { - "fn": 1, + "fn": 0, "fp": 0, "crash": 0 }, @@ -5846,7 +5846,7 @@ }, "hybrid": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 } }, @@ -6205,7 +6205,7 @@ "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 18, "fp": 0, "crash": 0 } @@ -6349,8 +6349,8 @@ "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 11, + "fp": 2, "crash": 0 } }, @@ -6422,7 +6422,7 @@ }, "hybrid": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 } }, @@ -6662,7 +6662,7 @@ }, "no-func-assign": { "runner": { - "fn": 1, + "fn": 0, "fp": 0, "crash": 0 }, @@ -6932,7 +6932,7 @@ }, "no-use-before-define": { "runner": { - "fn": 9, + "fn": 0, "fp": 0, "crash": 0 }, @@ -6943,7 +6943,7 @@ "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 1, "fp": 0, "crash": 0 } @@ -7196,7 +7196,7 @@ }, "hybrid": { "fn": 0, - "fp": 0, + "fp": 3, "crash": 0 } }, @@ -7328,8 +7328,8 @@ }, "no-unexpected-multiline": { "runner": { - "fn": 1, - "fp": 1, + "fn": 0, + "fp": 0, "crash": 0 }, "native": { @@ -7339,7 +7339,7 @@ "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 1, "fp": 0, "crash": 0 } @@ -7537,7 +7537,7 @@ "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 1, "fp": 0, "crash": 0 } @@ -7663,7 +7663,7 @@ "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 1, "fp": 0, "crash": 0 } @@ -7868,18 +7868,18 @@ }, "no-shadow": { "runner": { - "fn": 26, - "fp": 27, + "fn": 0, + "fp": 0, "crash": 0 }, "native": { - "fn": 8, - "fp": 4, + "fn": 2, + "fp": 0, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 2, "fp": 0, "crash": 0 } @@ -9121,14 +9121,14 @@ "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 1, + "fp": 4, "crash": 0 } }, "no-this-before-super": { "runner": { - "fn": 4, + "fn": 0, "fp": 0, "crash": 0 }, @@ -9175,7 +9175,7 @@ "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 2, "fp": 0, "crash": 0 } @@ -9409,8 +9409,8 @@ "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 1, + "fp": 2, "crash": 0 } }, @@ -9543,7 +9543,7 @@ "func-names": { "runner": { "fn": 0, - "fp": 1, + "fp": 0, "crash": 0 }, "native": { @@ -10100,8 +10100,8 @@ }, "constructor-super": { "runner": { - "fn": 2, - "fp": 1, + "fn": 0, + "fp": 0, "crash": 0 }, "native": { @@ -10165,7 +10165,7 @@ "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 1, "fp": 0, "crash": 0 } @@ -10226,7 +10226,7 @@ }, "sonarjs/no-collection-size-mischeck": { "runner": { - "fn": 13, + "fn": 0, "fp": 0, "crash": 0 }, @@ -10244,9 +10244,9 @@ }, "sonarjs/prefer-immediate-return": { "runner": { - "fn": 21, + "fn": 0, "fp": 0, - "crash": 24 + "crash": 0 }, "native": { "fn": 21, @@ -10407,7 +10407,7 @@ "sonarjs/no-incorrect-string-concat": { "runner": { "fn": 0, - "fp": 2, + "fp": 0, "crash": 0 }, "native": { @@ -10695,7 +10695,7 @@ "sonarjs/no-try-promise": { "runner": { "fn": 0, - "fp": 4, + "fp": 0, "crash": 0 }, "native": { @@ -10875,7 +10875,7 @@ "sonarjs/useless-string-operation": { "runner": { "fn": 0, - "fp": 2, + "fp": 0, "crash": 0 }, "native": { @@ -10911,7 +10911,7 @@ "sonarjs/strings-comparison": { "runner": { "fn": 0, - "fp": 4, + "fp": 0, "crash": 0 }, "native": { @@ -10929,7 +10929,7 @@ "sonarjs/no-alphabetical-sort": { "runner": { "fn": 0, - "fp": 7, + "fp": 0, "crash": 0 }, "native": { @@ -11000,7 +11000,7 @@ }, "sonarjs/arguments-order": { "runner": { - "fn": 30, + "fn": 0, "fp": 0, "crash": 0 }, @@ -11036,7 +11036,7 @@ }, "sonarjs/sql-queries": { "runner": { - "fn": 14, + "fn": 0, "fp": 0, "crash": 0 }, @@ -11073,7 +11073,7 @@ "sonarjs/assertions-in-tests": { "runner": { "fn": 0, - "fp": 7, + "fp": 0, "crash": 0 }, "native": { @@ -11199,7 +11199,7 @@ "sonarjs/no-require-or-define": { "runner": { "fn": 0, - "fp": 1, + "fp": 0, "crash": 0 }, "native": { @@ -11667,7 +11667,7 @@ "sonarjs/different-types-comparison": { "runner": { "fn": 0, - "fp": 1, + "fp": 0, "crash": 0 }, "native": { @@ -11882,7 +11882,7 @@ }, "sonarjs/bitwise-operators": { "runner": { - "fn": 2, + "fn": 0, "fp": 0, "crash": 0 }, @@ -12019,7 +12019,7 @@ "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 1, "fp": 0, "crash": 0 } @@ -12127,7 +12127,7 @@ "skip": 0 }, "hybrid": { - "fn": 0, + "fn": 2, "fp": 0, "crash": 0 } @@ -12152,7 +12152,7 @@ }, "sonarjs/reduce-initial-value": { "runner": { - "fn": 2, + "fn": 0, "fp": 0, "crash": 0 }, @@ -12279,7 +12279,7 @@ "sonarjs/existing-groups": { "runner": { "fn": 0, - "fp": 2, + "fp": 0, "crash": 0 }, "native": { @@ -12350,7 +12350,7 @@ }, "sonarjs/no-inconsistent-returns": { "runner": { - "fn": 1, + "fn": 0, "fp": 0, "crash": 0 }, @@ -12387,7 +12387,7 @@ "sonarjs/new-operator-misuse": { "runner": { "fn": 0, - "fp": 13, + "fp": 0, "crash": 0 }, "native": { @@ -12422,7 +12422,7 @@ }, "sonarjs/class-prototype": { "runner": { - "fn": 3, + "fn": 0, "fp": 0, "crash": 0 }, @@ -12728,8 +12728,8 @@ }, "sonarjs/void-use": { "runner": { - "fn": 1, - "fp": 1, + "fn": 0, + "fp": 0, "crash": 0 }, "native": { @@ -12945,7 +12945,7 @@ "sonarjs/operation-returning-nan": { "runner": { "fn": 0, - "fp": 5, + "fp": 0, "crash": 0 }, "native": { @@ -13089,7 +13089,7 @@ "sonarjs/post-message": { "runner": { "fn": 0, - "fp": 12, + "fp": 0, "crash": 0 }, "native": { @@ -13143,7 +13143,7 @@ "sonarjs/null-dereference": { "runner": { "fn": 0, - "fp": 1, + "fp": 0, "crash": 0 }, "native": { @@ -13215,7 +13215,7 @@ "sonarjs/values-not-convertible-to-numbers": { "runner": { "fn": 0, - "fp": 1, + "fp": 0, "crash": 0 }, "native": { @@ -13269,7 +13269,7 @@ "sonarjs/in-operator-type-error": { "runner": { "fn": 0, - "fp": 5, + "fp": 0, "crash": 0 }, "native": { @@ -13449,7 +13449,7 @@ "sonarjs/unused-named-groups": { "runner": { "fn": 0, - "fp": 25, + "fp": 0, "crash": 0 }, "native": { @@ -13485,7 +13485,7 @@ "sonarjs/non-number-in-arithmetic-expression": { "runner": { "fn": 0, - "fp": 8, + "fp": 0, "crash": 0 }, "native": { @@ -13701,7 +13701,7 @@ "sonarjs/no-for-in-iterable": { "runner": { "fn": 0, - "fp": 4, + "fp": 0, "crash": 0 }, "native": { @@ -13885,7 +13885,7 @@ "crash": 0 }, "native": { - "fn": 12, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -14083,7 +14083,7 @@ "crash": 0 }, "native": { - "fn": 4, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -14173,7 +14173,7 @@ "crash": 0 }, "native": { - "fn": 21, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -14245,7 +14245,7 @@ "crash": 0 }, "native": { - "fn": 282, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -14299,7 +14299,7 @@ "crash": 0 }, "native": { - "fn": 7, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -14767,7 +14767,7 @@ "crash": 0 }, "native": { - "fn": 11, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -14785,7 +14785,7 @@ "crash": 0 }, "native": { - "fn": 32, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -14821,7 +14821,7 @@ "crash": 0 }, "native": { - "fn": 5, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -14983,7 +14983,7 @@ "crash": 0 }, "native": { - "fn": 173, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -15313,8 +15313,8 @@ "skip": 0 }, "hybrid": { - "fn": 0, - "fp": 0, + "fn": 2, + "fp": 2, "crash": 0 } }, @@ -15332,7 +15332,7 @@ }, "hybrid": { "fn": 0, - "fp": 0, + "fp": 1, "crash": 0 } }, @@ -15343,7 +15343,7 @@ "crash": 0 }, "native": { - "fn": 5, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -15505,7 +15505,7 @@ "crash": 0 }, "native": { - "fn": 2, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -15559,7 +15559,7 @@ "crash": 0 }, "native": { - "fn": 3, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -15631,7 +15631,7 @@ "crash": 0 }, "native": { - "fn": 22, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 @@ -18200,9 +18200,9 @@ } }, "perf": { - "totalElapsedMs": 18044, - "runnerCasesPerSec": 5675, - "nativeCasesPerSec": 56052, - "totalCases": 72134 + "totalElapsedMs": 13144, + "runnerCasesPerSec": 6850, + "nativeCasesPerSec": 59024, + "totalCases": 56037 } } \ No newline at end of file From 8a1ab18f7a34cd594fc35f1caebc547a80b379b3 Mon Sep 17 00:00:00 2001 From: Eric San Date: Thu, 25 Jun 2026 00:35:07 +0800 Subject: [PATCH 3/7] fix(no-duplicate-type-constituents): don't equate undeclared type references `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`) and lib/user types still flag correctly. no-duplicate-type-constituents 79/82 -> 82/82. Hybrid 153 -> 150 gaps, 0 regressions. --- .../no_duplicate_type_constituents.zig | 17 ++++++++++++++++- tests/differential/baseline.json | 10 +++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/linter/native/typescript/no_duplicate_type_constituents.zig b/src/linter/native/typescript/no_duplicate_type_constituents.zig index 2032883e..9a156014 100644 --- a/src/linter/native/typescript/no_duplicate_type_constituents.zig +++ b/src/linter/native/typescript/no_duplicate_type_constituents.zig @@ -62,7 +62,22 @@ pub fn run(node: NodeIndex, ctx: *const LintContext) void { fn typeNodeIsError(node: NodeIndex, ctx: *const LintContext) bool { const tid = ctx.resolveTypeAnnotationNode(node); - return ctx.typeIdIsError(tid); + if (ctx.typeIdIsError(tid)) return true; + // An undeclared type reference (`A` with no declaration) is the TS error + // type — TSe treats two such references as distinct, non-equivalent errors, + // so they must not be equated as duplicates. 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 detect it structurally: a type_reference whose name is + // neither a known type NOR an in-scope type parameter is undeclared. + var n = node; + while (ctx.nodeTag(n) == .ts_parenthesized_type) n = ctx.nodeData(n).lhs; + if (ctx.nodeTag(n) == .ts_type_reference) { + const name = ctx.tokenText(ctx.nodeMainToken(n)); + if (name.len > 0 and !ctx.typeNameIsKnown(name) and + !ctx.typeAnnotationIsTypeParameter(n)) return true; + } + return false; } fn collectLeaves(node: NodeIndex, buf: *[32]NodeIndex, n: *usize, ctx: *const LintContext) void { diff --git a/tests/differential/baseline.json b/tests/differential/baseline.json index 4b27482b..9647b9a7 100644 --- a/tests/differential/baseline.json +++ b/tests/differential/baseline.json @@ -1034,13 +1034,13 @@ }, "native": { "fn": 0, - "fp": 3, + "fp": 0, "crash": 0, "skip": 0 }, "hybrid": { "fn": 0, - "fp": 3, + "fp": 0, "crash": 0 } }, @@ -18200,9 +18200,9 @@ } }, "perf": { - "totalElapsedMs": 13144, - "runnerCasesPerSec": 6850, - "nativeCasesPerSec": 59024, + "totalElapsedMs": 12930, + "runnerCasesPerSec": 6914, + "nativeCasesPerSec": 58853, "totalCases": 56037 } } \ No newline at end of file From 58a3e64906a6d08b79e3a03617830318aa0f39b9 Mon Sep 17 00:00:00 2001 From: Eric San Date: Thu, 25 Jun 2026 00:53:05 +0800 Subject: [PATCH 4/7] fix(no-misleading-character-class): resolve const-string pattern/flags variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- src/linter/lint_context.zig | 41 ++++++++++++++++++++++++++++---- tests/differential/baseline.json | 10 ++++---- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/linter/lint_context.zig b/src/linter/lint_context.zig index 23973359..b2684ab3 100644 --- a/src/linter/lint_context.zig +++ b/src/linter/lint_context.zig @@ -12505,15 +12505,35 @@ pub const LintContext = struct { const args = self.extraSlice(range); if (args.len == 0) return; const first_arg_raw: NodeIndex = @enumFromInt(args[0]); - const first_arg = self.nodeSkipGrouping(first_arg_raw); - const first_tag = self.ast.nodeTag(first_arg); + var first_arg = self.nodeSkipGrouping(first_arg_raw); + var first_tag = self.ast.nodeTag(first_arg); + // Resolve a const string variable used as the pattern: + // `const p = "[…]"; new RegExp(p)`. ESLint can't map char offsets back + // into the original source, so it reports on the whole argument node — + // collapse every reported span to the identifier via `report_override`. + var report_override: ?Span = null; + if (first_tag == .identifier) { + const init = self.constInitializerOf(first_arg) orelse return; + const init_node = self.nodeSkipGrouping(init); + const it = self.ast.nodeTag(init_node); + if (it != .string_literal and it != .template_literal) return; + report_override = self.nodeSpan(first_arg); + first_arg = init_node; + first_tag = it; + } if (first_tag != .string_literal and first_tag != .template_literal and first_tag != .regex_literal) return; var flags: []const u8 = ""; var flags_explicit = false; if (args.len >= 2) { const flags_arg_raw: NodeIndex = @enumFromInt(args[1]); - const flags_arg = self.nodeSkipGrouping(flags_arg_raw); - const ftag = self.ast.nodeTag(flags_arg); + var flags_arg = self.nodeSkipGrouping(flags_arg_raw); + var ftag = self.ast.nodeTag(flags_arg); + // Resolve a const string variable used as the flags argument. + if (ftag == .identifier) { + const finit = self.constInitializerOf(flags_arg) orelse return; + flags_arg = self.nodeSkipGrouping(finit); + ftag = self.ast.nodeTag(flags_arg); + } if (ftag != .string_literal and ftag != .template_literal) return; // bail on non-static flags const fs = self.nodeStaticStringValue(flags_arg) orelse return; flags = fs; @@ -12547,7 +12567,10 @@ pub const LintContext = struct { decodeJsStringLiteralMapped(arena, body) catch return; if (regexPatternHasSyntaxError(decoded.bytes, true, flags)) return; const flag_set = regex_parser.Flags.fromString(flags); - const allow_escape = self.noMisleadingAllowEscape(); + // When the pattern came from a resolved constant, escape forms in the + // variable's literal are not "visible" at the usage site, so allowEscape + // does not apply (matches ESLint, which reports on the argument node). + const allow_escape = report_override == null and self.noMisleadingAllowEscape(); const pat = regex_parser.parse(arena, decoded.bytes, .{ .flags = flag_set }) catch return; // Source-map base: first byte INSIDE the string literal in source // (just past the opening quote). @@ -12557,6 +12580,7 @@ pub const LintContext = struct { .decoded_len = decoded.bytes.len, .body_src_start = body_src_start, .body = body, + .override = report_override, }; self.walkMisleadingCharClassCall(pat.alternatives, decoded.bytes, flag_set, allow_escape, map_ctx); } @@ -12566,13 +12590,20 @@ pub const LintContext = struct { decoded_len: usize, body_src_start: u32, body: []const u8, + /// When the pattern came from a resolved constant (`const p = "…"; new + /// RegExp(p)`), decoded offsets don't map back to source — ESLint + /// reports on the whole argument node instead. When set, every + /// reported span collapses to this fixed range. + override: ?Span = null, fn srcStart(self: CallSourceMap, decoded_off: u32) u32 { + if (self.override) |o| return o.start; if (decoded_off >= self.map.len) return self.body_src_start + @as(u32, @intCast(self.decoded_len)); return self.body_src_start + self.map[decoded_off]; } fn srcEnd(self: CallSourceMap, decoded_off: u32) u32 { + if (self.override) |o| return o.end; // Walk forward through duplicate map entries (multi-byte // sequences from one escape) to land on the next char's // source offset, which is this char's source end. diff --git a/tests/differential/baseline.json b/tests/differential/baseline.json index 9647b9a7..e72a3a47 100644 --- a/tests/differential/baseline.json +++ b/tests/differential/baseline.json @@ -6343,13 +6343,13 @@ "crash": 0 }, "native": { - "fn": 11, + "fn": 8, "fp": 2, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 11, + "fn": 8, "fp": 2, "crash": 0 } @@ -18200,9 +18200,9 @@ } }, "perf": { - "totalElapsedMs": 12930, - "runnerCasesPerSec": 6914, - "nativeCasesPerSec": 58853, + "totalElapsedMs": 16339, + "runnerCasesPerSec": 5249, + "nativeCasesPerSec": 51085, "totalCases": 56037 } } \ No newline at end of file From 0525ba57e29e12882f23df4647d734b6697b45df Mon Sep 17 00:00:00 2001 From: Eric San Date: Thu, 25 Jun 2026 01:45:12 +0800 Subject: [PATCH 5/7] fix(no-misleading-character-class): correct ZWJ-sequence diagnostics (surrogate-aware) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- js/index.js | 7 ++-- src/linter/lint_context.zig | 62 ++++++++++++++++++++------------ tests/differential/baseline.json | 18 +++++----- 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/js/index.js b/js/index.js index 85efe643..201062c5 100644 --- a/js/index.js +++ b/js/index.js @@ -301,8 +301,11 @@ function _offsetToLineCol(lineStarts, offset, bytes) { } else if ((b & 0xF0) === 0xE0) { col++; i += 3; } else if ((b & 0xF8) === 0xF0) { - // 4-byte UTF-8 → surrogate pair in UTF-16. - col += 2; i += 4; + // 4-byte UTF-8 → surrogate pair (2 UTF-16 units). A span endpoint may + // land between the two surrogates — no-misleading-character-class reports + // ZWJ sequences at surrogate granularity. Count one unit for the partial. + if (i + 4 > end) { col += (end - i >= 2) ? 1 : 0; i = end; } + else { col += 2; i += 4; } } else { col++; i++; } diff --git a/src/linter/lint_context.zig b/src/linter/lint_context.zig index b2684ab3..4476c1ff 100644 --- a/src/linter/lint_context.zig +++ b/src/linter/lint_context.zig @@ -12769,10 +12769,16 @@ pub const LintContext = struct { const next = seq[i + 1]; if (curr.codepoint == 0x200D and prev.codepoint != 0x200D and next.codepoint != 0x200D) { if (allow_escape and map_ctx.isEscapeFormAt(prev.start) and map_ctx.isEscapeFormAt(curr.start) and map_ctx.isEscapeFormAt(next.start)) continue; + // Without u/v a supplementary middle splits into two + // differing surrogates, so its joins stay separate (see the + // literal-path reportZwjSeq). Source spans stay whole-char: + // the source map collapses the surrogate offsets onto the + // original escape, which is what ESLint reports. + const middle_shares = has_uv or !zwjCharIsSupplementary(seq[i - 1]); if (run_start == null) { run_start = i - 1; run_end = i + 1; - } else if (run_end == i - 1) { + } else if (run_end == i - 1 and middle_shares) { run_end = i + 1; } else { const s = seq[run_start.?]; @@ -12898,7 +12904,7 @@ pub const LintContext = struct { // BMP codepoints (U+0300-range marks, U+200D ZWJ) that // regexpp sees regardless of u/v. self.reportCombiningSeq(buf[0..seq_len], pat_start, pat_text, allow_escape); - self.reportZwjSeq(buf[0..seq_len], pat_start, pat_text, allow_escape); + self.reportZwjSeq(buf[0..seq_len], pat_start, flags.unicode or flags.unicode_sets, pat_text, allow_escape); // emojiModifier / regionalIndicator codepoints sit in the // supplementary plane. Without u/v regexpp splits them // into surrogate halves which can't match the predicates, @@ -12962,11 +12968,17 @@ pub const LintContext = struct { } } - fn reportZwjSeq(self: *const LintContext, seq: []const regex_parser.Character, pat_start: u32, pat_text: []const u8, allow_escape: bool) void { + fn reportZwjSeq(self: *const LintContext, seq: []const regex_parser.Character, pat_start: u32, has_uv: bool, pat_text: []const u8, allow_escape: bool) void { if (seq.len < 3) return; - // Walk for ZWJ joiners. ESLint coalesces overlapping ZWJ-joined - // sequences into a single diag, so we emit one report covering - // the contiguous run rather than one per joiner. + // Mirror ESLint's `zwj` generator, which operates on UTF-16 code UNITS. + // WITHOUT u/v, a supplementary emoji is two surrogates, so a ZWJ join is + // the triple [low-surrogate-of-before, ZWJ, high-surrogate-of-after]; + // consecutive joins coalesce ONLY when the separating character is a + // single BMP unit (a supplementary middle splits into two differing + // surrogates, so its joins stay separate). WITH u/v, every character is + // one code point, so consecutive joins always share the middle unit and + // coalesce. We keep emoji as one Character and model the no-u surrogate + // boundary by reporting at the emoji's UTF-8 midpoint (start+2). var run_start: ?usize = null; var run_end: usize = 0; var i: usize = 1; @@ -12976,32 +12988,38 @@ pub const LintContext = struct { const next = seq[i + 1]; if (curr.codepoint == 0x200D and prev.codepoint != 0x200D and next.codepoint != 0x200D) { if (allow_escape and charIsEscapeForm(prev, pat_text) and charIsEscapeForm(curr, pat_text) and charIsEscapeForm(next, pat_text)) continue; + const middle_shares = has_uv or !zwjCharIsSupplementary(seq[i - 1]); if (run_start == null) { run_start = i - 1; run_end = i + 1; - } else if (run_end == i - 1) { + } else if (run_end == i - 1 and middle_shares) { run_end = i + 1; } else { - // Emit previous run. - const s = seq[run_start.?]; - const e = seq[run_end]; - self.reportSpanWithMessageId(.{ - .start = pat_start + s.start, - .end = pat_start + e.end, - }, "zwj"); + self.emitZwj(seq[run_start.?], seq[run_end], pat_start, has_uv); run_start = i - 1; run_end = i + 1; } } } - if (run_start) |rs| { - const s = seq[rs]; - const e = seq[run_end]; - self.reportSpanWithMessageId(.{ - .start = pat_start + s.start, - .end = pat_start + e.end, - }, "zwj"); - } + if (run_start) |rs| self.emitZwj(seq[rs], seq[run_end], pat_start, has_uv); + } + + /// A supplementary-plane character we keep as a single Character but which + /// ESLint (without u/v) sees as a surrogate pair. Literal 4-byte UTF-8 only. + fn zwjCharIsSupplementary(c: regex_parser.Character) bool { + return c.codepoint > 0xFFFF and (c.end - c.start) == 4; + } + + fn emitZwj(self: *const LintContext, before: regex_parser.Character, after: regex_parser.Character, pat_start: u32, has_uv: bool) void { + // Under u/v every char is one code point → whole spans. Without u/v a + // supplementary char is two surrogates: start at the before-char's low + // surrogate (UTF-8 midpoint) and end at the after-char's high surrogate + // (also the midpoint). BMP chars always use their whole span. + const before_split = !has_uv and zwjCharIsSupplementary(before); + const after_split = !has_uv and zwjCharIsSupplementary(after); + const start = before.start + @as(u32, if (before_split) 2 else 0); + const end = if (after_split) after.start + 2 else after.end; + self.reportSpanWithMessageId(.{ .start = pat_start + start, .end = pat_start + end }, "zwj"); } /// True for U+1F3FB..U+1F3FF — the Fitzpatrick skin-tone modifiers diff --git a/tests/differential/baseline.json b/tests/differential/baseline.json index e72a3a47..e069d475 100644 --- a/tests/differential/baseline.json +++ b/tests/differential/baseline.json @@ -1321,13 +1321,13 @@ "crash": 0 }, "native": { - "fn": 4, + "fn": 0, "fp": 0, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 4, + "fn": 0, "fp": 0, "crash": 0 } @@ -6343,14 +6343,14 @@ "crash": 0 }, "native": { - "fn": 8, - "fp": 2, + "fn": 4, + "fp": 0, "crash": 0, "skip": 0 }, "hybrid": { - "fn": 8, - "fp": 2, + "fn": 4, + "fp": 0, "crash": 0 } }, @@ -18200,9 +18200,9 @@ } }, "perf": { - "totalElapsedMs": 16339, - "runnerCasesPerSec": 5249, - "nativeCasesPerSec": 51085, + "totalElapsedMs": 16502, + "runnerCasesPerSec": 4982, + "nativeCasesPerSec": 48436, "totalCases": 56037 } } \ No newline at end of file From e03642cefa7144160c2ca7b4fd01dd3b13cf7b3a Mon Sep 17 00:00:00 2001 From: Eric San Date: Thu, 25 Jun 2026 11:11:27 +0800 Subject: [PATCH 6/7] =?UTF-8?q?test(differential):=20upgrade=20jsx?= =?UTF-8?q?=E2=86=92tsx=20on=20parse=20error;=20add=20ESTree-fidelity=20ha?= =?UTF-8?q?rness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- tests/differential/baseline.json | 14 ++-- tests/differential/estree-fidelity.js | 93 +++++++++++++++++++++++++++ tests/differential/run.js | 12 ++-- 3 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 tests/differential/estree-fidelity.js diff --git a/tests/differential/baseline.json b/tests/differential/baseline.json index e069d475..5ab95044 100644 --- a/tests/differential/baseline.json +++ b/tests/differential/baseline.json @@ -4898,8 +4898,8 @@ }, "indent": { "runner": { - "fn": 1, - "fp": 45, + "fn": 0, + "fp": 13, "crash": 0 }, "native": { @@ -4909,8 +4909,8 @@ "skip": 0 }, "hybrid": { - "fn": 1, - "fp": 45, + "fn": 0, + "fp": 13, "crash": 0 } }, @@ -18200,9 +18200,9 @@ } }, "perf": { - "totalElapsedMs": 16502, - "runnerCasesPerSec": 4982, - "nativeCasesPerSec": 48436, + "totalElapsedMs": 16662, + "runnerCasesPerSec": 5065, + "nativeCasesPerSec": 47753, "totalCases": 56037 } } \ No newline at end of file diff --git a/tests/differential/estree-fidelity.js b/tests/differential/estree-fidelity.js new file mode 100644 index 00000000..15e13615 --- /dev/null +++ b/tests/differential/estree-fidelity.js @@ -0,0 +1,93 @@ +#!/usr/bin/env node +// ESTree-fidelity diff harness. +// +// Parses a snippet with both the reference parser (@typescript-eslint/parser) +// and ez's parser, then walks the reference tree comparing `type` and `range` +// at every node against ez's ESTree. Use it to pin down where ez's ESTree +// diverges from the parser the differential oracle uses — especially for the +// most AST-sensitive rules like `indent`. +// +// Usage: +// node tests/differential/estree-fidelity.js '' [lang] +// node tests/differential/estree-fidelity.js --file path.ts [lang] +// lang: js | ts | jsx | tsx (default inferred: ts if code has TS syntax else js) + +const path = require("path"); +const JS_ROOT = path.join(__dirname, "..", "..", "js"); +const { parseSource, getTagNames } = require(path.join(JS_ROOT, "index.js")); +const adapter = require(path.join(JS_ROOT, "estree-adapter.js")); +const tsParser = require(path.join(JS_ROOT, "node_modules", "@typescript-eslint", "parser", "dist", "index.js")); + +function parseRef(code, jsx) { + return tsParser.parse(code, { + range: true, + loc: false, + comment: false, + jsx: !!jsx, + // no project — purely syntactic ESTree, which is all indent needs. + }); +} + +function isNode(v) { return v && typeof v === "object" && typeof v.type === "string"; } + +// Walk the reference tree; at each node compare against the matching ez node. +function diff(ref, ours, out, pathStr) { + if (!ref) return; + if (!ours) { out.push(`${pathStr}: ref ${ref.type}[${ref.range}] — ez node MISSING`); return; } + const ourType = ours.type; + if (ourType !== ref.type) { + out.push(`${pathStr}: TYPE ref=${ref.type}[${ref.range}] ez=${ourType}[${ours.range}]`); + // types diverged — children won't line up; stop this branch. + return; + } + const rr = ref.range, or = ours.range; + if (!or || rr[0] !== or[0] || rr[1] !== or[1]) { + out.push(`${pathStr} ${ref.type}: RANGE ref=[${rr}] ez=[${or}]`); + } + // Recurse into child keys present on the reference node. + for (const key of Object.keys(ref)) { + if (key === "type" || key === "range" || key === "loc" || key === "parent") continue; + const rv = ref[key]; + if (Array.isArray(rv)) { + let ov; + try { ov = ours[key]; } catch { ov = undefined; } + for (let i = 0; i < rv.length; i++) { + if (!isNode(rv[i])) continue; + const oc = Array.isArray(ov) ? ov[i] : undefined; + diff(rv[i], oc, out, `${pathStr}.${key}[${i}]`); + } + } else if (isNode(rv)) { + let ov; + try { ov = ours[key]; } catch { ov = undefined; } + diff(rv, ov, out, `${pathStr}.${key}`); + } + } +} + +function main() { + const argv = process.argv.slice(2); + let code, lang; + if (argv[0] === "--file") { + code = require("fs").readFileSync(argv[1], "utf8"); + lang = argv[2]; + } else { + code = argv[0]; + lang = argv[1]; + } + if (code == null) { console.error("usage: estree-fidelity.js '' [lang]"); process.exit(2); } + const jsx = lang === "jsx" || lang === "tsx"; + if (!lang) lang = /\b(abstract|public|private|protected|readonly|implements|interface|enum|namespace|:\s*\w+\s*[=;)])/.test(code) ? "ts" : "js"; + + adapter.setTagNames(getTagNames()); + const ours = parseSource(code, { lang, sourceType: "module" }).root(); + let ref; + try { ref = parseRef(code, jsx); } + catch (e) { console.error("reference parse error:", e.message); process.exit(1); } + + const out = []; + diff(ref, ours, out, "Program"); + if (out.length === 0) console.log("✓ identical (type + range) across", code.split("\n").length, "lines"); + else { console.log(`${out.length} difference(s):`); for (const l of out) console.log(" " + l); } +} + +main(); diff --git a/tests/differential/run.js b/tests/differential/run.js index 8f697636..128f0cee 100644 --- a/tests/differential/run.js +++ b/tests/differential/run.js @@ -552,15 +552,17 @@ function runRunnerForRule(src, ruleName, ruleModule, ruleOptions, sourceType, tc // IMPORTANT: count JS errors BEFORE the TS parse because the NAPI parser shares one // internal buffer — after parse() is called again, ast._nodeTags points to the new // buffer and the JS error count is lost. - if (parseLang === "js" && ast._nodeTags && ast._nodeTags.includes(193 /* T.error_node */)) { - const _jsErrCount = ast._nodeTags.reduce((n, t) => n + (t === 193 ? 1 : 0), 0); - const _tsFilename = filename.replace(/\.js$/, ".ts") || filename; + // jsx upgrades to tsx (the oracle for these cases used @typescript-eslint/parser, + // which accepts TS syntax in a jsx-enabled file; our default jsx parse errors on it). + if ((parseLang === "js" || parseLang === "jsx") && ast._nodeTags && ast._nodeTags.includes(193 /* T.error_node */)) { + const _upgradeLang = parseLang === "jsx" ? "tsx" : "ts"; + const _tsFilename = filename.replace(/\.jsx?$/, parseLang === "jsx" ? ".tsx" : ".ts") || filename; try { - const _tsAst = parse(src, { filename: _tsFilename, lang: "ts", globals: zigGlobals, sourceType, + const _tsAst = parse(src, { filename: _tsFilename, lang: _upgradeLang, globals: zigGlobals, sourceType, parserOptions: tcLanguageOptions.parserOptions }); const _tsErrCount = _tsAst._nodeTags.reduce((n, t) => n + (t === 193 ? 1 : 0), 0); if (_tsErrCount === 0) ast = _tsAst; - } catch { /* keep JS ast */ } + } catch { /* keep original ast */ } } _runnerParseMs += Date.now() - _p0; // Re-use the caller-provided plugin identity (same object → buildVisitorMap fast path), From 2821c21db8a8cdeb22188722d9ea57f4c5574053 Mon Sep 17 00:00:00 2001 From: Eric San Date: Thu, 25 Jun 2026 11:33:35 +0800 Subject: [PATCH 7/7] =?UTF-8?q?test(differential):=20upgrade=20js=E2=86=92?= =?UTF-8?q?ts=20when=20TS=20is=20strictly=20cleaner=20(not=20only=20fully-?= =?UTF-8?q?clean)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tests/differential/baseline.json | 10 +++++----- tests/differential/run.js | 9 ++++++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/differential/baseline.json b/tests/differential/baseline.json index 5ab95044..1fbb7e47 100644 --- a/tests/differential/baseline.json +++ b/tests/differential/baseline.json @@ -6194,7 +6194,7 @@ }, "object-shorthand": { "runner": { - "fn": 18, + "fn": 6, "fp": 0, "crash": 0 }, @@ -6205,7 +6205,7 @@ "skip": 0 }, "hybrid": { - "fn": 18, + "fn": 6, "fp": 0, "crash": 0 } @@ -18200,9 +18200,9 @@ } }, "perf": { - "totalElapsedMs": 16662, - "runnerCasesPerSec": 5065, - "nativeCasesPerSec": 47753, + "totalElapsedMs": 18925, + "runnerCasesPerSec": 4755, + "nativeCasesPerSec": 46338, "totalCases": 56037 } } \ No newline at end of file diff --git a/tests/differential/run.js b/tests/differential/run.js index 128f0cee..896b44c5 100644 --- a/tests/differential/run.js +++ b/tests/differential/run.js @@ -555,13 +555,20 @@ function runRunnerForRule(src, ruleName, ruleModule, ruleOptions, sourceType, tc // jsx upgrades to tsx (the oracle for these cases used @typescript-eslint/parser, // which accepts TS syntax in a jsx-enabled file; our default jsx parse errors on it). if ((parseLang === "js" || parseLang === "jsx") && ast._nodeTags && ast._nodeTags.includes(193 /* T.error_node */)) { + // Count JS errors BEFORE the TS parse — the NAPI parser shares one internal + // buffer, so after parse() runs again `ast._nodeTags` points at the new buffer. + const _jsErrCount = ast._nodeTags.reduce((n, t) => n + (t === 193 ? 1 : 0), 0); const _upgradeLang = parseLang === "jsx" ? "tsx" : "ts"; const _tsFilename = filename.replace(/\.jsx?$/, parseLang === "jsx" ? ".tsx" : ".ts") || filename; try { const _tsAst = parse(src, { filename: _tsFilename, lang: _upgradeLang, globals: zigGlobals, sourceType, parserOptions: tcLanguageOptions.parserOptions }); const _tsErrCount = _tsAst._nodeTags.reduce((n, t) => n + (t === 193 ? 1 : 0), 0); - if (_tsErrCount === 0) ast = _tsAst; + // Upgrade when TS is strictly cleaner — covers the fully-clean case and the + // partially-clean case (TS parses most properties even when one unsupported + // type syntax like `(void)` produces a single ErrorNode). The oracle parsed + // these with @typescript-eslint/parser, so the cleaner TS AST matches better. + if (_tsErrCount < _jsErrCount) ast = _tsAst; } catch { /* keep original ast */ } } _runnerParseMs += Date.now() - _p0;