mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +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 {
|
pub fn is_jsdoc(&self, source_text: &str) -> bool {
|
||||||
self.is_leading() && self.is_block() && self.span.source_text(source_text).starts_with('*')
|
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_annotation_comment(&self, comment: &Comment) -> bool {
|
||||||
fn is_leading_comments(&self, comment: &Comment) -> bool {
|
let comment_content = comment.span.source_text(self.source_text);
|
||||||
(comment.is_jsdoc(self.source_text) || (comment.is_line() && self.is_annotation_comment(comment)))
|
ANNOTATION_MATCHER.find_iter(comment_content).count() != 0
|
||||||
&& comment.preceded_by_newline
|
|
||||||
// webpack comment `/*****/`
|
|
||||||
&& !comment.span.source_text(self.source_text).chars().all(|c| c == '*')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_comment(&mut self, comment: &Comment) {
|
fn is_legal_comment(&self, comment: &Comment) -> bool {
|
||||||
let comment_source = comment.real_span().source_text(self.source_text);
|
(self.options.comments || self.options.legal_comments.is_inline())
|
||||||
match comment.kind {
|
&& comment.is_legal(self.source_text)
|
||||||
CommentKind::Line => {
|
}
|
||||||
self.print_str(comment_source);
|
|
||||||
}
|
/// Weather to keep leading comments.
|
||||||
CommentKind::Block => {
|
fn is_leading_comments(&self, comment: &Comment) -> bool {
|
||||||
// Print block comments with our own indentation.
|
comment.preceded_by_newline
|
||||||
let lines = comment_source.split(is_line_terminator);
|
&& (comment.is_jsdoc(self.source_text)
|
||||||
for line in lines {
|
|| (comment.is_line() && self.is_annotation_comment(comment)))
|
||||||
if !line.starts_with("/*") {
|
&& !comment.span.source_text(self.source_text).chars().all(|c| c == '*')
|
||||||
self.print_indent();
|
// webpack comment `/*****/`
|
||||||
}
|
|
||||||
self.print_str(line.trim_start());
|
|
||||||
if !line.ends_with("*/") {
|
|
||||||
self.print_hard_newline();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn print_leading_comments(&mut self, start: u32) {
|
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 {
|
let Some(comments) = self.comments.remove(&start) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let (comments, unused_comments): (Vec<_>, Vec<_>) =
|
let (comments, unused_comments): (Vec<_>, Vec<_>) =
|
||||||
comments.into_iter().partition(|comment| self.is_leading_comments(comment));
|
comments.into_iter().partition(|comment| self.is_leading_comments(comment));
|
||||||
|
self.print_comments(start, &comments, unused_comments);
|
||||||
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 is_annotation_comment(&self, comment: &Comment) -> bool {
|
/// A statement comment also includes legal comments
|
||||||
let comment_content = comment.span.source_text(self.source_text);
|
pub(crate) fn print_statement_comments(&mut self, start: u32) {
|
||||||
ANNOTATION_MATCHER.find_iter(comment_content).count() != 0
|
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) {
|
pub(crate) fn print_annotation_comments(&mut self, node_start: u32) {
|
||||||
|
|
@ -168,4 +141,54 @@ impl<'a> Codegen<'a> {
|
||||||
true
|
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> {
|
impl<'a> Gen for Statement<'a> {
|
||||||
fn gen(&self, p: &mut Codegen, ctx: Context) {
|
fn gen(&self, p: &mut Codegen, ctx: Context) {
|
||||||
p.print_leading_comments(self.span().start);
|
p.print_statement_comments(self.span().start);
|
||||||
match self {
|
match self {
|
||||||
Self::BlockStatement(stmt) => stmt.print(p, ctx),
|
Self::BlockStatement(stmt) => stmt.print(p, ctx),
|
||||||
Self::BreakStatement(stmt) => stmt.print(p, ctx),
|
Self::BreakStatement(stmt) => stmt.print(p, ctx),
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ use crate::{
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
context::Context,
|
context::Context,
|
||||||
gen::{Gen, GenExpr},
|
gen::{Gen, GenExpr},
|
||||||
options::CodegenOptions,
|
options::{CodegenOptions, LegalComment},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Code generator without whitespace removal.
|
/// Code generator without whitespace removal.
|
||||||
|
|
@ -190,7 +190,7 @@ impl<'a> Codegen<'a> {
|
||||||
self.quote = if self.options.single_quote { b'\'' } else { b'"' };
|
self.quote = if self.options.single_quote { b'\'' } else { b'"' };
|
||||||
self.source_text = program.source_text;
|
self.source_text = program.source_text;
|
||||||
self.code.reserve(program.source_text.len());
|
self.code.reserve(program.source_text.len());
|
||||||
if self.options.print_annotation_comments() {
|
if self.options.print_comments() {
|
||||||
self.build_comments(&program.comments);
|
self.build_comments(&program.comments);
|
||||||
}
|
}
|
||||||
if let Some(path) = &self.options.source_map_path {
|
if let Some(path) = &self.options.source_map_path {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,30 @@
|
||||||
use std::path::PathBuf;
|
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.
|
/// Codegen Options.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CodegenOptions {
|
pub struct CodegenOptions {
|
||||||
|
|
@ -13,7 +38,7 @@ pub struct CodegenOptions {
|
||||||
/// Default is `false`.
|
/// Default is `false`.
|
||||||
pub minify: bool,
|
pub minify: bool,
|
||||||
|
|
||||||
/// Print comments?
|
/// Print all comments?
|
||||||
///
|
///
|
||||||
/// Default is `true`.
|
/// Default is `true`.
|
||||||
pub comments: bool,
|
pub comments: bool,
|
||||||
|
|
@ -25,6 +50,15 @@ pub struct CodegenOptions {
|
||||||
/// Default is `false`.
|
/// Default is `false`.
|
||||||
pub annotation_comments: bool,
|
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`
|
/// Override the source map path. This affects the `sourceMappingURL`
|
||||||
/// comment at the end of the generated code.
|
/// comment at the end of the generated code.
|
||||||
///
|
///
|
||||||
|
|
@ -40,12 +74,18 @@ impl Default for CodegenOptions {
|
||||||
minify: false,
|
minify: false,
|
||||||
comments: true,
|
comments: true,
|
||||||
annotation_comments: false,
|
annotation_comments: false,
|
||||||
|
legal_comments: LegalComment::default(),
|
||||||
source_map_path: None,
|
source_map_path: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CodegenOptions {
|
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 {
|
pub(crate) fn print_annotation_comments(&self) -> bool {
|
||||||
!self.minify && (self.comments || self.annotation_comments)
|
!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)]
|
#![allow(clippy::missing_panics_doc)]
|
||||||
pub mod esbuild;
|
pub mod esbuild;
|
||||||
pub mod jsdoc;
|
pub mod jsdoc;
|
||||||
|
pub mod legal_comments;
|
||||||
pub mod pure_comments;
|
pub mod pure_comments;
|
||||||
pub mod tester;
|
pub mod tester;
|
||||||
pub mod ts;
|
pub mod ts;
|
||||||
|
|
@ -12,24 +13,32 @@ use oxc_parser::Parser;
|
||||||
use oxc_span::SourceType;
|
use oxc_span::SourceType;
|
||||||
|
|
||||||
pub fn codegen(source_text: &str) -> String {
|
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 allocator = Allocator::default();
|
||||||
let source_type = SourceType::ts();
|
let source_type = SourceType::ts();
|
||||||
let ret = Parser::new(&allocator, source_text, source_type).parse();
|
let ret = Parser::new(&allocator, source_text, source_type).parse();
|
||||||
CodeGenerator::new()
|
let mut options = options.clone();
|
||||||
.with_options(CodegenOptions { single_quote: true, ..CodegenOptions::default() })
|
options.single_quote = true;
|
||||||
.build(&ret.program)
|
CodeGenerator::new().with_options(options).build(&ret.program).code
|
||||||
.code
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn snapshot(name: &str, cases: &[&str]) {
|
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;
|
use std::fmt::Write;
|
||||||
|
|
||||||
let snapshot = cases.iter().enumerate().fold(String::new(), |mut w, (i, case)| {
|
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
|
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);
|
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