Add Safe PHP functions

This commit is contained in:
Alex Cabal 2019-03-07 12:11:50 -06:00
parent 04a956886a
commit 58cc098058
260 changed files with 49458 additions and 45 deletions

View file

@ -0,0 +1,32 @@
language: php
sudo: false
php:
- 7.1
- 7.2
- 7.3
env:
global:
# We need to prefer source to get PHPStan test directory. Otherwise, it is removed from ZIP
- DEFAULT_COMPOSER_FLAGS="--no-interaction --no-progress --optimize-autoloader --prefer-source"
- TASK_TESTS=1
- TASK_TESTS_COVERAGE=0
- TASK_CS=1
- TASK_SCA=0
matrix:
- COMPOSER_FLAGS="--prefer-lowest"
- COMPOSER_FLAGS=""
cache:
directories:
- "$HOME/.composer/cache"
before_install:
- travis_retry composer global require $DEFAULT_COMPOSER_FLAGS hirak/prestissimo
install:
- travis_retry composer update $DEFAULT_COMPOSER_FLAGS $COMPOSER_FLAGS
- composer info -D | sort
- mkdir tmp
script:
- vendor/bin/phpunit --verbose;
- composer phpstan
- composer cs-check
after_success:
- vendor/bin/coveralls -v

View file

