Skip to content

Commit 692158c

Browse files
committed
Validate ignoreErrors regexes during DIC compilation (save time in each run)
1 parent 3b706cd commit 692158c

14 files changed

+326
-125
lines changed

‎conf/config.neon‎

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ extensions:
168168
rules: PHPStan\DependencyInjection\RulesExtension
169169
conditionalTags: PHPStan\DependencyInjection\ConditionalTagsExtension
170170
parametersSchema: PHPStan\DependencyInjection\ParametersSchemaExtension
171+
validateIgnoredErrors: PHPStan\DependencyInjection\ValidateIgnoredErrorsExtension
171172

172173
parametersSchema:
173174
bootstrapFiles: listOf(string())
@@ -484,11 +485,6 @@ services:
484485
fixerTmpDir: %fixerTmpDir%
485486
maximumNumberOfProcesses: %parallel.maximumNumberOfProcesses%
486487

487-
-
488-
class: PHPStan\Command\IgnoredRegexValidator
489-
arguments:
490-
parser: @regexParser
491-
492488
-
493489
class: PHPStan\Dependency\DependencyResolver
494490

@@ -1723,15 +1719,6 @@ services:
17231719
reflector: @betterReflectionReflector
17241720
autowired: false
17251721

1726-
regexParser:
1727-
class: Hoa\Compiler\Llk\Parser
1728-
factory: Hoa\Compiler\Llk\Llk::load(@regexGrammarStream)
1729-
1730-
regexGrammarStream:
1731-
class: Hoa\File\Read
1732-
arguments:
1733-
streamName: 'hoa://Library/Regex/Grammar.pp'
1734-
17351722
runtimeReflectionProvider:
17361723
class: PHPStan\Reflection\ReflectionProvider\ClassBlacklistReflectionProvider
17371724
arguments:

‎phpstan-baseline.neon‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ parameters:
9090
count: 1
9191
path: src/DependencyInjection/ParametersSchemaExtension.php
9292

93+
-
94+
message: "#^Method PHPStan\\\\DependencyInjection\\\\ValidateIgnoredErrorsExtension\\:\\:loadConfiguration\\(\\) throws checked exception Hoa\\\\Compiler\\\\Exception\\\\Exception but it's missing from the PHPDoc @throws tag\\.$#"
95+
count: 1
96+
path: src/DependencyInjection/ValidateIgnoredErrorsExtension.php
97+
98+
-
99+
message: "#^Method PHPStan\\\\DependencyInjection\\\\ValidateIgnoredErrorsExtension\\:\\:loadConfiguration\\(\\) throws checked exception Hoa\\\\File\\\\Exception\\\\Exception but it's missing from the PHPDoc @throws tag\\.$#"
100+
count: 1
101+
path: src/DependencyInjection/ValidateIgnoredErrorsExtension.php
102+
93103
-
94104
message: "#^Variable method call on PHPStan\\\\Reflection\\\\ClassReflection\\.$#"
95105
count: 2

‎src/Analyser/IgnoredErrorHelper.php‎

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,7 @@
44

