diff --git a/build/phpstan.neon b/build/phpstan.neon
index 63e9a82ace..61f9c9290c 100644
--- a/build/phpstan.neon
+++ b/build/phpstan.neon
@@ -22,17 +22,13 @@ parameters:
checkUninitializedProperties: true
checkMissingCallableSignature: true
excludePaths:
- - ../src/Reflection/SignatureMap/functionMap.php
- - ../src/Reflection/SignatureMap/functionMetadata.php
- ../tests/*/data/*
- ../tests/tmp/*
- ../tests/PHPStan/Analyser/nsrt/*
- ../tests/PHPStan/Analyser/traits/*
- ../tests/notAutoloaded/*
- - ../tests/PHPStan/Generics/functions.php
- ../tests/PHPStan/Reflection/UnionTypesTest.php
- ../tests/PHPStan/Reflection/MixedTypeTest.php
- - ../tests/PHPStan/Reflection/StaticTypeTest.php
- ../tests/e2e/magic-setter/*
- ../tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php
- ../tests/PHPStan/Command/IgnoredRegexValidatorTest.php
diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon
index 61482ac786..a924d8222a 100644
--- a/conf/bleedingEdge.neon
+++ b/conf/bleedingEdge.neon
@@ -59,5 +59,6 @@ parameters:
printfArrayParameters: true
preciseMissingReturn: true
validatePregQuote: true
+ noImplicitWildcard: true
stubFiles:
- ../stubs/bleedingEdge/Rule.stub
diff --git a/conf/config.neon b/conf/config.neon
index 8adebd5693..a6a75e7b59 100644
--- a/conf/config.neon
+++ b/conf/config.neon
@@ -94,6 +94,7 @@ parameters:
printfArrayParameters: false
preciseMissingReturn: false
validatePregQuote: false
+ noImplicitWildcard: false
fileExtensions:
- php
checkAdvancedIsset: false
@@ -269,6 +270,7 @@ extensions:
conditionalTags: PHPStan\DependencyInjection\ConditionalTagsExtension
parametersSchema: PHPStan\DependencyInjection\ParametersSchemaExtension
validateIgnoredErrors: PHPStan\DependencyInjection\ValidateIgnoredErrorsExtension
+ validateExcludePaths: PHPStan\DependencyInjection\ValidateExcludePathsExtension
rules:
- PHPStan\Rules\Debug\DumpTypeRule
@@ -676,6 +678,8 @@ services:
-
implement: PHPStan\File\FileExcluderRawFactory
+ arguments:
+ noImplicitWildcard: %featureToggles.noImplicitWildcard%
fileExcluderAnalyse:
class: PHPStan\File\FileExcluder
diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon
index 29efd1a8b3..a217bfc4e0 100644
--- a/conf/parametersSchema.neon
+++ b/conf/parametersSchema.neon
@@ -89,6 +89,7 @@ parametersSchema:
printfArrayParameters: bool()
preciseMissingReturn: bool()
validatePregQuote: bool()
+ noImplicitWildcard: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
diff --git a/src/Analyser/Ignore/IgnoredError.php b/src/Analyser/Ignore/IgnoredError.php
index 00cc1e7df8..ecbe6e1059 100644
--- a/src/Analyser/Ignore/IgnoredError.php
+++ b/src/Analyser/Ignore/IgnoredError.php
@@ -4,6 +4,7 @@
use Nette\Utils\Strings;
use PHPStan\Analyser\Error;
+use PHPStan\DependencyInjection\BleedingEdgeToggle;
use PHPStan\File\FileExcluder;
use PHPStan\File\FileHelper;
use PHPStan\ShouldNotHappenException;
@@ -85,7 +86,7 @@ public static function shouldIgnore(
}
if ($path !== null) {
- $fileExcluder = new FileExcluder($fileHelper, [$path]);
+ $fileExcluder = new FileExcluder($fileHelper, [$path], BleedingEdgeToggle::isBleedingEdge());
$isExcluded = $fileExcluder->isExcludedFromAnalysing($error->getFilePath());
if (!$isExcluded && $error->getTraitFilePath() !== null) {
return $fileExcluder->isExcludedFromAnalysing($error->getTraitFilePath());
diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php
index d38aee7f23..ff3bf242bd 100644
--- a/src/Command/CommandHelper.php
+++ b/src/Command/CommandHelper.php
@@ -18,6 +18,7 @@
use PHPStan\DependencyInjection\Container;
use PHPStan\DependencyInjection\ContainerFactory;
use PHPStan\DependencyInjection\DuplicateIncludedFilesException;
+use PHPStan\DependencyInjection\InvalidExcludePathsException;
use PHPStan\DependencyInjection\InvalidIgnoredErrorPatternsException;
use PHPStan\DependencyInjection\LoaderFactory;
use PHPStan\ExtensionInstaller\GeneratedConfig;
@@ -355,6 +356,13 @@ public static function begin(
$errorOutput->writeLineFormatted('');
}
throw new InceptionNotSuccessfulException();
+ } catch (InvalidExcludePathsException $e) {
+ $errorOutput->writeLineFormatted(sprintf('Invalid %s in excludePaths:', count($e->getErrors()) === 1 ? 'entry' : 'entries'));
+ foreach ($e->getErrors() as $error) {
+ $errorOutput->writeLineFormatted($error);
+ $errorOutput->writeLineFormatted('');
+ }
+ throw new InceptionNotSuccessfulException();
} catch (ValidationException $e) {
foreach ($e->getMessages() as $message) {
$errorOutput->writeLineFormatted('Invalid configuration:');
@@ -583,7 +591,7 @@ public static function begin(
$pathRoutingParser->setAnalysedFiles($files);
- $stubFilesExcluder = new FileExcluder($currentWorkingDirectoryFileHelper, $stubFilesProvider->getProjectStubFiles());
+ $stubFilesExcluder = new FileExcluder($currentWorkingDirectoryFileHelper, $stubFilesProvider->getProjectStubFiles(), true);
$files = array_values(array_filter($files, static fn (string $file) => !$stubFilesExcluder->isExcludedFromAnalysing($file)));
diff --git a/src/DependencyInjection/InvalidExcludePathsException.php b/src/DependencyInjection/InvalidExcludePathsException.php
new file mode 100644
index 0000000000..d230b21108
--- /dev/null
+++ b/src/DependencyInjection/InvalidExcludePathsException.php
@@ -0,0 +1,27 @@
+errors));
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+
+}
diff --git a/src/DependencyInjection/NeonAdapter.php b/src/DependencyInjection/NeonAdapter.php
index 15fb210b33..299bd18b3e 100644
--- a/src/DependencyInjection/NeonAdapter.php
+++ b/src/DependencyInjection/NeonAdapter.php
@@ -29,7 +29,7 @@
class NeonAdapter implements Adapter
{
- public const CACHE_KEY = 'v25-nette-di-again';
+ public const CACHE_KEY = 'v26-no-implicit-wildcard';
private const PREVENT_MERGING_SUFFIX = '!';
diff --git a/src/DependencyInjection/ValidateExcludePathsExtension.php b/src/DependencyInjection/ValidateExcludePathsExtension.php
new file mode 100644
index 0000000000..e98e512b31
--- /dev/null
+++ b/src/DependencyInjection/ValidateExcludePathsExtension.php
@@ -0,0 +1,68 @@
+getContainerBuilder();
+ if (!$builder->parameters['__validate']) {
+ return;
+ }
+
+ $excludePaths = $builder->parameters['excludePaths'];
+ if ($excludePaths === null) {
+ return;
+ }
+
+ $noImplicitWildcard = $builder->parameters['featureToggles']['noImplicitWildcard'];
+ if (!$noImplicitWildcard) {
+ return;
+ }
+
+ $paths = [];
+ if (array_key_exists('analyse', $excludePaths)) {
+ $paths = $excludePaths['analyse'];
+ }
+ if (array_key_exists('analyseAndScan', $excludePaths)) {
+ $paths = array_merge($paths, $excludePaths['analyseAndScan']);
+ }
+
+ $errors = [];
+ foreach (array_unique($paths) as $path) {
+ if (is_dir($path)) {
+ continue;
+ }
+ if (is_file($path)) {
+ continue;
+ }
+ if (FileExcluder::isFnmatchPattern($path)) {
+ continue;
+ }
+
+ $errors[] = sprintf('Path %s is neither a directory, nor a file path, nor a fnmatch pattern.', $path);
+ }
+
+ if (count($errors) === 0) {
+ return;
+ }
+
+ throw new InvalidExcludePathsException($errors);
+ }
+
+}
diff --git a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php
index cd93ab2247..572e7c6dcd 100644
--- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php
+++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php
@@ -11,6 +11,7 @@
use PHPStan\Analyser\NameScope;
use PHPStan\Command\IgnoredRegexValidator;
use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider;
+use PHPStan\File\FileExcluder;
use PHPStan\Php\PhpVersion;
use PHPStan\PhpDoc\DirectTypeNodeResolverExtensionRegistryProvider;
use PHPStan\PhpDoc\TypeNodeResolver;
@@ -34,6 +35,8 @@
use function count;
use function implode;
use function is_array;
+use function is_dir;
+use function is_file;
use function sprintf;
use const PHP_VERSION_ID;
@@ -55,6 +58,8 @@ public function loadConfiguration(): void
return;
}
+ $noImplicitWildcard = $builder->parameters['featureToggles']['noImplicitWildcard'];
+
/** @throws void */
$parser = Llk::load(new Read(__DIR__ . '/../../resources/RegexGrammar.pp'));
$reflectionProvider = new DummyReflectionProvider();
@@ -131,6 +136,36 @@ public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry
}
}
+ if ($noImplicitWildcard) {
+ foreach ($ignoreErrors as $ignoreError) {
+ if (!is_array($ignoreError)) {
+ continue;
+ }
+
+ if (isset($ignoreError['path'])) {
+ $ignorePaths = [$ignoreError['path']];
+ } elseif (isset($ignoreError['paths'])) {
+ $ignorePaths = $ignoreError['paths'];
+ } else {
+ continue;
+ }
+
+ foreach ($ignorePaths as $ignorePath) {
+ if (is_dir($ignorePath)) {
+ continue;
+ }
+ if (is_file($ignorePath)) {
+ continue;
+ }
+ if (FileExcluder::isFnmatchPattern($ignorePath)) {
+ continue;
+ }
+
+ $errors[] = sprintf('Path %s is neither a directory, nor a file path, nor a fnmatch pattern.', $ignorePath);
+ }
+ }
+ }
+
if (count($errors) === 0) {
return;
}
diff --git a/src/File/FileExcluder.php b/src/File/FileExcluder.php
index 2ea5271d5d..7ebf1aa20a 100644
--- a/src/File/FileExcluder.php
+++ b/src/File/FileExcluder.php
@@ -4,6 +4,8 @@
use function fnmatch;
use function in_array;
+use function is_dir;
+use function is_file;
use function preg_match;
use function str_starts_with;
use function strlen;
@@ -15,12 +17,26 @@ class FileExcluder
{
/**
- * Directories to exclude from analysing
+ * Paths to exclude from analysing
*
* @var string[]
*/
private array $literalAnalyseExcludes = [];
+ /**
+ * Directories to exclude from analysing
+ *
+ * @var string[]
+ */
+ private array $literalAnalyseDirectoryExcludes = [];
+
+ /**
+ * Files to exclude from analysing
+ *
+ * @var string[]
+ */
+ private array $literalAnalyseFilesExcludes = [];
+
/**
* fnmatch() patterns to use for excluding files and directories from analysing
* @var string[]
@@ -35,6 +51,7 @@ class FileExcluder
public function __construct(
FileHelper $fileHelper,
array $analyseExcludes,
+ private bool $noImplicitWildcard,
)
{
foreach ($analyseExcludes as $exclude) {
@@ -47,10 +64,22 @@ public function __construct(
$normalized .= DIRECTORY_SEPARATOR;
}
- if ($this->isFnmatchPattern($normalized)) {
+ if (self::isFnmatchPattern($normalized)) {
$this->fnmatchAnalyseExcludes[] = $normalized;
} else {
- $this->literalAnalyseExcludes[] = $fileHelper->absolutizePath($normalized);
+ if ($this->noImplicitWildcard) {
+ if (is_file($normalized)) {
+ $this->literalAnalyseFilesExcludes[] = $normalized;
+ } elseif (is_dir($normalized)) {
+ if (!$trailingDirSeparator) {
+ $normalized .= DIRECTORY_SEPARATOR;
+ }
+
+ $this->literalAnalyseDirectoryExcludes[] = $normalized;
+ }
+ } else {
+ $this->literalAnalyseExcludes[] = $fileHelper->absolutizePath($normalized);
+ }
}
}
@@ -69,6 +98,18 @@ public function isExcludedFromAnalysing(string $file): bool
return true;
}
}
+ if ($this->noImplicitWildcard) {
+ foreach ($this->literalAnalyseDirectoryExcludes as $exclude) {
+ if (str_starts_with($file, $exclude)) {
+ return true;
+ }
+ }
+ foreach ($this->literalAnalyseFilesExcludes as $exclude) {
+ if ($file === $exclude) {
+ return true;
+ }
+ }
+ }
foreach ($this->fnmatchAnalyseExcludes as $exclude) {
if (fnmatch($exclude, $file, $this->fnmatchFlags)) {
return true;
@@ -78,7 +119,7 @@ public function isExcludedFromAnalysing(string $file): bool
return false;
}
- private function isFnmatchPattern(string $path): bool
+ public static function isFnmatchPattern(string $path): bool
{
return preg_match('~[*?[\]]~', $path) > 0;
}
diff --git a/tests/PHPStan/File/FileExcluderTest.php b/tests/PHPStan/File/FileExcluderTest.php
index eebe8236e6..7477416b6c 100644
--- a/tests/PHPStan/File/FileExcluderTest.php
+++ b/tests/PHPStan/File/FileExcluderTest.php
@@ -19,7 +19,7 @@ public function testFilesAreExcludedFromAnalysingOnWindows(
{
$this->skipIfNotOnWindows();
- $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes);
+ $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes, false);
$this->assertSame($isExcluded, $fileExcluder->isExcludedFromAnalysing($filePath));
}
@@ -127,7 +127,7 @@ public function testFilesAreExcludedFromAnalysingOnUnix(
{
$this->skipIfNotOnUnix();
- $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes);
+ $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes, false);
$this->assertSame($isExcluded, $fileExcluder->isExcludedFromAnalysing($filePath));
}
@@ -208,4 +208,70 @@ public function dataExcludeOnUnix(): array
];
}
+ public function dataNoImplicitWildcard(): iterable
+ {
+ yield [
+ __DIR__ . '/tests/foo.php',
+ [
+ __DIR__ . '/test',
+ ],
+ false,
+ true,
+ ];
+
+ yield [
+ __DIR__ . '/tests/foo.php',
+ [
+ __DIR__ . '/test',
+ ],
+ true,
+ false,
+ ];
+
+ yield [
+ __DIR__ . '/test/foo.php',
+ [
+ __DIR__ . '/test',
+ ],
+ true,
+ true,
+ ];
+
+ yield [
+ __DIR__ . '/FileExcluderTest.php',
+ [
+ __DIR__ . '/FileExcluderTest.php',
+ ],
+ true,
+ true,
+ ];
+
+ yield [
+ __DIR__ . '/tests/foo.php',
+ [
+ __DIR__ . '/test*',
+ ],
+ true,
+ true,
+ ];
+ }
+
+ /**
+ * @dataProvider dataNoImplicitWildcard
+ * @param string[] $analyseExcludes
+ */
+ public function testNoImplicitWildcard(
+ string $filePath,
+ array $analyseExcludes,
+ bool $noImplicitWildcard,
+ bool $isExcluded,
+ ): void
+ {
+ $this->skipIfNotOnUnix();
+
+ $fileExcluder = new FileExcluder($this->getFileHelper(), $analyseExcludes, $noImplicitWildcard);
+
+ $this->assertSame($isExcluded, $fileExcluder->isExcludedFromAnalysing($filePath));
+ }
+
}
diff --git a/tests/PHPStan/File/test/.gitkeep b/tests/PHPStan/File/test/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2