Skip to content

Commit 8df0e41

Browse files
committed
refactor(linter/plugins): add parse function
1 parent 65bd558 commit 8df0e41

File tree

10 files changed

+380
-4
lines changed

10 files changed

+380
-4
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/oxlint/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@ doctest = false
2828

2929
[dependencies]
3030
oxc_allocator = { workspace = true, features = ["fixed_size"] }
31+
oxc_ast_visit = { workspace = true, features = ["serialize"] }
3132
oxc_diagnostics = { workspace = true }
3233
oxc_language_server = { workspace = true, features = ["linter"] }
3334
oxc_linter = { workspace = true }
35+
oxc_parser = { workspace = true }
36+
oxc_semantic = { workspace = true }
3437
oxc_span = { workspace = true }
3538

3639
bpaf = { workspace = true, features = ["autocomplete", "bright-color", "derive"] }

apps/oxlint/src-js/bindings.d.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
/* auto-generated by NAPI-RS */
22
/* eslint-disable */
3+
/**
4+
* Get offset within a `Uint8Array` which is aligned on `BUFFER_ALIGN`.
5+
*
6+
* Does not check that the offset is within bounds of `buffer`.
7+
* To ensure it always is, provide a `Uint8Array` of at least `BUFFER_SIZE + BUFFER_ALIGN` bytes.
8+
*/
9+
export declare function getBufferOffset(buffer: Uint8Array): number
10+
311
/** JS callback to lint a file. */
412
export type JsLintFileCb =
513
((arg0: string, arg1: number, arg2: Uint8Array | undefined | null, arg3: Array<number>, arg4: string) => string)
@@ -19,3 +27,34 @@ export type JsLoadPluginCb =
1927
* Returns `true` if linting succeeded without errors, `false` otherwise.
2028
*/
2129
export declare function lint(args: Array<string>, loadPlugin: JsLoadPluginCb, lintFile: JsLintFileCb): Promise<boolean>
30+
31+
/**
32+
* Parse AST into provided `Uint8Array` buffer, synchronously.
33+
*
34+
* Source text must be written into the start of the buffer, and its length (in UTF-8 bytes)
35+
* provided as `source_len`.
36+
*
37+
* This function will parse the source, and write the AST into the buffer, starting at the end.
38+
*
39+
* It also writes to the very end of the buffer the offset of `Program` within the buffer.
40+
*
41+
* Caller can deserialize data from the buffer on JS side.
42+
*
43+
* # SAFETY
44+
*
45+
* Caller must ensure:
46+
* * Source text is written into start of the buffer.
47+
* * Source text's UTF-8 byte length is `source_len`.
48+
* * The 1st `source_len` bytes of the buffer comprises a valid UTF-8 string.
49+
*
50+
* If source text is originally a JS string on JS side, and converted to a buffer with
51+
* `Buffer.from(str)` or `new TextEncoder().encode(str)`, this guarantees it's valid UTF-8.
52+
*
53+
* # Panics
54+
*
55+
* Panics if source text is too long, or AST takes more memory than is available in the buffer.
56+
*/
57+
export declare function parseRawSync(filename: string, buffer: Uint8Array, sourceLen: number): void
58+
59+
/** Returns `true` if raw transfer is supported on this platform. */
60+
export declare function rawTransferSupported(): boolean

apps/oxlint/src-js/bindings.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,5 +575,8 @@ if (!nativeBinding) {
575575
throw new Error(`Failed to load native binding`)
576576
}
577577

