use crate::common::create_test_backend;
use std::sync::Arc;
use tower_lsp::lsp_types::*;
fn get_code_actions(
backend: &phpantom_lsp::Backend,
uri: &str,
content: &str,
start_line: u32,
start_char: u32,
end_line: u32,
end_char: u32,
) -> Vec<CodeActionOrCommand> {
let params = CodeActionParams {
text_document: TextDocumentIdentifier {
uri: uri.parse().unwrap(),
},
range: Range {
start: Position::new(start_line, start_char),
end: Position::new(end_line, end_char),
},
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: WorkDoneProgressParams {
work_done_token: None,
},
partial_result_params: PartialResultParams {
partial_result_token: None,
},
};
backend.handle_code_action(uri, content, ¶ms)
}
fn find_extract_action(actions: &[CodeActionOrCommand]) -> Option<&CodeAction> {
actions.iter().find_map(|a| match a {
CodeActionOrCommand::CodeAction(ca)
if ca.disabled.is_none()
&& (ca.title.starts_with("Extract function")
|| ca.title.starts_with("Extract method")) =>
{
Some(ca)
}
_ => None,
})
}
fn resolve_action(
backend: &phpantom_lsp::Backend,
uri: &str,
content: &str,
action: &CodeAction,
) -> CodeAction {
backend
.open_files()
.write()
.insert(uri.to_string(), Arc::new(content.to_string()));
let (resolved, _) = backend.resolve_code_action(action.clone());
assert!(
resolved.edit.is_some(),
"resolved action should have an edit, title: {}",
resolved.title
);
resolved
}
fn apply_edit(content: &str, edit: &WorkspaceEdit) -> String {
let changes = edit.changes.as_ref().expect("edit should have changes");
let edits = changes
.values()
.next()
.expect("should have edits for one URI");
let mut sorted: Vec<&TextEdit> = edits.iter().collect();
sorted.sort_by(|a, b| {
b.range
.start
.line
.cmp(&a.range.start.line)
.then(b.range.start.character.cmp(&a.range.start.character))
});
let mut result = content.to_string();
for edit in sorted {
let start = position_to_offset(&result, edit.range.start);
let end = position_to_offset(&result, edit.range.end);
result.replace_range(start..end, &edit.new_text);
}
result
}
fn position_to_offset(content: &str, pos: Position) -> usize {
let mut offset = 0;
for (i, line) in content.lines().enumerate() {
if i == pos.line as usize {
return offset + pos.character as usize;
}
offset += line.len() + 1; }
offset
}
#[test]
fn offered_for_complete_statements_in_function() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo() {
$x = 1;
$y = 2;
echo $x + $y;
}
";
let actions = get_code_actions(&backend, uri, content, 2, 4, 3, 11);
let action = find_extract_action(&actions);
assert!(
action.is_some(),
"should offer extract action for complete statements"
);
assert_eq!(
action.unwrap().kind,
Some(CodeActionKind::REFACTOR_EXTRACT),
"should be refactor.extract"
);
}
#[test]
fn not_offered_for_empty_selection() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo() {
$x = 1;
}
";
let actions = get_code_actions(&backend, uri, content, 2, 4, 2, 4);
let action = find_extract_action(&actions);
assert!(
action.is_none(),
"should not offer extract for empty selection"
);
}
#[test]
fn not_offered_for_partial_expression() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo() {
$x = 1 + 2;
}
";
let actions = get_code_actions(&backend, uri, content, 2, 9, 2, 14);
let action = find_extract_action(&actions);
assert!(
action.is_none(),
"should not offer extract for partial expression"
);
}
#[test]
fn not_offered_when_return_without_trailing_return() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo($x) {
if ($x < 0) return null;
if ($x > 100) return 'overflow';
$data = process($x);
echo $data;
}
";
let actions = get_code_actions(&backend, uri, content, 2, 4, 4, 25);
let action = find_extract_action(&actions);
assert!(
action.is_some(),
"Phase 1 should offer the action (validation deferred to resolve)"
);
backend
.open_files()
.write()
.insert(uri.to_string(), Arc::new(content.to_string()));
let (resolved, _) = backend.resolve_code_action(action.unwrap().clone());
assert!(
resolved.edit.is_none(),
"resolve should produce no edit for unsafe returns"
);
}
#[test]
fn void_guard_extraction_produces_bool_pattern() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
class Validator {
public function handle($request): void
{
if (!$request) return;
if (!$this->authorize()) return;
$this->process($request);
$this->log($request);
}
}
";
let actions = get_code_actions(&backend, uri, content, 4, 8, 5, 40);
let action = find_extract_action(&actions).expect("should offer extract for void guards");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("if (!$this->handleGuard($request)) return;"),
"call site should use bool-flag pattern:\n{result}"
);
assert!(
result.contains("): bool"),
"extracted method should have bool return type:\n{result}"
);
assert!(
result.contains("return false;"),
"guard returns should be rewritten to return false:\n{result}"
);
assert!(
result.contains("return true;"),
"fall-through should be return true:\n{result}"
);
}
#[test]
fn uniform_false_guard_extraction() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
class Validator {
public function validate($dog, $cat): bool
{
if (!$dog) return false;
if (!$cat) return false;
return $this->check($dog, $cat);
}
}
";
let actions = get_code_actions(&backend, uri, content, 4, 8, 5, 32);
let action =
find_extract_action(&actions).expect("should offer extract for uniform false guards");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
let has_bool_guard_call = result
.contains("if (!$this->validateGuard($dog, $cat)) return false;")
|| result.contains("if (!$this->validateGuard($cat, $dog)) return false;");
assert!(
has_bool_guard_call,
"call site should use bool-flag pattern with false:\n{result}"
);
assert!(
result.contains("): bool"),
"extracted method should have bool return type:\n{result}"
);
assert!(
result.contains("return true;"),
"fall-through should be return true:\n{result}"
);
}
#[test]
fn uniform_null_guard_extraction_rewrites_returns() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
class Lookup {
public function find(int $id): ?User
{
if ($id <= 0) return null;
if (!$this->hasAccess()) return null;
return $this->repo->findById($id);
}
}
";
let actions = get_code_actions(&backend, uri, content, 4, 8, 5, 45);
let action =
find_extract_action(&actions).expect("should offer extract for uniform null guards");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("if (!$this->findGuard($id)) return null;"),
"call site should use bool-flag pattern with null:\n{result}"
);
assert!(
result.contains("): bool"),
"extracted method should have bool return type:\n{result}"
);
assert!(
result.contains("return false;"),
"null guards should be rewritten to return false:\n{result}"
);
let extracted_method_start = result.find("private function findGuard").unwrap();
let extracted_body = &result[extracted_method_start..];
assert!(
!extracted_body.contains("return null;"),
"extracted method should not contain return null:\n{result}"
);
}
#[test]
fn sentinel_null_extraction_for_different_values() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function classify(int $code): string
{
if ($code < 0) return 'negative';
if ($code === 0) return 'zero';
if ($code > 1000) return 'overflow';
return computeStatus($code);
}
";
let actions = get_code_actions(&backend, uri, content, 3, 4, 5, 41);
let action = find_extract_action(&actions)
.expect("should offer extract for different non-null return values");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("$result = tryClassify($code);"),
"call site should assign to $result:\n{result}"
);
assert!(
result.contains("if ($result !== null) return $result;"),
"call site should check sentinel:\n{result}"
);
assert!(
result.contains("): ?string"),
"extracted function should have ?string return type:\n{result}"
);
assert!(
result.contains("return null;"),
"extracted function should have return null as sentinel:\n{result}"
);
}
#[test]
fn null_guard_with_computed_value_extraction() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
class Animal {
private ?Frog $frog;
public function getSound(): ?string
{
if (!$this->frog) return null;
$sound = $this->frog->speak();
echo $sound;
}
}
";
let actions = get_code_actions(&backend, uri, content, 6, 8, 7, 38);
let action = find_extract_action(&actions)
.expect("should offer extract for null guard with computed value");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("$sound = $this->getSoundGuard("),
"call site should assign $sound from extracted call:\n{result}"
);
assert!(
result.contains("if ($sound === null) return null;"),
"call site should check $sound for null:\n{result}"
);
let extracted_start = result.find("private function getSoundGuard").unwrap();
let extracted_body = &result[extracted_start..];
assert!(
extracted_body.contains("return null;"),
"extracted method should contain guard's return null:\n{result}"
);
assert!(
extracted_body.contains("return $sound;"),
"extracted method should return $sound as fall-through:\n{result}"
);
}
#[test]
fn void_guard_with_computed_value_extraction() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
class Animal {
private ?Frog $frog;
public function process(): void
{
if (!$this->frog) return;
$sound = $this->frog->speak();
echo $sound;
}
}
";
let actions = get_code_actions(&backend, uri, content, 6, 8, 7, 38);
let action = find_extract_action(&actions)
.expect("should offer extract for void guard with computed value");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("$sound = $this->processGuard("),
"call site should assign $sound from extracted call:\n{result}"
);
assert!(
result.contains("if ($sound === null) return;"),
"call site should use bare return (void method):\n{result}"
);
let call_site_area = &result[..result.find("private function").unwrap()];
assert!(
!call_site_area.contains("return null;"),
"call site should not use return null in a void method:\n{result}"
);
let extracted_start = result.find("private function processGuard").unwrap();
let extracted_body = &result[extracted_start..];
assert!(
extracted_body.contains("return null;"),
"extracted method should rewrite void guard to return null:\n{result}"
);
assert!(
extracted_body.contains("return $sound;"),
"extracted method should return $sound as fall-through:\n{result}"
);
assert_eq!(
extracted_body.matches("return;").count(),
0,
"extracted method should not have bare return:\n{result}"
);
}
#[test]
fn offered_when_guard_clause_returns_with_trailing_return() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
class Foo {
public function multiAssign(array $items): int
{
$count = 0;
if (!$items) return $count;
if (!$items) return 0;
foreach ($items as $item) {
$count = $count + 1;
}
return $count;
}
}
";
let actions = get_code_actions(&backend, uri, content, 4, 8, 10, 22);
let action = find_extract_action(&actions)
.expect("should offer extract when guard returns + trailing return");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("return $this->getMultiAssignResult("),
"call site should pass return through:\n{result}"
);
assert!(
result.contains("if (!$items) return"),
"extracted method should keep guard clause returns:\n{result}"
);
}
#[test]
fn offered_when_trailing_return_is_last_statement() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo(): int {
$x = 1;
return $x;
}
";
let actions = get_code_actions(&backend, uri, content, 2, 4, 3, 14);
let action = find_extract_action(&actions).expect("should offer extract for trailing return");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("return getFooResult(") || result.contains("return $this->getFooResult("),
"call site should pass return through:\n{result}"
);
}
#[test]
fn not_offered_outside_function_body() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
$x = 1;
$y = 2;
";
let actions = get_code_actions(&backend, uri, content, 1, 0, 2, 7);
let action = find_extract_action(&actions);
assert!(
action.is_none(),
"should not offer extract outside function body"
);
}
#[test]
fn extracts_single_statement_as_function() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo() {
$x = 1;
echo $x;
}
";
let actions = get_code_actions(&backend, uri, content, 2, 4, 2, 11);
let action = find_extract_action(&actions).expect("should offer extract action");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
action.title.starts_with("Extract function"),
"should be extract function, not method: {}",
action.title
);
assert!(
result.contains("computeX()"),
"should contain call to extracted function: {result}"
);
assert!(
result.contains("function computeX()"),
"should define extracted function: {result}"
);
}
#[test]
fn extracts_multiple_statements() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo() {
$x = 1;
$y = 2;
$z = $x + $y;
echo $z;
}
";
let actions = get_code_actions(&backend, uri, content, 2, 4, 3, 11);
let action = find_extract_action(&actions).expect("should offer extract action");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("function extracted("),
"should define extracted function: {result}"
);
}
#[test]
fn passes_variables_read_after_selection_as_return_values() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo() {
$x = 10;
echo $x;
}
";
let actions = get_code_actions(&backend, uri, content, 2, 4, 2, 12);
let action = find_extract_action(&actions).expect("should offer extract action");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("$x = computeX("),
"should assign return value to $x: {result}"
);
assert!(
result.contains("return $x;"),
"extracted function should return $x: {result}"
);
}
#[test]
fn passes_variables_defined_before_selection_as_parameters() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo() {
$x = 10;
$y = $x + 5;
echo $y;
}
";
let actions = get_code_actions(&backend, uri, content, 3, 4, 3, 16);
let action = find_extract_action(&actions).expect("should offer extract action");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("computeY($x)"),
"should pass $x as argument: {result}"
);
assert!(
result.contains("$y = computeY("),
"should assign $y from return value: {result}"
);
}
#[test]
fn local_variables_stay_inside() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo() {
$temp = 1;
$temp = $temp + 2;
echo 'done';
}
";
let actions = get_code_actions(&backend, uri, content, 2, 4, 3, 22);
let action = find_extract_action(&actions).expect("should offer extract action");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("extracted()"),
"should call with no arguments (local var stays inside): {result}"
);
}
#[test]
fn extracts_as_method_when_this_is_used() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
class Calculator {
private int $value = 0;
public function compute() {
$x = $this->value;
echo $x;
}
}
";
let actions = get_code_actions(&backend, uri, content, 5, 8, 6, 16);
let action = find_extract_action(&actions).expect("should offer extract action");
assert!(
action.title.starts_with("Extract method"),
"should be extract method when $this is used: {}",
action.title
);
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("$this->renderCompute()"),
"should call via $this->: {result}"
);
assert!(
result.contains("private function renderCompute()"),
"extracted method should be private: {result}"
);
}
#[test]
fn extracts_static_method_when_in_static_context() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
class Util {
public static function run() {
$x = 1;
$y = 2;
echo $x + $y;
}
}
";
let actions = get_code_actions(&backend, uri, content, 3, 8, 4, 15);
let action = find_extract_action(&actions).expect("should offer extract action");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("private static function extracted("),
"extracted method should be private static: {result}"
);
}
#[test]
fn method_with_parameters_and_return() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
class Foo {
public function bar() {
$a = 5;
$b = $a * 2;
echo $b;
}
}
";
let actions = get_code_actions(&backend, uri, content, 4, 8, 4, 20);
let action = find_extract_action(&actions).expect("should offer extract action");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("$this->computeB($a)") || result.contains("computeB($a)"),
"should pass $a as argument: {result}"
);
assert!(
result.contains("$b = $this->computeB(") || result.contains("$b = computeB("),
"should assign $b from return: {result}"
);
assert!(
result.contains("return $b;"),
"extracted function should return $b: {result}"
);
}
#[test]
fn deduplicates_name_when_extracted_exists() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function extracted() {}
function foo() {
$x = 1;
echo $x;
}
";
let actions = get_code_actions(&backend, uri, content, 4, 4, 4, 11);
let action = find_extract_action(&actions).expect("should offer extract action");
assert!(
action.title.contains("Extract function"),
"should offer extract function action: {}",
action.title
);
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
!result.contains("\nfunction extracted()")
|| result.matches("function extracted").count() > 1
|| result.contains("function extracted1")
|| result.contains("function computeX"),
"should deduplicate or use contextual name: {result}"
);
}
#[test]
fn void_return_when_no_return_values() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo() {
$x = 1;
$y = 2;
}
";
let actions = get_code_actions(&backend, uri, content, 2, 4, 3, 11);
let action = find_extract_action(&actions).expect("should offer extract action");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("): void"),
"should have void return type: {result}"
);
}
#[test]
fn works_in_namespaced_function() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
namespace App\\Utils;
function helper() {
$x = 42;
echo $x;
}
";
let actions = get_code_actions(&backend, uri, content, 4, 4, 4, 12);
let action = find_extract_action(&actions);
assert!(
action.is_some(),
"should offer extract action in namespaced function"
);
}
#[test]
fn works_in_namespaced_class() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
namespace App\\Services;
class Greeter {
public function greet() {
$name = 'World';
echo 'Hello ' . $name;
}
}
";
let actions = get_code_actions(&backend, uri, content, 5, 8, 6, 31);
let action = find_extract_action(&actions);
assert!(
action.is_some(),
"should offer extract action in namespaced class method"
);
}
#[test]
fn extracts_entire_if_block() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo($x) {
if ($x > 0) {
echo 'positive';
}
echo 'done';
}
";
let actions = get_code_actions(&backend, uri, content, 2, 4, 4, 5);
let action = find_extract_action(&actions);
assert!(action.is_some(), "should offer extract for entire if block");
}
#[test]
fn extracts_entire_foreach() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo($items) {
foreach ($items as $item) {
echo $item;
}
echo 'done';
}
";
let actions = get_code_actions(&backend, uri, content, 2, 4, 4, 5);
let action = find_extract_action(&actions);
assert!(
action.is_some(),
"should offer extract for entire foreach block"
);
}
#[test]
fn code_action_kind_is_refactor_extract() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo() {
$x = 1;
echo $x;
}
";
let actions = get_code_actions(&backend, uri, content, 2, 4, 2, 11);
let action = find_extract_action(&actions).expect("should offer action");
assert_eq!(
action.kind,
Some(CodeActionKind::REFACTOR_EXTRACT),
"kind should be refactor.extract"
);
}
#[test]
fn extracted_method_has_correct_indentation() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
class Foo {
public function bar() {
$x = $this->baz();
echo $x;
}
public function baz(): int {
return 42;
}
}
";
let actions = get_code_actions(&backend, uri, content, 3, 8, 4, 16);
let action = find_extract_action(&actions).expect("should offer extract action");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("\n private function renderBar()"),
"extracted method should be indented at member level (4 spaces), got:\n{result}"
);
assert!(
!result.contains("\n private function"),
"extracted method must NOT be double-indented:\n{result}"
);
}
#[test]
fn extracted_method_body_lines_indented_consistently() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
class Foo {
public function dangerousInline(): void
{
$id = $this->generateId();
$this->save($id);
$this->log($id);
}
public function generateId(): string { return 'x'; }
public function save(string $id): void {}
public function log(string $id): void {}
}
";
let actions = get_code_actions(&backend, uri, content, 5, 8, 6, 24);
let action = find_extract_action(&actions).expect("should offer extract action");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
let in_extracted = result
.lines()
.skip_while(|l| !l.contains("private function extracted("))
.skip(1) .skip(1) .take_while(|l| !l.trim().starts_with('}'))
.filter(|l| !l.trim().is_empty())
.collect::<Vec<_>>();
assert!(
!in_extracted.is_empty(),
"should have body lines in extracted method:\n{result}"
);
for line in &in_extracted {
let indent = line.len() - line.trim_start().len();
assert_eq!(
indent, 8,
"body line should have 8 spaces indent, got {indent}: '{line}'\nfull result:\n{result}"
);
}
}
#[test]
fn offered_when_selection_starts_with_blank_line() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
class Foo {
public function multi(array $items): int
{
$count = 0;
foreach ($items as $item) {
$count = $count + $item;
}
return $count;
}
}
";
let actions = get_code_actions(&backend, uri, content, 5, 4, 8, 9);
let action = find_extract_action(&actions);
assert!(
action.is_some(),
"should offer extract when selection starts with a blank line, got actions: {:?}",
actions
.iter()
.map(|a| match a {
CodeActionOrCommand::CodeAction(ca) => ca.title.clone(),
CodeActionOrCommand::Command(cmd) => cmd.title.clone(),
})
.collect::<Vec<_>>()
);
}
#[test]
fn extracted_function_body_has_correct_indentation() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
function foo() {
$x = 1;
echo $x;
}
";
let actions = get_code_actions(&backend, uri, content, 2, 4, 2, 11);
let action = find_extract_action(&actions).expect("should offer extract action");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
result.contains("\nfunction computeX()"),
"extracted function should be at top level:\n{result}"
);
}
#[test]
fn accumulator_init_and_loop_not_passed_as_parameter() {
let backend = create_test_backend();
let uri = "file:///test.php";
let content = "\
<?php
class Foo {
public function multiAssign(array $items): int
{
$count = 0;
foreach ($items as $item) {
$count = $count + 1;
}
return $count;
}
}
";
let actions = get_code_actions(&backend, uri, content, 4, 8, 7, 9);
let action = find_extract_action(&actions).expect("should offer extract action");
let resolved = resolve_action(&backend, uri, content, action);
let result = apply_edit(content, resolved.edit.as_ref().unwrap());
assert!(
!result.contains("computeCount($count"),
"$count must not be passed as parameter (first write is inside selection):\n{result}"
);
assert!(
result.contains("$count = $this->computeCount("),
"$count should be assigned from the extracted method's return value:\n{result}"
);
}