Skip to content

Commit 2034873

Browse files
committed
Make tests work without stubs
1 parent f871ab7 commit 2034873

4 files changed

Lines changed: 329 additions & 38 deletions

File tree

build.rs

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@
88
//! indices into the embedded file array.
99
//!
1010
//! The generated file is consumed by `src/stubs.rs` at compile time.
11+
//!
12+
//! ## Re-run strategy
13+
//!
14+
//! The `stubs/` directory is gitignored, so Cargo's default "re-run when
15+
//! any package file changes" behaviour does not notice when
16+
//! `composer install` creates it. Explicit `rerun-if-changed` on paths
17+
//! inside `stubs/` also fails when the directory doesn't exist yet.
18+
//!
19+
//! Instead we watch the project root directory (`.`). Its mtime changes
20+
//! whenever a direct child like `stubs/` is created or removed. We also
21+
//! watch `build.rs` and `composer.lock` for targeted rebuilds.
22+
//!
23+
//! To avoid unnecessary recompilation of the main crate we compare the
24+
//! newly generated content against the existing output file and only write
25+
//! when something actually changed. This way `rustc` sees a stable mtime
26+
//! on `stub_map_generated.rs` and skips recompilation when the stubs
27+
//! haven't changed.
1128
1229
use std::collections::{BTreeMap, BTreeSet};
1330
use std::env;
@@ -22,12 +39,14 @@ const MAP_FILE: &str = "stubs/jetbrains/phpstorm-stubs/PhpStormStubsMap.php";
2239
const STUBS_DIR: &str = "stubs/jetbrains/phpstorm-stubs";
2340

2441
fn main() {
25-
// Tell Cargo to re-run this script when dependencies change.
26-
// We watch composer.lock (rather than PhpStormStubsMap.php directly)
27-
// because that's the file that changes when `composer update` pulls
28-
// a new version of phpstorm-stubs — more natural for PHP developers.
29-
println!("cargo:rerun-if-changed=composer.lock");
42+
// Watch the project root directory so that creating/removing `stubs/`
43+
// (which is gitignored) is detected via the directory mtime change.
44+
// Without this, Cargo's default "any package file" check ignores
45+
// gitignored paths, and explicit watches on non-existent paths don't
46+
// reliably trigger when they first appear.
47+
println!("cargo:rerun-if-changed=.");
3048
println!("cargo:rerun-if-changed=build.rs");
49+
println!("cargo:rerun-if-changed=composer.lock");
3150

3251
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
3352
let map_path = Path::new(&manifest_dir).join(MAP_FILE);
@@ -41,7 +60,13 @@ fn main() {
4160
"cargo:warning=Could not read PhpStormStubsMap.php ({}); generating empty stub index",
4261
e
4362
);
44-
write_empty_generated_file();
63+
let content = concat!(
64+
"pub(crate) static STUB_FILES: [&str; 0] = [];\n",
65+
"pub(crate) static STUB_CLASS_MAP: [(&str, usize); 0] = [];\n",
66+
"pub(crate) static STUB_FUNCTION_MAP: [(&str, usize); 0] = [];\n",
67+
"pub(crate) static STUB_CONSTANT_MAP: [(&str, usize); 0] = [];\n",
68+
);
69+
write_if_changed(content);
4570
return;
4671
}
4772
};
@@ -82,9 +107,6 @@ fn main() {
82107

83108
// ── Generate Rust source ────────────────────────────────────────────
84109

85-
let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
86-
let dest_path = Path::new(&out_dir).join("stub_map_generated.rs");
87-
88110
let mut out = String::with_capacity(512 * 1024);
89111

90112
// 1. The embedded file array.
@@ -170,7 +192,7 @@ fn main() {
170192
}
171193
out.push_str("];\n");
172194

173-
fs::write(&dest_path, &out).expect("Failed to write generated stub map");
195+
write_if_changed(&out);
174196
}
175197

176198
/// Parse one of the `const CLASSES = array(...)`, `const FUNCTIONS = array(...)`,
@@ -223,15 +245,19 @@ fn escape(s: &str) -> String {
223245
s.replace('\\', "\\\\").replace('"', "\\\"")
224246
}
225247

