Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Remove `#[Override]` code action.** When PHPStan reports `method.override`, `property.override`, or `property.overrideAttribute` (the attribute is present but the member does not actually override anything, or `#[Override]` on properties is not supported in the current PHP version), a quickfix removes the `#[Override]` attribute. If the attribute shares a line with other attributes, only the `Override` token is removed. The diagnostic is eagerly cleared once the attribute is gone.
- **Add `#[\ReturnTypeWillChange]` code action.** When PHPStan reports `method.tentativeReturnType`, a quickfix inserts `#[\ReturnTypeWillChange]` above the method declaration with correct indentation. The diagnostic is eagerly cleared once the attribute is present.
- **Simplify with null coalescing / null-safe operator.** Ternary expressions that guard against `null` are detected and a code action offers to rewrite them. `isset($x) ? $x : $default` and `$x !== null ? $x : $default` become `$x ?? $default`. `$x !== null ? $x->foo() : null` becomes `$x?->foo()` (PHP 8.0+ only).
- **Completion and signature help for `new self`, `new static`, and `new parent`.** Inside a class, typing `new sel` offers `self` and `static` as keyword completions with constructor parameter snippets. `parent` is offered when the class has a parent. Signature help triggers when typing inside the parentheses of `new self(`, `new static(`, or `new parent(`. Contributed by @RemcoSmitsDev in https://github.com/AJenbo/phpantom_lsp/pull/51.
- **Fix PHPDoc type mismatch code actions.** When PHPStan reports that a `@return`, `@param`, or `@var` tag has a type incompatible with the native type hint (`return.phpDocType`, `parameter.phpDocType`, `property.phpDocType`), two quickfixes are offered: update the tag type to match the native type, or remove the tag entirely. The diagnostic is eagerly cleared after applying either fix.

### Changed
Expand Down
28 changes: 23 additions & 5 deletions src/completion/call_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,20 @@ impl Backend {
}
}

/// Resolve class name keywords (`self`, `static`, `parent`) to actual
/// class names in the context of the current class.
fn resolve_class_name_keyword(class_name: &str, current_class: Option<&ClassInfo>) -> String {
match class_name {
"self" | "static" => current_class
.map(|c| c.name.clone())
.unwrap_or_else(|| class_name.to_string()),
"parent" => current_class
.and_then(|c| c.parent_class.clone())
.unwrap_or_else(|| class_name.to_string()),
_ => class_name.to_string(),
}
}

