Skip to content

Commit

Permalink
Validate ignoreErrors regexes during DIC compilation (save time in ea…
Browse files Browse the repository at this point in the history
…ch run)
  • Loading branch information
ondrejmirtes committed Mar 3, 2022
1 parent 3b706cd commit 692158c
Show file tree
Hide file tree
Showing 14 changed files with 326 additions and 125 deletions.
15 changes: 1 addition & 14 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ extensions:
rules: PHPStan\DependencyInjection\RulesExtension
conditionalTags: PHPStan\DependencyInjection\ConditionalTagsExtension
parametersSchema: PHPStan\DependencyInjection\ParametersSchemaExtension
validateIgnoredErrors: PHPStan\DependencyInjection\ValidateIgnoredErrorsExtension

parametersSchema:
bootstrapFiles: listOf(string())
Expand Down Expand Up @@ -484,11 +485,6 @@ services:
fixerTmpDir: %fixerTmpDir%
maximumNumberOfProcesses: %parallel.maximumNumberOfProcesses%

-
class: PHPStan\Command\IgnoredRegexValidator
arguments:
parser: @regexParser

-
class: PHPStan\Dependency\DependencyResolver

Expand Down Expand Up @@ -1723,15 +1719,6 @@ services:
reflector: @betterReflectionReflector
autowired: false

regexParser:
class: Hoa\Compiler\Llk\Parser
factory: Hoa\Compiler\Llk\Llk::load(@regexGrammarStream)

regexGrammarStream:
class: Hoa\File\Read
arguments:
streamName: 'hoa://Library/Regex/Grammar.pp'

runtimeReflectionProvider:
class: PHPStan\Reflection\ReflectionProvider\ClassBlacklistReflectionProvider
arguments:
Expand Down
10 changes: 10 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ parameters:
count: 1
path: src/DependencyInjection/ParametersSchemaExtension.php

-
message: "#^Method PHPStan\\\\DependencyInjection\\\\ValidateIgnoredErrorsExtension\\:\\:loadConfiguration\\(\\) throws checked exception Hoa\\\\Compiler\\\\Exception\\\\Exception but it's missing from the PHPDoc @throws tag\\.$#"
count: 1
path: src/DependencyInjection/ValidateIgnoredErrorsExtension.php

-
message: "#^Method PHPStan\\\\DependencyInjection\\\\ValidateIgnoredErrorsExtension\\:\\:loadConfiguration\\(\\) throws checked exception Hoa\\\\File\\\\Exception\\\\Exception but it's missing from the PHPDoc @throws tag\\.$#"
count: 1
path: src/DependencyInjection/ValidateIgnoredErrorsExtension.php

-
message: "#^Variable method call on PHPStan\\\\Reflection\\\\ClassReflection\\.$#"
count: 2
Expand Down
64 changes: 0 additions & 64 deletions src/Analyser/IgnoredErrorHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,7 @@

use Nette\Utils\Json;
use Nette\Utils\JsonException;
use Nette\Utils\RegexpException;
use Nette\Utils\Strings;
use PHPStan\Command\IgnoredRegexValidator;
use PHPStan\File\FileHelper;
use function array_keys;
use function array_map;
use function count;
use function implode;
use function is_array;
use function is_file;
use function sprintf;
Expand All @@ -23,7 +16,6 @@ class IgnoredErrorHelper
* @param (string|mixed[])[] $ignoreErrors
*/
public function __construct(
private IgnoredRegexValidator $ignoredRegexValidator,
private FileHelper $fileHelper,
private array $ignoreErrors,
private bool $reportUnmatchedIgnoredErrors,
Expand Down Expand Up @@ -73,48 +65,12 @@ public function initialize(): IgnoredErrorHelperResult
'ignoreError' => $ignoreError,
];
}

$ignoreMessage = $ignoreError['message'];
Strings::match('', $ignoreMessage);
if (isset($ignoreError['count'])) {
continue; // ignoreError coming from baseline will be correct
}
$validationResult = $this->ignoredRegexValidator->validate($ignoreMessage);
$ignoredTypes = $validationResult->getIgnoredTypes();
if (count($ignoredTypes) > 0) {
$errors[] = $this->createIgnoredTypesError($ignoreMessage, $ignoredTypes);
}

if ($validationResult->hasAnchorsInTheMiddle()) {
$errors[] = $this->createAnchorInTheMiddleError($ignoreMessage);
}

