Skip to content

Commit de2084b

Browse files
committed
Refactor code in preperation for additional feature work
1 parent 9bddf35 commit de2084b

85 files changed

Lines changed: 25441 additions & 26308 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/ARCHITECTURE.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ src/
2424
├── stubs.rs # Embedded phpstorm-stubs (build-time generated index)
2525
├── resolution.rs # Multi-phase class/function lookup and name resolution
2626
├── inheritance.rs # Base class inheritance merging (traits, parent chain)
27-
├── symbol_map.rs # Precomputed per-file symbol location map (SymbolSpan, VarDefSite, CallSite, SymbolMap)
27+
├── symbol_map/
28+
│ ├── mod.rs # Data structures (SymbolSpan, SymbolKind, VarDefSite, CallSite, SymbolMap) and impl
29+
│ ├── docblock.rs # Docblock symbol extraction (type span emission, @template/@method tag scanning, navigability filter)
30+
│ └── extraction.rs # AST walk that builds a SymbolMap (extract_symbol_map and all extract_from_* helpers)
2831
├── virtual_members/
2932
│ ├── mod.rs # VirtualMemberProvider trait, VirtualMembers struct, merge logic
3033
│ ├── laravel.rs # LaravelModelProvider (relationships, scopes, casts, accessors)
@@ -51,9 +54,14 @@ src/
5154
│ ├── resolver.rs # Resolve subject → ClassInfo (type resolution engine), shared resolve_callable_target
5255
│ ├── source_helpers.rs # Source-text scanning helpers (closure/callable return types, new-expression parsing, array access)
5356
│ ├── builder.rs # Build LSP CompletionItems from resolved ClassInfo
54-
│ ├── class_completion.rs # Class name, constant, and function completions
57+
│ ├── class_completion.rs # Class name completions (class, interface, trait, enum)
58+
│ ├── constant_completion.rs # Global constant name completions
59+
│ ├── function_completion.rs # Standalone function name completions
60+
│ ├── namespace_completion.rs # Namespace declaration completions
5561
│ ├── variable_completion.rs # Variable name completions and scope collection
5662
│ ├── variable_resolution.rs # Variable type resolution via assignment scanning
63+
│ ├── class_string_resolution.rs # Class-string variable resolution ($cls = User::class)
64+
│ ├── raw_type_inference.rs # Raw type inference for variable assignments (array shapes, array functions, generators)
5765
│ ├── foreach_resolution.rs # Foreach value/key and array destructuring type resolution
5866
│ ├── closure_resolution.rs # Closure and arrow-function parameter resolution
5967
│ ├── type_narrowing.rs # instanceof / assert / custom type guard narrowing
@@ -74,7 +82,10 @@ src/
7482
│ ├── mod.rs # Submodule declarations
7583
│ ├── resolve.rs # Core go-to-definition: symbol-map dispatch + text-based fallback
7684
│ ├── member.rs # Member-access resolution (->method, ::$prop, ::CONST) with stored offsets
77-
│ ├── variable.rs # Variable definition resolution (symbol-map → AST walk → text fallback)
85+
│ ├── variable/
86+
│ │ ├── mod.rs # VarDefSearchResult enum, Backend methods, tests
87+
│ │ ├── var_definition.rs # AST walk finding variable definition sites
88+
│ │ └── type_hint.rs # AST walk extracting type hints at definition sites
7889
│ └── implementation.rs # Go-to-implementation (interface/abstract → concrete classes)
7990
build.rs # Parses PhpStormStubsMap.php, generates stub index
8091
stubs/ # Composer vendor dir for jetbrains/phpstorm-stubs

docs/todo-laravel.md

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,27 @@ benefit) and an **Effort** estimate (implementation complexity):
7474

7575
---
7676

77-
#### 1. `$dates` array (deprecated)
77+
#### 1. `morphedByMany` missing from relationship method map
78+
79+
| | |
80+
|---|---|
81+
| **Impact** | ★★ — Any model using `morphedByMany` (the inverse of a polymorphic many-to-many) gets no virtual property or `_count` property for that relationship. |
82+
| **Effort** | ★ — One-line addition to `RELATIONSHIP_METHOD_MAP` in `virtual_members/laravel.rs`. |
83+
84+
`morphedByMany` is the inverse side of a polymorphic many-to-many
85+
relationship. It returns a `MorphToMany` instance (the same class as
86+
`morphToMany`), but the method name is not listed in
87+
`RELATIONSHIP_METHOD_MAP`. This means body inference
88+
(`infer_relationship_from_body`) does not recognise
89+
`$this->morphedByMany(Tag::class)` calls, so no virtual property or
90+
`_count` property is synthesized.
91+
92+
**Where to change:** Add `("morphedByMany", "MorphToMany")` to
93+
`RELATIONSHIP_METHOD_MAP` in `src/virtual_members/laravel.rs`.
94+
No other changes needed since `MorphToMany` is already in
95+
`COLLECTION_RELATIONSHIPS`.
96+
97+
#### 2. `$dates` array (deprecated)
7898

