Skip to content

Commit a31b664

Browse files
committed
Merge remote-tracking branch 'upstream/master' into handle-potential-first-class-callable
# Conflicts: # composer.json # src/Rules/UseSafeFunctionsRule.php
2 parents ce42c08 + 4e0c969 commit a31b664

23 files changed

+310
-132
lines changed

.github/workflows/tests.yaml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,19 @@ on:
1010

1111
jobs:
1212
Tests:
13-
runs-on: 'ubuntu-latest'
13+
runs-on: "ubuntu-latest"
1414
strategy:
1515
matrix:
1616
php:
17-
- '7.4'
18-
- '8.0'
19-
- '8.1'
20-
- '8.2'
21-
- '8.3'
22-
- '8.4'
23-
dependencies: ['highest']
17+
- "8.1"
18+
- "8.2"
19+
- "8.3"
20+
- "8.4"
21+
dependencies: ["highest"]
2422
include:
25-
- description: '(lowest)'
26-
php: '7.4'
27-
dependencies: 'lowest'
23+
- description: "(lowest)"
24+
php: "8.1"
25+
dependencies: "lowest"
2826

2927
name: PHP ${{ matrix.php }} ${{ matrix.description }}
3028
steps:
@@ -46,5 +44,7 @@ jobs:
4644
- name: "Upload test coverage"
4745
uses: codecov/codecov-action@v5
4846
with:
49-
files: './coverage.xml'
47+
files: "./coverage.xml"
5048
fail_ci_if_error: true
49+
env:
50+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

composer.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
}
1111
],
1212
"require": {
13-
"php": "^7.4 || ^8.0",
13+
"php": "^8.1",
1414
"phpstan/phpstan": "^2.0",
15-
"thecodingmachine/safe": "^1.0 || ^2.0"
15+
"thecodingmachine/safe": "^1.2 || ^2.0 || ^3.0",
16+
"nikic/php-parser": "^5"
1617
},
1718
"require-dev": {
18-
"nikic/php-parser": "^4.19.4",
19-
"phpunit/phpunit": "^9.6",
19+
"phpunit/phpunit": "^10.4",
2020
"php-coveralls/php-coveralls": "^2.1",
2121
"squizlabs/php_codesniffer": "^3.4"
2222
},
@@ -32,6 +32,7 @@
3232
},
3333
"scripts": {
3434
"phpstan": "phpstan analyse -c phpstan.neon --no-progress -vvv",
35+
"test": "XDEBUG_MODE=coverage phpunit",
3536
"cs-fix": "phpcbf",
3637
"cs-check": "phpcs"
3738
},

phpcs.xml.dist

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111

1212
<!-- Paths to check -->
1313
<file>src</file>
14+
<file>tests</file>
15+
<exclude-pattern>tests/**/data</exclude-pattern>
1416

1517
<rule ref="Generic.Files.LineLength">
1618
<properties>
1719
<property name="lineLimit" value="300"/>
1820
<property name="absoluteLineLimit" value="500"/>
1921
</properties>
2022
</rule>
21-
</ruleset>
23+
</ruleset>

phpstan-safe-rule.neon

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,11 @@ services:
1111
class: TheCodingMachine\Safe\PHPStan\Type\Php\ReplaceSafeFunctionsDynamicReturnTypeExtension
1212
tags:
1313
- phpstan.broker.dynamicFunctionReturnTypeExtension
14+
-
15+
class: TheCodingMachine\Safe\PHPStan\Type\Php\PregMatchParameterOutTypeExtension
16+
tags:
17+
- phpstan.functionParameterOutTypeExtension
18+
-
19+
class: TheCodingMachine\Safe\PHPStan\Type\Php\PregMatchTypeSpecifyingExtension
20+
tags:
21+
- phpstan.typeSpecifier.functionTypeSpecifyingExtension

phpstan.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ parameters:
22
level: max
33
paths:
44
- src
5+
- tests
6+
excludePaths:
7+
- tests/Rules/data
58
ignoreErrors:
69
-
710
message: '#^Implementing PHPStan\\Rules\\IdentifierRuleError is not covered by backward compatibility promise\. The interface might change in a minor PHPStan version\.$#'

