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
15 changes: 15 additions & 0 deletions crates/oxc_ast/src/ast/comment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 `/*!`.
/// <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.span.source_text(source_text);
source_text.starts_with('!')
|| source_text.contains("@license")
|| source_text.contains("@preserve")
}
}
133 changes: 78 additions & 55 deletions crates/oxc_codegen/src/comment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,34 +44,23 @@ 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.comments || 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) {
Expand All @@ -81,40 +70,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) {
Expand Down Expand Up @@ -168,4 +141,54 @@ impl<'a> Codegen<'a> {
true
}
}

fn print_comments(&mut self, start: u32, comments: &[Comment], unused_comments: Vec<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);
}
}

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();
}
}
}
}
}
}
2 changes: 1 addition & 1 deletion crates/oxc_codegen/src/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 2 additions & 2 deletions crates/oxc_codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use crate::{
pub use crate::{
context::Context,
gen::{Gen, GenExpr},
options::CodegenOptions,
options::{CodegenOptions, LegalComment},
};

/// Code generator without whitespace removal.
Expand Down Expand Up @@ -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 {
Expand Down
42 changes: 41 additions & 1 deletion crates/oxc_codegen/src/options.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
use std::path::PathBuf;

/// Legal comment
///
/// <https://esbuild.github.io/api/#legal-comments>
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
pub enum LegalComment {
/// Do not preserve any legal comments (default).
#[default]
None,
/// Preserve all legal comments.
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 {
Expand All @@ -13,7 +38,7 @@ pub struct CodegenOptions {
/// Default is `false`.
pub minify: bool,

/// Print comments?
/// Print all comments?
///
/// Default is `true`.
pub comments: bool,
Expand All @@ -25,6 +50,15 @@ pub struct CodegenOptions {
/// Default is `false`.
pub annotation_comments: bool,

/// Print legal comments.
///
/// Only takes into effect when `comments` is false.
///
/// <https://esbuild.github.io/api/#legal-comments>
///
/// Default is [LegalComment::None].
pub legal_comments: LegalComment,

/// Override the source map path. This affects the `sourceMappingURL`
/// comment at the end of the generated code.
///
Expand All @@ -40,12 +74,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)
}
Expand Down
15 changes: 15 additions & 0 deletions crates/oxc_codegen/tests/integration/legal_comments.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use crate::snapshot;

fn cases() -> Vec<&'static str> {
vec![
"/* @license */\n/* @license */\nfoo;bar;",
"/* @license */\n/* @preserve */\nfoo;bar;",
"/* @license */\n//! KEEP\nfoo;bar;",
"/* @license */\n/*! KEEP */\nfoo;bar;",
]
}

#[test]
fn legal_inline_comment() {
snapshot("legal_inline_comments", &cases());
}
21 changes: 15 additions & 6 deletions crates/oxc_codegen/tests/integration/main.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,24 +13,32 @@ use oxc_parser::Parser;
use oxc_span::SourceType;

pub fn codegen(source_text: &str) -> String {
codegen_options(source_text, &CodegenOptions::default())
}

pub fn codegen_options(source_text: &str, options: &CodegenOptions) -> String {
let allocator = Allocator::default();
let source_type = SourceType::ts();
let ret = Parser::new(&allocator, source_text, source_type).parse();
CodeGenerator::new()
.with_options(CodegenOptions { single_quote: true, ..CodegenOptions::default() })
.build(&ret.program)
.code
let mut options = options.clone();
options.single_quote = true;
CodeGenerator::new().with_options(options).build(&ret.program).code
}

pub fn snapshot(name: &str, cases: &[&str]) {
snapshot_options(name, cases, &CodegenOptions::default());
}

pub fn snapshot_options(name: &str, cases: &[&str], options: &CodegenOptions) {
use std::fmt::Write;

let snapshot = cases.iter().enumerate().fold(String::new(), |mut w, (i, case)| {
write!(w, "########## {i}\n{case}\n----------\n{}\n", codegen(case)).unwrap();
let result = codegen_options(case, options);
write!(w, "########## {i}\n{case}\n----------\n{result}\n",).unwrap();
w
});

insta::with_settings!({ prepend_module_to_snapshot => false, omit_expression => true }, {
insta::with_settings!({ prepend_module_to_snapshot => false, snapshot_suffix => "", omit_expression => true }, {
insta::assert_snapshot!(name, snapshot);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
source: crates/oxc_codegen/tests/integration/main.rs
---
########## 0
/* @license */
/* @license */
foo;bar;
----------
/* @license */
/* @license */
foo;
bar;

########## 1
/* @license */
/* @preserve */
foo;bar;
----------
/* @license */
/* @preserve */
foo;
bar;

########## 2
/* @license */
//! KEEP
foo;bar;
----------
/* @license */
//! KEEP
foo;
bar;

########## 3
/* @license */
/*! KEEP */
foo;bar;
----------
/* @license */
/*! KEEP */
foo;
bar;