Skip to main content

_diffctx/edges/semantic/
shell.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::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}