use std::num::NonZeroU32;
use memchr::memchr_iter;
use oxc_allocator::{Allocator, ArenaVec};
use oxc_ast::ast::{Comment, CommentContent, CommentKind, CommentPosition};
use oxc_span::Span;
use super::{Kind, Token};
#[derive(Debug)]
pub struct TriviaBuilder<'a> {
pub(crate) comments: ArenaVec<'a, Comment>,
pub(crate) irregular_whitespaces: Vec<Span>,
processed: usize,
saw_newline: bool,
saw_newline_for_comment: bool,
previous_token: Token,
pure_comments: Option<(u32, NonZeroU32)>,
no_side_effects_comments: Option<(u32, NonZeroU32)>,
}
const _: () = assert!(size_of::<Option<(u32, NonZeroU32)>>() == 8);
impl<'a> TriviaBuilder<'a> {
pub fn new_in(allocator: &'a Allocator) -> Self {
let mut previous_token = Token::default();
previous_token.set_kind(Kind::Undetermined);
Self {
comments: ArenaVec::new_in(&allocator),
irregular_whitespaces: vec![],
processed: 0,
saw_newline: true,
saw_newline_for_comment: true,
previous_token,
pure_comments: None,
no_side_effects_comments: None,
}
}
pub fn previous_token_pure_comments(&self) -> Option<(u32, NonZeroU32)> {
self.pure_comments
}
pub fn previous_token_no_side_effects_comments(&self) -> Option<(u32, NonZeroU32)> {
self.no_side_effects_comments
}
pub(super) fn set_pure_comments(&mut self, pure_comments: Option<(u32, NonZeroU32)>) {
self.pure_comments = pure_comments;
}
pub(super) fn clear_pure_comments(&mut self) {
self.pure_comments = None;
}
pub(super) fn set_no_side_effects_comments(&mut self, comments: Option<(u32, NonZeroU32)>) {
self.no_side_effects_comments = comments;
}
pub(super) fn clear_no_side_effects_comments(&mut self) {
self.no_side_effects_comments = None;
}
pub fn mark_pure_comments_applied(&mut self, (start, end): (u32, NonZeroU32)) {
for comment in &mut self.comments[start as usize..end.get() as usize] {
if comment.content == CommentContent::PureNotApplied {
comment.content = CommentContent::Pure;
}
}
}
pub fn mark_no_side_effects_comments_applied(&mut self, (start, end): (u32, NonZeroU32)) {
for comment in &mut self.comments[start as usize..end.get() as usize] {
if comment.content == CommentContent::NoSideEffectsNotApplied {
comment.content = CommentContent::NoSideEffects;
}
}
}
pub fn add_irregular_whitespace(&mut self, start: u32, end: u32) {
if let Some(last) = self.irregular_whitespaces.last()
&& start <= last.start
{
return;
}
self.irregular_whitespaces.push(Span::new(start, end));
}
pub fn add_line_comment(&mut self, start: u32, end: u32, source_text: &str) {
self.add_comment(Comment::new(start, end, CommentKind::Line), source_text);
}
pub fn add_block_comment(
&mut self,
start: u32,
end: u32,
kind: CommentKind,
source_text: &str,
) {
self.add_comment(Comment::new(start, end, kind), source_text);
}
pub fn handle_newline(&mut self) {
let len = self.comments.len();
if self.processed < len {
let becomes_trailing = {
let comment = &mut self.comments[len - 1];
comment.set_followed_by_newline(true);
!self.saw_newline && !Self::should_stay_leading(comment)
};
if becomes_trailing {
self.attach_pending_comments(
CommentPosition::Trailing,
self.previous_token.end(),
len,
);
}
}
self.saw_newline = true;
self.saw_newline_for_comment = true;
}
#[inline]
pub fn handle_token(&mut self, token: Token) {
self.saw_newline = false;
self.saw_newline_for_comment = false;
let len = self.comments.len();
if self.processed < len {
self.attach_pending_comments_to_token(token, len);
}
self.previous_token = token;
}
#[cold]
fn attach_pending_comments_to_token(&mut self, token: Token, len: usize) {
let previous_kind = self.previous_token.kind();
let can_attach_to_previous = previous_kind != Kind::Undetermined
&& (token.kind() == Kind::Eof
|| (Self::can_end_expression(previous_kind)
&& Self::continues_previous_expression(token.kind())));
let previous_token_end = self.previous_token.end();
for comment in &mut self.comments[self.processed..len] {
if can_attach_to_previous
&& !comment.preceded_by_newline()
&& !Self::should_stay_leading(comment)
{
comment.position = CommentPosition::Trailing;
comment.attached_to = previous_token_end;
} else {
comment.position = CommentPosition::Leading;
comment.attached_to = token.start();
}
}
self.processed = len;
}
#[cold]
fn attach_pending_comments(&mut self, position: CommentPosition, attached_to: u32, len: usize) {
for comment in &mut self.comments[self.processed..len] {
comment.position = position;
comment.attached_to = attached_to;
}
self.processed = len;
}
#[inline]
fn can_end_expression(kind: Kind) -> bool {
kind.is_identifier()
|| kind.is_literal()
|| matches!(
kind,
Kind::PrivateIdentifier
| Kind::This
| Kind::Super
| Kind::RParen
| Kind::RBrack
| Kind::RCurly
| Kind::RAngle
| Kind::Plus2
| Kind::Minus2
| Kind::NoSubstitutionTemplate
| Kind::TemplateTail
| Kind::JSXText
)
}
#[inline]
fn continues_previous_expression(kind: Kind) -> bool {
kind.is_binary_operator() || kind.is_logical_operator() || kind.is_assignment_operator()
}
fn should_be_treated_as_trailing_comment(&self) -> bool {
!self.saw_newline
&& !matches!(self.previous_token.kind(), Kind::Eq | Kind::LParen | Kind::Colon)
}
fn should_stay_leading(comment: &Comment) -> bool {
matches!(
comment.content,
CommentContent::Legal
| CommentContent::JsdocLegal
| CommentContent::Pure
| CommentContent::PureNotApplied
| CommentContent::NoSideEffects
| CommentContent::NoSideEffectsNotApplied
| CommentContent::PropertyKey
)
}
fn set_annotation_flags(&mut self, comment: &Comment, index: usize) {
let range = match comment.content {
CommentContent::PureNotApplied => &mut self.pure_comments,
CommentContent::NoSideEffectsNotApplied => &mut self.no_side_effects_comments,
_ => return,
};
#[expect(clippy::cast_possible_truncation)]
let index = index as u32;
let end = NonZeroU32::new(index + 1).unwrap();
if let Some((_, range_end)) = range {
*range_end = end;
} else {
*range = Some((index, end));
}
}
fn add_comment(&mut self, mut comment: Comment, source_text: &str) {
Self::parse_annotation(&mut comment, source_text);
if let Some(last_comment) = self.comments.last()
&& comment.span.start <= last_comment.span.start
{
if let Ok(index) = self
.comments
.binary_search_by_key(&comment.span.start, |existing| existing.span.start)
&& self.comments[index].span == comment.span
{
self.set_annotation_flags(&comment, index);
}
return;
}
comment.set_preceded_by_newline(self.saw_newline_for_comment);
let becomes_trailing = if comment.is_line() {
comment.set_followed_by_newline(true);
let becomes_trailing = self.should_be_treated_as_trailing_comment()
&& !Self::should_stay_leading(&comment);
self.saw_newline = true;
self.saw_newline_for_comment = true;
becomes_trailing
} else {
self.saw_newline_for_comment = false;
false
};
self.set_annotation_flags(&comment, self.comments.len());
self.comments.push(comment);
if becomes_trailing {
self.attach_pending_comments(
CommentPosition::Trailing,
self.previous_token.end(),
self.comments.len(),
);
}
}
fn parse_annotation(comment: &mut Comment, source_text: &str) {
let s = comment.content_span().source_text(source_text);
let bytes = s.as_bytes();
if bytes.is_empty() {
return;
}
match bytes[0] {
b'!' => {
comment.content = CommentContent::Legal;
return;
}
b'*' if comment.is_block() => {
if !bytes.iter().all(|&c| c == b'*') {
if contains_license_or_preserve_comment(s) {
comment.content = CommentContent::JsdocLegal;
} else {
comment.content = CommentContent::Jsdoc;
}
}
return;
}
_ => {}
}
let mut start = 0;
while start < bytes.len() && bytes[start].is_ascii_whitespace() {
start += 1;
}
if start >= bytes.len() {
return;
}
let rest = &bytes[start..];
if (rest.starts_with(b"@__KEY__") || rest.starts_with(b"#__KEY__") || !rest[0].is_ascii())
&& is_property_key_annotation(s)
{
comment.content = CommentContent::PropertyKey;
return;
}
match bytes[start] {
b'@' => {
start += 1;
if start >= bytes.len() {
return;
}
if bytes[start..].starts_with(b"vite") {
comment.content = CommentContent::Vite;
return;
}
if bytes[start..].starts_with(b"license") || bytes[start..].starts_with(b"preserve")
{
comment.content = CommentContent::Legal;
return;
}
}
b'#' => {
start += 1;
}
b'w' => {
if bytes[start..].starts_with(b"webpack")
&& start + 7 < bytes.len()
&& bytes[start + 7].is_ascii_uppercase()
{
comment.content = CommentContent::Webpack;
return;
}
}
b't' => {
if bytes[start..].starts_with(b"turbopack")
&& start + 9 < bytes.len()
&& bytes[start + 9].is_ascii_uppercase()
{
comment.content = CommentContent::Turbopack;
return;
}
}
b'v' | b'c' | b'n' | b'i' => {
let rest = &bytes[start..];
if rest.starts_with(b"v8 ignore")
|| rest.starts_with(b"c8 ignore")
|| rest.starts_with(b"node:coverage")
|| rest.starts_with(b"istanbul ignore")
{
comment.content = if is_coverage_ignore_file(rest) {
CommentContent::CoverageIgnoreFile
} else {
CommentContent::CoverageIgnore
};
return;
}
}
_ => {
if contains_license_or_preserve_comment(s) {
comment.content = CommentContent::Legal;
}
return;
}
}
if start < bytes.len() && bytes[start..].starts_with(b"__") {
let rest = &bytes[start + 2..];
if rest.starts_with(b"PURE__") {
comment.content = CommentContent::PureNotApplied;
return;
} else if rest.starts_with(b"NO_SIDE_EFFECTS__") {
comment.content = CommentContent::NoSideEffectsNotApplied;
return;
}
}
if contains_license_or_preserve_comment(s) {
comment.content = CommentContent::Legal;
}
}
}
#[inline]
fn is_property_key_annotation(source: &str) -> bool {
matches!(source.trim().strip_prefix(['@', '#']), Some("__KEY__"))
}
#[inline(always)]
fn contains_license_or_preserve_comment(s: &str) -> bool {
const LICENSE_LEN: usize = b"@license".len();
const PRESERVE_LEN: usize = b"@preserve".len();
let hay = s.as_bytes();
if hay.len() < LICENSE_LEN {
return false;
}
let search_len = hay.len() - LICENSE_LEN + 1;
for i in memchr_iter(b'@', &hay[..search_len]) {
debug_assert!(i < search_len);
debug_assert!(hay.len() - i >= LICENSE_LEN);
match unsafe { hay.get_unchecked(i + 1) } {
b'l'
if unsafe { hay.get_unchecked(i + 2..i + LICENSE_LEN) } == b"icense" =>
{
return true;
}
b'p' if hay.len() - i >= PRESERVE_LEN
&& unsafe { hay.get_unchecked(i + 2..i + PRESERVE_LEN) } == b"reserve" =>
{
return true;
}
_ => {}
}
}
false
}
fn is_coverage_ignore_file(source: &[u8]) -> bool {
fn starts_with_directive(source: &[u8], directive: &[u8]) -> bool {
source
.strip_prefix(directive)
.is_some_and(|rest| rest.first().is_none_or(u8::is_ascii_whitespace))
}
starts_with_directive(source, b"v8 ignore file")
|| starts_with_directive(source, b"istanbul ignore file")
}
#[cfg(test)]
mod test {
use oxc_allocator::Allocator;
use oxc_ast::{Comment, CommentContent, CommentKind, CommentPosition, ast::CommentNewlines};
use oxc_span::{SourceType, Span};
use crate::Parser;
fn get_comments(source_text: &str) -> Vec<Comment> {
let allocator = Allocator::default();
let source_type = SourceType::default();
let ret = Parser::new(&allocator, source_text, source_type).parse();
assert!(ret.diagnostics.is_empty());
ret.program.comments.iter().copied().collect::<Vec<_>>()
}
fn get_comments_typescript(source_text: &str) -> Vec<Comment> {
let allocator = Allocator::default();
let source_type = SourceType::default().with_typescript(true);
let ret = Parser::new(&allocator, source_text, source_type).parse();
assert!(ret.diagnostics.is_empty());
ret.program.comments.iter().copied().collect::<Vec<_>>()
}
#[test]
fn comment_attachments() {
let source_text = "
/* Leading 1 */
// Leading 2
/* Leading 3 */ token /* Trailing 1 */ // Trailing 2
// Leading of EOF token
";
let comments = get_comments(source_text);
let expected = [
Comment {
span: Span::new(9, 24),
kind: CommentKind::SingleLineBlock,
position: CommentPosition::Leading,
attached_to: 70,
newlines: CommentNewlines::Leading | CommentNewlines::Trailing,
content: CommentContent::None,
},
Comment {
span: Span::new(33, 45),
kind: CommentKind::Line,
position: CommentPosition::Leading,
attached_to: 70,
newlines: CommentNewlines::Leading | CommentNewlines::Trailing,
content: CommentContent::None,
},
Comment {
span: Span::new(54, 69),
kind: CommentKind::SingleLineBlock,
position: CommentPosition::Leading,
attached_to: 70,
newlines: CommentNewlines::Leading,
content: CommentContent::None,
},
Comment {
span: Span::new(76, 92),
kind: CommentKind::SingleLineBlock,
position: CommentPosition::Trailing,
attached_to: 75,
newlines: CommentNewlines::None,
content: CommentContent::None,
},
Comment {
span: Span::new(93, 106),
kind: CommentKind::Line,
position: CommentPosition::Trailing,
attached_to: 75,
newlines: CommentNewlines::Trailing,
content: CommentContent::None,
},
Comment {
span: Span::new(115, 138),
kind: CommentKind::Line,
position: CommentPosition::Leading,
attached_to: 147,
newlines: CommentNewlines::Leading | CommentNewlines::Trailing,
content: CommentContent::None,
},
];
assert_eq!(comments.len(), expected.len());
for (comment, expected) in comments.iter().copied().zip(expected) {
assert_eq!(comment, expected, "{}", comment.content_span().source_text(source_text));
}
}
#[test]
fn comment_attachments2() {
let source_text = "#!/usr/bin/env node
/* Leading 1 */
token /* Trailing 1 */
";
let comments = get_comments(source_text);
let expected = vec![
Comment {
span: Span::new(20, 35),
kind: CommentKind::SingleLineBlock,
position: CommentPosition::Leading,
attached_to: 36,
newlines: CommentNewlines::Leading | CommentNewlines::Trailing,
content: CommentContent::None,
},
Comment {
span: Span::new(42, 58),
kind: CommentKind::SingleLineBlock,
position: CommentPosition::Trailing,
attached_to: 41,
newlines: CommentNewlines::Trailing,
content: CommentContent::None,
},
];
assert_eq!(comments, expected);
}
#[test]
fn html_close_comments_after_irregular_line_terminators_are_leading() {
for line_terminator in ['\u{2028}', '\u{2029}'] {
let allocator = Allocator::default();
let source_text = format!("foo();{line_terminator}--> comment\nbar();");
let source_type = SourceType::default().with_script(true);
let ret = Parser::new(&allocator, &source_text, source_type).parse();
assert!(ret.diagnostics.is_empty());
let comments = &ret.program.comments;
assert_eq!(comments.len(), 1);
assert!(comments[0].is_leading());
assert!(comments[0].preceded_by_newline());
let bar_start = u32::try_from(source_text.find("bar").unwrap()).unwrap();
assert_eq!(comments[0].attached_to, bar_start);
}
}
#[test]
fn comments_around_binary_operands_attach_to_their_adjacent_tokens() {
let source_text = "/* a leading */ a /* a trailing */ + /* b leading */ b /* b trailing */";
let comments = get_comments(source_text);
let expected = [
(CommentPosition::Leading, 16),
(CommentPosition::Trailing, 17),
(CommentPosition::Leading, 53),
(CommentPosition::Trailing, 54),
];
assert_eq!(comments.len(), expected.len());
for (comment, (position, attached_to)) in comments.iter().zip(expected) {
assert_eq!(comment.position, position);
assert_eq!(comment.attached_to, attached_to);
}
}
#[test]
fn infix_comment_attachment_respects_line_and_unary_boundaries() {
for (source_text, position, attached_to) in [
("a /* trailing */\n+ b", CommentPosition::Trailing, 1),
("a\n/* leading */ +b", CommentPosition::Leading, 16),
("void /* leading */ +value", CommentPosition::Leading, 19),
("a /* webpackFoo: 1 */ + b", CommentPosition::Trailing, 1),
("foo(); /* trailing */", CommentPosition::Trailing, 6),
] {
let comments = get_comments(source_text);
assert_eq!(comments.len(), 1, "{source_text}");
assert_eq!(comments[0].position, position, "{source_text}");
assert_eq!(comments[0].attached_to, attached_to, "{source_text}");
}
}
#[test]
fn trailing_comment_attachment_survives_arrow_lookahead() {
let source_text = "const f = (value /* trailing */\n) => value;";
let comments = get_comments(source_text);
let value_end = u32::try_from(source_text.find("value ").unwrap() + "value".len()).unwrap();
assert_eq!(comments.len(), 1);
assert_eq!(comments[0].position, CommentPosition::Trailing);
assert_eq!(comments[0].attached_to, value_end);
}
#[test]
fn comment_attachments3() {
let source_text = "
/*
* A
**/
/*
* B
**/
token
";
let comments = get_comments(source_text);
let expected = vec![
Comment {
span: Span::new(1, 13),
kind: CommentKind::MultiLineBlock,
position: CommentPosition::Leading,
attached_to: 28,
newlines: CommentNewlines::Leading | CommentNewlines::Trailing,
content: CommentContent::None,
},
Comment {
span: Span::new(14, 26),
kind: CommentKind::MultiLineBlock,
position: CommentPosition::Leading,
attached_to: 28,
newlines: CommentNewlines::Leading | CommentNewlines::Trailing,
content: CommentContent::None,
},
];
assert_eq!(comments, expected);
}
#[test]
fn legal_comment_after_code_is_attached_to_next_token() {
let source_text = "foo();/**
* @license MIT
**/
function bar() {}";
let comments = get_comments(source_text);
let function_start = u32::try_from(source_text.find("function").unwrap()).unwrap();
assert_eq!(comments.len(), 1);
assert_eq!(comments[0].position, CommentPosition::Leading);
assert_eq!(comments[0].attached_to, function_start);
assert!(comments[0].is_legal());
assert!(comments[0].followed_by_newline());
}
#[test]
fn legal_line_comment_after_code_is_attached_to_next_token() {
let source_text = "foo();//! @license MIT\nfunction bar() {}";
let comments = get_comments(source_text);
let function_start = u32::try_from(source_text.find("function").unwrap()).unwrap();
assert_eq!(comments.len(), 1);
assert_eq!(comments[0].position, CommentPosition::Leading);
assert_eq!(comments[0].attached_to, function_start);
assert!(comments[0].is_legal());
assert!(comments[0].followed_by_newline());
}
#[test]
fn no_side_effects_block_comment_after_code_is_attached_to_next_token() {
let source_text = "function foo() {}/* #__NO_SIDE_EFFECTS__ */\nfunction bar() {}";
let comments = get_comments(source_text);
let bar_start = u32::try_from(source_text.rfind("function").unwrap()).unwrap();
assert_eq!(comments.len(), 1);
assert_eq!(comments[0].position, CommentPosition::Leading);
assert_eq!(comments[0].attached_to, bar_start);
assert!(comments[0].is_no_side_effects());
}
#[test]
fn no_side_effects_line_comment_after_code_is_attached_to_next_token() {
let source_text = "foo();// @__NO_SIDE_EFFECTS__\nfunction bar() {}";
let comments = get_comments(source_text);
let function_start = u32::try_from(source_text.find("function").unwrap()).unwrap();
assert_eq!(comments.len(), 1);
assert_eq!(comments[0].position, CommentPosition::Leading);
assert_eq!(comments[0].attached_to, function_start);
assert!(comments[0].is_no_side_effects());
assert!(comments[0].followed_by_newline());
}
#[test]
fn pure_block_comment_after_code_is_attached_to_next_token() {
let source_text = "foo();/* @__PURE__ */new Bar()";
let comments = get_comments(source_text);
let new_start = u32::try_from(source_text.find("new").unwrap()).unwrap();
assert_eq!(comments.len(), 1);
assert_eq!(comments[0].position, CommentPosition::Leading);
assert_eq!(comments[0].attached_to, new_start);
assert!(comments[0].is_pure());
}
#[test]
fn property_key_comment_after_code_is_attached_to_next_literal() {
for (source_text, literal) in [
("work();/* #__KEY__ */\n\"_field\";", "\"_field\""),
("work();// @__KEY__\n`_field`;", "`_field`"),
] {
let comments = get_comments(source_text);
let literal_start = u32::try_from(source_text.find(literal).unwrap()).unwrap();
assert_eq!(comments.len(), 1);
assert_eq!(comments[0].position, CommentPosition::Leading);
assert_eq!(comments[0].attached_to, literal_start);
assert!(comments[0].is_property_key_annotation());
assert!(comments[0].is_annotation());
}
}
#[test]
fn leading_comments_after_eq() {
let source_text = "
const v1 = // Leading comment 1
foo();
function foo(param =// Leading comment 2
new Foo()
) {}
";
let comments = get_comments(source_text);
let expected = vec![
Comment {
span: Span::new(24, 44),
kind: CommentKind::Line,
position: CommentPosition::Leading,
attached_to: 57,
newlines: CommentNewlines::Trailing,
content: CommentContent::None,
},
Comment {
span: Span::new(96, 116),
kind: CommentKind::Line,
position: CommentPosition::Leading,
attached_to: 129,
newlines: CommentNewlines::Trailing,
content: CommentContent::None,
},
];
assert_eq!(comments, expected);
}
#[test]
fn leading_comments_after_left_parenthesis() {
let source_text = "
call(// Leading comment 1
arguments)
(// Leading comment 2
arguments)
";
let comments = get_comments(source_text);
let expected = vec![
Comment {
span: Span::new(18, 38),
kind: CommentKind::Line,
position: CommentPosition::Leading,
attached_to: 55,
newlines: CommentNewlines::Trailing,
content: CommentContent::None,
},
Comment {
span: Span::new(79, 99),
kind: CommentKind::Line,
position: CommentPosition::Leading,
attached_to: 116,
newlines: CommentNewlines::Trailing,
content: CommentContent::None,
},
];
assert_eq!(comments, expected);
}
#[test]
fn leading_comments_after_colon() {
let source_text = "v = cond ? a : // Leading comment\nb;";
let comments = get_comments(source_text);
let expected = vec![Comment {
span: Span::new(15, 33),
kind: CommentKind::Line,
position: CommentPosition::Leading,
attached_to: 34,
newlines: CommentNewlines::Trailing,
content: CommentContent::None,
}];
assert_eq!(comments, expected);
}
#[test]
fn pure_comments_not_applied() {
let cases = [
"/* #__PURE__ */ React.createElement;",
"/* @__PURE__ */ someVariable;",
"/* #__PURE__ */ 42;",
"!/* #__PURE__ */ x;",
"/* #__PURE__ */ function foo() {}",
"/* #__PURE__ */ class Foo {}",
"/* #__PURE__ */ var x = foo();",
"const foo /* #__PURE__ */ = pureOperation();",
"export const X = /* @__PURE__ */ { a: 1 };",
"foo /* #__PURE__ */ = pureOperation();",
"foo /* #__PURE__ */ + pureOperation();",
"foo /* #__PURE__ */ && pureOperation();",
"foo /* #__PURE__ */ in pureOperation();",
"foo /* #__PURE__ */ as T;",
"foo /* #__PURE__ */ satisfies T;",
"foo /* #__PURE__ */ ? bar : baz;",
"foo /* #__PURE__ */, bar;",
"({ x /*#__PURE__*/: sideEffect() });",
"true ? x /*#__PURE__*/ : sideEffect();",
"foo /* #__PURE__ */++;",
"foo /* #__PURE__ */--;",
"foo /* #__PURE__ */.bar;",
"foo /* #__PURE__ */[bar];",
"foo /* #__PURE__ */();",
"foo /* #__PURE__ */?.bar;",
"foo /* #__PURE__ */!;",
r"foo /* #__PURE__ */ `bar`;",
"42 /* #__PURE__ */ + foo;",
"(foo) /* #__PURE__ */ + bar;",
"[foo] /* #__PURE__ */ + bar;",
"foo\n/* #__PURE__ */ + bar;",
"foo // #__PURE__\n+ bar;",
"(a = foo /* #__PURE__ */ + bar)",
];
for source_text in cases {
let comments = get_comments_typescript(source_text);
assert_eq!(comments[0].content, CommentContent::PureNotApplied, "{source_text}");
}
}
#[test]
fn pure_comment_applied_after_lookahead() {
let source_text = "export const X = /* @__PURE__ */ foo(/* comment */);";
let comments = get_comments(source_text);
assert_eq!(comments.len(), 2);
assert_eq!(comments[0].content, CommentContent::Pure, "{source_text}");
assert_eq!(comments[1].content, CommentContent::None, "{source_text}");
}
#[test]
fn pure_comment_applied() {
let cases = [
"/* #__PURE__ */ foo();",
"/* #__PURE__ */ (foo)();",
"/* #__PURE__ */ (new Foo());",
"a + /* #__PURE__ */ <T>foo(), bar;",
"x = /* #__PURE__ */ pureOperation() || y;",
"y || (x = /* #__PURE__ */ pureOperation());",
"y || /* #__PURE__ */ pureOperation();",
"function f() {} /* #__PURE__ */ (foo)();",
"/*#__PURE__*/ [foo][0]()",
"if (cond) /*#__PURE__*/ (foo)();",
"foo++\n/*#__PURE__*/ (bar)();",
"function f() {} /*#__PURE__*/ [foo][0]();",
];
for source_text in cases {
let comments = get_comments_typescript(source_text);
assert_eq!(comments[0].content, CommentContent::Pure, "{source_text}");
}
}
#[test]
fn multiple_pure_comments_applied() {
let source_text = "export const X = /*#__PURE__*/ /* comment */ /*@__PURE__*/ foo();";
let comments = get_comments(source_text);
assert_eq!(comments.len(), 3);
assert_eq!(comments[0].content, CommentContent::Pure);
assert_eq!(comments[1].content, CommentContent::None);
assert_eq!(comments[2].content, CommentContent::Pure);
}
#[test]
fn pure_comment_applied_on_member_chain() {
let cases = [
"/*#__PURE__*/ test().a.b.c;",
"/*#__PURE__*/ new Foo().a;",
"/*#__PURE__*/ test()[0].b;",
"class C { #bar; m() { /*#__PURE__*/ this.foo().#bar; } }",
"/*#__PURE__*/ foo()?.a.b;",
"/*#__PURE__*/ foo?.().a.b;",
"/*#__PURE__*/ foo?.()[0];",
];
for source_text in cases {
let comments = get_comments(source_text);
assert_eq!(comments[0].content, CommentContent::Pure, "{source_text}");
}
}
#[test]
fn annotation_comments_track_application_independently() {
let source_text = "/*#__PURE__*/ foo + /*#__PURE__*/ bar()";
let comments = get_comments(source_text);
assert_eq!(comments.len(), 2, "{source_text}");
assert_eq!(comments[0].content, CommentContent::PureNotApplied, "{source_text}");
assert_eq!(comments[1].content, CommentContent::Pure, "{source_text}");
let source_text = concat!(
"/*#__NO_SIDE_EFFECTS__*/ value;",
"/*#__NO_SIDE_EFFECTS__*/ /* comment */ /*@__NO_SIDE_EFFECTS__*/ function foo() {}",
);
let comments = get_comments(source_text);
assert_eq!(comments.len(), 4);
assert_eq!(comments[0].content, CommentContent::NoSideEffectsNotApplied,);
assert_eq!(comments[1].content, CommentContent::NoSideEffects);
assert_eq!(comments[2].content, CommentContent::None);
assert_eq!(comments[3].content, CommentContent::NoSideEffects);
}
#[test]
fn no_side_effects_comments_not_applied() {
let cases = [
"/* #__NO_SIDE_EFFECTS__ */",
"/* @__NO_SIDE_EFFECTS__ */ assert.ok(true);",
"/* #__NO_SIDE_EFFECTS__ */ const foo = 1, bar = () => {};",
"/* #__NO_SIDE_EFFECTS__ */ class Foo {}",
"/* #__NO_SIDE_EFFECTS__ */ let foo = () => {};",
"/* #__NO_SIDE_EFFECTS__ */ var foo = function() {};",
"const foo /* #__NO_SIDE_EFFECTS__ */ = () => {};",
];
for source_text in cases {
let comments = get_comments(source_text);
assert_eq!(
comments[0].content,
CommentContent::NoSideEffectsNotApplied,
"{source_text}"
);
}
}
#[test]
fn no_side_effects_comment_applied() {
let cases = [
"/* #__NO_SIDE_EFFECTS__ */ function foo() {}",
"/* #__NO_SIDE_EFFECTS__ */ async function foo() {}",
"/* #__NO_SIDE_EFFECTS__ */ export function foo() {}",
"export default /* #__NO_SIDE_EFFECTS__ */ function foo() {}",
"const foo = /* #__NO_SIDE_EFFECTS__ */ function() {};",
"const foo = /* #__NO_SIDE_EFFECTS__ */ () => {};",
"/* #__NO_SIDE_EFFECTS__ */ const foo = () => {};",
"/* #__NO_SIDE_EFFECTS__ */ export const foo = () => {};",
"[/* #__NO_SIDE_EFFECTS__ */ function() {}];",
];
for source_text in cases {
let comments = get_comments(source_text);
assert_eq!(comments[0].content, CommentContent::NoSideEffects, "{source_text}");
}
}
#[test]
fn comment_parsing() {
let data = [
("/*! legal */", CommentContent::Legal),
("/* @preserve */", CommentContent::Legal),
("/* @license */", CommentContent::Legal),
("/* foo @preserve */", CommentContent::Legal),
("/* foo @license */", CommentContent::Legal),
("/* foo @preserve*/", CommentContent::Legal),
("/* foo @license*/", CommentContent::Legal),
("/* foo @licensed*/", CommentContent::Legal),
("/* foo @License*/", CommentContent::None),
("/* foo @licens*/", CommentContent::None),
("/* foo @preserv*/", CommentContent::None),
("/* @foo @preserve */", CommentContent::Legal),
("/* @foo @license */", CommentContent::Legal),
("/** foo @preserve */", CommentContent::JsdocLegal),
("/** foo @license */", CommentContent::JsdocLegal),
("/** foo @license*/", CommentContent::JsdocLegal),
("// foo @license", CommentContent::Legal),
("/** jsdoc */", CommentContent::Jsdoc),
("/**/", CommentContent::None),
("/***/", CommentContent::None),
("/*@*/", CommentContent::None),
("/*@xreserve*/", CommentContent::None),
("/*@preserve*/", CommentContent::Legal),
("/*@voidzeroignoreme*/", CommentContent::None),
("/****/", CommentContent::None),
("/* @vite-ignore */", CommentContent::Vite),
("/* @vite-xxx */", CommentContent::Vite),
("/* webpackChunkName: 'my-chunk-name' */", CommentContent::Webpack),
("/* webpack */", CommentContent::None),
("/* @__PURE__ */", CommentContent::PureNotApplied),
("/* @__NO_SIDE_EFFECTS__ */", CommentContent::NoSideEffectsNotApplied),
("/* #__PURE__ */", CommentContent::PureNotApplied),
("/* #__NO_SIDE_EFFECTS__ */", CommentContent::NoSideEffectsNotApplied),
("/* @__KEY__ */", CommentContent::PropertyKey),
("/* #__KEY__ */", CommentContent::PropertyKey),
("/*\u{a0}@__KEY__\u{a0}*/", CommentContent::PropertyKey),
("//@__KEY__", CommentContent::PropertyKey),
("// #__KEY__", CommentContent::PropertyKey),
("/**\n * @__KEY__\n */", CommentContent::Jsdoc),
("/* __KEY__ */", CommentContent::None),
("/* @ __KEY__ */", CommentContent::None),
("/* @__key__ */", CommentContent::None),
("/* @__KEY___ */", CommentContent::None),
("/* @__KEY__ extra */", CommentContent::None),
("/* turbopackOptional: true */", CommentContent::Turbopack),
("/* v8 ignore next */", CommentContent::CoverageIgnore),
("/* v8 ignore filename */", CommentContent::CoverageIgnore),
("/* c8 ignore file */", CommentContent::CoverageIgnore),
("/* v8 ignore file */", CommentContent::CoverageIgnoreFile),
("// v8 ignore file", CommentContent::CoverageIgnoreFile),
("/* v8 ignore file -- @preserve */", CommentContent::CoverageIgnoreFile),
("/* istanbul ignore file */", CommentContent::CoverageIgnoreFile),
("// istanbul ignore file -- generated", CommentContent::CoverageIgnoreFile),
];
for (source_text, expected) in data {
let comments = get_comments(source_text);
assert_eq!(comments.len(), 1, "{source_text}");
assert_eq!(comments[0].content, expected, "{source_text}");
}
}
}