if ($validationResult->areAllErrorsIgnored()) {
$errors[] = sprintf("Ignored error %s has an unescaped '%s' which leads to ignoring all errors. Use '%s' instead.", $ignoreMessage, $validationResult->getWrongSequence(), $validationResult->getEscapedWrongSequence());
}
} else {
$otherIgnoreErrors[] = [
'index' => $i,
'ignoreError' => $ignoreError,
];
$ignoreMessage = $ignoreError;
Strings::match('', $ignoreMessage);
$validationResult = $this->ignoredRegexValidator->validate($ignoreMessage);
$ignoredTypes = $validationResult->getIgnoredTypes();
if (count($ignoredTypes) > 0) {
$errors[] = $this->createIgnoredTypesError($ignoreMessage, $ignoredTypes);
}

if ($validationResult->hasAnchorsInTheMiddle()) {
$errors[] = $this->createAnchorInTheMiddleError($ignoreMessage);
}

if ($validationResult->areAllErrorsIgnored()) {
$errors[] = sprintf("Ignored error %s has an unescaped '%s' which leads to ignoring all errors. Use '%s' instead.", $ignoreMessage, $validationResult->getWrongSequence(), $validationResult->getEscapedWrongSequence());
}
}
} catch (RegexpException $e) {
$errors[] = $e->getMessage();
} catch (JsonException $e) {
$errors[] = $e->getMessage();
}
Expand All @@ -123,24 +79,4 @@ public function initialize(): IgnoredErrorHelperResult
return new IgnoredErrorHelperResult($this->fileHelper, $errors, $otherIgnoreErrors, $ignoreErrorsByFile, $this->ignoreErrors, $this->reportUnmatchedIgnoredErrors);
}

/**
* @param array<string, string> $ignoredTypes
*/
private function createIgnoredTypesError(string $regex, array $ignoredTypes): string
{
return sprintf(
"Ignored error %s has an unescaped '|' which leads to ignoring more errors than intended. Use '\\|' instead.\n%s",
$regex,
sprintf(
"It ignores all errors containing the following types:\n%s",
implode("\n", array_map(static fn (string $typeDescription): string => sprintf('* %s', $typeDescription), array_keys($ignoredTypes))),
),
);
}

private function createAnchorInTheMiddleError(string $regex): string
{
return sprintf("Ignored error %s has an unescaped anchor '$' in the middle. This leads to unintended behavior. Use '\\$' instead.", $regex);
}

}
8 changes: 8 additions & 0 deletions src/Command/CommandHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use PHPStan\Command\Symfony\SymfonyStyle;
use PHPStan\DependencyInjection\Container;
use PHPStan\DependencyInjection\ContainerFactory;
use PHPStan\DependencyInjection\InvalidIgnoredErrorPatternsException;
use PHPStan\DependencyInjection\LoaderFactory;
use PHPStan\DependencyInjection\NeonAdapter;
use PHPStan\ExtensionInstaller\GeneratedConfig;
Expand Down Expand Up @@ -300,6 +301,13 @@ public static function begin(
$errorOutput->writeLineFormatted('<error>Invalid configuration:</error>');
$errorOutput->writeLineFormatted($e->getMessage());
throw new InceptionNotSuccessfulException();
} catch (InvalidIgnoredErrorPatternsException $e) {
$errorOutput->writeLineFormatted(sprintf('<error>Invalid %s in ignoreErrors:</error>', count($e->getErrors()) === 1 ? 'entry' : 'entries'));
foreach ($e->getErrors() as $error) {
$errorOutput->writeLineFormatted($error);
$errorOutput->writeLineFormatted('');
}
throw new InceptionNotSuccessfulException();
} catch (ServiceCreationException $e) {
$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]*)\(\)#');
if ($matches === null) {
Expand Down
27 changes: 27 additions & 0 deletions src/DependencyInjection/InvalidIgnoredErrorPatternsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);

namespace PHPStan\DependencyInjection;

use Exception;
use function implode;

