mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(codegen): print inline legal comments (#7054)
part of https://github.com/oxc-project/oxc/issues/7050
This commit is contained in:
parent
7122e0074b
commit
6516f9eabc
8 changed files with 210 additions and 66 deletions
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
15
crates/oxc_codegen/tests/integration/legal_comments.rs
Normal file
15
crates/oxc_codegen/tests/integration/legal_comments.rs
Normal 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());
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Reference in a new issue