Skip to main content

just_shield/
suppress.rs

1//! 무시 주석 — 탈출구 ①: 경고의 의도적 수용.
2//!
3//! 형식: `# just-shield: ignore R1 -- 사유`
4//! 단독 주석 행이면 다음 의미 행에, 행 끝 주석이면 같은 행에 적용된다.
5//! `--` 뒤 사유가 없으면 무시는 적용되지 않는다 — 미래의 동료(와 미래의 나)가
6//! "왜 무시했지?"를 알 수 있어야 하기 때문이다.
7
8const MARKER: &str = "just-shield:";
9
10/// 무시 주석 한 건.
11pub struct Directive {
12    /// 무시할 규칙 ID들 (예: ["R1", "R7"]).
13    pub rules: Vec<String>,
14    /// `--` 뒤의 사유. 없으면 무시가 적용되지 않는다.
15    pub reason: Option<String>,
16    /// 주석이 있는 행 (1부터).
17    pub comment_line: usize,
18    /// 무시가 적용될 행. 단독 주석인데 뒤에 의미 행이 없으면 None.
19    pub target_line: Option<usize>,
20}
21
22/// 파일 내용에서 모든 무시 주석을 찾는다.
23pub 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        // 마커가 주석 안에 있어야 한다 — 마커 앞에 '#'이 존재해야.
31        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        // 단독 주석 행 → 다음 의미 행에 적용, 행 끝 주석 → 같은 행에 적용.
62        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}