226-
/// Write an empty generated file when stubs are not available.
227-
fn write_empty_generated_file() {
248+
/// Write the generated file only if its content has actually changed.
249+
///
250+
/// This avoids bumping the mtime on `stub_map_generated.rs` when nothing
251+
/// changed, which in turn prevents `rustc` from unnecessarily recompiling
252+
/// the main crate.
253+
fn write_if_changed(content: &str) {
228254
let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
229255
let dest_path = Path::new(&out_dir).join("stub_map_generated.rs");
230-
let content = concat!(
231-
"pub(crate) static STUB_FILES: [&str; 0] = [];\n",
232-
"pub(crate) static STUB_CLASS_MAP: [(&str, usize); 0] = [];\n",
233-
"pub(crate) static STUB_FUNCTION_MAP: [(&str, usize); 0] = [];\n",
234-
"pub(crate) static STUB_CONSTANT_MAP: [(&str, usize); 0] = [];\n",
235-
);
236-
fs::write(&dest_path, content).expect("Failed to write empty generated stub map");
256+
257+
if let Ok(existing) = fs::read_to_string(&dest_path)
258+
&& existing == content {
259+
return;
260+
}
261+
262+
fs::write(&dest_path, content).expect("Failed to write generated stub map");
237263
}

src/lib.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,34 @@ impl Backend {
172172
}
173173
}
174174

175+
/// Create a `Backend` for tests with custom class, function, and constant
176+
/// stub indices.
177+
///
178+
/// This allows tests to inject minimal stub content so that they are
179+
/// fully self-contained and do not depend on `composer install`.
180+
pub fn new_test_with_all_stubs(
181+
stub_index: HashMap<&'static str, &'static str>,
182+
stub_function_index: HashMap<&'static str, &'static str>,
183+
stub_constant_index: HashMap<&'static str, &'static str>,
184+
) -> Self {
185+
Self {
186+
name: "PHPantomLSP".to_string(),
187+
version: "0.1.0".to_string(),
188+
open_files: Arc::new(Mutex::new(HashMap::new())),
189+
ast_map: Arc::new(Mutex::new(HashMap::new())),
190+
client: None,
191+
workspace_root: Arc::new(Mutex::new(None)),
192+
psr4_mappings: Arc::new(Mutex::new(Vec::new())),
193+
use_map: Arc::new(Mutex::new(HashMap::new())),
194+
namespace_map: Arc::new(Mutex::new(HashMap::new())),
195+
global_functions: Arc::new(Mutex::new(HashMap::new())),
196+
class_index: Arc::new(Mutex::new(HashMap::new())),
197+
stub_index,
198+
stub_function_index,
199+
stub_constant_index,
200+
}
201+
}
202+
175203
/// Create a `Backend` for tests with a specific workspace root and PSR-4
176204
/// mappings pre-configured.
177205
pub fn new_test_with_workspace(

tests/common/mod.rs

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,201 @@ interface BackedEnum extends UnitEnum
2929
}
3030
";
3131

