Skip to content

Commit 0f13f8b

Browse files
committed
Improve phpdoc completion
1 parent ea1942a commit 0f13f8b

6 files changed

Lines changed: 3073 additions & 132 deletions

File tree

example.php

Lines changed: 151 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,18 +1391,10 @@ public function getBody(): string { return ''; }
13911391

13921392
class ForeachKeyDemo {
13931393
/**
1394-
* Object keys: SplObjectStorage<Request, HttpResponse>
1395-
*
1396-
* @param \SplObjectStorage<Request, HttpResponse> $storage
1394+
* @
13971395
*/
13981396
public function objectKeys(\SplObjectStorage $storage): void {
1399-
// $req resolves to Request, $res resolves to HttpResponse
1400-
foreach ($storage as $req => $res) {
1401-
$req->getUri(); // Resolved: Request::getUri()
1402-
$req->method; // Resolved: Request::$method
1403-
$res->statusCode; // Resolved: HttpResponse::$statusCode
1404-
$res->getBody(); // Resolved: HttpResponse::getBody()
1405-
}
1397+
throw new MyCoolExceptionThatsInTheFunction();
14061398
}
14071399

14081400
public function weakMapKeys(): void {
@@ -1764,6 +1756,155 @@ public function demo(): void {
17641756
$response->status; // Resolved: string
17651757
$response->code; // Resolved: int
17661758

1759+
// ─── Smart @throws Completion ───────────────────────────────────────────────
1760+
//
1761+
// When typing `@` inside a docblock for a function/method, PHPantomLSP
1762+
// analyses the function body for `throw new ExceptionType(…)` statements
1763+
// that are NOT caught by an enclosing try/catch block. It then suggests:
1764+
//
1765+
// @throws ExceptionType
1766+
//
1767+
// for each uncaught exception, filtering out types already documented.
1768+
// If the exception class isn't imported, an auto-import `use` statement
1769+
// is added as an additional edit.
1770+
1771+
class NotFoundException extends \RuntimeException {}
1772+
class ValidationException extends \RuntimeException {}
1773+
class AuthorizationException extends \RuntimeException {}
1774+
1775+
class ThrowsCompletionDemo
1776+
{
1777+
/**
1778+
* Smart @throws: both exceptions are uncaught, so both are suggested.
1779+
* But only if they are not already hinted.
1780+
*
1781+
* @param int $id
1782+
* @return array
1783+
* @throws NotFoundException
1784+
* @throws ValidationException
1785+
*/
1786+
public function findOrFail(int $id): array
1787+
{
1788+
if ($id < 0) {
1789+
throw new ValidationException('ID must be positive');
1790+
}
1791+
$result = $this->lookup($id);
1792+
if ($result === null) {
1793+
throw new NotFoundException('Record not found');
1794+
}
1795+
return $result;
1796+
}
1797+
1798+
/**
1799+
* Caught exceptions are excluded: RuntimeException is caught,
1800+
* so only AuthorizationException is suggested.
1801+
*
1802+
* @throws AuthorizationException
1803+
*/
1804+
public function safeOperation(): void
1805+
{
1806+
$this->checkPermissions(); // may throw AuthorizationException
1807+
1808+
try {
1809+
throw new \RuntimeException('transient error');
1810+
} catch (\RuntimeException $e) {
1811+
// handled — not suggested in @throws
1812+
}
1813+
1814+
if (!$this->isAuthorized()) {
1815+
throw new AuthorizationException('Forbidden');
1816+
}
1817+
}
1818+
1819+
/**
1820+
* Multi-catch: both InvalidArgumentException and RuntimeException
1821+
* are caught, so only LogicException is suggested.
1822+
*
1823+
* @throws \LogicException
1824+
*/
1825+
public function multiCatchDemo(): void
1826+
{
1827+
try {
1828+
throw new \InvalidArgumentException('bad');
1829+
throw new \RuntimeException('runtime');
1830+
} catch (\InvalidArgumentException | \RuntimeException $e) {
1831+
// both caught
1832+
}
1833+
1834+
throw new \LogicException('uncaught');
1835+
}
1836+
1837+
// ── Propagated @throws from called methods ──────────────────────
1838+
1839+
/**
1840+
* Calling $this->safeOperation() propagates its @throws tag.
1841+
* Typing `@` here suggests:
1842+
*
1843+
* @throws AuthorizationException
1844+
*/
1845+
public function delegatedWork(): void
1846+
{
1847+
$this->safeOperation();
1848+
}
1849+
1850+
/**
1851+
* Multiple called methods propagate their @throws independently.
1852+
*
1853+
* @throws AuthorizationException (propagated from lookup via findOrFail's body)
1854+
* @throws NotFoundException (propagated from safeOperation)
1855+
*/
1856+
public function fullProcess(int $id): void
1857+
{
1858+
$this->safeOperation();
1859+
$result = $this->lookup($id);
1860+
if ($result === null) {
1861+
throw new NotFoundException('not found');
1862+
}
1863+
}
1864+
1865+
// ── throw $this->method() — return type detection ───────────────
1866+
1867+
/**
1868+
* `throw $this->makeException()` detects the return type of the
1869+
* called method and suggests it as @throws.
1870+
*
1871+
* @throws ValidationException
1872+
*/
1873+
public function throwExpression(): void
1874+
{
1875+
throw $this->makeException();
1876+
}
1877+
1878+
/**
1879+
* @return ValidationException
1880+
*/
1881+
private function makeException(): ValidationException
1882+
{
1883+
return new ValidationException('factory-produced error');
1884+
}
1885+
1886+
/**
1887+
* `throw self::createError()` also works with static calls.
1888+
* Typing `@` here suggests:
1889+
* @throws AuthorizationException (return type of createError)
1890+
*
1891+
* @throws AuthorizationException
1892+
*/
1893+
public function throwStaticExpression(): void
1894+
{
1895+
throw self::createError();
1896+
}
1897+
1898+
private static function createError(): AuthorizationException
1899+
{
1900+
return new AuthorizationException('static factory');
1901+
}
1902+
1903+
private function lookup(int $id): ?array { return null; }
1904+
private function checkPermissions(): void {}
1905+
private function isAuthorized(): bool { return true; }
1906+
}
1907+
17671908
// Intersection with \stdClass (makes properties writable)
17681909
/** @var object{name: string, value: int}&\stdClass $obj */
17691910
$obj = getUnknownValue();

src/completion/handler.rs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,11 @@ impl Backend {
4646
if let Some(content) = content {
4747
let classes = classes.unwrap_or_default();
4848

49-
// ── PHPDoc tag completion ────────────────────────────────
50-
// When the user types `@` inside a `/** … */` docblock,
51-
// offer context-aware PHPDoc / PHPStan tag suggestions.
52-
if let Some(prefix) =
53-
crate::completion::phpdoc::extract_phpdoc_prefix(&content, position)
54-
{
55-
let context = crate::completion::phpdoc::detect_context(&content, position);
56-
let items = crate::completion::phpdoc::build_phpdoc_completions(
57-
&content, &prefix, context, position,
58-
);
59-
if !items.is_empty() {
60-
return Ok(Some(CompletionResponse::Array(items)));
61-
}
62-
}
63-
6449
// Gather the current file's `use` statement mappings and namespace
6550
// so the class_loader can resolve short names like `Resource` to
6651
// their fully-qualified equivalents like `Klarna\Rest\Resource`.
52+
// These are loaded early because PHPDoc `@throws` completion
53+
// needs them for auto-import edits.
6754
let file_use_map: HashMap<String, String> = if let Ok(map) = self.use_map.lock() {
6855
map.get(&uri).cloned().unwrap_or_default()
6956
} else {
@@ -76,6 +63,26 @@ impl Backend {
7663
None
7764
};
7865

66+
// ── PHPDoc tag completion ────────────────────────────────
67+
// When the user types `@` inside a `/** … */` docblock,
68+
// offer context-aware PHPDoc / PHPStan tag suggestions.
69+
if let Some(prefix) =
70+
crate::completion::phpdoc::extract_phpdoc_prefix(&content, position)
71+
{
72+
let context = crate::completion::phpdoc::detect_context(&content, position);
73+
let items = crate::completion::phpdoc::build_phpdoc_completions(
74+
&content,
75+
&prefix,
76+
context,
77+
position,
78+
&file_use_map,
79+
&file_namespace,
80+
);
81+
if !items.is_empty() {
82+
return Ok(Some(CompletionResponse::Array(items)));
83+
}
84+
}
85+
7986
// ── Named argument completion ───────────────────────────
8087
// When the cursor is inside the parentheses of a function or
8188
// method call, offer parameter names as `name:` completions.

0 commit comments

Comments
 (0)