Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ The symbol map also stores:
- **Variable definition sites** (`var_defs`): records every assignment, parameter, foreach binding, catch variable, static/global declaration, and destructuring site with its byte offset, scope boundary, and `effective_from` offset (for assignments, this is the end of the statement so the RHS sees the previous definition). Go-to-definition for `$var` finds the most recent definition before the cursor within the enclosing scope via binary search.
- **Scope boundaries** (`scopes`): function, method, closure, and arrow function body ranges. Used by `find_enclosing_scope` to determine which scope the cursor is in.
- **Template parameter definitions** (`template_defs`): `@template` tag locations so that template parameter names (e.g. `TKey`, `TModel`) that appear in docblock types can be resolved to their declaration site.
- **Candidate render sites** (`view_receiver_sites`): the view names a method call spells when only the receiver's *type* decides whether it renders — a constructor-injected `Factory $views` behind `$this->views->make('page')`, a mailable held in a local. Extraction runs before the file's classes are resolved and cannot type the receiver, so it records the candidates and `blade/typed_receiver.rs` confirms them lazily through the shared type engine, once per file. Consumers of view keys (the call-site diagnostics, call-site inference, `lookup_symbol_map`, find-references) read the confirmed spans alongside the map's own `LaravelStringKey` spans. The reference candidate index takes the *unconfirmed* candidates, since a file has to be findable before it can be asked.
- **Type-dependent Laravel string sites** (`view_receiver_sites`, `resource_receiver_sites`): a method or property can name a view, database/queue/broadcast connection, or queue only after its receiver or enclosing class is typed. Extraction runs before the file's classes are resolved, so it records compact candidates and `blade/typed_receiver.rs` confirms them lazily through the shared type engine, once per symbol-map generation. Consumers read those confirmed spans beside the map's direct `LaravelStringKey` spans. The reference index stores one coarse entry for each unconfirmed resource candidate, so a file remains findable without indexing the same site as every family it might become.
- **Source-defined Laravel names** (`laravel_source_strings`): literal rate-limiter registrations, runtime config-resource definitions, and confirmed queue names are indexed incrementally per URI. Replacing or deleting a file removes only that file's contribution; completion and diagnostics query the sorted index instead of rescanning the workspace.

### Tier 2: Stored Byte Offsets (cross-file jumps)