/// Build a [`ResolvedCallableTarget`] for a constructor call.
///
/// Loads and merges the class, then extracts `__construct` parameters.
Expand Down Expand Up @@ -265,11 +279,15 @@ impl Backend {

match effective {
// ── Constructor: `new ClassName` or `new ClassName()` ────
SubjectExpr::NewExpr { class_name } => Self::resolve_constructor_callable(
class_name,
&class_loader,
&self.resolved_class_cache,
),
SubjectExpr::NewExpr { class_name } => {
let resolved_class_name =
Self::resolve_class_name_keyword(class_name, rctx.current_class);
Self::resolve_constructor_callable(
&resolved_class_name,
&class_loader,
&self.resolved_class_cache,
)
}

// ── Instance method call: `$subject->method(…)` ─────────
SubjectExpr::MethodCall { base, method } => {
Expand Down
108 changes: 104 additions & 4 deletions src/completion/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ use crate::completion::class_completion::{
use crate::completion::named_args::{NamedArgContext, parse_existing_args};
use crate::docblock::types::PHPDOC_TYPE_KEYWORDS;
use crate::symbol_map::SymbolKind;
use crate::types::ClassInfo;
use crate::types::{CompletionTarget, FileContext};
use crate::util::{find_class_at_offset, position_to_byte_offset, position_to_offset};

Expand Down Expand Up @@ -1089,6 +1090,94 @@ impl Backend {

// ─── Strategy: class / constant / function completion ────────────────

/// Build completion item for class keywords (`self`, `static`, `parent`)
/// in `new` expression contexts.
///
/// When the cursor is inside a class and typing `new s`, these keywords
/// should be offered alongside regular class names. If the current class
/// has a constructor, the completion includes parameter snippets.
fn build_class_keyword_completions(
&self,
prefix: &str,
current_class: Option<&ClassInfo>,
) -> Vec<CompletionItem> {
let mut items = Vec::new();

let Some(current_class) = current_class else {
return items;
};

let prefix_lower = prefix.to_lowercase();

for keyword in ["self", "static"] {
if !keyword.starts_with(&prefix_lower) {
continue;
}

let mut item = CompletionItem {
label: keyword.to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some("Instantiate current class".to_string()),
filter_text: Some(keyword.to_string()),
sort_text: Some(format!("0_{keyword}")),
..CompletionItem::default()
};

// Add constructor snippet if available
if let Some(ctor) = current_class
.methods
.iter()
.find(|m| m.name == "__construct")
{
let snippet =
crate::completion::builder::build_callable_snippet(keyword, &ctor.parameters);
item.insert_text = Some(snippet);
item.insert_text_format = Some(InsertTextFormat::SNIPPET);
} else {
item.insert_text = Some(format!("{}()$0", keyword));
item.insert_text_format = Some(InsertTextFormat::SNIPPET);
}

items.push(item);
}

// `parent` - reference the parent class
if "parent".starts_with(&prefix_lower)
&& let Some(parent_name) = &current_class.parent_class
{
let mut item = CompletionItem {
label: "parent".to_string(),
kind: Some(CompletionItemKind::KEYWORD),
detail: Some(format!("Instantiate parent class ({})", parent_name)),
filter_text: Some("parent".to_string()),
sort_text: Some("0_parent".to_string()),
..CompletionItem::default()
};

// Try to load parent class and get its constructor
if let Some(parent_cls) = self.find_or_load_class(parent_name) {
if let Some(ctor) = parent_cls.methods.iter().find(|m| m.name == "__construct") {
let snippet = crate::completion::builder::build_callable_snippet(
"parent",
&ctor.parameters,
);
item.insert_text = Some(snippet);
item.insert_text_format = Some(InsertTextFormat::SNIPPET);
} else {
item.insert_text = Some("parent()$0".to_string());
item.insert_text_format = Some(InsertTextFormat::SNIPPET);
}
} else {
item.insert_text = Some("parent()$0".to_string());
item.insert_text_format = Some(InsertTextFormat::SNIPPET);
}

items.push(item);
}

items
}

/// Try to offer class name, constant, and function completions.
///
/// When there is no `->` or `::` operator, check whether the user is
Expand Down Expand Up @@ -1327,14 +1416,25 @@ impl Backend {
// instanceof), only class names are valid — skip constants
// and functions.
if class_ctx.is_class_only() {
if class_items.is_empty() {
return None;
}
let items = if paren_follows_cursor(content, position) {
let mut items = if paren_follows_cursor(content, position) {
strip_snippet_parens(class_items)
} else {
class_items
};

// For `new` expressions, also offer `self`, `static`, and `parent`
// keywords when inside a class.
if class_ctx.is_new() {
let cursor_offset = position_to_offset(content, position);
let current_class = find_class_at_offset(&ctx.classes, cursor_offset);
let keyword_items = self.build_class_keyword_completions(&partial, current_class);
items.extend(keyword_items);
}

if items.is_empty() {
return None;
}

return Some(CompletionResponse::List(CompletionList {
is_incomplete: class_incomplete,
items,
Expand Down
2 changes: 1 addition & 1 deletion src/php_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ impl PhpType {
}
}
PhpType::Generic(name, args) if !args.is_empty() => {
let value = if name == "Generator" {
let value = if Self::short_name_of(name) == "Generator" {
// Generator<TKey, TValue, TSend, TReturn>: value is
// the 2nd param (index 1). When only one param is
// given, treat it as the value type.
Expand Down
Loading