diff --git a/Cargo.lock b/Cargo.lock index 4acf759dfc2db..c4e937386a2f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2572,6 +2572,7 @@ dependencies = [ "napi-derive", "oxc-miette", "oxc_allocator", + "oxc_ast", "oxc_ast_visit", "oxc_data_structures", "oxc_diagnostics", diff --git a/apps/oxlint/Cargo.toml b/apps/oxlint/Cargo.toml index d645d7740a082..1eb7bc59a8e2d 100644 --- a/apps/oxlint/Cargo.toml +++ b/apps/oxlint/Cargo.toml @@ -28,6 +28,7 @@ doctest = false [dependencies] oxc_allocator = { workspace = true, features = ["fixed_size"] } +oxc_ast = { workspace = true } oxc_ast_visit = { workspace = true, features = ["serialize"] } oxc_data_structures = { workspace = true, features = ["rope"] } oxc_diagnostics = { workspace = true } diff --git a/apps/oxlint/src-js/plugins/comments.ts b/apps/oxlint/src-js/plugins/comments.ts index 7535d4773d98d..c8ea4f1242624 100644 --- a/apps/oxlint/src-js/plugins/comments.ts +++ b/apps/oxlint/src-js/plugins/comments.ts @@ -2,7 +2,7 @@ * Comment class, object pooling, and deserialization. */ -import { ast, buffer, initAst, sourceText } from "./source_code.ts"; +import { buffer, initSourceText, sourceText } from "./source_code.ts"; import { COMMENTS_OFFSET, COMMENTS_LEN_OFFSET, @@ -85,48 +85,34 @@ Object.defineProperty(Comment.prototype, "loc", { enumerable: true }); * Initialize comments for current file. * * Deserializes comments from the buffer using object pooling. - * If the program has a hashbang, prepends a `Shebang` comment. + * If the program has a hashbang, sets first comment to a `Shebang` comment. */ export function initComments(): void { debugAssert(comments === null, "Comments already initialized"); - if (ast === null) initAst(); - debugAssertIsNonNull(ast); + if (sourceText === null) initSourceText(); debugAssertIsNonNull(sourceText); debugAssertIsNonNull(buffer); const { uint32 } = buffer; const programPos32 = uint32[DATA_POINTER_POS_32] >> 2; - let pos = uint32[programPos32 + (COMMENTS_OFFSET >> 2)]; + const commentsPos = uint32[programPos32 + (COMMENTS_OFFSET >> 2)]; const commentsLen = uint32[programPos32 + (COMMENTS_LEN_OFFSET >> 2)]; - // Determine total number of comments (including shebang if present) - const { hashbang } = ast; - let index = +(hashbang !== null); - const totalLen = commentsLen + index; - // Grow cache if needed (one-time cost as cache warms up) - while (cachedComments.length < totalLen) { + while (cachedComments.length < commentsLen) { cachedComments.push(new Comment()); } - // If there's a hashbang, populate slot 0 with `Shebang` comment - if (index !== 0) { - debugAssertIsNonNull(hashbang); - - const comment = cachedComments[0]; - comment.type = "Shebang"; - comment.value = hashbang.value; - comment.range[0] = comment.start = hashbang.start; - comment.range[1] = comment.end = hashbang.end; - } - // Deserialize comments from buffer - while (index < totalLen) { - const comment = cachedComments[index++]; + for (let i = 0; i < commentsLen; i++) { + const comment = cachedComments[i]; + + const pos = commentsPos + i * COMMENT_SIZE, + pos32 = pos >> 2; - const start = uint32[pos >> 2]; - const end = uint32[(pos + 4) >> 2]; + const start = uint32[pos32]; + const end = uint32[pos32 + 1]; const isBlock = buffer[pos + COMMENT_KIND_OFFSET] !== COMMENT_LINE_KIND; comment.type = isBlock ? "Block" : "Line"; @@ -135,8 +121,13 @@ export function initComments(): void { comment.value = sourceText.slice(start + 2, end - (+isBlock << 1)); comment.range[0] = comment.start = start; comment.range[1] = comment.end = end; + } - pos += COMMENT_SIZE; + // Set first comment as `Shebang` if file has hashbang. + // Rust side adds hashbang comment to start of comments `Vec` as a `Line` comment. + if (commentsLen > 0) { + const firstComment = cachedComments[0]; + if (firstComment.start === 0 && sourceText.startsWith("#!")) firstComment.type = "Shebang"; } // Use `slice` rather than copying comments one-by-one into a new array. @@ -145,11 +136,11 @@ export function initComments(): void { // // If the comments array from previous file is longer than the current one, // reuse it and truncate it to avoid the memcpy entirely. - if (previousComments.length >= totalLen) { - previousComments.length = totalLen; + if (previousComments.length >= commentsLen) { + previousComments.length = commentsLen; comments = previousComments; } else { - comments = previousComments = cachedComments.slice(0, totalLen); + comments = previousComments = cachedComments.slice(0, commentsLen); } // Check `comments` have valid ranges and are in ascending order diff --git a/apps/oxlint/src/js_plugins/parse.rs b/apps/oxlint/src/js_plugins/parse.rs index fac522fc1ac68..8578081b6770d 100644 --- a/apps/oxlint/src/js_plugins/parse.rs +++ b/apps/oxlint/src/js_plugins/parse.rs @@ -7,6 +7,7 @@ use napi::bindgen_prelude::Uint8Array; use napi_derive::napi; use oxc_allocator::Allocator; +use oxc_ast::ast::{Comment, CommentKind}; use oxc_ast_visit::utf8_to_utf16::Utf8ToUtf16; use oxc_estree_tokens::{ESTreeTokenOptionsJS, update_tokens}; use oxc_linter::RawTransferMetadata2 as RawTransferMetadata; @@ -205,6 +206,15 @@ unsafe fn parse_raw_impl( program.source_text = source_text; } + // If file has a hashbang, add it to comments. + // It will be converted to a `Shebang` comment on JS side. + if let Some(hashbang) = &program.hashbang { + program.comments.insert( + 0, + Comment::new(hashbang.span.start, hashbang.span.end, CommentKind::Line), + ); + } + // Convert spans to UTF-16. // If source starts with BOM, create converter which ignores the BOM. let span_converter = if has_bom { diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index 1ddfb5972f631..adca9e8426d67 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -14,8 +14,11 @@ use std::{ string::ToString, }; -use oxc_allocator::{Allocator, AllocatorPool, CloneIn, TakeIn}; -use oxc_ast::{ast::Program, ast_kind::AST_TYPE_MAX}; +use oxc_allocator::{Allocator, AllocatorPool, CloneIn, TakeIn, Vec as ArenaVec}; +use oxc_ast::{ + ast::{Comment, CommentKind, Program}, + ast_kind::AST_TYPE_MAX, +}; use oxc_ast_macros::ast; use oxc_ast_visit::utf8_to_utf16::Utf8ToUtf16; use oxc_data_structures::box_macros::boxed_array; @@ -477,6 +480,14 @@ impl Linter { // `allocator` is a fixed-size allocator, so no need to clone AST into a new one let tokens = ctx_host.parser_tokens_mut().take_in(allocator).into_bump_slice_mut(); + // If file has a hashbang, add it to comments. + // It will be converted to a `Shebang` comment on JS side. + if let Some(hashbang) = &program.hashbang { + program + .comments + .insert(0, Comment::new(hashbang.span.start, hashbang.span.end, CommentKind::Line)); + } + self.convert_and_call_external_linter( external_rules, path, @@ -525,6 +536,26 @@ impl Linter { // to be later in the buffer than all other strings in the AST, and the allocator bumps downwards. let new_source_text = js_allocator.alloc_str(original_source_text); + // If file has a hashbang, add it to comments. + // It will be converted to a `Shebang` comment on JS side. + // Clear the original `Vec` to avoid cloning it again below. + let comments = if let Some(hashbang) = &original_program.hashbang { + let mut comments_with_hashbang = + ArenaVec::with_capacity_in(original_program.comments.len() + 1, &js_allocator); + comments_with_hashbang.push(Comment::new( + hashbang.span.start, + hashbang.span.end, + CommentKind::Line, + )); + comments_with_hashbang.extend(original_program.comments.iter().copied()); + + original_program.comments.clear(); + + Some(comments_with_hashbang) + } else { + None + }; + // Clone `Program` into fixed-size allocator. // We need to allocate the `Program` struct ITSELF in the allocator, not just its contents. // `clone_in` returns a value on the stack, but we need it in the allocator for raw transfer. @@ -534,6 +565,11 @@ impl Linter { js_allocator.alloc(program) }; + // If added hashbang comment, set comments to the new `Vec` including hashbang comment + if let Some(comments) = comments { + program.comments = comments; + } + // Clone tokens into fixed-size allocator let tokens = js_allocator.alloc_slice_copy(ctx_host.parser_tokens());