Skip to content

fix(resolve): as? cast nullability, semicolon-bound same-line smart cast - #320

Merged
Hessesian merged 2 commits into
mainfrom
fix/gap-scout-findings-batch1
Sep 14, 2026
Merged

Hessesian merged 2 commits into
mainfrom
fix/gap-scout-findings-batch1

Conversation

@Hessesian

Copy link
Copy Markdown
Owner

Summary

Follow-up to #319 (already merged): two real findings from Copilot's review of that PR, both verified red-then-green.

  • infer_as_expr_type treated as? identically to as, always returning the bare target type. Kotlin's safe cast always yields a nullable result regardless of whether the target type itself carries a ? — x as? Activity is Activity?, never bare Activity. Now checks the as_expression's own operator token.
  • Making if_is_smart_cast's same-line scan inclusive (fix(resolve): same-line if-is smart-cast, as-cast type inference, nested-ctor qualifier loss #319) covered the whole line regardless of column: if (x is Y) x.use(); x.other() — a brace-less if only guards ONE statement, ending at the first top-level ;. Threaded the cursor's column through smart_cast_type_at_line so the same-line case can decline once the access falls past that semicolon. Every other caller (multi-line bodies, no column available) passes None and keeps its existing behavior exactly.

Test plan

  • cargo test — 1941 passed, 0 failed
  • cargo clippy --all-targets -- -D warnings — clean
  • Both findings have a red-before/green-after test

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

Unresolved moderate issues remain in safe-cast nullability, same-line smart-cast boundaries, and qualified constructor inference.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Improves Kotlin safe-cast type inference, same-line smart-cast handling, and nested constructor resolution.

Changes:

  • Adds nullable as? type inference.
  • Bounds same-line smart casts by cursor position and semicolons.
  • Preserves qualified constructor names and adds regression tests.
File summaries
File Summary and findings
src/resolver/tests.rs Adds and updates smart-cast regression tests.
src/resolver/infer.rs Threads cursor columns into smart-cast inference. Nit (1 vote): rename col to column.
src/resolver/infer_lines.rs Implements same-line smart-cast handling. Moderate (3 votes): semicolon detection can use earlier or nested delimiters. Moderate (1 vote): trailing-lambda braces can suppress valid narrowing. Nit (3 votes): rename col to column.
src/queries.rs Adds the as_expression CST kind.
src/indexer/infer/mod_tests.rs Adds cast and nested-constructor inference tests.
src/indexer/infer/expr_type.rs Infers cast expression types. Moderate (2 votes): function-type safe casts must wrap the whole function type in nullability. Nit (1 vote): use StrExt::is_nullable().
src/indexer/infer/chain.rs Preserves qualified constructor names. Moderate (1 vote): normalize spaced qualifier segments. Moderate (1 vote): avoid treating value-qualified calls as type paths.
Review details

Suppressed comments (5)

