Skip to content

Commit fc4f37e

Browse files
committed
Embedded stubs track upstream master
1 parent 2e155fa commit fc4f37e

9 files changed

Lines changed: 543 additions & 104 deletions

File tree

build.rs

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,16 @@ use flate2::read::GzDecoder;
4242
use serde::Deserialize;
4343
use tar::Archive;
4444

45-
const GITHUB_REPO: &str = "JetBrains/phpstorm-stubs";
45+
/// Direct tarball URL for the `master` branch.
46+
///
47+
/// Unlike the releases API endpoint this does not require authentication
48+
/// and always points at the latest commit. PHPStan uses the same
49+
/// approach (see their `update-phpstorm-stubs.yml` workflow).
50+
const MASTER_TARBALL_URL: &str =
51+
"https://github.com/JetBrains/phpstorm-stubs/archive/refs/heads/master.tar.gz";
52+
53+
/// GitHub API URL to query the latest commit SHA on `master`.
54+
const MASTER_SHA_URL: &str = "https://api.github.com/repos/JetBrains/phpstorm-stubs/commits/master";
4655

4756
/// Relative path from the crate root to the stubs map file.
4857
const MAP_FILE: &str = "stubs/jetbrains/phpstorm-stubs/PhpStormStubsMap.php";
@@ -51,11 +60,10 @@ const MAP_FILE: &str = "stubs/jetbrains/phpstorm-stubs/PhpStormStubsMap.php";
5160
/// relative paths found in the map file).
5261
const STUBS_DIR: &str = "stubs/jetbrains/phpstorm-stubs";
5362