7999
| | |
80100
|---|---|
@@ -93,7 +113,7 @@ Merge these into `casts_definitions` at a lower priority than explicit
93113
`$casts` entries, or add a separate field on `ClassInfo` and handle
94114
priority in the provider.
95115

96-
#### 2. Custom Eloquent builders (`HasBuilder` / `#[UseEloquentBuilder]`)
116+
#### 3. Custom Eloquent builders (`HasBuilder` / `#[UseEloquentBuilder]`)
97117

98118
| | |
99119
|---|---|
@@ -131,7 +151,7 @@ declares a custom builder via `@use HasBuilder<X>` in `use_generics`
131151
or a `newEloquentBuilder()` method with a non-default return type.
132152
If found, load and resolve that builder class instead.
133153

134-
#### 3. `abort_if`/`abort_unless` type narrowing
154+
#### 4. `abort_if`/`abort_unless` type narrowing
135155

136156
| | |
137157
|---|---|
@@ -175,7 +195,7 @@ to subsequent code:
175195
This is similar to the existing guard clause narrowing but triggered
176196
by specific function names rather than `if` + early return.
177197

178-
#### 4. `collect()` and other helper functions lose generic type info
198+
#### 5. `collect()` and other helper functions lose generic type info
179199

180200
| | |
181201
|---|---|
@@ -223,7 +243,7 @@ before passing it to `type_hint_to_classes`. See the general TODO
223243
item (§ PHP Language Feature Gaps, "Function-level `@template`
224244
generic resolution") for the full implementation plan.
225245

226-
#### 5. Factory `has*`/`for*` relationship methods
246+
#### 6. Factory `has*`/`for*` relationship methods
227247

228248
| | |
229249
|---|---|
@@ -261,7 +281,7 @@ The `has*` variant should accept optional `int $count` and
261281
`array|callable $state` parameters; `for*` should accept
262282
`array|callable $state`.
263283

264-
#### 6. `$pivot` property on BelongsToMany related models
284+
#### 7. `$pivot` property on BelongsToMany related models
265285

266286
| | |
267287
|---|---|
@@ -307,7 +327,7 @@ the `BelongsToMany` relationship stubs. If the user's stub set
307327
includes these annotations, it already works through our PHPDoc
308328
provider.
309329

310-
#### 7. `withSum()` / `withAvg()` / `withMin()` / `withMax()` aggregate properties
330+
#### 8. `withSum()` / `withAvg()` / `withMin()` / `withMax()` aggregate properties
311331

312332
| | |
313333
|---|---|
@@ -323,7 +343,7 @@ aggregate function (`withSum`/`withAvg` → `float`,
323343

324344
The `@property` workaround applies here too.
325345

326-
#### 8. Higher-order collection proxies
346+
#### 9. Higher-order collection proxies
327347

328348
| | |
329349
|---|---|
@@ -346,7 +366,7 @@ and `HigherOrderCollectionProxyExtension`, which resolve the proxy's
346366
template types and delegate property/method lookups to the collection's
347367
value type.
348368

349-
#### 9. `SoftDeletes` trait methods on Builder
369+
#### 10. `SoftDeletes` trait methods on Builder
350370

351371
| | |
352372
|---|---|
@@ -374,7 +394,7 @@ type — e.g. `Builder<static>` instead of `Builder<User>`. This is
374394
a minor gap but not worth a dedicated fix until custom builder
375395
support (gap §7) is implemented.
376396

377-
#### 10. `View::withX()` and `RedirectResponse::withX()` dynamic methods
397+
#### 11. `View::withX()` and `RedirectResponse::withX()` dynamic methods
378398

379399
| | |
380400
|---|---|
@@ -406,7 +426,7 @@ hard-coding the two known classes. A simpler approach: add
406426
`@method` tags to bundled stubs for the most common dynamic `with*`
407427
methods, or document this as a known limitation.
408428

409-
#### 11. `$appends` array
429+
#### 12. `$appends` array
410430

411431
| | |
412432
|---|---|
@@ -417,4 +437,25 @@ The `$appends` property lists accessor names that should always be
417437
included in `toArray()` / `toJson()`. These reference existing
418438
accessors, so in most cases the accessor method itself already produces
419439
the virtual property. Parsing `$appends` would only help when the
420-
accessor is defined in an unloaded parent class.
440+
accessor is defined in an unloaded parent class.
441+
442+
#### 13. Relationship classification matches short name only
443+
444+
| | |
445+
|---|---|
446+
| **Impact** | ★ — Nearly all codebases use Laravel's built-in relationship classes, so false positives are rare in practice. |
447+
| **Effort** | ★★ — Need to resolve the return type's FQN before matching, which may require a class loader call. |
448+
449+
`classify_relationship` in `virtual_members/laravel.rs` strips the
450+
return type down to its short name (via `short_name`) and matches
451+
against a hardcoded list (`HasMany`, `BelongsTo`, etc.). This means
452+
any class whose short name collides with a Laravel relationship class
453+
(e.g. a custom `App\Relations\HasMany` that does not extend
454+
Eloquent's) would be incorrectly classified as a relationship.
455+
456+
The fix would be to resolve the return type to its FQN (using the
457+
class loader or use-map) and verify it lives under
458+
`Illuminate\Database\Eloquent\Relations\` (or extends a class that
459+
does) before classifying. The short-name-only path could remain as a
460+
fast-path fallback when the FQN is already in the
461+
`Illuminate\Database\Eloquent\Relations` namespace.

src/completion/array_shape.rs

Lines changed: 2 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -749,205 +749,5 @@ pub(super) fn build_list_type_from_push_types(types: &[String]) -> Option<String
749749
}
750750

751751
#[cfg(test)]
752-
mod tests {
753-
use super::*;
754-
755-
#[test]
756-
fn test_detect_single_quote_empty() {
757-
// $config['
758-
let content = "<?php\n$config['";
759-
let pos = Position {
760-
line: 1,
761-
character: 9,
762-
};
763-
let ctx = detect_array_key_context(content, pos).unwrap();
764-
assert_eq!(ctx.var_name, "$config");
765-
assert_eq!(ctx.partial_key, "");
766-
assert_eq!(ctx.quote_char, Some('\''));
767-
assert_eq!(ctx.key_start_col, 9);
768-
assert!(ctx.prefix_keys.is_empty());
769-
}
770-
771-
#[test]
772-
fn test_detect_single_quote_partial() {
773-
// $config['na
774-
let content = "<?php\n$config['na";
775-
let pos = Position {
776-
line: 1,
777-
character: 11,
778-
};
779-
let ctx = detect_array_key_context(content, pos).unwrap();
780-
assert_eq!(ctx.var_name, "$config");
781-
assert_eq!(ctx.partial_key, "na");
782-
assert_eq!(ctx.quote_char, Some('\''));
783-
assert_eq!(ctx.key_start_col, 9);
784-
assert!(ctx.prefix_keys.is_empty());
785-
}
786-
787-
#[test]
788-
fn test_detect_double_quote_empty() {
789-
let content = "<?php\n$config[\"";
790-
let pos = Position {
791-
line: 1,
792-
character: 9,
793-
};
794-
let ctx = detect_array_key_context(content, pos).unwrap();
795-
assert_eq!(ctx.var_name, "$config");
796-
assert_eq!(ctx.partial_key, "");
797-
assert_eq!(ctx.quote_char, Some('"'));
798-
assert_eq!(ctx.key_start_col, 9);
799-
assert!(ctx.prefix_keys.is_empty());
800-
}
801-
802-
#[test]
803-
fn test_detect_bracket_only() {
804-
// $config[
805-
let content = "<?php\n$config[";
806-
let pos = Position {
807-
line: 1,
808-
character: 8,
809-
};
810-
let ctx = detect_array_key_context(content, pos).unwrap();
811-
assert_eq!(ctx.var_name, "$config");
812-
assert_eq!(ctx.partial_key, "");
813-
assert_eq!(ctx.quote_char, None);
814-
assert_eq!(ctx.key_start_col, 8);
815-
assert!(ctx.prefix_keys.is_empty());
816-
}
817-
818-
#[test]
819-
fn test_no_context_without_bracket() {
820-
let content = "<?php\n$config";
821-
let pos = Position {
822-
line: 1,
823-
character: 7,
824-
};
825-
assert!(detect_array_key_context(content, pos).is_none());
826-
}
827-
828-
#[test]
829-
fn test_no_context_without_variable() {
830-
let content = "<?php\nfoo['";
831-
let pos = Position {
832-
line: 1,
833-
character: 5,
834-
};
835-
assert!(detect_array_key_context(content, pos).is_none());
836-
}
837-
838-
#[test]
839-
fn test_detect_chained_single_key() {
840-
// $response['meta'][
841-
let content = "<?php\n$response['meta'][";
842-
let pos = Position {
843-
line: 1,
844-
character: 18,
845-
};
846-
let ctx = detect_array_key_context(content, pos).unwrap();
847-
assert_eq!(ctx.var_name, "$response");
848-
assert_eq!(ctx.partial_key, "");
849-
assert_eq!(ctx.quote_char, None);
850-
assert_eq!(ctx.prefix_keys, vec!["meta"]);
851-
}
852-
853-
#[test]
854-
fn test_detect_chained_single_key_with_quote() {
855-
// $response['meta']['
856-
let content = "<?php\n$response['meta']['";
857-
let pos = Position {
858-
line: 1,
859-
character: 19,
860-
};
861-
let ctx = detect_array_key_context(content, pos).unwrap();
862-
assert_eq!(ctx.var_name, "$response");
863-
assert_eq!(ctx.partial_key, "");
864-
assert_eq!(ctx.quote_char, Some('\''));
865-
assert_eq!(ctx.prefix_keys, vec!["meta"]);
866-
}
867-
868-
#[test]
869-
fn test_detect_chained_two_keys() {
870-
// $data['a']['b'][
871-
let content = "<?php\n$data['a']['b'][";
872-
let pos = Position {
873-
line: 1,
874-
character: 16,
875-
};
876-
let ctx = detect_array_key_context(content, pos).unwrap();
877-
assert_eq!(ctx.var_name, "$data");
878-
assert_eq!(ctx.prefix_keys, vec!["a", "b"]);
879-
}
880-
881-
#[test]
882-
fn test_detect_autoclosed_bracket() {
883-
// $config[] — cursor between [ and ]
884-
let content = "<?php\n$config[]";
885-
let pos = Position {
886-
line: 1,
887-
character: 8,
888-
};
889-
let ctx = detect_array_key_context(content, pos).unwrap();
890-
assert_eq!(ctx.var_name, "$config");
891-
assert_eq!(ctx.partial_key, "");
892-
assert_eq!(ctx.quote_char, None);
893-
assert_eq!(ctx.key_start_col, 8);
894-
}
895-
896-
#[test]
897-
fn test_detect_autoclosed_quote_bracket() {
898-
// $config[''] — cursor between the two quotes
899-
let content = "<?php\n$config['']";
900-
let pos = Position {
901-
line: 1,
902-
character: 9,
903-
};
904-
let ctx = detect_array_key_context(content, pos).unwrap();
905-
assert_eq!(ctx.var_name, "$config");
906-
assert_eq!(ctx.partial_key, "");
907-
assert_eq!(ctx.quote_char, Some('\''));
908-
assert_eq!(ctx.key_start_col, 9);
909-
}
910-
911-
#[test]
912-
fn test_build_list_type_single() {
913-
let types = vec!["User".to_string()];
914-
assert_eq!(
915-
build_list_type_from_push_types(&types),
916-
Some("list<User>".to_string())
917-
);
918-
}
919-
920-
#[test]
921-
fn test_build_list_type_union() {
922-
let types = vec!["User".to_string(), "AdminUser".to_string()];
923-
assert_eq!(
924-
build_list_type_from_push_types(&types),
925-
Some("list<User|AdminUser>".to_string())
926-
);
927-
}
928-
929-
#[test]
930-
fn test_build_list_type_deduplicates() {
931-
let types = vec![
932-
"User".to_string(),
933-
"User".to_string(),
934-
"AdminUser".to_string(),
935-
];
936-
assert_eq!(
937-
build_list_type_from_push_types(&types),
938-
Some("list<User|AdminUser>".to_string())
939-
);
940-
}
941-
942-
#[test]
943-
fn test_build_list_type_empty() {
944-
let types: Vec<String> = vec![];
945-
assert_eq!(build_list_type_from_push_types(&types), None);
946-
}
947-
948-
#[test]
949-
fn test_build_list_type_all_mixed() {
950-
let types = vec!["mixed".to_string(), "mixed".to_string()];
951-
assert_eq!(build_list_type_from_push_types(&types), None);
952-
}
953-
}
752+
#[path = "array_shape_tests.rs"]
753+
mod tests;

0 commit comments

Comments
 (0)