class InvalidIgnoredErrorPatternsException extends Exception
{

/**
* @param string[] $errors
*/
public function __construct(private array $errors)
{
parent::__construct(implode("\n", $this->errors));
}

/**
* @return string[]
*/
public function getErrors(): array
{
return $this->errors;
}

}
2 changes: 1 addition & 1 deletion src/DependencyInjection/NeonAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
class NeonAdapter implements Adapter
{

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

private const PREVENT_MERGING_SUFFIX = '!';

Expand Down
138 changes: 138 additions & 0 deletions src/DependencyInjection/ValidateIgnoredErrorsExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php declare(strict_types = 1);

namespace PHPStan\DependencyInjection;

use Hoa\Compiler\Llk\Llk;
use Hoa\File\Read;
use Nette\DI\CompilerExtension;
use Nette\Utils\RegexpException;
use Nette\Utils\Strings;
use PHPStan\Analyser\NameScope;
use PHPStan\Command\IgnoredRegexValidator;
use PHPStan\PhpDoc\DirectTypeNodeResolverExtensionRegistryProvider;
use PHPStan\PhpDoc\TypeNodeResolver;
use PHPStan\PhpDoc\TypeNodeResolverExtensionRegistry;
use PHPStan\PhpDoc\TypeStringResolver;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\TypeParser;
use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider;
use PHPStan\Reflection\ReflectionProvider\DummyReflectionProvider;
use PHPStan\Reflection\ReflectionProviderStaticAccessor;
use PHPStan\Type\DirectTypeAliasResolverProvider;
use PHPStan\Type\Type;
use PHPStan\Type\TypeAliasResolver;
use function array_keys;
use function array_map;
use function count;
use function implode;
use function is_array;
use function sprintf;

class ValidateIgnoredErrorsExtension extends CompilerExtension
{

/**
* @throws InvalidIgnoredErrorPatternsException
*/
public function loadConfiguration(): void
{
$parser = Llk::load(new Read('hoa://Library/Regex/Grammar.pp'));
$reflectionProvider = new DummyReflectionProvider();
ReflectionProviderStaticAccessor::registerInstance($reflectionProvider);
$ignoredRegexValidator = new IgnoredRegexValidator(
$parser,
new TypeStringResolver(
new Lexer(),
new TypeParser(new ConstExprParser()),
new TypeNodeResolver(
new DirectTypeNodeResolverExtensionRegistryProvider(
new class implements TypeNodeResolverExtensionRegistry {

public function getExtensions(): array
{
return [];
}

},
),
new DirectReflectionProviderProvider($reflectionProvider),
new DirectTypeAliasResolverProvider(new class implements TypeAliasResolver {

public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool
{
return false;
}

public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type
{
return null;
}

}),
),
),
);

$builder = $this->getContainerBuilder();
$ignoreErrors = $builder->parameters['ignoreErrors'];
$errors = [];

foreach ($ignoreErrors as $ignoreError) {
try {
if (is_array($ignoreError)) {
if (isset($ignoreError['count'])) {
continue; // ignoreError coming from baseline will be correct
}
$ignoreMessage = $ignoreError['message'];
} else {
$ignoreMessage = $ignoreError;
}

Strings::match('', $ignoreMessage);
$validationResult = $ignoredRegexValidator->validate($ignoreMessage);
$ignoredTypes = $validationResult->getIgnoredTypes();
if (count($ignoredTypes) > 0) {
$errors[] = $this->createIgnoredTypesError($ignoreMessage, $ignoredTypes);
}

if ($validationResult->hasAnchorsInTheMiddle()) {
$errors[] = $this->createAnchorInTheMiddleError($ignoreMessage);
}

if ($validationResult->areAllErrorsIgnored()) {
$errors[] = sprintf("Ignored error %s has an unescaped '%s' which leads to ignoring all errors. Use '%s' instead.", $ignoreMessage, $validationResult->getWrongSequence(), $validationResult->getEscapedWrongSequence());
}
} catch (RegexpException $e) {
$errors[] = $e->getMessage();
}
}

if (count($errors) === 0) {
return;
}

throw new InvalidIgnoredErrorPatternsException($errors);
}

/**
* @param array<string, string> $ignoredTypes
*/
private function createIgnoredTypesError(string $regex, array $ignoredTypes): string
{
return sprintf(
"Ignored error %s has an unescaped '|' which leads to ignoring more errors than intended. Use '\\|' instead.\n%s",
$regex,
sprintf(
"It ignores all errors containing the following types:\n%s",
implode("\n", array_map(static fn (string $typeDescription): string => sprintf('* %s', $typeDescription), array_keys($ignoredTypes))),
),
);
}

private function createAnchorInTheMiddleError(string $regex): string
{
return sprintf("Ignored error %s has an unescaped anchor '$' in the middle. This leads to unintended behavior. Use '\\$' instead.", $regex);
}

}
17 changes: 17 additions & 0 deletions src/PhpDoc/DirectTypeNodeResolverExtensionRegistryProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);

namespace PHPStan\PhpDoc;

class DirectTypeNodeResolverExtensionRegistryProvider implements TypeNodeResolverExtensionRegistryProvider
{

public function __construct(private TypeNodeResolverExtensionRegistry $registry)
{
}

public function getRegistry(): TypeNodeResolverExtensionRegistry
{
return $this->registry;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public function __construct(private Container $container)
public function getRegistry(): TypeNodeResolverExtensionRegistry
{
if ($this->registry === null) {
$this->registry = new TypeNodeResolverExtensionRegistry(
$this->registry = new TypeNodeResolverExtensionAwareRegistry(
$this->container->getByType(TypeNodeResolver::class),
$this->container->getServicesByTag(TypeNodeResolverExtension::EXTENSION_TAG),
);
Expand Down
Loading

0 comments on commit 692158c

Please sign in to comment.