@@ -2494,6 +2494,151 @@ async fn test_completion_inline_var_docblock_override_allowed_for_object() {
24942494 }
24952495}
24962496
2497+ /// Inline `@var` without variable name at top level resolves the type
2498+ /// for the immediately following assignment.
2499+ #[tokio::test]
2500+ async fn test_completion_inline_var_docblock_no_varname_top_level() {
2501+ let backend = create_test_backend();
2502+
2503+ let uri = Url::parse("file:///inlinevar_toplevel.php").unwrap();
2504+ let text = concat!(
2505+ "<?php\n",
2506+ "class User {\n",
2507+ " public string $name;\n",
2508+ " public function getEmail(): string {}\n",
2509+ "}\n",
2510+ "/** @var User */\n",
2511+ "$red = a();\n",
2512+ "$red->\n",
2513+ );
2514+
2515+ let open_params = DidOpenTextDocumentParams {
2516+ text_document: TextDocumentItem {
2517+ uri: uri.clone(),
2518+ language_id: "php".to_string(),
2519+ version: 1,
2520+ text: text.to_string(),
2521+ },
2522+ };
2523+ backend.did_open(open_params).await;
2524+
2525+ let completion_params = CompletionParams {
2526+ text_document_position: TextDocumentPositionParams {
2527+ text_document: TextDocumentIdentifier { uri },
2528+ position: Position {
2529+ line: 7,
2530+ character: 6,
2531+ },
2532+ },
2533+ work_done_progress_params: WorkDoneProgressParams::default(),
2534+ partial_result_params: PartialResultParams::default(),
2535+ context: None,
2536+ };
2537+
2538+ let result = backend.completion(completion_params).await.unwrap();
2539+ assert!(
2540+ result.is_some(),
2541+ "Should return completions for @var User without variable name at top level"
2542+ );
2543+
2544+ match result.unwrap() {
2545+ CompletionResponse::Array(items) => {
2546+ let names: Vec<&str> = items
2547+ .iter()
2548+ .map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
2549+ .collect();
2550+ assert!(
2551+ names.contains(&"name"),
2552+ "Should include 'name' from User, got: {:?}",
2553+ names
2554+ );
2555+ assert!(
2556+ names.contains(&"getEmail"),
2557+ "Should include 'getEmail' from User, got: {:?}",
2558+ names
2559+ );
2560+ }
2561+ _ => panic!("Expected CompletionResponse::Array"),
2562+ }
2563+ }
2564+
2565+ /// Inline `@var` without variable name for a cross-file class at top level.
2566+ #[tokio::test]
2567+ async fn test_completion_inline_var_docblock_no_varname_cross_file() {
2568+ let (backend, _dir) = create_psr4_workspace(
2569+ r#"{ "autoload": { "psr-4": { "App\\": "src/" } } }"#,
2570+ &[(
2571+ "src/Models/User.php",
2572+ concat!(
2573+ "<?php\n",
2574+ "namespace App\\Models;\n",
2575+ "class User {\n",
2576+ " public string $name;\n",
2577+ " public function getEmail(): string {}\n",
2578+ "}\n",
2579+ ),
2580+ )],
2581+ );
2582+
2583+ let uri = Url::parse("file:///crossfile_inlinevar.php").unwrap();
2584+ let text = concat!(
2585+ "<?php\n",
2586+ "use App\\Models\\User;\n",
2587+ "/** @var User */\n",
2588+ "$red = a();\n",
2589+ "$red->\n",
2590+ );
2591+
2592+ let open_params = DidOpenTextDocumentParams {
2593+ text_document: TextDocumentItem {
2594+ uri: uri.clone(),
2595+ language_id: "php".to_string(),
2596+ version: 1,
2597+ text: text.to_string(),
2598+ },
2599+ };
2600+ backend.did_open(open_params).await;
2601+
2602+ let completion_params = CompletionParams {
2603+ text_document_position: TextDocumentPositionParams {
2604+ text_document: TextDocumentIdentifier { uri },
2605+ position: Position {
2606+ line: 4,
2607+ character: 6,
2608+ },
2609+ },
2610+ work_done_progress_params: WorkDoneProgressParams::default(),
2611+ partial_result_params: PartialResultParams::default(),
2612+ context: None,
2613+ };
2614+
2615+ let result = backend.completion(completion_params).await.unwrap();
2616+ assert!(
2617+ result.is_some(),
2618+ "Should return completions for cross-file @var User without variable name"
2619+ );
2620+
2621+ match result.unwrap() {
2622+ CompletionResponse::Array(items) => {
2623+ let names: Vec<&str> = items
2624+ .iter()
2625+ .map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
2626+ .collect();
2627+ assert!(
2628+ names.contains(&"name"),
2629+ "Should include 'name' from User, got: {:?}",
2630+ names
2631+ );
2632+ assert!(
2633+ names.contains(&"getEmail"),
2634+ "Should include 'getEmail' from User, got: {:?}",
2635+ names
2636+ );
2637+ }
2638+ _ => panic!("Expected CompletionResponse::Array"),
2639+ }
2640+ }
2641+
24972642#[tokio::test]
24982643async fn test_completion_inline_var_docblock_unconditional_reassignment() {
24992644 let backend = create_test_backend();
0 commit comments