Skip to main content

_diffctx/edges/semantic/
perl.rs

1use std::path::{Path, PathBuf};
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use crate::config::weights::EDGE_WEIGHTS;
8use crate::types::Fragment;
9
10use super::super::EdgeDict;
11use super::super::base::{self, EdgeBuilder, add_edge, add_edges_from_ids, discover_files_by_refs};
12
13fn is_perl_file(path: &Path) -> bool {
14    let ext = base::file_ext(path);
15    ext == ".pl" || ext == ".pm"
16}
17
18static USE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*use\s+([\w:]+)").unwrap());
19static REQUIRE_RE: Lazy<Regex> =
20    Lazy::new(|| Regex::new(r##"(?m)^\s*require\s+(?:['"]([^'"]+)['"]|([\w:]+))"##).unwrap());
21static PACKAGE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*package\s+([\w:]+)").unwrap());
22static SUB_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^\s*sub\s+(\w+)").unwrap());
23static ISA_RE: Lazy<Regex> = Lazy::new(|| {
24    Regex::new(
25        r##"(?m)(?:use\s+(?:parent|base)\s+.*?['"]([\w:]+)['"]|@ISA\s*=.*?['"]([\w:]+)['"])"##,
26    )
27    .unwrap()
28});
29static METHOD_CALL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\b(\w+)->(\w+)").unwrap());
30
31fn extract_uses(content: &str) -> FxHashSet<String> {
32    let mut refs = FxHashSet::default();
33    for cap in USE_RE.captures_iter(content) {
34        let name = &cap[1];
35        if !PERL_PRAGMAS.contains(name) {
36            refs.insert(name.to_string());
37        }
38    }
39    for cap in REQUIRE_RE.captures_iter(content) {
40        if let Some(m) = cap.get(1) {
41            refs.insert(m.as_str().to_string());
42        }
43        if let Some(m) = cap.get(2) {
44            refs.insert(m.as_str().to_string());
45        }
46    }
47    refs
48}
49
50static PERL_PRAGMAS: Lazy<FxHashSet<&str>> = Lazy::new(|| {
51    [
52        "strict",
53        "warnings",
54        "utf8",
55        "lib",
56        "constant",
57        "vars",
58        "feature",
59        "Exporter",
60        "Carp",
61        "Data::Dumper",
62        "File::Basename",
63    ]
64    .iter()
65    .copied()
66    .collect()
67});
68
69fn extract_packages(content: &str) -> FxHashSet<String> {
70    PACKAGE_RE
71        .captures_iter(content)
72        .map(|c| {
73            let full = &c[1];
74            full.split("::").last().unwrap_or(full).to_string()
75        })
76        .collect()
77}
78
79fn extract_subs(content: &str) -> FxHashSet<String> {
80    SUB_RE
81        .captures_iter(content)
82        .map(|c| c[1].to_string())
83        .collect()
84}
85
86fn extract_parents(content: &str) -> FxHashSet<String> {
87    let mut parents = FxHashSet::default();
88    for cap in ISA_RE.captures_iter(content) {
89        if let Some(m) = cap.get(1) {
90            parents.insert(
91                m.as_str()
92                    .split("::")
93                    .last()
94                    .unwrap_or(m.as_str())
95                    .to_string(),
96            );
97        }
98        if let Some(m) = cap.get(2) {
99            parents.insert(
100                m.as_str()
101                    .split("::")
102                    .last()
103                    .unwrap_or(m.as_str())
104                    .to_string(),
105            );
106        }
107    }
108    parents
109}
110
111pub struct PerlEdgeBuilder;
112
113impl EdgeBuilder for PerlEdgeBuilder {
114    fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
115        let frags: Vec<&Fragment> = fragments
116            .iter()
117            .filter(|f| is_perl_file(Path::new(f.path())))
118            .collect();
119        if frags.is_empty() {
120            return FxHashMap::default();
121        }
122
123        let use_w = EDGE_WEIGHTS["perl_use"].forward;
124        let fn_w = EDGE_WEIGHTS["perl_fn"].forward;
125        let method_w = EDGE_WEIGHTS["perl_method"].forward;
126        let inherit_w = EDGE_WEIGHTS["perl_inheritance"].forward;
127        let reverse_factor = EDGE_WEIGHTS["perl_use"].reverse_factor;
128
129        let idx = base::FragmentIndex::new(fragments, repo_root);
130        let mut name_to_defs: FxHashMap<String, Vec<_>> = FxHashMap::default();
131        for f in &frags {
132            for name in extract_packages(&f.content) {
133                name_to_defs
134                    .entry(name.to_lowercase())
135                    .or_default()
136                    .push(f.id.clone());
137            }
138            for name in extract_subs(&f.content) {
139                name_to_defs
140                    .entry(name.to_lowercase())
141                    .or_default()
142                    .push(f.id.clone());
143            }
144        }
145
146        let mut edges: EdgeDict = FxHashMap::default();
147
148        for f in &frags {
149            let self_defs = extract_subs(&f.content);
150            for use_name in extract_uses(&f.content) {
151                let leaf = use_name.split("::").last().unwrap_or(&use_name);
152                base::link_by_name(&f.id, leaf, &idx, &mut edges, use_w, reverse_factor);
153            }
154            for parent in extract_parents(&f.content) {
155                if let Some(targets) = name_to_defs.get(&parent.to_lowercase()) {
156                    add_edges_from_ids(&mut edges, &f.id, targets, inherit_w, reverse_factor);
157                }
158            }
159            for cap in METHOD_CALL_RE.captures_iter(&f.content) {
160                let method = &cap[2];
161                if self_defs.contains(method) {
162                    continue;
163                }
164                if let Some(targets) = name_to_defs.get(&method.to_lowercase()) {
165                    for t in targets {
166                        if t != &f.id {
167                            add_edge(&mut edges, &f.id, t, method_w, reverse_factor);
168                        }
169                    }
170                }
171            }
172            for id in &f.identifiers {
173                if self_defs.contains(id) {
174                    continue;
175                }
176                if let Some(targets) = name_to_defs.get(&id.to_lowercase()) {
177                    for t in targets {
178                        if t != &f.id {
179                            add_edge(&mut edges, &f.id, t, fn_w, reverse_factor);
180                        }
181                    }
182                }
183            }
184        }
185        edges
186    }
187
188    fn discover_related_files(
189        &self,
190        changed: &[PathBuf],
191        candidates: &[PathBuf],
192        repo_root: Option<&Path>,
193        file_cache: Option<&FxHashMap<PathBuf, String>>,
194    ) -> Vec<PathBuf> {
195        let pl_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_perl_file(f)).collect();
196        if pl_changed.is_empty() {
197            return vec![];
198        }
199        let mut refs = FxHashSet::default();
200        for f in &pl_changed {
201            if let Some(content) = base::read_file_cached(f, file_cache) {
202                refs.extend(extract_uses(&content));
203            }
204        }
205        discover_files_by_refs(&refs, changed, candidates, repo_root)
206    }
207}