Skip to content
Open
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 @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Array-callable navigation.** Method-name strings in array callables — `[Controller::class, 'method']` and `[$object, 'method']` — now resolve like a real member reference. This makes go-to-definition, find-references, and rename work on Laravel controller actions such as `Route::get('/', [IndexPageController::class, 'indexPage'])`.
- **Array-callable method completion.** Typing inside the method-name string of an array callable (`[Controller::class, '|']`) now offers method name completions from the resolved class, including inherited and trait methods. Works with `Class::class` constants, `$this`, and typed variables. (thanks @calebdw)
- **Convert arrow function to closure.** A new `refactor.rewrite` code action converts arrow functions to anonymous closures (`fn($x) => $x * 2` to `function($x) { return $x * 2; }`). Variables from the outer scope are automatically captured via a `use()` clause. Preserves `static` and return type hints. Contributed by @calebdw in https://github.com/PHPantom-dev/phpantom_lsp/pull/191.
- **`@phpstan-sealed` tag support.** The `@phpstan-sealed FooClass|BarClass` PHPDoc tag is now recognized. Class names in the tag are treated as type references, preventing false "unused import" diagnostics. Docblock completion also offers the tag. (contributed by @calebdw)
- **Magic methods complete when implemented.** Magic methods declared on a class (`__invoke`, `__toString`, `__call`, and the rest) are now offered in member completion, so explicit calls like `$x->__invoke()` autocomplete and support go-to-definition. They are sorted below the regular methods so they never appear at the top of the list.
- **Staleness detection and auto-refresh.** The class index, function index, and constant index now stay fresh automatically. When PHP files are created or deleted outside the editor (e.g. `git checkout`, code generation), the indices update without a restart, and edits made outside the editor are reflected the next time the file is used. When `composer.json` or `composer.lock` changes (e.g. after `composer install`), vendor packages are rescanned automatically.
- **`#[ArrayShape]` attribute support.** Functions and methods annotated with `#[ArrayShape(["key" => "type", ...])]` (used by ~84 phpstorm-stubs entries) now produce array shape key completions, hover type info, and correct type resolution. Affects commonly used functions like `parse_url`, `stat`, `pathinfo`, `gc_status`, `getimagesize`, and `session_get_cookie_params`.
Expand Down
1 change: 1 addition & 0 deletions src/completion/phpdoc/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const TYPE_TAGS: &[&str] = &[
"phpstan-assert-if-false",
"phpstan-require-extends",
"phpstan-require-implements",
"phpstan-sealed",
"psalm-param",
"psalm-return",
];
Expand Down
5 changes: 5 additions & 0 deletions src/completion/phpdoc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,11 @@ const PHPSTAN_CLASS_TAGS: &[TagDef] = &[
detail: "PHPStan: require implementing a specific interface",
label: Some("@phpstan-require-implements InterfaceName"),
},
TagDef {
tag: "@phpstan-sealed",
detail: "PHPStan: restrict which classes may extend/implement this class",
label: Some("@phpstan-sealed ClassName|OtherClass"),
},
];

const PHPSTAN_PROPERTY_TAGS: &[TagDef] = &[];
Expand Down
1 change: 1 addition & 0 deletions src/diagnostics/unused_imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ const PHPDOC_TYPE_TAGS: &[&str] = &[
"@phpstan-implements",
"@phpstan-require-extends",
"@phpstan-require-implements",
"@phpstan-sealed",
"@psalm-extends",
"@psalm-implements",
];
Expand Down
12 changes: 12 additions & 0 deletions src/docblock/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,18 @@ mod tests {
);
}

#[test]
fn phpstan_sealed_tag_parsed() {
let doc = "/**\n * @phpstan-sealed FooClass|BarClass\n */";
let info = parse_docblock_for_tags(doc).expect("should parse");
assert_eq!(info.tags.len(), 1);
// mago-docblock doesn't have a dedicated variant for @phpstan-sealed,
// so it falls through to TagKind::Other.
assert_eq!(info.tags[0].kind, TagKind::Other);
assert_eq!(info.tags[0].name, "phpstan-sealed");
assert_eq!(info.tags[0].description, "FooClass|BarClass");
}

#[test]
fn multiline_return_description_uses_newlines() {
let doc = "/**\n * @return array an array containing all the elements of arr1\n * after applying the callback function to each one.\n */";
Expand Down
2 changes: 1 addition & 1 deletion src/symbol_map/docblock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ const TYPE_FIRST_KINDS: &[TagKind] = &[
/// listed here because `mago-docblock` now maps them to dedicated
/// `TagKind::PsalmReturn` / `PsalmParam` / `PsalmVar` variants (handled
/// in `TYPE_FIRST_KINDS` above).
const TYPE_FIRST_OTHER_NAMES: &[&str] = &[];
const TYPE_FIRST_OTHER_NAMES: &[&str] = &["phpstan-sealed"];

use crate::docblock::templates::{TEMPLATE_KINDS, variance_for};

Expand Down
37 changes: 37 additions & 0 deletions src/symbol_map/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,43 @@ fn docblock_phpstan_require_implements_produces_class_reference() {
}
}

#[test]
fn docblock_phpstan_sealed_produces_class_references() {
let php = concat!(
"<?php\n",
"use App\\FooClass;\n",
"use App\\BarClass;\n",
"/** @phpstan-sealed FooClass|BarClass */\n",
"class BaseClass {}\n"
);
let map = parse_and_extract(php);
let docblock_start = php.find("/**").unwrap();
let foo_in_doc = php[docblock_start..].find("FooClass").unwrap() + docblock_start;

let hit = map.lookup(foo_in_doc as u32);
assert!(
hit.is_some(),
"Should find FooClass in @phpstan-sealed docblock"
);
if let SymbolKind::ClassReference { ref name, .. } = hit.unwrap().kind {
assert_eq!(name, "FooClass");
} else {
panic!("Expected ClassReference for FooClass");
}

let bar_in_doc = php[docblock_start..].find("BarClass").unwrap() + docblock_start;
let hit2 = map.lookup(bar_in_doc as u32);
assert!(
hit2.is_some(),
"Should find BarClass in @phpstan-sealed docblock"
);
if let SymbolKind::ClassReference { ref name, .. } = hit2.unwrap().kind {
assert_eq!(name, "BarClass");
} else {
panic!("Expected ClassReference for BarClass");
}
}

#[test]
fn docblock_fqn_type() {
let php = concat!(
Expand Down
Loading