@@ -1391,18 +1391,10 @@ public function getBody(): string { return ''; }
13911391
13921392class 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 ();
0 commit comments