Skip to main content

_diffctx/edges/semantic/
php.rs

1use 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}