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
16 changes: 16 additions & 0 deletions config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,22 @@
"none"
],
"default": "full"
},
"exclude": {
"type": "array",
"items": {
"type": "string"
},
"description": "Paths the workspace scanners skip, in gitignore syntax relative to the workspace root: a bare name matches at any depth, a pattern containing / anchors to the root, a trailing / restricts to directories, and a leading ! re-includes. Applies to background discovery only; files opened in the editor are always served.",
"default": []
},
"extensions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Extra file extensions (without the dot) treated as PHP source during workspace discovery, e.g. [\"module\", \"inc\", \"theme\"] for Drupal. .php is always included.",
"default": []
}
}
},
Expand Down
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Indexing excludes and extra PHP extensions.** `.phpantom.toml` now supports `[indexing] exclude` — gitignore-style patterns relative to the workspace root that background discovery skips (generated code, test fixtures, upload directories) — and `[indexing] extensions`, extra file extensions treated as PHP source (e.g. `["module", "inc", "theme"]` for Drupal), with matching file watchers so edits to those files refresh the index. Excludes apply to every workspace scanner, including the Drupal web-root scan; files opened in the editor are always served regardless. Contributed by @syntlyx.

- **Config return type inference.** `config('database.default')`, `Config::get('app.name')`, and `$repository->get('mail.from')` now infer their return type from the project's `config/*.php` files. Scalar values resolve to their base type (`string`, `int`, `bool`), `env()` defaults resolve through their fallback argument, and nested arrays resolve to array shapes with typed keys. Framework default configs from `vendor/laravel/framework/config/` fill in any keys the project's own config file leaves unset, so a partially published `config/app.php` still resolves the framework defaults it does not override. Parsed config trees are cached and invalidated when config files change. Contributed by @calebdw.
- **Semantic token modes.** `.phpantom.toml` now supports `[semantic_tokens] mode = "contextual" | "full" | "off"`. The default `contextual` mode emits only context-sensitive highlighting that complements editor syntax grammars, while `full` keeps the previous broad semantic-token stream and `off` disables semantic tokens. Contributed by @calebdw.
- **`@phpstan-ignore` identifiers are highlighted and completed.** PHPStan ignore comments now highlight the `@phpstan-ignore` tag and each listed error identifier in both docblocks and ordinary `//` comments. Identifier completion works inside the comma-separated ignore list, using PHPStan diagnostic codes already seen in the current file while staying out of per-code parenthesized reasons. Contributed by @calebdw.
Expand Down Expand Up @@ -72,6 +74,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Drupal test base classes resolve.** Module tests extend `Drupal\Tests\UnitTestCase`, `Drupal\KernelTests\KernelTestBase`, and friends, which live under `web/core/tests/` — a directory the Drupal scanner used to skip by name, so every module test reported `Class 'Drupal\Tests\UnitTestCase' not found`. Test directories are now indexed by default; projects that want a smaller index can trim them with the new `[indexing] exclude` setting. Contributed by @syntlyx.
- **Vendor files under a symlinked workspace root classify correctly.** On a workspace whose real path goes through a symlink (macOS `/var` → `/private/var`, link-farm checkouts), vendor classes were classified as project code: hover lost the package provenance badge, completion ranked them as project symbols, and vendor service providers were scanned as if they were user code. The vendor directory's canonical path is now matched alongside the raw one. The `analyse` CLI had the mirror-image bug — it compared canonicalized file paths against the raw vendor path, so `analyse` walked and reported on the whole vendor tree; it now skips vendor regardless of symlinks. Contributed by @syntlyx.
- **`parent::SOME_CONSTANT` resolves to a type.** A class constant reached through the `parent` keyword produced no type at all, so hover on it was blank and anything derived from it lost the value, while the same constant reached through `self`, `static`, or the class name resolved normally. Constants inherited further up the chain resolve through `parent::` too.
- **A template parameter bound only by the argument it type-checks no longer flags a false positive.** PHPUnit's `assertSame(url('/login'), $x)` (and any other call where a `@template` is bound solely by the parameter being checked, such as `assertSame`'s `$expected`) could report a type mismatch: the substituted parameter type is derived from resolving that exact argument, so comparing the argument to it again is circular and, when the two resolution passes disagree on an ambiguous expression, produced a spurious diagnostic. Such a parameter is no longer checked against its own argument.
- **`self`, `static`, and `parent` in a parameter type resolve to a real class.** A method declared `canChangeTo(self $next)` used to be checked against the literal keyword, so passing an instance of the declaring class was reported as "expects self, got State". The keywords now resolve wherever the call is made from, including through a property (`$this->state->canChangeTo(State::B)`), where the enclosing class is not the one declaring the method. `self` on an inherited method binds to the class that declares it, so a parent instance is still accepted when the method is called on a subclass, and a `parent` parameter is now checked instead of skipped. Mismatches name the class the keyword resolves to rather than the keyword.
Expand Down
14 changes: 11 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,17 @@ message = "^Call to deprecated function some_legacy_helper\\(\\)"

### `[indexing]`

