Skip to content

fix(resolve): enum name/ordinal go-to-def + whole-receiver-as-T generic return substitution - #318

Merged
Hessesian merged 2 commits into
mainfrom
fix/enum-intrinsics-and-generic-return-subst
Sep 14, 2026
Merged

Hessesian merged 2 commits into
mainfrom
fix/enum-intrinsics-and-generic-return-subst

Conversation

@Hessesian

Copy link
Copy Markdown
Owner

Summary

Two scouted fixes from the "name"/"title"/"text" Gap investigation the user pushed back on (correctly flagging real value, not import-filtering noise). Stacked on #317.

  • .name/.ordinal resolution: these are compiler-generated kotlin.Enum instance members, same class of gap as .entries/values()/valueOf() which synthesize_enum_members already handled. Its own doc comment claimed .name/.ordinal were "a separate, already-handled concern" via the indexer's synthetic_enum_field — that helper only feeds type inference (hover/inlay chain propagation), never a real go-to-def Location, so qualified resolution dead-ended. Extended the same synthesis site. Confirmed on the real Moneta corpus: name drops out of the member-ref Gap list entirely (was fix: recover annotation-mis-parsed interfaces + strip suspend before receiver type #182, e.g. Insurance.EType.entries.firstOrNull { it.name.equals(...) }).

  • Generic return-type substitution for whole-receiver-as-T: StrategyOutcome::finalize's ReceiverDerived arm only substituted a type param nested inside the receiver's own type argument (List<T>'s T, via build_type_arg_subst). It missed the shape where the extension's own type parameter IS the whole receiver (fun <T> T?.required(field): T) — filled the gap using build_ext_fn_type_subst, an existing helper already used by a different call path, without overriding any key the existing substitution already resolved. Covered by a unit test using the real resolve_call_expr_type entry point (not just the substitution helper in isolation).

CACHE_VERSION bumped (31→32): the parser's synthesized-symbol set changed shape.

Known limitation (documented, not fixed here)

The second fix is real and correctly tested, but corpus verification showed it does not by itself move the title/text Gap counts. Root cause: chain.rs's receiver_based_method never reaches the new substitution for required()'s actual real-world declaration shape (a member extension on a bare generic type parameter receiver, fun <T : Any> T?.required(...): T inside class NullableScope) — find_method_return_type_for_type returns None before substitution ever runs, because extension_by_receiver keys extensions by literal receiver text ("T"), with no wildcard match against a concrete receiver ("String"). That's a separate, deeper extension-registry gap affecting both type inference and (likely) other consumers — left for its own scoped investigation rather than folded in here.

Test plan

  • cargo test — 1930 passed, 0 failed
  • cargo clippy --all-targets -- -D warnings — clean
  • Both fixes have red-before/green-after unit tests
  • Live-verified against the real Moneta corpus via resolution-accuracy: overall member-ref recall 91.1% (flat/slightly up vs. prior 90.8-91.0%), name gone from the Gap top-20

🤖 Generated with Claude Code

https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF

Hessesian added a commit that referenced this pull request Sep 14, 2026
Triaged all 10 findings from the automated review (1 posted inline,
9 suppressed but listed in the review body). Fixed 6, each with a
red-before-fix-verified regression test:

- companion_applies was gated on nested.is_empty(), so Outer.Inner.member()
  never reached Inner's own companion object even though anchors_for had
  already normalized the anchor down to Inner itself.
- The last-resort JAR-extension fallback in resolve_qualified keyed on the
  qualifier's literal ROOT spelling ("Outer") instead of the leaf
  ("Inner") — extension_by_receiver keys by the receiver's own simple name.
- supertype_extension was skipped whenever inherited_members was non-empty,
  with no regard for arity — the same bug own_type_extension was already
  fixed for a few lines above, on its sibling field.
- The Java-getter synthetic-property retry checked the RECEIVER's own
  declaring-file language, rejecting a getter genuinely inherited from a
  Java ancestor whenever the receiver's own class is Kotlin-declared. Now
  checks each getter candidate's own declaring file.
- resolve_from_class_hierarchy_scoped's per-ancestor closure fell through to
  an unscoped find_name_in_uri whenever the container-tagged lookup came up
  empty — but an empty result there usually just means "this ancestor has
  no member of this name" (normal while walking up), not "no container
  metadata exists". Reintroduced the exact cross-container leak this file's
  own container-tag scoping exists to prevent.
- The denylist prefix check only matched a DESCENDANT package
  (trailing-dot starts_with), never the bare prefix package itself.

Also fixed: default_kotlin_import_tie_break now gated on the origin file's
own language — it was applying Kotlin's default-import set even from a
Java origin, which can never implicitly import kotlin.*.

Investigated but NOT changed (documented, not silently dropped):
- package.rs's coarse whole-JAR package fallback (Copilot flagged it as
  imprecise for a multi-package JAR) — tried removing it, but that broke
  three real existing tests that specifically depend on it for JARs with no
  per-symbol package table. Reverted; the imprecision risk is real but this
  codebase's own tests show the fallback is the tested, intentional
  trade-off, not an oversight.
