code_moniker_check/scenario/
run.rs1use std::cmp::Ordering;
2use std::path::Path;
3
4use super::Scenario;
5use super::expect::ExpectedViolation;
6use crate::check::command::{
7 CheckRun, FileError, FileReport, MemoryCheckWorkspace, check_project_files_workspace,
8 check_project_workspace,
9};
10use crate::check::config;
11use code_moniker_core::lang::Lang;
12use code_moniker_workspace::lang::path_to_lang;
13
14const RULES_FILE: &str = ".code-moniker.toml";
15
16#[derive(Clone, Debug, Default, Eq, PartialEq)]
17pub struct ScenarioRun {
18 pub actual: Vec<ExpectedViolation>,
19 pub missing: Vec<ExpectedViolation>,
20 pub unexpected: Vec<ExpectedViolation>,
21 pub errors: Vec<String>,
22 pub silent_rules: Vec<String>,
23 pub stale_undemonstrated: Vec<String>,
24}
25
26impl ScenarioRun {
27 fn from_check(run: &CheckRun, root: &Path, scenario: &Scenario) -> Self {
28 let actual = collect_actual(run, root);
29 let (missing, unexpected) = diff_expectations(&scenario.expects, &actual);
30 let (silent_rules, stale_undemonstrated) = coverage(run, scenario);
31 Self {
32 actual,
33 missing,
34 unexpected,
35 errors: collect_errors(run, root),
36 silent_rules,
37 stale_undemonstrated,
38 }
39 }
40
41 pub fn is_match(&self) -> bool {
42 self.missing.is_empty() && self.unexpected.is_empty() && self.errors.is_empty()
43 }
44
45 pub fn mismatch_summary(&self) -> String {
46 let mut lines = Vec::new();
47 for missing in &self.missing {
48 lines.push(format!("missing: {missing}"));
49 }
50 for unexpected in &self.unexpected {
51 lines.push(format!("unexpected: {unexpected}"));
52 }
53 for error in &self.errors {
54 lines.push(format!("error: {error}"));
55 }
56 lines.join("\n")
57 }
58}
59
60impl Scenario {
61 pub fn run(&self, root: &Path, scheme: &str) -> anyhow::Result<ScenarioRun> {
62 let run = self.check(root, &[], scheme, true)?;
63 Ok(ScenarioRun::from_check(&run, root, self))
64 }
65
66 pub fn check(
67 &self,
68 root: &Path,
69 files: &[std::path::PathBuf],
70 scheme: &str,
71 report: bool,
72 ) -> anyhow::Result<CheckRun> {
73 let cfg = config::load_from_str(
74 self.rules.as_deref().unwrap_or(""),
75 RULES_FILE,
76 Some(self.effective_default_rules()),
77 )?;
78 let workspace = self.memory_workspace(root)?;
79 let (reports, errors) = check_scenario_workspace(&workspace, files, &cfg, scheme, report)?;
80 Ok(CheckRun {
81 reports,
82 errors,
83 elapsed_ms: 0,
84 skip_reason: None,
85 })
86 }
87
88 fn memory_workspace(&self, root: &Path) -> anyhow::Result<MemoryCheckWorkspace> {
89 let mut workspace = MemoryCheckWorkspace::new(root);
90 for file in &self.files {
91 let Some(lang) = scenario_file_lang(file)? else {
92 continue;
93 };
94 workspace = workspace.with_file(Path::new(&file.path), &file.body, lang);
95 }
96 Ok(workspace)
97 }
98
99 pub fn bless(&self, document: &str, actual: &[ExpectedViolation]) -> String {
100 let mut entries: Vec<String> = self
101 .undemonstrated
102 .iter()
103 .map(|rule| format!("! {} {}", rule.rule_id, rule.reason))
104 .collect();
105 entries.extend(actual.iter().map(ToString::to_string));
106 let mut body = entries.join("\n");
107 if !body.is_empty() {
108 body.push('\n');
109 }
110 match self.expect_span {
111 Some((start, end)) => format!("{}{}{}", &document[..start], body, &document[end..]),
112 None => {
113 let separator = if document.ends_with('\n') { "" } else { "\n" };
114 format!("{document}{separator}\n```cm:expect\n{body}```\n")
115 }
116 }
117 }
118}
119
120fn check_scenario_workspace(
121 workspace: &MemoryCheckWorkspace,
122 files: &[std::path::PathBuf],
123 cfg: &crate::check::Config,
124 scheme: &str,
125 report: bool,
126) -> anyhow::Result<(Vec<FileReport>, Vec<FileError>)> {
127 if files.is_empty() {
128 return check_project_workspace(workspace.root(), cfg, scheme, report, workspace);
129 }
130 check_project_files_workspace(workspace.root(), files, cfg, scheme, report, workspace)
131}
132
133fn scenario_file_lang(file: &super::ScenarioFile) -> anyhow::Result<Option<Lang>> {
134 match file.fence.as_str() {
135 "" => Ok(Some(path_to_lang(Path::new(&file.path))?)),
136 "rust" | "rs" => Ok(Some(Lang::Rs)),
137 "ts" | "typescript" => Ok(Some(Lang::Ts)),
138 "python" | "py" => Ok(Some(Lang::Python)),
139 "go" => Ok(Some(Lang::Go)),
140 "java" => Ok(Some(Lang::Java)),
141 "cs" | "csharp" => Ok(Some(Lang::Cs)),
142 "sql" | "plpgsql" => Ok(Some(Lang::Sql)),
143 "text" | "txt" | "md" | "markdown" => Ok(None),
144 _ => Ok(Some(path_to_lang(Path::new(&file.path))?)),
145 }
146}
147
148fn collect_actual(run: &CheckRun, root: &Path) -> Vec<ExpectedViolation> {
149 let mut actual: Vec<_> = run
150 .file_violations()
151 .map(|(path, violation)| ExpectedViolation {
152 rule_id: violation.rule_id.clone(),
153 path: relative_display(path, root),
154 lines: violation.lines,
155 })
156 .collect();
157 actual.sort();
158 actual
159}
160
161fn collect_errors(run: &CheckRun, root: &Path) -> Vec<String> {
162 run.error_summaries()
163 .map(|(path, error)| format!("{}: {error}", relative_display(path, root)))
164 .collect()
165}
166
167fn coverage(run: &CheckRun, scenario: &Scenario) -> (Vec<String>, Vec<String>) {
168 let excused: Vec<&str> = scenario
169 .undemonstrated
170 .iter()
171 .map(|rule| rule.rule_id.as_str())
172 .collect();
173 let silent = collect_silent_rules(run);
174 let stale = excused
175 .iter()
176 .filter(|rule_id| !silent.iter().any(|silent| silent == *rule_id))
177 .map(ToString::to_string)
178 .collect();
179 let silent = silent
180 .into_iter()
181 .filter(|rule_id| !excused.contains(&rule_id.as_str()))
182 .collect();
183 (silent, stale)
184}
185
186fn collect_silent_rules(run: &CheckRun) -> Vec<String> {
187 run.rule_violation_totals()
188 .into_iter()
189 .filter(|(_, violations)| *violations == 0)
190 .map(|(rule_id, _)| rule_id.to_string())
191 .collect()
192}
193
194fn relative_display(path: &Path, root: &Path) -> String {
195 path.strip_prefix(root)
196 .unwrap_or(path)
197 .display()
198 .to_string()
199 .replace('\\', "/")
200}
201
202fn diff_expectations(
203 expected: &[ExpectedViolation],
204 actual: &[ExpectedViolation],
205) -> (Vec<ExpectedViolation>, Vec<ExpectedViolation>) {
206 let mut missing = Vec::new();
207 let mut unexpected = Vec::new();
208 let mut left = expected.iter().peekable();
209 let mut right = actual.iter().peekable();
210 while let (Some(expected), Some(actual)) = (left.peek(), right.peek()) {
211 match expected.cmp(actual) {
212 Ordering::Equal => {
213 left.next();
214 right.next();
215 }
216 Ordering::Less => missing.extend(left.next().cloned()),
217 Ordering::Greater => unexpected.extend(right.next().cloned()),
218 }
219 }
220 missing.extend(left.cloned());
221 unexpected.extend(right.cloned());
222 (missing, unexpected)
223}