Skip to content
Open
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
4 changes: 2 additions & 2 deletions config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@
"properties": {
"strategy": {
"type": "string",
"description": "The indexing strategy for class discovery. \"composer\" (default): use Composer's classmap, fall back to self-scan. \"self\": scan all PHP files, ignore classmap. \"full\": background-parse all files (not yet implemented). \"none\": no proactive scanning, classmap only.",
"description": "The indexing strategy for class discovery. \"full\" (default): scan PHP files, then background-parse user files to populate symbol and reference indexes. \"composer\": use Composer's classmap, fall back to self-scan. \"self\": scan all PHP files, ignore classmap. \"none\": no proactive scanning, classmap only.",
"enum": [
"composer",
"self",
"full",
"none"
],
"default": "composer"
"default": "full"
}
}
},
Expand Down
8 changes: 4 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ PHPantom is built in layers. Each layer is independently useful and independentl
- **Layer 1: Single file.** Parse the open file, extract classes/functions/symbols. Completion, hover, and go-to-definition work within the file with no cross-file resolution at all.
- **Layer 2: On-demand resolution.** When a symbol references a class in another file, resolve it through the `fqn_uri_index` or PSR-4 and parse that file. Only the files actually needed are touched.
- **Layer 3: FQN-to-URI index.** A name-to-URI index covering the whole project. Enables class name completion and O(1) cross-file lookup. Built from Composer's classmap or self-generated via a fast byte-level scan.
- **Layer 4: Full index (opt-in).** Background-parse every file in the fqn_uri_index. Enables workspace symbols, fast find-references, and rich completion item detail.
- **Layer 4: Full index (default).** Background-parse every file in the fqn_uri_index. Enables workspace symbols, fast find-references, and rich completion item detail.

Each layer builds on the one below it. A bug in the FQN index doesn't break single-file completion. A slow full index doesn't block on-demand resolution. New features can be developed and tested against the lower layers without waiting for a full project scan. This is also why PHPantom starts fast: Layer 0-2 are ready in milliseconds, Layer 3 takes seconds, and Layer 4 (when enabled) fills in over the following minute.
Each layer builds on the one below it. A bug in the FQN index doesn't break single-file completion. A slow full index doesn't block on-demand resolution. New features can be developed and tested against the lower layers without waiting for a full project scan. This is also why PHPantom starts fast: Layer 0-2 are ready in milliseconds, Layer 3 takes seconds, and Layer 4 fills in afterward.

## Module Layout

Expand Down Expand Up @@ -687,9 +687,9 @@ Scanning is parallelised using a two-phase approach: directory walks collect fil

The indexing strategy is configurable via `[indexing] strategy` in `.phpantom.toml`:

- **`"composer"`** (default) — merged classmap + self-scan. Load Composer's classmap (if it exists) as a skip set, then self-scan all PSR-4 and vendor directories for anything the classmap missed. Whatever the classmap already covers is a free performance win; whatever it's missing, we find ourselves. No completeness heuristic needed.
- **`"full"`** (default) — same discovery as `"self"`, then background-parses user PHP files to populate symbol maps and the reference candidate index.
- **`"composer"`** — merged classmap + self-scan. Load Composer's classmap (if it exists) as a skip set, then self-scan all PSR-4 and vendor directories for anything the classmap missed. Whatever the classmap already covers is a free performance win; whatever it's missing, we find ourselves. No completeness heuristic needed.
- **`"self"`** — always self-scan, ignoring Composer's classmap entirely. Equivalent to the merged approach with an empty skip set.
- **`"full"`** — same as `"self"` for now; reserved for future background indexing.
- **`"none"`** — no proactive scanning; uses Composer's classmap if present but never self-scans to fill gaps.

The merged pipeline works in three steps: (1) load `autoload_classmap.php` into a `HashMap<String, PathBuf>`, (2) collect the classmap's file paths into a `HashSet<PathBuf>` skip set, (3) self-scan all PSR-4 and vendor directories, skipping files already in the skip set. The result is a merged index: classmap entries for everything Composer already knew about, plus self-scanned entries for everything it missed. When the classmap is complete (the common case), the self-scanner walks directories but skips every file, finishing almost instantly. When the classmap is empty or absent, it falls back to a full self-scan. When the classmap is partial (e.g. vendor classes only), vendor files are skipped and only user code is scanned. Every state of the classmap helps.
Expand Down
20 changes: 11 additions & 9 deletions docs/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,33 +246,35 @@ This creates a `.phpantom.toml` in the current directory. Currently supported se

