brokk_bifrost_ruby/
mixins.rs1use crate::declarations::{is_descendable_container, qualified_internal_name, ruby_node_text};
15use crate::graph_support::RubySource;
16use brokk_bifrost_core::analyzer::type_relations::{TypeRelation, TypeRelationKind};
17use brokk_bifrost_core::analyzer::{CodeUnit, ProjectFile};
18use brokk_bifrost_core::hash::HashSet;
19use tree_sitter::Node;
20
21#[derive(Clone)]
22pub struct RubyForwardMixinSpec {
23 pub kind: TypeRelationKind,
24 pub raw_target: String,
25}
26
27pub fn raw_mixin_specs_for_type(node: Node<'_>, source: &str) -> Vec<RubyForwardMixinSpec> {
28 let Some(body) = node.child_by_field_name("body") else {
29 return Vec::new();
30 };
31 let mut specs = Vec::new();
32 let mut stack = vec![body];
33 while let Some(current) = stack.pop() {
34 let mut cursor = current.walk();
35 for child in current.named_children(&mut cursor) {
36 match child.kind() {
37 "call" => {
38 let Some(kind) = mixin_call_kind(child, source) else {
39 continue;
40 };
41 let Some(arguments) = child.child_by_field_name("arguments") else {
42 continue;
43 };
44 let mut arg_cursor = arguments.walk();
45 let mut call_specs = Vec::new();
46 for argument in arguments.named_children(&mut arg_cursor) {
47 if matches!(argument.kind(), "constant" | "scope_resolution")
48 && let Some(raw_target) = qualified_internal_name(argument, source)
49 {
50 call_specs.push(RubyForwardMixinSpec { kind, raw_target });
51 }
52 }
53 specs.extend(call_specs.into_iter().rev());
54 }
55 kind if is_descendable_container(kind) => stack.push(child),
56 _ => {}
57 }
58 }
59 }
60 specs
61}
62
63pub fn encode_superclass_relation(raw_target: &str) -> String {
64 encode_owner_relation("superclass", raw_target)
65}
66
67pub fn encode_mixin_relation(spec: &RubyForwardMixinSpec) -> String {
68 let kind = match spec.kind {
69 TypeRelationKind::MixinInclude => "include",
70 TypeRelationKind::MixinPrepend => "prepend",
71 TypeRelationKind::MixinExtend => "extend",
72 _ => unreachable!("Ruby mixin extractor only emits mixin relations"),
73 };
74 encode_owner_relation(kind, &spec.raw_target)
75}
76
77pub struct RubyOwnerRelationFact {
78 pub kind: Option<TypeRelationKind>,
79 pub raw_target: String,
80}
81
82fn encode_owner_relation(kind: &str, raw_target: &str) -> String {
83 serde_json::json!({ "kind": kind, "target": raw_target }).to_string()
84}
85
86pub fn decode_owner_relation(
87 encoded: &str,
88 expected_target: &str,
89) -> Option<RubyOwnerRelationFact> {
90 let value: serde_json::Value = serde_json::from_str(encoded).ok()?;
91 let raw_target = value.get("target")?.as_str()?.to_string();
92 if raw_target != expected_target {
93 return None;
94 }
95 let kind = match value.get("kind")?.as_str()? {
96 "superclass" => None,
97 "include" => Some(TypeRelationKind::MixinInclude),
98 "prepend" => Some(TypeRelationKind::MixinPrepend),
99 "extend" => Some(TypeRelationKind::MixinExtend),
100 _ => return None,
101 };
102 Some(RubyOwnerRelationFact { kind, raw_target })
103}
104
105pub fn ruby_collect_mixin_relations(ruby: &dyn RubySource) -> Vec<TypeRelation> {
106 let mut relations = Vec::new();
107 for file in ruby.get_analyzed_files() {
108 for owner in ruby
109 .declarations(&file)
110 .into_iter()
111 .filter(|unit| unit.is_class() || unit.is_module())
112 {
113 for spec in ruby_forward_mixin_specs(ruby, &owner) {
114 if let Some(target) = ruby_resolve_mixin_target(ruby, &file, &spec.raw_target) {
115 relations.push(TypeRelation {
116 from: owner.clone(),
117 to: target,
118 kind: spec.kind,
119 });
120 }
121 }
122 }
123 }
124 relations
125}
126
127pub fn ruby_forward_mixin_specs(
131 ruby: &dyn RubySource,
132 owner: &CodeUnit,
133) -> Vec<RubyForwardMixinSpec> {
134 ruby.forward_owner_relation_facts(owner)
135 .into_iter()
136 .filter_map(|fact| {
137 fact.kind.map(|kind| RubyForwardMixinSpec {
138 kind,
139 raw_target: fact.raw_target,
140 })
141 })
142 .collect()
143}
144
145pub fn ruby_forward_superclass_targets(ruby: &dyn RubySource, owner: &CodeUnit) -> Vec<String> {
146 ruby.forward_owner_relation_facts(owner)
147 .into_iter()
148 .filter(|fact| fact.kind.is_none())
149 .map(|fact| fact.raw_target)
150 .collect()
151}
152
153fn ruby_resolve_mixin_target(
154 ruby: &dyn RubySource,
155 file: &ProjectFile,
156 raw: &str,
157) -> Option<CodeUnit> {
158 let visible_files = ruby_visible_mixin_files(ruby, file);
159 ruby.declarations(file)
160 .into_iter()
161 .find(|unit| ruby_type_matches(unit, raw))
162 .or_else(|| {
163 ruby.imported_code_units_of(file)
164 .iter()
165 .find(|unit| ruby_type_matches(unit, raw))
166 .cloned()
167 })
168 .or_else(|| {
169 ruby.definitions(raw).find(|unit| {
170 (unit.is_class() || unit.is_module()) && visible_files.contains(unit.source())
171 })
172 })
173 .or_else(|| {
174 ruby.all_declarations()
175 .filter(|unit| visible_files.contains(unit.source()))
176 .find(|unit| ruby_type_matches(unit, raw))
177 })
178}
179
180fn ruby_visible_mixin_files(ruby: &dyn RubySource, file: &ProjectFile) -> HashSet<ProjectFile> {
181 let mut files = HashSet::default();
182 files.insert(file.clone());
183 files.extend(
184 ruby.imported_code_units_of(file)
185 .iter()
186 .map(|unit| unit.source().clone()),
187 );
188 files
189}
190
191fn ruby_type_matches(unit: &CodeUnit, raw: &str) -> bool {
192 (unit.is_class() || unit.is_module())
193 && (unit.fq_name() == raw || unit.short_name() == raw || unit.identifier() == raw)
194}
195
196fn mixin_call_kind(node: Node<'_>, source: &str) -> Option<TypeRelationKind> {
197 if node.child_by_field_name("receiver").is_some() {
198 return None;
199 }
200 let method = node.child_by_field_name("method")?;
201 match ruby_node_text(method, source).trim() {
202 "include" => Some(TypeRelationKind::MixinInclude),
203 "prepend" => Some(TypeRelationKind::MixinPrepend),
204 "extend" => Some(TypeRelationKind::MixinExtend),
205 _ => None,
206 }
207}