Expand Down
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Laravel named resources are editor-aware.** Configured service names now complete in calls, typed methods, container attributes, and middleware; hover identifies the resource family, Ctrl+Click opens the declaration, references bridge direct config access, and misspellings are diagnosed. Source-defined rate limiters and free-form queue names get the same editor support without treating open-ended names as errors. Contributed by @shuvroroy (#368).
- **Storage disk names are navigable wherever Laravel accepts one.** `Storage::disk()`, `fake()`, `persistentFake()`, `forgetDisk()`, and the `#[Storage]` container attribute now complete from `config/filesystems.php`; hover identifies the backing entry, Ctrl+Click opens it, and find-references links every use. Array values passed to `forgetDisk()` work individually. Calls that require a configured disk report misspellings, while test fakes and cache eviction keep accepting the ad-hoc names Laravel allows at runtime. Contributed by @shuvroroy (#368).
- **A standalone function shows how many times it is used.** The reference count that sits above a class, method, property, and constant was missing from every function declared outside a class, and a file holding nothing but functions (a helpers file, most of a procedural codebase) got no counts at all, because the walk that draws them starts at the file's classes. Functions are counted now too, and Ctrl+Click on a function's own name at its declaration offers the list of its usages instead of doing nothing, which is how the same click already behaved on a class or a method. Contributed by @petrovo-as.
- **PHPantom can run in the browser.** The whole type engine now compiles to WebAssembly, so a web editor can have PHPantom's completion, hover, go-to-definition, symbol highlighting and rename without a server to talk to and without a round-trip per keystroke. The module speaks ordinary LSP JSON-RPC over four exported functions, so a browser LSP client can be pointed at it through a thin transport, and it needs no filesystem: the PHP standard library stubs are compiled in and open documents live in memory. This is what the [PHPStan playground](https://phpstan.org/try) is built on. Every release ships a prebuilt module, so a host can pin a version rather than build its own. See [wasm.md](wasm.md) for the host interface. Contributed by @ondrejmirtes.
- **A path helper opens the file it names.** `base_path('routes/web.php')`, `app_path()`, `config_path()`, `database_path()`, `lang_path()`, `public_path()`, `resource_path()`, and `storage_path()` each anchor their argument to a conventional directory, but the argument was still just a string: no link, no completion, and a typo showed up only at runtime. The argument is a clickable link now and go-to-definition follows it, and typing one completes a segment at a time from the directory the path has reached so far, directories first so the next segment follows on. `lang_path()` respects the `resources/lang` directory an application upgraded from Laravel 8 still has. A directory completes but is not a link, since an editor cannot open a folder as a document. Contributed by @shuvroroy (#334).
Expand Down Expand Up @@ -126,6 +128,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Laravel discovery handles macOS path aliases consistently.** Temporary and workspace paths may be exposed as `/var` while their canonical spelling is `/private/var`; vendor pruning and Blade view refreshes now treat those spellings as the same location, so vendor files stay excluded and newly created or deleted templates refresh completion correctly. Contributed by @shuvroroy (#368).
- **Editing a service provider takes effect immediately.** What a Laravel service provider registers was read once, when the project was first indexed, and never again. A container binding written afterwards did not resolve, hover, or navigate until the editor was restarted, and the same went for the view directories, translation directories, route files, config files, and Blade component namespaces a provider registers. Saving or editing a provider now re-reads it, and adding one to `bootstrap/providers.php` (or `config/app.php`) picks it up as well. A key that two providers bind still ends up with whichever of them the container itself would let win.
- **A request accessor written with named arguments keeps its key.** `$request->file(key: 'photos')` and `$request->header(default: 'x')` read the named argument as whichever positional slot it happened to land in, so a keyed `file()` call resolved as though it named no field at all and a default-only `header()` call resolved as though its default text were the key. `header()`, `query()`, `cookie()`, `input()`, `post()`, and `file()` now bind a named argument to the parameter it actually names, including on an app's own `FormRequest` subclass, which never redeclares the accessor itself.
- **Blade partial variables stay typed during CLI analysis.** `phpantom_lsp analyze` now discovers view and include callers while running without the editor's reference index, and direct variables passed in template data retain nearby `@var` overrides. Contributed by @shuvroroy (#337).
Expand Down
2 changes: 0 additions & 2 deletions docs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,8 @@ unlikely to move the needle for most users.
| L24 | [Translation depth: JSON lang files, locales, placeholders](todo/laravel.md#l24-translation-depth-json-lang-files-locales-placeholders) | Medium-High | Medium-High |
| L46 | [`->can()` on a user model the receiver does not name](todo/laravel.md#l46-can-on-a-user-model-the-receiver-does-not-name) | Medium-High | Medium-High |
| L30 | [Eloquent attribute-array key completion](todo/laravel.md#l30-eloquent-attribute-array-key-completion) | Medium | Medium |
| L32 | [Config-backed named-resource strings](todo/laravel.md#l32-config-backed-named-resource-strings) (log channels, cache stores, guards, connections, rate limiters) | Medium | Medium |
| L49 | [Unguarded Eloquent mass assignment diagnostic](todo/laravel.md#l49-unguarded-eloquent-mass-assignment-diagnostic) | Medium | Medium |
| L17 | [Additional string contexts without booting](todo/laravel.md#l17-additional-string-contexts-without-booting) (middleware, assets, validation, Inertia) | Medium | Medium-High |
| L25 | [Storage disk name strings](todo/laravel.md#l25-storage-disk-name-strings) | Low-Medium | Low |
| L31 | [String-key rename, highlight, and semantic tokens](todo/laravel.md#l31-string-key-rename-highlight-and-semantic-tokens) | Low-Medium | Medium |
| L42 | [Morph alias completion in array positions](todo/laravel.md#l42-morph-alias-completion-in-array-positions) | Low-Medium | Medium |
| L3 | `$dates` array (deprecated) | Low-Medium | Medium |
Expand Down
49 changes: 0 additions & 49 deletions docs/todo/laravel.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,18 +582,6 @@ still partially lack:
empty string as the value). No fix when the group file itself doesn't
exist yet; that case still just diagnoses.

#### L25. Storage disk name strings

**Impact: Low-Medium · Complexity: Low**

`Storage::disk('...')` and the `#[Storage]` container attribute already
complete against `filesystems.disks.*`, navigate to the disk's entry in
`config/filesystems.php`, and flag an unknown disk. `Storage::fake()`,
`persistentFake()`, and `forgetDisk()` still name a disk with none of
that: their return type is patched to `FilesystemAdapter`, but the
disk-name argument itself gets no completion, go-to-definition, or
diagnostic.

#### L27. Legacy `Controller@method` action strings

**Impact: Low · Complexity: Low**
Expand Down Expand Up @@ -650,43 +638,6 @@ updates the `->name()` declaration and all usages; highlight and
semantic tokens reuse the existing spans. Renaming a view name implies
moving the Blade file — defer that one until the rest is in place.

#### L32. Config-backed named-resource strings

**Impact: Medium · Complexity: Medium**

L25 (storage disks) is one instance of a general pattern: a method
argument names an entry under a known config subtree, and the config
scanner already parses those files. Auth guards (`auth('...')`,
`Auth::guard()`, `->middleware('auth:web')`), cache stores
(`Cache::store()`), log channels (`Log::channel()`), and storage disks
(L25) already complete against their config subtree — but all of them
route through the generic `LaravelStringKind::Config` kind rather than
a dedicated one, so they get completion plus the shared config
diagnostics/go-to-definition and nothing family-specific (a "cache
store" hovers with the same generic wording as any other config key).
`Log::stack()` (array values) isn't recognized at all. Generalize into
a declarative table of `(trigger context, config path)` pairs so each
new family is one table row, and cover the rest of the family in one
pass:

- **Database connections** — `DB::connection()`, `->connection()` /
`$connection` on models and jobs → `database.connections.*`.
- **Queue connections and queues** — `Queue::connection()`,
`->onConnection()` → `queue.connections.*`; `->onQueue()` names are
free-form (completion from literals seen elsewhere, no diagnostic).
- **Mailers** — `Mail::mailer()` → `mail.mailers.*`.
- **Broadcast connections** — `Broadcast::connection()` →
`broadcasting.connections.*`.
- **Rate limiter names** — not config-backed: registered via
`RateLimiter::for('name', …)` in providers. Scan literal
registrations (same shape as the macro scanner) and validate
`throttle:name` middleware parameters and `new RateLimited('name')`
against the set.

Each family gets the full string-kind treatment for free once wired
as a `LaravelStringKey`: completion, go-to-definition (jump to the
config entry), hover, diagnostics, and references.

#### L39. Unused view and translation key detection

**Impact: Low · Complexity: Medium**
Expand Down
79 changes: 79 additions & 0 deletions examples/laravel/app/Demo.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,36 @@
use Database\Factories\AnnotatedPostFactory;
use Database\Factories\BlogAuthorFactory;
use Database\Factories\EditorialFactory;
use Illuminate\Container\Attributes\Auth as InjectAuth;
use Illuminate\Container\Attributes\Authenticated as InjectAuthenticated;
use Illuminate\Container\Attributes\Cache as InjectCache;
use Illuminate\Container\Attributes\Database as InjectDatabase;
use Illuminate\Container\Attributes\Log as InjectLog;
use Illuminate\Container\Attributes\Storage as InjectStorage;
use Illuminate\Contracts\Broadcasting\Factory as BroadcastFactory;
use Illuminate\Contracts\Queue\Factory as QueueFactory;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\Request;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Queue\Middleware\RateLimited;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Schedule;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\View;
Expand Down Expand Up @@ -834,6 +851,65 @@ public function laravelConfig(): void
}


// ── Config-backed named resources and source-defined names ─────────

public function injectedNamedResources(
#[InjectAuth(guard: 'admin')] mixed $guard,
#[InjectAuthenticated(guard: 'admin')] mixed $user,
#[InjectCache(memo: true, store: 'memory')] mixed $cache,
#[InjectLog(channel: 'daily')] mixed $logger,
#[InjectStorage(disk: 'pantry')] mixed $disk,
#[InjectDatabase(connection: 'mysql')] mixed $database,
): void
{
// Contextual-attribute arguments complete and navigate against the
// same family-specific config entries as their facade counterparts.
}

public function namedLaravelResources(): void
{
// Each string completes from its own config subtree. Hover names the
// resource family, Ctrl+Click opens that entry, and references include
// both resource calls and direct config() access to the same entry.
auth('admin');
Auth::guard('admin');
Cache::store('memory');
Log::channel('daily');
Log::stack(['daily', 'stderr']);
Storage::disk('pantry');
DB::connection('mysql');
Queue::connection('redis');
Mail::mailer('transactional');
Broadcast::connection('internal');
config('cache.stores.memory');

// Receiver types disambiguate connection strings. Queue names remain
// free-form, so PHPantom learns them from literals without diagnosing
// a name it has not seen before.
(new BroadcastMessage([]))
->onConnection('redis')
->onQueue('mail');

// The limiter is registered in DemoServiceProvider. Its name resolves
// from both middleware parameters and queue middleware constructors.
Route::middleware(['auth:admin', 'throttle:uploads']);
new RateLimited('uploads');
}

public function typedNamedResourceConnections(
ConnectionResolverInterface $database,
QueueFactory $queues,
BroadcastFactory $broadcasts,
): void
{
// All three contracts expose connection(), so the receiver type picks
// the database, queue, or broadcasting config subtree respectively.
$database->connection('mysql');
$queues->connection('redis');
$broadcasts->connection('internal');
}


// ── Cache::remember() — closure return type binding ─────────────────

public function cacheRemember(): void
Expand Down Expand Up @@ -1089,8 +1165,11 @@ public function storageFake(): void
{
// fake() declares the Filesystem contract but always builds a
// FilesystemAdapter, so the adapter-only assertion helpers resolve.
// Disk names complete from config/filesystems.php and navigate back
// to their entries anywhere the Storage facade accepts one.
Storage::fake('avatars')->assertExists('me.png');
Storage::persistentFake('logs')->assertMissing('old.log');
Storage::forgetDisk(['avatars', 'logs']);
}


Expand Down
7 changes: 7 additions & 0 deletions examples/laravel/app/Providers/DemoServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
use App\Support\PlainOven;
use App\View\Composers\SidebarComposer;
use Carbon\CarbonImmutable;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\View;
use Illuminate\View\View as ViewInstance;
Expand Down Expand Up @@ -93,6 +95,11 @@ public function boot(): void
// those of an App\Policies\BlogPostPolicy that does not exist.
Gate::policy(BlogPost::class, PublishingPolicy::class);

// Named limiters are source-defined rather than config-backed.
// PHPantom indexes this registration so the name completes and
// navigates from route or queue middleware.
RateLimiter::for('uploads', fn () => Limit::perMinute(60));

// A macro registered here becomes a real method on Collection:
// it autocompletes, hovers with this signature, and type-checks.
Collection::macro('sumField', function (string $field): float {
Expand Down
11 changes: 11 additions & 0 deletions examples/laravel/config/broadcasting.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

return [
'default' => 'internal',

'connections' => [
'internal' => [
'driver' => 'log',
],
],
];
11 changes: 11 additions & 0 deletions examples/laravel/config/cache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

return [
'default' => 'memory',

'stores' => [
'memory' => [
'driver' => 'array',
],
],
];
15 changes: 15 additions & 0 deletions examples/laravel/config/filesystems.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@

'disks' => [

'avatars' => [
'driver' => 'local',
'root' => 'storage/app/avatars',
],

'logs' => [
'driver' => 'local',
'root' => 'storage/app/logs',
],

's3' => [
'driver' => 's3',
'bucket' => 'demo',
],

// A disk whose driver the framework does not ship. It is built by the
// `Storage::extend('pantry', ...)` registration in DemoServiceProvider,
// and PHPantom reads that closure's return type rather than giving up
Expand Down
Loading
Loading