Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 78 additions & 21 deletions crates/oxc_ast/src/ast/comment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,43 @@ pub enum CommentPosition {
Trailing = 1,
}

/// Annotation comment that has special meaning.
#[ast]
#[generate_derive(CloneIn, ContentEq)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
pub enum CommentAnnotation {
/// No Annotation
#[default]
None = 0,

/// `/** jsdoc */`
/// <https://jsdoc.app>
Jsdoc = 1,

/// Legal Comment
/// e.g. `/* @license */`, `/* @preserve */`, or starts with `//!` or `/*!`.
///
/// <https://esbuild.github.io/api/#legal-comments>
Legal = 2,

/// `/* #__PURE__ */`
/// <https://github.com/javascript-compiler-hints/compiler-notations-spec>
Pure = 3,

/// `/* #__NO_SIDE_EFFECTS__ */`
NoSideEffects = 4,

/// Webpack magic comment
/// e.g. `/* webpackChunkName */`
/// <https://webpack.js.org/api/module-methods/#magic-comments>
Webpack = 5,

/// Vite comment
/// e.g. `/* @vite-ignore */`
/// <https://github.com/search?q=repo%3Avitejs%2Fvite%20vite-ignore&type=code>
Vite = 6,
}

/// A comment in source code.
#[ast]
#[generate_derive(CloneIn, ContentEq)]
Expand All @@ -64,6 +101,9 @@ pub struct Comment {

/// Whether this comment has a tailing newline.
pub followed_by_newline: bool,

/// Comment Annotation
pub annotation: CommentAnnotation,
}

impl Comment {
Expand All @@ -78,6 +118,15 @@ impl Comment {
position: CommentPosition::Trailing,
preceded_by_newline: false,
followed_by_newline: false,
annotation: CommentAnnotation::None,
}
}

/// Gets the span of the comment content.
pub fn content_span(&self) -> Span {
match self.kind {
CommentKind::Line => Span::new(self.span.start + 2, self.span.end),
CommentKind::Block => Span::new(self.span.start + 2, self.span.end - 2),
}
}

Expand All @@ -101,35 +150,43 @@ impl Comment {
self.position == CommentPosition::Trailing
}