- The line-scan fallback in find_name_in_uri_after_line isn't
  container-bounded even when container_name is known (only matters for
  un-indexed names, e.g. constructor params, colliding with a same-named
  sibling elsewhere in a degenerate JAR-stub file) — a correct fix needs the
  container's own end-line threaded through, which isn't available at every
  call site; left for its own investigation.
- The Java-getter retry doesn't filter candidates to zero-arg overloads —
  needs arity data threaded through member_tiers's Vec<Location>, which
  currently loses it; left for its own investigation.

Verified: cargo test (1933 passed), cargo clippy -D warnings clean, and a
full resolution-accuracy scan against the real Moneta corpus shows no new
anomalies in the Gap/FilteredCandidate lists (a small aggregate recall
delta vs. the prior measurement is fully explained by this branch not yet
containing PR #318's separate enum-intrinsics fix).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF
Hessesian added a commit that referenced this pull request Sep 14, 2026
Triaged all 10 findings from the automated review (1 posted inline,
9 suppressed but listed in the review body). Fixed 6, each with a
red-before-fix-verified regression test:

- companion_applies was gated on nested.is_empty(), so Outer.Inner.member()
  never reached Inner's own companion object even though anchors_for had
  already normalized the anchor down to Inner itself.
- The last-resort JAR-extension fallback in resolve_qualified keyed on the
  qualifier's literal ROOT spelling ("Outer") instead of the leaf
  ("Inner") — extension_by_receiver keys by the receiver's own simple name.
- supertype_extension was skipped whenever inherited_members was non-empty,
  with no regard for arity — the same bug own_type_extension was already
  fixed for a few lines above, on its sibling field.
- The Java-getter synthetic-property retry checked the RECEIVER's own
  declaring-file language, rejecting a getter genuinely inherited from a
  Java ancestor whenever the receiver's own class is Kotlin-declared. Now
  checks each getter candidate's own declaring file.
- resolve_from_class_hierarchy_scoped's per-ancestor closure fell through to
  an unscoped find_name_in_uri whenever the container-tagged lookup came up
  empty — but an empty result there usually just means "this ancestor has
  no member of this name" (normal while walking up), not "no container
  metadata exists". Reintroduced the exact cross-container leak this file's
  own container-tag scoping exists to prevent.
- The denylist prefix check only matched a DESCENDANT package
  (trailing-dot starts_with), never the bare prefix package itself.

Also fixed: default_kotlin_import_tie_break now gated on the origin file's
own language — it was applying Kotlin's default-import set even from a
Java origin, which can never implicitly import kotlin.*.

Investigated but NOT changed (documented, not silently dropped):
- package.rs's coarse whole-JAR package fallback (Copilot flagged it as
  imprecise for a multi-package JAR) — tried removing it, but that broke
  three real existing tests that specifically depend on it for JARs with no
  per-symbol package table. Reverted; the imprecision risk is real but this
  codebase's own tests show the fallback is the tested, intentional
  trade-off, not an oversight.
