Skip to main content

gobject_ast/model/
comment.rs

1use serde::Serialize;
2
3use crate::model::SourceLocation;
4
5#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
6#[serde(rename_all = "snake_case")]
7pub enum CommentKind {
8    /// Single-line comment: // ...
9    Line,
10    /// Multi-line comment: /* ... */
11    Block,
12}
13
14#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
15#[serde(rename_all = "snake_case")]
16pub enum CommentPosition {
17    /// Comment appears before the associated node
18    Leading,
19    /// Comment appears after the associated node (same line)
20    Trailing,
21    /// Comment appears inside a node (e.g., between statements)
22    Inner,
23}
24
25#[derive(Debug, Clone, Serialize)]
26pub struct Comment {
27    /// The comment text (without // or /* */ delimiters)
28    pub text: String,
29    /// Location in source
30    pub location: SourceLocation,
31    /// Line or block comment
32    pub kind: CommentKind,
33    /// Position relative to associated node
34    pub position: CommentPosition,
35}
36
37impl Comment {
38    pub fn new(
39        text: String,
40        location: SourceLocation,
41        kind: CommentKind,
42        position: CommentPosition,
43    ) -> Self {
44        Self {
45            text,
46            location,
47            kind,
48            position,
49        }
50    }
51
52    /// Check if comment contains a specific annotation (case-insensitive)
53    pub fn contains(&self, pattern: &str) -> bool {
54        self.body().to_lowercase().contains(&pattern.to_lowercase())
55    }
56
57    /// The comment body without delimiters (`//`, `/* */`).
58    pub fn body(&self) -> &str {
59        let t = self.text.trim();
60        match self.kind {
61            CommentKind::Line => t.strip_prefix("//").unwrap_or(t).trim_start(),
62            CommentKind::Block => t
63                .strip_prefix("/*")
64                .and_then(|s| s.strip_suffix("*/"))
65                .unwrap_or(t)
66                .trim(),
67        }
68    }
69
70    /// Extract gobject-linter-ignore rule names from comment
71    /// Returns Some(vec![rule_names]) if this is an ignore directive
72    pub fn extract_ignore_rules(&self) -> Option<Vec<String>> {
73        let text = self.body();
74
75        if let Some(after_prefix) = text
76            .strip_prefix("gobject-linter-ignore:")
77            .or_else(|| text.strip_prefix("gobject-linter-ignore-next-line:"))
78            .or_else(|| text.strip_prefix("goblint-ignore:"))
79            .or_else(|| text.strip_prefix("goblint-ignore-next-line:"))
80        {
81            let rules: Vec<String> = after_prefix
82                .split(',')
83                .map(|s| s.trim().to_string())
84                .filter(|s| !s.is_empty())
85                .collect();
86            return Some(rules);
87        }
88
89        None
90    }
91
92    /// Check if this is a GTK-Doc style documentation comment
93    pub fn is_gtk_doc(&self) -> bool {
94        matches!(self.kind, CommentKind::Block)
95            && matches!(self.position, CommentPosition::Leading)
96            && self.text.starts_with("/**")
97    }
98
99    /// Check if this is a TODO/FIXME/HACK/XXX comment
100    pub fn is_marker(&self) -> bool {
101        let upper = self.body().to_uppercase();
102        upper.contains("TODO")
103            || upper.contains("FIXME")
104            || upper.contains("HACK")
105            || upper.contains("XXX")
106    }
107}