578-
const { lint } = nativeBinding
578+
const { getBufferOffset, lint, parseRawSync, rawTransferSupported } = nativeBinding
579+
export { getBufferOffset }
579580
export { lint }
581+
export { parseRawSync }
582+
export { rawTransferSupported }
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import {
2+
getBufferOffset,
3+
rawTransferSupported as rawTransferSupportedBinding,
4+
parseRawSync,
5+
} from "../bindings.js";
6+
import { debugAssert, debugAssertIsNonNull } from "../utils/asserts.js";
7+
import { buffers } from "../plugins/lint.js";
8+
import { BUFFER_SIZE, BUFFER_ALIGN, DATA_POINTER_POS_32 } from "../generated/constants.js";
9+
10+
import type { BufferWithArrays } from "../plugins/types.js";
11+
12+
// Size array buffer for raw transfer
13+
const ARRAY_BUFFER_SIZE = BUFFER_SIZE + BUFFER_ALIGN;
14+
15+
// 1 GiB
16+
const ONE_GIB = 1 << 30;
17+
18+
// Text encoder for encoding source text into buffer
19+
const textEncoder = new TextEncoder();
20+
21+
// Buffer for raw transfer
22+
let buffer: BufferWithArrays | null = null;
23+
24+
// Whether raw transfer is supported
25+
let rawTransferIsSupported: boolean | null = null;
26+
27+
/**
28+
* Parser source text into buffer.
29+
* @param path - Path of file to parse
30+
* @param sourceText - Source text to parse
31+
* @throws {Error} If raw transfer is not supported on this platform, or parsing failed
32+
*/
33+
export function parse(path: string, sourceText: string) {
34+
if (!rawTransferSupported()) {
35+
throw new Error(
36+
"`RuleTester` is not supported on 32-bit or big-endian systems, versions of NodeJS prior to v22.0.0, " +
37+
"versions of Deno prior to v2.0.0, or other runtimes",
38+
);
39+
}
40+
41+
// Initialize buffer, if not already
42+
if (buffer === null) initBuffer();
43+
debugAssertIsNonNull(buffer);
44+
45+
// Write source into start of buffer.
46+
// `TextEncoder` cannot write into a `Uint8Array` larger than 1 GiB,
47+
// so create a view into buffer of this size to write into.
48+
const sourceBuffer = new Uint8Array(buffer.buffer, buffer.byteOffset, ONE_GIB);
49+
const { read, written: sourceByteLen } = textEncoder.encodeInto(sourceText, sourceBuffer);
50+
if (read !== sourceText.length) throw new Error("Failed to write source text into buffer");
51+
52+
// Parse into buffer
53+
parseRawSync(path, buffer, sourceByteLen);
54+
55+
// Check parsing succeeded.
56+
// 0 is used as sentinel value to indicate parsing failed.
57+
// TODO: Get parsing error details from Rust to display nicely.
58+
const programOffset = buffer.uint32[DATA_POINTER_POS_32];
59+
if (programOffset === 0) throw new Error("Parsing failed");
60+
}
61+
62+
/**
63+
* Create a `Uint8Array` which is 2 GiB in size, with its start aligned on 4 GiB.
64+
*
65+
* Store it in `buffer`, and also in `buffers` array, so it's accessible to `lintFileImpl` by passing `0`as `bufferId`.
66+
*
67+
* Achieve this by creating a 6 GiB `ArrayBuffer`, getting the offset within it that's aligned to 4 GiB,
68+
* chopping off that number of bytes from the start, and shortening to 2 GiB.
69+
*
70+
* It's always possible to obtain a 2 GiB slice aligned on 4 GiB within a 6 GiB buffer,
71+
* no matter how the 6 GiB buffer is aligned.
72+
*
73+
* Note: On systems with virtual memory, this only consumes 6 GiB of *virtual* memory.
74+
* It does not consume physical memory until data is actually written to the `Uint8Array`.
75+
* Physical memory consumed corresponds to the quantity of data actually written.
76+
*/
77+
export function initBuffer() {
78+
// Create buffer
79+
const arrayBuffer = new ArrayBuffer(ARRAY_BUFFER_SIZE);
80+
const offset = getBufferOffset(new Uint8Array(arrayBuffer));
81+
buffer = new Uint8Array(arrayBuffer, offset, BUFFER_SIZE) as BufferWithArrays;
82+
buffer.uint32 = new Uint32Array(arrayBuffer, offset, BUFFER_SIZE / 4);
83+
buffer.float64 = new Float64Array(arrayBuffer, offset, BUFFER_SIZE / 8);
84+
85+
// Store in `buffers`, at index 0
86+
debugAssert(buffers.length === 0);
87+
buffers.push(buffer);
88+
}
89+
90+
/**
91+
* Returns `true` if raw transfer is supported.
92+
*
93+
* Raw transfer is only supported on 64-bit little-endian systems,
94+
* and NodeJS >= v22.0.0 or Deno >= v2.0.0.
95+
*
96+
* Versions of NodeJS prior to v22.0.0 do not support creating an `ArrayBuffer` larger than 4 GiB.
97+
* Bun (as at v1.2.4) also does not support creating an `ArrayBuffer` larger than 4 GiB.
98+
* Support on Deno v1 is unknown and it's EOL, so treating Deno before v2.0.0 as unsupported.
99+
*
100+
* No easy way to determining pointer width (64 bit or 32 bit) in JS,
101+
* so call a function on Rust side to find out.
102+
*
103+
* @returns {boolean} - `true` if raw transfer is supported on this platform
104+
*/
105+
function rawTransferSupported() {
106+
if (rawTransferIsSupported === null) {
107+
rawTransferIsSupported = rawTransferRuntimeSupported() && rawTransferSupportedBinding();
108+
}
109+
return rawTransferIsSupported;
110+
}
111+
112+
declare global {
113+
var Bun: unknown;
114+
var Deno:
115+
| {
116+
version: {
117+
deno: string;
118+
};
119+
}
120+
| undefined;
121+
}
122+
123+
// Checks copied from:
124+
// https://github.com/unjs/std-env/blob/ab15595debec9e9115a9c1d31bc7597a8e71dbfd/src/runtimes.ts
125+
// MIT license: https://github.com/unjs/std-env/blob/ab15595debec9e9115a9c1d31bc7597a8e71dbfd/LICENCE
126+
function rawTransferRuntimeSupported() {
127+
let global;
128+
try {
129+
global = globalThis;
130+
} catch {
131+
return false;
132+
}
133+
134+
const isBun = !!global.Bun || !!global.process?.versions?.bun;
135+
if (isBun) return false;
136+
137+
const isDeno = !!global.Deno;
138+
if (isDeno) {
139+
const match = Deno!.version?.deno?.match(/^(\d+)\./);
140+
return !!match && +match[1] >= 2;
141+
}
142+
143+
const isNode = global.process?.release?.name === "node";
144+
if (!isNode) return false;
145+
146+
const match = process.version?.match(/^v(\d+)\./);
147+
return !!match && +match[1] >= 22;
148+
}

apps/oxlint/src-js/plugins/lint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import type { AfterHook, BufferWithArrays } from "./types.ts";
2929
// All buffers sent from Rust are stored in this array, indexed by `bufferId` (also sent from Rust).
3030
// Buffers are only added to this array, never removed, so no buffers will be garbage collected
3131
// until the process exits.
32-
const buffers: (BufferWithArrays | null)[] = [];
32+
export const buffers: (BufferWithArrays | null)[] = [];
3333

3434
// Array of `after` hooks to run after traversal. This array reused for every file.
3535
const afterHooks: AfterHook[] = [];

apps/oxlint/src/js_plugins/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
mod external_linter;
22
mod raw_fs;
33

4+
#[cfg(all(target_pointer_width = "64", target_endian = "little"))]
5+
pub mod parse;
6+
47
pub use external_linter::create_external_linter;
58
pub use raw_fs::RawTransferFileSystem;

0 commit comments

Comments
 (0)