- The line-scan fallback in find_name_in_uri_after_line isn't
  container-bounded even when container_name is known (only matters for
  un-indexed names, e.g. constructor params, colliding with a same-named
  sibling elsewhere in a degenerate JAR-stub file) — a correct fix needs the
  container's own end-line threaded through, which isn't available at every
  call site; left for its own investigation.
- The Java-getter retry doesn't filter candidates to zero-arg overloads —
  needs arity data threaded through member_tiers's Vec<Location>, which
  currently loses it; left for its own investigation.

Verified: cargo test (1933 passed), cargo clippy -D warnings clean, and a
full resolution-accuracy scan against the real Moneta corpus shows no new
anomalies in the Gap/FilteredCandidate lists (a small aggregate recall
delta vs. the prior measurement is fully explained by this branch not yet
containing PR #318's separate enum-intrinsics fix).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF
Hessesian added a commit that referenced this pull request Sep 14, 2026
…easured gap fixes (#317)

* fix(resolve): stop leaking a sibling class's member across a JAR's synthetic file boundary

Real bug behind navController.navigate(route = "...") never resolving on a
real Android corpus: androidx.navigation's NavController.navigate is a real
overloaded JVM member (not an extension), and NavHostController extends it
without declaring its own navigate. Both classes compile into one synthetic
per-JAR FileData.

Two compounding bugs in the JAR-derived member lookup:

1. find_name_in_uri_after_line's final fallback matched the closest
   same-named symbol "at or after" a container's line with no container
   check at all -- for a JAR packing several classes into one file, this
   mis-attributed NavController's own navigate overloads to NavHostController
   purely by file position, corrupting resolve_qualified into treating an
   inherited member as an own, wrong-arity one.
2. resolve_from_class_hierarchy_scoped's ancestor walk used the single-
   result, arity-blind find_name_in_uri per ancestor instead of the
   container- and overload-aware find_all_names_scoped_to_container, so even
   reaching the right ancestor only ever returned one arbitrary overload.

find_name_in_uri_after_line now takes an optional container name and only
trusts a symbol's real container tag over position; a new
find_all_names_with_container_in_uri helper lets the hierarchy walk look up
every overload scoped to the ancestor's class name directly, without first
needing to resolve that ancestor's own declaration Location.

Verified on the real Moneta corpus: navigate goes from 220-224 occurrences
in the resolution-accuracy Gap top-20 to zero.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF

* fix(resolve): resolve a Java getter as the Kotlin synthetic property it exposes

Kotlin's Java-interop synthetic-property rule: `obj.foo` retries as
`obj.getFoo()` when `foo` names no real member and the declaring file is
Java. Implements the read-only, getFoo-only slice of that rule inside
resolve_qualified's uppercase-branch member tier, after the real member/
inherited-member lookups (now unified behind member_or_inherited_member)
return empty and before the supertype-extension tier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF

* fix(resolve): match the JAR-extension probe's fallback by declaration, not name alone, and de-duplicate the star-import package list and default-import constant

resolve_qualified's item-1 JAR-extension fallback materialized its Location
by finding the first symbol in the declaring file whose bare name matched,
not the actual extension declaration -- so a same-named, unrelated symbol
elsewhere in the file could win, returning the wrong range. Fixed by
matching through extension_declaration_matches (receiver + container), the
same helper the sibling in-scope lookup already uses. The fallback stays
deliberately fail-open (no extension_is_in_scope check) -- that behavior is
load-bearing for resolve_extension_fn_on_uppercase_qualifier and is now
guarded by its own regression test.

Also de-duplicates two small helpers that straddle the resolve.rs/
package_scope.rs boundary planned for the next task: a single
star_import_packages() replaces three copies of the same star-import filter,
and is_default_import_package() now reads KOTLIN_DEFAULT_IMPORT_PACKAGES
instead of carrying its own identical 10-entry list.

Corpus measurement (resolution-accuracy /home/ocel/Work/Moneta/android):
member recall 90.8% (150626/165876), no regression from the most recent
known baseline (90.3%, 149718/165795) -- expected, since item 1 is a
goto-definition precision fix the benchmark doesn't score and items 2-3 are
behavior-preserving deduplications.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF

* refactor(resolver): split resolve.rs into a spine and nine named siblings

Pure mechanical move, zero behavior change: resolve.rs shrinks from 2753 to
686 lines. Moved by resolution-order taxonomy into container.rs, package.rs,
tie_break.rs, package_scope.rs, imports.rs, extension.rs, scope_check.rs,
qualified.rs, and platform_types.rs, per the task-2 brief's module boundary
table (moved innermost-first, compiling and testing after each module).

Includes the brief's one documented exception: corrects the wrong comment in
resolve_qualified (now in qualified.rs) claiming an "exact-key extension" had
already been ruled out before the supertype-extension fallback — it hadn't;
member_or_inherited_member's inherited-member half is a pure member lookup.

1910 tests pass before and after (0 failed, 3 ignored), clippy and fmt clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF

* refactor(resolve): give resolve_qualified one parse, one anchor, and one precedence ladder

`resolve_qualified` implemented the same three-stage sequence twice -- parse the
qualifier, normalize it to a receiver anchor, run a member/extension precedence
ladder -- once for an uppercase (`Outer.Inner`) root and once for a lowercase
(`variable.field`) root. Because the sequence was written twice with no shared
boundary, the two ladders had drifted: a tier that exists on one branch and not
the other, and an anchor re-derived per branch so an extension probe could stay
keyed on the ROOT while the anchor had already moved to the leaf.

The four stages are now named and separately testable:

  parse_qualifier   -> QualifierRoot  (no Indexer, no IO)
  anchors_for       -> ReceiverAnchor (qualifier -> receiver, nothing else)
  candidates_on     -> QualifiedCandidates (the four tiers)
  companion_member_on                  (a different question, kept separate)

`ReceiverAnchor` carries the leaf type's full declaration `Location` -- file and
range -- because `find_all_names_scoped_to_container` scopes its member search
by the container's declaration range, not merely its file. It also carries the
LEAF type's simple name, which makes "probe keyed on the root while the anchor
has moved to `Inner`" unrepresentable rather than merely fixed.

Deliberate, named behaviour correction (not a regression): the two branches did
not merely differ in which tiers existed, they checked them in a DIFFERENT
ORDER. The uppercase branch probed the own-type extension FIRST and returned
early, before its member tier was ever computed; the lowercase branch ran
members first and reached its extension tier last. One ladder can encode only
one order, and the lowercase branch's is the correct one -- Kotlin resolves a
real member over a same-named extension. The uppercase branch's
extension-first check was the outlier and is corrected, not averaged.
`resolve_qualified_uppercase_receiver_own_member_now_wins_over_own_type_extension`
is the test for exactly this, and is red against the previous code.
`resolve_imported_extension_preferred_over_member` encoded the old order and is
inverted and renamed to `resolve_member_preferred_over_imported_extension`.

Precedence is carried by candidate ORDER, not by refusing to compute a tier: a
same-named member does not always satisfy the call's arity, so the own-type
extension is appended below a winning member rather than dropped, matching what
the supertype-extension tier has always done. Measured: short-circuiting it
instead cost 791 `IMockProvider.loadJSONFromAssets` call sites (a 1-arg
extension shadowed by a 2-arg member), which fell out of resolution into the
ambiguous bucket.

Moneta corpus (`resolution-accuracy`, before -> after):
  member recall      90.9% (150924/166021) -> 90.9% (150553/165558)
  Gap (member)       8214 -> 8121
  FilteredCandidate  6883 -> 6884
No new name in either the Gap or FilteredCandidate top-20; remaining per-name
movement is single-digit and within the tool's run-to-run variance, with
`collectEmit` (-13) and `collectLatest` (-9) leaving the ambiguous bucket.

Tests: 1957 -> 1964 (7 added, 0 failures).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF

* test(resolve): lock in the own-type extension tier's coverage post-Task-2b

Task 2b's decomposition of resolve_qualified (parse_qualifier -> anchors_for
-> candidates_on) already populates own_type_extension unconditionally, for
both TypePath and ValuePath roots and every nesting depth, since both
families normalize through the same ReceiverAnchor before the ladder runs.
Task 3's planned ~20-line fix (jar-promotion-latency-budget-plan) is fully
absorbed: all six cases from its brief (lowercase-root wrong-arity member,
uppercase-root regression guard, member-wins-on-arity-match, supertype
fallback still reached, and both finding-11 nested-type-qualifier exits) are
green against today's code with no production change. These tests are the
proof.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF

* fix(resolver): stop dropping the hierarchy walk's budget at the ambiguity-safe tail

`supertype_targets` threads its own `sidecar_budget: &mut usize` into the
per-hop `ensure_jar_definitions_for` promotion, but its two calls into
`resolve_symbol_hierarchy_ambiguity_safe` (the nested-type "outer"
resolution and the final leaf-name tail) never received it. That function's
`resolve_chain` tail (`ResolveIo::HierarchyAmbiguitySafe`) reads via
`indexer.lookup_definitions`, which internally promotes with a hardcoded
zero budget -- so a supertype name resolvable only through this ambiguity-
safe path, and living in a not-yet-materialized JAR, silently fell through
to a zero-budget promotion even mid-walk with real budget remaining.

Adds `sidecar_budget: &mut usize` to `resolve_symbol_hierarchy_ambiguity_safe`
and spends it on a pre-promotion of `name` before falling into
`resolve_chain`, threaded through from both `supertype_targets` call sites.
Plain `&mut usize`, matching every other budget site in this codebase --
independent of the still-parked `LatencyClass`/`JarPromotionBudget` work.

New test: hierarchy_walk_shares_its_budget_with_the_ambiguity_safe_tail,
modeled on the promotion-counting harness at tests.rs:7568. Verified red
against pre-fix code, green after.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF

* test(resolve): split a two-assertion test to satisfy the no-"and" naming rule

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF

* fix(resolve): a missing nested-type segment must yield no anchor, not fall back to the root

Copilot review finding on #317: `Outer.Missing.member` -- `Outer` resolves
but `Missing` names no real nested type of it -- fell through to the
declaration-less anchor fallback keyed on `root` ("Outer") whenever the
nested walk produced zero anchors, regardless of why it was empty. That
silently resolved `member` against `Outer` itself, as if `.Missing` had
never been written. The fallback is now reserved for a genuinely unresolved
root (no indexed declaration at all); a resolved root whose nested segment
failed yields no anchors.

Also fixes a Copilot finding on #315: a decoy-overload test comment said
`navigate(Int)` but the fixture encodes a 0-arg overload via
`param_counts: (0, 0)` -- text now matches what's actually asserted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF

* fix(resolve): address PR #317 Copilot review findings

Triaged all 10 findings from the automated review (1 posted inline,
9 suppressed but listed in the review body). Fixed 6, each with a
red-before-fix-verified regression test:

- companion_applies was gated on nested.is_empty(), so Outer.Inner.member()
  never reached Inner's own companion object even though anchors_for had
  already normalized the anchor down to Inner itself.
- The last-resort JAR-extension fallback in resolve_qualified keyed on the
  qualifier's literal ROOT spelling ("Outer") instead of the leaf
  ("Inner") — extension_by_receiver keys by the receiver's own simple name.
- supertype_extension was skipped whenever inherited_members was non-empty,
  with no regard for arity — the same bug own_type_extension was already
  fixed for a few lines above, on its sibling field.
- The Java-getter synthetic-property retry checked the RECEIVER's own
  declaring-file language, rejecting a getter genuinely inherited from a
  Java ancestor whenever the receiver's own class is Kotlin-declared. Now
  checks each getter candidate's own declaring file.
- resolve_from_class_hierarchy_scoped's per-ancestor closure fell through to
  an unscoped find_name_in_uri whenever the container-tagged lookup came up
  empty — but an empty result there usually just means "this ancestor has
  no member of this name" (normal while walking up), not "no container
  metadata exists". Reintroduced the exact cross-container leak this file's
  own container-tag scoping exists to prevent.
- The denylist prefix check only matched a DESCENDANT package
  (trailing-dot starts_with), never the bare prefix package itself.

Also fixed: default_kotlin_import_tie_break now gated on the origin file's
own language — it was applying Kotlin's default-import set even from a
Java origin, which can never implicitly import kotlin.*.

Investigated but NOT changed (documented, not silently dropped):
- package.rs's coarse whole-JAR package fallback (Copilot flagged it as
  imprecise for a multi-package JAR) — tried removing it, but that broke
  three real existing tests that specifically depend on it for JARs with no
  per-symbol package table. Reverted; the imprecision risk is real but this
  codebase's own tests show the fallback is the tested, intentional
  trade-off, not an oversight.
