Skip to content

Commit 41f5cc9

Browse files
ondrejmirtesclaude
andcommitted
Do not export functions declared inside functions
The exported nodes of an analysed file are collected during the analysis, which also reaches the function declarations inside function bodies. When the file changes, the result cache restore parses it again with ExportedNodeVisitor, which does not look inside function bodies. The two lists never matched, so every edit of such a file looked like a symbol disappeared, and all files with errors were re-analysed with it. Inner named functions are not supported anyway (function.inner), so they are left out of the exported nodes. On WordPress core, appending a comment to wp-includes/canonical.php, which declares lowercase_octets() inside redirect_canonical(), re-analysed 400 files. Now it re-analyses 1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdwtgiEHwkF5BTz73jgHQQ
1 parent ec0d4b6 commit 41f5cc9

7 files changed

Lines changed: 64 additions & 0 deletions

File tree

‎.github/workflows/e2e-tests.yml‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,20 @@ jobs:
428428
../bashunit -a contains 'Result cache restored. 2 files will be reanalysed.' "$OUTPUT"
429429
../bashunit -a not_contains 'Bar.php:10:Instantiated class ResultCacheE2EScannedExportedNodes\NewDep not found.' "$OUTPUT"
430430
../bashunit -a contains 'Foo.php:10:Method ResultCacheE2EScannedExportedNodes\Foo::doFoo() should return int but returns string.' "$OUTPUT"
431+
- script: |
432+
cd e2e/result-cache-inner-function
433+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
434+
echo "$OUTPUT"
435+
../bashunit -a contains 'Outer.php:8:Inner named functions are not supported by PHPStan.' "$OUTPUT"
436+
../bashunit -a contains 'Other.php:10:Method ResultCacheE2EInnerFunction\Other::doOther() should return int but returns string.' "$OUTPUT"
437+
# A function declared inside another function is not an exported node, so an edit of the
438+
# file declaring it looks like no symbol appeared or disappeared: only the file itself is
439+
# re-analysed, not every file with errors.
440+
patch -b src/Outer.php < body.patch
441+
INFO=$(../../bin/phpstan result-cache-info --json)
442+
echo "$INFO"
443+
../bashunit -a equals 'true' "$(echo "$INFO" | jq --raw-output '.resultCacheUsed')"
444+
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
431445
- script: |
432446
cd e2e/result-cache-scanned-trait
433447
../../bin/phpstan analyse
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
--- src/Outer.php
2+
+++ src/Outer.php
3+
@@ -11,5 +11,7 @@
4+
}
5+
}
6+
7+
- return inner($s);
8+
+ $lower = inner($s);
9+
+
10+
+ return $lower;
11+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
parameters:
2+
level: 8
3+
tmpDir: tmp
4+
paths:
5+
- src
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace ResultCacheE2EInnerFunction;
4+
5+
class Other
6+
{
7+
8+
public function doOther(): int
9+
{
10+
return 'not an int';
11+
}
12+
13+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace ResultCacheE2EInnerFunction;
4+
5+
function outer(string $s): string
6+
{
7+
if (!function_exists('ResultCacheE2EInnerFunction\inner')) {
8+
function inner(string $s): string
9+
{
10+
return strtolower($s);
11+
}
12+
}
13+
14+
return inner($s);
15+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.*

‎src/Dependency/DependencyResolver.php‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,11 @@ public function resolveDependencies(Node $node, Scope $scope): ?NodeDependencies
164164
$this->collectNodeDependencies($node, $scope, $dependenciesReflections, $dependenciesFilePaths);
165165
}
166166

167+
// A function declared inside another function is not supported (function.inner), and the
168+
// restore does not look inside function bodies for exported nodes (ExportedNodeVisitor):
169+
// exporting it here would make every edit of its file look like a symbol disappeared.
167170
$exportedNode = ($nodeProfile & self::PROFILE_EXPORT) !== 0
171+
&& !($node instanceof Node\Stmt\Function_ && $scope->getFunction() !== null)
168172
? $this->exportedNodeResolver->resolve($node, $this->nameScopeTracker->getNamespaceUses())
169173
: null;
170174

0 commit comments

Comments
 (0)