Skip to content

Commit 82d25f5

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 ae124b3 commit 82d25f5

16 files changed

Lines changed: 441 additions & 419 deletions

src/packs.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub(crate) mod monkey_patch_detection;
1818
pub(crate) mod pack;
1919
pub(crate) mod parsing;
2020
pub(crate) mod raw_configuration;
21+
pub(crate) mod template;
2122
pub(crate) mod text;
2223
pub(crate) mod walk_directory;
2324

@@ -101,15 +102,16 @@ pub fn check(
101102
OutputFormat::Packwerk => {
102103
text::write_text(
103104
&result,
105+
configuration,
104106
std::io::stdout(),
105107
color_mode_for(color),
106108
)?;
107109
}
108110
OutputFormat::CSV => {
109-
csv::write_csv(&result, std::io::stdout())?;
111+
csv::write_csv(&result, configuration, std::io::stdout())?;
110112
}
111113
OutputFormat::JSON => {
112-
json::write_json(&result, std::io::stdout())?;
114+
json::write_json(&result, configuration, std::io::stdout())?;
113115
}
114116
}
115117

src/packs/checker.rs

Lines changed: 8 additions & 126 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,77 +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.message.replace(
106-
REFERENCE_LOCATION_PLACEHOLDER,
107-
&format!("{}\n", location),
108-
)
109-
} else {
110-
// Default template - prepend location
111-
format!("{}\n{}", location, violation.message)
112-
}
113-
}
114-
115-
fn write_violations(&self, f: &mut Formatter<'_>) -> fmt::Result {
116-
if !self.reportable_violations.is_empty() {
117-
let mut sorted_violations: Vec<&Violation> =
118-
self.reportable_violations.iter().collect();
119-
sorted_violations.sort_by(|a, b| a.message.cmp(&b.message));
120-
121-
writeln!(f, "{} violation(s) detected:", sorted_violations.len())?;
122-
123-
for violation in sorted_violations {
124-
let formatted = Self::format_violation_message(violation);
125-
writeln!(f, "{}\n", formatted)?;
126-
}
127-
}
128-
129-
if !self.stale_violations.is_empty() {
130-
writeln!(
131-
f,
132-
"There were stale violations found, please run `{} update`",
133-
bin_locater::packs_bin_name(),
134-
)?;
135-
}
136-
137-
if !self.strict_mode_violations.is_empty() {
138-
for v in self.strict_mode_violations.iter() {
139-
let error_message =
140-
build_strict_violation_message(&v.identifier);
141-
writeln!(f, "{}", error_message)?;
142-
}
143-
}
144-
Ok(())
145-
}
146-
}
147-
148-
impl Display for CheckAllResult {
149-
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
150-
if self.has_violations() {
151-
self.write_violations(f)
152-
} else {
153-
write!(f, "No violations detected!")
154-
}
155-
}
15690
}
15791
struct CheckAllBuilder<'a> {
15892
configuration: &'a Configuration,
@@ -543,57 +477,5 @@ fn remove_reference_to_dependency(
543477
write_pack_to_disk(&updated_pack)?;
544478
Ok(())
545479
}
546-
#[cfg(test)]
547-
mod tests {
548-
use std::collections::HashSet;
549-
550-
use crate::packs::checker::{
551-
CheckAllResult, Violation, ViolationIdentifier,
552-
};
553-
use crate::packs::SourceLocation;
554-
555-
#[test]
556-
fn test_write_violations() {
557-
let check_result = CheckAllResult {
558-
reportable_violations: [Violation {
559-
message: "Privacy violation: `::Foo::PrivateClass` is private to `foo`, but referenced from `bar`".to_string(),
560-
identifier: ViolationIdentifier {
561-
violation_type: "Privacy".to_string(),
562-
strict: false,
563-
file: "foo/bar/file1.rb".to_string(),
564-
constant_name: "::Foo::PrivateClass".to_string(),
565-
referencing_pack_name: "bar".to_string(),
566-
defining_pack_name: "foo".to_string(),
567-
},
568-
source_location: SourceLocation { line: 10, column: 5 },
569-
},
570-
Violation {
571-
message: "Dependency violation: `::Foo::AnotherClass` is not allowed to depend on `::Bar::SomeClass`".to_string(),
572-
identifier: ViolationIdentifier {
573-
violation_type: "Dependency".to_string(),
574-
strict: false,
575-
file: "foo/bar/file2.rb".to_string(),
576-
constant_name: "::Foo::AnotherClass".to_string(),
577-
referencing_pack_name: "foo".to_string(),
578-
defining_pack_name: "bar".to_string(),
579-
},
580-
source_location: SourceLocation { line: 15, column: 3 },
581-
}].iter().cloned().collect(),
582-
stale_violations: Vec::new(),
583-
strict_mode_violations: HashSet::new(),
584-
};
585-
586-
let expected_output = "2 violation(s) detected:
587-
foo/bar/file2.rb:15:3
588-
Dependency violation: `::Foo::AnotherClass` is not allowed to depend on `::Bar::SomeClass`
589-
590-
foo/bar/file1.rb:10:5
591-
Privacy violation: `::Foo::PrivateClass` is private to `foo`, but referenced from `bar`
592-
593-
";
594-
595-
let actual = format!("{}", check_result);
596-
597-
assert_eq!(actual, expected_output);
598-
}
599-
}
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: 34 additions & 6 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,46 @@ 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 {
3333
build_expected_violation_with_constant(
34-
message,
3534
violation_type,
3635
strict,
3736
String::from("::Bar"),
3837
)
3938
}
4039

