Skip to content

Commit df15284

Browse files
committed
Log stub version and clean up stub handeling
1 parent 1cf94fe commit df15284

10 files changed

Lines changed: 64 additions & 89 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ jobs:
5858
- uses: shivammathur/setup-php@v2
5959
with:
6060
php-version: "8.4"
61+
ini-values: zend.assertions=1
6162
- run: php -l example.php
63+
- run: php -d zend.assertions=1 example.php
6264

6365
benchmark:
6466
name: Benchmark

.github/workflows/release.yml

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,6 @@ jobs:
5252
with:
5353
key: ${{ matrix.target }}
5454

55-
- uses: shivammathur/setup-php@v2
56-
with:
57-
php-version: '8.4'
58-
tools: composer
59-
60-
- name: Cache Composer stubs
61-
uses: actions/cache@v4
62-
with:
63-
path: stubs
64-
key: composer-stubs-${{ hashFiles('composer.lock') }}
65-
restore-keys: composer-stubs-
66-
67-
- run: composer install --no-interaction --prefer-dist
68-
6955
- name: Build
7056
run: cargo build --release --target ${{ matrix.target }}
7157

build.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,21 @@ fn main() {
7676
if let Err(e) = fetch_stubs(&manifest_dir) {
7777
eprintln!("cargo:warning=Failed to fetch stubs from GitHub: {}", e);
7878
eprintln!("cargo:warning=Building without stubs (network may be unavailable).");
79+
println!("cargo:rustc-env=PHPANTOM_STUBS_VERSION=none");
7980
write_empty_stubs();
8081
return;
8182
}
8283
}
8384