/// Returns `true` if this comment is a JSDoc comment. Implies `is_leading`
/// and `is_block`.
pub fn is_jsdoc(&self, source_text: &str) -> bool {
self.is_leading() && self.is_block() && {
let span = self.content_span();
!span.is_empty() && source_text.as_bytes()[span.start as usize] == b'*'
}
/// Is comment with special meaning.
pub fn is_annotation(self) -> bool {
self.annotation != CommentAnnotation::None
}

/// Returns `true` if this comment is a JSDoc comment. Implies `is_leading` and `is_block`.
pub fn is_jsdoc(self) -> bool {
self.is_leading() && self.annotation == CommentAnnotation::Jsdoc
}

/// Legal comments
///
/// A "legal comment" is considered to be any statement-level comment
/// that contains `@license` or `@preserve` or that starts with `//!` or `/*!`.
///
/// <https://esbuild.github.io/api/#legal-comments>
pub fn is_legal(&self, source_text: &str) -> bool {
if !self.is_leading() {
return false;
}
let source_text = self.content_span().source_text(source_text);
source_text.starts_with('!')
|| source_text.contains("@license")
|| source_text.contains("@preserve")
pub fn is_legal(self) -> bool {
self.is_leading() && self.annotation == CommentAnnotation::Legal
}

/// Gets the span of the comment content.
pub fn content_span(&self) -> Span {
match self.kind {
CommentKind::Line => Span::new(self.span.start + 2, self.span.end),
CommentKind::Block => Span::new(self.span.start + 2, self.span.end - 2),
}
/// Is `/* @__PURE__*/`.
pub fn is_pure(self) -> bool {
self.annotation == CommentAnnotation::Pure
}

/// Is `/* @__NO_SIDE_EFFECTS__*/`.
pub fn is_no_side_effects(self) -> bool {
self.annotation == CommentAnnotation::NoSideEffects
}

/// Is webpack magic comment.
pub fn is_webpack(self) -> bool {
self.annotation == CommentAnnotation::Webpack
}

/// Is vite special comment.
pub fn is_vite(self) -> bool {
self.annotation == CommentAnnotation::Vite
}
}
12 changes: 10 additions & 2 deletions crates/oxc_ast/src/generated/assert_layouts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1396,14 +1396,18 @@ const _: () = {
assert!(size_of::<CommentPosition>() == 1);
assert!(align_of::<CommentPosition>() == 1);

assert!(size_of::<Comment>() == 16);
assert!(size_of::<CommentAnnotation>() == 1);
assert!(align_of::<CommentAnnotation>() == 1);

assert!(size_of::<Comment>() == 24);
assert!(align_of::<Comment>() == 8);
assert!(offset_of!(Comment, span) == 0);
assert!(offset_of!(Comment, attached_to) == 8);
assert!(offset_of!(Comment, kind) == 12);
assert!(offset_of!(Comment, position) == 13);
assert!(offset_of!(Comment, preceded_by_newline) == 14);
assert!(offset_of!(Comment, followed_by_newline) == 15);
assert!(offset_of!(Comment, annotation) == 16);
};

#[cfg(target_pointer_width = "32")]
Expand Down Expand Up @@ -2795,14 +2799,18 @@ const _: () = {
assert!(size_of::<CommentPosition>() == 1);
assert!(align_of::<CommentPosition>() == 1);

assert!(size_of::<Comment>() == 16);
assert!(size_of::<CommentAnnotation>() == 1);
assert!(align_of::<CommentAnnotation>() == 1);

assert!(size_of::<Comment>() == 20);
assert!(align_of::<Comment>() == 4);
assert!(offset_of!(Comment, span) == 0);
assert!(offset_of!(Comment, attached_to) == 8);
assert!(offset_of!(Comment, kind) == 12);
assert!(offset_of!(Comment, position) == 13);
assert!(offset_of!(Comment, preceded_by_newline) == 14);
assert!(offset_of!(Comment, followed_by_newline) == 15);
assert!(offset_of!(Comment, annotation) == 16);
};

#[cfg(not(any(target_pointer_width = "64", target_pointer_width = "32")))]
Expand Down
16 changes: 16 additions & 0 deletions crates/oxc_ast/src/generated/derive_clone_in.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4154,6 +4154,21 @@ impl<'alloc> CloneIn<'alloc> for CommentPosition {
}
}

impl<'alloc> CloneIn<'alloc> for CommentAnnotation {
type Cloned = CommentAnnotation;
fn clone_in(&self, _: &'alloc Allocator) -> Self::Cloned {
match self {
Self::None => CommentAnnotation::None,
Self::Jsdoc => CommentAnnotation::Jsdoc,
Self::Legal => CommentAnnotation::Legal,
Self::Pure => CommentAnnotation::Pure,
Self::NoSideEffects => CommentAnnotation::NoSideEffects,
Self::Webpack => CommentAnnotation::Webpack,
Self::Vite => CommentAnnotation::Vite,
}
}
}

impl<'alloc> CloneIn<'alloc> for Comment {
type Cloned = Comment;
fn clone_in(&self, allocator: &'alloc Allocator) -> Self::Cloned {
Expand All @@ -4164,6 +4179,7 @@ impl<'alloc> CloneIn<'alloc> for Comment {
position: CloneIn::clone_in(&self.position, allocator),
preceded_by_newline: CloneIn::clone_in(&self.preceded_by_newline, allocator),
followed_by_newline: CloneIn::clone_in(&self.followed_by_newline, allocator),
annotation: CloneIn::clone_in(&self.annotation, allocator),
}
}
}
7 changes: 7 additions & 0 deletions crates/oxc_ast/src/generated/derive_content_eq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2497,12 +2497,19 @@ impl ContentEq for CommentPosition {
}
}

impl ContentEq for CommentAnnotation {
fn content_eq(&self, other: &Self) -> bool {
self == other
}
}