55
use Nette\Utils\Json;
66
use Nette\Utils\JsonException;
7-
use Nette\Utils\RegexpException;
8-
use Nette\Utils\Strings;
9-
use PHPStan\Command\IgnoredRegexValidator;
107
use PHPStan\File\FileHelper;
11-
use function array_keys;
12-
use function array_map;
13-
use function count;
14-
use function implode;
158
use function is_array;
169
use function is_file;
1710
use function sprintf;
@@ -23,7 +16,6 @@ class IgnoredErrorHelper
2316
* @param (string|mixed[])[] $ignoreErrors
2417
*/
2518
public function __construct(
26-
private IgnoredRegexValidator $ignoredRegexValidator,
2719
private FileHelper $fileHelper,
2820
private array $ignoreErrors,
2921
private bool $reportUnmatchedIgnoredErrors,
@@ -73,48 +65,12 @@ public function initialize(): IgnoredErrorHelperResult
7365
'ignoreError' => $ignoreError,
7466
];
7567
}
76-
77-
$ignoreMessage = $ignoreError['message'];
78-
Strings::match('', $ignoreMessage);
79-
if (isset($ignoreError['count'])) {
80-
continue; // ignoreError coming from baseline will be correct
81-
}
82-
$validationResult = $this->ignoredRegexValidator->validate($ignoreMessage);
83-
$ignoredTypes = $validationResult->getIgnoredTypes();
84-
if (count($ignoredTypes) > 0) {
85-
$errors[] = $this->createIgnoredTypesError($ignoreMessage, $ignoredTypes);
86-
}
87-
88-
if ($validationResult->hasAnchorsInTheMiddle()) {
89-
$errors[] = $this->createAnchorInTheMiddleError($ignoreMessage);
90-
}
91-
92-
if ($validationResult->areAllErrorsIgnored()) {
93-
$errors[] = sprintf("Ignored error %s has an unescaped '%s' which leads to ignoring all errors. Use '%s' instead.", $ignoreMessage, $validationResult->getWrongSequence(), $validationResult->getEscapedWrongSequence());
94-
}
9568
} else {
9669
$otherIgnoreErrors[] = [
9770
'index' => $i,
9871
'ignoreError' => $ignoreError,
9972
];
100-
$ignoreMessage = $ignoreError;
101-
Strings::match('', $ignoreMessage);
102-
$validationResult = $this->ignoredRegexValidator->validate($ignoreMessage);
103-
$ignoredTypes = $validationResult->getIgnoredTypes();
104-
if (count($ignoredTypes) > 0) {
105-
$errors[] = $this->createIgnoredTypesError($ignoreMessage, $ignoredTypes);
106-
}
107-
108-
if ($validationResult->hasAnchorsInTheMiddle()) {
109-
$errors[] = $this->createAnchorInTheMiddleError($ignoreMessage);
110-
}
111-
112-
if ($validationResult->areAllErrorsIgnored()) {
113-
$errors[] = sprintf("Ignored error %s has an unescaped '%s' which leads to ignoring all errors. Use '%s' instead.", $ignoreMessage, $validationResult->getWrongSequence(), $validationResult->getEscapedWrongSequence());
114-
}
11573
}
116-
} catch (RegexpException $e) {
117-
$errors[] = $e->getMessage();
11874
} catch (JsonException $e) {
11975
$errors[] = $e->getMessage();
12076
}
@@ -123,24 +79,4 @@ public function initialize(): IgnoredErrorHelperResult
12379
return new IgnoredErrorHelperResult($this->fileHelper, $errors, $otherIgnoreErrors, $ignoreErrorsByFile, $this->ignoreErrors, $this->reportUnmatchedIgnoredErrors);
12480
}
12581

126-
/**
127-
* @param array<string, string> $ignoredTypes
128-
*/
129-
private function createIgnoredTypesError(string $regex, array $ignoredTypes): string
130-
{
131-
return sprintf(
132-
"Ignored error %s has an unescaped '|' which leads to ignoring more errors than intended. Use '\\|' instead.\n%s",
133-
$regex,
134-
sprintf(
135-
"It ignores all errors containing the following types:\n%s",
136-
implode("\n", array_map(static fn (string $typeDescription): string => sprintf('* %s', $typeDescription), array_keys($ignoredTypes))),
137-
),
138-
);
139-
}
140-
141-
private function createAnchorInTheMiddleError(string $regex): string
142-
{
143-
return sprintf("Ignored error %s has an unescaped anchor '$' in the middle. This leads to unintended behavior. Use '\\$' instead.", $regex);
144-
}
145-
14682
}