- The line-scan fallback in find_name_in_uri_after_line isn't
  container-bounded even when container_name is known (only matters for
  un-indexed names, e.g. constructor params, colliding with a same-named
  sibling elsewhere in a degenerate JAR-stub file) — a correct fix needs the
  container's own end-line threaded through, which isn't available at every
  call site; left for its own investigation.
- The Java-getter retry doesn't filter candidates to zero-arg overloads —
  needs arity data threaded through member_tiers's Vec<Location>, which
  currently loses it; left for its own investigation.

Verified: cargo test (1933 passed), cargo clippy -D warnings clean, and a
full resolution-accuracy scan against the real Moneta corpus shows no new
anomalies in the Gap/FilteredCandidate lists (a small aggregate recall
delta vs. the prior measurement is fully explained by this branch not yet
containing PR #318's separate enum-intrinsics fix).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
@Hessesian
Hessesian changed the base branch from fix/extension-supertype-variable-receiver to main September 14, 2026 08:57
Hessesian and others added 2 commits September 14, 2026 11:01
…eceiver-as-T in generic returns

Two root causes found while scouting the "name"/"title"/"text" corpus Gap
entries the user flagged as real (not import-filtering noise):

- `.name`/`.ordinal` are compiler-generated kotlin.Enum instance members,
  same class of gap as `.entries`/`values()`/`valueOf()` (already handled).
  A stale doc comment claimed they were "already handled" by the indexer's
  synthetic_enum_field, but that only feeds type inference, never a real
  go-to-def Location -- qualified resolution dead-ended. Extended
  synthesize_enum_members to also synthesize name/ordinal, the same way
  entries/values/valueOf already are. Confirmed on the real Moneta corpus:
  "name" drops out of the member-ref Gap list entirely (was #182 e.g. at
  InsuranceExtensions.kt:136, `it.name.equals(...)` over `.entries`).

- `StrategyOutcome::finalize`'s ReceiverDerived arm only substituted a type
  param NESTED inside the receiver's own type argument (`List<T>`'s `T`),
  via build_type_arg_subst. It missed the shape where the extension's own
  type parameter IS the whole receiver (`fun <T> T?.required(field): T`) --
  effective_type ("String") declares no class type params, so raw_return
  ("T") stayed unsubstituted. Filled the gap with build_ext_fn_type_subst
  (an existing helper already used by a different call path), without
  overriding any key the existing substitution already resolved.

