char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
c7a9569
main
1# [ path = "common/http.rs" ] 2# [ allow ( dead_code )] 3mod http; 4# [ path = "common/lsp.rs" ] 5# [ allow ( dead_code )] 6mod support; 7 8use std:: fs; 9use std:: process:: Command ; 10 11use serde_json:: json; 12use sha2:: Digest ; 13use support::{ Server , avril, backend, files}; 14 15fn hover_contains ( 16server : & mut Server , 17path : & str , 18text : & str , 19symbol : & str , 20expected : & str , 21) ->bool { 22 server. hover ( path, text, symbol). contains ( expected) 23} 24 25# [ test ] 26fn live_avril_resolution_unsaved_graphs_and_disk_config_invalidation () { 27let Some ( backend) =backend () else { 28eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 29return ; 30}; 31let dir = tempfile:: tempdir (). unwrap (); 32let root = dir. path (); 33let main ="import {value} from \"alias\"; import {process} from \"avril:std\"; const checked: number = value; value; process.args; throw Error(\"must not execute\");" ; 34let mut config =json! ({ 35"imports" : { "alias" : "./dep.ts" }, 36"scopes" : { 37"./a.ts" : { "scoped" : "./one.ts" }, 38"./b.ts" : { "scoped" : "./two.ts" } 39} 40}); 41files ( 42 root, 43& [ 44( "avril.conf" , & config. to_string ()), 45( "main.ts" , main), 46( "dep.ts" , "export const value = 42 as const;" ), 47( "other.ts" , "export const value = 99 as const;" ), 48( "a.ts" , "import {value} from \"scoped\"; value;" ), 49( "b.ts" , "import {value} from \"scoped\"; value;" ), 50( "one.ts" , "export const value = 1 as const;" ), 51( "two.ts" , "export const value = 2 as const;" ), 52( 53"types.js" , 54"// @ts-self-types=\"./decl.d.ts\"\nexport const declared = \"wrong\";" , 55), 56( "decl.d.ts" , "export declare const declared: 123;" ), 57( 58"lib/avril.conf" , 59"name:'@demo/lib', exports:{'.':'./mod.ts'}, imports:{alias:'./private.ts'}" , 60), 61( "lib/mod.ts" , "export {value} from \"alias\";" ), 62( "lib/private.ts" , "export const value = 17 as const;" ), 63], 64); 65let mut server =Server :: start ( root, & backend, & [ "--offline" ]); 66 server. initialize (); 67 server. open ( "main.ts" , main); 68 69// Local aliases and embedded builtins. 70assert! ( hover_contains ( 71& mut server, 72"main.ts" , 73 main, 74"value" , 75"value: 42" 76)); 77let links = server. definitions ( "main.ts" , main, "value" ); 78assert_eq! ( links[ 0 ][ "targetUri" ], json! ( server. uri ( "dep.ts" ))); 79let links = server. definitions ( "main.ts" , main, "args" ); 80assert! ( 81 links[ 0 ][ "targetUri" ] 82. as_str () 83. unwrap () 84. starts_with ( "avril-builtin:" ) 85); 86 87// Unsaved dependency edits and version rejection. 88 server. open ( "dep.ts" , "export const value = \"edited\" as const;" ); 89assert! ( hover_contains ( 90& mut server, 91"main.ts" , 92 main, 93"value" , 94"value: \"edited\"" 95)); 96assert! ( 97 server 98. diagnostics ( "main.ts" ) 99. iter () 100. any ( |d| d[ "code" ] ==json! ( 2322 )) 101); 102 server. change ( "dep.ts" , "export const value = 0;" , 0 ); 103assert! ( hover_contains ( 104& mut server, 105"main.ts" , 106 main, 107"value" , 108"value: \"edited\"" 109)); 110 server. notify ( 111"textDocument/didClose" , 112json! ({ "textDocument" : { "uri" : server. uri ( "dep.ts" )}}), 113); 114assert! ( hover_contains ( 115& mut server, 116"main.ts" , 117 main, 118"value" , 119"value: 42" 120)); 121 122// New unsaved imports grow the graph and follow declaration redirects. 123let text ="import {declared} from \"./types.js\"; declared;" ; 124 server. change ( "main.ts" , text, 2 ); 125assert! ( hover_contains ( 126& mut server, 127"main.ts" , 128 text, 129"declared" , 130"declared: 123" 131)); 132let links = server. definitions ( "main.ts" , text, "declared" ); 133assert_eq! ( links[ 0 ][ "targetUri" ], json! ( server. uri ( "decl.d.ts" ))); 134 server. change ( "main.ts" , main, 3 ); 135 136// Plain files retain avril module boundaries and top-level await. 137for valuein [ 1 , 2 ] { 138let path =format! ( "plain{value}.ts" ); 139let text =format! ( "const value = {value}; await Promise.resolve(value); value;" ); 140files ( root, & [( & path, & text)]); 141 server. open ( & path, & text); 142assert! ( hover_contains ( 143& mut server, 144& path, 145& text, 146"value" , 147& format! ( "value: {value}" ) 148)); 149assert! ( server. diagnostics ( & path). is_empty ()); 150} 151 152// Exact per-referrer scopes. 153for ( path, value) in [( "a.ts" , 1 ), ( "b.ts" , 2 )] { 154let text = fs:: read_to_string ( root. join ( path)). unwrap (); 155 server. open ( path, & text); 156assert! ( hover_contains ( 157& mut server, 158 path, 159& text, 160"value" , 161& format! ( "value: {value}" ) 162)); 163} 164 165// On-disk dependency and configuration changes need no snapshot regeneration. 166 fs:: write ( root. join ( "dep.ts" ), "export const value = 43 as const;" ). unwrap (); 167 server. eventually ( |s|hover_contains ( s, "main.ts" , main, "value" , "value: 43" )); 168 config[ "imports" ][ "alias" ] =json! ( "./other.ts" ); 169 fs:: write ( root. join ( "avril.conf" ), config. to_string ()). unwrap (); 170 server. eventually ( |s|hover_contains ( s, "main.ts" , main, "value" , "value: 99" )); 171 172// Local package exports and private maps. 173 fs:: write ( 174 root. join ( "avril.conf" ), 175"deps:[{from:'./lib',name:'library'}]" , 176) 177. unwrap (); 178let text ="import {value} from \"library\"; value;" ; 179 server. change ( "main.ts" , text, 4 ); 180assert! ( hover_contains ( 181& mut server, 182"main.ts" , 183 text, 184"value" , 185"value: 17" 186)); 187let links = server. definitions ( "main.ts" , text, "value" ); 188assert_eq! ( links[ 0 ][ "targetUri" ], json! ( server. uri ( "lib/private.ts" ))); 189 190// Bad configuration fails closed and then recovers. 191 fs:: write ( root. join ( "avril.conf" ), "imports: {" ). unwrap (); 192 server. notify ( "workspace/didChangeWatchedFiles" , json! ({ "changes" : []})); 193let text ="import {value} from \"alias\"; value;" ; 194 server. change ( "main.ts" , text, 5 ); 195let links = server. definitions ( "main.ts" , text, "value" ); 196assert_eq! ( links. len (), 1 , "{links:?}" ); 197assert_eq! ( links[ 0 ][ "targetUri" ], server. uri ( "main.ts" )); 198assert_eq! ( 199 support:: selected ( text, & links[ 0 ][ "targetSelectionRange" ]), 200"value" 201); 202assert! ( 203 server 204. diagnostics ( "main.ts" ) 205. iter () 206. any ( |d| d[ "message" ]. as_str (). unwrap (). contains ( "avril.conf" )) 207); 208 fs:: write ( root. join ( "avril.conf" ), config. to_string ()). unwrap (); 209 server. eventually ( |s|hover_contains ( s, "main.ts" , text, "value" , "value: 99" )); 210assert! ( 211 !root. join ( ".avril" ). exists (), 212"No editor snapshot directory" 213); 214assert! ( 215 !root. join ( "avril.lock" ). exists (), 216"Editor must not write lockfiles" 217); 218assert! ( 219 !root. join ( "tsconfig.json" ). exists (), 220"Editor must not create tsconfig files" 221); 222 server. close (); 223} 224 225# [ test ] 226fn ignore_files_allow_startup_and_live_rule_changes () { 227let Some ( backend) =backend () else { 228eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 229return ; 230}; 231let dir = tempfile:: tempdir (). unwrap (); 232let root = dir. path (); 233let main ="export const value = 42;" ; 234files ( 235 root, 236& [ 237( "main.ts" , main), 238( ".gitignore" , "git-ignored.ts\n" ), 239( ".ignore" , "ignored.ts\n" ), 240( "git-ignored.ts" , "export const gitIgnoredValue = 1;" ), 241( "ignored.ts" , "export const ignoredValue = 2;" ), 242], 243); 244let mut server =Server :: start ( root, & backend, & [ "--offline" ]); 245 server. initialize (); 246 server. open ( "main.ts" , main); 247for ignorein [ ".gitignore" , ".ignore" ] { 248 fs:: read_to_string ( root. join ( ignore)). unwrap (); 249} 250assert! ( server. hover ( "main.ts" , main, "value" ). contains ( "value: 42" )); 251let symbols = server 252. call ( "workspace/symbol" , json! ({ "query" : "IgnoredValue" })) 253. unwrap (); 254assert_eq! ( symbols, json! ([])); 255 256for ( ignore, symbol) in [ 257( ".gitignore" , "gitIgnoredValue" ), 258( ".ignore" , "ignoredValue" ), 259] { 260 fs:: write ( root. join ( ignore), "\n" ). unwrap (); 261 server. eventually ( |s|{ 262let symbols = s 263. call ( "workspace/symbol" , json! ({ "query" : symbol})) 264. unwrap (); 265 symbols 266. as_array () 267. unwrap () 268. iter () 269. any ( |s| s[ "name" ] == symbol) 270}); 271} 272 server. close (); 273} 274 275# [ test ] 276fn live_project_enforces_js_exec_authority_for_overlaid_documents () { 277let Some ( backend) =backend () else { 278eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 279return ; 280}; 281let dir = tempfile:: tempdir (). unwrap (); 282let root = dir. path (); 283files ( root, & [( "main.ts" , "export const value = 42;" )]); 284let mut server =Server :: start ( root, & backend, & [ "-0" , "--offline" ]); 285 server. initialize (); 286let text ="export const value = 99; value;" ; 287 server. open ( "main.ts" , text); 288let error = server 289. call ( 290"textDocument/hover" , 291json! ({ 292"textDocument" :{ "uri" : server. uri ( "main.ts" )}, 293"position" : support:: position_at ( text, text. rfind ( "value" ). unwrap ()), 294}), 295) 296. unwrap_err (); 297assert_eq! ( error. code , -32602 , "{error:?}" ); 298assert! ( 299 server 300. diagnostics ( "main.ts" ) 301. iter () 302. any ( |d| d[ "message" ]. as_str (). unwrap (). contains ( "PermissionDenied" )) 303); 304 server. close (); 305} 306 307# [ test ] 308fn live_jsx_programs_honor_scoped_configuration_and_external_extends () { 309let Some ( backend) =backend () else { 310eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 311return ; 312}; 313let dir = tempfile:: tempdir (). unwrap (); 314let root = dir. path (); 315let external = tempfile:: tempdir (). unwrap (); 316let jsx ="export namespace JSX { interface Element {} interface IntrinsicElements { panel: {count:number} } } export declare function jsx(...args:unknown[]): JSX.Element; export declare const jsxs:typeof jsx;" ; 317let main ="export const view = <panel count={1}/>; view;" ; 318files ( 319 root, 320& [ 321( "avril.conf" , "imports:{'ui/jsx-runtime':'./runtime.d.ts'}" ), 322( 323"tsconfig.json" , 324& json! ({ "extends" : external. path (). join ( "base.json" ). to_str (). unwrap ()}) 325. to_string (), 326), 327( "main.tsx" , main), 328( "runtime.d.ts" , jsx), 329( 330"classic/tsconfig.json" , 331& json! ({ "compilerOptions" : { "jsx" : "react" , "jsxFactory" : "h" }}). to_string (), 332), 333( 334"classic/main.tsx" , 335"import {view as imported} from \"../main.tsx\"; declare function h(...args:unknown[]):unknown; const view = <panel/>; view; imported;" , 336), 337], 338); 339let base = |source : & str |{ 340 fs:: write ( 341 external. path (). join ( "base.json" ), 342json! ({ "compilerOptions" : { "jsx" : "react-jsx" , "jsxImportSource" : source}}). to_string (), 343) 344. unwrap (); 345}; 346base ( "ui" ); 347let mut server =Server :: start ( root, & backend, & [ "--offline" ]); 348 server. initialize (); 349 server. open ( "main.tsx" , main); 350 server. hover ( "main.tsx" , main, "view" ); 351assert! ( 352 !server 353. diagnostics ( "main.tsx" ) 354. iter () 355. any ( |d| d[ "code" ] ==json! ( 2875 ) || d[ "code" ] ==json! ( 2307 )) 356); 357let classic = fs:: read_to_string ( root. join ( "classic/main.tsx" )). unwrap (); 358 server. open ( "classic/main.tsx" , & classic); 359assert! ( server. hover ( "main.tsx" , main, "view" ). contains ( "Element" )); 360 server. hover ( "classic/main.tsx" , & classic, "view" ); 361let links = server. definitions ( "classic/main.tsx" , & classic, "imported" ); 362assert_eq! ( links[ 0 ][ "targetUri" ], json! ( server. uri ( "main.tsx" ))); 363assert! ( !server. diagnostics ( "classic/main.tsx" ). iter (). any ( |d|{ 364[ 2875 , 2307 , 2874 ] 365. iter () 366. any ( |code| d[ "code" ] ==json! ( code)) 367})); 368base ( "missing-ui" ); 369 server. eventually ( |s|{ 370 s. hover ( "main.tsx" , main, "view" ); 371 s. diagnostics ( "main.tsx" ) 372. iter () 373. any ( |d| d[ "code" ] ==json! ( 2875 ) || d[ "code" ] ==json! ( 2307 )) 374}); 375base ( "ui" ); 376 server. eventually ( |s| s. hover ( "main.tsx" , main, "view" ). contains ( "Element" )); 377base ( "missing-again" ); 378 server. eventually ( |s|{ 379 s. hover ( "main.tsx" , main, "view" ); 380 s. diagnostics ( "main.tsx" ) 381. iter () 382. any ( |d| d[ "code" ] ==json! ( 2875 ) || d[ "code" ] ==json! ( 2307 )) 383}); 384 server. close (); 385} 386 387# [ test ] 388fn live_jsr_resolution_shares_cache_lock_vendor_integrity_and_acquisition () { 389let Some ( backend) =backend () else { 390eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 391return ; 392}; 393let dir = tempfile:: tempdir (). unwrap (); 394let root = dir. path (); 395let registry = http:: Http :: new (); 396let sources =[ 397( 398"mod.js" , 399"// @ts-self-types=\"./mod.d.ts\"\nexport const answer = 123;" , 400), 401( "mod.d.ts" , "export declare const answer: 123;" ), 402]; 403let mut manifest = serde_json:: Map :: new (); 404for ( name, text) in sources{ 405 registry. source ( & format! ( "@demo/data/1.0.0/{name}" ), text); 406let hash =format! ( "{:x}" , sha2:: Sha256 :: digest ( text. as_bytes ())); 407 manifest. insert ( 408format! ( "/{name}" ), 409json! ({ "size" : text. len (), "checksum" : format! ( "sha256-{hash}" )}), 410); 411} 412 registry. source ( 413"@demo/data/meta.json" , 414& json! ({ "versions" : { " 1 . 0 . 0 " : {}}}). to_string (), 415); 416 registry. source ( 417"@demo/data/1.0.0_meta.json" , 418& json! ({ "manifest" : manifest, "exports" : { " . " : "./mod.js" }, "moduleGraph2" : {}}) 419. to_string (), 420); 421let main ="import {answer} from \"data\"; answer;" ; 422files ( 423 root, 424& [ 425( "avril.conf" , "imports:{data:'jsr:@demo/data@1'}" ), 426( "main.ts" , main), 427], 428); 429let flags =[ 430format! ( "--jsr-registry={}" , registry. url ), 431format! ( "--cache-dir={}" , root. join ( "cache" ). display ()), 432]; 433let flag_refs: Vec < & str > = flags. iter (). map ( String :: as_str). collect (); 434// Only explicit runtime preparation commands may write the lock. 435let cached =Command :: new ( avril ()) 436. current_dir ( root) 437. args ([ "cache" ]) 438. args ( & flag_refs) 439. arg ( "main.ts" ) 440. output () 441. unwrap (); 442assert! ( 443 cached. status . success (), 444"{}" , 445String :: from_utf8_lossy ( & cached. stderr ) 446); 447let lock = fs:: read_to_string ( root. join ( "avril.lock" )). unwrap (); 448 449for modein [ "online" , "offline" , "vendor" ] { 450if mode =="vendor" { 451let vendored =Command :: new ( avril ()) 452. current_dir ( root) 453. args ([ "vendor" ]) 454. args ( & flag_refs) 455. args ([ "--offline" , "--frozen" ]) 456. arg ( format! ( "--out={}" , root. join ( "vendor" ). display ())) 457. arg ( "main.ts" ) 458. output () 459. unwrap (); 460assert! ( 461 vendored. status . success (), 462"{}" , 463String :: from_utf8_lossy ( & vendored. stderr ) 464); 465// Runtime vendoring covers executable inputs; supply the type artifact too. 466files ( root, & [( "vendor/@demo/data/1.0.0/mod.d.ts" , sources[ 1 ]. 1 )]); 467} 468let before = registry. requests . lock (). unwrap (). len (); 469let mut options: Vec < String > = flags. to_vec (); 470 options. push ( "--frozen" . into ()); 471if mode !="online" { 472 options. push ( "--offline" . into ()); 473} 474if mode =="vendor" { 475 options. push ( format! ( "--vendor={}" , root. join ( "vendor" ). display ())); 476 options. push ( format! ( 477"--cache-dir={}" , 478 root. join ( "empty-cache" ). display () 479)); 480} 481let option_refs: Vec < & str > = options. iter (). map ( String :: as_str). collect (); 482let mut server =Server :: start ( root, & backend, & option_refs); 483 server. initialize (); 484 server. open ( "main.ts" , main); 485assert! ( hover_contains ( 486& mut server, 487"main.ts" , 488 main, 489"answer" , 490"answer: 123" 491)); 492let links = server. definitions ( "main.ts" , main, "answer" ); 493let target = links[ 0 ][ "targetUri" ]. as_str (). unwrap (). to_owned (); 494assert! ( 495 target. starts_with ( "avril-http:" ) && target. ends_with ( "mod.d.ts" ), 496"{target}" 497); 498let remote = server 499. call ( "avril/readDocument" , json! ({ "uri" : target})) 500. unwrap () 501. as_str () 502. unwrap () 503. to_owned (); 504assert! ( remote. contains ( "answer: 123" )); 505assert_eq! ( 506 server 507. call ( 508"avril/readDocument" , 509json! ({ "uri" : target. replace ( "%40" , "@" )}) 510) 511. unwrap () 512. as_str () 513. unwrap (), 514 remote 515); 516assert! ( 517 server 518. hover_uri ( & target, & remote, "answer" ) 519. contains ( "answer: 123" ) 520); 521if mode =="vendor" { 522let path = root. join ( "vendor/@demo/data/1.0.0/mod.d.ts" ); 523 fs:: write ( & path, "export declare const answer: 999;" ). unwrap (); 524 server. eventually ( |s|{ 525 s. hover ( "main.ts" , main, "answer" ); 526 s. diagnostics_uri ( & target). iter (). any ( |d|{ 527let message = d[ "message" ]. as_str (). unwrap (). to_lowercase (); 528 message. contains ( "integrity" ) 529 || message. contains ( "checksum" ) 530 || message. contains ( "size" ) 531}) 532}); 533let links = server. definitions ( "main.ts" , main, "answer" ); 534assert_eq! ( links. len (), 1 , "{links:?}" ); 535// The verified JavaScript source remains navigable; the corrupted 536// declaration must never supply the target or its text. 537assert_eq! ( links[ 0 ][ "targetUri" ], target. replace ( "mod.d.ts" , "mod.js" )); 538assert_eq! ( 539 support:: selected ( sources[ 0 ]. 1 , & links[ 0 ][ "targetSelectionRange" ]), 540"answer" 541); 542 fs:: write ( & path, sources[ 1 ]. 1 ). unwrap (); 543 server. eventually ( |s|{ 544let links = s. definitions ( "main.ts" , main, "answer" ); 545 links[ 0 ][ "targetUri" ] ==json! ( target) && s. diagnostics_uri ( & target). is_empty () 546}); 547} 548assert_eq! ( fs:: read_to_string ( root. join ( "avril.lock" )). unwrap (), lock); 549if mode !="online" { 550assert_eq! ( 551 registry. requests . lock (). unwrap (). len (), 552 before, 553"Offline language tooling made network requests" 554); 555} 556 server. close (); 557} 558 559let before = registry. requests . lock (). unwrap (). len (); 560let denied_options =[ 561 flag_refs[ 0 ], 562 flag_refs[ 1 ], 563& format! ( "--deny-js-exec={}" , registry. url ), 564]; 565let mut denied =Server :: start ( root, & backend, & denied_options); 566 denied. initialize (); 567 denied. open ( "main.ts" , main); 568 denied. hover ( "main.ts" , main, "answer" ); 569assert! ( 570 denied 571. diagnostics ( "main.ts" ) 572. iter () 573. any ( |d| d[ "message" ]. as_str (). unwrap (). contains ( "PermissionDenied" )) 574); 575let links = denied. definitions ( "main.ts" , main, "answer" ); 576assert_eq! ( links. len (), 1 , "{links:?}" ); 577assert_eq! ( links[ 0 ][ "targetUri" ], denied. uri ( "main.ts" )); 578assert_eq! ( 579 support:: selected ( main, & links[ 0 ][ "targetSelectionRange" ]), 580"answer" 581); 582assert_eq! ( 583 registry. requests . lock (). unwrap (). len (), 584 before, 585"Denied resolution made registry requests" 586); 587assert_eq! ( fs:: read_to_string ( root. join ( "avril.lock" )). unwrap (), lock); 588 denied. close (); 589}