diff --git a/crates/oxc_ast/src/ast/comment.rs b/crates/oxc_ast/src/ast/comment.rs index 2150eb0886050d..ec436eb584650b 100644 --- a/crates/oxc_ast/src/ast/comment.rs +++ b/crates/oxc_ast/src/ast/comment.rs @@ -125,4 +125,19 @@ impl Comment { pub fn is_jsdoc(&self, source_text: &str) -> bool { self.is_leading() && self.is_block() && self.span.source_text(source_text).starts_with('*') } + + /// Legal comments + /// + /// A "legal comment" is considered to be any statement-level comment + /// that contains `@license` or `@preserve` or that starts with `//!` or `/*!`. + /// + pub fn is_legal(&self, source_text: &str) -> bool { + if !self.is_leading() { + return false; + } + let source_text = self.span.source_text(source_text); + source_text.starts_with('!') + || source_text.contains("@license") + || source_text.contains("@preserve") + } } diff --git a/crates/oxc_codegen/src/comment.rs b/crates/oxc_codegen/src/comment.rs index 3db8da578844a9..793e6ff1577d96 100644 --- a/crates/oxc_codegen/src/comment.rs +++ b/crates/oxc_codegen/src/comment.rs @@ -44,34 +44,22 @@ impl<'a> Codegen<'a> { }) } + fn is_annotation_comment(&self, comment: &Comment) -> bool { + let comment_content = comment.span.source_text(self.source_text); + ANNOTATION_MATCHER.find_iter(comment_content).count() != 0 + } + + fn is_legal_comment(&self, comment: &Comment) -> bool { + self.options.legal_comments.is_inline() && comment.is_legal(self.source_text) + } + /// Weather to keep leading comments. fn is_leading_comments(&self, comment: &Comment) -> bool { - (comment.is_jsdoc(self.source_text) || (comment.is_line() && self.is_annotation_comment(comment))) - && comment.preceded_by_newline - // webpack comment `/*****/` + comment.preceded_by_newline + && (comment.is_jsdoc(self.source_text) + || (comment.is_line() && self.is_annotation_comment(comment))) && !comment.span.source_text(self.source_text).chars().all(|c| c == '*') - } - - fn print_comment(&mut self, comment: &Comment) { - let comment_source = comment.real_span().source_text(self.source_text); - match comment.kind { - CommentKind::Line => { - self.print_str(comment_source); - } - CommentKind::Block => { - // Print block comments with our own indentation. - let lines = comment_source.split(is_line_terminator); - for line in lines { - if !line.starts_with("/*") { - self.print_indent(); - } - self.print_str(line.trim_start()); - if !line.ends_with("*/") { - self.print_hard_newline(); - } - } - } - } + // webpack comment `/*****/` } pub(crate) fn print_leading_comments(&mut self, start: u32) { @@ -81,40 +69,24 @@ impl<'a> Codegen<'a> { 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)); - - if comments.first().is_some_and(|c| c.preceded_by_newline) { - // Skip printing newline if this comment is already on a newline. - if self.last_byte().is_some_and(|b| b != b'\n' && b != b'\t') { - self.print_hard_newline(); - self.print_indent(); - } - } - - for (i, comment) in comments.iter().enumerate() { - if i >= 1 && comment.preceded_by_newline { - self.print_hard_newline(); - self.print_indent(); - } - - self.print_comment(comment); - } - - if comments.last().is_some_and(|c| c.is_line() || c.followed_by_newline) { - self.print_hard_newline(); - self.print_indent(); - } - - if !unused_comments.is_empty() { - self.comments.insert(start, unused_comments); - } + self.print_comments(start, &comments, unused_comments); } - fn is_annotation_comment(&self, comment: &Comment) -> bool { - let comment_content = comment.span.source_text(self.source_text); - ANNOTATION_MATCHER.find_iter(comment_content).count() != 0 + /// A statement comment also includes legal comments + pub(crate) fn print_statement_comments(&mut self, start: u32) { + if self.options.minify { + return; + } + 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.is_legal_comment(comment) + }); + self.print_comments(start, &comments, unused_comments); } pub(crate) fn print_annotation_comments(&mut self, node_start: u32) { @@ -168,4 +140,54 @@ impl<'a> Codegen<'a> { true } } + + fn print_comments(&mut self, start: u32, comments: &[Comment], unused_comments: Vec) { + if comments.first().is_some_and(|c| c.preceded_by_newline) { + // Skip printing newline if this comment is already on a newline. + if self.last_byte().is_some_and(|b| b != b'\n' && b != b'\t') { + self.print_hard_newline(); + self.print_indent(); + } + } + + for (i, comment) in comments.iter().enumerate() { + if i >= 1 && comment.preceded_by_newline { + self.print_hard_newline(); + self.print_indent(); + } + + self.print_comment(comment); + } + + if comments.last().is_some_and(|c| c.is_line() || c.followed_by_newline) { + self.print_hard_newline(); + self.print_indent(); + } + + if !unused_comments.is_empty() { + self.comments.insert(start, unused_comments); + } + } + + fn print_comment(&mut self, comment: &Comment) { + let comment_source = comment.real_span().source_text(self.source_text); + match comment.kind { + CommentKind::Line => { + self.print_str(comment_source); + } + CommentKind::Block => { + // Print block comments with our own indentation. + let lines = comment_source.split(is_line_terminator); + for line in lines { + if !line.starts_with("/*") { + self.print_indent(); + } + self.print_str(line.trim_start()); + if !line.ends_with("*/") { + self.print_hard_newline(); + } + } + } + } + } } diff --git a/crates/oxc_codegen/src/gen.rs b/crates/oxc_codegen/src/gen.rs index 975c2a03c6963b..e9d60de5a0b653 100644 --- a/crates/oxc_codegen/src/gen.rs +++ b/crates/oxc_codegen/src/gen.rs @@ -76,7 +76,7 @@ impl<'a> Gen for Directive<'a> { impl<'a> Gen for Statement<'a> { fn gen(&self, p: &mut Codegen, ctx: Context) { - p.print_leading_comments(self.span().start); + p.print_statement_comments(self.span().start); match self { Self::BlockStatement(stmt) => stmt.print(p, ctx), Self::BreakStatement(stmt) => stmt.print(p, ctx), diff --git a/crates/oxc_codegen/src/lib.rs b/crates/oxc_codegen/src/lib.rs index ec97b58cece4c6..089f6289482387 100644 --- a/crates/oxc_codegen/src/lib.rs +++ b/crates/oxc_codegen/src/lib.rs @@ -33,7 +33,7 @@ use crate::{ pub use crate::{ context::Context, gen::{Gen, GenExpr}, - options::CodegenOptions, + options::{CodegenOptions, LegalComment}, }; /// Code generator without whitespace removal. @@ -190,7 +190,7 @@ impl<'a> Codegen<'a> { self.quote = if self.options.single_quote { b'\'' } else { b'"' }; self.source_text = program.source_text; self.code.reserve(program.source_text.len()); - if self.options.print_annotation_comments() { + if self.options.print_comments() { self.build_comments(&program.comments); } if let Some(path) = &self.options.source_map_path { diff --git a/crates/oxc_codegen/src/options.rs b/crates/oxc_codegen/src/options.rs index b692751c096891..7b2e89fe73038b 100644 --- a/crates/oxc_codegen/src/options.rs +++ b/crates/oxc_codegen/src/options.rs @@ -1,5 +1,30 @@ use std::path::PathBuf; +/// Legal comment +/// +/// +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +pub enum LegalComment { + /// Do not preserve any legal comments. + None, + /// Preserve all legal comments (default). + #[default] + Inline, + /// Move all legal comments to the end of the file. + Eof, + /// Move all legal comments to a .LEGAL.txt file and link to them with a comment. + Linked, + /// Move all legal comments to a .LEGAL.txt file but to not link to them. + External, +} + +impl LegalComment { + /// Is inline mode. + pub fn is_inline(self) -> bool { + self == Self::Inline + } +} + /// Codegen Options. #[derive(Debug, Clone)] pub struct CodegenOptions { @@ -25,6 +50,13 @@ pub struct CodegenOptions { /// Default is `false`. pub annotation_comments: bool, + /// Print legal comments. + /// + /// Only takes into effect when `comments` is false. + /// + /// + pub legal_comments: LegalComment, + /// Override the source map path. This affects the `sourceMappingURL` /// comment at the end of the generated code. /// @@ -40,12 +72,18 @@ impl Default for CodegenOptions { minify: false, comments: true, annotation_comments: false, + legal_comments: LegalComment::default(), source_map_path: None, } } } impl CodegenOptions { + pub(crate) fn print_comments(&self) -> bool { + !self.minify + && (self.comments || self.annotation_comments || self.legal_comments.is_inline()) + } + pub(crate) fn print_annotation_comments(&self) -> bool { !self.minify && (self.comments || self.annotation_comments) } diff --git a/crates/oxc_codegen/tests/integration/legal_comments.rs b/crates/oxc_codegen/tests/integration/legal_comments.rs new file mode 100644 index 00000000000000..9c2b7f814859a8 --- /dev/null +++ b/crates/oxc_codegen/tests/integration/legal_comments.rs @@ -0,0 +1,9 @@ +use crate::snapshot; + +#[test] +fn legal_comment() { + let cases = + vec!["/* @license */foo;", "/* @preserve */foo;", "//! KEEP\nfoo;", "/*! KEEP */\nfoo;"]; + + snapshot("legal_comments", &cases); +} diff --git a/crates/oxc_codegen/tests/integration/main.rs b/crates/oxc_codegen/tests/integration/main.rs index b2c5c4f9f15c1b..354c12cc294ab9 100644 --- a/crates/oxc_codegen/tests/integration/main.rs +++ b/crates/oxc_codegen/tests/integration/main.rs @@ -1,6 +1,7 @@ #![allow(clippy::missing_panics_doc)] pub mod esbuild; pub mod jsdoc; +pub mod legal_comments; pub mod pure_comments; pub mod tester; pub mod ts; diff --git a/crates/oxc_codegen/tests/integration/snapshots/legal_comments.snap b/crates/oxc_codegen/tests/integration/snapshots/legal_comments.snap new file mode 100644 index 00000000000000..5f31bccbb64427 --- /dev/null +++ b/crates/oxc_codegen/tests/integration/snapshots/legal_comments.snap @@ -0,0 +1,26 @@ +--- +source: crates/oxc_codegen/tests/integration/main.rs +--- +########## 0 +/* @license */foo; +---------- +/* @license */foo; + +########## 1 +/* @preserve */foo; +---------- +/* @preserve */foo; + +########## 2 +//! KEEP +foo; +---------- +//! KEEP +foo; + +########## 3 +/*! KEEP */ +foo; +---------- +/*! KEEP */ +foo;