mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
fix(ast_visit): add the trimmed prefix length to translation table offsets (#26173)
Closes #26162. `Utf8ToUtf16::new_with_offset` seeds the translation table with an absolute BOM entry, `{ utf8_offset: 3, utf16_difference: 3 }`, but `build_translations` records later entries relative to the trimmed source text. From the first non-ASCII character on, the table is 3 bytes short and offsets land on the wrong side of a boundary. For the issue repro, a JS plugin reading `context.sourceCode.getAllComments()[0]` gets `0..4`, so `text.slice(start, end)` is `"/*x*"` instead of `"/*x*/"`. Parsing the BOM'd file and converting comment spans the way `parse.rs` does gives `comment span = 0..4`, `"/*x*"` before the change and `comment span = 0..5`, `"/*x*/"` after. Add `offset` back on when pushing the entry so the table stays absolute. This is one line in `build_translations`. `Utf8ToUtf16::new` passes `offset: 0`, so the parser, coverage, and benchmarks are unchanged. The four non-zero-offset callers are in the linter's JS-plugin path: `crates/oxc_linter/src/lib.rs:752` (BOM) and `:754` (leading newline of a Vue/Astro/Svelte fragment), `apps/oxlint/src/js_plugins/parse.rs:249` and `fix.rs:34`. Each trims the prefix from `source_text` before constructing this type. I left the other two spots the reporter flagged alone because they look like separate bugs. Both new tests fail on `db66f58` before the change and pass after: - `translate_offsets_after_trimmed_bom`: comment end `8` converts to `4`, expected `5`. - `translate_offsets_with_trimmed_prefix`: sweeps every byte offset of three sources against the same source with nothing trimmed, for a 1-byte and a 3-byte prefix. One source puts the non-ASCII characters in the SIMD aligned-chunk path rather than the short-source path. Ran on `db66f58`, macOS arm64, rustc 1.98.0: - `cargo test -p oxc_ast_visit --lib --features serialize`: 5 passed, 0 failed (3 passed, 2 failed without the fix) - `cargo test -p oxc_linter`: 1256 passed, 0 failed - `cargo clippy -p oxc_ast_visit --all-features --all-targets -- -D warnings`: clean - `cargo fmt --all --check`: clean Per the AI usage policy: I used an AI assistant on this. The diagnosis, the tests and the numbers above are all mine and I ran every command quoted here.
This commit is contained in:
1 parent
6365db964b
commit
f07dca52c4
2 files changed
+61
-2
No files matched your search
@@ -157,6 +157,8 @@ mod test {
|
||||
|
||||
use super::Utf8ToUtf16;
|
||||
|
||||
const BOM_LEN: u32 = 3;
|
||||
|
||||
#[test]
|
||||
fn translate_ast() {
|
||||
let allocator = Allocator::new();
|
||||
@@ -205,6 +207,61 @@ mod test {
|
||||
assert_eq!(convert_back(11), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translate_offsets_after_trimmed_bom() {
|
||||
// Source text has the BOM trimmed off the start, but spans are still relative to the file
|
||||
// including the BOM, so the offsets in the table are too.
|
||||
let table = Utf8ToUtf16::new_with_offset("/*x*/'é'", BOM_LEN);
|
||||
let mut converter = table.converter().unwrap();
|
||||
|
||||
// (UTF-8 offset including BOM, UTF-16 offset excluding BOM)
|
||||
let offsets = [(3u32, 0u32), (8, 5), (9, 6), (11, 7), (12, 8)];
|
||||
for (utf8_offset, expected_utf16_offset) in offsets {
|
||||
let mut offset = utf8_offset;
|
||||
converter.convert_offset(&mut offset);
|
||||
assert_eq!(offset, expected_utf16_offset);
|
||||
}
|
||||
|
||||
for (expected_utf8_offset, utf16_offset) in offsets {
|
||||
let mut offset = utf16_offset;
|
||||
converter.convert_offset_back(&mut offset);
|
||||
assert_eq!(offset, expected_utf8_offset);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translate_offsets_with_trimmed_prefix() {
|
||||
// Linter trims a BOM, or the leading newline of a Vue/Astro/Svelte fragment, off the source
|
||||
// text before building the table. Neither is part of the UTF-16 offsets, so an offset in
|
||||
// the trimmed source has to convert the same way it would with nothing trimmed.
|
||||
let sources = [
|
||||
"/*x*/'é'",
|
||||
"/*x*/'🤨'",
|
||||
// Long enough that the non-ASCII characters land in the aligned-chunk path
|
||||
"/*xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx*/'£ऊ🤨'",
|
||||
];
|
||||
|
||||
for source_text in sources {
|
||||
for trimmed_len in [1u32, BOM_LEN] {
|
||||
let trimmed_table = Utf8ToUtf16::new_with_offset(source_text, trimmed_len);
|
||||
let mut trimmed = trimmed_table.converter().unwrap();
|
||||
let whole_table = Utf8ToUtf16::new(source_text);
|
||||
let mut whole = whole_table.converter().unwrap();
|
||||
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
for utf8_offset in 0..=(source_text.len() as u32) {
|
||||
let mut expected = utf8_offset;
|
||||
whole.convert_offset(&mut expected);
|
||||
|
||||
let mut actual = utf8_offset + trimmed_len;
|
||||
trimmed.convert_offset(&mut actual);
|
||||
|
||||
assert_eq!(actual, expected, "{source_text:?} at offset {utf8_offset}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translate_offsets() {
|
||||
assert_eq!('_'.len_utf8(), 1);
|
||||
|
||||
@@ -90,11 +90,13 @@ pub fn build_translations(source_text: &str, translations: &mut Vec<Translation>
|
||||
|
||||
// Record the index of the end of this Unicode character, because it's only offsets
|
||||
// *after* this Unicode character that need to be shifted.
|
||||
// Addition cannot overflow because length of source text is max `u32::MAX`.
|
||||
// Offsets are relative to the start of the untrimmed source, so `offset` is added
|
||||
// back on. Addition cannot overflow because the untrimmed source text is max
|
||||
// `u32::MAX` bytes long.
|
||||
let bytes_in_char =
|
||||
difference_for_this_byte as usize + usize::from(byte >= 0xF0) + 1;
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
let utf8_offset = (start_offset + index + bytes_in_char) as u32;
|
||||
let utf8_offset = (start_offset + index + bytes_in_char) as u32 + offset;
|
||||
translations.push(Translation { utf8_offset, utf16_difference });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user