| Key | Type | Default | Description |
| ---------- | ------ | -------- | ----------- |
| `strategy` | string | `"full"` | Class discovery strategy: `"full"`, `"composer"`, `"self"`, or `"none"`. See [Indexing Strategy](#indexing-strategy) below. |
| Key | Type | Default | Description |
| ------------ | -------- | -------- | ----------- |
| `strategy` | string | `"full"` | Class discovery strategy: `"full"`, `"composer"`, `"self"`, or `"none"`. See [Indexing Strategy](#indexing-strategy) below. |
| `exclude` | string[] | `[]` | Paths the workspace scanners skip, in gitignore syntax relative to the workspace root: a bare name matches at any depth, a pattern containing `/` anchors to the root, a trailing `/` restricts to directories, and a leading `!` re-includes. Applies to background discovery only — files opened in the editor are always served. |
| `extensions` | string[] | `[]` | Extra file extensions (without the dot) treated as PHP source during workspace discovery, e.g. `["module", "inc", "theme"]` for Drupal. `.php` is always included. |

```toml
[indexing]
exclude = ["generated", "web/sites/default/files"]
extensions = ["module", "install", "theme"]
```

### `[semantic_tokens]`

Expand Down
2 changes: 1 addition & 1 deletion docs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ unlikely to move the needle for most users.
| X3 | Completion item detail on demand (`completionItem/resolve`) | Medium | Medium |
| X7 | [Recency tracking](todo/indexing.md#x7-recency-tracking) | Medium | Medium |
| X2 | Parallel file processing — remaining work | Low-Medium | Medium |
| X9 | [Honor editor file excludes and PHP associations during indexing](todo/indexing.md#x9-honor-editor-file-excludes-and-php-associations-during-indexing) | Low-Medium | Medium |
| X9 | [Forward editor file excludes and PHP associations to the server](todo/indexing.md#x9-forward-editor-file-excludes-and-php-associations-to-the-server) | Low-Medium | Medium |
| X6 | Disk cache (evaluate later) | Medium | High |
| | **[Inline Completion](todo/inline-completion.md)** | | |
| N1 | Template engine (type-aware snippets) | Medium | High |
Expand Down
39 changes: 12 additions & 27 deletions docs/todo/indexing.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,43 +343,28 @@ ready to implement.

---

## X9. Honor editor file excludes and PHP associations during indexing
## X9. Forward editor file excludes and PHP associations to the server

**Impact: Low-Medium · Effort: Medium**

This task spans both the server and the IDE plugins. The server side
teaches the directory walkers to honor a generic list of exclude globs
and extra PHP extensions. The client side (each editor extension) must
gather the editor's effective `files.exclude` / `files.associations`
and forward them to the server, since only the extension has access to
those editor settings.

The workspace scanners discover files by the `.php` extension and do
not consult any exclude list. Two pieces of information the editor
already has are ignored:

- **`files.exclude` (and a PHPantom-specific exclude glob).** Large
generated/vendored directories that the user has hidden from the
editor are still walked and parsed by the indexer. Skipping them
would cut startup work and avoid indexing irrelevant symbols.
- **`files.associations`.** Files mapped to PHP under a non-`.php`
extension (e.g. `.module`, `.inc`, `.theme` in Drupal) are not
discovered by the byte-level scanners, so their classes/functions
are missing from the index. Note that *open* associated files
already work, because VS Code reports them with the `php` language
id and the client's document selector matches on language id, not
extension. Only background discovery is affected.
The server side of this task has shipped: the directory walkers honor
`[indexing] exclude` (gitignore-style patterns) and `[indexing]
extensions` (extra PHP extensions) from `.phpantom.toml`. What remains
is the client side: each editor extension must gather the editor's
effective `files.exclude` / `files.associations` and forward them to
the server, since only the extension has access to those editor
settings. Today a user has to mirror those editor settings into
`.phpantom.toml` by hand.

### Approach

The client passes the effective exclude globs and the set of
PHP-associated extensions to the server (via `initializationOptions`,
or by responding to `workspace/configuration` the way Intelephense's
middleware merges VS Code's native `files.exclude` /
`files.associations` into the server config). The directory walkers in
`classmap_scanner.rs` and `util.rs` consult the exclude globs before
descending, and treat the extra associated extensions as PHP when
collecting candidate files.
`files.associations` into the server config). The server merges them
into the same compiled filters the `.phpantom.toml` keys feed
(`classmap_scanner::IndexFilters`).

### Editor-agnostic note

Expand Down
13 changes: 10 additions & 3 deletions src/analyse/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pub async fn run(options: AnalyseOptions) -> i32 {
// calls are no-ops.
let backend = Backend::new_headless();
*backend.workspace_root().write() = Some(root.to_path_buf());
*backend.workspace.config.lock() = cfg.clone();
backend.set_config(cfg.clone());

let composer_package = composer::read_composer_package(root);

Expand Down Expand Up @@ -700,8 +700,15 @@ pub(crate) fn discover_user_files(
continue;
}

let skip_vendor = if filter_overlaps_psr4 {
vendor_dirs.clone()
let skip_vendor: Vec<PathBuf> = if filter_overlaps_psr4 {
// The walker compares canonicalized entry paths below, so
// canonicalize the vendor dirs too — otherwise a symlinked
// workspace root (macOS `/var` → `/private/var`, monorepo
// link farms) never matches and vendor is walked anyway.
vendor_dirs
.iter()
.map(|v| v.canonicalize().unwrap_or_else(|_| v.clone()))
.collect()
} else {
// User explicitly targeted this path — don't skip vendor
// subdirectories within it.
Expand Down
Loading
Loading