CACHE_VERSION bumped: the parser's synthesized-symbol set changed shape.

Note: the second fix is real and covered by a unit test, but does not by
itself move the "title"/"text" corpus Gap counts -- corpus verification
after the fix showed chain.rs's receiver_based_method never reaches this
substitution for required()'s real declaration shape (a member extension
on a bare generic type parameter receiver): find_method_return_type_for_type
returns None before substitution ever runs, because extension_by_receiver
keys extensions by literal receiver text ("T"), with no wildcard match for
a concrete receiver ("String"). That is a separate, deeper registry gap,
not this fix's scope -- left for its own investigation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF
…aming rule

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7ZonwYUh94VsuQphiHykF
@Hessesian
Hessesian force-pushed the fix/enum-intrinsics-and-generic-return-subst branch from 22d3fa9 to 3ecfc69 Compare September 14, 2026 09:02
@Hessesian
Hessesian requested a lite review from Copilot September 14, 2026 09:20
@Hessesian
Hessesian merged commit eaf7204 into main Sep 14, 2026
5 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Resolver correctness issues and unsafe callable metadata lookup remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Fixes enum name/ordinal go-to-definition and whole-receiver generic return substitution.

Changes:

  • Synthesizes enum name and ordinal members.
  • Adds receiver-based generic substitution and regression tests.
  • Bumps the cache version to 32.
