_diffctx/edges/semantic/
php.rs1use std::path::{Path, PathBuf};
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use crate::config::extensions::PHP_EXTENSIONS;
8use crate::config::weights::EDGE_WEIGHTS;
9use crate::types::Fragment;
10
11use super::super::EdgeDict;
12use super::super::base::{self, EdgeBuilder, add_edge, add_edges_from_ids, discover_files_by_refs};
13
14fn is_php_file(path: &Path) -> bool {
15 let ext = base::file_ext(path);
16 PHP_EXTENSIONS.contains(ext.as_str())
17}
18
19static USE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*use\s+([\w\\]+)").unwrap());
20static NAMESPACE_RE: Lazy<Regex> =
21 Lazy::new(|| Regex::new(r"(?m)^\s*namespace\s+([\w\\]+)").unwrap());
22static REQUIRE_RE: Lazy<Regex> = Lazy::new(|| {
23 Regex::new(r#"(?m)^\s*(?:require_once|require|include_once|include)\s+['"]([^'"]+)['"]"#)
24 .unwrap()
25});
26static DEF_RE: Lazy<Regex> = Lazy::new(|| {
27 Regex::new(r"(?m)^\s*(?:abstract\s+)?(?:class|interface|trait|enum)\s+([A-Z]\w*)").unwrap()
28});
29static FUNC_DEF_RE: Lazy<Regex> = Lazy::new(|| {
30 Regex::new(r"(?m)^\s*(?:public|protected|private|static)?\s*function\s+([a-zA-Z_]\w*)").unwrap()
31});
32static EXTENDS_RE: Lazy<Regex> =
33 Lazy::new(|| Regex::new(r"(?:extends|implements)\s+([\w\\,\s]+)").unwrap());
34static TYPE_REF_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b([A-Z]\w*)\b").unwrap());
35
36fn extract_uses(content: &str) -> FxHashSet<String> {
37 USE_RE
38 .captures_iter(content)
39 .map(|c| {
40 let full = &c[1];
41 full.split('\\').last().unwrap_or(full).to_string()
42 })
43 .collect()
44}
45
46fn extract_requires(content: &str) -> FxHashSet<String> {
47 REQUIRE_RE
48 .captures_iter(content)
49 .map(|c| c[1].to_string())
50 .collect()
51}
52
53fn extract_namespace(content: &str) -> Option<String> {
54 NAMESPACE_RE.captures(content).map(|c| c[1].to_string())
55}
56
57fn extract_defs(content: &str) -> FxHashSet<String> {
58 let mut defs: FxHashSet<String> = DEF_RE
59 .captures_iter(content)
60 .map(|c| c[1].to_string())
61 .collect();
62 defs.extend(FUNC_DEF_RE.captures_iter(content).map(|c| c[1].to_string()));
63 defs
64}
65
66fn extract_inheritance(content: &str) -> FxHashSet<String> {
67 let mut refs = FxHashSet::default();
68 for cap in EXTENDS_RE.captures_iter(content) {
69 for part in cap[1].split(',') {
70 let name = part.trim().split('\\').last().unwrap_or("").trim();
71 if !name.is_empty() {
72 refs.insert(name.to_string());
73 }
74 }
75 }
76 refs
77}
78
79pub struct PhpEdgeBuilder;
80
81impl EdgeBuilder for PhpEdgeBuilder {
82 fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
83 let frags: Vec<&Fragment> = fragments
84 .iter()
85 .filter(|f| is_php_file(Path::new(f.path())))
86 .collect();
87 if frags.is_empty() {
88 return FxHashMap::default();
89 }
90
91 let use_w = EDGE_WEIGHTS["php_use"].forward;
92 let require_w = EDGE_WEIGHTS["php_require"].forward;
93 let inherit_w = EDGE_WEIGHTS["php_inheritance"].forward;
94 let type_w = EDGE_WEIGHTS["php_type"].forward;
95 let reverse_factor = EDGE_WEIGHTS["php_use"].reverse_factor;
96
97 let idx = base::FragmentIndex::new(fragments, repo_root);
98 let mut name_to_defs: FxHashMap<String, Vec<_>> = FxHashMap::default();
99 let mut ns_to_frags: FxHashMap<String, Vec<_>> = FxHashMap::default();
100 for f in &frags {
101 for name in extract_defs(&f.content) {
102 name_to_defs
103 .entry(name.to_lowercase())
104 .or_default()
105 .push(f.id.clone());
106 }
107 if let Some(ns) = extract_namespace(&f.content) {
108 ns_to_frags.entry(ns).or_default().push(f.id.clone());
109 }
110 }
111
112 let mut edges: EdgeDict = FxHashMap::default();
113
114 for f in &frags {
115 let self_defs = extract_defs(&f.content);
116 for req in extract_requires(&f.content) {
117 base::link_by_name(&f.id, &req, &idx, &mut edges, require_w, reverse_factor);
118 }
119 for use_name in extract_uses(&f.content) {
120 if let Some(targets) = name_to_defs.get(&use_name.to_lowercase()) {
121 add_edges_from_ids(&mut edges, &f.id, targets, use_w, reverse_factor);
122 }
123 }
124 for parent in extract_inheritance(&f.content) {
125 if let Some(targets) = name_to_defs.get(&parent.to_lowercase()) {
126 add_edges_from_ids(&mut edges, &f.id, targets, inherit_w, reverse_factor);
127 }
128 }
129 for tr in TYPE_REF_RE.captures_iter(&f.content) {
130 let name = &tr[1];
131 if self_defs.contains(name) {
132 continue;
133 }
134 if let Some(targets) = name_to_defs.get(&name.to_lowercase()) {
135 for t in targets {
136 if t != &f.id {
137 add_edge(&mut edges, &f.id, t, type_w, reverse_factor);
138 }
139 }
140 }
141 }
142 }
143 edges
144 }
145
146 fn discover_related_files(
147 &self,
148 changed: &[PathBuf],
149 candidates: &[PathBuf],
150 repo_root: Option<&Path>,
151 file_cache: Option<&FxHashMap<PathBuf, String>>,
152 ) -> Vec<PathBuf> {
153 let php_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_php_file(f)).collect();
154 if php_changed.is_empty() {
155 return vec![];
156 }
157 let mut refs = FxHashSet::default();
158 for f in &php_changed {
159 if let Some(content) = base::read_file_cached(f, file_cache) {
160 refs.extend(extract_requires(&content));
161 refs.extend(extract_uses(&content));
162 }
163 }
164 discover_files_by_refs(&refs, changed, candidates, repo_root)
165 }
166}