src/indexer/infer/chain.rs:854

  • utf8_text preserves source whitespace, so a legal spaced qualifier such as Outer . Builder(x) produces Outer . Builder. The dotted resolver splits that into segments containing spaces and cannot resolve the nested type. Build this name from normalized CST identifier segments (or trim each segment) rather than using the raw callee span, and add a spaced-qualifier regression test.
    let type_name = if ctx.callee.kind() == KIND_NAV_EXPR {
        ctx.callee
            .utf8_text(ctx.bytes)
            .map(str::to_owned)

src/indexer/infer/chain.rs:855

  • This branch runs for every KIND_NAV_EXPR, including value-qualified calls such as factory.Builder(...), not only type-qualified nested constructors. If indexed method resolution misses an uppercase method, returning factory.Builder as the inferred type misclassifies a value path as a type path; preserve the dotted text only when the callee is actually rooted at an uppercase/type identifier, and otherwise avoid or retain the appropriate leaf fallback.
    let type_name = if ctx.callee.kind() == KIND_NAV_EXPR {
        ctx.callee
            .utf8_text(ctx.bytes)
            .map(str::to_owned)
            .unwrap_or_else(|_| ctx.fn_name.to_owned())

src/indexer/infer/expr_type.rs:337

  • Use the canonical StrExt::is_nullable() helper instead of reimplementing nullability with ends_with('?'). The helper is the repository-wide nullable check and handles trailing whitespace consistently (src/str_ext.rs:25-30).
    if is_safe_cast && !type_text.ends_with('?') {

src/resolver/infer.rs:260

  • The new col parameter is an abbreviation; use the full name column (and update its uses) so this position-sensitive API follows the repository's no-abbreviated-names guideline.
fn smart_cast_narrowed_type(
    indexer: &Indexer,
    name: &str,
    uri: &Url,
    line: u32,
    col: Option<u32>,
) -> Option<String> {

src/resolver/infer_lines.rs:793

  • Balanced braces on the same line are treated as an if block and suppress the narrowing. In valid if (value is Target) value.use { }, extract_if_is_type finds the test but opens == closes, so this new inclusive path still reports no cast for the member access. Distinguish braces belonging to a trailing lambda from an if body (or use the CST) and add a regression test.
                    // A brace-less `if` on the CURSOR's own line covers only
                    // ONE statement, ending at the first top-level `;` — an
                    // access after that `;` on the same physical line
                    // belongs to a later, unguarded statement (Copilot
                    // review finding). Only checked for this exact shape:
  • Files reviewed: 7/7 changed files
  • Comments generated: 3
  • 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 thread src/indexer/infer/expr_type.rs Outdated
Comment on lines +337 to +338
if is_safe_cast && !type_text.ends_with('?') {
Some(format!("{type_text}?"))
Comment thread src/resolver/infer_lines.rs Outdated
Comment on lines +799 to +801
lines[i].find(';').is_some_and(|byte_pos| {
let semi_col = lines[i][..byte_pos].encode_utf16().count() as u32;
col > semi_col
Comment thread src/resolver/infer_lines.rs Outdated
Comment on lines +689 to +690
line: u32,
col: Option<u32>,
Two real findings from Copilot's review of PR #319, both verified red-then-green:

- infer_as_expr_type treated `as?` identically to `as`, always returning
  the bare target type. Kotlin's safe cast always yields a nullable result
  regardless of whether the target type itself carries a `?` in source --
  `x as? Activity` is `Activity?`, never bare `Activity`. Now checks the
  as_expression's own operator token.

- Making if_is_smart_cast's same-line scan inclusive (PR #319) covered the
  whole line regardless of column: `if (x is Y) x.use(); x.other()` -- a
  brace-less if only guards ONE statement, ending at the first top-level
  `;`. Threaded the cursor's column through smart_cast_type_at_line so the
  same-line case can decline once the access falls past that semicolon.
  Every other caller (multi-line bodies, no column available) passes None
  and keeps its existing behavior exactly.

Verified: cargo test (1941 passed), cargo clippy -D warnings clean.
All 8 findings from the automated review (3 posted inline, 5 more in the
review body's suppressed-comments list), each verified red-then-green:

- infer_as_expr_type: `as?` on a function-type target misrendered nullability
  onto the return type (`(String) -> Int?`) instead of the whole function
  value (`((String) -> Int)?`). A bare function type is the only Kotlin
  type-annotation shape starting with `(`, so that's a sufficient signal to
  wrap the whole thing before appending `?`.
- Same function: use StrExt::is_nullable() instead of reimplementing the
  check with ends_with('?').
- if_is_smart_cast's same-line bound searched for the terminating `;` from
  the start of the physical line, not from after the if-condition's own
  `)` — an EARLIER statement's semicolon (`foo(); if (x is Y) x.use()`)
  was mistaken for this if's own terminator. Also, balanced braces from a
  trailing lambda on the same line (`if (x is Y) x.use { }`) were
  indistinguishable from an unrelated multi-line block and suppressed a
  valid narrow. Rewrote the bound as a proper forward scan from the
  if-condition's own end, depth-tracking parens/brackets/braces so only a
  genuinely top-level `;` counts as the statement's terminator.
- Renamed the new `col` parameter/binding to `column` throughout (repo's
  no-abbreviated-names guideline).
- constructor_fallback: the qualified-constructor-name fix used the
  callee's raw span text, which preserves source whitespace (`Outer .
  Builder(x)` produced spaced, unresolvable segments). Now builds the
  dotted name from clean identifier segments via collect_nav_segments.
- Same function: the fix fired for EVERY navigation_expression callee,
  including a value-qualified call whose root is a lowercase variable
  (`factory.Builder(...)`), misclassifying a value path as a type path.
  Now requires the chain's root segment to itself be uppercase (a type)
  before treating the dotted text as a constructed type name, falling back
  to the bare leaf (the pre-existing behavior for this shape) otherwise.

Verified: cargo test (1946 passed), cargo clippy -D warnings clean, and a
resolution-accuracy scan against the real Moneta corpus confirms no
regression on this work's own targets (`start`, `fragmentArguments`,
`fragmentBundle` all still absent from the Gap top-20). Aggregate recall
moved 91.4%→90.8% since the prior measurement; no single new anomalous
entry appeared in the Gap/FilteredCandidate top lists tied to these
changes, and this session has repeatedly measured swings of this size on
an otherwise-unchanged binary (corpus-scan non-determinism), so this is
treated as noise rather than a proven regression pending further evidence.
@Hessesian
Hessesian force-pushed the fix/gap-scout-findings-batch1 branch from 83b6bf7 to aef3ef2 Compare September 14, 2026 11:25
@Hessesian
Hessesian merged commit 934e6fc into main Sep 14, 2026
4 checks passed
@Hessesian
Hessesian deleted the fix/gap-scout-findings-batch1 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