Skip to content

ContextType Rules - take[2] Introduce TypeProviderResolver to detect types for each scope #87

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 27, 2025
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ includes:
- vendor/struggle-for-php/sfp-phpstan-psr-log/extension.neon
```

and, set parameters `enableLogLevelMethodRule` and `enableContextTypeRule`
and, set parameters `enableLogMethodLevelRule` and `enableContextTypeRule`

```neon
parameters:
sfpPsrLog:
enableLogLevelMethodRule: true # default:false
enableLogMethodLevelRule: true # default:false
enableContextTypeRule: true # default:false
```

Expand Down
3 changes: 2 additions & 1 deletion composer-require-checker.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"PHPStan\\Type\\FloatType",
"PHPStan\\Type\\IntegerType",
"PHPStan\\Type\\StringType",
"PHPStan\\Type\\TypeCombinator"
"PHPStan\\Type\\TypeCombinator",
"PHPStan\\Reflection\\ClassReflection"
]
}
2 changes: 1 addition & 1 deletion example/phpstan.default.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# docker run -v $(realpath .):/github/workspace -w=/github/workspace ghcr.io/laminas/laminas-continuous-integration:1 \
# '{"php":"8.2","dependencies":"latest","extensions":[],"ini":["memory_limit=-1"],"command":"./vendor/bin/phpstan analyse -c ./example/phpstan.default.neon --no-progress --error-format=junit | xmllint --format -"}'
# docker run -v $(realpath .):/github/workspace -w=/github/workspace ghcr.io/laminas/laminas-continuous-integration:1 \
# '{"php":"8.2","dependencies":"latest","extensions":[],"ini":["memory_limit=-1"],"command":"diff <(./vendor/bin/phpstan analyse -c ./example/phpstan.default.neon --no-progress --error-format=junit | xmllint --format -) ./test/example.output"}'
# '{"php":"8.2","dependencies":"latest","extensions":[],"ini":["memory_limit=-1"],"command":"diff <(./vendor/bin/phpstan analyse -c ./example/phpstan.default.neon --no-progress --error-format=junit | xmllint --format -) ./test/example.default.output"}'

parameters:
level: 5
Expand Down
4 changes: 2 additions & 2 deletions example/phpstan.enableContextTypeRule.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# docker run -v $(realpath .):/github/workspace -w=/github/workspace ghcr.io/laminas/laminas-continuous-integration:1 \
# '{"php":"8.2","dependencies":"latest","extensions":[],"ini":["memory_limit=-1"],"command":"./vendor/bin/phpstan analyse -c ./example/phpstan.enableContextTypeRule.neon --no-progress --error-format=junit | xmllint --format -"}'
# docker run -v $(realpath .):/github/workspace -w=/github/workspace ghcr.io/laminas/laminas-continuous-integration:1 \
# '{"php":"8.2","dependencies":"latest","extensions":[],"ini":["memory_limit=-1"],"command":"diff <(./vendor/bin/phpstan analyse -c ./example/phpstan.enableContextTypeRule.neon --no-progress --error-format=junit | xmllint --format -) ./test/example.output"}'
# '{"php":"8.2","dependencies":"latest","extensions":[],"ini":["memory_limit=-1"],"command":"diff <(./vendor/bin/phpstan analyse -c ./example/phpstan.enableContextTypeRule.neon --no-progress --error-format=junit | xmllint --format -) ./test/example.enableContextTypeRule.output"}'

parameters:
level: 5
Expand All @@ -13,7 +13,7 @@ parameters:
paths:
- %currentWorkingDirectory%/example/src
sfpPsrLog:
enableLogLevelMethodRule: true
enableLogMethodLevelRule: true
enableContextTypeRule: true

# services:
Expand Down
2 changes: 1 addition & 1 deletion example/phpstan.recommend.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# docker run -v $(realpath .):/github/workspace -w=/github/workspace ghcr.io/laminas/laminas-continuous-integration:1 \
# '{"php":"8.2","dependencies":"latest","extensions":[],"ini":["memory_limit=-1"],"command":"./vendor/bin/phpstan analyse -c ./example/phpstan.recommend.neon --no-progress --error-format=junit | xmllint --format -"}'
# docker run -v $(realpath .):/github/workspace -w=/github/workspace ghcr.io/laminas/laminas-continuous-integration:1 \
# '{"php":"8.2","dependencies":"latest","extensions":[],"ini":["memory_limit=-1"],"command":"diff <(./vendor/bin/phpstan analyse -c ./example/phpstan.recommend.neon --no-progress --error-format=junit | xmllint --format -) ./test/example.output"}'
# '{"php":"8.2","dependencies":"latest","extensions":[],"ini":["memory_limit=-1"],"command":"diff <(./vendor/bin/phpstan analyse -c ./example/phpstan.recommend.neon --no-progress --error-format=junit | xmllint --format -) ./test/example.recommend.output"}'

parameters:
level: 5
Expand Down
36 changes: 19 additions & 17 deletions phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,28 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/squizlabs/php_codesniffer/phpcs.xsd">

<arg name="basepath" value="."/>
<arg name="cache" value=".phpcs-cache"/>
<arg name="colors"/>
<arg name="extensions" value="php"/>
<arg name="parallel" value="80"/>
<arg name="basepath" value="."/>
<arg name="cache" value=".phpcs-cache"/>
<arg name="colors"/>
<arg name="extensions" value="php"/>
<arg name="parallel" value="80"/>

<!-- Show progress -->
<arg value="p"/>
<!-- Show progress -->
<arg value="p"/>

<!-- Paths to check -->
<file>src</file>
<file>test</file>
<!-- Paths to check -->
<file>src</file>
<file>test</file>

<exclude-pattern>test/TypeProvider/data/*</exclude-pattern>
<exclude-pattern>test/TypeProvider/data/*</exclude-pattern>

<!-- Include all rules from Laminas Coding Standard -->
<rule ref="LaminasCodingStandard"/>
<!-- Include all rules from Laminas Coding Standard -->
<rule ref="LaminasCodingStandard">
<exclude name="SlevomatCodingStandard.Commenting.ForbiddenAnnotations" />
</rule>

<rule ref="PSR12">
<exclude name="Generic.Files.LineLength"/>
<exclude name="WebimpressCodingStandard.Formatting.StringClassReference" />
</rule>
<rule ref="PSR12">
<exclude name="Generic.Files.LineLength"/>
<exclude name="WebimpressCodingStandard.Formatting.StringClassReference" />
</rule>
</ruleset>
29 changes: 17 additions & 12 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ parametersSchema:
enableMessageStaticStringRule: bool(),
reportContextExceptionLogLevel: schema(string(), nullable()),
contextKeyOriginalPattern: schema(string(), nullable()),
enableLogLevelMethodRule: bool()
enableLogMethodLevelRule: bool()
enableContextTypeRule: bool()
])

Expand All @@ -14,7 +14,7 @@ parameters:
enableMessageStaticStringRule: true
reportContextExceptionLogLevel: 'debug'
contextKeyOriginalPattern: null
enableLogLevelMethodRule: false
enableLogMethodLevelRule: false
enableContextTypeRule: false

conditionalTags:
Expand All @@ -23,7 +23,7 @@ conditionalTags:
Sfp\PHPStan\Psr\Log\Rules\MessageStaticStringRule:
phpstan.rules.rule: %sfpPsrLog.enableMessageStaticStringRule%
Sfp\PHPStan\Psr\Log\Rules\LogMethodLevelRule:
phpstan.rules.rule: %sfpPsrLog.enableLogLevelMethodRule%
phpstan.rules.rule: %sfpPsrLog.enableLogMethodLevelRule%
Sfp\PHPStan\Psr\Log\Rules\ContextTypeRule:
phpstan.rules.rule: %sfpPsrLog.enableContextTypeRule%

Expand Down Expand Up @@ -51,20 +51,25 @@ services:

-
class: Sfp\PHPStan\Psr\Log\Rules\ContextTypeRule
arguments:
contextTypeProvider: @sfpPsrLogContextTypeProvider

sfpPsrLogContextTypeProvider :
class: Sfp\PHPStan\Psr\Log\TypeProvider\Psr3ContextTypeProvider
arguments:
exceptionClass: '\Throwable'
# -
# class: Sfp\PHPStan\Psr\Log\Rules\ContextTypeRule
# arguments:
# contextTypeProviderResolver: @contextTypeProviderResolver


# # BigQuery
# contextTypeProviderResolver:

# psrLogContextTypeProvider :
# class: Sfp\PHPStan\Psr\Log\TypeProvider\Psr3ContextTypeProvider
# arguments:
# exceptionClass: '\Exception'

# BigQuery
#
# sfpPsrLogContextTypeProvider :
# psrLogContextTypeProvider :
# class: Sfp\PHPStan\Psr\Log\TypeProvider\BigQueryContextTypeProvider
# arguments:
# schemaFile:

# -
# class: Sfp\PHPStan\Psr\Log\TypeMapping\BigQuery\GenericTableFieldSchemaJsonPayloadTypeMapper
32 changes: 9 additions & 23 deletions src/Rules/ContextTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\ObjectType;
use Sfp\PHPStan\Psr\Log\TypeProvider\ContextTypeProviderInterface;
use Sfp\PHPStan\Psr\Log\TypeProvider\Psr3ContextTypeProvider;
use Sfp\PHPStan\Psr\Log\TypeProviderResolver\AnyScopeContextTypeProviderResolver;
use Sfp\PHPStan\Psr\Log\TypeProviderResolver\ContextTypeProviderResolverInterface;

use function count;
use function in_array;
Expand All @@ -22,12 +23,12 @@
*/
final class ContextTypeRule implements Rule
{
/** @var ContextTypeProviderInterface */
private $contextTypeProvider;
/** @var ContextTypeProviderResolverInterface */
private $contextTypeProviderResolver;

public function __construct(?ContextTypeProviderInterface $contextTypeProvider = null)
public function __construct(?ContextTypeProviderResolverInterface $contextTypeProviderResolver)
{
$this->contextTypeProvider = $contextTypeProvider ?? new Psr3ContextTypeProvider();
$this->contextTypeProviderResolver = $contextTypeProviderResolver ?? new AnyScopeContextTypeProviderResolver(new Psr3ContextTypeProvider());
}

public function getNodeType(): string
Expand Down Expand Up @@ -62,22 +63,6 @@ public function processNode(Node $node, Scope $scope): array

$contextArgumentNo = 1;
if ($methodName === 'log') {
if (count($args) < 2) {
return [];
}

$logLevelType = $scope->getType($args[0]->value);

$logLevels = [];
foreach ($logLevelType->getConstantStrings() as $constantString) {
$logLevels[] = $constantString->getValue();
}

if (count($logLevels) === 0) {
// cant find logLevels
return [];
}

$contextArgumentNo = 2;
} elseif (! in_array($methodName, LogLevelListInterface::LOGGER_LEVEL_METHODS, true)) {
return [];
Expand All @@ -87,8 +72,9 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

$expectedContextType = $this->contextTypeProvider->getType();
$argContextType = $scope->getType($args[$contextArgumentNo]->value);
$argContextType = $scope->getType($args[$contextArgumentNo]->value);

$expectedContextType = $this->contextTypeProviderResolver->resolveContextTypeProvider($scope, $argContextType)->getType();

$ret = $expectedContextType->accepts($argContextType, true);

Expand Down
10 changes: 3 additions & 7 deletions src/Rules/LogMethodLevelRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use PHPStan\Type\ObjectType;

use function count;
use function implode;
use function in_array;
use function sprintf;

Expand All @@ -22,7 +21,7 @@
final class LogMethodLevelRule implements Rule
{
private const ERROR_INVALID_LEVEL = <<<'MESSAGE'
Parameter #1 $level of method Psr\Log\LoggerInterface::log() expects 'alert'|'critical'|'debug'|'emergency'|'error'|'info'|'notice'|'warning', '%s' given.
Parameter #1 $level of method Psr\Log\LoggerInterface::log() expects 'alert'|'critical'|'debug'|'emergency'|'error'|'info'|'notice'|'warning', %s given.
MESSAGE;

public function getNodeType(): string
Expand Down Expand Up @@ -67,12 +66,9 @@ public function processNode(Node $node, Scope $scope): array
}

if (count($logLevels) === 0) {
// cant find logLevels
return [
RuleErrorBuilder::message(
<<<'MESSAGE'
Parameter #1 $level of method Psr\Log\LoggerInterface::log() expects 'alert'|'critical'|'debug'|'emergency'|'error'|'info'|'notice'|'warning'.
MESSAGE
sprintf(self::ERROR_INVALID_LEVEL, $logLevelType->toPhpDocNode()->__toString())
)->identifier('sfpPsrLog.logMethodLevel')->build(),
];
}
Expand All @@ -90,7 +86,7 @@ public function processNode(Node $node, Scope $scope): array

return [
RuleErrorBuilder::message(
sprintf(self::ERROR_INVALID_LEVEL, implode(', ', $invalidLogLevels))
sprintf(self::ERROR_INVALID_LEVEL, $logLevelType->toPhpDocNode()->__toString())
)->identifier('sfpPsrLog.logMethodLevel')->build(),
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/**
* @see https://cloud.google.com/bigquery/docs/reference/rest/v2/tables?hl=en#TableFieldSchema
*
* @api
* @phpstan-type non_record_field_type 'STRING'|'BYTES'|'INTEGER'|'INT64'|'FLOAT'|'FLOAT64'|'BOOLEAN'|'BOOL'|'TIMESTAMP'|'DATE'|'TIME'|'DATETIME'|'GEOGRAPHY'|'NUMERIC'|'BIGNUMERIC'|'JSON'|'RANGE'
* @phpstan-type field_type non_record_field_type|'RECORD'|'STRUCT'
* @phpstan-type schema_item_minimal array{name: string, type: field_type}
Expand Down
3 changes: 3 additions & 0 deletions src/TypeProvider/ContextTypeProviderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

use PHPStan\Type\Type;

/**
* @api
*/
interface ContextTypeProviderInterface
{
public function getType(): Type;
Expand Down
24 changes: 24 additions & 0 deletions src/TypeProviderResolver/AnyScopeContextTypeProviderResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Sfp\PHPStan\Psr\Log\TypeProviderResolver;

use PHPStan\Analyser\Scope;
use PHPStan\Type\Type;
use Sfp\PHPStan\Psr\Log\TypeProvider\ContextTypeProviderInterface;

final class AnyScopeContextTypeProviderResolver implements ContextTypeProviderResolverInterface
{
/** @var ContextTypeProviderInterface */
private $contextTypeProvider;
public function __construct(ContextTypeProviderInterface $contextTypeProvider)
{
$this->contextTypeProvider = $contextTypeProvider;
}

public function resolveContextTypeProvider(Scope $scope, Type $contextType): ContextTypeProviderInterface
{
return $this->contextTypeProvider;
}
}
14 changes: 14 additions & 0 deletions src/TypeProviderResolver/ContextTypeProviderResolverInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Sfp\PHPStan\Psr\Log\TypeProviderResolver;

use PHPStan\Analyser\Scope;
use PHPStan\Type\Type;
use Sfp\PHPStan\Psr\Log\TypeProvider\ContextTypeProviderInterface;

interface ContextTypeProviderResolverInterface
{
public function resolveContextTypeProvider(Scope $scope, Type $contextType): ContextTypeProviderInterface;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Sfp\PHPStan\Psr\Log\TypeProviderResolver;

use LogicException;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Type\Type;
use Sfp\PHPStan\Psr\Log\TypeProvider\ContextTypeProviderInterface;
use Sfp\PHPStan\Psr\Log\TypeProvider\Psr3ContextTypeProvider;

final class LayeredScopeContextTypeProviderResolver implements ContextTypeProviderResolverInterface
{
/** @phpstan-var array<class-string, ContextTypeProviderInterface> */
private $layerSet;

/** @var AnyScopeContextTypeProviderResolver */
private $anyScopeContextTypeProviderResolver;

/** @var bool */
private $fallbackAnyScope;

/**
* @phpstan-param array<class-string, ContextTypeProviderInterface> $layerSet
*/
public function __construct(array $layerSet, bool $fallbackAnyScope = true)
{
$this->layerSet = $layerSet;
$this->fallbackAnyScope = $fallbackAnyScope;
$this->anyScopeContextTypeProviderResolver = new AnyScopeContextTypeProviderResolver(new Psr3ContextTypeProvider());
}

public function resolveContextTypeProvider(Scope $scope, Type $contextType): ContextTypeProviderInterface
{
$classReflection = $scope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
if ($this->fallbackAnyScope) {
return $this->anyScopeContextTypeProviderResolver->resolveContextTypeProvider($scope, $contextType);
}
throw new LogicException('can not find belongs to ');
}

foreach ($this->layerSet as $interface => $contextTypeProvider) {
if ($classReflection->implementsInterface($interface)) {
return $contextTypeProvider;
}
}

if (! $this->fallbackAnyScope) {
throw new LogicException('can not find belongs to ');
}

return $this->anyScopeContextTypeProviderResolver->resolveContextTypeProvider($scope, $contextType);
}
}
Loading
Loading