From c49f4ce41509199db72c73faee4676d583cefacb Mon Sep 17 00:00:00 2001 From: camc314 <18101008+camc314@users.noreply.github.com> Date: Sun, 9 Aug 2026 09:18:05 +0000 Subject: [PATCH] fix(parser): preserve terminal license comments (#25400) ## Summary - Preserve legal comments whose `@license` marker ends exactly at comment-content EOF. - Add parser and minifier regression coverage. ## Details The optimized marker scan excluded the final valid start for the eight-byte `@license` marker. Comments such as `/*x@license*/` were parsed but classified as ordinary comments and could therefore be dropped from legal-comment output or extraction. The parser hot path now includes that final candidate while retaining unchecked reads, with an explicit remaining-length guard before reading the nine-byte `@preserve` marker. ## Ecosystem compatibility This makes Oxc recognize terminal markers such as `/*x@license*/` and `//x@license`, matching tested behavior in esbuild 0.28.1 and SWC 1.15.47 when their legal/`"some"` comment retention is enabled. --- .../tests/peephole/dead_code_elimination.rs | 5 +++ crates/oxc_parser/src/lexer/trivia_builder.rs | 31 +++++++++++++------ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/crates/oxc_minifier/tests/peephole/dead_code_elimination.rs b/crates/oxc_minifier/tests/peephole/dead_code_elimination.rs index f23be4e732..07205deaae 100644 --- a/crates/oxc_minifier/tests/peephole/dead_code_elimination.rs +++ b/crates/oxc_minifier/tests/peephole/dead_code_elimination.rs @@ -700,6 +700,11 @@ fn preserve_at_license_legal_comment_when_dce_removes_anchor() { "/* @license */\nconsole.log('val');", CompressOptions::dce(), ); + test_with_options( + "/*x@license*/\nconst foo = 'val';\nconsole.log(foo);", + "/*x@license*/\nconsole.log('val');", + CompressOptions::dce(), + ); } #[test] diff --git a/crates/oxc_parser/src/lexer/trivia_builder.rs b/crates/oxc_parser/src/lexer/trivia_builder.rs index 518ccd0ee5..b7edac8a3e 100644 --- a/crates/oxc_parser/src/lexer/trivia_builder.rs +++ b/crates/oxc_parser/src/lexer/trivia_builder.rs @@ -369,31 +369,34 @@ impl<'a> TriviaBuilder<'a> { } } -#[expect(clippy::inline_always)] #[inline(always)] fn contains_license_or_preserve_comment(s: &str) -> bool { + const LICENSE_LEN: usize = b"@license".len(); + const PRESERVE_LEN: usize = b"@preserve".len(); let hay = s.as_bytes(); - if hay.len() < 9 { + if hay.len() < LICENSE_LEN { return false; } - let search_len = hay.len() - 8; + let search_len = hay.len() - LICENSE_LEN + 1; for i in memchr_iter(b'@', &hay[..search_len]) { debug_assert!(i < search_len); - // SAFETY: we `i` has a max val of len of bytes - 8, so accessing `i + 1` is safe + debug_assert!(hay.len() - i >= LICENSE_LEN); + // SAFETY: `search_len` only includes candidate starts with at least `LICENSE_LEN` bytes + // remaining, so `i + 1` and the full `@license` range are in bounds. match unsafe { hay.get_unchecked(i + 1) } { // spellchecker:off b'l' - // SAFETY: we `i` has a max val of len of bytes - 8, so accessing `i + 7` is safe - if unsafe { hay.get_unchecked(i + 2..i + 1 + 7) } == b"icense" => + // SAFETY: The candidate bound proves the full `@license` range is in bounds. + if unsafe { hay.get_unchecked(i + 2..i + LICENSE_LEN) } == b"icense" => { return true; } - b'p' - // SAFETY: we `i` has a max val of len of bytes - 8, so accessing `i + 8` is safe - if unsafe { hay.get_unchecked(i + 2..i + 1 + 8) } == b"reserve" => + b'p' if hay.len() - i >= PRESERVE_LEN + // SAFETY: The preceding guard proves the full `@preserve` range is in bounds. + && unsafe { hay.get_unchecked(i + 2..i + PRESERVE_LEN) } == b"reserve" => { return true; } @@ -783,10 +786,20 @@ function bar() {}"; ("/* @license */", CommentContent::Legal), ("/* foo @preserve */", CommentContent::Legal), ("/* foo @license */", CommentContent::Legal), + ("/* foo @preserve*/", CommentContent::Legal), + ("/* foo @license*/", CommentContent::Legal), + ("/* foo @licensed*/", CommentContent::Legal), + ("/* foo @License*/", CommentContent::None), + // spellchecker:disable-next-line + ("/* foo @licens*/", CommentContent::None), + // spellchecker:disable-next-line + ("/* foo @preserv*/", CommentContent::None), ("/* @foo @preserve */", CommentContent::Legal), ("/* @foo @license */", CommentContent::Legal), ("/** foo @preserve */", CommentContent::JsdocLegal), ("/** foo @license */", CommentContent::JsdocLegal), + ("/** foo @license*/", CommentContent::JsdocLegal), + ("// foo @license", CommentContent::Legal), ("/** jsdoc */", CommentContent::Jsdoc), ("/**/", CommentContent::None), ("/***/", CommentContent::None),