oxc_ast/ast/
comment.rs

1#![warn(missing_docs)]
2use bitflags::bitflags;
3
4use oxc_allocator::{Allocator, CloneIn};
5use oxc_ast_macros::ast;
6use oxc_estree::ESTree;
7use oxc_span::{ContentEq, Span};
8
9/// Indicates a line or block comment.
10#[ast]
11#[generate_derive(CloneIn, ContentEq, ESTree)]
12#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
13#[estree(no_rename_variants, no_ts_def)]
14pub enum CommentKind {
15    /// Line comment
16    #[default]
17    Line = 0,
18    /// Block comment
19    Block = 1,
20}
21
22/// Information about a comment's position relative to a token.
23#[ast]
24#[generate_derive(CloneIn, ContentEq)]
25#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
26pub enum CommentPosition {
27    /// Comments prior to a token until another token or trailing comment.
28    ///
29    /// e.g.
30    ///
31    /// ```ignore
32    /// /* leading */ token;
33    /// /* leading */
34    /// // leading
35    /// token;
36    /// ```
37    #[default]
38    Leading = 0,
39
40    /// Comments tailing a token until a newline.
41    /// e.g. `token /* trailing */ // trailing`
42    Trailing = 1,
43}
44
45/// Annotation comment that has special meaning.
46#[ast]
47#[generate_derive(CloneIn, ContentEq)]
48#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
49pub enum CommentContent {
50    /// No Annotation
51    #[default]
52    None = 0,
53
54    /// Legal Comment
55    /// e.g. `/* @license */`, `/* @preserve */`, or starts with `//!` or `/*!`.
56    /// <https://esbuild.github.io/api/#legal-comments>
57    Legal = 1,
58
59    /// `/** jsdoc */`
60    /// <https://jsdoc.app>
61    Jsdoc = 2,
62
63    /// A jsdoc containing legal annotation.
64    /// `/** @preserve */`
65    JsdocLegal = 3,
66
67    /// `/* #__PURE__ */`
68    /// <https://github.com/javascript-compiler-hints/compiler-notations-spec>
69    Pure = 4,
70
71    /// `/* #__NO_SIDE_EFFECTS__ */`
72    NoSideEffects = 5,
73
74    /// Webpack magic comment
75    /// e.g. `/* webpackChunkName */`
76    /// <https://webpack.js.org/api/module-methods/#magic-comments>
77    Webpack = 6,
78
79    /// Vite comment
80    /// e.g. `/* @vite-ignore */`
81    /// <https://github.com/search?q=repo%3Avitejs%2Fvite%20vite-ignore&type=code>
82    Vite = 7,
83
84    /// Code Coverage Ignore
85    /// `v8 ignore`, `c8 ignore`, `node:coverage`, `istanbul ignore`
86    /// <https://github.com/oxc-project/oxc/issues/10091>
87    CoverageIgnore = 8,
88}
89
90bitflags! {
91    #[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
92    /// State of newlines around a comment.
93    pub struct CommentNewlines: u8 {
94        /// Preceded by a newline
95        const Leading = 1 << 0;
96        /// Followed by a newline
97        const Trailing = 1 << 1;
98        /// No newlines before or after
99        const None = 0;
100    }
101}
102
103/// Dummy type to communicate the content of `CommentFlags` to `oxc_ast_tools`.
104#[ast(foreign = CommentNewlines)]
105#[expect(dead_code)]
106struct CommentNewlinesAlias(u8);
107
108impl ContentEq for CommentNewlines {
109    fn content_eq(&self, other: &Self) -> bool {
110        self == other
111    }
112}
113
114impl<'alloc> CloneIn<'alloc> for CommentNewlines {
115    type Cloned = Self;
116
117    fn clone_in(&self, _: &'alloc Allocator) -> Self::Cloned {
118        *self
119    }
120}
121
122/// A comment in source code.
123#[ast]
124#[generate_derive(CloneIn, ContentEq, ESTree)]
125#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
126#[estree(add_fields(value = CommentValue), no_ts_def, no_parent)]
127pub struct Comment {
128    /// The span of the comment text, with leading and trailing delimiters.
129    pub span: Span,
130
131    /// Start of token this leading comment is attached to.
132    /// `/* Leading */ token`
133    ///                ^ This start
134    /// NOTE: Trailing comment attachment is not computed yet.
135    #[estree(skip)]
136    pub attached_to: u32,
137
138    /// Line or block comment
139    #[estree(rename = "type")]
140    pub kind: CommentKind,
141
142    /// Leading or trailing comment
143    #[estree(skip)]
144    pub position: CommentPosition,
145
146    /// Whether this comment has newlines around it.
147    /// Used to avoid becoming a trailing comment in codegen.
148    #[estree(skip)]
149    pub newlines: CommentNewlines,
150
151    /// Content of the comment
152    #[estree(skip)]
153    pub content: CommentContent,
154}
155
156impl Comment {
157    /// Create a line or block comment at a given location.
158    #[inline]
159    pub fn new(start: u32, end: u32, kind: CommentKind) -> Self {
160        let span = Span::new(start, end);
161        Self {
162            span,
163            attached_to: 0,
164            kind,
165            position: CommentPosition::Trailing,
166            newlines: CommentNewlines::None,
167            content: CommentContent::None,
168        }
169    }
170
171    /// Gets the span of the comment content.
172    pub fn content_span(&self) -> Span {
173        match self.kind {
174            CommentKind::Line => Span::new(self.span.start + 2, self.span.end),
175            CommentKind::Block => Span::new(self.span.start + 2, self.span.end - 2),
176        }
177    }
178
179    /// Returns `true` if this is a line comment.
180    #[inline]
181    pub fn is_line(self) -> bool {
182        self.kind == CommentKind::Line
183    }
184
185    /// Returns `true` if this is a block comment.
186    #[inline]
187    pub fn is_block(self) -> bool {
188        self.kind == CommentKind::Block
189    }
190
191    /// Returns `true` if this comment is before a token.
192    #[inline]
193    pub fn is_leading(self) -> bool {
194        self.position == CommentPosition::Leading
195    }
196
197    /// Returns `true` if this comment is after a token.
198    #[inline]
199    pub fn is_trailing(self) -> bool {
200        self.position == CommentPosition::Trailing
201    }
202
203    /// Is comment without a special meaning.
204    #[inline]
205    pub fn is_normal(self) -> bool {
206        self.content == CommentContent::None
207    }
208
209    /// Is comment with special meaning.
210    #[inline]
211    pub fn is_annotation(self) -> bool {
212        self.content != CommentContent::None
213            && self.content != CommentContent::Legal
214            && self.content != CommentContent::Jsdoc
215            && self.content != CommentContent::JsdocLegal
216    }
217
218    /// Returns `true` if this comment is a JSDoc comment. Implies `is_leading` and `is_block`.
219    #[inline]
220    pub fn is_jsdoc(self) -> bool {
221        matches!(self.content, CommentContent::Jsdoc | CommentContent::JsdocLegal)
222            && self.is_leading()
223    }
224
225    /// Legal comments
226    ///
227    /// A "legal comment" is considered to be any statement-level comment
228    /// that contains `@license` or `@preserve` or that starts with `//!` or `/*!`.
229    ///
230    /// <https://esbuild.github.io/api/#legal-comments>
231    #[inline]
232    pub fn is_legal(self) -> bool {
233        matches!(self.content, CommentContent::Legal | CommentContent::JsdocLegal)
234            && self.is_leading()
235    }
236
237    /// Is `/* @__PURE__*/`.
238    #[inline]
239    pub fn is_pure(self) -> bool {
240        self.content == CommentContent::Pure
241    }
242
243    /// Is `/* @__NO_SIDE_EFFECTS__*/`.
244    #[inline]
245    pub fn is_no_side_effects(self) -> bool {
246        self.content == CommentContent::NoSideEffects
247    }
248
249    /// Is webpack magic comment.
250    #[inline]
251    pub fn is_webpack(self) -> bool {
252        self.content == CommentContent::Webpack
253    }
254
255    /// Is vite special comment.
256    #[inline]
257    pub fn is_vite(self) -> bool {
258        self.content == CommentContent::Vite
259    }
260
261    /// Is coverage ignore comment.
262    #[inline]
263    pub fn is_coverage_ignore(self) -> bool {
264        self.content == CommentContent::CoverageIgnore && self.is_leading()
265    }
266
267    /// Returns `true` if this comment is preceded by a newline.
268    #[inline]
269    pub fn preceded_by_newline(self) -> bool {
270        self.newlines.contains(CommentNewlines::Leading)
271    }
272
273    /// Returns `true` if this comment is followed by a newline.
274    #[inline]
275    pub fn followed_by_newline(self) -> bool {
276        self.newlines.contains(CommentNewlines::Trailing)
277    }
278
279    /// Sets the state of `newlines` to include/exclude a newline before the comment.
280    #[inline]
281    pub fn set_preceded_by_newline(&mut self, preceded_by_newline: bool) {
282        self.newlines.set(CommentNewlines::Leading, preceded_by_newline);
283    }
284
285    /// Sets the state of `newlines` to include/exclude a newline after the comment.
286    #[inline]
287    pub fn set_followed_by_newline(&mut self, followed_by_newline: bool) {
288        self.newlines.set(CommentNewlines::Trailing, followed_by_newline);
289    }
290}