40+
pub fn build_expected_violation_with_layers(
41+
violation_type: CheckerType,
42+
strict: bool,
43+
defining_layer: &str,
44+
referencing_layer: &str,
45+
) -> Violation {
46+
Violation {
47+
identifier: ViolationIdentifier {
48+
violation_type,
49+
strict,
50+
file: String::from("packs/foo/app/services/foo.rb"),
51+
constant_name: String::from("::Bar"),
52+
referencing_pack_name: String::from("packs/foo"),
53+
defining_pack_name: String::from("packs/bar"),
54+
},
55+
source_location: SourceLocation { line: 3, column: 1 },
56+
referencing_pack_relative_yml: String::from(
57+
"packs/foo/package.yml",
58+
),
59+
defining_layer: Some(defining_layer.to_string()),
60+
referencing_layer: Some(referencing_layer.to_string()),
61+
}
62+
}
63+
4164
pub fn build_expected_violation_with_constant(
42-
message: String,
43-
violation_type: String,
65+
violation_type: CheckerType,
4466
strict: bool,
4567
constant_name: String,
4668
) -> Violation {
4769
Violation {
48-
message,
4970
identifier: ViolationIdentifier {
5071
violation_type,
5172
strict,
@@ -55,6 +76,11 @@ pub mod tests {
5576
defining_pack_name: String::from("packs/bar"),
5677
},
5778
source_location: SourceLocation { line: 3, column: 1 },
79+
referencing_pack_relative_yml: String::from(
80+
"packs/foo/package.yml",
81+
),
82+
defining_layer: None,
83+
referencing_layer: None,
5884
}
5985
}
6086

@@ -79,8 +105,10 @@ pub mod tests {
79105
}
80106

81107
pub fn default_referencing_pack() -> Pack {
108+
use std::path::PathBuf;
82109
Pack {
83110
name: "packs/foo".to_owned(),
111+
relative_path: PathBuf::from("packs/foo"),
84112
..Pack::default()
85113
}
86114
}

src/packs/checker/dependency.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,15 @@ mod tests {
220220
name: "packs/bar".to_owned(),
221221
..default_defining_pack()
222222
}),
223-
referencing_pack: Pack{
223+
referencing_pack: Pack {
224224
relative_path: PathBuf::from("packs/foo"),
225225
enforce_dependencies: Some(CheckerSetting::True),
226-
..default_referencing_pack()},
226+
..default_referencing_pack()
227+
},
227228
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)),
229+
CheckerType::Dependency,
230+
false,
231+
)),
230232
};
231233
test_check(
232234
&Checker {
@@ -248,13 +250,15 @@ mod tests {
248250
name: "packs/bar".to_owned(),
249251
..default_defining_pack()
250252
}),
251-
referencing_pack: Pack{
253+
referencing_pack: Pack {
252254
relative_path: PathBuf::from("packs/foo"),
253255
enforce_dependencies: Some(CheckerSetting::Strict),
254-
..default_referencing_pack()},
256+
..default_referencing_pack()
257+
},
255258
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)),
259+
CheckerType::Dependency,
260+
true,
261+
)),
258262
};
259263
test_check(
260264
&Checker {

src/packs/checker/folder_privacy.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,14 @@ mod tests {
8585
enforce_folder_privacy: Some(CheckerSetting::True),
8686
..default_defining_pack()
8787
}),
88-
referencing_pack: Pack{
88+
referencing_pack: Pack {
8989
relative_path: PathBuf::from("packs/foo"),
90-
..default_referencing_pack()},
90+
..default_referencing_pack()
91+
},
9192
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)),
93+
CheckerType::FolderPrivacy,
94+
false,
95+
)),
9496
};
9597
test_check(
9698
&Checker {
@@ -150,12 +152,14 @@ mod tests {
150152
enforce_folder_privacy: Some(CheckerSetting::Strict),
151153
..default_defining_pack()
152154
}),
153-
referencing_pack: Pack{
155+
referencing_pack: Pack {
154156
relative_path: PathBuf::from("packs/foo"),
155-
..default_referencing_pack()},
157+
..default_referencing_pack()
158+
},
156159
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)),
160+
CheckerType::FolderPrivacy,
161+
true,
162+
)),
159163
};
160164
test_check(
161165
&Checker {

0 commit comments

Comments
 (0)