Skip to content

Commit 58760e2

Browse files
committed
Centralize template expansion in dedicated module
Template expansion was scattered between checkers and formatters, making it difficult to maintain consistency between default and user-supplied templates. The reference_location placeholder needed special handling per output format. Changes: - Add template.rs with expand(), build_violation_vars(), and helpers - Move all template expansion from pack_checker to formatters - Violation struct now stores data only (no pre-expanded message) - ViolationIdentifier.violation_type is now CheckerType enum - Each formatter handles reference_location appropriately: - text: colorized or plain based on color mode - json: cleared (location in separate fields) - csv: plain format - Default templates restored to include {{reference_location}}
1 parent 59c6b29 commit 58760e2

16 files changed

Lines changed: 387 additions & 397 deletions

src/packs.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub(crate) mod dependencies;
1515
pub(crate) mod ignored;
1616
pub(crate) mod json;
1717
pub(crate) mod monkey_patch_detection;
18+
pub(crate) mod template;
1819
pub(crate) mod text;
1920
pub(crate) mod pack;
2021
pub(crate) mod parsing;
@@ -99,13 +100,18 @@ pub fn check(
99100

100101
match output_format {
101102
OutputFormat::Packwerk => {
102-
text::write_text(&result, std::io::stdout(), color_mode_for(color))?;
103+
text::write_text(
104+
&result,
105+
configuration,
106+
std::io::stdout(),
107+
color_mode_for(color),
108+
)?;
103109
}
104110
OutputFormat::CSV => {
105-
csv::write_csv(&result, std::io::stdout())?;
111+
csv::write_csv(&result, configuration, std::io::stdout())?;
106112
}
107113
OutputFormat::JSON => {
108-
json::write_json(&result, std::io::stdout())?;
114+
json::write_json(&result, configuration, std::io::stdout())?;
109115
}
110116
}
111117

src/packs/checker.rs

Lines changed: 8 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ use rayon::prelude::IntoParallelRefIterator;
2424
use rayon::prelude::ParallelIterator;
2525
use reference::Reference;
2626
use std::collections::HashMap;
27-
use std::fmt;
28-
use std::fmt::Display;
29-
use std::fmt::Formatter;
3027
use std::{collections::HashSet, path::PathBuf};
3128
use tracing::debug;
3229

@@ -35,7 +32,7 @@ use super::reference_extractor::get_all_references;
3532

3633
#[derive(PartialEq, Clone, Eq, Hash, Debug)]
3734
pub struct ViolationIdentifier {
38-
pub violation_type: String,
35+
pub violation_type: CheckerType,
3936
pub strict: bool,
4037
pub file: String,
4138
pub constant_name: String,
@@ -52,13 +49,15 @@ pub struct ViolationIdentifier {
5249
/// - Keeping line/column out of the identity makes violations stable across
5350
/// minor code movements that shift line numbers
5451
///
55-
/// `message` contains the violation description without the file location prefix.
56-
/// Formatters are responsible for combining source_location with message as needed.
52+
/// Violations store only data - template expansion happens in formatters.
5753
#[derive(PartialEq, Clone, Eq, Hash, Debug)]
5854
pub struct Violation {
59-
pub message: String,
6055
pub identifier: ViolationIdentifier,
6156
pub source_location: SourceLocation,
57+
// Additional data for template expansion:
58+
pub referencing_pack_relative_yml: String,
59+
pub defining_layer: Option<String>,
60+
pub referencing_layer: Option<String>,
6261
}
6362

6463
pub(crate) trait CheckerInterface {
@@ -82,76 +81,12 @@ pub struct CheckAllResult {
8281
pub strict_mode_violations: HashSet<Violation>,
8382
}
8483

85-
const REFERENCE_LOCATION_PLACEHOLDER: &str = "{{reference_location}}";
86-
8784
impl CheckAllResult {
8885
pub fn has_violations(&self) -> bool {
8986
!self.reportable_violations.is_empty()
9087
|| !self.stale_violations.is_empty()
9188
|| !self.strict_mode_violations.is_empty()
9289
}
93-
94-
/// Format a violation message, substituting `{{reference_location}}` if present.
95-
fn format_violation_message(violation: &Violation) -> String {
96-
let location = format!(
97-
"{}:{}:{}",
98-
violation.identifier.file,
99-
violation.source_location.line,
100-
violation.source_location.column
101-
);
102-
103-
if violation.message.contains(REFERENCE_LOCATION_PLACEHOLDER) {
104-
// Custom template uses {{reference_location}} - substitute it
105-
violation
106-
.message
107-
.replace(REFERENCE_LOCATION_PLACEHOLDER, &format!("{}\n", location))
108-
} else {
109-
// Default template - prepend location
110-
format!("{}\n{}", location, violation.message)
111-
}
112-
}
113-
114-
fn write_violations(&self, f: &mut Formatter<'_>) -> fmt::Result {
115-
if !self.reportable_violations.is_empty() {
116-
let mut sorted_violations: Vec<&Violation> =
117-
self.reportable_violations.iter().collect();
118-
sorted_violations.sort_by(|a, b| a.message.cmp(&b.message));
119-
120-
writeln!(f, "{} violation(s) detected:", sorted_violations.len())?;
121-
122-
for violation in sorted_violations {
123-
let formatted = Self::format_violation_message(violation);
124-
writeln!(f, "{}\n", formatted)?;
125-
}
126-
}
127-
128-
if !self.stale_violations.is_empty() {
129-
writeln!(
130-
f,
131-
"There were stale violations found, please run `{} update`",
132-
bin_locater::packs_bin_name(),
133-
)?;
134-
}
135-
136-
if !self.strict_mode_violations.is_empty() {
137-
for v in self.strict_mode_violations.iter() {
138-
let error_message =
139-
build_strict_violation_message(&v.identifier);
140-
writeln!(f, "{}", error_message)?;
141-
}
142-
}
143-
Ok(())
144-
}
145-
}
146-
147-
impl Display for CheckAllResult {
148-
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
149-
if self.has_violations() {
150-
self.write_violations(f)
151-
} else {
152-
write!(f, "No violations detected!")
153-
}
154-
}
15590
}
15691
struct CheckAllBuilder<'a> {
15792
configuration: &'a Configuration,
@@ -542,57 +477,5 @@ fn remove_reference_to_dependency(
542477
write_pack_to_disk(&updated_pack)?;
543478
Ok(())
544479
}
545-
#[cfg(test)]
546-
mod tests {
547-
use std::collections::HashSet;
548-
549-
use crate::packs::checker::{
550-
CheckAllResult, Violation, ViolationIdentifier,
551-
};
552-
use crate::packs::SourceLocation;
553-
554-
#[test]
555-
fn test_write_violations() {
556-
let check_result = CheckAllResult {
557-
reportable_violations: [Violation {
558-
message: "Privacy violation: `::Foo::PrivateClass` is private to `foo`, but referenced from `bar`".to_string(),
559-
identifier: ViolationIdentifier {
560-
violation_type: "Privacy".to_string(),
561-
strict: false,
562-
file: "foo/bar/file1.rb".to_string(),
563-
constant_name: "::Foo::PrivateClass".to_string(),
564-
referencing_pack_name: "bar".to_string(),
565-
defining_pack_name: "foo".to_string(),
566-
},
567-
source_location: SourceLocation { line: 10, column: 5 },
568-
},
569-
Violation {
570-
message: "Dependency violation: `::Foo::AnotherClass` is not allowed to depend on `::Bar::SomeClass`".to_string(),
571-
identifier: ViolationIdentifier {
572-
violation_type: "Dependency".to_string(),
573-
strict: false,
574-
file: "foo/bar/file2.rb".to_string(),
575-
constant_name: "::Foo::AnotherClass".to_string(),
576-
referencing_pack_name: "foo".to_string(),
577-
defining_pack_name: "bar".to_string(),
578-
},
579-
source_location: SourceLocation { line: 15, column: 3 },
580-
}].iter().cloned().collect(),
581-
stale_violations: Vec::new(),
582-
strict_mode_violations: HashSet::new(),
583-
};
584-
585-
let expected_output = "2 violation(s) detected:
586-
foo/bar/file2.rb:15:3
587-
Dependency violation: `::Foo::AnotherClass` is not allowed to depend on `::Bar::SomeClass`
588-
589-
foo/bar/file1.rb:10:5
590-
Privacy violation: `::Foo::PrivateClass` is private to `foo`, but referenced from `bar`
591-
592-
";
593-
594-
let actual = format!("{}", check_result);
595-
596-
assert_eq!(actual, expected_output);
597-
}
598-
}
480+
// Note: Display impl was removed from CheckAllResult. Use write_text() directly with Configuration.
481+
// Tests for text formatting are in text.rs

src/packs/checker/common_test.rs

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod tests {
77
checker::{
88
reference::Reference, CheckerInterface, ViolationIdentifier,
99
},
10+
checker_configuration::CheckerType,
1011
pack::Pack,
1112
Configuration, PackSet, SourceLocation, Violation,
1213
};
@@ -26,26 +27,40 @@ pub mod tests {
2627
}
2728

2829
pub fn build_expected_violation(
29-
message: String,
30-
violation_type: String,
30+
violation_type: CheckerType,
3131
strict: bool,
3232
) -> Violation {
33-
build_expected_violation_with_constant(
34-
message,
35-
violation_type,
36-
strict,
37-
String::from("::Bar"),
38-
)
33+
build_expected_violation_with_constant(violation_type, strict, String::from("::Bar"))
34+
}
35+
36+
pub fn build_expected_violation_with_layers(
37+
violation_type: CheckerType,
38+
strict: bool,
39+
defining_layer: &str,
40+
referencing_layer: &str,
41+
) -> Violation {
42+
Violation {
43+
identifier: ViolationIdentifier {
44+
violation_type,
45+
strict,
46+
file: String::from("packs/foo/app/services/foo.rb"),
47+
constant_name: String::from("::Bar"),
48+
referencing_pack_name: String::from("packs/foo"),
49+
defining_pack_name: String::from("packs/bar"),
50+
},
51+
source_location: SourceLocation { line: 3, column: 1 },
52+
referencing_pack_relative_yml: String::from("packs/foo/package.yml"),
53+
defining_layer: Some(defining_layer.to_string()),
54+
referencing_layer: Some(referencing_layer.to_string()),
55+
}
3956
}
4057

4158
pub fn build_expected_violation_with_constant(
42-
message: String,
43-
violation_type: String,
59+
violation_type: CheckerType,
4460
strict: bool,
4561
constant_name: String,
4662
) -> Violation {
4763
Violation {
48-
message,
4964
identifier: ViolationIdentifier {
5065
violation_type,
5166
strict,
@@ -55,6 +70,9 @@ pub mod tests {
5570
defining_pack_name: String::from("packs/bar"),
5671
},
5772
source_location: SourceLocation { line: 3, column: 1 },
73+
referencing_pack_relative_yml: String::from("packs/foo/package.yml"),
74+
defining_layer: None,
75+
referencing_layer: None,
5876
}
5977
}
6078

@@ -79,8 +97,10 @@ pub mod tests {
7997
}
8098

8199
pub fn default_referencing_pack() -> Pack {
100+
use std::path::PathBuf;
82101
Pack {
83102
name: "packs/foo".to_owned(),
103+
relative_path: PathBuf::from("packs/foo"),
84104
..Pack::default()
85105
}
86106
}

src/packs/checker/dependency.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,7 @@ mod tests {
225225
enforce_dependencies: Some(CheckerSetting::True),
226226
..default_referencing_pack()},
227227
expected_violation: Some(build_expected_violation(
228-
"Dependency violation: `::Bar` belongs to `packs/bar`, but `packs/foo/package.yml` does not specify a dependency on `packs/bar`.".to_string(),
229-
"dependency".to_string(), false)),
228+
CheckerType::Dependency, false)),
230229
};
231230
test_check(
232231
&Checker {
@@ -253,8 +252,7 @@ mod tests {
253252
enforce_dependencies: Some(CheckerSetting::Strict),
254253
..default_referencing_pack()},
255254
expected_violation: Some(build_expected_violation(
256-
"Dependency violation: `::Bar` belongs to `packs/bar`, but `packs/foo/package.yml` does not specify a dependency on `packs/bar`.".to_string(),
257-
"dependency".to_string(), true)),
255+
CheckerType::Dependency, true)),
258256
};
259257
test_check(
260258
&Checker {

src/packs/checker/folder_privacy.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,7 @@ mod tests {
8989
relative_path: PathBuf::from("packs/foo"),
9090
..default_referencing_pack()},
9191
expected_violation: Some(build_expected_violation(
92-
"Folder Privacy violation: `::Bar` belongs to `packs/bar`, which is private to `packs/foo` as it is not a sibling pack or parent pack.".to_string(),
93-
"folder_privacy".to_string(), false)),
92+
CheckerType::FolderPrivacy, false)),
9493
};
9594
test_check(
9695
&Checker {
@@ -154,8 +153,7 @@ mod tests {
154153
relative_path: PathBuf::from("packs/foo"),
155154
..default_referencing_pack()},
156155
expected_violation: Some(build_expected_violation(
157-
"Folder Privacy violation: `::Bar` belongs to `packs/bar`, which is private to `packs/foo` as it is not a sibling pack or parent pack.".to_string(),
158-
"folder_privacy".to_string(), true)),
156+
CheckerType::FolderPrivacy, true)),
159157
};
160158
test_check(
161159
&Checker {

src/packs/checker/layer.rs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::collections::HashMap;
2-
31
use super::pack_checker::PackChecker;
42
use super::{CheckerInterface, ValidatorInterface};
53
use crate::packs::checker::Reference;
@@ -120,10 +118,7 @@ impl CheckerInterface for Checker {
120118
return Ok(None);
121119
}
122120

123-
let mut map: HashMap<&str, String> = HashMap::new();
124-
map.insert("{{defining_layer}}", defining_layer.into());
125-
map.insert("{{referencing_layer}}", referencing_layer.into());
126-
pack_checker.violation(Some(map))
121+
pack_checker.violation(Some((defining_layer, referencing_layer)))
127122
}
128123
_ => Ok(None),
129124
}
@@ -141,7 +136,7 @@ mod tests {
141136
use std::path::PathBuf;
142137

143138
use crate::packs::checker::common_test::tests::{
144-
build_expected_violation, default_defining_pack,
139+
build_expected_violation_with_layers, default_defining_pack,
145140
default_referencing_pack, test_check, TestChecker,
146141
};
147142
use crate::packs::checker_configuration::CheckerType;
@@ -204,9 +199,8 @@ mod tests {
204199
layer: Some("utilities".to_string()),
205200
..default_referencing_pack()
206201
},
207-
expected_violation: Some(build_expected_violation(
208-
"Layer violation: `::Bar` belongs to `packs/bar` (whose layer is `product`) cannot be accessed from `packs/foo` (whose layer is `utilities`)".to_string(),
209-
"layer".to_string(), false)),
202+
expected_violation: Some(build_expected_violation_with_layers(
203+
CheckerType::Layer, false, "product", "utilities")),
210204
};
211205
test_check(&checker_with_layers(), &mut test_checker)
212206
}
@@ -228,9 +222,8 @@ mod tests {
228222
layer: Some("utilities".to_string()),
229223
..default_referencing_pack()
230224
},
231-
expected_violation: Some(build_expected_violation(
232-
"Layer violation: `::Bar` belongs to `packs/bar` (whose layer is `product`) cannot be accessed from `packs/foo` (whose layer is `utilities`)".to_string(),
233-
"layer".to_string(), true)),
225+
expected_violation: Some(build_expected_violation_with_layers(
226+
CheckerType::Layer, true, "product", "utilities")),
234227
};
235228
test_check(&checker_with_layers(), &mut test_checker)
236229
}

0 commit comments

Comments
 (0)