backhopper_core/compat/
added_file.rs1use std::collections::BTreeMap;
20
21use crate::compat::source_attributes::{
22 behaviour_resolves, extract_behaviours, extract_includes, resolve_include,
23};
24use crate::compat::target_tree_index::TargetTreeIndex;
25use crate::compat::test_suite::{TestSuiteFile, is_stdlib_or_otp};
26use crate::model::names::{ModuleName, RelativePath};
27use crate::model::verdict::{IncludeDirective, Reason};
28
29#[non_exhaustive]
30#[derive(Debug, Clone, Default, PartialEq, Eq)]
31pub struct AddedFileFindings {
32 pub reasons: Vec<Reason>,
33 pub missing_test_modules: BTreeMap<RelativePath, BTreeMap<ModuleName, usize>>,
39}
40
41impl AddedFileFindings {
42 pub fn new(
43 reasons: Vec<Reason>,
44 missing_test_modules: BTreeMap<RelativePath, BTreeMap<ModuleName, usize>>,
45 ) -> Self {
46 Self {
47 reasons,
48 missing_test_modules,
49 }
50 }
51
52 pub fn is_empty(&self) -> bool {
53 self.reasons.is_empty() && self.missing_test_modules.is_empty()
54 }
55
56 fn merge(&mut self, other: AddedFileFindings) {
57 self.reasons.extend(other.reasons);
58 for (suite, by_mod) in other.missing_test_modules {
59 let entry = self.missing_test_modules.entry(suite).or_default();
60 for (m, count) in by_mod {
61 *entry.entry(m).or_insert(0) += count;
62 }
63 }
64 }
65}
66
67pub fn analyse_added_file(
71 path: &RelativePath,
72 content: &str,
73 target: &TargetTreeIndex,
74 search_path_globs: &[String],
75) -> AddedFileFindings {
76 let mut out = AddedFileFindings::default();
77 let is_erl = ends_with_ignore_case(path.as_str(), ".erl");
78 let is_hrl = ends_with_ignore_case(path.as_str(), ".hrl");
79 if !is_erl && !is_hrl {
80 return out;
81 }
82 if is_erl && is_test_suite_path(path) {
83 out.merge(analyse_test_suite(path, content, target, search_path_globs));
84 }
85 if is_erl {
86 out.merge(analyse_behaviours(path, content, target, search_path_globs));
87 }
88 out.merge(analyse_includes(path, content, target));
89 out
90}
91
92pub fn analyse_added_files<'a, I>(
95 files: I,
96 target: &TargetTreeIndex,
97 search_path_globs: &[String],
98) -> AddedFileFindings
99where
100 I: IntoIterator<Item = (&'a RelativePath, &'a str)>,
101{
102 let mut out = AddedFileFindings::default();
103 for (path, content) in files {
104 out.merge(analyse_added_file(path, content, target, search_path_globs));
105 }
106 out
107}
108
109fn analyse_test_suite(
110 path: &RelativePath,
111 content: &str,
112 target: &TargetTreeIndex,
113 search_path_globs: &[String],
114) -> AddedFileFindings {
115 let parsed = match TestSuiteFile::new(path.clone(), content.to_owned()).parse() {
116 Ok(p) => p,
117 Err(_) => return AddedFileFindings::default(),
118 };
119 let resolved = parsed.resolve(target, search_path_globs);
120 let findings = resolved.into_findings();
121 let mut missing = BTreeMap::new();
122 if let Some((suite, by_mod)) = findings.diagnostic_entry {
123 missing.insert(suite, by_mod);
124 }
125 AddedFileFindings {
126 reasons: findings.reasons,
127 missing_test_modules: missing,
128 }
129}
130
131fn analyse_behaviours(
132 path: &RelativePath,
133 content: &str,
134 target: &TargetTreeIndex,
135 search_path_globs: &[String],
136) -> AddedFileFindings {
137 let mut reasons = Vec::new();
138 for b in extract_behaviours(content) {
139 if is_stdlib_or_otp(&b.behaviour) {
140 continue;
141 }
142 if behaviour_resolves(target, search_path_globs, &b.behaviour) {
143 continue;
144 }
145 reasons.push(Reason::BehaviourModuleMissing {
146 source_path: path.clone(),
147 behaviour: b.behaviour,
148 });
149 }
150 AddedFileFindings {
151 reasons,
152 missing_test_modules: BTreeMap::new(),
153 }
154}
155
156fn analyse_includes(
157 path: &RelativePath,
158 content: &str,
159 target: &TargetTreeIndex,
160) -> AddedFileFindings {
161 let mut reasons = Vec::new();
162 for inc in extract_includes(content) {
163 if is_stdlib_include_lib(&inc.directive) {
164 continue;
165 }
166 if let Err(attempted) = resolve_include(target, path, &inc.directive) {
167 reasons.push(Reason::HeaderFileMissing {
168 source_path: path.clone(),
169 include_directive: inc.directive,
170 attempted_paths: attempted,
171 });
172 }
173 }
174 AddedFileFindings {
175 reasons,
176 missing_test_modules: BTreeMap::new(),
177 }
178}
179
180pub fn is_stdlib_include_lib(directive: &IncludeDirective) -> bool {
186 let path = match directive {
187 IncludeDirective::IncludeLib { path } => path,
188 IncludeDirective::Include { .. } => return false,
189 };
190 let app = path.split('/').next().unwrap_or("");
191 matches!(
192 app,
193 "kernel"
194 | "stdlib"
195 | "compiler"
196 | "common_test"
197 | "ct"
198 | "eunit"
199 | "crypto"
200 | "ssl"
201 | "inets"
202 | "public_key"
203 | "asn1"
204 | "snmp"
205 | "tools"
206 | "wx"
207 | "xmerl"
208 | "syntax_tools"
209 | "edoc"
210 | "debugger"
211 | "diameter"
212 | "erts"
213 | "et"
214 | "ftp"
215 | "megaco"
216 | "mnesia"
217 | "observer"
218 | "odbc"
219 | "os_mon"
220 | "parsetools"
221 | "reltool"
222 | "runtime_tools"
223 | "sasl"
224 | "ssh"
225 | "tftp"
226 | "proper"
227 | "meck"
228 )
229}
230
231fn is_test_suite_path(path: &RelativePath) -> bool {
232 ends_with_ignore_case(path.as_str(), "_SUITE.erl")
233}
234
235fn ends_with_ignore_case(s: &str, suffix: &str) -> bool {
236 let sb = s.as_bytes();
237 let suf = suffix.as_bytes();
238 sb.len() >= suf.len() && sb[sb.len() - suf.len()..].eq_ignore_ascii_case(suf)
239}