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
29 changes: 25 additions & 4 deletions src/packs/checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ 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 {

Copy link
Copy Markdown
Contributor

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 strict off ViolationIdentifier and onto Violation instead of normalizing at comparison time. Your comment here already says why: strict describes how a violation should be treated, not which violation it is. The doc comment just below at checker.rs:55-64 sets the same rule for source_location, that the identifier defines sameness for comparison against package_todo.yml, "which doesn't store line/column." strict isn'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, and build_strict_violation_message never reads the field. Constructors are pack.rs:195, which is where #41 starts and which then stops having to invent strict: false, plus pack_checker.rs:180 and four test constructors. You'd get all three comparison sites back to plain contains(&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 strict from a manual PartialEq/Hash. Violation's derived Eq/Hash delegate to the identifier, and get_all_violations dedupes into a HashSet<Violation>, so making strict: true equal strict: false lets an insert keep the wrong flag, which build_strict_mode_violations then filters on.

recorded_key() is correct as written. This is about where the field lives, not about a bug.

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 @@ -142,7 +156,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 +169,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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this moves from HashSet<&ViolationIdentifier> to an owned HashSet<ViolationIdentifier>, so it now clones 4 Strings per found violation rather than copying a pointer. Small next to parsing 15.6k files, so fine to leave.

If you want the cheaper version, a borrowed key tuple that excludes strict avoids the allocations entirely. Moving strict onto Violation (see my note on recorded_key) would also let this go back to borrowing.

.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 @@ -196,9 +213,13 @@ impl<'a> CheckAllBuilder<'a> {
Ok(stale_violations)
}

/// `found_violation_identifiers` is keyed by [`ViolationIdentifier::recorded_key`].
/// `todo_violation_identifier` needs no such normalization: it comes from
/// `pack_set.all_violations`, which rebuilds every recorded violation with
/// `strict: false` already, so it is its own recorded key.
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 =
Expand Down
13 changes: 12 additions & 1 deletion tests/check_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,10 @@ fn test_check_without_stale_violations() -> Result<(), Box<dyn Error>> {

#[test]
fn test_check_with_strict_mode() -> Result<(), Box<dyn Error>> {
// The violation here IS recorded in packs/foo/package_todo.yml, so it has to
// match its recorded entry: reported neither as a new violation nor as a
// stale todo. Strict mode still fails the run, which is what keeps this at
// exit 1, so the two strict messages are the whole of the output.
cargo_bin_cmd!("pks")
.arg("--project-root")
.arg("tests/fixtures/uses_strict_mode")
Expand All @@ -332,7 +336,14 @@ fn test_check_with_strict_mode() -> Result<(), Box<dyn Error>> {
))
.stdout(predicate::str::contains(
"packs/foo cannot have dependency violations on packs/bar because strict mode is enabled for dependency violations in the enforcing pack's package.yml file",
));
))
.stdout(
predicate::str::contains(
"There were stale violations found, please run `packs update`",
)
.not(),
)
.stdout(predicate::str::contains("violation(s) detected:").not());

common::teardown();
Ok(())
Expand Down