_diffctx/edges/semantic/
ruby.rs1use std::path::{Path, PathBuf};
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5use rustc_hash::{FxHashMap, FxHashSet};
6
7use crate::config::extensions::RUBY_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_ruby_file(path: &Path) -> bool {
15 let ext = base::file_ext(path);
16 RUBY_EXTENSIONS.contains(ext.as_str())
17}
18
19static REQUIRE_RE: Lazy<Regex> =
20 Lazy::new(|| Regex::new(r#"(?m)^\s*(?:require|require_relative)\s+['"]([^'"]+)['"]"#).unwrap());
21static DEF_RE: Lazy<Regex> = Lazy::new(|| {
22 Regex::new(r"(?m)^\s*(?:class|module|def)\s+([A-Za-z_]\w*(?:::[A-Za-z_]\w*)*)").unwrap()
23});
24static MIXIN_RE: Lazy<Regex> = Lazy::new(|| {
25 Regex::new(r"(?m)^\s*(?:include|extend|prepend)\s+([A-Z]\w*(?:::[A-Z]\w*)*)").unwrap()
26});
27static CONST_REF_RE: Lazy<Regex> =
28 Lazy::new(|| Regex::new(r"\b([A-Z][A-Za-z_]*(?:::[A-Z][A-Za-z_]*)*)\b").unwrap());
29
30fn extract_requires(content: &str) -> FxHashSet<String> {
31 REQUIRE_RE
32 .captures_iter(content)
33 .map(|c| c[1].to_string())
34 .collect()
35}
36
37fn extract_defines(content: &str) -> FxHashSet<String> {
38 DEF_RE
39 .captures_iter(content)
40 .map(|c| {
41 let name = &c[1];
42 name.split("::").last().unwrap_or(name).to_string()
43 })
44 .collect()
45}
46
47fn extract_mixins(content: &str) -> FxHashSet<String> {
48 MIXIN_RE
49 .captures_iter(content)
50 .map(|c| {
51 let name = &c[1];
52 name.split("::").last().unwrap_or(name).to_string()
53 })
54 .collect()
55}
56
57fn extract_const_refs(content: &str) -> FxHashSet<String> {
58 CONST_REF_RE
59 .captures_iter(content)
60 .map(|c| c[1].to_string())
61 .collect()
62}
63
64pub struct RubyEdgeBuilder;
65
66impl EdgeBuilder for RubyEdgeBuilder {
67 fn build(&self, fragments: &[Fragment], repo_root: Option<&Path>) -> EdgeDict {
68 let frags: Vec<&Fragment> = fragments
69 .iter()
70 .filter(|f| is_ruby_file(Path::new(f.path())))
71 .collect();
72 if frags.is_empty() {
73 return FxHashMap::default();
74 }
75
76 let require_w = EDGE_WEIGHTS["ruby_require"].forward;
77 let include_w = EDGE_WEIGHTS["ruby_include"].forward;
78 let const_w = EDGE_WEIGHTS["ruby_const"].forward;
79 let reverse_factor = EDGE_WEIGHTS["ruby_require"].reverse_factor;
80
81 let idx = base::FragmentIndex::new(fragments, repo_root);
82 let mut name_to_defs: FxHashMap<String, Vec<_>> = FxHashMap::default();
83 for f in &frags {
84 for name in extract_defines(&f.content) {
85 name_to_defs
86 .entry(name.to_lowercase())
87 .or_default()
88 .push(f.id.clone());
89 }
90 }
91
92 let mut edges: EdgeDict = FxHashMap::default();
93
94 for f in &frags {
95 let self_defs = extract_defines(&f.content);
96 for req in extract_requires(&f.content) {
97 base::link_by_name(&f.id, &req, &idx, &mut edges, require_w, reverse_factor);
98 }
99 for mixin in extract_mixins(&f.content) {
100 if let Some(targets) = name_to_defs.get(&mixin.to_lowercase()) {
101 add_edges_from_ids(&mut edges, &f.id, targets, include_w, reverse_factor);
102 }
103 }
104 for cref in extract_const_refs(&f.content) {
105 let leaf = cref.split("::").last().unwrap_or(&cref);
106 if self_defs.contains(leaf) {
107 continue;
108 }
109 if let Some(targets) = name_to_defs.get(&leaf.to_lowercase()) {
110 for t in targets {
111 if t != &f.id {
112 add_edge(&mut edges, &f.id, t, const_w, reverse_factor);
113 }
114 }
115 }
116 }
117 }
118 edges
119 }
120
121 fn discover_related_files(
122 &self,
123 changed: &[PathBuf],
124 candidates: &[PathBuf],
125 repo_root: Option<&Path>,
126 file_cache: Option<&FxHashMap<PathBuf, String>>,
127 ) -> Vec<PathBuf> {
128 let rb_changed: Vec<&PathBuf> = changed.iter().filter(|f| is_ruby_file(f)).collect();
129 if rb_changed.is_empty() {
130 return vec![];
131 }
132 let mut refs = FxHashSet::default();
133 for f in &rb_changed {
134 if let Some(content) = base::read_file_cached(f, file_cache) {
135 refs.extend(extract_requires(&content));
136 }
137 }
138 discover_files_by_refs(&refs, changed, candidates, repo_root)
139 }
140}