Skip to content

feat: semantic token validation for array callable method strings #187

Description

@calebdw

Summary

Array callable method-name strings like [SortableController::class, 'sort'] currently receive a method semantic token unconditionally, coloring them as methods even when the method name is misspelled (e.g. 'sortaaaa'). The string should only receive method coloring when the member actually exists on the resolved class.

Current behavior

[SortableController::class, 'sort']      // colored as method ✓
[SortableController::class, 'sortaaaa']  // also colored as method ✗ (should be string)

Both strings are colored blue (method token) because semantic_tokens.rs unconditionally emits TT_METHOD for every MemberAccess span. The resolve_member_modifiers stub (line ~502) always returns 0 without checking member existence.

Expected behavior

When the subject is a known class name (from a ::class constant), resolve the class via the class index and check if the member exists. If it doesn't, skip the semantic token so the text reverts to default string coloring.

For variable subjects ($this, $obj), full variable resolution is expensive — these could continue emitting unconditionally, or be resolved cheaply when $this refers to the enclosing class.

Implementation notes

An initial attempt at implementing this in resolve_member_modifiers showed correct behavior on file open (cold start) but stale coloring during editing. The root cause is the async update_ast timing: semantic tokens are requested before the symbol map is rebuilt, and no workspace/semanticTokens/refresh notification is sent after the async rebuild completes. Key areas:

  1. semantic_tokens.rs:272-296MemberAccess arm unconditionally returns (tt, mods)
  2. semantic_tokens.rs:502-515resolve_member_modifiers is a stub returning 0
  3. server.rs:592-637 — async did_change path: after update_ast completes, schedule_diagnostics is called but no client.semantic_tokens_refresh() is sent

Suggested approach

  • Implement resolve_member_modifiers to return Option<u32>None means member not found, skip the semantic token
  • For non-variable subjects: resolve via use map → namespace → bare name, then check member existence on the fully resolved class (including inheritance/traits)
  • Handle __call/__callStatic/__get magic methods as catch-alls
  • Add client.semantic_tokens_refresh() after async update_ast completes to fix stale tokens during editing
  • This would also benefit regular static calls like Foo::nonExistentMethod(), not just array callables

Related

This builds on the array-callable navigation work in commit 21a0105 and the array-callable completion work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions