Skip to content

Commit 1bdccb2

Browse files
committed
Maps Eloquent magic properties
1 parent 9a4e2ae commit 1bdccb2

12 files changed

Lines changed: 4857 additions & 244 deletions

File tree

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ Provider priority order (highest first):
259259

260260
Two providers are currently registered in `default_providers()`:
261261

262-
- **`LaravelModelProvider`** (`virtual_members/laravel.rs`): synthesizes virtual members for classes extending `Illuminate\Database\Eloquent\Model`. Currently produces relationship properties: methods returning known relationship types (`HasMany`, `HasOne`, `BelongsTo`, etc.) generate a virtual property with the same name, typed from the relationship's generic parameters (e.g. `HasMany<Post, $this>` produces a `$posts` property typed as `\Illuminate\Database\Eloquent\Collection<Post>`). Highest priority among virtual member providers.
262+
- **`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.
263263
- **`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`.
264264

265265
### Precedence Rules

docs/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- **`newCollection()` override detection.** Eloquent models that override the `newCollection()` method now resolve to the custom collection class declared in the method's return type. This is the third detection mechanism alongside `#[CollectedBy]` and `@use HasCollection<X>`. Priority order: attribute, trait, method override. Works with short names resolved via `use` imports and fully-qualified return types. The standard `Collection` return type is correctly ignored. Custom collection methods appear after `->get()`, on relationship properties, and in builder chains.
1313
- **Body-inferred relationship properties.** Eloquent relationship methods that lack `@return` annotations now produce virtual properties by scanning the method body for patterns like `$this->hasMany(Post::class)`. The relationship type is inferred from the method name (`hasMany` to `HasMany`, `belongsTo` to `BelongsTo`, etc.) and the related model class is extracted from the first `::class` argument. Supports all 10 relationship types including `HasOneThrough`. Fully-qualified class names, extra foreign key arguments, and chained builder calls (e.g. `->latest()`) are handled. When both a `@return` annotation and a body pattern are present, the annotation takes priority. Projects that don't use Larastan no longer need to add annotations for basic relationship completion.
1414
- **Laravel accessor and mutator virtual properties.** Eloquent models with legacy accessors (`getFullNameAttribute()`) or modern Laravel 9+ accessors (methods returning `Illuminate\Database\Eloquent\Casts\Attribute`) now produce virtual properties. The property name is derived by converting the method name to snake_case: `getFullNameAttribute` produces `$full_name`, and `avatarUrl()` produces `$avatar_url`. Legacy accessors use the method's declared return type; modern accessors use `mixed`. Works alongside relationship properties, scope methods, and builder forwarding. `getAttribute()` (a real Eloquent method) is correctly excluded.
15+
- **Eloquent cast properties.** Properties defined in a model's `$casts` array or `casts()` method now produce typed virtual properties. Cast type strings are mapped to PHP types: `datetime`/`date` to `\Carbon\Carbon`, `immutable_datetime`/`immutable_date` to `\Carbon\CarbonImmutable`, `boolean`/`bool` to `bool`, `integer`/`int` to `int`, `float`/`double`/`real`/`decimal:N` to `float`, `string`/`encrypted`/`hashed` to `string`, `array`/`json` to `array`, `object` to `object`, `collection` to `\Illuminate\Support\Collection`. Enum casts (e.g. `Status::class`) resolve to the enum class itself. Classes implementing `Illuminate\Contracts\Database\Eloquent\Castable` also resolve to themselves. Custom cast classes are resolved by loading the class and inspecting the `get()` method's return type (native or `@return` docblock). When the `get()` method has no return type, the resolver falls back to the first generic argument from `@implements CastsAttributes<TGet, TSet>` on the cast class. A `:argument` suffix (e.g. `Address::class.':nullable'`) is stripped before resolution. Format suffixes (e.g. `datetime:Y-m-d`) are handled. Both sources are merged: the `casts()` method overrides the `$casts` property for overlapping columns, matching Laravel's runtime behaviour. Works with `$this->`, instance variables, cross-file PSR-4 resolution, indirect model subclasses, double-quoted strings, and coexists with relationship properties, scope methods, and accessors.
16+
- **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.
1517

1618
- **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.
1719
- **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.

docs/todo-laravel.md

Lines changed: 5 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -8,58 +8,6 @@ virtual member provider design, see `ARCHITECTURE.md`.
88

99
---
1010

11-
## Current state
12-
13-
The `LaravelModelProvider` in `src/virtual_members/laravel.rs` is the
14-
highest-priority virtual member provider. It synthesizes virtual members
15-
for classes that extend `Illuminate\Database\Eloquent\Model`:
16-
17-
1. **Relationship properties.** All 10 relationship types (`HasOne`,
18-
`HasMany`, `BelongsTo`, `BelongsToMany`, `MorphOne`, `MorphMany`,
19-
`MorphTo`, `MorphToMany`, `HasManyThrough`, `HasOneThrough`).
20-
Supports Larastan-style `@return HasMany<Post, $this>` annotations
21-
and body-inferred relationships: when no `@return` annotation is
22-
present, the method body is scanned for patterns like
23-
`$this->hasMany(Post::class)` to infer the relationship type
24-
automatically. Chaining through relationship properties resolves
25-
end-to-end. Collection-type relationship properties use the
26-
*related* model's custom collection (not the owning model's).
27-
28-
2. **Scope methods.** `scopeActive(Builder $query)` produces `active()`
29-
as both static and instance virtual methods. The `$query` parameter is
30-
stripped, extra parameters are preserved, return types default to
31-
`Builder<static>` when absent or `void`.
32-
33-
3. **Builder-as-static forwarding.** `User::where('active', true)->
34-
orderBy('name')->get()` resolves the full chain. Template parameters
35-
(`TModel`) are substituted to the concrete model class.
36-
`Query\Builder` methods are included via `@mixin`. `BuildsQueries`
37-
trait methods (`first()`, `firstOrFail()`, `sole()`) work through
38-
`@use` generics.
39-
40-
4. **Custom Eloquent collections.** Three detection mechanisms:
41-
`#[CollectedBy]`, `@use HasCollection<X>`, and `newCollection()`
42-
method override. Custom collection methods appear after `->get()`,
43-
after static `Model::get()`, and on collection-type relationship
44-
properties. Priority order: attribute > trait > method override.
45-
46-
5. **Go-to-definition.** Jumps to `Builder::where()`,
47-
`Query\Builder::orderBy()`, `BuildsQueries::first()`, and scope
48-
methods all work through `find_builder_forwarded_method`.
49-
50-
6. **Accessors and mutators.** Legacy accessors (`getFullNameAttribute()`)
51-
and modern Laravel 9+ accessors (methods returning
52-
`Illuminate\Database\Eloquent\Casts\Attribute`) produce virtual
53-
properties. The property name is derived by converting the method
54-
name portion to snake_case (`getFullNameAttribute``full_name`,
55-
`avatarUrl()``avatar_url`). Legacy accessors use the method's
56-
return type; modern accessors use `mixed`.
57-
58-
Test coverage: 154 unit tests in `laravel.rs`, 85 integration tests in
59-
`completion_laravel.rs`, 15 integration tests in `definition_laravel.rs`.
60-
61-
---
62-
6311
## Known gaps (documented in tests)
6412

6513
### 1. Variable assignment from builder-forwarded static method in GTD
@@ -78,29 +26,7 @@ needs the source location).
7826

7927
## Missing features
8028

81-
### 2. Eloquent casts
82-
83-
Properties defined in the `$casts` array (or `casts()` method) should
84-
produce typed virtual properties. For example:
85-
86-
```php
87-
protected $casts = [
88-
'created_at' => 'datetime', // → Carbon
89-
'options' => 'array', // → array
90-
'is_admin' => 'boolean', // → bool
91-
];
92-
```
93-
94-
This requires parsing the `$casts` property initializer or `casts()`
95-
method body to extract key-value pairs, then mapping cast type strings
96-
to PHP types. Common mappings: `datetime``Carbon\Carbon`,
97-
`array`/`json``array`, `boolean`/`bool``bool`,
98-
`integer`/`int``int`, `float`/`double`/`real`/`decimal:*``float`,
99-
`string``string`, `collection`
100-
`Illuminate\Support\Collection`, custom cast classes → inspect
101-
their `get()` return type.
102-
103-
### 3. Factory support
29+
### 2. Factory support
10430

10531
`User::factory()->create()` is ubiquitous in Laravel test code. The
10632
`factory()` static method returns a `HasFactory` trait method that
@@ -114,14 +40,14 @@ produces a factory instance. Resolving the chain requires:
11440
This is medium complexity because it involves a naming convention
11541
(model name → factory name) and cross-file resolution.
11642

117-
### 4. Closure parameter inference in collection pipelines
43+
### 3. Closure parameter inference in collection pipelines
11844

11945
`$users->map(fn($u) => $u->...)` does not infer `$u` as the
12046
collection's element type. This is a general generics/callable
12147
inference problem, not Laravel-specific, but Laravel collection
12248
pipelines are the most common place users encounter it.
12349

124-
### 5. Query scope chaining on Builder instances
50+
### 4. Query scope chaining on Builder instances
12551

12652
Inside a scope method body, `$query->verified()` (calling another
12753
scope) does not offer scope method completions. Scope methods are
@@ -157,8 +83,8 @@ Builder instances, not just Model classes.
15783
- **No SQL/migration parsing.** Model column types are not inferred from
15884
database schemas or migration files.
15985
- **Larastan-style hints preferred.** We expect relationship methods to be
160-
annotated in the style that Larastan expects. Fallback heuristics (item 3
161-
above) are best-effort.
86+
annotated in the style that Larastan expects. Fallback heuristics
87+
are best-effort.
16288
- **Facades fall back to `@method`.** Facades whose `getFacadeAccessor()`
16389
returns a string alias cannot be resolved. `@method` tags on facade
16490
classes provide completion without template intelligence.

0 commit comments

Comments
 (0)