File summaries
File Summary
src/resolver/tests.rs Tests enum member resolution; coverage should use instance access and reject Flavor.name.
src/parser.rs Adds synthetic enum members; current exposure can affect invalid qualified and unqualified lookups.
src/indexer/infer/mod_tests.rs Adds whole-receiver generic substitution coverage.
src/indexer/infer/chain.rs Applies receiver substitutions; callable metadata lookup may select the wrong declaration and adds unnecessary hot-path work.
src/indexer/cache.rs Bumps the cache version.
Review details

Suppressed comments (3)

src/indexer/infer/chain.rs:619

  • This lookup is unconditional for every ReceiverDerived call, including methods whose raw_return is already concrete. It repeats a name scan and can invoke ensure_jar_definitions_for on the chain-inference hot path, adding avoidable work to ordinary calls; gate it on an unresolved generic return or reuse metadata from method resolution.
                if let Some(info) = ctx.deps.find_fun_callable_info(ctx.fn_name, ctx.uri) {
                    if !info.extension_receiver_type.is_empty() && !info.type_params.is_empty() {

src/parser.rs:555

  • name/ordinal are instance-only members, but synthesizing them with container: Some(cls.name) makes them direct members of the enum's type scope. resolve_qualified treats a TypePath such as Flavor.name as companion-only (see src/resolver/qualified.rs:92-105), so this causes invalid class-qualified accesses to resolve. Keep these symbols available from value/receiver lookups without exposing them from the type-root candidate tier, or model them through kotlin.Enum; add a negative Flavor.name regression test.
        symbols.push(SymbolEntry {
            name: "name".to_owned(),
            kind: SymbolKind::PROPERTY,
            visibility: cls.visibility,
            range: cls.selection_range,

src/resolver/tests.rs:1152

  • Flavor is passed as a type qualifier here, so this exercises Flavor.name, not the intended instance access (flavor.name/it.name). Per resolve_qualified, type-qualified lookup must not return instance members; with the new symbols this assertion passes only by introducing that invalid behavior and therefore cannot prove the Moneta regression. Test through a value or lambda receiver and separately assert that Flavor.name remains unresolved.
    assert!(
        !resolve_symbol(&idx, "name", Some("Flavor"), &uri).is_empty(),
        "Flavor.name (kotlin.Enum.name) did not resolve"
  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +618 to +622
if let Some(info) = ctx.deps.find_fun_callable_info(ctx.fn_name, ctx.uri) {
if !info.extension_receiver_type.is_empty() && !info.type_params.is_empty() {
let ext_subst = build_ext_fn_type_subst(
&info.extension_receiver_type,
&receiver_type,
Comment thread src/parser.rs
Comment on lines +551 to +555
symbols.push(SymbolEntry {
name: "name".to_owned(),
kind: SymbolKind::PROPERTY,
visibility: cls.visibility,
range: cls.selection_range,
@Hessesian
Hessesian deleted the fix/enum-intrinsics-and-generic-return-subst branch September 14, 2026 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants