_diffctx/edges/semantic/
perl.rs1use 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}