32+
// ─── Function stubs ─────────────────────────────────────────────────────────
33+
// Minimal PHP stubs for built-in functions grouped by extension/category.
34+
35+
static ARRAY_FUNCTIONS_STUB: &str = "\
36+
<?php
37+
/**
38+
* @param callable|null $callback
39+
* @param array $array
40+
* @param array ...$arrays
41+
* @return array
42+
*/
43+
function array_map(?callable $callback, array $array, array ...$arrays): array {}
44+
45+
/**
46+
* @param array &$array
47+
* @return mixed
48+
*/
49+
function array_pop(array &$array): mixed {}
50+
51+
/**
52+
* @param array &$array
53+
* @param mixed ...$values
54+
* @return int
55+
*/
56+
function array_push(array &$array, mixed ...$values): int {}
57+
58+
/**
59+
* @param string|int $key
60+
* @param array $array
61+
* @return bool
62+
*/
63+
function array_key_exists(string|int $key, array $array): bool {}
64+
";
65+
66+
static STRING_FUNCTIONS_STUB: &str = "\
67+
<?php
68+
/**
69+
* @param string $haystack
70+
* @param string $needle
71+
* @return bool
72+
*/
73+
function str_contains(string $haystack, string $needle): bool {}
74+
75+
/**
76+
* @param string $string
77+
* @param int $offset
78+
* @param int|null $length
79+
* @return string
80+
*/
81+
function substr(string $string, int $offset, ?int $length = null): string {}
82+
";
83+
84+
static JSON_FUNCTIONS_STUB: &str = "\
85+
<?php
86+
/**
87+
* @param string $json
88+
* @param bool|null $associative
89+
* @param int $depth
90+
* @param int $flags
91+
* @return mixed
92+
*/
93+
function json_decode(string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed {}
94+
";
95+
96+
static DATE_FUNCTIONS_STUB: &str = "\
97+
<?php
98+
/**
99+
* @param string|null $datetime
100+
* @param DateTimeZone|null $timezone
101+
* @return DateTime|false
102+
*/
103+
function date_create(?string $datetime = \"now\", ?DateTimeZone $timezone = null): DateTime|false {}
104+
";
105+
106+
static SIMPLEXML_FUNCTIONS_STUB: &str = "\
107+
<?php
108+
/**
109+
* @param string $data
110+
* @param string|null $class_name
111+
* @param int $options
112+
* @param string $namespace_or_prefix
113+
* @param bool $is_prefix
114+
* @return SimpleXMLElement|false
115+
*/
116+
function simplexml_load_string(string $data, ?string $class_name = null, int $options = 0, string $namespace_or_prefix = \"\", bool $is_prefix = false): SimpleXMLElement|false {}
117+
";
118+
119+
static PCRE_FUNCTIONS_STUB: &str = "\
120+
<?php
121+
/**
122+
* @param string $pattern
123+
* @param string $subject
124+
* @param array|null &$matches
125+
* @param int $flags
126+
* @param int $offset
127+
* @return int|false
128+
*/
129+
function preg_match(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int|false {}
130+
";
131+
132+
// ─── Class stubs ────────────────────────────────────────────────────────────
133+
134+
static DATETIME_CLASS_STUB: &str = "\
135+
<?php
136+
class DateTime
137+
{
138+
public function __construct(?string $datetime = \"now\", ?DateTimeZone $timezone = null) {}
139+
140+
/**
141+
* @param string $format
142+
* @return string
143+
*/
144+
public function format(string $format): string {}
145+
146+
/**
147+
* @param string $modifier
148+
* @return DateTime|false
149+
*/
150+
public function modify(string $modifier): DateTime|false {}
151+
152+
/**
153+
* @return int
154+
*/
155+
public function getTimestamp(): int {}
156+
157+
/**
158+
* @param int $year
159+
* @param int $month
160+
* @param int $day
161+
* @return DateTime
162+
*/
163+
public function setDate(int $year, int $month, int $day): DateTime {}
164+
165+
/**
166+
* @param int $hour
167+
* @param int $minute
168+
* @param int $second
169+
* @param int $microsecond
170+
* @return DateTime
171+
*/
172+
public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): DateTime {}
173+
}
174+
";
175+
176+
static SIMPLEXMLELEMENT_CLASS_STUB: &str = "\
177+
<?php
178+
class SimpleXMLElement
179+
{
180+
/**
181+
* @param string $expression
182+
* @return array|false|null
183+
*/
184+
public function xpath(string $expression): array|false|null {}
185+
186+
/**
187+
* @param string|null $namespaceOrPrefix
188+
* @param bool $isPrefix
189+
* @return SimpleXMLElement|null
190+
*/
191+
public function children(?string $namespaceOrPrefix = null, bool $isPrefix = false): ?SimpleXMLElement {}
192+
193+
/**
194+
* @param string|null $namespaceOrPrefix
195+
* @param bool $isPrefix
196+
* @return SimpleXMLElement|null
197+
*/
198+
public function attributes(?string $namespaceOrPrefix = null, bool $isPrefix = false): ?SimpleXMLElement {}
199+
200+
/**
201+
* @param string $qualifiedName
202+
* @param string|null $value
203+
* @param string|null $namespace
204+
* @return SimpleXMLElement|null
205+
*/
206+
public function addChild(string $qualifiedName, ?string $value = null, ?string $namespace = null): ?SimpleXMLElement {}
207+
208+
/**
209+
* @return string
210+
*/
211+
public function getName(): string {}
212+
}
213+
";
214+
215+
// ─── Constant stubs ─────────────────────────────────────────────────────────
216+
217+
static CONSTANTS_STUB: &str = "\
218+
<?php
219+
define('PHP_EOL', \"\\n\");
220+
define('PHP_INT_MAX', 9223372036854775807);
221+
define('PHP_INT_MIN', -9223372036854775808);
222+
define('PHP_MAJOR_VERSION', 8);
223+
define('SORT_ASC', 4);
224+
define('SORT_DESC', 3);
225+
";
226+
32227
/// Create a test backend whose `stub_index` contains minimal `UnitEnum`
33228
/// and `BackedEnum` stubs. This makes "embedded stub" tests fully
34229
/// self-contained — they no longer require a prior `composer install`.
@@ -39,6 +234,48 @@ pub fn create_test_backend_with_stubs() -> Backend {
39234
Backend::new_test_with_stubs(stubs)
40235
}
41236

237+
/// Create a test backend with embedded PHP stubs for built-in functions,
238+
/// classes, and constants. This makes the stub-function tests fully
239+
/// self-contained — they work whether or not phpstorm-stubs are installed.
240+
pub fn create_test_backend_with_function_stubs() -> Backend {
241+
// ── Class stubs ──
242+
let mut class_stubs: HashMap<&'static str, &'static str> = HashMap::new();
243+
class_stubs.insert("DateTime", DATETIME_CLASS_STUB);
244+
class_stubs.insert("SimpleXMLElement", SIMPLEXMLELEMENT_CLASS_STUB);
245+
class_stubs.insert("UnitEnum", UNIT_ENUM_STUB);
246+
class_stubs.insert("BackedEnum", BACKED_ENUM_STUB);
247+
248+
// ── Function stubs ──
249+
let mut function_stubs: HashMap<&'static str, &'static str> = HashMap::new();
250+
// Array functions (all point to the same source)
251+
function_stubs.insert("array_map", ARRAY_FUNCTIONS_STUB);
252+
function_stubs.insert("array_pop", ARRAY_FUNCTIONS_STUB);
253+
function_stubs.insert("array_push", ARRAY_FUNCTIONS_STUB);
254+
function_stubs.insert("array_key_exists", ARRAY_FUNCTIONS_STUB);
255+
// String functions
256+
function_stubs.insert("str_contains", STRING_FUNCTIONS_STUB);
257+
function_stubs.insert("substr", STRING_FUNCTIONS_STUB);
258+
// JSON functions
259+
function_stubs.insert("json_decode", JSON_FUNCTIONS_STUB);
260+
// Date functions
261+
function_stubs.insert("date_create", DATE_FUNCTIONS_STUB);
262+
// SimpleXML functions
263+
function_stubs.insert("simplexml_load_string", SIMPLEXML_FUNCTIONS_STUB);
264+
// PCRE functions
265+
function_stubs.insert("preg_match", PCRE_FUNCTIONS_STUB);
266+
267+
// ── Constant stubs ──
268+
let mut constant_stubs: HashMap<&'static str, &'static str> = HashMap::new();
269+
constant_stubs.insert("PHP_EOL", CONSTANTS_STUB);
270+
constant_stubs.insert("PHP_INT_MAX", CONSTANTS_STUB);
271+
constant_stubs.insert("PHP_INT_MIN", CONSTANTS_STUB);
272+
constant_stubs.insert("PHP_MAJOR_VERSION", CONSTANTS_STUB);
273+
constant_stubs.insert("SORT_ASC", CONSTANTS_STUB);
274+
constant_stubs.insert("SORT_DESC", CONSTANTS_STUB);
275+
276+
Backend::new_test_with_all_stubs(class_stubs, function_stubs, constant_stubs)
277+
}
278+
42279
/// Helper: create a temp workspace with a composer.json and PHP files,
43280
/// then return a Backend configured with that workspace root + PSR-4 mappings.
44281
pub fn create_psr4_workspace(

0 commit comments

Comments
 (0)