mod change_visibility;
pub(crate) mod cursor_context;
mod extract_constant;
mod extract_function;
mod extract_variable;
mod generate_constructor;
mod generate_getter_setter;
mod generate_property_hooks;
pub(crate) mod implement_methods;
mod import_class;
mod inline_variable;
pub(crate) mod phpstan;
mod promote_constructor_param;
mod remove_unused_import;
pub(crate) use remove_unused_import::build_line_deletion_edit;
mod replace_deprecated;
mod simplify_null;
mod update_docblock;
use mago_span::HasSpan;
use mago_syntax::ast::class_like::member::ClassLikeMember;
use mago_syntax::ast::sequence::Sequence;
use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::*;
use crate::Backend;
pub(super) fn detect_indent_from_members<'a>(
members: &Sequence<'a, ClassLikeMember<'a>>,
content: &str,
) -> String {
if let Some(first) = members.first() {
let offset = first.span().start.offset as usize;
let line_start = content[..offset]
.rfind('\n')
.map(|pos| pos + 1)
.unwrap_or(0);
let line_prefix = &content[line_start..offset];
let indent: String = line_prefix
.chars()
.take_while(|c| c.is_whitespace())
.collect();
if !indent.is_empty() {
return indent;
}
}
" ".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct CodeActionData {
pub action_kind: String,
pub uri: String,
pub range: Range,
#[serde(default)]
pub extra: serde_json::Value,
}
impl Backend {
pub fn handle_code_action(
&self,
uri: &str,
content: &str,
params: &CodeActionParams,
) -> Vec<CodeActionOrCommand> {
let mut actions = Vec::new();
self.collect_import_class_actions(uri, content, params, &mut actions);
self.collect_remove_unused_import_actions(uri, content, params, &mut actions);
self.collect_implement_methods_actions(uri, content, params, &mut actions);
self.collect_replace_deprecated_actions(uri, content, params, &mut actions);
self.collect_phpstan_actions(uri, content, params, &mut actions);
self.collect_change_visibility_actions(uri, content, params, &mut actions);
self.collect_update_docblock_actions(uri, content, params, &mut actions);
self.collect_promote_constructor_param_actions(uri, content, params, &mut actions);
self.collect_generate_constructor_actions(uri, content, params, &mut actions);
self.collect_generate_getter_setter_actions(uri, content, params, &mut actions);
self.collect_generate_property_hook_actions(uri, content, params, &mut actions);
self.collect_extract_constant_actions(uri, content, params, &mut actions);
self.collect_extract_variable_actions(uri, content, params, &mut actions);
self.collect_extract_function_actions(uri, content, params, &mut actions);
self.collect_inline_variable_actions(uri, content, params, &mut actions);
self.collect_simplify_null_actions(uri, content, params, &mut actions);
actions
}
pub fn resolve_code_action(&self, mut action: CodeAction) -> (CodeAction, Option<String>) {
let data_value = match &action.data {
Some(v) => v.clone(),
None => return (action, None),
};
let data: CodeActionData = match serde_json::from_value(data_value) {
Ok(d) => d,
Err(_) => return (action, None),
};
let content = match self.get_file_content(&data.uri) {
Some(c) => c,
None => return (action, None),
};
let result = match data.action_kind.as_str() {
"phpstan.addThrows" => {
let edit = self.resolve_add_throws(&data, &content);
if edit.is_some() {
self.expand_sibling_checked_exception_diags(&data, &content, &mut action);
}
edit
}
"phpstan.removeThrows" => self.resolve_remove_throws(&data, &content),
"phpstan.addOverride" => self.resolve_add_override(&data, &content),
"phpstan.addIgnore" => self.resolve_add_ignore(&data, &content),
"phpstan.removeIgnore" => self.resolve_remove_ignore(&data, &content),
"phpstan.removeOverride" => self.resolve_remove_override(&data, &content),
"phpstan.addReturnTypeWillChange" => {
self.resolve_add_return_type_will_change(&data, &content)
}
"phpstan.fixPhpDocType.update" | "phpstan.fixPhpDocType.remove" => {
self.resolve_fix_phpdoc_type(&data, &content)
}
"phpstan.newStatic.addTag"
| "phpstan.newStatic.finalClass"
| "phpstan.newStatic.finalConstructor" => self.resolve_new_static(&data, &content),
"phpstan.fixPrefixedClass" => self.resolve_fix_prefixed_class(&data, &content),
"phpstan.removeAssert" => self.resolve_remove_assert(&data, &content),
"phpstan.fixReturnType.stripExpr"
| "phpstan.fixReturnType.changeTypeToActual"
| "phpstan.fixReturnType.changeType"
| "phpstan.fixReturnType.addType"
| "phpstan.fixReturnType.updateReturnType" => {
self.resolve_fix_return_type(&data, &content)
}
"phpstan.removeUnusedReturnType" => {
self.resolve_remove_unused_return_type(&data, &content)
}
"phpstan.addIterableType" => self.resolve_add_iterable_type(&data, &content),
"phpstan.removeUnreachable" => self.resolve_remove_unreachable(&data, &content),
"refactor.changeVisibility" => self.resolve_change_visibility(&data, &content),
"quickfix.removeUnusedImport" | "quickfix.removeAllUnusedImports" => {
self.resolve_remove_unused_import(&data, &content, action.diagnostics.as_deref())
}
"refactor.extractConstant" | "refactor.extractConstantAll" => {
self.resolve_extract_constant(&data, &content)
}
"refactor.extractVariable" | "refactor.extractVariableAll" => {
self.resolve_extract_variable(&data, &content)
}
"refactor.extractFunction" => self.resolve_extract_function(&data, &content),
"refactor.inlineVariable" => self.resolve_inline_variable(&data, &content),
_ => None,
};
if let Some(edit) = result {
action.edit = Some(edit);
}
let republish_uri = if let Some(ref diags) = action.diagnostics
&& !diags.is_empty()
&& action.edit.is_some()
{
if data.action_kind.starts_with("phpstan.")
|| data.action_kind == "refactor.changeVisibility"
{
self.clear_phpstan_diagnostics_after_resolve(&data.uri, diags);
}
{
let mut suppressed = self.diag_suppressed.lock();
suppressed.extend(diags.iter().cloned());
}
Some(data.uri.clone())
} else {
None
};
(action, republish_uri)
}
fn expand_sibling_checked_exception_diags(
&self,
data: &CodeActionData,
content: &str,
action: &mut CodeAction,
) {
use crate::code_actions::phpstan::add_throws::{
extract_exception_fqn, find_enclosing_function_line_range,
};
let diag_message = match data
.extra
.get("diagnostic_message")
.and_then(|v| v.as_str())
{
Some(m) => m,
None => return,
};
let exception_fqn = match extract_exception_fqn(diag_message) {
Some(fqn) => fqn,
None => return,
};
let diag_line = match data.extra.get("diagnostic_line").and_then(|v| v.as_u64()) {
Some(l) => l as usize,
None => return,
};
let (func_start, func_end) = match find_enclosing_function_line_range(content, diag_line) {
Some(range) => range,
None => return,
};
let existing_diags = action.diagnostics.get_or_insert_with(Vec::new);
let cache = self.phpstan_last_diags.lock();
let cached = match cache.get(&data.uri) {
Some(c) => c,
None => return,
};
for cached_d in cached {
let ident = match &cached_d.code {
Some(NumberOrString::String(s)) => s.as_str(),
_ => continue,
};
if ident != "missingType.checkedException" {
continue;
}
let cached_fqn: String = match extract_exception_fqn(&cached_d.message) {
Some(fqn) => fqn,
None => continue,
};
if !cached_fqn.eq_ignore_ascii_case(&exception_fqn) {
continue;
}
let line = cached_d.range.start.line as usize;
if line < func_start || line > func_end {
continue;
}
let already_present = existing_diags.iter().any(|d| {
d.range == cached_d.range
&& d.message == cached_d.message
&& d.code == cached_d.code
});
if already_present {
continue;
}
existing_diags.push(cached_d.clone());
}
}
fn clear_phpstan_diagnostics_after_resolve(&self, uri: &str, resolved_diags: &[Diagnostic]) {
let mut cache = self.phpstan_last_diags.lock();
if let Some(cached) = cache.get_mut(uri) {
cached.retain(|cached_d| {
!resolved_diags.iter().any(|resolved_d| {
cached_d.range == resolved_d.range
&& cached_d.message == resolved_d.message
&& cached_d.code == resolved_d.code
})
});
}
}
}
pub(crate) fn make_code_action_data(
action_kind: &str,
uri: &str,
range: &Range,
extra: serde_json::Value,
) -> serde_json::Value {
serde_json::to_value(CodeActionData {
action_kind: action_kind.to_string(),
uri: uri.to_string(),
range: *range,
extra,
})
.unwrap_or_default()
}