char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
6534c3e
main
1//! Independent regression probe of the stock native TypeScript API contract, 2//! speaking framed JSON-RPC directly to the backend instead of reusing the 3//! adapter under test. Set AVRIL_TEST_TYPESCRIPT to the backend's absolute 4//! path; the probe is skipped otherwise. 5 6use std:: cell:: RefCell ; 7use std:: collections::{ BTreeMap , HashMap }; 8use std:: io::{ BufRead , BufReader , Write }; 9use std:: path::{ Path , PathBuf }; 10use std:: process::{ Child , ChildStdin , Command , Stdio }; 11use std:: rc:: Rc ; 12use std:: sync:: Mutex ; 13use std:: sync:: mpsc::{ Receiver , channel}; 14use std:: time::{ Duration , Instant }; 15 16use serde_json::{ Value , json}; 17 18fn backend () ->Option < PathBuf > { 19let path = std:: env:: var_os ( "AVRIL_TEST_TYPESCRIPT" ) ?; 20if path. is_empty () { 21return None ; 22} 23Some ( PathBuf :: from ( path)) 24} 25 26fn write_frame ( writer : & mut impl Write , message : & Value ) -> std:: io:: Result <()> { 27let body = serde_json:: to_vec ( message). unwrap (); 28write! ( writer, "Content-Length: {}\r\n\r\n" , body. len ()) ?; 29 writer. write_all ( & body) ?; 30 writer. flush () 31} 32 33fn read_frame ( reader : & mut impl BufRead ) -> std:: io:: Result < Option < Value >> { 34let mut length =None ; 35loop { 36let mut line =String :: new (); 37if reader. read_line ( & mut line) ? ==0 { 38return Ok ( None ); 39} 40let line = line. trim_end_matches ([ '\r' , '\n' ]); 41if line. is_empty () { 42break ; 43} 44if let Some (( name, value)) = line. split_once ( ':' ) 45 && name. trim (). eq_ignore_ascii_case ( "Content-Length" ) 46{ 47 length =Some ( value. trim (). parse ::< usize >(). unwrap ()); 48} 49} 50let mut body =vec! [ 0 ; length. unwrap ()]; 51 reader. read_exact ( & mut body) ?; 52Ok ( Some ( serde_json:: from_slice ( & body). unwrap ())) 53} 54 55# [ derive ( Debug )] 56enum CallError { 57Rpc { code : i64 , message : String }, 58Failed ( String ), 59} 60 61impl std:: fmt:: Display for CallError { 62fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ >) -> std:: fmt:: Result { 63match self { 64CallError :: Rpc { code, message} =>write! ( f, "RPC {code}: {message}" ), 65CallError :: Failed ( message) =>write! ( f, "{message}" ), 66} 67} 68} 69 70type Callback =Box < dyn FnMut ( & Value ) ->Result < Value , String >>; 71 72struct ApiServer { 73child : Child , 74stdin : Option < Mutex < ChildStdin >>, 75messages : Receiver < Value >, 76callbacks : HashMap < String , Callback >, 77next_id : u64 , 78failed : Option < String >, 79} 80 81impl ApiServer { 82fn start ( binary : & Path , callbacks : & [ & str ]) ->ApiServer { 83let mut child =Command :: new ( binary) 84. env_clear () 85. args ([ 86"--api" , 87"--async" , 88"--cwd" , 89"/avril-lsp" , 90"--callbacks" , 91& callbacks. join ( "," ), 92]) 93. stdin ( Stdio :: piped ()) 94. stdout ( Stdio :: piped ()) 95. stderr ( Stdio :: inherit ()) 96. spawn () 97. unwrap (); 98let stdin =Some ( Mutex :: new ( child. stdin . take (). unwrap ())); 99let mut stdout =BufReader :: new ( child. stdout . take (). unwrap ()); 100let ( tx, messages) =channel (); 101 std:: thread:: spawn ( move ||{ 102while let Ok ( Some ( message)) =read_frame ( & mut stdout) { 103if tx. send ( message). is_err () { 104return ; 105} 106} 107}); 108ApiServer { 109 child, 110 stdin, 111 messages, 112callbacks : HashMap :: new (), 113next_id : 0 , 114failed : None , 115} 116} 117 118fn send ( & self , message : & Value ) { 119write_frame ( & mut * self . stdin . as_ref (). unwrap (). lock (). unwrap (), message). unwrap (); 120} 121 122fn call ( & mut self , method : & str , params : Value ) ->Result < Value , CallError > { 123if let Some ( error) =& self . failed { 124return Err ( CallError :: Failed ( error. clone ())); 125} 126self . next_id +=1 ; 127let id =self . next_id ; 128self . send ( & json! ({ "id" : id, "method" : method, "params" : params})); 129let deadline =Instant :: now () +Duration :: from_secs ( 30 ); 130loop { 131let remaining = deadline. saturating_duration_since ( Instant :: now ()); 132if remaining. is_zero () { 133self . failed =Some ( format! ( "Timed out waiting for {method}" )); 134return Err ( CallError :: Failed ( self . failed . clone (). unwrap ())); 135} 136match self . messages . recv_timeout ( remaining) { 137Ok ( message) =>{ 138if let Some ( callback_method) = message[ "method" ]. as_str () { 139// Callbacks may arrive while a request is in flight; 140// answering them inline keeps the reader progressing. 141let callback_id = message[ "id" ]. clone (); 142let response =match self . callbacks . get_mut ( callback_method) { 143Some ( callback) =>match callback ( & message[ "params" ]) { 144Ok ( result) =>json! ({ "id" : callback_id, "result" : result}), 145Err ( error) =>json! ({ 146"id" : callback_id, 147"error" : { "code" : -32603 , "message" : error} 148}), 149}, 150None =>json! ({ 151"id" : callback_id, 152"error" : { 153"code" : -32603 , 154"message" : format! ( "Unknown callback: {callback_method}" ) 155} 156}), 157}; 158self . send ( & response); 159} else if message[ "id" ]. as_u64 () ==Some ( id) { 160if let Some ( error) = message. get ( "error" ) { 161return Err ( CallError :: Rpc { 162code : error[ "code" ]. as_i64 (). unwrap_or_default (), 163message : error[ "message" ]. as_str (). unwrap_or_default (). to_owned (), 164}); 165} 166return Ok ( message[ "result" ]. clone ()); 167} 168} 169Err ( _) =>{ 170self . failed =Some ( "API server closed stdout" . into ()); 171return Err ( CallError :: Failed ( self . failed . clone (). unwrap ())); 172} 173} 174} 175} 176 177fn close ( mut self ) { 178drop ( self . stdin . take ()); 179let deadline =Instant :: now () +Duration :: from_secs ( 2 ); 180loop { 181match self . child . try_wait (). unwrap () { 182Some ( status) =>{ 183assert! ( status. success (), "API server exited with {status}" ); 184return ; 185} 186None if Instant :: now () >= deadline =>{ 187self . child . kill (). unwrap (); 188self . child . wait (). unwrap (); 189panic! ( "API server did not exit" ); 190} 191None => std:: thread:: sleep ( Duration :: from_millis ( 10 )), 192} 193} 194} 195} 196 197impl Drop for ApiServer { 198fn drop ( & mut self ) { 199let _ =self . child . kill (); 200let _ =self . child . wait (); 201} 202} 203 204fn version ( binary : & Path ) ->String { 205let output =Command :: new ( binary). arg ( "--version" ). output (). unwrap (); 206assert! ( output. status . success (), "Cannot run backend" ); 207String :: from_utf8 ( output. stdout ). unwrap (). trim (). to_owned () 208} 209 210# [ test ] 211fn capabilities () { 212let Some ( binary) =backend () else { 213eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 214return ; 215}; 216println! ( "{}" , version ( & binary)); 217let mut api =ApiServer :: start ( & binary, & []); 218println! ( "initialize: {}" , api. call ( "initialize" , json! ({})). unwrap ()); 219match api. call ( "createModuleResolver" , json! ({ "compilerOptions" : {}})) { 220Ok ( resolver) =>{ 221 api. call ( "releaseModuleResolver" , json! ({ "resolver" : resolver})) 222. unwrap (); 223println! ( "Custom module-resolution API: available" ); 224} 225Err ( CallError :: Rpc { message, ..}) =>{ 226assert! ( 227 message. contains ( "unknown API method \"createModuleResolver\"" ), 228"Unexpected capability failure: {message}" 229); 230println! ( "Custom module-resolution API: unavailable" ); 231} 232Err ( error) =>panic! ( "Unexpected capability failure: {error}" ), 233} 234 api. close (); 235} 236 237struct Fixture { 238original_to_file : BTreeMap < String , String >, 239directory_to_original : BTreeMap < String , String >, 240files : BTreeMap < String , String >, 241} 242 243impl Fixture { 244fn add ( & mut self , original : & str , source : & str ) ->String { 245let extension =if original. ends_with ( ".d.ts" ) { 246".d.ts" 247} else if original. ends_with ( ".tsx" ) { 248".tsx" 249} else { 250".ts" 251}; 252let directory =format! ( "/avril-lsp/modules/{}" , self . original_to_file . len ()); 253let file =format! ( "{directory}/source{extension}" ); 254self . original_to_file 255. insert ( original. to_owned (), file. clone ()); 256self . directory_to_original 257. insert ( directory, original. to_owned ()); 258self . files . insert ( file. clone (), source. to_owned ()); 259 file 260} 261} 262 263fn utf16_index ( line : & str , byte_index : usize ) ->usize { 264 line[ ..byte_index]. encode_utf16 (). count () 265} 266 267fn layer ( files : Vec <( String , String )>) ->Value { 268let mut map = serde_json:: Map :: new (); 269for ( name, text) in files{ 270 map. insert ( name, json! ( text)); 271} 272Value :: Object ( map) 273} 274 275# [ test ] 276fn native_api_contract () { 277let Some ( binary) =backend () else { 278eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 279return ; 280}; 281println! ( "{}" , version ( & binary)); 282let lib_directory = binary. parent (). unwrap (). to_path_buf (); 283let fs_callbacks =[ 284"readFile" , 285"fileExists" , 286"directoryExists" , 287"getAccessibleEntries" , 288]; 289let mut callback_names: Vec < String > = fs_callbacks. iter (). map ( ToString :: to_string). collect (); 290 callback_names. extend ( 291[ 292"realpath:identity" , 293"stat:fakeStat" , 294"writeFile:error" , 295"removeFile:error" , 296] 297. iter () 298. map ( ToString :: to_string), 299); 300let callback_refs: Vec < & str > = callback_names. iter (). map ( String :: as_str). collect (); 301let mut api =ApiServer :: start ( & binary, & callback_refs); 302 303let fs_calls =Rc :: new ( RefCell :: new ( Vec ::<( String , String , bool )>:: new ())); 304for methodin fs_callbacks{ 305let fs_calls = fs_calls. clone (); 306let lib_directory = lib_directory. clone (); 307 api. callbacks . insert ( 308 method. to_owned (), 309Box :: new ( move |params|{ 310let path = params 311. as_str () 312. ok_or_else ( ||format! ( "Unexpected filesystem callback: {method}" )) ?; 313let library_name = path 314. strip_prefix ( & format! ( "{}/" , lib_directory. display ())) 315. filter ( |name|{ 316 name. starts_with ( "lib." ) 317 && name. ends_with ( ".d.ts" ) 318 && name 319. bytes () 320. all ( |b| b. is_ascii_alphanumeric () ||matches! ( b, b'.' |b'-' )) 321}); 322let allowed = library_name. is_some () 323 ||Path :: new ( path) == lib_directory 324 || path. starts_with ( "bundled:///libs/" ) 325 || path =="bundled:///libs" ; 326 fs_calls 327. borrow_mut () 328. push (( method. to_owned (), path. to_owned (), allowed)); 329if allowed{ 330return Ok ( json! ({ "kind" : "useOS" })); 331} 332if method =="readFile" { 333return Ok ( json! ({ "kind" : "missing" })); 334} 335Ok ( json! ({ 336"kind" : "value" , 337"value" : if method =="getAccessibleEntries" { 338json! ({ "files" : [], "directories" : []}) 339} else { 340json! ( false ) 341} 342})) 343}), 344); 345} 346 347let mut snapshots: Vec < u64 > =Vec :: new (); 348let mut resolvers: Vec < u64 > =Vec :: new (); 349let mut passed: Vec < String > =Vec :: new (); 350let pass = |passed : & mut Vec < String >, message : & str |{ 351 passed. push ( message. to_owned ()); 352println! ( "PASS {message}" ); 353}; 354 355println! ( "initialize: {}" , api. call ( "initialize" , json! ({})). unwrap ()); 356let raw_options =json! ({ 357"strict" : true , "noEmit" : true , "skipLibCheck" : true , "target" : "esnext" , 358"module" : "esnext" , "moduleResolution" : "bundler" , 359"allowImportingTsExtensions" : true , "jsx" : "react-jsx" , 360"jsxImportSource" : "@fixture/ui" , "types" : [] 361}); 362let mut fixture =Fixture { 363original_to_file : BTreeMap :: new (), 364directory_to_original : BTreeMap :: new (), 365files : BTreeMap :: new (), 366}; 367let main_uri ="file:///workspace/app/main.tsx" ; 368let main_source =[ 369"import { process } from \"avril:std\";" , 370"import { answer } from \"jsr:@fixture/data\";" , 371"import { label } from \"./local.ts\";" , 372"const emoji = \"😀\"; const wrong: number = \"bad\";" , 373"const view = <panel count=\"wrong\" />;" , 374"process.args.push(\"x\");" , 375"export const result = { answer, label, view };" , 376] 377. join ( "\n" ); 378let main = fixture. add ( main_uri, & main_source); 379let a_uri ="file:///workspace/app/a.ts" ; 380let b_uri ="file:///workspace/app/b.ts" ; 381let a = fixture. add ( 382 a_uri, 383"import { value } from \"scoped\"; export const result: \"A\" = value;" , 384); 385let b = fixture. add ( 386 b_uri, 387"import { value } from \"scoped\"; export const result: \"B\" = value;" , 388); 389let completion_source ="import { process } from \"avril:std\"; process.args;" ; 390let completion = fixture. add ( "file:///workspace/app/completion.ts" , completion_source); 391 fixture. add ( 392"file:///workspace/deps/a.ts" , 393"export const value = \"A\" as const;" , 394); 395 fixture. add ( 396"file:///workspace/deps/b.ts" , 397"export const value = \"B\" as const;" , 398); 399 fixture. add ( 400"file:///workspace/app/local.ts" , 401"export const label = \"local\";" , 402); 403 fixture. add ( 404"https://jsr.io/@fixture/data/1.0.0/mod.d.ts" , 405"export declare const answer: 42;" , 406); 407 fixture. add ( 408"file:///workspace/ui/jsx-runtime.d.ts" , 409"\nexport namespace JSX {\n interface Element {}\n interface IntrinsicElements { panel: { count: number } }\n}\nexport declare function jsx(...args: unknown[]): JSX.Element;\nexport declare const jsxs: typeof jsx;\nexport declare const Fragment: unique symbol;\n" , 410); 411 412// The production catalog includes both handwritten and generated declarations. 413let mut roots =vec! [ main. clone (), a. clone (), b. clone (), completion. clone ()]; 414for ( name, text) in avril_runtime:: stdlib:: declarations () { 415 roots. push ( fixture. add ( & format! ( "avril:///lib/{name}" ), text)); 416} 417 418let fixture =Rc :: new ( RefCell :: new ( fixture)); 419let calls =Rc :: new ( RefCell :: new ( Vec ::< Value >:: new ())); 420let parse_options = |api : & mut ApiServer , compiler_options : & Value , roots : & [ String ] |{ 421// The wire protocol wants normalized enum values, not tsconfig spellings. 422let parsed = api 423. call ( 424"parseJsonConfigFileContent" , 425json! ({ 426"json" : { "compilerOptions" : compiler_options, "files" : roots}, 427"configDirectory" : "/avril-lsp" 428}), 429) 430. unwrap (); 431assert! ( 432 parsed[ "errors" ]. as_array (). unwrap (). is_empty (), 433"Invalid compiler options: {}" , 434 parsed[ "errors" ] 435); 436 parsed[ "options" ]. clone () 437}; 438let options =parse_options ( & mut api, & raw_options, & roots); 439 440// Resolution policy is pinned to the resolver, including for older snapshots. 441let register_resolver = |api : & mut ApiServer , 442resolvers : & mut Vec < u64 >, 443calls : & Rc < RefCell < Vec < Value >>>, 444fixture : & Rc < RefCell < Fixture >>, 445compiler_options : & Value , 446swap_scopes : bool | 447 ->u64 { 448let callback =format! ( "avril/resolve/{}" , resolvers. len ()); 449let calls = calls. clone (); 450let fixture = fixture. clone (); 451 api. callbacks . insert ( 452 callback. clone (), 453Box :: new ( move |params|{ 454 calls. borrow_mut (). push ( params. clone ()); 455let fixture = fixture. borrow (); 456let original = fixture 457. directory_to_original 458. get ( params[ "containingDirectory" ]. as_str (). unwrap ()); 459let resolved =match params[ "moduleName" ]. as_str (). unwrap () { 460"scoped" =>{ 461let ( dep_a, dep_b) =( 462& fixture. original_to_file [ "file:///workspace/deps/a.ts" ], 463& fixture. original_to_file [ "file:///workspace/deps/b.ts" ], 464); 465match ( original. map ( String :: as_str), swap_scopes) { 466( Some ( "file:///workspace/app/a.ts" ), false ) =>Some ( dep_a), 467( Some ( "file:///workspace/app/a.ts" ), true ) =>Some ( dep_b), 468( Some ( "file:///workspace/app/b.ts" ), false ) =>Some ( dep_b), 469( Some ( "file:///workspace/app/b.ts" ), true ) =>Some ( dep_a), 470 _ =>None , 471} 472} 473"jsr:@fixture/data" =>Some ( 474& fixture. original_to_file [ "https://jsr.io/@fixture/data/1.0.0/mod.d.ts" ], 475), 476"@fixture/ui/jsx-runtime" =>{ 477Some ( & fixture. original_to_file [ "file:///workspace/ui/jsx-runtime.d.ts" ]) 478} 479"./local.ts" 480if original. map ( String :: as_str) 481 ==Some ( "file:///workspace/app/main.tsx" ) => 482{ 483Some ( & fixture. original_to_file [ "file:///workspace/app/local.ts" ]) 484} 485 _ =>None , 486}; 487Ok ( match resolved{ 488Some ( file) =>json! ({ "resolvedFileName" : file}), 489None =>Value :: Null , 490}) 491}), 492); 493let id = api 494. call ( 495"createModuleResolver" , 496json! ({ 497"compilerOptions" : compiler_options, 498"resolveModuleNameCallback" : callback 499}), 500) 501. unwrap () 502. as_u64 () 503. unwrap (); 504 resolvers. push ( id); 505 id 506}; 507 508let ( main_f, a_f, b_f) =( main. clone (), a. clone (), b. clone ()); 509let files_snapshot = fixture. borrow (). files . clone (); 510let mut resolver_id = 511register_resolver ( & mut api, & mut resolvers, & calls, & fixture, & options, false ); 512let _ = resolver_id; 513let mut snapshot = api 514. call ( 515"createSnapshot" , 516json! ({ 517"fileSystem" : { "kind" : "layer" , "files" : files_snapshot}, 518"createPrograms" : [{ 519"rootFiles" : roots. clone (), "compilerOptions" : options, 520"options" : { "moduleResolver" : resolver_id} 521}] 522}), 523) 524. unwrap (); 525 snapshots. push ( snapshot[ "snapshot" ]. as_u64 (). unwrap ()); 526let project = snapshot[ "operation" ][ "createdPrograms" ][ 0 ] 527. as_str () 528. unwrap () 529. to_owned (); 530assert! ( 531 snapshot[ "projects" ][ 0 ][ "parsedCommandLine" ][ "errors" ] 532. as_array () 533. unwrap () 534. is_empty (), 535"Invalid compiler options" 536); 537let initial = snapshot. clone (); 538 539// Helper closures over the API. 540macro_rules ! diagnostics{ 541( $snapshot: expr, $selected: expr) =>{ 542 api. call ( 543"getSemanticDiagnostics" , 544json! ({ 545"snapshot" : $snapshot[ "snapshot" ], "project" : project, 546"files" : $selected 547}), 548) 549. unwrap () 550}; 551} 552 553let first =diagnostics! ( snapshot, [ main_f. clone (), a_f. clone (), b_f. clone ()]); 554let first = first. as_array (). unwrap (); 555assert_eq! ( first. len (), 3 , "Expected exactly three intentional errors" ); 556assert_eq! ( 557 first. iter (). filter ( |d| d[ "code" ] ==json! ( 2322 )). count (), 5582 , 559"Missing assignment/JSX diagnostics" 560); 561assert! ( 562 first. iter (). any ( |d| d[ "code" ] ==json! ( 2339 )), 563"Actual avril declarations did not enforce readonly process.args" 564); 565assert! ( 566 first. iter (). all ( |d| d[ "fileName" ] ==json! ( main_f)), 567"Scoped resolution or declarations failed" 568); 569pass ( 570& mut passed, 571"unsaved TSX + actual avril declarations + local/JSR-shaped fixture imports" , 572); 573{ 574let calls = calls. borrow (); 575let fixture = fixture. borrow (); 576assert! ( 577 calls 578. iter () 579. any ( |c| c[ "moduleName" ] ==json! ( "@fixture/ui/jsx-runtime" )), 580"JSX implicit import bypassed resolver" 581); 582for uriin [ a_uri, b_uri] { 583assert! ( 584 calls. iter (). any ( |c|{ 585 fixture 586. directory_to_original 587. get ( c[ "containingDirectory" ]. as_str (). unwrap ()) 588. map ( String :: as_str) 589 ==Some ( uri) 590 && c[ "moduleName" ] ==json! ( "scoped" ) 591}), 592"No {uri} callback" 593); 594} 595} 596pass ( 597& mut passed, 598"per-file scopes via compiler-only directories; original source unmodified" , 599); 600let unicode = first 601. iter () 602. find ( |d| d[ "startPosition" ][ "line" ] ==json! ( 3 )) 603. unwrap (); 604let line = main_source. lines (). nth ( 3 ). unwrap (); 605assert_eq! ( 606 unicode[ "startPosition" ][ "character" ]. as_u64 (). unwrap () as usize , 607utf16_index ( line, line. find ( "wrong" ). unwrap ()), 608"Diagnostic position is not UTF-16" 609); 610pass ( 611& mut passed, 612"diagnostic UTF-16 positions after an astral character" , 613); 614 615let completions = api 616. call ( 617"getCompletionsAtPosition" , 618json! ({ 619"snapshot" : snapshot[ "snapshot" ], "project" : project, 620"file" : fixture. borrow (). original_to_file [ "file:///workspace/app/completion.ts" ], 621"position" : completion_source. rfind ( "process." ). unwrap () +"process." . len () 622}), 623) 624. unwrap (); 625let names: Vec < & str > = completions[ "entries" ] 626. as_array () 627. unwrap () 628. iter () 629. map ( |entry| entry[ "name" ]. as_str (). unwrap ()) 630. collect (); 631assert! ( names. contains ( & "args" ), "Missing process.args completion" ); 632assert! ( names. contains ( & "cwd" ), "Missing process.cwd completion" ); 633pass ( & mut passed, "member completion from real avril types" ); 634 635let position =utf16_index ( & main_source, main_source. rfind ( "answer" ). unwrap ()); 636let ty = api 637. call ( 638"getTypeAtPosition" , 639json! ({ 640"snapshot" : snapshot[ "snapshot" ], "project" : project, 641"file" : main_f, "position" : position 642}), 643) 644. unwrap (); 645let type_text = api 646. call ( 647"typeToString" , 648json! ({ 649"snapshot" : snapshot[ "snapshot" ], "project" : project, "type" : ty[ "id" ] 650}), 651) 652. unwrap (); 653assert_eq! ( type_text, json! ( "42" ), "Unexpected type text: {type_text}" ); 654let symbol = api 655. call ( 656"getSymbolAtPosition" , 657json! ({ 658"snapshot" : snapshot[ "snapshot" ], "project" : project, 659"file" : main_f, "position" : position 660}), 661) 662. unwrap (); 663assert! ( 664 !symbol[ "declarations" ]. as_array (). unwrap (). is_empty (), 665"Missing symbol declaration handles" 666); 667pass ( 668& mut passed, 669"hover building blocks: type text and symbol/declaration handles" , 670); 671 672let alias = api 673. call ( 674"getSymbolAtPosition" , 675json! ({ 676"snapshot" : snapshot[ "snapshot" ], "project" : project, 677"file" : main_f, "position" : main_source. find ( "answer" ). unwrap () 678}), 679) 680. unwrap (); 681let target = api 682. call ( 683"getAliasedSymbol" , 684json! ({ 685"snapshot" : snapshot[ "snapshot" ], "project" : project, 686"symbol" : alias[ "reference" ] 687}), 688) 689. unwrap (); 690let remote_file = 691 fixture. borrow (). original_to_file [ "https://jsr.io/@fixture/data/1.0.0/mod.d.ts" ]. clone (); 692assert! ( 693 target[ "declarations" ] 694. as_array () 695. unwrap () 696. iter () 697. any ( |handle| handle 698. as_str () 699. unwrap () 700. ends_with ( & format! ( ".{remote_file}" ))), 701"Import alias did not lead to remote declaration" 702); 703let local_references = api 704. call ( 705"getReferencesToSymbolInFile" , 706json! ({ 707"snapshot" : snapshot[ "snapshot" ], "project" : project, 708"file" : main_f, "symbol" : alias[ "reference" ] 709}), 710) 711. unwrap (); 712let local_references = local_references. as_array (). unwrap (); 713assert! ( 714 local_references. len () >=2 , 715"Missing import/use reference handles" 716); 717let references = api 718. call ( 719"getReferencedSymbolsForNode" , 720json! ({ 721"snapshot" : snapshot[ "snapshot" ], "project" : project, 722"node" : local_references[ 0 ], 723"position" : main_source. find ( "answer" ). unwrap () 724}), 725) 726. unwrap (); 727let mut handles: Vec < String > =Vec :: new (); 728for entryin references. as_array (). unwrap () { 729 handles. push ( entry[ "definition" ]. as_str (). unwrap (). to_owned ()); 730 handles. extend ( 731 entry[ "references" ] 732. as_array () 733. unwrap () 734. iter () 735. map ( |h| h. as_str (). unwrap (). to_owned ()), 736); 737} 738let handle_original = |handle : & str | ->String { 739let mut parts = handle. splitn ( 3 , '.' ); 740 parts. next (). unwrap (); 741 parts. next (). unwrap (); 742let file = parts. next (). unwrap (); 743 fixture 744. borrow () 745. original_to_file 746. iter () 747. find ( |( _, candidate) |* candidate == file) 748. map ( |( original, _) | original. clone ()) 749. unwrap_or_else ( ||panic! ( "Unknown compiler file: {file}" )) 750}; 751let reference_originals: Vec < String > = handles. iter (). map ( |h|handle_original ( h)). collect (); 752assert! ( reference_originals. iter (). any ( |u| u == main_uri)); 753assert! ( 754 reference_originals 755. iter () 756. any ( |u| u =="https://jsr.io/@fixture/data/1.0.0/mod.d.ts" ) 757); 758pass ( 759& mut passed, 760"cross-file definition/reference handles map back to original module URIs" , 761); 762 763let fixed = main_source 764. replace ( "number = \"bad\"" , "number = 1" ) 765. replace ( "count=\"wrong\"" , "count={1}" ) 766. replace ( "process.args.push(\"x\")" , "process.args.includes(\"x\")" ); 767 snapshot = api 768. call ( 769"updateSnapshot" , 770json! ({ 771"snapshot" : snapshot[ "snapshot" ], 772"changes" : { 773"fileSystem" : { "kind" : "layer" , "files" : layer( vec! [( main_f. clone (), fixed)])}, 774"ensurePrograms" : [ project] 775} 776}), 777) 778. unwrap (); 779 snapshots. push ( snapshot[ "snapshot" ]. as_u64 (). unwrap ()); 780assert_eq! ( 781diagnostics! ( snapshot, [ main_f. clone (), a_f. clone (), b_f. clone ()]) 782. as_array () 783. unwrap () 784. len (), 7850 , 786"Unsaved fixes did not clear diagnostics" 787); 788assert_eq! ( 789diagnostics! ( initial, [ main_f. clone (), a_f. clone (), b_f. clone ()]) 790. as_array () 791. unwrap () 792. len (), 7933 , 794"Old snapshot changed after edit" 795); 796pass ( 797& mut passed, 798"incremental unsaved edits; old snapshot remains queryable and unchanged" , 799); 800 801let b_edited =format! ( 802"{}\nimport {{ result as bypass }} from \"{}\"; void bypass;" , 803 fixture. borrow (). files [ & b_f], 804 a_f 805); 806let denied = api 807. call ( 808"updateSnapshot" , 809json! ({ 810"snapshot" : snapshot[ "snapshot" ], 811"changes" : { 812"fileSystem" : { "kind" : "layer" , "files" : layer( vec! [( b_f. clone (), b_edited)])}, 813"ensurePrograms" : [ project] 814} 815}), 816) 817. unwrap (); 818 snapshots. push ( denied[ "snapshot" ]. as_u64 (). unwrap ()); 819let denied_diagnostics =diagnostics! ( denied, [ main_f. clone (), a_f. clone (), b_f. clone ()]); 820let denied_diagnostics = denied_diagnostics. as_array (). unwrap (); 821assert_eq! ( 822 denied_diagnostics. len (), 8231 , 824"Null resolver result fell back to an existing compiler file" 825); 826assert_eq! ( denied_diagnostics[ 0 ][ "code" ], json! ( 2307 )); 827assert_eq! ( 828diagnostics! ( snapshot, [ main_f. clone (), a_f. clone (), b_f. clone ()]) 829. as_array () 830. unwrap () 831. len (), 8320 , 833"Branch snapshot modified its parent" 834); 835pass ( 836& mut passed, 837"unmapped imports stay unresolved even when the target exists in the virtual filesystem" , 838); 839 840 resolver_id =register_resolver ( & mut api, & mut resolvers, & calls, & fixture, & options, true ); 841 snapshot = api 842. call ( 843"updateSnapshot" , 844json! ({ 845"snapshot" : snapshot[ "snapshot" ], 846"changes" : { 847"reconfigurePrograms" : [{ 848"id" : project, "rootFiles" : roots, "compilerOptions" : options, 849"options" : { "moduleResolver" : resolver_id} 850}] 851} 852}), 853) 854. unwrap (); 855 snapshots. push ( snapshot[ "snapshot" ]. as_u64 (). unwrap ()); 856let remapped =diagnostics! ( snapshot, [ main_f. clone (), a_f. clone (), b_f. clone ()]); 857let remapped = remapped. as_array (). unwrap (); 858assert_eq! ( 859 remapped. len (), 8602 , 861"Resolver replacement did not invalidate scoped imports" 862); 863assert! ( 864 remapped. iter (). all ( |d|{ 865 d[ "code" ] ==json! ( 2322 ) &&( d[ "fileName" ] ==json! ( a_f) || d[ "fileName" ] ==json! ( b_f)) 866}), 867"Resolver replacement did not invalidate scoped imports" 868); 869assert_eq! ( 870diagnostics! ( initial, [ main_f. clone (), a_f. clone (), b_f. clone ()]) 871. as_array () 872. unwrap () 873. len (), 8743 , 875"Resolver replacement affected the old snapshot" 876); 877pass ( 878& mut passed, 879"import-map change: new resolver + reconfiguration invalidates cached resolutions" , 880); 881 882let raw_options_dev =json! ({ 883"strict" : true , "noEmit" : true , "skipLibCheck" : true , "target" : "esnext" , 884"module" : "esnext" , "moduleResolution" : "bundler" , 885"allowImportingTsExtensions" : true , "jsx" : "react-jsxdev" , 886"jsxImportSource" : "@fixture/dev-ui" , "types" : [] 887}); 888let development_options =parse_options ( & mut api, & raw_options_dev, & roots); 889 resolver_id =register_resolver ( 890& mut api, 891& mut resolvers, 892& calls, 893& fixture, 894& development_options, 895true , 896); 897 snapshot = api 898. call ( 899"updateSnapshot" , 900json! ({ 901"snapshot" : snapshot[ "snapshot" ], 902"changes" : { 903"reconfigurePrograms" : [{ 904"id" : project, "rootFiles" : roots, 905"compilerOptions" : development_options, 906"options" : { "moduleResolver" : resolver_id} 907}] 908} 909}), 910) 911. unwrap (); 912 snapshots. push ( snapshot[ "snapshot" ]. as_u64 (). unwrap ()); 913let jsx_diagnostics =diagnostics! ( snapshot, [ main_f. clone (), a_f. clone (), b_f. clone ()]); 914{ 915let calls = calls. borrow (); 916assert! ( 917 calls 918. iter () 919. any ( |c| c[ "moduleName" ] ==json! ( "@fixture/dev-ui/jsx-dev-runtime" )), 920"JSX settings did not update implicit resolution" 921); 922} 923assert! ( 924 jsx_diagnostics. as_array (). unwrap (). iter (). any ( |d|{ 925 d[ "fileName" ] ==json! ( main_f) 926 && d[ "text" ]. as_str (). unwrap (). contains ( "jsx-dev-runtime" ) 927}), 928"Missing runtime was not diagnosed" 929); 930pass ( 931& mut passed, 932"JSX configuration change re-resolves the generated runtime import" , 933); 934 935// Negative control: same-directory filenames lose per-file scope identity. 936let raw_calls =Rc :: new ( RefCell :: new ( Vec ::< Value >:: new ())); 937{ 938let raw_calls = raw_calls. clone (); 939let dep_a = fixture. borrow (). original_to_file [ "file:///workspace/deps/a.ts" ]. clone (); 940 api. callbacks . insert ( 941"avril/directory-control" . to_owned (), 942Box :: new ( move |params|{ 943if params[ "moduleName" ] !=json! ( "scoped" ) { 944return Ok ( Value :: Null ); 945} 946 raw_calls. borrow_mut (). push ( params. clone ()); 947Ok ( json! ({ "resolvedFileName" : dep_a})) 948}), 949); 950} 951let control_resolver = api 952. call ( 953"createModuleResolver" , 954json! ({ 955"compilerOptions" : options, 956"resolveModuleNameCallback" : "avril/directory-control" 957}), 958) 959. unwrap () 960. as_u64 () 961. unwrap (); 962 resolvers. push ( control_resolver); 963let raw_a ="/avril-lsp/unmapped/a.ts" ; 964let raw_b ="/avril-lsp/unmapped/b.ts" ; 965let ( a_source, b_source, dep_a_source) ={ 966let fixture = fixture. borrow (); 967( 968 fixture. files [ & a_f]. clone (), 969 fixture. files [ & b_f]. clone (), 970 fixture. files [ & fixture. original_to_file [ "file:///workspace/deps/a.ts" ]]. clone (), 971) 972}; 973let dep_a_file = fixture. borrow (). original_to_file [ "file:///workspace/deps/a.ts" ]. clone (); 974let control = api 975. call ( 976"createSnapshot" , 977json! ({ 978"fileSystem" : { "kind" : "layer" , "files" : layer ( vec! [ 979( raw_a. to_owned (), a_source), 980( raw_b. to_owned (), b_source), 981( dep_a_file. clone (), dep_a_source), 982])}, 983"createPrograms" : [{ 984"rootFiles" : [ raw_a, raw_b], "compilerOptions" : options, 985"options" : { "moduleResolver" : control_resolver} 986}] 987}), 988) 989. unwrap (); 990 snapshots. push ( control[ "snapshot" ]. as_u64 (). unwrap ()); 991let control_diagnostics = api 992. call ( 993"getSemanticDiagnostics" , 994json! ({ 995"snapshot" : control[ "snapshot" ], 996"project" : control[ "operation" ][ "createdPrograms" ][ 0 ] 997}), 998) 999. unwrap (); 1000let control_diagnostics = control_diagnostics. as_array (). unwrap (); 1001assert_eq! ( 1002 control_diagnostics. len (), 10031 , 1004"Directory-only control did not reproduce the scope collision" 1005); 1006assert_eq! ( control_diagnostics[ 0 ][ "code" ], json! ( 2322 )); 1007assert_eq! ( control_diagnostics[ 0 ][ "fileName" ], json! ( raw_b)); 1008{ 1009let raw_calls = raw_calls. borrow (); 1010assert! ( !raw_calls. is_empty ()); 1011assert! ( 1012 raw_calls. iter (). all ( |call|{ 1013 call[ "containingDirectory" ] ==json! ( "/avril-lsp/unmapped" ) 1014 && call. get ( "containingFile" ). is_none () 1015}), 1016"Callback now exposes a full referrer; reassess virtual directories" 1017); 1018} 1019pass ( 1020& mut passed, 1021"negative control: original same-directory filenames lose per-file scope identity" , 1022); 1023{ 1024let fs_calls = fs_calls. borrow (); 1025assert! ( 1026 fs_calls 1027. iter () 1028. any ( |( method, _, allowed) | method =="readFile" &&* allowed), 1029"No standard library reads observed" 1030); 1031assert! ( 1032 fs_calls. iter (). any ( |( _, _, allowed) | !allowed), 1033"No out-of-library probes observed" 1034); 1035} 1036pass ( 1037& mut passed, 1038"filesystem callbacks serve standard libraries and deny other host-file probes" , 1039); 1040 1041for methodin [ 1042"getQuickInfoAtPosition" , 1043"getDefinitionAtPosition" , 1044"findRenameLocations" , 1045"textDocument/hover" , 1046"textDocument/definition" , 1047"textDocument/rename" , 1048] { 1049match api. call ( 1050 method, 1051json! ({ 1052"snapshot" : snapshot[ "snapshot" ], "project" : project, 1053"file" : main_f, "position" : position 1054}), 1055) { 1056Err ( CallError :: Rpc { message, ..}) =>{ 1057assert! ( 1058 message. contains ( "unknown API method" ), 1059"Unexpected failure probing {method}: {message}" 1060); 1061println! ( "GAP {method}: not exposed by the API server" ); 1062} 1063 other =>panic! ( "Unexpectedly supported {method}: {other:?}" ), 1064} 1065} 1066pass ( 1067& mut passed, 1068"feature-gap probes distinguish absent methods from project/type errors" , 1069); 1070println! ( 1071"\n{} checks passed; {} resolver callbacks, {} retained snapshots." , 1072 passed. len (), 1073 calls. borrow (). len (), 1074 snapshots. len () 1075); 1076 1077for snapshot_idin snapshots. iter (). rev () { 1078 api. call ( "release" , json! ({ "snapshot" : snapshot_id})) 1079. unwrap (); 1080} 1081for resolverin & resolvers{ 1082 api. call ( "releaseModuleResolver" , json! ({ "resolver" : resolver})) 1083. unwrap (); 1084} 1085 api. close (); 1086}