@@ -7,7 +7,7 @@ use std::{
77 rc:: Rc ,
88} ;
99
10- use oxc_allocator:: Allocator ;
10+ use oxc_allocator:: { Allocator , AllocatorPool , CloneIn } ;
1111use oxc_ast:: { ast:: Program , ast_kind:: AST_TYPE_MAX } ;
1212use oxc_ast_macros:: ast;
1313use oxc_ast_visit:: utf8_to_utf16:: Utf8ToUtf16 ;
@@ -148,18 +148,24 @@ impl Linter {
148148 context_sub_hosts : Vec < ContextSubHost < ' a > > ,
149149 allocator : & ' a Allocator ,
150150 ) -> Vec < Message > {
151- self . run_with_disable_directives ( path, context_sub_hosts, allocator) . 0
151+ self . run_with_disable_directives ( path, context_sub_hosts, allocator, None ) . 0
152152 }
153153
154154 /// Same as `run` but also returns the disable directives for the file
155155 ///
156+ /// # Parameters
157+ /// - `js_allocator_pool`: Optional pool of fixed-size allocators for copying AST before JS transfer.
158+ /// When `Some`, the AST will be copied into a fixed-size allocator before passing to JS plugins,
159+ /// allowing the main allocator to be a standard (non-fixed-size) allocator.
160+ ///
156161 /// # Panics
157162 /// Panics in debug mode if running with and without optimizations produces different diagnostic counts.
158163 pub fn run_with_disable_directives < ' a > (
159164 & self ,
160165 path : & Path ,
161166 context_sub_hosts : Vec < ContextSubHost < ' a > > ,
162167 allocator : & ' a Allocator ,
168+ js_allocator_pool : Option < & AllocatorPool > ,
163169 ) -> ( Vec < Message > , Option < DisableDirectives > ) {
164170 let ResolvedLinterState { rules, config, external_rules } = self . config . resolve ( path) ;
165171
@@ -350,7 +356,13 @@ impl Linter {
350356 // can mutably access `ctx_host` via `Rc::get_mut` without panicking due to multiple references.
351357 drop ( rules) ;
352358
353- self . run_external_rules ( & external_rules, path, & mut ctx_host, allocator) ;
359+ self . run_external_rules (
360+ & external_rules,
361+ path,
362+ & mut ctx_host,
363+ allocator,
364+ js_allocator_pool,
365+ ) ;
354366
355367 // Report unused directives is now handled differently with type-aware linting
356368
@@ -382,65 +394,157 @@ impl Linter {
382394 ( diagnostics, disable_directives)
383395 }
384396
397+ #[ cfg( all( target_pointer_width = "64" , target_endian = "little" ) ) ]
385398 fn run_external_rules < ' a > (
386399 & self ,
387400 external_rules : & [ ( ExternalRuleId , ExternalOptionsId , AllowWarnDeny ) ] ,
388401 path : & Path ,
389402 ctx_host : & mut Rc < ContextHost < ' a > > ,
390403 allocator : & ' a Allocator ,
404+ js_allocator_pool : Option < & AllocatorPool > ,
391405 ) {
392406 if external_rules. is_empty ( ) {
393407 return ;
394408 }
395409
396- // `external_linter` always exists when `external_rules` is not empty
397- let external_linter = self . external_linter . as_ref ( ) . unwrap ( ) ;
410+ // If `js_allocator_pool` is provided, use clone-into-fixed-allocator approach
411+ if let Some ( js_pool) = js_allocator_pool {
412+ self . clone_into_fixed_size_allocator_and_run_external_rules (
413+ external_rules,
414+ path,
415+ ctx_host,
416+ js_pool,
417+ ) ;
418+ return ;
419+ }
420+
421+ // Extract `Semantic` from `ContextHost`, and get a mutable reference to `Program`.
422+ //
423+ // It's not possible to obtain a `&mut Program` while `Semantic` exists, because `Semantic`
424+ // contains `AstNodes`, which contains `AstKind`s for every AST nodes, each of which contains
425+ // an immutable `&` ref to an AST node.
426+ // Obtaining a `&mut Program` while `Semantic` exists would be illegal aliasing.
427+ //
428+ // So instead we get a pointer to `Program`.
429+ // The pointer is obtained initially from `&Program` in `Semantic`, but that pointer
430+ // has no provenance for mutation, so can't be converted to `&mut Program`.
431+ // So create a new pointer to `Program` which inherits `data_end_ptr`'s provenance,
432+ // which does allow mutation.
433+ //
434+ // We then drop `Semantic`, after which no references to any AST nodes remain.
435+ // We can then safety convert the pointer to `&mut Program`.
436+ //
437+ // `Program` was created in `allocator`, and that allocator is a `FixedSizeAllocator`,
438+ // so only has 1 chunk. So `data_end_ptr` and `Program` are within the same allocation.
439+ // All callers of `Linter::run` obtain `allocator` and `Semantic` from `ModuleContent`,
440+ // which ensure they are in same allocation.
441+ // However, we have no static guarantee of this, so strictly speaking it's unsound.
442+ // TODO: It would be better to avoid the need for a `&mut Program` here, and so avoid this
443+ // sketchy behavior.
444+ let ctx_host = Rc :: get_mut ( ctx_host) . unwrap ( ) ;
445+ let semantic = mem:: take ( ctx_host. semantic_mut ( ) ) ;
446+ let program_addr = NonNull :: from ( semantic. nodes ( ) . program ( ) ) . addr ( ) ;
447+ let mut program_ptr =
448+ allocator. data_end_ptr ( ) . cast :: < Program < ' a > > ( ) . with_addr ( program_addr) ;
449+ drop ( semantic) ;
450+ // SAFETY: Now that we've dropped `Semantic`, no references to any AST nodes remain,
451+ // so can get a mutable reference to `Program` without aliasing violations.
452+ let program = unsafe { program_ptr. as_mut ( ) } ;
453+
454+ self . convert_and_call_external_linter ( external_rules, path, ctx_host, allocator, program) ;
455+ }
456+
457+ #[ cfg( not( all( target_pointer_width = "64" , target_endian = "little" ) ) ) ]
458+ fn run_external_rules < ' a > (
459+ & self ,
460+ _external_rules : & [ ( ExternalRuleId , ExternalOptionsId , AllowWarnDeny ) ] ,
461+ _path : & Path ,
462+ _ctx_host : & mut Rc < ContextHost < ' a > > ,
463+ _allocator : & ' a Allocator ,
464+ _js_allocator_pool : Option < & AllocatorPool > ,
465+ ) {
466+ // External rules (JS plugins) are not supported on non-64-bit or big-endian platforms
467+ }
398468
399- let ( program_offset, source_text, span_converter) = {
400- // Extract `Semantic` from `ContextHost`, and get a mutable reference to `Program`.
401- //
402- // It's not possible to obtain a `&mut Program` while `Semantic` exists, because `Semantic`
403- // contains `AstNodes`, which contains `AstKind`s for every AST nodes, each of which contains
404- // an immutable `&` ref to an AST node.
405- // Obtaining a `&mut Program` while `Semantic` exists would be illegal aliasing.
406- //
407- // So instead we get a pointer to `Program`.
408- // The pointer is obtained initially from `&Program` in `Semantic`, but that pointer
409- // has no provenance for mutation, so can't be converted to `&mut Program`.
410- // So create a new pointer to `Program` which inherits `data_end_ptr`'s provenance,
411- // which does allow mutation.
412- //
413- // We then drop `Semantic`, after which no references to any AST nodes remain.
414- // We can then safety convert the pointer to `&mut Program`.
415- //
416- // `Program` was created in `allocator`, and that allocator is a `FixedSizeAllocator`,
417- // so only has 1 chunk. So `data_end_ptr` and `Program` are within the same allocation.
418- // All callers of `Linter::run` obtain `allocator` and `Semantic` from `ModuleContent`,
419- // which ensure they are in same allocation.
420- // However, we have no static guarantee of this, so strictly speaking it's unsound.
421- // TODO: It would be better to avoid the need for a `&mut Program` here, and so avoid this
422- // sketchy behavior.
423- let ctx_host = Rc :: get_mut ( ctx_host) . unwrap ( ) ;
424- let semantic = mem:: take ( ctx_host. semantic_mut ( ) ) ;
425- let program_addr = NonNull :: from ( semantic. nodes ( ) . program ( ) ) . addr ( ) ;
426- let mut program_ptr =
427- allocator. data_end_ptr ( ) . cast :: < Program < ' a > > ( ) . with_addr ( program_addr) ;
428- drop ( semantic) ;
429- // SAFETY: Now that we've dropped `Semantic`, no references to any AST nodes remain,
430- // so can get a mutable reference to `Program` without aliasing violations.
431- let program = unsafe { program_ptr. as_mut ( ) } ;
432-
433- // Convert spans to UTF-16
434- let span_converter = Utf8ToUtf16 :: new ( program. source_text ) ;
435- span_converter. convert_program ( program) ;
436- span_converter. convert_comments ( & mut program. comments ) ;
437-
438- // Get offset of `Program` within buffer (bottom 32 bits of pointer)
439- let program_offset = ptr:: from_ref ( program) as u32 ;
440-
441- ( program_offset, program. source_text , span_converter)
469+ /// Clone AST into a fixed-size allocator and run external rules.
470+ ///
471+ /// This copies the AST and source text from the standard allocator into a fixed-size
472+ /// allocator before passing to JS plugins. This allows using standard allocators for
473+ /// parsing/linting while still supporting JS plugin raw transfer.
474+ #[ cfg( all( target_pointer_width = "64" , target_endian = "little" ) ) ]
475+ fn clone_into_fixed_size_allocator_and_run_external_rules (
476+ & self ,
477+ external_rules : & [ ( ExternalRuleId , ExternalOptionsId , AllowWarnDeny ) ] ,
478+ path : & Path ,
479+ ctx_host : & mut Rc < ContextHost < ' _ > > ,
480+ js_allocator_pool : & AllocatorPool ,
481+ ) {
482+ let js_allocator = js_allocator_pool. get ( ) ;
483+
484+ // Get the original source text and program from semantic
485+ let ctx_host_ref = Rc :: get_mut ( ctx_host) . unwrap ( ) ;
486+ let semantic = mem:: take ( ctx_host_ref. semantic_mut ( ) ) ;
487+ let original_program = semantic. nodes ( ) . program ( ) ;
488+ let original_source_text = original_program. source_text ;
489+
490+ // Copy source text to the START of the fixed-size allocator.
491+ // This is critical - the JS deserializer expects source text at offset 0.
492+ // SAFETY: `js_allocator` is from a fixed-size allocator pool, which wraps the allocator
493+ // in a custom `Drop` that doesn't actually drop it (it returns it to the pool), so the
494+ // memory remains valid. This matches the safety requirements of `alloc_bytes_start`.
495+ let new_source_text: & str = unsafe {
496+ let bytes = original_source_text. as_bytes ( ) ;
497+ let ptr = js_allocator. alloc_bytes_start ( bytes. len ( ) ) ;
498+ ptr:: copy_nonoverlapping ( bytes. as_ptr ( ) , ptr. as_ptr ( ) , bytes. len ( ) ) ;
499+ std:: str:: from_utf8_unchecked ( std:: slice:: from_raw_parts ( ptr. as_ptr ( ) , bytes. len ( ) ) )
442500 } ;
443501
502+ // Clone `Program` into fixed-size allocator.
503+ // We need to allocate the `Program` struct ITSELF in the allocator, not just its contents.
504+ // `clone_in` returns a value on the stack, but we need it in the allocator for raw transfer.
505+ let program: & mut Program < ' _ > = {
506+ let mut program = original_program. clone_in ( & js_allocator) ;
507+ program. source_text = new_source_text;
508+ js_allocator. alloc ( program)
509+ } ;
510+
511+ // Drop semantic now that we've copied what we need
512+ drop ( semantic) ;
513+
514+ self . convert_and_call_external_linter (
515+ external_rules,
516+ path,
517+ ctx_host_ref,
518+ & js_allocator,
519+ program,
520+ ) ;
521+
522+ // The `AllocatorGuard` (`js_allocator`) is dropped here, returning the allocator to the pool.
523+ // This ensures that we never have too many allocators in play at once, avoiding OOM.
524+ }
525+
526+ /// Convert spans to UTF-16, write metadata, call external linter, and process diagnostics.
527+ ///
528+ /// This is the common code path shared by both `run_external_rules` and
529+ /// `clone_into_fixed_size_allocator_and_run_external_rules`.
530+ fn convert_and_call_external_linter (
531+ & self ,
532+ external_rules : & [ ( ExternalRuleId , ExternalOptionsId , AllowWarnDeny ) ] ,
533+ path : & Path ,
534+ ctx_host : & ContextHost < ' _ > ,
535+ allocator : & Allocator ,
536+ program : & mut Program < ' _ > ,
537+ ) {
538+ let source_text = program. source_text ;
539+
540+ // Convert spans to UTF-16
541+ let span_converter = Utf8ToUtf16 :: new ( source_text) ;
542+ span_converter. convert_program ( program) ;
543+ span_converter. convert_comments ( & mut program. comments ) ;
544+
545+ // Get offset of `Program` within buffer (bottom 32 bits of pointer)
546+ let program_offset = ptr:: from_ref ( program) as u32 ;
547+
444548 // Write offset of `Program` in metadata at end of buffer
445549 let metadata = RawTransferMetadata :: new ( program_offset) ;
446550 let metadata_ptr = allocator. end_ptr ( ) . cast :: < RawTransferMetadata > ( ) ;
@@ -470,6 +574,9 @@ impl Linter {
470574 "{}" . to_string ( )
471575 } ) ;
472576
577+ // `external_linter` always exists when `external_rules` is not empty
578+ let external_linter = self . external_linter . as_ref ( ) . unwrap ( ) ;
579+
473580 // Pass AST and rule IDs + options IDs to JS
474581 let result = ( external_linter. lint_file ) (
475582 path. to_owned ( ) ,
0 commit comments