@ -0,0 +1,16 @@
[![Latest Stable Version](https://poser.pugx.org/thecodingmachine/phpstan-safe-rule/v/stable)](https://packagist.org/packages/thecodingmachine/phpstan-safe-rule)
[![Total Downloads](https://poser.pugx.org/thecodingmachine/phpstan-safe-rule/downloads)](https://packagist.org/packages/thecodingmachine/phpstan-safe-rule)
[![Latest Unstable Version](https://poser.pugx.org/thecodingmachine/phpstan-safe-rule/v/unstable)](https://packagist.org/packages/thecodingmachine/phpstan-safe-rule)
[![License](https://poser.pugx.org/thecodingmachine/phpstan-safe-rule/license)](https://packagist.org/packages/thecodingmachine/phpstan-safe-rule)
[![Build Status](https://travis-ci.org/thecodingmachine/phpstan-safe-rule.svg?branch=master)](https://travis-ci.org/thecodingmachine/phpstan-safe-rule)
[![Coverage Status](https://coveralls.io/repos/thecodingmachine/phpstan-safe-rule/badge.svg?branch=master&service=github)](https://coveralls.io/github/thecodingmachine/phpstan-safe-rule?branch=master)
PHPStan rules for thecodingmachine/safe
=======================================
The [thecodingmachine/safe](https://github.com/thecodingmachine/safe) package provides a set of core PHP functions rewritten to throw exceptions instead of returning `false` when an error is encountered.
This PHPStan rule will help you detect unsafe function call and will propose you to use the `thecodingmachine/safe` variant instead.
Please read [thecodingmachine/safe documentation](https://github.com/thecodingmachine/safe) for details about installation and usage.

View file

@ -0,0 +1,44 @@
{
"name": "thecodingmachine/phpstan-safe-rule",
"description": "A PHPStan rule to detect safety issues. Must be used in conjunction with thecodingmachine/safe",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "David Négrier",
"email": "d.negrier@thecodingmachine.com"
}
],
"require": {
"php": "^7.1",
"phpstan/phpstan": "^0.10 | ^0.11",
"thecodingmachine/safe": "^0.1.11"
},
"require-dev": {
"phpunit/phpunit": "^7.5.2",
"php-coveralls/php-coveralls": "^2.1",
"squizlabs/php_codesniffer": "^3.4"
},
"autoload": {
"psr-4": {
"TheCodingMachine\\Safe\\PHPStan\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"TheCodingMachine\\Safe\\PHPStan\\": "tests/"
}
},
"scripts": {
"phpstan": "phpstan analyse src -c phpstan.neon --level=7 --no-progress -vvv",
"cs-fix": "phpcbf",
"cs-check": "phpcs"
},
"extra": {
"branch-alias": {
"dev-master": "0.1-dev"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0"?>
<ruleset name="Expressive Skeleton coding standard">
<description>Expressive Skeleton coding standard</description>
<!-- display progress -->
<arg value="p"/>
<arg name="colors"/>
<!-- inherit rules from: -->
<rule ref="PSR2"/>
<!-- Paths to check -->
<file>src</file>
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="300"/>
<property name="absoluteLineLimit" value="500"/>
</properties>
</rule>
</ruleset>

View file

@ -0,0 +1,9 @@
services:
-
class: TheCodingMachine\Safe\PHPStan\Rules\UseSafeFunctionsRule
tags:
- phpstan.rules.rule
-
class: TheCodingMachine\Safe\PHPStan\Type\Php\ReplaceSafeFunctionsDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

View file

@ -0,0 +1,4 @@
parameters:
ignoreErrors:
includes:
- phpstan-safe-rule.neon

View file

@ -0,0 +1,36 @@
<phpunit
bootstrap="tests/bootstrap.php"
colors="true"
backupGlobals="false"
backupStaticAttributes="false"
beStrictAboutChangesToGlobalState="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutTodoAnnotatedTests="true"
failOnRisky="true"
failOnWarning="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<testsuites>
<testsuite name="Test suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
<logging>
<log
type="coverage-text"
target="php://stdout"
showUncoveredFiles="true"
showOnlySummary="true"
/>
<log type="coverage-html" target="build/coverage"/>
<log type="coverage-clover" target="build/logs/clover.xml"/>
</logging>
</phpunit>

View file

@ -0,0 +1,43 @@
<?php
namespace TheCodingMachine\Safe\PHPStan\Rules;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Rules\Rule;
use PHPStan\ShouldNotHappenException;
use TheCodingMachine\Safe\PHPStan\Utils\FunctionListLoader;
/**
* This rule checks that no superglobals are used in code.
*/
class UseSafeFunctionsRule implements Rule
{
public function getNodeType(): string
{
return Node\Expr\FuncCall::class;
}
/**
* @param Node\Expr\FuncCall $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Node\Name) {
return [];
}
$functionName = $node->name->toString();
$unsafeFunctions = FunctionListLoader::getFunctionList();
if (isset($unsafeFunctions[$functionName])) {
return ["Function $functionName is unsafe to use. It can return FALSE instead of throwing an exception. Please add 'use function Safe\\$functionName;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library."];
}
return [];
}
}

View file

@ -0,0 +1,79 @@
<?php declare(strict_types = 1);
namespace TheCodingMachine\Safe\PHPStan\Type\Php;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\ArrayType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\MixedType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
class ReplaceSafeFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{
/** @var array<string, int> */
private $functions = [
'Safe\preg_replace' => 2,
];
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return array_key_exists($functionReflection->getName(), $this->functions);
}
public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope
): Type {
$type = $this->getPreliminarilyResolvedTypeFromFunctionCall($functionReflection, $functionCall, $scope);
$possibleTypes = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
if (TypeCombinator::containsNull($possibleTypes)) {
$type = TypeCombinator::addNull($type);
}
return $type;
}
private function getPreliminarilyResolvedTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope
): Type {
$argumentPosition = $this->functions[$functionReflection->getName()];
if (count($functionCall->args) <= $argumentPosition) {
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}
$subjectArgumentType = $scope->getType($functionCall->args[$argumentPosition]->value);
$defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
if ($subjectArgumentType instanceof MixedType) {
return TypeUtils::toBenevolentUnion($defaultReturnType);
}
$stringType = new StringType();
$arrayType = new ArrayType(new MixedType(), new MixedType());
$isStringSuperType = $stringType->isSuperTypeOf($subjectArgumentType);
$isArraySuperType = $arrayType->isSuperTypeOf($subjectArgumentType);
$compareSuperTypes = $isStringSuperType->compareTo($isArraySuperType);
if ($compareSuperTypes === $isStringSuperType) {
return $stringType;
} elseif ($compareSuperTypes === $isArraySuperType) {
if ($subjectArgumentType instanceof ArrayType) {
return $subjectArgumentType->generalizeValues();
}
return $subjectArgumentType;
}
return $defaultReturnType;
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace TheCodingMachine\Safe\PHPStan\Utils;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\MethodReflection;
class FunctionListLoader
{
private static $functions;
/**
* @return string[]
*/
public static function getFunctionList(): array
{
if (self::$functions === null) {
if (\file_exists(__DIR__.'/../../../safe/generated/functionsList.php')) {
$functions = require __DIR__.'/../../../safe/generated/functionsList.php';
} elseif (\file_exists(__DIR__.'/../../vendor/thecodingmachine/safe/generated/functionsList.php')) {
$functions = require __DIR__.'/../../vendor/thecodingmachine/safe/generated/functionsList.php';
} else {
throw new \RuntimeException('Could not find thecodingmachine/safe\'s functionsList.php file.');
}
// Let's index these functions by their name
self::$functions = \Safe\array_combine($functions, $functions);
}
return self::$functions;
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace TheCodingMachine\Safe\PHPStan\Rules;
use PHPStan\Rules\FunctionCallParametersCheck;
use PHPStan\Rules\Methods\CallMethodsRule;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Testing\RuleTestCase;
use TheCodingMachine\Safe\PHPStan\Type\Php\ReplaceSafeFunctionsDynamicReturnTypeExtension;
class CallMethodRuleTest extends RuleTestCase
{
protected function getRule(): Rule
{
$broker = $this->createBroker();
$ruleLevelHelper = new RuleLevelHelper($broker, true, true, true);
return new CallMethodsRule(
$broker,
new FunctionCallParametersCheck($ruleLevelHelper, true, true),
$ruleLevelHelper,
true,
true
);
}
public function testSafePregReplace()
{
// FIXME: this rule actually runs code but will always return no error because the rule executed is not the correct one.
// This provides code coverage but assert is not ok.
$this->analyse([__DIR__ . '/data/safe_pregreplace.php'], []);
}
/**
* @return \PHPStan\Type\DynamicFunctionReturnTypeExtension[]
*/
public function getDynamicFunctionReturnTypeExtensions(): array
{
return [new ReplaceSafeFunctionsDynamicReturnTypeExtension()];
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace TheCodingMachine\Safe\PHPStan\Rules;
use PHPStan\Testing\RuleTestCase;
use TheCodingMachine\Safe\PHPStan\Type\Php\ReplaceSafeFunctionsDynamicReturnTypeExtension;
class UseSafeFunctionsRuleTest extends RuleTestCase
{
protected function getRule(): \PHPStan\Rules\Rule
{
return new UseSafeFunctionsRule();
}
public function testCatch()
{
$this->analyse([__DIR__ . '/data/fopen.php'], [
[
"Function fopen is unsafe to use. It can return FALSE instead of throwing an exception. Please add 'use function Safe\\fopen;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library.",
4,
],
]);
}
public function testNoCatchSafe()
{
$this->analyse([__DIR__ . '/data/safe_fopen.php'], []);
}
public function testExprCall()
{
$this->analyse([__DIR__ . '/data/undirect_call.php'], []);
}
}

View file

@ -0,0 +1,5 @@
<?php
function foo() {
$fp = fopen('foobar', 'r');
}

View file

@ -0,0 +1,6 @@
<?php
use function Safe\fopen;
function foo() {
$fp = fopen('foobar', 'r');
}

View file

@ -0,0 +1,7 @@
<?php
use function Safe\preg_replace;
$x = preg_replace('/foo/', 'bar', 'baz');
$y = stripos($x, 'foo');
$x = preg_replace(['/foo/'], ['bar'], ['baz']);

View file

@ -0,0 +1,5 @@
<?php
function foo() {
$toto = 'fopen';
$toto('foobar', 'r');
}

View file

@ -0,0 +1,15 @@
<?php
namespace TheCodingMachine\Safe\PHPStan\Utils;
use PHPUnit\Framework\TestCase;
class FunctionListLoaderTest extends TestCase
{
public function testGetFunctionList()
{
$functions = FunctionListLoader::getFunctionList();
$this->assertArrayHasKey('fopen', $functions);
}
}

View file

@ -0,0 +1,33 @@
<?php declare(strict_types=1);
use Nette\Configurator;
require_once __DIR__ . '/../vendor/autoload.php';
/*require_once __DIR__ . '/TestCase.php';
require_once __DIR__ . '/PHPStan/Rules/AbstractRuleTest.php';
require_once __DIR__ . '/PHPStan/Rules/AlwaysFailRule.php';
require_once __DIR__ . '/PHPStan/Rules/DummyRule.php';*/
/*
$rootDir = __DIR__ . '/..';
$tmpDir = $rootDir . '/tmp';
$confDir = $rootDir . '/vendor/phpstan/phpstan/conf';
$configurator = new Configurator();
$configurator->defaultExtensions = [];
$configurator->setDebugMode(true);
$configurator->setTempDirectory($tmpDir);
$configurator->addConfig($confDir . '/config.neon');
$configurator->addConfig($confDir . '/config.level5.neon');
$configurator->addParameters([
'rootDir' => $rootDir,
'tmpDir' => $tmpDir,
'currentWorkingDirectory' => $rootDir,
'cliArgumentsVariablesRegistered' => false,
]);
$container = $configurator->createContainer();
PHPStan\Testing\TestCase::setContainer($container);
PHPStan\Type\TypeCombinator::setUnionTypesEnabled(true);
require_once __DIR__ . '/phpstan-bootstrap.php';
*/

View file

@ -0,0 +1,4 @@
<?php declare(strict_types=1);
class_alias(\ReturnTypes\Foo::class, \ReturnTypes\FooAlias::class, true);
class_alias(\TestAccessProperties\FooAccessProperties::class, \TestAccessProperties\FooAccessPropertiesAlias::class, true);