Skip to content

Commit 1e7feda

Browse files
authored
feat: generalized ignore comments (#200)
1 parent 18c21d2 commit 1e7feda

File tree

5 files changed

+90
-44
lines changed

5 files changed

+90
-44
lines changed

Diff for: Cargo.lock

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ serde-sarif = "0.6.5"
3232
serde_json = "1.0.133"
3333
serde_yaml = "0.9.34"
3434
terminal-link = "0.1.0"
35-
yamlpath = "0.11.1"
35+
yamlpath = "0.12.0"
3636

3737
[profile.release]
3838
lto = true

Diff for: docs/usage.md

+4-11
Original file line numberDiff line numberDiff line change
@@ -112,21 +112,14 @@ choose to *selectively ignore* results via either special ignore comments
112112
113113
Ignore comment support was added in `v0.6.0`.
114114
115-
!!! tip
116-
117-
Ignore comments are currently limited to a single rule at a time.
118-
119-
This means that you **can't currently** write something like
120-
`zizmor: ignore[*]` or `zizmor: ignore[foo,bar]` to ignore more than
121-
one thing per line.
122-
123-
If support for more generalized ignore comments is important to you,
124-
see #189 for ongoing discussion about design.
125-
126115
Findings can be ignored inline with `# zizmor: ignore[rulename]` comments.
127116
This is ideal for one-off ignores, where a whole `zizmor.yml` configuration
128117
file would be too heavyweight.
129118
119+
Multiple different audits can be ignored with a single comment by
120+
separating each rule with a comma, e.g.
121+
`# zizmor: ignore[artipacked,ref-confusion]`.
122+
130123
These comments can be placed anywhere in any span identified by a finding.
131124
132125
For example, to ignore a single `artipacked` finding:

Diff for: src/finding/locate.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
use anyhow::Result;
55

6-
use super::{ConcreteLocation, Feature, SymbolicLocation};
6+
use super::{Comment, ConcreteLocation, Feature, SymbolicLocation};
77
use crate::models::Workflow;
88

99
pub(crate) struct Locator {}
@@ -51,6 +51,12 @@ impl Locator {
5151
location: ConcreteLocation::from(&feature.location),
5252
parent_location: ConcreteLocation::from(&parent_feature.location),
5353
feature: workflow.document.extract_with_leading_whitespace(&feature),
54+
comments: workflow
55+
.document
56+
.feature_comments(&feature)
57+
.into_iter()
58+
.map(Comment)
59+
.collect(),
5460
parent_feature: workflow
5561
.document
5662
.extract_with_leading_whitespace(&parent_feature),

Diff for: src/finding/mod.rs

+76-29
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
//! Models and APIs for handling findings and their locations.
22
3-
use std::borrow::Cow;
3+
use std::{borrow::Cow, sync::LazyLock};
44

55
use anyhow::Result;
66
use clap::ValueEnum;
77
use locate::Locator;
8+
use regex::Regex;
89
use serde::Serialize;
910
use terminal_link::Link;
1011

@@ -186,6 +187,29 @@ impl From<&yamlpath::Location> for ConcreteLocation {
186187
}
187188
}
188189

190+
static IGNORE_EXPR: LazyLock<Regex> =
191+
LazyLock::new(|| Regex::new(r"^# zizmor: ignore\[(.+)\]\s*$").unwrap());
192+
193+
/// Represents a single source comment.
194+
#[derive(Serialize)]
195+
#[serde(transparent)]
196+
pub(crate) struct Comment<'w>(&'w str);
197+
198+
impl<'w> Comment<'w> {
199+
fn ignores(&self, rule_id: &str) -> bool {
200+
// Extracts foo,bar from `# zizmor: ignore[foo,bar]`
201+
let Some(caps) = IGNORE_EXPR.captures(self.0) else {
202+
return false;
203+
};
204+
205+
caps.get(1)
206+
.unwrap()
207+
.as_str()
208+
.split(",")
209+
.any(|r| r.trim() == rule_id)
210+
}
211+
}
212+
189213
/// An extracted feature, along with its concrete location.
190214
#[derive(Serialize)]
191215
pub(crate) struct Feature<'w> {
@@ -200,6 +224,9 @@ pub(crate) struct Feature<'w> {
200224
/// The feature's textual content.
201225
pub(crate) feature: &'w str,
202226