‎src/Command/CommandHelper.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use PHPStan\Command\Symfony\SymfonyStyle;
2121
use PHPStan\DependencyInjection\Container;
2222
use PHPStan\DependencyInjection\ContainerFactory;
23+
use PHPStan\DependencyInjection\InvalidIgnoredErrorPatternsException;
2324
use PHPStan\DependencyInjection\LoaderFactory;
2425
use PHPStan\DependencyInjection\NeonAdapter;
2526
use PHPStan\ExtensionInstaller\GeneratedConfig;
@@ -300,6 +301,13 @@ public static function begin(
300301
$errorOutput->writeLineFormatted('<error>Invalid configuration:</error>');
301302
$errorOutput->writeLineFormatted($e->getMessage());
302303
throw new InceptionNotSuccessfulException();
304+
} catch (InvalidIgnoredErrorPatternsException $e) {
305+
$errorOutput->writeLineFormatted(sprintf('<error>Invalid %s in ignoreErrors:</error>', count($e->getErrors()) === 1 ? 'entry' : 'entries'));
306+
foreach ($e->getErrors() as $error) {
307+
$errorOutput->writeLineFormatted($error);
308+
$errorOutput->writeLineFormatted('');
309+
}
310+
throw new InceptionNotSuccessfulException();
303311
} catch (ServiceCreationException $e) {
304312
$matches = Strings::match($e->getMessage(), '#Service of type (?<serviceType>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\\\\]*[a-zA-Z0-9_\x7f-\xff]): Service of type (?<parserServiceType>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\\\\]*[a-zA-Z0-9_\x7f-\xff]) needed by \$(?<parameterName>[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*) in (?<methodName>[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)\(\)#');
305313
if ($matches === null) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\DependencyInjection;
4+
5+
use Exception;
6+
use function implode;
7+
8+
class InvalidIgnoredErrorPatternsException extends Exception
9+
{
10+
11+
/**
12+
* @param string[] $errors
13+
*/
14+
public function __construct(private array $errors)
15+
{
16+
parent::__construct(implode("\n", $this->errors));
17+
}
18+
19+
/**
20+
* @return string[]
21+
*/
22+
public function getErrors(): array
23+
{
24+
return $this->errors;
25+
}
26+
27+
}

‎src/DependencyInjection/NeonAdapter.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
class NeonAdapter implements Adapter
2929
{
3030

31-
public const CACHE_KEY = 'v15-symfony-camel-case';
31+
public const CACHE_KEY = 'v16-ignored-errors-validate';
3232

3333
private const PREVENT_MERGING_SUFFIX = '!';
3434

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\DependencyInjection;
4+
5+
use Hoa\Compiler\Llk\Llk;
6+
use Hoa\File\Read;
7+
use Nette\DI\CompilerExtension;
8+
use Nette\Utils\RegexpException;
9+
use Nette\Utils\Strings;
10+
use PHPStan\Analyser\NameScope;
11+
use PHPStan\Command\IgnoredRegexValidator;
12+
use PHPStan\PhpDoc\DirectTypeNodeResolverExtensionRegistryProvider;
13+
use PHPStan\PhpDoc\TypeNodeResolver;
14+
use PHPStan\PhpDoc\TypeNodeResolverExtensionRegistry;
15+
use PHPStan\PhpDoc\TypeStringResolver;
16+
use PHPStan\PhpDocParser\Lexer\Lexer;
17+
use PHPStan\PhpDocParser\Parser\ConstExprParser;
18+
use PHPStan\PhpDocParser\Parser\TypeParser;
19+
use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider;
20+
use PHPStan\Reflection\ReflectionProvider\DummyReflectionProvider;
21+
use PHPStan\Reflection\ReflectionProviderStaticAccessor;
22+
use PHPStan\Type\DirectTypeAliasResolverProvider;
23+
use PHPStan\Type\Type;
24+
use PHPStan\Type\TypeAliasResolver;
25+
use function array_keys;
26+
use function array_map;
27+
use function count;
28+
use function implode;
29+
use function is_array;
30+
use function sprintf;
31+
32+
class ValidateIgnoredErrorsExtension extends CompilerExtension
33+
{
34+
35+
/**
36+
* @throws InvalidIgnoredErrorPatternsException
37+
*/
38+
public function loadConfiguration(): void
39+
{
40+
$parser = Llk::load(new Read('hoa://Library/Regex/Grammar.pp'));
41+
$reflectionProvider = new DummyReflectionProvider();
42+
ReflectionProviderStaticAccessor::registerInstance($reflectionProvider);
43+
$ignoredRegexValidator = new IgnoredRegexValidator(
44+
$parser,
45+
new TypeStringResolver(
46+
new Lexer(),
47+
new TypeParser(new ConstExprParser()),
48+
new TypeNodeResolver(
49+
new DirectTypeNodeResolverExtensionRegistryProvider(
50+
new class implements TypeNodeResolverExtensionRegistry {
51+
52+
public function getExtensions(): array
53+
{
54+
return [];
55+
}
56+
57+
},
58+
),
59+
new DirectReflectionProviderProvider($reflectionProvider),
60+
new DirectTypeAliasResolverProvider(new class implements TypeAliasResolver {
61+
62+
public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool
63+
{
64+
return false;
65+
}
66+
67+
public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type
68+
{
69+
return null;
70+
}
71+
72+
}),
73+
),
74+
),
75+
);
76+
77+
$builder = $this->getContainerBuilder();
78+
$ignoreErrors = $builder->parameters['ignoreErrors'];
79+
$errors = [];
80+
81+
foreach ($ignoreErrors as $ignoreError) {
82+
try {
83+
if (is_array($ignoreError)) {
84+
if (isset($ignoreError['count'])) {
85+
continue; // ignoreError coming from baseline will be correct
86+
}
87+
$ignoreMessage = $ignoreError['message'];
88+
} else {
89+
$ignoreMessage = $ignoreError;
90+
}
91+
92+
Strings::match('', $ignoreMessage);
93+
$validationResult = $ignoredRegexValidator->validate($ignoreMessage);
94+
$ignoredTypes = $validationResult->getIgnoredTypes();
95+
if (count($ignoredTypes) > 0) {
96+
$errors[] = $this->createIgnoredTypesError($ignoreMessage, $ignoredTypes);
97+
}
98+
99+
if ($validationResult->hasAnchorsInTheMiddle()) {
100+
$errors[] = $this->createAnchorInTheMiddleError($ignoreMessage);
101+
}
102+
103+
if ($validationResult->areAllErrorsIgnored()) {
104+
$errors[] = sprintf("Ignored error %s has an unescaped '%s' which leads to ignoring all errors. Use '%s' instead.", $ignoreMessage, $validationResult->getWrongSequence(), $validationResult->getEscapedWrongSequence());
105+
}
106+
} catch (RegexpException $e) {
107+
$errors[] = $e->getMessage();
108+
}
109+
}
110+
111+
if (count($errors) === 0) {
112+
return;
113+
}
114+
115+
throw new InvalidIgnoredErrorPatternsException($errors);
116+
}
117+
118+
/**
119+
* @param array<string, string> $ignoredTypes
120+
*/
121+
private function createIgnoredTypesError(string $regex, array $ignoredTypes): string
122+
{
123+
return sprintf(
124+
"Ignored error %s has an unescaped '|' which leads to ignoring more errors than intended. Use '\\|' instead.\n%s",
125+
$regex,
126+
sprintf(
127+
"It ignores all errors containing the following types:\n%s",
128+
implode("\n", array_map(static fn (string $typeDescription): string => sprintf('* %s', $typeDescription), array_keys($ignoredTypes))),
129+
),
130+
);
131+
}
132+
133+
private function createAnchorInTheMiddleError(string $regex): string
134+
{
135+
return sprintf("Ignored error %s has an unescaped anchor '$' in the middle. This leads to unintended behavior. Use '\\$' instead.", $regex);
136+
}
137+
138+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\PhpDoc;
4+
5+
class DirectTypeNodeResolverExtensionRegistryProvider implements TypeNodeResolverExtensionRegistryProvider
6+
{
7+
8+
public function __construct(private TypeNodeResolverExtensionRegistry $registry)
9+
{
10+
}
11+
12+
public function getRegistry(): TypeNodeResolverExtensionRegistry
13+
{
14+
return $this->registry;
15+
}
16+
17+
}

‎src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public function __construct(private Container $container)
1616
public function getRegistry(): TypeNodeResolverExtensionRegistry
1717
{
1818
if ($this->registry === null) {
19-
$this->registry = new TypeNodeResolverExtensionRegistry(
19+
$this->registry = new TypeNodeResolverExtensionAwareRegistry(
2020
$this->container->getByType(TypeNodeResolver::class),
2121
$this->container->getServicesByTag(TypeNodeResolverExtension::EXTENSION_TAG),
2222
);

0 commit comments

Comments
 (0)