references_rename_test.rs
1 mod common; 2 3 use common::TestFixture; 4 use ecolog_lsp::server::handlers::{handle_prepare_rename, handle_references, handle_rename}; 5 use tower_lsp::lsp_types::{ 6 Position, ReferenceContext, ReferenceParams, RenameParams, TextDocumentIdentifier, 7 TextDocumentPositionParams, 8 }; 9 10 #[tokio::test] 11 async fn test_find_references_direct_reference() { 12 let fixture = TestFixture::new().await; 13 14 let uri = fixture.create_file( 15 "test.js", 16 "const url = process.env.DB_URL;\nconsole.log(process.env.DB_URL);", 17 ); 18 19 fixture.index_workspace().await; 20 21 fixture 22 .state 23 .document_manager 24 .open( 25 uri.clone(), 26 "javascript".to_string(), 27 "const url = process.env.DB_URL;\nconsole.log(process.env.DB_URL);".to_string(), 28 1, 29 ) 30 .await; 31 32 let params = ReferenceParams { 33 text_document_position: TextDocumentPositionParams { 34 text_document: TextDocumentIdentifier { uri: uri.clone() }, 35 position: Position { 36 line: 0, 37 character: 24, 38 }, 39 }, 40 work_done_progress_params: Default::default(), 41 partial_result_params: Default::default(), 42 context: ReferenceContext { 43 include_declaration: true, 44 }, 45 }; 46 47 let result = handle_references(params, &fixture.state).await; 48 49 assert!(result.is_some(), "Expected to find references"); 50 let locations = result.unwrap(); 51 52 assert!( 53 locations.len() >= 2, 54 "Expected at least 2 references, found {}", 55 locations.len() 56 ); 57 } 58 59 #[tokio::test] 60 async fn test_find_references_across_files() { 61 let fixture = TestFixture::new().await; 62 63 fixture.create_file("a.js", "const key = process.env.API_KEY;"); 64 fixture.create_file("b.ts", "const apiKey = process.env.API_KEY;"); 65 66 fixture.index_workspace().await; 67 68 let uri_a = fixture.create_file("a.js", "const key = process.env.API_KEY;"); 69 fixture 70 .state 71 .document_manager 72 .open( 73 uri_a.clone(), 74 "javascript".to_string(), 75 "const key = process.env.API_KEY;".to_string(), 76 1, 77 ) 78 .await; 79 80 let params = ReferenceParams { 81 text_document_position: TextDocumentPositionParams { 82 text_document: TextDocumentIdentifier { uri: uri_a }, 83 position: Position { 84 line: 0, 85 character: 24, 86 }, 87 }, 88 work_done_progress_params: Default::default(), 89 partial_result_params: Default::default(), 90 context: ReferenceContext { 91 include_declaration: true, 92 }, 93 }; 94 95 let result = handle_references(params, &fixture.state).await; 96 97 assert!(result.is_some(), "Expected to find references"); 98 let locations = result.unwrap(); 99 100 assert!( 101 locations.len() >= 2, 102 "Expected at least 2 references across files, found {}", 103 locations.len() 104 ); 105 } 106 107 #[tokio::test] 108 async fn test_find_references_no_refs_for_unknown_var() { 109 let fixture = TestFixture::new().await; 110 111 let uri = fixture.create_file("test.js", "const x = process.env.UNKNOWN_VAR;"); 112 113 fixture.index_workspace().await; 114 115 fixture 116 .state 117 .document_manager 118 .open( 119 uri.clone(), 120 "javascript".to_string(), 121 "const x = process.env.UNKNOWN_VAR;".to_string(), 122 1, 123 ) 124 .await; 125 126 let params = ReferenceParams { 127 text_document_position: TextDocumentPositionParams { 128 text_document: TextDocumentIdentifier { uri }, 129 position: Position { 130 line: 0, 131 character: 22, 132 }, 133 }, 134 work_done_progress_params: Default::default(), 135 partial_result_params: Default::default(), 136 context: ReferenceContext { 137 include_declaration: false, 138 }, 139 }; 140 141 let result = handle_references(params, &fixture.state).await; 142 143 assert!(result.is_some()); 144 } 145 146 #[tokio::test] 147 async fn test_prepare_rename_valid_env_var() { 148 let fixture = TestFixture::new().await; 149 150 let uri = fixture.create_file("test.js", "const url = process.env.DB_URL;"); 151 152 fixture 153 .state 154 .document_manager 155 .open( 156 uri.clone(), 157 "javascript".to_string(), 158 "const url = process.env.DB_URL;".to_string(), 159 1, 160 ) 161 .await; 162 163 let params = TextDocumentPositionParams { 164 text_document: TextDocumentIdentifier { uri }, 165 position: Position { 166 line: 0, 167 character: 24, 168 }, 169 }; 170 171 let result = handle_prepare_rename(params, &fixture.state).await; 172 173 assert!( 174 result.is_some(), 175 "Prepare rename should succeed for valid env var" 176 ); 177 } 178 179 #[tokio::test] 180 async fn test_rename_env_var() { 181 let fixture = TestFixture::new().await; 182 183 let uri = fixture.create_file( 184 "test.js", 185 "const url = process.env.DB_URL;\nconst x = process.env.DB_URL;", 186 ); 187 188 fixture.index_workspace().await; 189 190 fixture 191 .state 192 .document_manager 193 .open( 194 uri.clone(), 195 "javascript".to_string(), 196 "const url = process.env.DB_URL;\nconst x = process.env.DB_URL;".to_string(), 197 1, 198 ) 199 .await; 200 201 let params = RenameParams { 202 text_document_position: TextDocumentPositionParams { 203 text_document: TextDocumentIdentifier { uri }, 204 position: Position { 205 line: 0, 206 character: 24, 207 }, 208 }, 209 new_name: "DATABASE_URL".to_string(), 210 work_done_progress_params: Default::default(), 211 }; 212 213 let result = handle_rename(params, &fixture.state).await; 214 215 assert!(result.is_some(), "Rename should return edits"); 216 let edit = result.unwrap(); 217 218 assert!(edit.changes.is_some(), "WorkspaceEdit should have changes"); 219 let changes = edit.changes.unwrap(); 220 221 assert!( 222 !changes.is_empty(), 223 "Should have edits for at least one file" 224 ); 225 } 226 227 #[tokio::test] 228 async fn test_rename_invalid_new_name() { 229 let fixture = TestFixture::new().await; 230 231 let uri = fixture.create_file("test.js", "const url = process.env.DB_URL;"); 232 233 fixture 234 .state 235 .document_manager 236 .open( 237 uri.clone(), 238 "javascript".to_string(), 239 "const url = process.env.DB_URL;".to_string(), 240 1, 241 ) 242 .await; 243 244 let params = RenameParams { 245 text_document_position: TextDocumentPositionParams { 246 text_document: TextDocumentIdentifier { uri }, 247 position: Position { 248 line: 0, 249 character: 24, 250 }, 251 }, 252 new_name: "123_INVALID".to_string(), 253 work_done_progress_params: Default::default(), 254 }; 255 256 let result = handle_rename(params, &fixture.state).await; 257 258 assert!( 259 result.is_none(), 260 "Rename with invalid name should return None" 261 ); 262 } 263 264 #[tokio::test] 265 async fn test_prepare_rename_in_env_file() { 266 let fixture = TestFixture::new().await; 267 268 let env_uri = fixture.create_file(".env", "API_KEY=secret123\nDB_URL=postgres://localhost"); 269 270 fixture 271 .state 272 .document_manager 273 .open( 274 env_uri.clone(), 275 "plaintext".to_string(), 276 "API_KEY=secret123\nDB_URL=postgres://localhost".to_string(), 277 1, 278 ) 279 .await; 280 281 let params = TextDocumentPositionParams { 282 text_document: TextDocumentIdentifier { uri: env_uri }, 283 position: Position { 284 line: 0, 285 character: 3, 286 }, 287 }; 288 289 let result = handle_prepare_rename(params, &fixture.state).await; 290 291 assert!( 292 result.is_some(), 293 "Prepare rename should succeed for env var in .env file" 294 ); 295 } 296 297 #[tokio::test] 298 async fn test_rename_from_env_file() { 299 let fixture = TestFixture::new().await; 300 301 let env_uri = fixture.create_file(".env", "API_KEY=secret123"); 302 303 let js_uri = fixture.create_file("app.js", "const key = process.env.API_KEY;"); 304 let ts_uri = fixture.create_file("config.ts", "const apiKey = process.env.API_KEY;"); 305 306 fixture.index_workspace().await; 307 308 fixture 309 .state 310 .document_manager 311 .open( 312 env_uri.clone(), 313 "plaintext".to_string(), 314 "API_KEY=secret123".to_string(), 315 1, 316 ) 317 .await; 318 fixture 319 .state 320 .document_manager 321 .open( 322 js_uri.clone(), 323 "javascript".to_string(), 324 "const key = process.env.API_KEY;".to_string(), 325 1, 326 ) 327 .await; 328 fixture 329 .state 330 .document_manager 331 .open( 332 ts_uri.clone(), 333 "typescript".to_string(), 334 "const apiKey = process.env.API_KEY;".to_string(), 335 1, 336 ) 337 .await; 338 339 let params = RenameParams { 340 text_document_position: TextDocumentPositionParams { 341 text_document: TextDocumentIdentifier { 342 uri: env_uri.clone(), 343 }, 344 position: Position { 345 line: 0, 346 character: 3, 347 }, 348 }, 349 new_name: "AUTH_TOKEN".to_string(), 350 work_done_progress_params: Default::default(), 351 }; 352 353 let result = handle_rename(params, &fixture.state).await; 354 355 assert!( 356 result.is_some(), 357 "Rename from .env file should return edits" 358 ); 359 let edit = result.unwrap(); 360 361 assert!(edit.changes.is_some(), "WorkspaceEdit should have changes"); 362 let changes = edit.changes.unwrap(); 363 364 assert!( 365 !changes.is_empty(), 366 "Should have edits for at least one file" 367 ); 368 369 assert!( 370 changes.contains_key(&env_uri), 371 "Changes should include the .env file" 372 ); 373 } 374 375 #[tokio::test] 376 async fn test_rename_from_env_file_updates_code_files() { 377 let fixture = TestFixture::new().await; 378 379 let env_uri = fixture.create_file(".env", "DB_HOST=localhost"); 380 381 let js_content = "const host = process.env.DB_HOST;\nconst url = `http://${host}`;"; 382 let js_uri = fixture.create_file("server.js", js_content); 383 384 fixture.index_workspace().await; 385 386 fixture 387 .state 388 .document_manager 389 .open( 390 env_uri.clone(), 391 "plaintext".to_string(), 392 "DB_HOST=localhost".to_string(), 393 1, 394 ) 395 .await; 396 fixture 397 .state 398 .document_manager 399 .open( 400 js_uri.clone(), 401 "javascript".to_string(), 402 js_content.to_string(), 403 1, 404 ) 405 .await; 406 407 let params = RenameParams { 408 text_document_position: TextDocumentPositionParams { 409 text_document: TextDocumentIdentifier { 410 uri: env_uri.clone(), 411 }, 412 position: Position { 413 line: 0, 414 character: 2, 415 }, 416 }, 417 new_name: "DATABASE_HOST".to_string(), 418 work_done_progress_params: Default::default(), 419 }; 420 421 let result = handle_rename(params, &fixture.state).await; 422 423 assert!(result.is_some(), "Rename should succeed"); 424 let edit = result.unwrap(); 425 let changes = edit.changes.expect("Should have changes"); 426 427 if changes.contains_key(&js_uri) { 428 let js_edits = &changes[&js_uri]; 429 430 assert!(js_edits.len() >= 1, "JS file should have at least 1 edit"); 431 } 432 433 assert!( 434 changes.contains_key(&env_uri), 435 ".env file should be in changes" 436 ); 437 } 438 439 #[tokio::test] 440 async fn test_workspace_index_stats() { 441 let fixture = TestFixture::new().await; 442 443 fixture.create_file("a.js", "process.env.API_KEY"); 444 fixture.create_file("b.ts", "process.env.DB_URL"); 445 fixture.create_file("c.py", "os.environ['DEBUG']"); 446 447 fixture.index_workspace().await; 448 449 let stats = fixture.state.workspace_index.stats(); 450 451 assert!( 452 stats.total_files >= 1, 453 "Should have indexed at least 1 file" 454 ); 455 assert!( 456 stats.total_env_vars >= 1, 457 "Should have indexed at least 1 env var" 458 ); 459 } 460 461 #[tokio::test] 462 async fn test_workspace_index_files_for_env_var() { 463 let fixture = TestFixture::new().await; 464 465 fixture.create_file("a.js", "const k = process.env.API_KEY;"); 466 fixture.create_file("b.ts", "const key = process.env.API_KEY;"); 467 fixture.create_file("c.js", "const port = process.env.PORT;"); 468 469 fixture.index_workspace().await; 470 471 let api_key_files = fixture.state.workspace_index.files_for_env_var("API_KEY"); 472 473 assert!( 474 api_key_files.len() >= 2, 475 "Expected at least 2 files with API_KEY, found {}", 476 api_key_files.len() 477 ); 478 } 479 480 #[tokio::test] 481 async fn test_rename_does_not_affect_similar_names() { 482 let fixture = TestFixture::new().await; 483 484 let env_uri = fixture.create_file(".env", "DEBUG=on\nDEBUGHAT=value"); 485 486 let js_content = "const d = process.env.DEBUG;\nconst dh = process.env.DEBUGHAT;"; 487 let js_uri = fixture.create_file("app.js", js_content); 488 489 fixture.index_workspace().await; 490 491 fixture 492 .state 493 .document_manager 494 .open( 495 env_uri.clone(), 496 "plaintext".to_string(), 497 "DEBUG=on\nDEBUGHAT=value".to_string(), 498 1, 499 ) 500 .await; 501 fixture 502 .state 503 .document_manager 504 .open( 505 js_uri.clone(), 506 "javascript".to_string(), 507 js_content.to_string(), 508 1, 509 ) 510 .await; 511 512 let params = RenameParams { 513 text_document_position: TextDocumentPositionParams { 514 text_document: TextDocumentIdentifier { 515 uri: env_uri.clone(), 516 }, 517 position: Position { 518 line: 0, 519 character: 0, 520 }, 521 }, 522 new_name: "SOMEWHAT".to_string(), 523 work_done_progress_params: Default::default(), 524 }; 525 526 let result = handle_rename(params, &fixture.state).await; 527 528 assert!(result.is_some(), "Rename should succeed"); 529 let edit = result.unwrap(); 530 let changes = edit.changes.expect("Should have changes"); 531 532 let env_edits = changes.get(&env_uri).expect(".env should have edits"); 533 assert_eq!(env_edits.len(), 1, "Should only rename DEBUG, not DEBUGHAT"); 534 535 let first_edit = &env_edits[0]; 536 assert_eq!(first_edit.new_text, "SOMEWHAT"); 537 assert_eq!(first_edit.range.start.line, 0); 538 assert_eq!(first_edit.range.start.character, 0); 539 assert_eq!(first_edit.range.end.line, 0); 540 assert_eq!(first_edit.range.end.character, 5); 541 542 if let Some(js_edits) = changes.get(&js_uri) { 543 assert_eq!( 544 js_edits.len(), 545 1, 546 "Should only rename DEBUG reference, not DEBUGHAT" 547 ); 548 let js_edit = &js_edits[0]; 549 assert_eq!(js_edit.new_text, "SOMEWHAT"); 550 } 551 }