fix(laravel): index commands registered via withCommands() and their aliases - #323
Open
krist7599555 wants to merge 3 commits into
Open
fix(laravel): index commands registered via withCommands() and their aliases#323krist7599555 wants to merge 3 commits into
krist7599555 wants to merge 3 commits into
Conversation
The Artisan command index only treated classes whose short name ends in
`Command` or files under `Console/Commands/` as candidates, so commands
registered from arbitrary directories (e.g. `withCommands()` in
bootstrap/app.php pointing at app/Actions/Sync) were never indexed. The
LSP then reported `$this->call('sync:forma-projects')` as "Unknown
command" even though `php artisan list` registers it.
Three gaps closed:
- Candidate discovery now also scans every non-vendor project class,
letting scan_command_file's extends-Command / attribute checks decide.
- The byte pre-filter (build + incremental refresh paths) now recognizes
the Laravel 11+ `#[Signature]` attribute, which previously slipped
past the lowercase `signature` token check.
- command_from_class and command_signature_at_offset now read the
`#[Signature('...')]` attribute as the command name / signature, so
commands declared the modern way are indexed and their options validate.
The analyze (CLI) path already built the index (added in 6dc9a7a), so
this makes phpantom_lsp analyze and the LSP validate command names
against the same, complete command set.
…butes
Commands can answer to alternative names declared via #[Aliases([...])],
the aliases: argument of #[Signature] / #[AsCommand], or Symfony's
#[AsCommand(aliases: ...)]. phpantom ignored all of them, so calling a
valid alias (->call('cache:clean') for a cache:clear command) was
reported as "Unknown command" — the same false-positive class as the
withCommands() discovery gap.
- CommandEntry gains an aliases: Vec<String> field; LaravelCommandIndex
indexes each alias alongside the primary name (get()/all_names()
resolve aliases), so the unknown-command diagnostic accepts them.
- command_aliases() recovers aliases from #[Aliases([...])] (which wins,
mirroring configureFromAttributes) and the aliases: named/positional
argument of #[Signature] / #[AsCommand].
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
krist7599555
force-pushed
the
fix/laravel-command-index-discovery
branch
from
August 5, 2026 10:35
e07dcd7 to
b86ae52
Compare
krist7599555
force-pushed
the
fix/laravel-command-index-discovery
branch
from
August 5, 2026 10:46
b86ae52 to
66af47b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Caution
This is vibe code fix
How this was discovered
Found in a Laravel 13 project whose Artisan commands live in
app/Actions/Sync/, registered viawithCommands()inbootstrap/app.phpand declared with the Laravel 11#[Signature]attribute.The LSP flagged
$this->call('sync:forma-projects')as "Unknown command: 'sync:forma-projects'", while at the same time:php artisan listregistered the command, andphpantom_lsp analyze(viacomposer analyze) reported zero findings.So the CLI and the LSP disagreed about the same command set. Digging into the source showed why:
analyzealso runscollect_fast_diagnostics, but the command-name check is gated on a non-empty index (if command_names.is_empty() { continue; }), and the headless analyze path never built the index — so the check was skipped entirely and "No errors" was not actually validation.*Command-named classes and files underConsole/Commands/, and name recovery didn't understand the#[Signature]attribute — so the command was genuinely missing from the index and the warning was a false positive.The analyze-side index gap has since been addressed upstream (6dc9a7a); the discovery and attribute gaps remained and are what this PR fixes.
Summary
The Artisan command index only treated classes whose short name ends in
Commandor files underConsole/Commands/as candidates, and recovered command names only from$signature,$name, and#[AsCommand]. Laravel 11+ registers commands differently:withCommands()inbootstrap/app.phploads commands from arbitrary directories (e.g.app/Actions/Sync) whose classes use the#[Signature('name {--opt}')]attribute — none of which the index recognizes.#[Aliases([...])], thealiases:argument of#[Signature]/#[AsCommand], or Symfony's#[AsCommand(aliases: ...)]— none of which are indexed.Both gaps produce the same false positive:
$this->call('sync:forma-projects')(orArtisan::call('<alias>')) is reported as "Unknown command" by the LSP even thoughphp artisan listregisters it.Fix
1. Complete candidate discovery (
server.rs—build_laravel_command_index+refresh_laravel_command_index)Candidates are now: short name ends in
Commandor underConsole/Commands/or any non-vendor project class —scan_command_file's extends-Command/attribute checks decide. The byte pre-filter (signature/Signature/AsCommand/$name) keeps this cheap.2. Recover the name from
#[Signature](commands.rs)signature_attribute_value()helper — the Laravel 11+ attribute form of$signature— used incommand_from_class(after#[AsCommand], before the$signatureproperty) andcommand_signature_at_offset(so$this->option('...')validates for attribute-declared commands).3. Index command aliases (
commands.rs)CommandEntrygainsaliases: Vec<String>;LaravelCommandIndexindexes each alias alongside the primary name (get()/all_names()resolve aliases).command_aliases()recovers aliases from#[Aliases([...])](which wins, mirroringIlluminate\Console\Command::configureFromAttributes) and thealiases:argument of#[Signature]/#[AsCommand](named or positional).Tests
Five unit tests in
commands_tests.rs:scans_signature_attribute_command_class/resolves_enclosing_signature_from_attribute—#[Signature]name + option recovery.indexes_standalone_aliases_attribute/indexes_signature_and_as_command_aliases/index_resolves_alias_names— alias recovery from all three attribute sources, plus index lookup via alias.Verification
withCommands([... app/Actions/Sync]):$this->call('sync:does-not-exist')→ correctly reportedUnknown command;composer analyzestays at zero findings, and now genuinely validates command names.Notes for maintainers
analyze(CLI) path already builds the index (added in6dc9a7a1), so this change makes both modes agree — no CLI-side change needed here.laravel/framework13.x(v13.24.0):Aliases,Description,Help,Hidden,Signature,Usage—Description/Help/Hidden/Usagedon't affect name resolution, so only the aliases family was addressed.