You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/ARCHITECTURE.md
+4-4Lines changed: 4 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -321,7 +321,7 @@ Provider priority order (highest first):
321
321
322
322
Two providers are currently registered in `default_providers()`:
323
323
324
-
-**`LaravelModelProvider`** (`virtual_members/laravel.rs`): synthesizes virtual members for classes extending `Illuminate\Database\Eloquent\Model`. Produces relationship properties (methods returning `HasMany`, `HasOne`, `BelongsTo`, etc. generate a virtual property typed from the relationship's generic parameters), scope methods (`scopeActive` becomes `active()` as both static and instance), Builder-as-static forwarding (`User::where()->get()` resolves end-to-end), accessors (legacy `getXAttribute()` and modern `Attribute` casts), and cast properties (`$casts` array or `casts()` method entries are mapped to PHP types like `datetime` to `\Carbon\Carbon`, `boolean` to `bool`, custom cast classes to their `get()` return type). Highest priority among virtual member providers. Scope methods are also injected onto `Builder<Model>` instances via a post-generic-substitution hook in `type_hint_to_classes_depth` (see "Scope Methods on Builder Instances" below).
324
+
- **`LaravelModelProvider`** (`virtual_members/laravel.rs`): synthesizes virtual members for classes extending `Illuminate\Database\Eloquent\Model`. Produces relationship properties (methods returning `HasMany`, `HasOne`, `BelongsTo`, etc. generate a virtual property typed from the relationship's generic parameters), scope methods (both the `scopeActive` naming convention and the `#[Scope]` attribute from Laravel 11+ are supported; either style becomes `active()` as both static and instance), Builder-as-static forwarding (`User::where()->get()` resolves end-to-end), accessors (legacy `getXAttribute()` and modern `Attribute` casts), and cast properties (`$casts` array or `casts()` method entries are mapped to PHP types like `datetime` to `\Carbon\Carbon`, `boolean` to `bool`, custom cast classes to their `get()` return type). Highest priority among virtual member providers. Scope methods are also injected onto `Builder<Model>` instances via a post-generic-substitution hook in `type_hint_to_classes_depth` (see "Scope Methods on Builder Instances" below).
325
325
-**`PHPDocProvider`** (`virtual_members/phpdoc.rs`): parses `@method`, `@property`, `@property-read`, `@property-write`, and `@mixin` tags from the class-level docblock stored in `ClassInfo.class_docblock`. Explicit `@method` / `@property` tags are not parsed eagerly during AST extraction; instead, the raw docblock string is preserved and parsed lazily when `provide` is called. For `@mixin` tags, the provider loads the referenced classes and merges their public members. Within the provider, explicit tags take precedence over mixin members. Recurses into mixin-of-mixin chains up to `MAX_MIXIN_DEPTH`.
326
326
327
327
### Precedence Rules
@@ -334,15 +334,15 @@ Two providers are currently registered in `default_providers()`:
334
334
335
335
### Scope Methods on Builder Instances
336
336
337
-
Scope methods are synthesized on the Model class by `LaravelModelProvider`, but they also need to be available on `Builder<Model>` instances (e.g. `Brand::where('id', $id)->isActive()`). This cannot be handled by a virtual member provider alone because providers run before generic argument substitution, so the provider would not know the concrete model type.
337
+
Scope methods (both `scopeX` convention and `#[Scope]` attribute) are synthesized on the Model class by `LaravelModelProvider`, but they also need to be available on `Builder<Model>` instances (e.g. `Brand::where('id', $id)->isActive()`). This cannot be handled by a virtual member provider alone because providers run before generic argument substitution, so the provider would not know the concrete model type.
338
338
339
339
Instead, scope injection happens in three places:
340
340
341
341
1.**Post-generic-substitution hook** (`completion/resolver.rs`, inside `type_hint_to_classes_depth`): after `resolve_class_fully` + `apply_generic_args` produces a `Builder<User>` class, the resolver detects that the result is an Eloquent Builder with a concrete model generic argument. It calls `build_scope_methods_for_builder(model_name, class_loader)` which loads the model, fully resolves it, extracts scope methods, and returns them as instance methods with `static` in return types substituted to the concrete model name. These are merged onto the Builder's method list, giving `$q->active()` after `$q = User::where(...)`.
342
342
343
-
2.**Scope body Builder enrichment** (`completion/variable_resolution.rs`, `enrich_builder_type_in_scope`): inside a scope method body, the `$query` parameter is typically typed as `Builder` without generic arguments. The enrichment function detects when the enclosing method is a scope (name starts with `scope`, length > 5) on a class that extends Eloquent Model, and the parameter type is `Builder` without generics. It rewrites the type to `Builder<EnclosingModel>`, which then flows through the generic-args path and triggers the post-substitution hook above. This makes `$query->otherScope()` resolve inside scope bodies.
343
+
2.**Scope body Builder enrichment** (`completion/variable_resolution.rs`, `enrich_builder_type_in_scope`): inside a scope method body, the `$query` parameter is typically typed as `Builder` without generic arguments. The enrichment function detects when the enclosing method is a scope (either the `scopeX` naming convention or the `#[Scope]` attribute) on a class that extends Eloquent Model, and the parameter type is `Builder` without generics. It rewrites the type to `Builder<EnclosingModel>`, which then flows through the generic-args path and triggers the post-substitution hook above. This makes `$query->otherScope()` resolve inside scope bodies.
344
344
345
-
3.**Go-to-definition fallback** (`definition/member.rs`, `find_scope_on_builder_model`): when go-to-definition resolves a member on a Builder class and the normal lookup chain (own members, traits, parents, mixins, builder forwarding) fails, this fallback checks whether the member is a scope method injected from the model. It confirms the resolved candidate (with injected scopes) has the method, extracts the model name from the scope method's return type generic argument, loads the model, and finds the `scopeXxx`declaration through the model's inheritance chain. This bridges the gap between completion (which works on the merged ClassInfo) and GTD (which traces back to the declaring source file).
345
+
3.**Go-to-definition fallback** (`definition/member.rs`, `find_scope_on_builder_model`): when go-to-definition resolves a member on a Builder class and the normal lookup chain (own members, traits, parents, mixins, builder forwarding) fails, this fallback checks whether the member is a scope method injected from the model. It confirms the resolved candidate (with injected scopes) has the method, extracts the model name from the scope method's return type generic argument, loads the model, and finds the declaration through the model's inheritance chain. For `scopeX`-style scopes it looks for `scopeXxx`; for `#[Scope]`-attributed methods it falls back to the original method name. This bridges the gap between completion (which works on the merged ClassInfo) and GTD (which traces back to the declaring source file).
346
346
347
347
### Interface Inheritance in Traits/Used Interfaces
Copy file name to clipboardExpand all lines: docs/CHANGELOG.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
22
22
-**Eloquent `$attributes` default properties.** Entries in a model's `$attributes` property array now produce typed virtual properties as a fallback. Types are inferred from the literal default values: strings, booleans, integers, floats, `null`, and arrays. Columns that already have a `$casts` entry are skipped, so casts always take priority. Works with `$this->`, instance variables, cross-file PSR-4 resolution, double-quoted keys, and negative numeric literals.
23
23
-**Virtual member provider abstraction.** Introduced the `VirtualMemberProvider` trait and `VirtualMembers` struct in a new `virtual_members` module. This provides a priority-ordered pipeline for synthesizing members from `@method`/`@property` tags, `@mixin` classes, and framework-specific patterns (e.g. Laravel relationships, scopes, Builder forwarding). All completion and go-to-definition call sites now use the new `resolve_class_fully` entry point, which applies base inheritance resolution followed by virtual member providers. No providers are registered yet, so behavior is unchanged. This is the foundation for upcoming Laravel support.
24
24
- **Laravel relationship properties.** Classes extending `Illuminate\Database\Eloquent\Model` now get virtual properties synthesized from relationship methods. A method returning `HasMany<Post, $this>` produces a `$posts` property typed as `\Illuminate\Database\Eloquent\Collection<Post>`, and `HasOne<Profile, $this>` produces a `$profile` property typed as `Profile`. Supports `HasOne`, `HasMany`, `BelongsTo`, `BelongsToMany`, `MorphOne`, `MorphMany`, `MorphTo`, `MorphToMany`, and `HasManyThrough`. Generic type parameters are extracted from Larastan-style `@return` annotations. The synthesized properties sit at the highest virtual member priority, beating `@property` tags from ide-helper and `@mixin` members. Works with `$this->`, instance variables, relationship methods defined in traits, indirect Model subclasses (through a BaseModel), fully-qualified return types, and cross-file PSR-4 resolution. Chaining through relationship properties (e.g. `$user->profile->getBio()`) resolves to the related model's members.
25
-
-**Laravel scope methods.** Methods starting with `scope` on Eloquent model classes (e.g. `scopeActive`, `scopeOfType`) now produce virtual methods with the prefix stripped and the first letter lowercased (`active`, `ofType`). The first `$query` parameter is removed since Laravel injects it automatically. Scope methods are available as both static and instance methods, so `User::active()` and `$user->active()` both resolve. If the scope method returns `void` or has no return type, the synthesized method defaults to `\Illuminate\Database\Eloquent\Builder<static>`. Custom return types and extra parameters beyond `$query` are preserved.
25
+
-**Laravel scope methods.** Methods starting with `scope` on Eloquent model classes (e.g. `scopeActive`, `scopeOfType`) now produce virtual methods with the prefix stripped and the first letter lowercased (`active`, `ofType`). The first `$query` parameter is removed since Laravel injects it automatically. Scope methods are available as both static and instance methods, so `User::active()` and `$user->active()` both resolve. If the scope method returns `void` or has no return type, the synthesized method defaults to `\Illuminate\Database\Eloquent\Builder<static>`. Custom return types and extra parameters beyond `$query` are preserved. The `#[Scope]` attribute (Laravel 11+) is also supported as an alternative to the `scopeX` naming convention: methods decorated with `#[\Illuminate\Database\Eloquent\Attributes\Scope]` use their own name directly as the public-facing scope name. Both styles work on model classes, on Builder instances, inside scope method bodies, in traits, and with go-to-definition.
26
26
-**Scope methods on Builder instances.** Eloquent scope methods now resolve on Builder instances, not just on the Model class itself. `Brand::where('id', $id)->isActive()` and `$query->active()->ofGenre('fiction')->get()` now offer scope completions and go-to-definition end-to-end. When the Builder's `TModel` template parameter is a concrete model (e.g. `Builder<User>`), the model's scope methods are injected as instance methods on the Builder. Go-to-definition on a scope called through a Builder (e.g. `$q->isActive()`) jumps to the `scopeIsActive` method on the model class. Scope return types are mapped so that chaining continues on the Builder. Inside scope method bodies, `$query->otherScope()` also resolves for both completion and go-to-definition: the `Builder` parameter type is automatically enriched with the enclosing model's generic argument. Works with scopes defined in traits, inherited from parent models, across files via PSR-4, and through multi-step builder chains.
27
27
-**Laravel Builder-as-static forwarding.** Static method calls on Eloquent model classes are forwarded to the Eloquent Builder. `User::where('email', $e)->orderBy('name')->get()` now resolves end-to-end. The provider loads `\Illuminate\Database\Eloquent\Builder`, fully resolves it (including `@mixin` members from `Query\Builder`), and presents its public instance methods as static virtual methods on the model. Return types are mapped so that `static`/`$this`/`self` on Builder resolve to `Builder<ConcreteModel>` (the chain continues on the builder), and template parameters like `TModel` resolve to the concrete model class. Methods like `get()` return `Collection<User>`, `first()` returns `User|null`, and chain methods like `where()` and `orderBy()` return `Builder<User>` for continued chaining. Works with indirect model subclasses, coexists with scope methods and relationship properties, and includes Query Builder methods forwarded via `@mixin`.
28
28
-**Union completion sorting.** When a variable has a union type (e.g. `Dog|Cat` from a match or ternary), members shared by all types in the union (the intersection) now sort above members that only exist on a subset of types. Branch-only members display their originating class name as a label detail suffix, giving an at-a-glance visual hint in the completion popup. No completions are removed. Single-type completions are unaffected.
0 commit comments