Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/oxlint/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
51 changes: 21 additions & 30 deletions apps/oxlint/src-js/plugins/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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";
Expand All @@ -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.
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions apps/oxlint/src/js_plugins/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
40 changes: 38 additions & 2 deletions crates/oxc_linter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Comment>` 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.
Expand All @@ -534,6 +565,11 @@ impl Linter {
js_allocator.alloc(program)
};

// If added hashbang comment, set comments to the new `Vec<Comment>` 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());

Expand Down
Loading