_diffctx/edges/semantic/
shell.rs1use std::path::{Path, PathBuf};
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use crate::config::extensions::SHELL_EXTENSIONS;
8use crate::config::weights::EDGE_WEIGHTS;
9use crate::types::Fragment;
10
11use super::super::EdgeDict;
12use super::super::base::{self, EdgeBuilder, FragmentIndex, discover_files_by_refs, link_by_name};
13
14fn is_shell_file(path: &Path) -> bool {
15 let ext = base::file_ext(path);
16 SHELL_EXTENSIONS.contains(ext.as_str())
17}
18
19static SOURCE_RE: Lazy<Regex> =
20 Lazy::new(|| Regex::new(r#"(?m)^\s*(?:source|\.)\s+["']?([^"'\s;]+)"#).unwrap());
21static SCRIPT_CALL_RE: Lazy<Regex> =
22 Lazy::new(|| Regex::new(r"(?:bash|sh|python|python3|node|ruby|perl)\s+(\S+)").unwrap());
23static EXEC_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\./(\S+)").unwrap());
24
25fn extract_refs(content: &str) -> FxHashSet<String> {
26 let mut refs = FxHashSet::default();
27 for cap in SOURCE_RE.captures_iter(content) {
28 refs.insert(cap[1].to_string());
29 }
30 for cap in SCRIPT_CALL_RE.captures_iter(content) {
31 refs.insert(cap[1].to_string());
32 }
33 for cap in EXEC_RE.captures_iter(content) {
34 refs.insert(cap[1].to_string());
35 }
36 refs
37}
38
39pub struct ShellEdgeBuilder;
40
41impl EdgeBuilder for ShellEdgeBuilder {
42 fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
43 let sh_frags: Vec<&Fragment> = fragments
44 .iter()
45 .filter(|f| is_shell_file(Path::new(f.path())))
46 .collect();
47 if sh_frags.is_empty() {
48 return FxHashMap::default();
49 }
50
51 let source_weight = EDGE_WEIGHTS["shell_source"].forward;
52 let script_weight = EDGE_WEIGHTS["shell_script"].forward;
53 let source_reverse = EDGE_WEIGHTS["shell_source"].reverse_factor;
54 let script_reverse = EDGE_WEIGHTS["shell_script"].reverse_factor;
55
56 let idx = FragmentIndex::new(fragments, repo_root);
57
58 let mut edges: EdgeDict = FxHashMap::default();
59
60 for f in &sh_frags {
61 let content = &f.content;
62
63 for cap in SOURCE_RE.captures_iter(content) {
64 let ref_path = &cap[1];
65 link_by_name(
66 &f.id,
67 ref_path,
68 &idx,
69 &mut edges,
70 source_weight,
71 source_reverse,
72 );
73 }
74
75 for cap in SCRIPT_CALL_RE.captures_iter(content) {
76 let ref_path = &cap[1];
77 link_by_name(
78 &f.id,
79 ref_path,
80 &idx,
81 &mut edges,
82 script_weight,
83 script_reverse,
84 );
85 }
86
87 for cap in EXEC_RE.captures_iter(content) {
88 let ref_path = &cap[1];
89 link_by_name(
90 &f.id,
91 ref_path,
92 &idx,
93 &mut edges,
94 script_weight,
95 script_reverse,
96 );
97 }
98 }
99
100 edges
101 }
102
103 fn discover_related_files(
104 &self,
105 changed: &[PathBuf],
106 candidates: &[PathBuf],
107 repo_root: Option<&Path>,
108 file_cache: Option<&FxHashMap<PathBuf, String>>,
109 ) -> Vec<PathBuf> {
110 let sh_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_shell_file(f)).collect();
111 if sh_changed.is_empty() {
112 return vec![];
113 }
114
115 let mut all_refs = FxHashSet::default();
116 for f in &sh_changed {
117 let content = base::read_file_cached(f, file_cache);
118 if let Some(c) = content {
119 all_refs.extend(extract_refs(&c));
120 }
121 }
122
123 discover_files_by_refs(&all_refs, changed, candidates, repo_root)
124 }
125}