227+
/// Any comments within the feature's span.
228+
pub(crate) comments: Vec<Comment<'w>>,
229+
203230
/// The feature's parent's textual content.
204231
pub(crate) parent_feature: &'w str,
205232
}
@@ -273,7 +300,7 @@ impl<'w> FindingBuilder<'w> {
273300
.map(|l| l.clone().concretize(workflow))
274301
.collect::<Result<Vec<_>>>()?;
275302

276-
let should_ignore = self.ignored_from_inlined_comment(workflow, &locations, self.ident);
303+
let should_ignore = self.ignored_from_inlined_comment(&locations, self.ident);
277304

278305
Ok(Finding {
279306
ident: self.ident,
@@ -288,34 +315,54 @@ impl<'w> FindingBuilder<'w> {
288315
})
289316
}
290317

291-
fn ignored_from_inlined_comment(
292-
&self,
293-
workflow: &Workflow,
294-
locations: &[Location],
295-
id: &str,
296-
) -> bool {
297-
let document_lines = &workflow.document.source().lines().collect::<Vec<_>>();
298-
let line_ranges = locations
318+
fn ignored_from_inlined_comment(&self, locations: &[Location], id: &str) -> bool {
319+
locations
299320
.iter()
300-
.map(|l| {
301-
(
302-
l.concrete.location.start_point.row,
303-
l.concrete.location.end_point.row,
304-
)
305-
})
306-
.collect::<Vec<_>>();
307-
308-
let inlined_ignore = format!("# zizmor: ignore[{}]", id);
309-
for (start, end) in line_ranges {
310-
for document_line in start..(end + 1) {
311-
if let Some(line) = document_lines.get(document_line) {
312-
if line.rfind(&inlined_ignore).is_some() {
313-
return true;
314-
}
315-
}
316-
}
317-
}
321+
.flat_map(|l| &l.concrete.comments)
322+
.any(|c| c.ignores(id))
323+
}
324+
}
318325

319-
false
326+
#[cfg(test)]
327+
mod tests {
328+
use crate::finding::Comment;
329+
330+
#[test]
331+
fn test_comment_ignores() {
332+
let cases = &[
333+
// Trivial cases.
334+
("# zizmor: ignore[foo]", "foo", true),
335+
("# zizmor: ignore[foo,bar]", "foo", true),
336+
// Dashes are OK.
337+
("# zizmor: ignore[foo,bar,foo-bar]", "foo-bar", true),
338+
// Spaces are OK.
339+
("# zizmor: ignore[foo, bar, foo-bar]", "foo-bar", true),
340+
// Extra commas and duplicates are nonsense but OK.
341+
("# zizmor: ignore[foo,foo,,foo,,,,foo,]", "foo", true),
342+
// Valid ignore, but not a match.
343+
("# zizmor: ignore[foo,bar]", "baz", false),
344+
// Invalid ignore: empty rule list.
345+
("# zizmor: ignore[]", "", false),
346+
("# zizmor: ignore[]", "foo", false),
347+
// Invalid ignore: no commas.
348+
("# zizmor: ignore[foo bar]", "foo", false),
349+
// Invalid ignore: missing opening and/or closing [].
350+
("# zizmor: ignore[foo", "foo", false),
351+
("# zizmor: ignore foo", "foo", false),
352+
("# zizmor: ignore foo]", "foo", false),
353+
// Invalid ignore: space after # and : is mandatory and fixed.
354+
("# zizmor:ignore[foo]", "foo", false),
355+
("#zizmor: ignore[foo]", "foo", false),
356+
("# zizmor: ignore[foo]", "foo", false),
357+
("# zizmor: ignore[foo]", "foo", false),
358+
];
359+
360+
for (comment, rule, ignores) in cases {
361+
assert_eq!(
362+
Comment(comment).ignores(rule),
363+
*ignores,
364+
"{comment} does not ignore {rule}"
365+
)
366+
}
320367
}
321368
}

0 commit comments

Comments
 (0)