1const MARKER: &str = "just-shield:";
9
10pub struct Directive {
12 pub rules: Vec<String>,
14 pub reason: Option<String>,
16 pub comment_line: usize,
18 pub target_line: Option<usize>,
20}
21
22pub fn parse(content: &str) -> Vec<Directive> {
24 let lines: Vec<&str> = content.lines().collect();
25 let mut out = Vec::new();
26 for (i, raw) in lines.iter().enumerate() {
27 let Some(pos) = raw.find(MARKER) else {
28 continue;
29 };
30 if !raw[..pos].contains('#') {
32 continue;
33 }
34 let after = raw[pos + MARKER.len()..].trim_start();
35 let Some(rest) = after.strip_prefix("ignore") else {
36 continue;
37 };
38 let (rules_part, reason) = match rest.split_once("--") {
39 Some((r, why)) => {
40 let why = why.trim();
41 (
42 r.trim(),
43 if why.is_empty() {
44 None
45 } else {
46 Some(why.to_string())
47 },
48 )
49 }
50 None => (rest.trim(), None),
51 };
52 let rules: Vec<String> = rules_part
53 .split([',', ' '])
54 .map(str::trim)
55 .filter(|s| !s.is_empty())
56 .map(str::to_string)
57 .collect();
58 if rules.is_empty() {
59 continue;
60 }
61 let whole_line_comment = raw.trim_start().starts_with('#');
63 let target_line = if whole_line_comment {
64 lines[i + 1..]
65 .iter()
66 .position(|l| {
67 let t = l.trim();
68 !t.is_empty() && !t.starts_with('#')
69 })
70 .map(|j| i + 1 + j + 1)
71 } else {
72 Some(i + 1)
73 };
74 out.push(Directive {
75 rules,
76 reason,
77 comment_line: i + 1,
78 target_line,
79 });
80 }
81 out
82}
83
84#[cfg(test)]
85mod tests {
86 use super::parse;
87
88 #[test]
89 fn standalone_comment_targets_next_meaningful_line() {
90 let d =
91 parse("a: 1\n# just-shield: ignore R1 -- 검토 완료\n\n# 다른 주석\n- uses: x/y@v1\n");
92 assert_eq!(d.len(), 1);
93 assert_eq!(d[0].rules, vec!["R1"]);
94 assert_eq!(d[0].reason.as_deref(), Some("검토 완료"));
95 assert_eq!(d[0].target_line, Some(5));
96 }
97
98 #[test]
99 fn trailing_comment_targets_same_line() {
100 let d = parse("permissions: write-all # just-shield: ignore R7 -- 배포 잡\n");
101 assert_eq!(d[0].target_line, Some(1));
102 assert_eq!(d[0].rules, vec!["R7"]);
103 }
104
105 #[test]
106 fn missing_reason_is_kept_but_marked() {
107 let d = parse("# just-shield: ignore R1\n- uses: x/y@v1\n");
108 assert_eq!(d.len(), 1);
109 assert!(d[0].reason.is_none());
110 }
111
112 #[test]
113 fn multiple_rules_and_non_directives() {
114 let d = parse(
115 "# just-shield: ignore R1, R7 -- 둘 다 수용\nx: 1\n# 그냥 주석\njust-shield: ignore R9 (주석 아님)\n",
116 );
117 assert_eq!(d.len(), 1);
118 assert_eq!(d[0].rules, vec!["R1", "R7"]);
119 }
120}