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
3 changes: 3 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ jobs:
- script: |
cd e2e/bug-15231
../../bin/phpstan analyze
- script: |
cd e2e/bug-15482
../../bin/phpstan analyse
- script: |
cd e2e/bug-15102
composer install
Expand Down
4 changes: 4 additions & 0 deletions e2e/bug-15482/phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
parameters:
level: max
paths:
- src
3 changes: 3 additions & 0 deletions e2e/bug-15482/src/a_usage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

echo BAR;
8 changes: 8 additions & 0 deletions e2e/bug-15482/src/z_functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

function foo(): string
{
return stdClass::class;
}

const BAR = 'bar';
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ public function clean(string $contents, int $maxMatches): string
if ($inType) {
$typeLevel--;

if ($typeLevel === 0) {
if ($typeLevel <= 0) {
$typeLevel = 0;
$inType = false;
}
}
Expand Down Expand Up @@ -146,7 +147,10 @@ public function clean(string $contents, int $maxMatches): string
if (isset($this->typeConfig[$char])) {
$type = $this->typeConfig[$char];

if (substr($this->contents, $this->index, $type['length']) === $type['name']) {
if (
substr($this->contents, $this->index, $type['length']) === $type['name']
&& $this->match('~.\\b(?<![\\$:>])' . $type['name'] . '\\b~Ais', $match, $this->index - 1)
) {
if ($maxMatches === 1 && $this->match($type['pattern'], $match, $this->index - 1)) {
return $clean . $match[0];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ final class SymbolFinderInFilesTest extends PHPStanTestCase
*/
public static function dataFindSymbols(): iterable
{
yield 'class name expressions do not hide later global constants' => [
__DIR__ . '/data/symbol-finder/class-name-before-global-constant.php',
[
[],
['symbolfinderclassname', 'classname', 'symbolfinderanonymousclass', 'symbolfindernamedargument', 'symbolfindernamedargumentcall', 'symbolfinderspacedclassconstant'],
['AFTER_CLASS_NAME', 'AFTER_CLASS_PREFIX', 'AFTER_ANONYMOUS_CLASS', 'AFTER_NAMED_CLASS_ARGUMENT', 'AFTER_SPACED_CLASS_CONSTANT'],
],
];

yield 'namespaced constants do not leak a class entry' => [
__DIR__ . '/data/symbol-finder/namespaced-constants.php',
[
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php declare(strict_types = 1);

function symbolFinderClassName(): string
{
return \stdClass::class;
}

const AFTER_CLASS_NAME = 'found';

function className(): string
{
return 'not a class declaration';
}

const AFTER_CLASS_PREFIX = 'still found';

function symbolFinderAnonymousClass(): object
{
return new class {
public const MEMBER = 'not global';
};
}

const AFTER_ANONYMOUS_CLASS = 'also found';

function symbolFinderNamedArgument(string $class): string
{
return $class;
}

function symbolFinderNamedArgumentCall(): string
{
return symbolFinderNamedArgument(class: 'named argument');
}

const AFTER_NAMED_CLASS_ARGUMENT = 'found after named argument';

function symbolFinderSpacedClassConstant(): string
{
return \stdClass:: class;
}

const AFTER_SPACED_CLASS_CONSTANT = 'found after spaced class constant';
11 changes: 8 additions & 3 deletions turbo-ext/src/SymbolScan.h
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,8 @@ inline void PhpFileCleaner::clean(zend_long maxMatches, std::string &out)
if (inType) {
typeLevel--;

if (typeLevel == 0) {
if (typeLevel <= 0) {
typeLevel = 0;
inType = false;
}
}
Expand Down Expand Up @@ -448,8 +449,12 @@ inline void PhpFileCleaner::clean(zend_long maxMatches, std::string &out)
for (const TypeConfig &type : types) {
if (type.firstByte != c) continue;

if (index + type.length <= len && memcmp(contents + index, type.name, type.length) == 0) {
if (maxMatches == 1 && prevByteOpensKeyword(index)) {
if (index + type.length <= len
&& memcmp(contents + index, type.name, type.length) == 0
&& prevByteOpensKeyword(index)
&& (index + type.length == len || !isWordByte((unsigned char) contents[index + type.length]))
) {
if (maxMatches == 1) {
size_t end;
if (matchSpacesAndName(index + type.length, &end)) {
clean.append(contents + index - 1, end - (index - 1));
Expand Down
1 change: 1 addition & 0 deletions turbo-ext/tests/php-file-cleaner-corpus.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
'short echo tag' => "<?php\nclass A {}\n?>\n<?= 'class NotAClass' ?>\n<?php class B {}\n",
'anon class' => "<?php\n\$a = new class extends Foo implements Bar {};\nclass Real {}\n",
'property named class' => "<?php\nclass A { public \$class = 1; }\n\$x = Foo::class;\n\$y = \$obj->class;\n",
'class name before global constant' => "<?php\nfunction f() { return Foo::class; }\nconst AFTER_CLASS_NAME = 1;\nfunction className() {}\nconst AFTER_CLASS_PREFIX = 2;\n",
'function by reference' => "<?php\nfunction &refFn() {}\nfunction plain(): void {}\n",
'namespace braces' => "<?php\nnamespace A\\B { class C {} }\nnamespace D { const E = 1; }\n",
'no php tag' => "just text, no php at all\n",
Expand Down
1 change: 1 addition & 0 deletions turbo-ext/tests/symbol-finder-corpus.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
'interface const' => "<?php\ninterface I { const X = 1; }\n",
'enum const' => "<?php\nenum E: string { case A = 'a'; const X = 1; }\n",
'global constants' => "<?php\nconst A = 1;\nconst B = 2;\n",
'class name before global constant' => "<?php\nfunction f() { return Foo::class; }\nconst AFTER_CLASS_NAME = 1;\nfunction className() {}\nconst AFTER_CLASS_PREFIX = 2;\n",
'define' => "<?php\ndefine('FOO', 1);\ndefine(\"N\\\\BAR\", 2);\ndefine('N\\\\\\\\BAZ', 3);\n",
'define leading slash' => "<?php\n\\define('SLASHED', 1);\n",
'define with variable' => "<?php\ndefine(\$name, 1);\n\$x = 'function evil(';\n",
Expand Down
Loading