85+
// Emit the stubs version so the binary can log it at runtime.
86+
// fetch_stubs writes a `.version` file next to the extracted stubs.
87+
let version_file = Path::new(&manifest_dir).join("stubs/.version");
88+
let stubs_version = fs::read_to_string(&version_file)
89+
.ok()
90+
.map(|v| v.trim().to_string())
91+
.unwrap_or_else(|| "unknown".to_string());
92+
println!("cargo:rustc-env=PHPANTOM_STUBS_VERSION={}", stubs_version);
93+
8494
let map_content = match fs::read_to_string(&map_path) {
8595
Ok(c) => c,
8696
Err(e) => {
@@ -276,6 +286,11 @@ fn fetch_stubs(manifest_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
276286
}
277287
}
278288

289+
// Record which version was fetched so subsequent builds (that skip
290+
// the download because stubs/ already exists) can still read it.
291+
let version_file = Path::new(manifest_dir).join("stubs/.version");
292+
let _ = fs::write(&version_file, &release.tag_name);
293+
279294
eprintln!(
280295
"cargo:warning=Successfully downloaded phpstorm-stubs {}",
281296
release.tag_name

docs/ARCHITECTURE.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -346,14 +346,15 @@ PHPantom bundles the [JetBrains phpstorm-stubs](https://github.com/JetBrains/php
346346

347347
The `build.rs` script:
348348

349-
1. Reads `stubs/jetbrains/phpstorm-stubs/PhpStormStubsMap.php` — a generated index mapping symbol names to file paths.
350-
2. Emits `stub_map_generated.rs` containing:
349+
1. If `stubs/jetbrains/phpstorm-stubs/` does not exist, fetches the latest release tarball from the JetBrains/phpstorm-stubs GitHub repository and extracts it. This makes `cargo install` and `cargo build` work without any prior setup.
350+
2. Reads `stubs/jetbrains/phpstorm-stubs/PhpStormStubsMap.php` — a generated index mapping symbol names to file paths.
351+
3. Emits `stub_map_generated.rs` containing:
351352
- `STUB_FILES`: an array of `include_str!(...)` calls embedding every referenced PHP file (~502 files, ~8.5MB of source).
352353
- `STUB_CLASS_MAP`: maps class/interface/trait names → index into `STUB_FILES`.
353354
- `STUB_FUNCTION_MAP`: maps function names → index into `STUB_FILES`.
354355
- `STUB_CONSTANT_MAP`: maps constant names → index into `STUB_FILES`.
355356

356-
The build script watches `composer.lock` for changes, so running `composer update` followed by `cargo build` automatically picks up new stub versions.
357+
The stubs are cached in the `stubs/` directory (gitignored). To update to the latest version, delete `stubs/` and rebuild.
357358

358359
### Runtime Lookup — Classes
359360

@@ -464,7 +465,7 @@ User code is never filtered. The `php_version` field in `DocblockCtx` is `None`
464465

465466
### Graceful Degradation
466467

467-
If the stubs aren't installed (e.g. `composer install` hasn't been run), `build.rs` generates empty arrays and the build succeeds. The LSP just won't know about built-in PHP symbols.
468+
If the stubs can't be fetched (e.g. no network access during the build), `build.rs` generates empty arrays and the build succeeds. The LSP just won't know about built-in PHP symbols.
468469

469470
## Inheritance Resolution
470471

docs/BUILDING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ cargo clippy -- -D warnings
3636
cargo clippy --tests -- -D warnings
3737
cargo fmt --check
3838
php -l example.php
39+
php -d zend.assertions=1 example.php
3940
```
4041

41-
All five must pass with zero warnings and zero failures.
42+
All six must pass with zero warnings and zero failures.
4243

4344
### Manual LSP Testing
4445

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5353
- **Concurrent read access to shared state.** All read-heavy maps now use `parking_lot::RwLock` instead of `std::sync::Mutex`, allowing multiple requests to read in parallel.
5454
- **Parallel workspace indexing.** Find References and other workspace-wide operations now parse files across multiple CPU cores. The workspace walk respects `.gitignore` rules instead of hardcoding directory names to skip. Self-scan classmap building, PSR-4 directory scanning, and vendor package scanning now read and scan files in parallel across all CPU cores. The byte-level PHP scanner uses `memchr` SIMD acceleration to skip comments, strings, and heredocs instead of scanning byte-by-byte. Redundant `composer.json` reads during initialization have been eliminated.
5555
- **Merged classmap + self-scan pipeline.** The Composer classmap and the self-scanner are no longer mutually exclusive. PHPantom always loads the Composer classmap (when present) and then self-scans all PSR-4 and vendor directories, skipping files the classmap already covers. Stale or incomplete classmaps are no longer a problem: whatever Composer already knew about is a free performance win, and whatever it missed is discovered automatically. The fragile completeness heuristic that decided whether to trust the classmap has been removed. Monorepo subprojects use the same merged pipeline.
56+
- **Automatic stub fetching.** The build script now downloads phpstorm-stubs from GitHub automatically when the `stubs/` directory is missing. `cargo install` and `cargo build` work without any prior setup. Composer is no longer required to build PHPantom.
5657

5758
### Fixed
5859

docs/SETUP.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ Download the latest binary for your platform from [GitHub Releases](https://gith
1717
See [BUILDING.md](BUILDING.md) for full instructions. Quick version:
1818

1919
```bash
20-
composer install
2120
cargo build --release
2221
# Binary is at target/release/phpantom_lsp
2322
```

docs/todo/external-stubs.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -554,9 +554,8 @@ but it is not a priority.
554554
### Building without embedded stubs
555555

556556
The `build.rs` script already handles a missing `stubs/` directory
557-
gracefully by generating empty arrays. An IDE extension that wants
558-
to distribute PHPantom without embedded stubs simply skips
559-
`composer install` in the PHPantom build directory. The binary
557+
gracefully by generating empty arrays. If the automatic GitHub
558+
fetch fails (e.g. no network access during the build), the binary
560559
compiles and runs normally; it just has no built-in fallback for
561560
PHP standard library symbols.
562561

src/server.rs

Lines changed: 30 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,6 @@ impl Backend {
548548
}
549549

550550
let (mappings, vendor_dir) = composer::parse_composer_json(root);
551-
let mapping_count = mappings.len();
552551

553552
// Parse the raw composer.json once so that build_self_scan_composer
554553
// can reuse it without redundant I/O.
@@ -572,15 +571,10 @@ impl Backend {
572571
.await;
573572
}
574573

575-
let (classmap, classmap_source) = match strategy {
574+
let (classmap, source_label) = match strategy {
576575
IndexingStrategy::None => {
577576
let cm = composer::parse_autoload_classmap(root, &vendor_dir);
578-
let source = if cm.is_empty() {
579-
"none"
580-
} else {
581-
"composer classmap"
582-
};
583-
(cm, source)
577+
(cm, "composer")
584578
}
585579
IndexingStrategy::SelfScan | IndexingStrategy::Full => {
586580
let skip_paths = HashSet::new();
@@ -595,11 +589,6 @@ impl Backend {
595589
}
596590
IndexingStrategy::Composer => {
597591
// ── Merged classmap + self-scan pipeline ─────────────
598-
// Always load the Composer classmap (if it exists) and
599-
// then self-scan with a skip set built from the classmap
600-
// file paths. Whatever the classmap already covers is a
601-
// free performance win; whatever it's missing, we find
602-
// ourselves. No completeness heuristic needed.
603592
let composer_cm = composer::parse_autoload_classmap(root, &vendor_dir);
604593
let skip_paths: HashSet<PathBuf> = composer_cm.values().cloned().collect();
605594
let scan = self.build_self_scan_composer(
@@ -613,16 +602,11 @@ impl Backend {
613602
for (fqcn, path) in scan.classmap {
614603
merged.entry(fqcn).or_insert(path);
615604
}
616-
let source = if skip_paths.is_empty() {
617-
"self-scan"
618-
} else {
619-
"composer classmap + self-scan"
620-
};
621-
(merged, source)
605+
(merged, "composer+scan")
622606
}
623607
};
624608

625-
let classmap_count = classmap.len();
609+
let symbol_count = classmap.len();
626610
*self.classmap.write() = classmap;
627611

628612
// ── Autoload files ──────────────────────────────────────────
@@ -631,25 +615,20 @@ impl Backend {
631615
.await;
632616
}
633617

634-
let autoload_count = self.scan_autoload_files(root, &vendor_dir);
618+
self.scan_autoload_files(root, &vendor_dir);
635619

636-
let func_index_count = self.autoload_function_index.read().len();
637-
let const_index_count = self.autoload_constant_index.read().len();
638-
639-
let index_suffix = if func_index_count > 0 || const_index_count > 0 {
640-
format!(
641-
", {} indexed function(s), {} indexed constant(s)",
642-
func_index_count, const_index_count
643-
)
644-
} else {
645-
String::new()
646-
};
620+
let symbol_count = symbol_count
621+
+ self.autoload_function_index.read().len()
622+
+ self.autoload_constant_index.read().len();
647623

648624
self.log(
649625
MessageType::INFO,
650626
format!(
651-
"PHPantom initialized! PHP {}, {} PSR-4 mapping(s), {} classmap entries ({}), {} autoload file(s){}",
652-
php_version, mapping_count, classmap_count, classmap_source, autoload_count, index_suffix
627+
"PHPantom: PHP {}, {} symbols from {}, stubs {}",
628+
php_version,
629+
symbol_count,
630+
source_label,
631+
crate::stubs::STUBS_VERSION
653632
),
654633
)
655634
.await;
@@ -690,8 +669,6 @@ impl Backend {
690669

691670
// Collect subproject root paths for the skip set.
692671
let mut skip_dirs: HashSet<PathBuf> = HashSet::new();
693-
let mut total_mapping_count = 0usize;
694-
let mut total_autoload_count = 0usize;
695672
let sub_count = subprojects.len();
696673

697674
for (sub_idx, (sub_root, vendor_dir)) in subprojects.iter().enumerate() {
@@ -720,7 +697,6 @@ impl Backend {
720697

721698
// ── PSR-4 mappings ──────────────────────────────────────
722699
let (mappings, _) = composer::parse_composer_json(sub_root);
723-
total_mapping_count += mappings.len();
724700

725701
// Resolve base_path values to absolute paths so that
726702
// resolve_class_path works regardless of workspace_root.
@@ -744,7 +720,7 @@ impl Backend {
744720
self.add_vendor_dir(&vendor_path);
745721

746722
// ── Autoload files ──────────────────────────────────────
747-
total_autoload_count += self.scan_autoload_files(sub_root, vendor_dir);
723+
self.scan_autoload_files(sub_root, vendor_dir);
748724

749725
// ── Merged classmap + self-scan ──────────────────────────
750726
// Load the subproject's Composer classmap as a skip set,
@@ -789,24 +765,18 @@ impl Backend {
789765
}
790766
}
791767

792-
let classmap_count = self.classmap.read().len();
793-
let func_index_count = self.autoload_function_index.read().len();
794-
let const_index_count = self.autoload_constant_index.read().len();
795-
796-
let index_suffix = if func_index_count > 0 || const_index_count > 0 {
797-
format!(
798-
", {} indexed function(s), {} indexed constant(s)",
799-
func_index_count, const_index_count
800-
)
801-
} else {
802-
String::new()
803-
};
768+
let symbol_count = self.classmap.read().len()
769+
+ self.autoload_function_index.read().len()
770+
+ self.autoload_constant_index.read().len();
804771

805772
self.log(
806773
MessageType::INFO,
807774
format!(
808-
"PHPantom initialized! PHP {}, {} subproject(s), {} PSR-4 mapping(s), {} classmap entries, {} autoload file(s){}",
809-
php_version, subprojects.len(), total_mapping_count, classmap_count, total_autoload_count, index_suffix
775+
"PHPantom: PHP {}, {} symbols from {} subprojects, stubs {}",
776+
php_version,
777+
symbol_count,
778+
subprojects.len(),
779+
crate::stubs::STUBS_VERSION
810780
),
811781
)
812782
.await;
@@ -839,26 +809,20 @@ impl Backend {
839809
let scan = classmap_scanner::scan_workspace_fallback_full(root, &skip_dirs);
840810
self.populate_autoload_indices(&scan);
841811

842-
let classmap_count = scan.classmap.len();
812+
let symbol_count = scan.classmap.len();
843813
*self.classmap.write() = scan.classmap;
844814

845-
let func_index_count = self.autoload_function_index.read().len();
846-
let const_index_count = self.autoload_constant_index.read().len();
847-
848-
let index_suffix = if func_index_count > 0 || const_index_count > 0 {
849-
format!(
850-
", {} indexed function(s), {} indexed constant(s)",
851-
func_index_count, const_index_count
852-
)
853-
} else {
854-
String::new()
855-
};
815+
let symbol_count = symbol_count
816+
+ self.autoload_function_index.read().len()
817+
+ self.autoload_constant_index.read().len();
856818

857819
self.log(
858820
MessageType::INFO,
859821
format!(
860-
"PHPantom initialized! PHP {}, {} classmap entries (workspace scan){}",
861-
php_version, classmap_count, index_suffix
822+
"PHPantom: PHP {}, {} symbols from workspace scan, stubs {}",
823+
php_version,
824+
symbol_count,
825+
crate::stubs::STUBS_VERSION
862826
),
863827
)
864828
.await;

src/stubs.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ use std::collections::HashMap;
3636
// Pull in the generated static arrays.
3737
include!(concat!(env!("OUT_DIR"), "/stub_map_generated.rs"));
3838

39+
/// The phpstorm-stubs version that was embedded at build time.
40+
///
41+
/// Set by `build.rs` via `cargo:rustc-env`. Contains the GitHub release
42+
/// tag (e.g. `"v2025.3"`), `"unknown"` when stubs were present but the
43+
/// version file was missing, or `"none"` when stubs could not be fetched.
44+
pub const STUBS_VERSION: &str = env!("PHPANTOM_STUBS_VERSION");
45+
3946
/// Build a lookup table mapping class/interface/trait short names to their
4047
/// embedded PHP source code.
4148
///

0 commit comments

Comments
 (0)