[indexing]
# How PHPantom discovers classes across the workspace.
# "composer" (default) - use Composer classmap, self-scan on fallback
# "self" - always self-scan, ignore Composer classmap
# "none" - no proactive scanning, Composer classmap only
# strategy = "composer"
# "full" (default) - scan PHP files and background-parse user files
# "composer" - use Composer classmap, self-scan on fallback
# "self" - always self-scan, ignore Composer classmap
# "none" - no proactive scanning, Composer classmap only
# strategy = "full"
```

The file is optional. When absent, all settings use their defaults. New settings will be added as features land. Unknown keys are silently ignored, so the file is forward-compatible.

### Indexing Strategy

By default, PHPantom trusts Composer's autoloader to determine which classes exist in your project. This is intentional: it means completions, diagnostics, and go-to-definition reflect what your code will actually see at runtime. Classes that aren't autoloadable don't appear, because using them would be an error.
By default, PHPantom builds a full workspace index: it discovers PHP files, then background-parses user files to populate symbol maps and the reference candidate index. This gives complete cross-file references, implementation lookup, and workspace-wide navigation without per-feature scanning.

The `strategy` setting controls this behaviour:

| Strategy | Behaviour |
| --- | --- |
| `"composer"` (default) | Use Composer's classmap when available, self-scan to fill gaps. Results match what `composer dump-autoload` knows about. |
| `"full"` (default) | Scan PHP files, then background-parse user files to populate symbol and reference indexes. |
| `"composer"` | Use Composer's classmap when available, self-scan to fill gaps. Results stay closer to what `composer dump-autoload` knows about. |
| `"self"` | Ignore Composer's classmap entirely and scan every PHP file in the workspace. Discovers all classes regardless of autoloading. |
| `"none"` | Use only Composer's classmap with no fallback scanning. The most conservative option. |

Most projects should leave this at the default. Change it to `"self"` if your project loads classes outside of Composer (custom autoloaders, `require_once`, legacy inclusion patterns). Be aware that `"self"` will also surface vendor-internal classes and potential duplicates that Composer's autoloader would never load.
Most projects should leave this at the default. Change it to `"composer"` or `"none"` only if you want a lighter or more Composer-constrained index.

### Classes from other files are not found

PHPantom resolves cross-file classes through Composer's autoloading rules (PSR-4 mappings and the generated classmap). If a class exists in your project but PHPantom reports it as unknown, the most common causes are:
PHPantom resolves cross-file classes through the full workspace index by default. If a class exists in your project but PHPantom reports it as unknown, the most common causes are:

1. **The class isn't Composer-autoloadable.** If your project loads classes via `require_once`, `include`, or a custom autoloader alongside Composer, those classes won't be discovered by default. Set `strategy = "self"` in `.phpantom.toml` to scan all files.
1. **The file is excluded from the workspace walk.** Check ignored directories and `.gitignore` rules. If you explicitly set `strategy = "composer"` or `"none"`, classes outside Composer's autoload rules may be skipped.

2. **Composer's classmap is stale.** Run `composer dump-autoload` to regenerate it. PHPantom reads the classmap at startup.

Expand Down
35 changes: 20 additions & 15 deletions docs/todo/indexing.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,26 @@ Four indexing strategies, selectable via `.phpantom.toml`:

```toml
[indexing]
# "composer" (default) - merged classmap + self-scan
# "self" - always self-scan, ignore composer classmap
# "full" - background-parse all project files for rich intelligence
# "none" - no proactive scanning
strategy = "composer"
# "full" (default) - background-parse all project files for rich intelligence
# "composer" - merged classmap + self-scan
# "self" - always self-scan, ignore composer classmap
# "none" - no proactive scanning
strategy = "full"
```

### `"composer"` (default)
### `"full"` (default)

Background-parse user PHP files for rich intelligence after discovery.
This is the zero-config experience and populates the symbol/reference
indexes used by workspace-wide navigation.

### `"composer"`

Merged classmap + self-scan. Load Composer's classmap (if it exists)
as a skip set, then self-scan all PSR-4 and vendor directories for
anything the classmap missed. Whatever the classmap already covers is
a free performance win; whatever it's missing, we find ourselves. No
completeness heuristic needed. This is the zero-config experience.
completeness heuristic needed.

### `"self"`

Expand Down Expand Up @@ -246,7 +252,8 @@ scanning, and complete completion item detail.

### Trigger

When `strategy = "full"` is set in `.phpantom.toml`.
By default. Users can opt out with `strategy = "composer"`, `"self"`, or
`"none"` in `.phpantom.toml`.

### Design: self + second pass

Expand Down Expand Up @@ -299,9 +306,9 @@ Each stage improves on the last without blocking the previous one.

Currently we store `ClassInfo`, `FunctionInfo`, and `SymbolMap`
structs that are not as lean as they could be. For a 21K-file
codebase, full indexing will use meaningful RAM. This is acceptable
because it's an opt-in mode, but we should profile and trim struct
sizes over time. The aim is to stay under 512 MB for a full project.
codebase, full indexing will use meaningful RAM. Since full indexing is
the default, we should profile and trim struct sizes over time. The aim
is to stay under 512 MB for a full project.

The performance prerequisites above (P1 `Arc<ClassInfo>`,
`Arc<String>`, `Arc<SymbolMap>`) directly reduce memory usage by
Expand All @@ -315,11 +322,9 @@ With the full index populated, `workspace/symbol` becomes a simple
filter over the uri_classes_index and global_functions maps. No additional
infrastructure needed.

In other modes, workspace symbols still works but only returns results
When full indexing is disabled, workspace symbols still works but only returns results
from already-parsed files (opened files, on-demand resolutions, stubs).
When the user invokes workspace symbols outside of full mode, show a
one-time hint suggesting they enable `strategy = "full"` in
`.phpantom.toml` for complete coverage.
Complete coverage requires the default `strategy = "full"`.

---

Expand Down
23 changes: 12 additions & 11 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,13 +301,14 @@ impl PhpcsConfig {
pub struct IndexingConfig {
/// The indexing strategy.
///
/// - `"composer"` (default) — use Composer's classmap when available,
/// - `"full"` (default) — same discovery as `"self"`, then
/// background-parse every user PHP file to populate symbol and
/// reference indexes.
/// - `"composer"` — use Composer's classmap when available,
/// fall back to self-scan when it is missing or incomplete.
/// - `"self"` — scan every PHP file under the workspace root,
/// ignoring Composer's generated classmap and PSR-4 mappings.
/// Vendor packages are still scanned via `installed.json`.
/// - `"full"` — background-parse every PHP file for rich intelligence
/// (not yet implemented, treated as `"self"` for now).
/// - `"none"` — no proactive scanning. Still uses Composer's classmap
/// if present, still resolves on demand, but never falls back to
/// self-scan.
Expand All @@ -323,20 +324,20 @@ impl IndexingConfig {
/// The indexing strategy that controls class discovery behaviour.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IndexingStrategy {
/// Background-parse every PHP file for rich intelligence.
#[default]
Full,
/// Merged classmap + self-scan. Load Composer's classmap (if it
/// exists) as a skip set, then self-scan all PSR-4 and vendor
/// directories for anything the classmap missed. Whatever the
/// classmap already covers is a free performance win; whatever it's
/// missing, we find ourselves. No completeness heuristic needed.
#[default]
Composer,
/// Scan every PHP file under the workspace root, ignoring
/// Composer's generated classmap and PSR-4 mappings entirely.
/// The vendor directory is scanned separately (via
/// `installed.json`) since it is typically gitignored.
SelfScan,
/// Background-parse every PHP file for rich intelligence.
Full,
/// No proactive scanning. Uses Composer's classmap if present but
/// never self-scans to fill gaps.
None,
Expand Down Expand Up @@ -546,7 +547,7 @@ mod tests {
assert!(!config.diagnostics.unresolved_member_access_enabled());
assert!(!config.diagnostics.extra_arguments_enabled());
assert!(!config.diagnostics.report_magic_properties_enabled());
assert_eq!(config.indexing.strategy(), IndexingStrategy::Composer);
assert_eq!(config.indexing.strategy(), IndexingStrategy::Full);
assert!(config.formatting.php_cs_fixer.is_none());
assert!(config.formatting.phpcbf.is_none());
assert!(config.formatting.timeout.is_none());
Expand Down Expand Up @@ -575,7 +576,7 @@ mod tests {
assert!(!config.diagnostics.unresolved_member_access_enabled());
assert!(!config.diagnostics.extra_arguments_enabled());
assert!(!config.diagnostics.report_magic_properties_enabled());
assert_eq!(config.indexing.strategy(), IndexingStrategy::Composer);
assert_eq!(config.indexing.strategy(), IndexingStrategy::Full);
assert!(config.formatting.php_cs_fixer.is_none());
assert!(config.formatting.phpcbf.is_none());
assert!(config.phpstan.command.is_none());
Expand All @@ -593,7 +594,7 @@ mod tests {
assert!(!config.diagnostics.unresolved_member_access_enabled());
assert!(!config.diagnostics.extra_arguments_enabled());
assert!(!config.diagnostics.report_magic_properties_enabled());
assert_eq!(config.indexing.strategy(), IndexingStrategy::Composer);
assert_eq!(config.indexing.strategy(), IndexingStrategy::Full);
assert!(config.formatting.php_cs_fixer.is_none());
assert!(config.formatting.phpcbf.is_none());
assert!(config.phpstan.command.is_none());
Expand Down Expand Up @@ -865,12 +866,12 @@ analyze-timeout = 45000
}

#[test]
fn indexing_strategy_defaults_to_composer() {
fn indexing_strategy_defaults_to_full() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(CONFIG_FILE_NAME);
std::fs::write(&path, "[indexing]\n").unwrap();
let config = load_config(dir.path()).unwrap();
assert_eq!(config.indexing.strategy(), IndexingStrategy::Composer);
assert_eq!(config.indexing.strategy(), IndexingStrategy::Full);
}

#[test]
Expand Down
Loading
Loading