Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions src/packs/checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ pub struct ViolationIdentifier {
pub referencing_pack_name: String,
pub defining_pack_name: String,
}

impl ViolationIdentifier {
/// `strict` describes how a violation should be treated, not which violation
/// it is, and `package_todo.yml` has nowhere to record it, so recorded
/// violations are always rebuilt with `strict: false`. Compare through this
/// so a violation in a strict pack can still match its recorded entry.
pub fn recorded_key(&self) -> Self {
Self {
strict: false,
..self.clone()
}
}
}
/// A violation combines an identifier with display metadata.
///
/// `source_location` is intentionally separate from `ViolationIdentifier` because:
Expand Down Expand Up @@ -124,7 +137,7 @@ impl<'a> CheckAllBuilder<'a> {
.cloned()
.collect(),
strict_mode_violations: self
.build_strict_mode_violations()
.build_strict_mode_violations(recorded_violations)
.into_iter()
.collect(),
})
Expand All @@ -142,7 +155,10 @@ impl<'a> CheckAllBuilder<'a> {
self.found_violations
.violations
.iter()
.filter(|v| !recorded_violations.contains(&v.identifier))
.filter(|v| {
!recorded_violations
.contains(&v.identifier.recorded_key())
})
.collect()
};
reportable_violations
Expand All @@ -152,11 +168,11 @@ impl<'a> CheckAllBuilder<'a> {
&mut self,
recorded_violations: &'a HashSet<ViolationIdentifier>,
) -> anyhow::Result<Vec<&'a ViolationIdentifier>> {
let found_violation_identifiers: HashSet<&ViolationIdentifier> = self
let found_violation_identifiers: HashSet<ViolationIdentifier> = self
.found_violations
.violations
.par_iter()
.map(|v| &v.identifier)
.map(|v| v.identifier.recorded_key())
.collect();
let relative_files = self
.found_violations
Expand Down Expand Up @@ -198,23 +214,36 @@ impl<'a> CheckAllBuilder<'a> {

fn is_stale_violation(
relative_files: &HashSet<&str>,
found_violation_identifiers: &HashSet<&ViolationIdentifier>,
found_violation_identifiers: &HashSet<ViolationIdentifier>,
todo_violation_identifier: &ViolationIdentifier,
) -> bool {
let violation_path_exists =
relative_files.contains(todo_violation_identifier.file.as_str());
if violation_path_exists {
!found_violation_identifiers.contains(todo_violation_identifier)
!found_violation_identifiers
.contains(&todo_violation_identifier.recorded_key())
} else {
true // The todo violation references a file that no longer exists
}
}

fn build_strict_mode_violations(&self) -> Vec<Violation> {
/// Strict mode reports violations that are not already recorded in a
/// `package_todo.yml`, matching packwerk's `unlisted_strict_mode_violations`
/// (Shopify/packwerk#368). Turning strict on therefore blocks new violations
/// without also requiring every recorded one to be fixed first.
fn build_strict_mode_violations(
&self,
recorded_violations: &HashSet<ViolationIdentifier>,
) -> Vec<Violation> {
self.found_violations
.violations
.iter()
.filter(|v| v.identifier.strict)
.filter(|v| {
self.configuration.ignore_recorded_violations
|| !recorded_violations
.contains(&v.identifier.recorded_key())
})
.cloned()
.collect()
}
Expand Down
43 changes: 40 additions & 3 deletions tests/check_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,11 +320,31 @@ fn test_check_without_stale_violations() -> Result<(), Box<dyn Error>> {
}

#[test]
fn test_check_with_strict_mode() -> Result<(), Box<dyn Error>> {
fn test_check_with_recorded_strict_mode_violation() -> Result<(), Box<dyn Error>>
{
// The violation is already recorded in packs/foo/package_todo.yml, so
// strict mode tolerates it and only blocks new ones. Matches packwerk's
// `unlisted_strict_mode_violations` (Shopify/packwerk#368).
cargo_bin_cmd!("pks")
.arg("--project-root")
.arg("tests/fixtures/uses_strict_mode")
.arg("check")
.assert()
.code(0)
.stdout(predicate::str::contains("No violations detected!"));

common::teardown();
Ok(())
}

#[test]
fn test_check_with_recorded_strict_mode_violation_ignoring_todo(
) -> Result<(), Box<dyn Error>> {
cargo_bin_cmd!("pks")
.arg("--project-root")
.arg("tests/fixtures/uses_strict_mode")
.arg("check")
.arg("--ignore-recorded-violations")
.assert()
.code(1)
.stdout(predicate::str::contains(
Expand All @@ -338,18 +358,35 @@ fn test_check_with_strict_mode() -> Result<(), Box<dyn Error>> {
Ok(())
}

#[test]
fn test_check_with_unrecorded_strict_mode_violation(
) -> Result<(), Box<dyn Error>> {
// No package_todo.yml entry for this one, so strict mode must still fail.
cargo_bin_cmd!("pks")
.arg("--project-root")
.arg("tests/fixtures/contains_strict_violations")
.arg("check")
.assert()
.code(1)
.stdout(predicate::str::contains(
"packs/foo cannot have privacy violations on packs/bar because strict mode is enabled for privacy violations in the enforcing pack's package.yml file",
));

common::teardown();
Ok(())
}

#[test]
fn test_check_with_strict_mode_output_csv() -> Result<(), Box<dyn Error>> {
cargo_bin_cmd!("pks")
.arg("--project-root")
.arg("tests/fixtures/uses_strict_mode")
.arg("tests/fixtures/contains_strict_violations")
.arg("check")
.arg("-o")
.arg("csv")
.assert()
.code(1)
.stdout(predicate::str::contains("Violation,Strict?,File,Constant,Referencing Pack,Defining Pack,Message"))
.stdout(predicate::str::contains("privacy,true,packs/foo/app/services/foo.rb,::Bar,packs/foo,packs/bar,packs/foo cannot have privacy violations on packs/bar because strict mode is enabled for privacy violations in the enforcing pack\'s package.yml file"))
.stdout(predicate::str::contains(
"privacy,true,packs/foo/app/services/foo.rb,::Bar,packs/foo,packs/bar,packs/foo cannot have privacy violations on packs/bar because strict mode is enabled for privacy violations in the enforcing pack\'s package.yml file",
));
Expand Down