Skip to content

Commit

Permalink
feat(codegen): print inline legal comments
Browse files Browse the repository at this point in the history
part of #7050
  • Loading branch information
Boshen committed Nov 1, 2024
1 parent e4daa64 commit e17e8ee
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 58 deletions.
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")
}
}
132 changes: 77 additions & 55 deletions crates/oxc_codegen/src/comment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -168,4 +140,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
38 changes: 38 additions & 0 deletions 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.
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 {
Expand All @@ -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.
///
/// <https://esbuild.github.io/api/#legal-comments>
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 +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)
}
Expand Down
9 changes: 9 additions & 0 deletions crates/oxc_codegen/tests/integration/legal_comments.rs
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 1 addition & 0 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 Down
26 changes: 26 additions & 0 deletions crates/oxc_codegen/tests/integration/snapshots/legal_comments.snap
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit e17e8ee

Please sign in to comment.