use std::collections::{BTreeMap, BTreeSet};
use std::env;
use std::fs;
use std::path::Path;
const MAP_FILE: &str = "stubs/jetbrains/phpstorm-stubs/PhpStormStubsMap.php";
const STUBS_DIR: &str = "stubs/jetbrains/phpstorm-stubs";
fn main() {
println!("cargo:rerun-if-changed=.");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=composer.lock");
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
let map_path = Path::new(&manifest_dir).join(MAP_FILE);
let map_content = match fs::read_to_string(&map_path) {
Ok(c) => c,
Err(e) => {
eprintln!(
"cargo:warning=Could not read PhpStormStubsMap.php ({}); generating empty stub index",
e
);
let content = concat!(
"pub(crate) static STUB_FILES: [&str; 0] = [];\n",
"pub(crate) static STUB_CLASS_MAP: [(&str, usize); 0] = [];\n",
"pub(crate) static STUB_FUNCTION_MAP: [(&str, usize); 0] = [];\n",
"pub(crate) static STUB_CONSTANT_MAP: [(&str, usize); 0] = [];\n",
);
write_if_changed(content);
return;
}
};
let class_map = parse_section(&map_content, "CLASSES");
let function_map = parse_section(&map_content, "FUNCTIONS");
let constant_map = parse_section(&map_content, "CONSTANTS");
let mut unique_files = BTreeSet::new();
for path in class_map.values() {
unique_files.insert(path.as_str());
}
for path in function_map.values() {
unique_files.insert(path.as_str());
}
for path in constant_map.values() {
unique_files.insert(path.as_str());
}
let stubs_base = Path::new(&manifest_dir).join(STUBS_DIR);
let existing_files: Vec<&str> = unique_files
.iter()
.copied()
.filter(|rel| stubs_base.join(rel).is_file())
.collect();
let file_index: BTreeMap<&str, usize> = existing_files
.iter()
.enumerate()
.map(|(i, &p)| (p, i))
.collect();
let mut out = String::with_capacity(512 * 1024);
out.push_str("/// Embedded PHP stub file contents.\n");
out.push_str("///\n");
out.push_str("/// Each entry corresponds to one PHP file from phpstorm-stubs,\n");
out.push_str("/// embedded at compile time via `include_str!`.\n");
out.push_str(&format!(
"pub(crate) static STUB_FILES: [&str; {}] = [\n",
existing_files.len()
));
for rel_path in &existing_files {
let abs = stubs_base.join(rel_path);
let abs_str = abs.to_string_lossy().replace('\\', "/");
out.push_str(&format!(" include_str!(\"{}\")", abs_str));
out.push_str(",\n");
}
out.push_str("];\n\n");
let class_entries: Vec<(&str, usize)> = class_map
.iter()
.filter_map(|(name, path)| {
file_index
.get(path.as_str())
.map(|&idx| (name.as_str(), idx))
})
.collect();
out.push_str("/// Maps PHP class/interface/trait short names to an index into\n");
out.push_str("/// [`STUB_FILES`].\n");
out.push_str(&format!(
"pub(crate) static STUB_CLASS_MAP: [(&str, usize); {}] = [\n",
class_entries.len()
));
for (name, idx) in &class_entries {
out.push_str(&format!(" (\"{}\", {}),\n", escape(name), idx));
}
out.push_str("];\n\n");
let function_entries: Vec<(&str, usize)> = function_map
.iter()
.filter_map(|(name, path)| {
file_index
.get(path.as_str())
.map(|&idx| (name.as_str(), idx))
})
.collect();
out.push_str("/// Maps PHP function names (including namespaced ones) to an index\n");
out.push_str("/// into [`STUB_FILES`].\n");
out.push_str(&format!(
"pub(crate) static STUB_FUNCTION_MAP: [(&str, usize); {}] = [\n",
function_entries.len()
));
for (name, idx) in &function_entries {
out.push_str(&format!(" (\"{}\", {}),\n", escape(name), idx));
}
out.push_str("];\n\n");
let constant_entries: Vec<(&str, usize)> = constant_map
.iter()
.filter_map(|(name, path)| {
file_index
.get(path.as_str())
.map(|&idx| (name.as_str(), idx))
})
.collect();
out.push_str("/// Maps PHP constant names (including namespaced ones) to an index\n");
out.push_str("/// into [`STUB_FILES`].\n");
out.push_str(&format!(
"pub(crate) static STUB_CONSTANT_MAP: [(&str, usize); {}] = [\n",
constant_entries.len()
));
for (name, idx) in &constant_entries {
out.push_str(&format!(" (\"{}\", {}),\n", escape(name), idx));
}
out.push_str("];\n");
write_if_changed(&out);
}
fn parse_section(content: &str, section_name: &str) -> BTreeMap<String, String> {
let mut map = BTreeMap::new();
let marker = format!("const {} = array (", section_name);
let start = match content.find(&marker) {
Some(pos) => pos + marker.len(),
None => return map,
};
for line in content[start..].lines() {
let trimmed = line.trim();
if trimmed == ");" {
break;
}
if let Some(entry) = parse_map_entry(trimmed) {
map.insert(entry.0, entry.1);
}
}
map
}
fn parse_map_entry(line: &str) -> Option<(String, String)> {
let trimmed = line.trim().trim_end_matches(',');
let (lhs, rhs) = trimmed.split_once(" => ")?;
let key = lhs.trim().strip_prefix('\'')?.strip_suffix('\'')?;
let value = rhs.trim().strip_prefix('\'')?.strip_suffix('\'')?;
let key = php_unescape_single_quoted(key);
let value = php_unescape_single_quoted(value);
Some((key, value))
}
fn php_unescape_single_quoted(s: &str) -> String {
s.replace("\\\\", "\x00")
.replace("\\'", "'")
.replace('\x00', "\\")
}
fn escape(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
fn write_if_changed(content: &str) {
let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
let dest_path = Path::new(&out_dir).join("stub_map_generated.rs");
if let Ok(existing) = fs::read_to_string(&dest_path)
&& existing == content
{
return;
}
fs::write(&dest_path, content).expect("Failed to write generated stub map");
}