feat(codegen): print inline legal comments (#7054)

part of https://github.com/oxc-project/oxc/issues/7050
This commit is contained in:
Boshen 2024-11-01 16:56:56 +00:00
parent 7122e0074b
commit 6516f9eabc
8 changed files with 210 additions and 66 deletions

View file

@ -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")
}
}

View file

@ -44,34 +44,23 @@ impl<'a> Codegen<'a> {
})
}
/// 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.span.source_text(self.source_text).chars().all(|c| c == '*')
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 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();
}
}
}
}
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.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 == '*')
// webpack comment `/*****/`
}
pub(crate) fn print_leading_comments(&mut self, start: u32) {
@ -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) {
@ -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();
}
}
}
}
}
}

View file

@ -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),

View file

@ -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 {

View file

@ -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 {
@ -13,7 +38,7 @@ pub struct CodegenOptions {
/// Default is `false`.
pub minify: bool,
/// Print comments?
/// Print all comments?
///
/// Default is `true`.
pub comments: bool,
@ -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.
///
@ -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)
}

View file

@ -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());
}

View file

@ -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;
@ -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);
});
}

View file

@ -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;