phpunit.xml.dist

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,35 @@
11
<?xml version="1.0"?>
22
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3-
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
4-
bootstrap="tests/bootstrap.php"
3+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
4+
bootstrap="vendor/autoload.php"
55
colors="true"
66
backupGlobals="false"
7-
backupStaticAttributes="false"
87
beStrictAboutChangesToGlobalState="true"
98
beStrictAboutOutputDuringTests="true"
109
beStrictAboutTestsThatDoNotTestAnything="true"
11-
beStrictAboutTodoAnnotatedTests="true"
1210
failOnRisky="true"
1311
failOnWarning="true"
14-
convertErrorsToExceptions="true"
15-
convertNoticesToExceptions="true"
16-
convertWarningsToExceptions="true"
17-
>
12+
cacheDirectory=".phpunit.cache"
13+
backupStaticProperties="false">
1814
<testsuites>
1915
<testsuite name="Test suite">
2016
<directory>./tests/</directory>
2117
</testsuite>
2218
</testsuites>
2319
<coverage>
24-
<include>
25-
<directory suffix=".php">./src</directory>
26-
</include>
2720
<report>
2821
<clover outputFile="build/logs/clover.xml"/>
2922
<html outputDirectory="build/coverage"/>
3023
<text outputFile="php://stdout" showUncoveredFiles="true" showOnlySummary="true"/>
3124
</report>
3225
</coverage>
26+
<source>
27+
<include>
28+
<directory suffix=".php">./src</directory>
29+
</include>
30+
<exclude>
31+
<!-- type checking doesn't count towards coverage for some reason -->
32+
<directory>src/Type/Php/</directory>
33+
</exclude>
34+
</source>
3335
</phpunit>

src/Rules/Error/SafeFunctionRuleError.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ public function __construct(Name $nodeName, int $line)
1818

1919
public function getIdentifier(): string
2020
{
21-
return self::IDENTIFIER_PREFIX . 'class';
21+
return self::IDENTIFIER_PREFIX . 'function';
2222
}
2323
}

src/Rules/UseSafeFunctionsRule.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@ public function processNode(Node $node, Scope $scope): array
3939

