-
Notifications
You must be signed in to change notification settings - Fork 2
Match recorded violations in strict packs #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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: | ||
|
|
@@ -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(), | ||
| }) | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: this moves from If you want the cheaper version, a borrowed key tuple that excludes |
||
| .found_violations | ||
| .violations | ||
| .par_iter() | ||
| .map(|v| &v.identifier) | ||
| .map(|v| v.identifier.recorded_key()) | ||
| .collect(); | ||
| let relative_files = self | ||
| .found_violations | ||
|
|
@@ -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()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: this call does nothing. The found-side |
||
| } 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| { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking:
// src/packs/package_todo.rs:144
if violation.identifier.strict {
continue;
}That line is pre-existing and untouched here, but this filter is what starts depending on those todo entries. On A routine I'd call this blocking rather than a pre-existing quirk to port later, because packwerk does the opposite here: # lib/packwerk/offense_collection.rb#add_offense
if strict_mode_violation?(offense)
add_to_package_todo(offense) if already_listed
strict_mode_violations << offense
else
add_to_package_todo(offense)
endAn unlisted strict violation never gets added, so you can't silence strict mode by running Suggested fix: in This doesn't require changing |
||
| self.configuration.ignore_recorded_violations | ||
| || !recorded_violations | ||
| .contains(&v.identifier.recorded_key()) | ||
| }) | ||
| .cloned() | ||
| .collect() | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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!")); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test gap worth pinning: a strict pack with some recorded and some unrecorded violations in the same run. I checked and the behavior is right. Adding a second, unrecorded reference alongside the recorded Also worth a |
||
|
|
||
| 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( | ||
|
|
@@ -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", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No coverage lost here, just flagging why for the record: the removed line is byte-identical to the one kept below it, so this drops a duplicate assertion. The duplication was pointing at something real, though. Unrecorded strict violations get reported twice, since |
||
| )); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Design note, non-blocking, and fine to defer to a follow-up.
Consider moving
strictoffViolationIdentifierand ontoViolationinstead of normalizing at comparison time. Your comment here already says why:strictdescribes how a violation should be treated, not which violation it is. The doc comment just below atchecker.rs:55-64sets the same rule forsource_location, that the identifier defines sameness for comparison againstpackage_todo.yml, "which doesn't store line/column."strictisn't stored there either.The change is mechanical. Every reader of
.identifier.strict(json.rs:56,90;csv.rs:12,53;package_todo.rs:144) already has a full&Violation, andbuild_strict_violation_messagenever reads the field. Constructors arepack.rs:195, which is where #41 starts and which then stops having to inventstrict: false, pluspack_checker.rs:180and four test constructors. You'd get all three comparison sites back to plaincontains(&v.identifier), #41 becomes impossible to express instead of something a future call site has to remember to guard, and the extra allocations go away.One alternative to skip: excluding
strictfrom a manualPartialEq/Hash.Violation's derivedEq/Hashdelegate to the identifier, andget_all_violationsdedupes into aHashSet<Violation>, so makingstrict: trueequalstrict: falselets an insert keep the wrong flag, whichbuild_strict_mode_violationsthen filters on.recorded_key()is correct as written. This is about where the field lives, not about a bug.