Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
92e0dc1
feat(php): Add scalable reference CodeLens
sidux Aug 26, 2026
0ccf3f5
fix(indexing): Reuse workspace index for annotations
sidux Aug 27, 2026
be8c516
fix(indexing): Batch member reference counts
sidux Aug 27, 2026
4176e9e
fix(indexing): Reuse forward scopes for member counts
sidux Aug 27, 2026
5757191
fix(indexing): Coalesce member count refreshes
sidux Aug 27, 2026
e1daa32
fix(indexing): Cache semantic member targets
sidux Aug 27, 2026
f4bea42
fix(references): keep member batching framework-neutral
sidux Aug 27, 2026
d06fdfe
feat(navigation): Navigate PHP symbols in YAML and XML
sidux Aug 25, 2026
66ef462
feat(navigation): Index YAML and XML PHP references
sidux Aug 26, 2026
dc6652f
feat(frameworks): Add Symfony and Doctrine resource navigation
sidux Jul 6, 2026
63cd4fa
fix(symfony): Preserve framework CodeLens destinations
sidux Aug 5, 2026
1f54594
fix(doctrine): Bound reverse relationship CodeLens
sidux Aug 27, 2026
5354811
fix(navigation): Preserve Symfony resource fallbacks
sidux Aug 27, 2026
7c90214
fix(doctrine): Cache repository mapping index
sidux Aug 27, 2026
2c60084
fix(indexing): Index framework reference lookups
sidux Aug 27, 2026
6106c78
test(frameworks): avoid redundant URI allocations
sidux Aug 27, 2026
af3df00
feat(symfony): Index PHP configuration resources
sidux Jul 29, 2026
ff40130
feat(symfony): Add service container intelligence
sidux Jul 29, 2026
fd29607
fix(symfony): Handle Unicode in PHP resource scans
sidux Jul 29, 2026
086d08a
feat(symfony): Add route intelligence
sidux Jul 29, 2026
46ca1b9
feat(symfony): Add Twig template intelligence
sidux Jul 29, 2026
0995004
feat(symfony): Add translation intelligence
sidux Jul 29, 2026
ba8db30
feat(php): Map transparent proxies to real classes
sidux Aug 25, 2026
58a0ac0
feat(php): add call hierarchy
sidux Aug 30, 2026
b15472c
feat(symfony): Navigate event publishers and listeners
sidux Aug 25, 2026
22e6a6b
feat(symfony): Add events and Messenger intelligence
sidux Jul 29, 2026
845866b
perf(symfony): index Messenger CodeLens mappings
sidux Aug 27, 2026
596c503
feat(symfony): expose events in call hierarchy
sidux Aug 30, 2026
8894f02
feat(symfony): Resolve configured ExpressionLanguage strings
sidux Aug 25, 2026
bc45de5
feat(symfony): Add forms, validation, and config schemas
sidux Jul 29, 2026
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
286 changes: 286 additions & 0 deletions config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,31 @@
"type": "string",
"description": "Override the detected PHP version (e.g. \"8.3\"). When unset, PHPantom infers from composer.json's platform or require.php.",
"pattern": "^\\d+\\.\\d+(\\.\\d+)?$"
},
"proxies": {
"type": "array",
"description": "Generated transparent-proxy discovery rules. Matching subclasses keep their PHP type, while project metadata is attributed to their real parent class.",
"items": {
"type": "object",
"properties": {
"paths": {
"type": "array",
"description": "Workspace-relative PHP files, directories, or glob patterns to scan for generated proxy subclasses.",
"items": {
"type": "string"
}
},
"marker-interface": {
"type": "string",
"description": "Fully-qualified interface that a generated subclass must directly implement to be treated as a transparent proxy."
}
},
"required": [
"paths",
"marker-interface"
]
},
"default": []
}
}
},
Expand Down Expand Up @@ -147,6 +172,267 @@
}
}
},
"symfony": {
"type": "object",
"description": "Symfony runtime metadata recovered statically from compiled containers and configured PHP attributes.",
"properties": {
"container": {
"type": "object",
"description": "Controls static compiled-container discovery. Container PHP is read as text and is never executed.",
"properties": {
"enabled": {
"type": "boolean",
"description": "Read Symfony compiled-container metadata.",
"default": true
},
"environment": {
"type": "string",
"description": "Cache environment used by automatic var/cache/<environment> discovery.",
"default": "dev"
},
"paths": {
"type": "array",
"description": "Optional workspace-relative compiled-container files, directories, or glob patterns. The newest useful match wins.",
"items": {
"type": "string"
}
}
}
},
"events": {
"type": "object",
"description": "Configures event-name matching and attribute adapters. Exact listener wiring comes from the compiled container.",
"properties": {
"ignored-prefixes": {
"type": "array",
"description": "Prefixes removed before event names are compared.",
"items": {
"type": "string"
}
},
"ignored-suffixes": {
"type": "array",
"description": "Suffixes removed before event names are compared.",
"items": {
"type": "string"
}
},
"publishers": {
"type": "array",
"description": "Rules that derive Symfony event publishers from PHP method attributes.",
"items": {
"type": "object",
"properties": {
"attribute": {
"type": "string",
"description": "Fully-qualified publisher attribute class."
},
"name-argument": {
"type": "string",
"description": "Named argument containing an explicit event name."
},
"name-position": {
"type": "integer",
"description": "Zero-based positional fallback for the explicit event-name argument.",
"minimum": 0
},
"dispatch-argument": {
"type": "string",
"description": "Named argument containing dispatch enum cases."
},
"dispatch-position": {
"type": "integer",
"description": "Zero-based positional fallback for the dispatch argument.",
"minimum": 0
},
"default-dispatch": {
"type": "array",
"description": "Dispatch names used when the attribute omits its dispatch argument.",
"items": {
"type": "string"
}
},
"dispatch-cases": {
"type": "object",
"description": "Map from PHP enum case name to the event-name dispatch segment.",
"additionalProperties": {
"type": "string"
}
},
"name-template": {
"type": "string",
"description": "Derived event-name template. Supports {dispatch}, {class}, {class_snake}, {method}, {method_snake}, {method_suffix}, and {method_suffix_snake}."
},
"explicit-name-template": {
"type": "string",
"description": "Template used for explicit names. Supports the same placeholders plus {name}.",
"default": "{name}"
},
"default-methods": {
"type": "array",
"description": "Method names that omit the method suffix.",
"items": {
"type": "string"
}
},
"skip": {
"type": "array",
"description": "Conditions that omit one derived dispatch when another attribute argument is set.",
"items": {
"type": "object",
"properties": {
"dispatch": {
"type": "string"
},
"argument": {
"type": "string"
},
"position": {
"type": "integer",
"minimum": 0
}
},
"required": [
"dispatch",
"argument"
]
}
}
},
"required": [
"attribute",
"name-template"
]
}
},
"subscribers": {
"type": "array",
"description": "Optional rules that expose event names written in subscriber method attributes before or without a compiled container.",
"items": {
"type": "object",
"properties": {
"attribute": {
"type": "string",
"description": "Fully-qualified subscriber attribute class."
},
"name-argument": {
"type": "string"
},
"name-position": {
"type": "integer",
"minimum": 0
},
"transport-argument": {
"type": "string"
},
"transport-position": {
"type": "integer",
"minimum": 0
},
"transport-cases": {
"type": "object",
"description": "Map from PHP enum case name to an event-name suffix.",
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"attribute"
]
}
}
}
},
"expression-language": {
"type": "object",
"description": "Maps configured PHP attribute and constructor arguments to Symfony ExpressionLanguage strings and their PHP type contracts.",
"properties": {
"attributes": {
"type": "array",
"description": "Method-attribute arguments that contain an expression string or an array of expression strings.",
"items": {
"type": "object",
"properties": {
"attribute": {
"type": "string",
"description": "Fully-qualified PHP method attribute class."
},
"argument": {
"type": "string",
"description": "Named argument containing expressions."
},
"position": {
"type": "integer",
"description": "Zero-based positional fallback for the expression argument.",
"minimum": 0
},
"method-parameters": {
"type": "boolean",
"description": "Bind expression roots to method parameters with the same name.",
"default": false
},
"bindings": {
"type": "object",
"description": "Map expression roots to return, parameter:<name-or-index>, or class:<FQN> type sources.",
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"attribute"
]
}
},
"constructors": {
"type": "array",
"description": "Expression object constructors nested inside PHP method attributes.",
"items": {
"type": "object",
"properties": {
"class": {
"type": "string",
"description": "Fully-qualified expression object class."
},
"argument": {
"type": "string",
"description": "Named constructor argument containing the expression."
},
"position": {
"type": "integer",
"description": "Zero-based positional fallback for the constructor argument.",
"minimum": 0
},
"inside-attribute-prefixes": {
"type": "array",
"description": "Only match constructors nested in attributes whose FQN starts with one of these prefixes. An empty list matches any attribute.",
"items": {
"type": "string"
}
},
"method-parameters": {
"type": "boolean",
"description": "Bind expression roots to method parameters with the same name.",
"default": false
},
"bindings": {
"type": "object",
"description": "Map expression roots to return, parameter:<name-or-index>, or class:<FQN> type sources.",
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"class"
]
}
}
}
}
}
},
"formatting": {
"type": "object",
"description": "Controls the formatting strategy. PHPantom ships a built-in formatter (PER-CS 2.0 style). Projects with php-cs-fixer or PHP_CodeSniffer in composer.json require-dev automatically use those tools instead. Explicit configuration here always takes priority.",
Expand Down
25 changes: 22 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@ src/
├── composer.rs # composer.json / PSR-4 autoload parsing
├── names.rs # Name resolution (FQN, use-map, namespace)
├── reference_index.rs # Workspace-wide reference index for find-references / rename
├── reference_counts.rs # Background-computed member reference counts for the declaration inlay hints
├── reference_counts.rs # Bounded exact member-reference cache for declaration hints and lenses
│ # Class & type resolution
├── resolution.rs # Multi-phase class/function lookup across files (find_or_load_class)
├── proxy_metadata.rs # Transparent proxy → real-class relations for metadata consumers
├── symfony/ # Compiled-container, event, and ExpressionLanguage adapters
├── class_lookup.rs # Subtype checks (is_subtype_of_typed) and class-lookup helpers
├── inheritance/ # Parent/trait/mixin member merging, generics substitution
├── virtual_members/ # Synthesized members: phpdoc.rs (@method/@property/@mixin) + laravel/ (one file per Eloquent/framework feature)
Expand Down Expand Up @@ -98,6 +100,7 @@ src/
│ # LSP features (one module each)
├── hover/ # Hover: symbol-map dispatch, type/signature/docblock formatting
├── definition/ # Go-to-definition (resolve, member, variable/, implementation, type_definition)
├── resource_navigation.rs # Schema-free PHP class/member indexing and navigation in YAML/XML
├── references/, rename/, highlight/
├── signature_help.rs, semantic_tokens.rs, inlay_hints.rs, folding.rs, code_lens.rs
├── document_symbols.rs, document_links.rs, workspace_symbols.rs, formatting.rs
Expand Down Expand Up @@ -141,6 +144,20 @@ diagnostics, hover, go-to-definition, and signature help, not just completion
below). Do not build a second type-resolution path: extend the engine here so
every consumer benefits.

### Framework Runtime Metadata

Framework-generated files feed small metadata adapters instead of creating a
second symbol resolver. `proxy_metadata.rs` is the shared proxy-to-real-class
relation. `symfony/container.rs` reads compiled containers as text and exposes
listener registrations and proxied service candidates; it never includes PHP.
`symfony/events.rs` combines that exact runtime wiring with configured
attribute rules, then serves go-to-definition, references, and code lenses.
`symfony/expressions.rs` maps configured attribute and constructor arguments to
method parameter, return, or fixed-class types, then delegates member chains to
the shared PHP type engine for navigation and diagnostics.
Metadata owners are canonicalized through the proxy relation before lookup, so
all consumers agree on the real class without rewriting normal PHP types.

## External Crates

PHPantom uses several crates from the [Mago](https://github.com/carthage-software/mago)
Expand Down Expand Up @@ -877,12 +894,14 @@ When the user invokes "Find All References", PHPantom scans all user files for o
Before scanning, `ensure_workspace_indexed` ensures all user files have symbol maps:

1. **Phase 1: fqn_uri_index files (user only)** — files already known from `update_ast` calls. Vendor and stub URIs are skipped.
2. **Phase 2: `.gitignore`-aware workspace walk** — uses the `ignore` crate's `WalkBuilder` to recursively discover PHP files under the workspace root, respecting `.gitignore` rules (including nested and global gitignore files). This automatically skips generated/cached directories like `storage/framework/views/` (Laravel blade cache), `var/cache/` (Symfony), and `node_modules/`. The vendor directory is always skipped regardless of `.gitignore` content. Hidden directories are skipped by default.
2. **Phase 2: `.gitignore`-aware workspace walk** — uses the `ignore` crate's `WalkBuilder` to recursively discover PHP plus YAML/XML resource files under the workspace root, respecting `.gitignore` rules (including nested and global gitignore files). This automatically skips generated/cached directories like `storage/framework/views/` (Laravel blade cache), `var/cache/` (Symfony), and `node_modules/`. The vendor directory is always skipped regardless of `.gitignore` content. Hidden directories are skipped by default.

Both phases parse files in parallel using `std::thread::scope`. The work is split into chunks (one per CPU core) and each thread reads a file from disk and calls `update_ast`, which acquires write locks briefly to store results while the expensive parsing step runs without any locks held. Batches of 2 or fewer files skip threading overhead.
PHP files are parsed in parallel using `std::thread::scope`. The work is split into chunks (one per CPU core) and each thread reads a file from disk and calls `update_ast`, which acquires write locks briefly to store results while the expensive parsing step runs without any locks held. Batches of 2 or fewer files skip threading overhead. YAML/XML files take the lightweight schema-free scanner and publish synthetic class/member symbol maps into the same reference index.

Parsed files stay cached in `uri_classes_index`, `symbol_maps`, `file_imports`, and `file_namespaces` after the scan completes. There is no post-scan eviction; keeping the entries means subsequent operations (a second find-references call, go-to-definition on a cross-file symbol) benefit from the work already done.

The workspace reference index keeps its primary map deliberately coarse: it stores candidate URIs and occurrence counts, not a second copy of every source position. CodeLens can therefore answer a conclusive zero without a semantic scan. The first nonzero member query resolves every member receiver in each candidate file while one forward-walked variable scope is active, packs the target class atoms by symbol-span index, and retains that compact per-file semantic layer for later member names. Candidate files are filled in parallel; edits evict their own layer, and signature changes clear layers whose receiver types may have changed. Exact locations remain bounded behind the 50,000-location annotation cache. Refresh-capable clients receive the lens after the background result is ready; other clients retain lazy `codeLens/resolve` as a compatibility path.

### Cross-file scanning

The `user_file_symbol_maps()` helper snapshots all symbol maps whose URI does not fall under the vendor directory or the internal stub scheme. With `Arc<SymbolMap>`, the snapshot is a vector of cheap reference-count increments rather than deep clones. Four scanners use this snapshot:
Expand Down
Loading
Loading