4040
if (isset($unsafeFunctions[$functionName])) {
4141
if (! $node->isFirstClassCallable()) {
42+
if ($functionName === "json_decode" || $functionName === "json_encode") {
43+
foreach ($node->args as $arg) {
44+
if ($arg instanceof Node\Arg &&
45+
$arg->name instanceof Node\Identifier &&
46+
$arg->name->toLowerString() === "flags"
47+
) {
48+
if ($this->argValueIncludeJSONTHROWONERROR($arg)) {
49+
return [];
50+
}
51+
}
52+
}
53+
}
54+
4255
if ($functionName === "json_decode"
4356
&& $this->argValueIncludeJSONTHROWONERROR($node->getArgs()[3] ?? null)
4457
) {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php declare(strict_types = 1);
2+
3+
/*
4+
Blatantly copy-pasted from PHPStan's source code but with isFunctionSupported changed
5+
6+
https://github.com/phpstan/phpstan-src/blob/e664bed7b62e2a58d571fb631ddf47030914a2b5/src/Type/Php/PregMatchParameterOutTypeExtension.php
7+
*/
8+
namespace TheCodingMachine\Safe\PHPStan\Type\Php;
9+
10+
use PHPStan\Type\Php\RegexArrayShapeMatcher;
11+
use PhpParser\Node\Expr\FuncCall;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\Reflection\FunctionReflection;
14+
use PHPStan\Reflection\ParameterReflection;
15+
use PHPStan\TrinaryLogic;
16+
use PHPStan\Type\FunctionParameterOutTypeExtension;
17+
use PHPStan\Type\Type;
18+
use function in_array;
19+
20+
final class PregMatchParameterOutTypeExtension implements FunctionParameterOutTypeExtension
21+
{
22+
23+
public function __construct(
24+
private RegexArrayShapeMatcher $regexShapeMatcher,
25+
) {
26+
}
27+
28+
public function isFunctionSupported(FunctionReflection $functionReflection, ParameterReflection $parameter): bool
29+
{
30+
return in_array($functionReflection->getName(), ['Safe\preg_match', 'Safe\preg_match_all'], true)
31+
// the parameter is named different, depending on PHP version.
32+
&& in_array($parameter->getName(), ['subpatterns', 'matches'], true);
33+
}
34+
35+
public function getParameterOutTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, ParameterReflection $parameter, Scope $scope): ?Type
36+
{
37+
$args = $funcCall->getArgs();
38+
$patternArg = $args[0] ?? null;
39+
$matchesArg = $args[2] ?? null;
40+
$flagsArg = $args[3] ?? null;
41+
42+
if ($patternArg === null || $matchesArg === null
43+
) {
44+
return null;
45+
}
46+
47+
$flagsType = null;
48+
if ($flagsArg !== null) {
49+
$flagsType = $scope->getType($flagsArg->value);
50+
}
51+
52+
if ($functionReflection->getName() === 'Safe\preg_match') {
53+
return $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope);
54+
}
55+
return $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope);
56+
}
57+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php declare(strict_types = 1);
2+
3+
/*
4+
Blatantly copy-pasted from PHPStan's source code but with isFunctionSupported changed
5+
*/
6+
namespace TheCodingMachine\Safe\PHPStan\Type\Php;
7+
8+
use PHPStan\Type\Php\RegexArrayShapeMatcher;
9+
use PhpParser\Node\Expr\FuncCall;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Analyser\SpecifiedTypes;
12+
use PHPStan\Analyser\TypeSpecifier;
13+
use PHPStan\Analyser\TypeSpecifierAwareExtension;
14+
use PHPStan\Analyser\TypeSpecifierContext;
15+
use PHPStan\Reflection\FunctionReflection;
16+
use PHPStan\TrinaryLogic;
17+
use PHPStan\Type\FunctionTypeSpecifyingExtension;
18+
use function in_array;
19+
use function strtolower;
20+
21+
final class PregMatchTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
22+
{
23+
24+
private TypeSpecifier $typeSpecifier;
25+
26+
public function __construct(
27+
private RegexArrayShapeMatcher $regexShapeMatcher,
28+
) {
29+
}
30+
31+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
32+
{
33+
$this->typeSpecifier = $typeSpecifier;
34+
}
35+
36+
public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool
37+
{
38+
return in_array(strtolower($functionReflection->getName()), ['safe\preg_match', 'safe\preg_match_all'], true) && !$context->null();
39+
}
40+
41+
public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
42+
{
43+
$args = $node->getArgs();
44+
$patternArg = $args[0] ?? null;
45+
$matchesArg = $args[2] ?? null;
46+
$flagsArg = $args[3] ?? null;
47+
48+
if ($patternArg === null || $matchesArg === null
49+
) {
50+
return new SpecifiedTypes();
51+
}
52+
53+
$flagsType = null;
54+
if ($flagsArg !== null) {
55+
$flagsType = $scope->getType($flagsArg->value);
56+
}
57+
58+
if ($functionReflection->getName() === 'Safe\preg_match') {
59+
$matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
60+
} else {
61+
$matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
62+
}
63+
if ($matchedType === null) {
64+
return new SpecifiedTypes();
65+
}
66+
67+
$overwrite = false;
68+
if ($context->false()) {
69+
$overwrite = true;
70+
$context = $context->negate();
71+
}
72+
73+
$types = $this->typeSpecifier->create(
74+
$matchesArg->value,
75+
$matchedType,
76+
$context,
77+
$scope,
78+
)->setRootExpr($node);
79+
if ($overwrite) {
80+
$types = $types->setAlwaysOverwriteTypes();
81+
}
82+
83+
return $types;
84+
}
85+
}

0 commit comments

Comments
 (0)