#![allow(dead_code)]
use phpantom_lsp::Backend;
use std::collections::HashMap;
use std::fs;
use std::sync::Arc;
use tower_lsp::lsp_types::*;
pub fn create_test_backend() -> Backend {
Backend::new_test()
}
pub fn create_test_backend_with_full_stubs() -> Backend {
Backend::new_test_with_full_stubs()
}
static UNIT_ENUM_STUB: &str = "\
<?php
interface UnitEnum
{
/** @return static[] */
public static function cases(): array;
public readonly string $name;
}
";
static BACKED_ENUM_STUB: &str = "\
<?php
interface BackedEnum extends UnitEnum
{
public static function from(int|string $value): static;
public static function tryFrom(int|string $value): ?static;
public readonly int|string $value;
}
";
static ARRAY_FUNCTIONS_STUB: &str = "\
<?php
/**
* @param callable|null $callback
* @param array $array
* @param array ...$arrays
* @return array
*/
function array_map(?callable $callback, array $array, array ...$arrays): array {}
/**
* @param array &$array
* @return mixed
*/
function array_pop(array &$array): mixed {}
/**
* @param array &$array
* @param mixed ...$values
* @return int
*/
function array_push(array &$array, mixed ...$values): int {}
/**
* @param string|int $key
* @param array $array
* @return bool
*/
function array_key_exists(string|int $key, array $array): bool {}
";
static STRING_FUNCTIONS_STUB: &str = "\
<?php
/**
* @param string $haystack
* @param string $needle
* @return bool
*/
function str_contains(string $haystack, string $needle): bool {}
/**
* @param string $string
* @param int $offset
* @param int|null $length
* @return string
*/
function substr(string $string, int $offset, ?int $length = null): string {}
";
static JSON_FUNCTIONS_STUB: &str = "\
<?php
/**
* @param string $json
* @param bool|null $associative
* @param int $depth
* @param int $flags
* @return mixed
*/
function json_decode(string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed {}
";
static DATE_FUNCTIONS_STUB: &str = "\
<?php
/**
* @param string|null $datetime
* @param DateTimeZone|null $timezone
* @return DateTime|false
*/
function date_create(?string $datetime = \"now\", ?DateTimeZone $timezone = null): DateTime|false {}
";
static SIMPLEXML_FUNCTIONS_STUB: &str = "\
<?php
/**
* @param string $data
* @param string|null $class_name
* @param int $options
* @param string $namespace_or_prefix
* @param bool $is_prefix
* @return SimpleXMLElement|false
*/
function simplexml_load_string(string $data, ?string $class_name = null, int $options = 0, string $namespace_or_prefix = \"\", bool $is_prefix = false): SimpleXMLElement|false {}
";
static PCRE_FUNCTIONS_STUB: &str = "\
<?php
/**
* @param string $pattern
* @param string $subject
* @param array|null &$matches
* @param int $flags
* @param int $offset
* @return int|false
*/
function preg_match(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int|false {}
";
static DATETIME_CLASS_STUB: &str = "\
<?php
class DateTime
{
public function __construct(?string $datetime = \"now\", ?DateTimeZone $timezone = null) {}
/**
* @param string $format
* @return string
*/
public function format(string $format): string {}
/**
* @param string $modifier
* @return DateTime|false
*/
public function modify(string $modifier): DateTime|false {}
/**
* @return int
*/
public function getTimestamp(): int {}
/**
* @param int $year
* @param int $month
* @param int $day
* @return DateTime
*/
public function setDate(int $year, int $month, int $day): DateTime {}
/**
* @param int $hour
* @param int $minute
* @param int $second
* @param int $microsecond
* @return DateTime
*/
public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): DateTime {}
}
";
static SIMPLEXMLELEMENT_CLASS_STUB: &str = "\
<?php
class SimpleXMLElement
{
/**
* @param string $expression
* @return array|false|null
*/
public function xpath(string $expression): array|false|null {}
/**
* @param string|null $namespaceOrPrefix
* @param bool $isPrefix
* @return SimpleXMLElement|null
*/
public function children(?string $namespaceOrPrefix = null, bool $isPrefix = false): ?SimpleXMLElement {}
/**
* @param string|null $namespaceOrPrefix
* @param bool $isPrefix
* @return SimpleXMLElement|null
*/
public function attributes(?string $namespaceOrPrefix = null, bool $isPrefix = false): ?SimpleXMLElement {}
/**
* @param string $qualifiedName
* @param string|null $value
* @param string|null $namespace
* @return SimpleXMLElement|null
*/
public function addChild(string $qualifiedName, ?string $value = null, ?string $namespace = null): ?SimpleXMLElement {}
/**
* @return string
*/
public function getName(): string {}
}
";
static STDCLASS_STUB: &str = "\
<?php
/**
* Created by typecasting to object.
* @link https://php.net/manual/en/reserved.classes.php
*/
class stdClass {}
";
static CLOSURE_CLASS_STUB: &str = "\
<?php
/**
* Class used to represent anonymous functions.
* @link https://php.net/manual/en/class.closure.php
*/
final class Closure
{
private function __construct() {}
/**
* @param callable $callback
* @return Closure
*/
public static function fromCallable(callable $callback): Closure {}
/**
* @param object|null $newThis
* @param string|null $newScope
* @return Closure|null
*/
public function bindTo(?object $newThis, ?string $newScope = \"static\"): ?Closure {}
/**
* @param Closure|null $closure
* @param object|null $newThis
* @param string|null $newScope
* @return Closure|null
*/
public static function bind(?Closure $closure, ?object $newThis, ?string $newScope = \"static\"): ?Closure {}
/**
* @param mixed ...$args
* @return mixed
*/
public function call(object $newThis, mixed ...$args): mixed {}
public function __invoke(): mixed {}
}
";
static EXCEPTION_CLASS_STUB: &str = "\
<?php
class Exception implements Throwable
{
public function __construct(string $message = \"\", int $code = 0, ?Throwable $previous = null) {}
/**
* @return string
*/
final public function getMessage(): string {}
/**
* @return int
*/
final public function getCode(): int {}
/**
* @return string
*/
final public function getFile(): string {}
/**
* @return int
*/
final public function getLine(): int {}
/**
* @return array
*/
final public function getTrace(): array {}
/**
* @return string
*/
final public function getTraceAsString(): string {}
/**
* @return ?Throwable
*/
final public function getPrevious(): ?Throwable {}
/**
* @return string
*/
public function __toString(): string {}
}
";
static RUNTIME_EXCEPTION_CLASS_STUB: &str = "\
<?php
class RuntimeException extends Exception {}
";
static CONSTANTS_STUB: &str = "\
<?php
define('PHP_EOL', \"\\n\");
define('PHP_INT_MAX', 9223372036854775807);
define('PHP_INT_MIN', -9223372036854775808);
define('PHP_MAJOR_VERSION', 8);
define('SORT_ASC', 4);
define('SORT_DESC', 3);
";
pub fn create_test_backend_with_exception_stubs() -> Backend {
let mut stubs: HashMap<&'static str, &'static str> = HashMap::new();
stubs.insert("Exception", EXCEPTION_CLASS_STUB);
stubs.insert("RuntimeException", RUNTIME_EXCEPTION_CLASS_STUB);
Backend::new_test_with_stubs(stubs)
}
pub fn create_test_backend_with_stdclass_stub() -> Backend {
let mut stubs: HashMap<&'static str, &'static str> = HashMap::new();
stubs.insert("stdClass", STDCLASS_STUB);
Backend::new_test_with_stubs(stubs)
}
pub fn create_test_backend_with_closure_stub() -> Backend {
let mut stubs: HashMap<&'static str, &'static str> = HashMap::new();
stubs.insert("Closure", CLOSURE_CLASS_STUB);
Backend::new_test_with_stubs(stubs)
}
pub fn create_test_backend_with_stubs() -> Backend {
let mut stubs: HashMap<&'static str, &'static str> = HashMap::new();
stubs.insert("UnitEnum", UNIT_ENUM_STUB);
stubs.insert("BackedEnum", BACKED_ENUM_STUB);
Backend::new_test_with_stubs(stubs)
}
pub fn create_test_backend_with_function_stubs() -> Backend {
let mut class_stubs: HashMap<&'static str, &'static str> = HashMap::new();
class_stubs.insert("DateTime", DATETIME_CLASS_STUB);
class_stubs.insert("SimpleXMLElement", SIMPLEXMLELEMENT_CLASS_STUB);
class_stubs.insert("UnitEnum", UNIT_ENUM_STUB);
class_stubs.insert("BackedEnum", BACKED_ENUM_STUB);
let mut function_stubs: HashMap<&'static str, &'static str> = HashMap::new();
function_stubs.insert("array_map", ARRAY_FUNCTIONS_STUB);
function_stubs.insert("array_pop", ARRAY_FUNCTIONS_STUB);
function_stubs.insert("array_push", ARRAY_FUNCTIONS_STUB);
function_stubs.insert("array_key_exists", ARRAY_FUNCTIONS_STUB);
function_stubs.insert("str_contains", STRING_FUNCTIONS_STUB);
function_stubs.insert("substr", STRING_FUNCTIONS_STUB);
function_stubs.insert("json_decode", JSON_FUNCTIONS_STUB);
function_stubs.insert("date_create", DATE_FUNCTIONS_STUB);
function_stubs.insert("simplexml_load_string", SIMPLEXML_FUNCTIONS_STUB);
function_stubs.insert("preg_match", PCRE_FUNCTIONS_STUB);
let mut constant_stubs: HashMap<&'static str, &'static str> = HashMap::new();
constant_stubs.insert("PHP_EOL", CONSTANTS_STUB);
constant_stubs.insert("PHP_INT_MAX", CONSTANTS_STUB);
constant_stubs.insert("PHP_INT_MIN", CONSTANTS_STUB);
constant_stubs.insert("PHP_MAJOR_VERSION", CONSTANTS_STUB);
constant_stubs.insert("SORT_ASC", CONSTANTS_STUB);
constant_stubs.insert("SORT_DESC", CONSTANTS_STUB);
Backend::new_test_with_all_stubs(class_stubs, function_stubs, constant_stubs)
}
pub fn create_psr4_workspace(
composer_json: &str,
files: &[(&str, &str)],
) -> (Backend, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("failed to create temp dir");
fs::write(dir.path().join("composer.json"), composer_json)
.expect("failed to write composer.json");
for (rel_path, content) in files {
let full = dir.path().join(rel_path);
if let Some(parent) = full.parent() {
fs::create_dir_all(parent).expect("failed to create dirs");
}
fs::write(&full, content).expect("failed to write PHP file");
}
let (mappings, _vendor_dir) = phpantom_lsp::composer::parse_composer_json(dir.path());
let backend = Backend::new_test_with_workspace(dir.path().to_path_buf(), mappings);
(backend, dir)
}
pub fn create_psr4_workspace_with_exception_stubs(
composer_json: &str,
files: &[(&str, &str)],
) -> (Backend, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("failed to create temp dir");
fs::write(dir.path().join("composer.json"), composer_json)
.expect("failed to write composer.json");
for (rel_path, content) in files {
let full = dir.path().join(rel_path);
if let Some(parent) = full.parent() {
fs::create_dir_all(parent).expect("failed to create dirs");
}
fs::write(&full, content).expect("failed to write PHP file");
}
let (mappings, _vendor_dir) = phpantom_lsp::composer::parse_composer_json(dir.path());
let mut stubs: HashMap<&'static str, &'static str> = HashMap::new();
stubs.insert("Exception", EXCEPTION_CLASS_STUB);
stubs.insert("RuntimeException", RUNTIME_EXCEPTION_CLASS_STUB);
let backend = Backend::new_test_with_stubs(stubs);
*backend.workspace_root().write() = Some(dir.path().to_path_buf());
*backend.psr4_mappings().write() = mappings;
(backend, dir)
}
pub fn create_psr4_workspace_with_enum_stubs(
composer_json: &str,
files: &[(&str, &str)],
) -> (Backend, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("failed to create temp dir");
fs::write(dir.path().join("composer.json"), composer_json)
.expect("failed to write composer.json");
for (rel_path, content) in files {
let full = dir.path().join(rel_path);
if let Some(parent) = full.parent() {
fs::create_dir_all(parent).expect("failed to create dirs");
}
fs::write(&full, content).expect("failed to write PHP file");
}
let (mappings, _vendor_dir) = phpantom_lsp::composer::parse_composer_json(dir.path());
let mut stubs: HashMap<&'static str, &'static str> = HashMap::new();
stubs.insert("UnitEnum", UNIT_ENUM_STUB);
stubs.insert("BackedEnum", BACKED_ENUM_STUB);
let backend = Backend::new_test_with_stubs(stubs);
*backend.workspace_root().write() = Some(dir.path().to_path_buf());
*backend.psr4_mappings().write() = mappings;
(backend, dir)
}
pub fn inject_phpstan_diag(
backend: &Backend,
uri: &str,
line: u32,
message: &str,
identifier: &str,
) -> Diagnostic {
let diag = Diagnostic {
range: Range {
start: Position::new(line, 0),
end: Position::new(line, 80),
},
severity: Some(DiagnosticSeverity::ERROR),
code: Some(NumberOrString::String(identifier.to_string())),
source: Some("PHPStan".to_string()),
message: message.to_string(),
..Default::default()
};
{
let mut cache = backend.phpstan_last_diags().lock();
cache.entry(uri.to_string()).or_default().push(diag.clone());
}
diag
}
pub fn get_code_actions_at(
backend: &Backend,
uri: &str,
content: &str,
line: u32,
character: u32,
) -> Vec<CodeActionOrCommand> {
let params = CodeActionParams {
text_document: TextDocumentIdentifier {
uri: uri.parse().unwrap(),
},
range: Range {
start: Position::new(line, character),
end: Position::new(line, character),
},
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)
}
pub fn get_code_actions_on_line(
backend: &Backend,
uri: &str,
content: &str,
line: u32,
) -> Vec<CodeActionOrCommand> {
let params = CodeActionParams {
text_document: TextDocumentIdentifier {
uri: uri.parse().unwrap(),
},
range: Range {
start: Position::new(line, 0),
end: Position::new(line, 80),
},
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)
}
pub fn find_action<'a>(actions: &'a [CodeActionOrCommand], prefix: &str) -> Option<&'a CodeAction> {
actions.iter().find_map(|a| match a {
CodeActionOrCommand::CodeAction(ca) if ca.title.starts_with(prefix) => Some(ca),
_ => None,
})
}
pub fn resolve_action(
backend: &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
}
pub fn extract_edits(action: &CodeAction) -> Vec<TextEdit> {
let edit = action.edit.as_ref().expect("action should have an edit");
let changes = edit.changes.as_ref().expect("edit should have changes");
changes.values().flat_map(|v| v.iter()).cloned().collect()
}
pub fn apply_edits(content: &str, edits: &[TextEdit]) -> String {
let mut result = content.to_string();
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))
});
for edit in sorted {
let start = lsp_pos_to_offset(&result, edit.range.start);
let end = lsp_pos_to_offset(&result, edit.range.end);
result.replace_range(start..end, &edit.new_text);
}
result
}
pub fn lsp_pos_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;
}
content.len()
}