54-
/// GitHub API response for latest release.
63+
/// GitHub API response for a single commit (only the fields we need).
5564
#[derive(Deserialize)]
56-
struct GitHubRelease {
57-
tag_name: String,
58-
tarball_url: String,
65+
struct GitHubCommit {
66+
sha: String,
5967
}
6068

6169
fn main() {
@@ -229,23 +237,26 @@ fn main() {
229237
}
230238

231239
fn fetch_stubs(manifest_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
232-
let api_url = format!(
233-
"https://api.github.com/repos/{}/releases/latest",
234-
GITHUB_REPO
235-
);
236-
237-
let mut response = ureq::get(&api_url)
240+
// Try to fetch the commit SHA for a meaningful version string.
241+
// This is best-effort — if it fails we still download the tarball.
242+
let commit_sha = match ureq::get(MASTER_SHA_URL)
238243
.header("User-Agent", "phpantom-lsp-build")
239244
.header("Accept", "application/vnd.github.v3+json")
240-
.call()?;
245+
.call()
246+
{
247+
Ok(mut resp) => {
248+
let commit: GitHubCommit = resp.body_mut().read_json()?;
249+
commit.sha
250+
}
251+
Err(_) => String::from("unknown"),
252+
};
241253

242-
let release: GitHubRelease = response.body_mut().read_json()?;
243254
eprintln!(
244-
"cargo:warning=Downloading phpstorm-stubs {}",
245-
release.tag_name
255+
"cargo:warning=Downloading phpstorm-stubs master ({})",
256+
&commit_sha[..commit_sha.len().min(10)]
246257
);
247258

248-
let mut tarball_response = ureq::get(&release.tarball_url)
259+
let mut tarball_response = ureq::get(MASTER_TARBALL_URL)
249260
.header("User-Agent", "phpantom-lsp-build")
250261
.call()?;
251262

@@ -290,11 +301,12 @@ fn fetch_stubs(manifest_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
290301
// Record which version was fetched so subsequent builds (that skip
291302
// the download because stubs/ already exists) can still read it.
292303
let version_file = Path::new(manifest_dir).join("stubs/.version");
293-
let _ = fs::write(&version_file, &release.tag_name);
304+
let version_label = format!("master@{}", &commit_sha[..commit_sha.len().min(10)]);
305+
let _ = fs::write(&version_file, &version_label);
294306

295307
eprintln!(
296308
"cargo:warning=Successfully downloaded phpstorm-stubs {}",
297-
release.tag_name
309+
version_label
298310
);
299311
Ok(())
300312
}

docs/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3333

3434
### Changed
3535

36+
- **Embedded stubs track upstream master.** The bundled phpstorm-stubs are now pulled from the `master` branch instead of the latest GitHub release, matching what PHPStan does. This brings in upstream fixes and new PHP version annotations weeks or months before a formal release.
37+
- **Version-aware stub types.** `#[LanguageLevelTypeAware]` attributes in phpstorm-stubs are now resolved against the project's PHP version. Functions, methods, parameters, and properties whose types changed across PHP versions (e.g. `int|false` in 7.x becoming `int` in 8.0) now show the correct type for your version. This eliminates false-positive diagnostics and incorrect completions from stale type annotations.
38+
3639
- **Pull diagnostics.** Diagnostics are now delivered via the LSP 3.17 pull model (`textDocument/diagnostic`) when the editor supports it. The editor requests diagnostics only for visible files, and cross-file invalidation uses `workspace/diagnostic/refresh` instead of recomputing every open tab. Clients without pull support fall back to the previous push model automatically.
3740
- **Class name completion ranking.** Completions now rank by match quality first (exact match, then starts-with, then substring), so typing `Order` puts `Order` above `OrderLine` above `CheckOrderFlowJob` regardless of where the class comes from. Within each match quality group, use-imported and same-namespace classes appear first, followed by everything else sorted by namespace affinity (classes from heavily-imported namespaces rank higher).
3841
- **Use-import completion.** Same-namespace classes no longer appear in `use` statement completions (PHP auto-resolves them without an import). Classes that are already imported are filtered out. Namespace affinity still ranks the remaining candidates.

docs/todo.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,6 @@ within the same impact tier.
1919

2020
# Scheduled Sprints
2121

22-
## Sprint 3 — Diagnostics quality (finishing touches)
23-
24-
| # | Item | Impact | Effort |
25-
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
26-
| E0 | [Switch embedded stubs to master + LanguageLevelTypeAware](todo/external-stubs.md#e0-switch-embedded-stubs-to-master-and-apply-languageleveltypeaware-patches) | High | Low |
27-
2822
## Sprint 4 — Refactoring toolkit
2923

3024
| # | Item | Impact | Effort |

docs/todo/external-stubs.md

Lines changed: 0 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -16,81 +16,6 @@ within the same impact tier.
1616

1717
---
1818

19-
## E0. Switch embedded stubs to `master` and apply `LanguageLevelTypeAware` patches
20-
21-
**Impact: High · Effort: Low**
22-
23-
The embedded phpstorm-stubs are pinned to the latest GitHub **release**,
24-
which ships infrequently (the current release is about six months old).
25-
In the meantime the stubs `master` branch receives ongoing fixes and new
26-
PHP version additions. Using a stale release causes false-positive
27-
diagnostics and missing completions for PHP features that were already
28-
corrected upstream.
29-
30-
PHPStan takes `dev-master` of the stubs on a weekly automated schedule
31-
(see their `update-phpstorm-stubs.yml` workflow). We should do the same.
32-
33-
### Part 1 — Pull `master` instead of `latest release`
34-
35-
`build.rs` currently calls the GitHub API for the latest release and
36-
downloads its tarball. Change `fetch_stubs` to clone or download the
37-
`master` branch HEAD instead:
38-
39-
- Replace the `releases/latest` API call with a direct tarball download
40-
of `master`:
41-
`https://github.com/JetBrains/phpstorm-stubs/archive/refs/heads/master.tar.gz`
42-
- No API call needed — the tarball URL is stable and does not require
43-
authentication.
44-
- Record the commit SHA (from the archive's embedded git metadata or a
45-
separate HEAD query) in `stubs/.version` so `STUBS_VERSION` remains
46-
meaningful.
47-
- The rest of `build.rs` (map parsing, code generation) is unchanged.
48-
49-
### Part 2 — Patch `LanguageLevelTypeAware` overloads
50-
51-
phpstorm-stubs uses `#[LanguageLevelTypeAware]` to annotate functions
52-
and parameters whose types differ by PHP version. When two overloads of
53-
the same function exist (one for an older PHP version, one for a newer),
54-
PHPantom's parser can end up with the wrong variant — or both, causing
55-
duplicate entries.
56-
57-
The `PhpStormStubsElementAvailable` filter (`parser/mod.rs`) already
58-
handles this for version-gated elements. However `LanguageLevelTypeAware`
59-
on return types and parameter types is not yet processed: the attribute
60-
is present in the stub but PHPantom reads the raw type annotation
61-
(which may be the oldest-PHP fallback string) instead of selecting the
62-
correct variant for the target PHP version.
63-
64-
Inspect how the attribute is used in the stubs:
65-
66-
```
67-
#[LanguageLevelTypeAware(['8.0' => 'string|false'], default: 'string')]
68-
```
69-
70-
The attribute's first argument is an array mapping minimum PHP version
71-
strings to the type annotation to use from that version onward. The
72-
`default` named argument is the fallback for older versions.
73-
74-
**Implementation:** In `parser/mod.rs`, after the existing
75-
`PhpStormStubsElementAvailable` filter, add a pass that reads
76-
`LanguageLevelTypeAware` attributes on return types and parameters,
77-
selects the appropriate type string for the configured `php_version`,
78-
and substitutes it in place of the raw annotation. This mirrors the
79-
logic PHPStan implements in their stub-loading layer.
80-
81-
If fully implementing the attribute parsing is complex, a pragmatic
82-
first step is to always select the **newest** type variant (highest
83-
version key) rather than version-matching. This eliminates the most
84-
common false positives (stubs that gained union types or `never` return
85-
types in recent PHP versions) without requiring per-project version
86-
configuration.
87-
88-
### Prerequisites
89-
90-
None. Both changes are isolated to `build.rs` and `parser/mod.rs`.
91-
92-
---
93-
9419
## Current state
9520

9621
PHPantom embeds JetBrains phpstorm-stubs at compile time via

src/parser/classes.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1522,10 +1522,23 @@ impl Backend {
15221522
php_version,
15231523
doc_ctx,
15241524
);
1525-
let native_return_type = method
1525+
let raw_native_return_type = method
15261526
.return_type_hint
15271527
.as_ref()
15281528
.map(|rth| extract_hint_string(&rth.hint));
1529+
1530+
// Check for a #[LanguageLevelTypeAware] override on the
1531+
// method's return type. When present, it replaces the
1532+
// native type hint with the version-appropriate string.
1533+
let native_return_type = if let Some(ctx) = doc_ctx
1534+
&& let Some(ver) = ctx.php_version
1535+
&& let Some(override_type) =
1536+
super::extract_language_level_type(&method.attribute_lists, ctx, ver)
1537+
{
1538+
Some(override_type)
1539+
} else {
1540+
raw_native_return_type
1541+
};
15291542
let is_static = method.modifiers.iter().any(|m| m.is_static());
15301543
let visibility = extract_visibility(method.modifiers.iter());
15311544

@@ -1875,6 +1888,21 @@ impl Backend {
18751888
Property::Hooked(h) => Some(&h.attribute_lists),
18761889
};
18771890

1891+
// Apply #[LanguageLevelTypeAware] override to property types.
1892+
// When present, the attribute's version-appropriate type
1893+
// string replaces the native type hint.
1894+
if let Some(ctx) = doc_ctx
1895+
&& let Some(ver) = ctx.php_version
1896+
&& let Some(attr_lists) = prop_attr_lists
1897+
&& let Some(override_type) =
1898+
super::extract_language_level_type(attr_lists, ctx, ver)
1899+
{
1900+
for prop in &mut prop_infos {
1901+
prop.type_hint = Some(override_type.clone());
1902+
prop.native_type_hint = Some(override_type.clone());
1903+
}
1904+
}
1905+
18781906
// Apply PHPDoc `@var` override, `@deprecated`, `@see`, and
18791907
// description for each property.
18801908
if let Some(ctx) = doc_ctx

src/parser/functions.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,24 @@ impl Backend {
4848
php_version,
4949
doc_ctx,
5050
);
51-
let native_return_type = func
51+
let raw_native_return_type = func
5252
.return_type_hint
5353
.as_ref()
5454
.map(|rth| extract_hint_string(&rth.hint));
5555

56+
// Check for a #[LanguageLevelTypeAware] override on the
57+
// function's return type. When present, it replaces the
58+
// native type hint with the version-appropriate string.
59+
let native_return_type = if let Some(ctx) = doc_ctx
60+
&& let Some(ver) = ctx.php_version
61+
&& let Some(override_type) =
62+
super::extract_language_level_type(&func.attribute_lists, ctx, ver)
63+
{
64+
Some(override_type)
65+
} else {
66+
raw_native_return_type
67+
};
68+
5669
// Apply PHPDoc `@return` override for the function.
5770
// Also extract PHPStan conditional return types,
5871
// type assertion annotations, and `@deprecated` if present.

0 commit comments

Comments
 (0)