1+ //! Build script for PHPantomLSP.
2+ //!
3+ //! Parses `stubs/jetbrains/phpstorm-stubs/PhpStormStubsMap.php` and generates
4+ //! a Rust source file (`stub_map_generated.rs`) that:
5+ //!
6+ //! 1. Embeds every referenced PHP stub file via `include_str!`.
7+ //! 2. Provides static arrays mapping class names and function names to
8+ //! indices into the embedded file array.
9+ //!
10+ //! The generated file is consumed by `src/stubs.rs` at compile time.
11+
12+ use std:: collections:: { BTreeMap , BTreeSet } ;
13+ use std:: env;
14+ use std:: fs;
15+ use std:: path:: Path ;
16+
17+ /// Relative path from the crate root to the stubs map file.
18+ const MAP_FILE : & str = "stubs/jetbrains/phpstorm-stubs/PhpStormStubsMap.php" ;
19+
20+ /// Relative path from the crate root to the stubs directory (the base for
21+ /// relative paths found in the map file).
22+ const STUBS_DIR : & str = "stubs/jetbrains/phpstorm-stubs" ;
23+
24+ 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" ) ;
30+ println ! ( "cargo:rerun-if-changed=build.rs" ) ;
31+
32+ let manifest_dir = env:: var ( "CARGO_MANIFEST_DIR" ) . expect ( "CARGO_MANIFEST_DIR not set" ) ;
33+ let map_path = Path :: new ( & manifest_dir) . join ( MAP_FILE ) ;
34+
35+ let map_content = match fs:: read_to_string ( & map_path) {
36+ Ok ( c) => c,
37+ Err ( e) => {
38+ // If stubs aren't installed yet, generate an empty map so the
39+ // build still succeeds (just without built-in stubs).
40+ eprintln ! (
41+ "cargo:warning=Could not read PhpStormStubsMap.php ({}); generating empty stub index" ,
42+ e
43+ ) ;
44+ write_empty_generated_file ( ) ;
45+ return ;
46+ }
47+ } ;
48+
49+ // ── Parse the three sections ────────────────────────────────────────
50+
51+ let class_map = parse_section ( & map_content, "CLASSES" ) ;
52+ let function_map = parse_section ( & map_content, "FUNCTIONS" ) ;
53+ let constant_map = parse_section ( & map_content, "CONSTANTS" ) ;
54+
55+ // ── Collect unique file paths ───────────────────────────────────────
56+
57+ let mut unique_files = BTreeSet :: new ( ) ;
58+ for path in class_map. values ( ) {
59+ unique_files. insert ( path. as_str ( ) ) ;
60+ }
61+ for path in function_map. values ( ) {
62+ unique_files. insert ( path. as_str ( ) ) ;
63+ }
64+ for path in constant_map. values ( ) {
65+ unique_files. insert ( path. as_str ( ) ) ;
66+ }
67+
68+ // Only keep files that actually exist on disk.
69+ let stubs_base = Path :: new ( & manifest_dir) . join ( STUBS_DIR ) ;
70+ let existing_files: Vec < & str > = unique_files
71+ . iter ( )
72+ . copied ( )
73+ . filter ( |rel| stubs_base. join ( rel) . is_file ( ) )
74+ . collect ( ) ;
75+
76+ // Build a path → index mapping.
77+ let file_index: BTreeMap < & str , usize > = existing_files
78+ . iter ( )
79+ . enumerate ( )
80+ . map ( |( i, & p) | ( p, i) )
81+ . collect ( ) ;
82+
83+ // ── Generate Rust source ────────────────────────────────────────────
84+
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+
88+ let mut out = String :: with_capacity ( 512 * 1024 ) ;
89+
90+ // 1. The embedded file array.
91+ out. push_str ( "/// Embedded PHP stub file contents.\n " ) ;
92+ out. push_str ( "///\n " ) ;
93+ out. push_str ( "/// Each entry corresponds to one PHP file from phpstorm-stubs,\n " ) ;
94+ out. push_str ( "/// embedded at compile time via `include_str!`.\n " ) ;
95+ out. push_str ( & format ! (
96+ "pub(crate) static STUB_FILES: [&str; {}] = [\n " ,
97+ existing_files. len( )
98+ ) ) ;
99+ for rel_path in & existing_files {
100+ // Build the include_str! path relative to the generated file's
101+ // location ($OUT_DIR). We use an absolute path rooted at CARGO_MANIFEST_DIR
102+ // to avoid fragile relative path arithmetic.
103+ let abs = stubs_base. join ( rel_path) ;
104+ let abs_str = abs. to_string_lossy ( ) . replace ( '\\' , "/" ) ;
105+ out. push_str ( & format ! ( " include_str!(\" {}\" )" , abs_str) ) ;
106+ out. push_str ( ",\n " ) ;
107+ }
108+ out. push_str ( "];\n \n " ) ;
109+
110+ // 2. Class name → file index mapping.
111+ let class_entries: Vec < ( & str , usize ) > = class_map
112+ . iter ( )
113+ . filter_map ( |( name, path) | file_index. get ( path. as_str ( ) ) . map ( |& idx| ( name. as_str ( ) , idx) ) )
114+ . collect ( ) ;
115+
116+ out. push_str ( "/// Maps PHP class/interface/trait short names to an index into\n " ) ;
117+ out. push_str ( "/// [`STUB_FILES`].\n " ) ;
118+ out. push_str ( & format ! (
119+ "pub(crate) static STUB_CLASS_MAP: [(&str, usize); {}] = [\n " ,
120+ class_entries. len( )
121+ ) ) ;
122+ for ( name, idx) in & class_entries {
123+ out. push_str ( & format ! ( " (\" {}\" , {}),\n " , escape( name) , idx) ) ;
124+ }
125+ out. push_str ( "];\n \n " ) ;
126+
127+ // 3. Function name → file index mapping.
128+ let function_entries: Vec < ( & str , usize ) > = function_map
129+ . iter ( )
130+ . filter_map ( |( name, path) | file_index. get ( path. as_str ( ) ) . map ( |& idx| ( name. as_str ( ) , idx) ) )
131+ . collect ( ) ;
132+
133+ out. push_str ( "/// Maps PHP function names (including namespaced ones) to an index\n " ) ;
134+ out. push_str ( "/// into [`STUB_FILES`].\n " ) ;
135+ out. push_str ( & format ! (
136+ "pub(crate) static STUB_FUNCTION_MAP: [(&str, usize); {}] = [\n " ,
137+ function_entries. len( )
138+ ) ) ;
139+ for ( name, idx) in & function_entries {
140+ out. push_str ( & format ! ( " (\" {}\" , {}),\n " , escape( name) , idx) ) ;
141+ }
142+ out. push_str ( "];\n \n " ) ;
143+
144+ // 4. Constant name → file index mapping.
145+ let constant_entries: Vec < ( & str , usize ) > = constant_map
146+ . iter ( )
147+ . filter_map ( |( name, path) | file_index. get ( path. as_str ( ) ) . map ( |& idx| ( name. as_str ( ) , idx) ) )
148+ . collect ( ) ;
149+
150+ out. push_str ( "/// Maps PHP constant names (including namespaced ones) to an index\n " ) ;
151+ out. push_str ( "/// into [`STUB_FILES`].\n " ) ;
152+ out. push_str ( & format ! (
153+ "pub(crate) static STUB_CONSTANT_MAP: [(&str, usize); {}] = [\n " ,
154+ constant_entries. len( )
155+ ) ) ;
156+ for ( name, idx) in & constant_entries {
157+ out. push_str ( & format ! ( " (\" {}\" , {}),\n " , escape( name) , idx) ) ;
158+ }
159+ out. push_str ( "];\n " ) ;
160+
161+ fs:: write ( & dest_path, & out) . expect ( "Failed to write generated stub map" ) ;
162+ }
163+
164+ /// Parse one of the `const CLASSES = array(...)`, `const FUNCTIONS = array(...)`,
165+ /// or `const CONSTANTS = array(...)` sections from the PhpStormStubsMap.php file.
166+ ///
167+ /// Returns a `BTreeMap<String, String>` of `symbol_name → relative_file_path`.
168+ fn parse_section ( content : & str , section_name : & str ) -> BTreeMap < String , String > {
169+ let mut map = BTreeMap :: new ( ) ;
170+
171+ // Find the start: `const SECTION = array (`
172+ let marker = format ! ( "const {} = array (" , section_name) ;
173+ let start = match content. find ( & marker) {
174+ Some ( pos) => pos + marker. len ( ) ,
175+ None => return map,
176+ } ;
177+
178+ // Walk line by line until we hit `);`
179+ for line in content[ start..] . lines ( ) {
180+ let trimmed = line. trim ( ) ;
181+ if trimmed == ");" {
182+ break ;
183+ }
184+
185+ // Lines look like: 'ClassName' => 'relative/path.php',
186+ if let Some ( entry) = parse_map_entry ( trimmed) {
187+ map. insert ( entry. 0 , entry. 1 ) ;
188+ }
189+ }
190+
191+ map
192+ }
193+
194+ /// Parse a single `'key' => 'value',` line.
195+ fn parse_map_entry ( line : & str ) -> Option < ( String , String ) > {
196+ // Strip leading whitespace and trailing comma.
197+ let trimmed = line. trim ( ) . trim_end_matches ( ',' ) ;
198+
199+ // Split on ` => `.
200+ let ( lhs, rhs) = trimmed. split_once ( " => " ) ?;
201+
202+ // Strip surrounding single quotes.
203+ let key = lhs. trim ( ) . strip_prefix ( '\'' ) ?. strip_suffix ( '\'' ) ?;
204+ let value = rhs. trim ( ) . strip_prefix ( '\'' ) ?. strip_suffix ( '\'' ) ?;
205+
206+ Some ( ( key. to_string ( ) , value. to_string ( ) ) )
207+ }
208+
209+ /// Escape a string for embedding in a Rust string literal.
210+ fn escape ( s : & str ) -> String {
211+ s. replace ( '\\' , "\\ \\ " ) . replace ( '"' , "\\ \" " )
212+ }
213+
214+ /// Write an empty generated file when stubs are not available.
215+ fn write_empty_generated_file ( ) {
216+ let out_dir = env:: var ( "OUT_DIR" ) . expect ( "OUT_DIR not set" ) ;
217+ let dest_path = Path :: new ( & out_dir) . join ( "stub_map_generated.rs" ) ;
218+ let content = concat ! (
219+ "pub(crate) static STUB_FILES: [&str; 0] = [];\n " ,
220+ "pub(crate) static STUB_CLASS_MAP: [(&str, usize); 0] = [];\n " ,
221+ "pub(crate) static STUB_FUNCTION_MAP: [(&str, usize); 0] = [];\n " ,
222+ "pub(crate) static STUB_CONSTANT_MAP: [(&str, usize); 0] = [];\n " ,
223+ ) ;
224+ fs:: write ( & dest_path, content) . expect ( "Failed to write empty generated stub map" ) ;
225+ }
0 commit comments