impl ContentEq for Comment {
fn content_eq(&self, other: &Self) -> bool {
ContentEq::content_eq(&self.attached_to, &other.attached_to)
&& ContentEq::content_eq(&self.kind, &other.kind)
&& ContentEq::content_eq(&self.position, &other.position)
&& ContentEq::content_eq(&self.preceded_by_newline, &other.preceded_by_newline)
&& ContentEq::content_eq(&self.followed_by_newline, &other.followed_by_newline)
&& ContentEq::content_eq(&self.annotation, &other.annotation)
}
}
2 changes: 1 addition & 1 deletion crates/oxc_ast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ mod generated {
pub use generated::{ast_builder, ast_kind};

pub use crate::{
ast::comment::{Comment, CommentKind, CommentPosition},
ast::comment::{Comment, CommentAnnotation, CommentKind, CommentPosition},
ast_builder::AstBuilder,
ast_builder_impl::NONE,
ast_kind::{AstKind, AstType},
Expand Down
88 changes: 23 additions & 65 deletions crates/oxc_codegen/src/comment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ pub type CommentsMap = FxHashMap</* attached_to */ u32, Vec<Comment>>;

impl Codegen<'_> {
pub(crate) fn build_comments(&mut self, comments: &[Comment]) {
self.comments.reserve(comments.len());
for comment in comments {
// Omit pure comments because they are handled separately.
if comment.is_pure() || comment.is_no_side_effects() {
continue;
}
self.comments.entry(comment.attached_to).or_default().push(*comment);
}
}
Expand All @@ -18,34 +23,9 @@ impl Codegen<'_> {
self.comments.contains_key(&start)
}

pub(crate) fn has_non_annotation_comment(&self, start: u32) -> bool {
if self.options.print_annotation_comments() {
self.comments.get(&start).is_some_and(|comments| {
comments.iter().any(|comment| !self.is_pure_comment(comment))
})
} else {
self.has_comment(start)
}
}

/// `#__PURE__` Notation Specification
///
/// <https://github.com/javascript-compiler-hints/compiler-notations-spec/blob/c14f7e197cb225c9eee877143536665ce3150712/pure-notation-spec.md>
fn is_pure_comment(&self, comment: &Comment) -> bool {
let s = comment.content_span().source_text(self.source_text).trim_start();
if let Some(s) = s.strip_prefix(['@', '#']) {
s.starts_with("__PURE__") || s.starts_with("__NO_SIDE_EFFECTS__")
} else {
false
}
}

/// Whether to keep leading comments.
fn is_leading_comments(&self, comment: &Comment) -> bool {
comment.preceded_by_newline
&& (comment.is_jsdoc(self.source_text) && !self.is_pure_comment(comment))
&& !comment.content_span().source_text(self.source_text).chars().all(|c| c == '*')
// webpack comment `/*****/`
fn should_keep_leading_comment(comment: &Comment) -> bool {
comment.preceded_by_newline && comment.is_annotation()
}

pub(crate) fn print_leading_comments(&mut self, start: u32) {
Expand All @@ -55,26 +35,18 @@ impl Codegen<'_> {
let Some(comments) = self.comments.remove(&start) else {
return;
};
let (comments, unused_comments): (Vec<_>, Vec<_>) =
comments.into_iter().partition(|comment| self.is_leading_comments(comment));
self.print_comments(start, &comments, unused_comments);
let comments =
comments.into_iter().filter(Self::should_keep_leading_comment).collect::<Vec<_>>();
self.print_comments(&comments);
}

pub(crate) fn get_statement_comments(
&mut self,
start: u32,
) -> Option<(Vec<Comment>, Vec<Comment>)> {
pub(crate) fn get_statement_comments(&mut self, start: u32) -> Option<Vec<Comment>> {
let comments = self.comments.remove(&start)?;

let mut leading_comments = vec![];
let mut unused_comments = vec![];

for comment in comments {
if self.is_leading_comments(&comment) {
leading_comments.push(comment);
continue;
}
if comment.is_legal(self.source_text) {
if comment.is_legal() {
match &self.options.legal_comments {
LegalComment::None if self.options.comments => {
leading_comments.push(comment);
Expand All @@ -91,33 +63,28 @@ impl Codegen<'_> {
LegalComment::None => {}
}
}
unused_comments.push(comment);
if Self::should_keep_leading_comment(&comment) {
leading_comments.push(comment);
continue;
}
}

Some((leading_comments, unused_comments))
Some(leading_comments)
}

/// A statement comment also includes legal comments
#[inline]
pub(crate) fn print_statement_comments(&mut self, start: u32) {
if !self.print_comments {
return;
}
if let Some((comments, unused)) = self.get_statement_comments(start) {
self.print_comments(start, &comments, unused);
if self.print_comments {
if let Some(comments) = self.get_statement_comments(start) {
self.print_comments(&comments);
}
}
}

pub(crate) fn print_expr_comments(&mut self, start: u32) -> bool {
let Some(comments) = self.comments.remove(&start) else { return false };

let (annotation_comments, comments): (Vec<_>, Vec<_>) =
comments.into_iter().partition(|comment| self.is_pure_comment(comment));

if !annotation_comments.is_empty() {
self.comments.insert(start, annotation_comments);
}

for comment in &comments {
self.print_hard_newline();
self.print_indent();
Expand All @@ -132,12 +99,7 @@ impl Codegen<'_> {
}
}

pub(crate) fn print_comments(
&mut self,
start: u32,
comments: &[Comment],
unused_comments: Vec<Comment>,
) {
pub(crate) fn print_comments(&mut self, comments: &[Comment]) {
for (i, comment) in comments.iter().enumerate() {
if i == 0 {
if comment.preceded_by_newline {
Expand All @@ -160,7 +122,7 @@ impl Codegen<'_> {
if comment.preceded_by_newline {
self.print_hard_newline();
self.print_indent();
} else if comment.is_legal(self.source_text) {
} else if comment.is_legal() {
self.print_hard_newline();
}
}
Expand All @@ -173,10 +135,6 @@ impl Codegen<'_> {
}
}
}

if !unused_comments.is_empty() {
self.comments.insert(start, unused_comments);
}
}

fn print_comment(&mut self, comment: &Comment) {
Expand Down
Loading
Loading