20250203
This commit is contained in:
21
vendor/staabm/side-effects-detector/LICENSE
vendored
Normal file
21
vendor/staabm/side-effects-detector/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Markus Staab
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
57
vendor/staabm/side-effects-detector/README.md
vendored
Normal file
57
vendor/staabm/side-effects-detector/README.md
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
Analyzes php-code for side-effects.
|
||||
|
||||
When code has no side-effects it can e.g. be used with `eval($code)` in the same process without interfering.
|
||||
[Side-effects are classified](https://github.com/staabm/side-effects-detector/blob/main/lib/SideEffect.php) into categories to filter them more easily depending on your use-case.
|
||||
|
||||
## Install
|
||||
|
||||
`composer require staabm/side-effects-detector`
|
||||
|
||||
## Usage
|
||||
|
||||
Example:
|
||||
|
||||
```php
|
||||
use staabm\SideEffectsDetector\SideEffectsDetector;
|
||||
|
||||
$code = '<?php version_compare(PHP_VERSION, "8.0", ">=") or echo("skip because attributes are only available since PHP 8.0");';
|
||||
|
||||
$detector = new SideEffectsDetector();
|
||||
// [SideEffect::STANDARD_OUTPUT]
|
||||
var_dump($detector->getSideEffects($code));
|
||||
```
|
||||
|
||||
In case functions are called which are not known to have side-effects - e.g. userland functions - `null` is returned.
|
||||
|
||||
```php
|
||||
use staabm\SideEffectsDetector\SideEffectsDetector;
|
||||
|
||||
$code = '<?php userlandFunction();';
|
||||
|
||||
$detector = new SideEffectsDetector();
|
||||
// [SideEffect::MAYBE]
|
||||
var_dump($detector->getSideEffects($code));
|
||||
```
|
||||
|
||||
Code might have multiple side-effects:
|
||||
|
||||
```php
|
||||
use staabm\SideEffectsDetector\SideEffectsDetector;
|
||||
|
||||
$code = '<?php include "some-file.php"; echo "hello world"; exit(1);';
|
||||
|
||||
$detector = new SideEffectsDetector();
|
||||
// [SideEffect::SCOPE_POLLUTION, SideEffect::STANDARD_OUTPUT, SideEffect::PROCESS_EXIT]
|
||||
var_dump($detector->getSideEffects($code));
|
||||
```
|
||||
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Non goals are:
|
||||
- find the best possible answer for all cases
|
||||
- add runtime dependencies
|
||||
|
||||
If you are in need of a fully fledged side-effect analysis, use more advanced tools like PHPStan.
|
||||
|
||||
Look at the test-suite to get an idea of [supported use-cases](https://github.com/staabm/side-effects-detector/blob/main/tests/SideEffectsDetectorTest.php).
|
38
vendor/staabm/side-effects-detector/composer.json
vendored
Normal file
38
vendor/staabm/side-effects-detector/composer.json
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "staabm/side-effects-detector",
|
||||
"license": "MIT",
|
||||
"description": "A static analysis tool to detect side effects in PHP code",
|
||||
"keywords": ["static analysis"],
|
||||
"autoload": {
|
||||
"classmap": ["lib/"]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"classmap": [
|
||||
"tests/"
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"ext-tokenizer": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan": "^1.12.6",
|
||||
"phpunit/phpunit": "^9.6.21",
|
||||
"symfony/var-dumper": "^5.4.43",
|
||||
"tomasvotruba/type-coverage": "1.0.0",
|
||||
"tomasvotruba/unused-public": "1.0.0"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"phpstan/extension-installer": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"qa": ["@test", "@phpstan"],
|
||||
"phpstan": "phpstan analyze",
|
||||
"test": "phpunit"
|
||||
}
|
||||
}
|
42
vendor/staabm/side-effects-detector/lib/SideEffect.php
vendored
Normal file
42
vendor/staabm/side-effects-detector/lib/SideEffect.php
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace staabm\SideEffectsDetector;
|
||||
|
||||
/**
|
||||
* @api
|
||||
*/
|
||||
final class SideEffect {
|
||||
/**
|
||||
* die, exit, throw.
|
||||
*/
|
||||
const PROCESS_EXIT = 'process_exit';
|
||||
|
||||
/**
|
||||
* class definition, func definition, include, require, global var, unset, goto
|
||||
*/
|
||||
const SCOPE_POLLUTION = 'scope_pollution';
|
||||
|
||||
/**
|
||||
* fwrite, unlink...
|
||||
*/
|
||||
const INPUT_OUTPUT = 'input_output';
|
||||
|
||||
/**
|
||||
* echo, print.
|
||||
*/
|
||||
const STANDARD_OUTPUT = 'standard_output';
|
||||
|
||||
/**
|
||||
* code for sure has side-effects, we don't have enough information to classify it.
|
||||
*/
|
||||
const UNKNOWN_CLASS = 'unknown_class';
|
||||
|
||||
/**
|
||||
* code might have side-effects, but we can't tell for sure.
|
||||
*/
|
||||
const MAYBE = 'maybe_has_side_effects';
|
||||
|
||||
private function __construct() {
|
||||
// nothing todo
|
||||
}
|
||||
}
|
372
vendor/staabm/side-effects-detector/lib/SideEffectsDetector.php
vendored
Normal file
372
vendor/staabm/side-effects-detector/lib/SideEffectsDetector.php
vendored
Normal file
@@ -0,0 +1,372 @@
|
||||
<?php
|
||||
|
||||
namespace staabm\SideEffectsDetector;
|
||||
|
||||
final class SideEffectsDetector {
|
||||
/**
|
||||
* @var array<int>
|
||||
*/
|
||||
private array $scopePollutingTokens = [
|
||||
T_CLASS,
|
||||
T_FUNCTION,
|
||||
T_NEW,
|
||||
T_EVAL,
|
||||
T_GLOBAL,
|
||||
T_GOTO,
|
||||
T_HALT_COMPILER,
|
||||
T_INCLUDE,
|
||||
T_INCLUDE_ONCE,
|
||||
T_REQUIRE,
|
||||
T_REQUIRE_ONCE,
|
||||
T_THROW,
|
||||
T_UNSET,
|
||||
T_UNSET_CAST
|
||||
];
|
||||
|
||||
private const PROCESS_EXIT_TOKENS = [
|
||||
T_EXIT
|
||||
];
|
||||
|
||||
private const OUTPUT_TOKENS = [
|
||||
T_PRINT,
|
||||
T_ECHO,
|
||||
T_INLINE_HTML
|
||||
];
|
||||
|
||||
private const SCOPE_POLLUTING_FUNCTIONS = [
|
||||
'putenv',
|
||||
'setlocale',
|
||||
'class_exists',
|
||||
'ini_set',
|
||||
];
|
||||
|
||||
private const STANDARD_OUTPUT_FUNCTIONS = [
|
||||
'printf',
|
||||
'vprintf'
|
||||
];
|
||||
|
||||
private const INPUT_OUTPUT_FUNCTIONS = [
|
||||
'fopen',
|
||||
'file_get_contents',
|
||||
'file_put_contents',
|
||||
'fwrite',
|
||||
'fputs',
|
||||
'fread',
|
||||
'unlink'
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, array{'hasSideEffects': bool}>
|
||||
*/
|
||||
private array $functionMetadata;
|
||||
|
||||
public function __construct() {
|
||||
$functionMeta = require __DIR__ . '/functionMetadata.php';
|
||||
if (!is_array($functionMeta)) {
|
||||
throw new \RuntimeException('Invalid function metadata');
|
||||
}
|
||||
$this->functionMetadata = $functionMeta;
|
||||
|
||||
if (defined('T_ENUM')) {
|
||||
$this->scopePollutingTokens[] = T_ENUM;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
*
|
||||
* @return array<SideEffect::*>
|
||||
*/
|
||||
public function getSideEffects(string $code): array {
|
||||
$tokens = token_get_all($code);
|
||||
|
||||
$sideEffects = [];
|
||||
for ($i = 0; $i < count($tokens); $i++) {
|
||||
$token = $tokens[$i];
|
||||
|
||||
if (!is_array($token)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->isAnonymousFunction($tokens, $i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($token[0], self::OUTPUT_TOKENS, true)) {
|
||||
$sideEffects[] = SideEffect::STANDARD_OUTPUT;
|
||||
continue;
|
||||
}
|
||||
if (in_array($token[0], self::PROCESS_EXIT_TOKENS, true)) {
|
||||
$sideEffects[] = SideEffect::PROCESS_EXIT;
|
||||
continue;
|
||||
}
|
||||
if (in_array($token[0], $this->scopePollutingTokens, true)) {
|
||||
$sideEffects[] = SideEffect::SCOPE_POLLUTION;
|
||||
|
||||
$i++;
|
||||
if (in_array($token[0], [T_FUNCTION, T_CLASS], true)) {
|
||||
$this->consumeWhitespaces($tokens, $i);
|
||||
}
|
||||
|
||||
// consume function/class-name
|
||||
if (
|
||||
!array_key_exists($i, $tokens)
|
||||
|| !is_array($tokens[$i])
|
||||
|| $tokens[$i][0] !== T_STRING
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$functionCall = $this->getFunctionCall($tokens, $i);
|
||||
if ($functionCall !== null) {
|
||||
$callSideEffect = $this->getFunctionCallSideEffect($functionCall);
|
||||
if ($callSideEffect !== null) {
|
||||
$sideEffects[] = $callSideEffect;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$methodCall = $this->getMethodCall($tokens, $i);
|
||||
if ($methodCall !== null) {
|
||||
$sideEffects[] = SideEffect::MAYBE;
|
||||
continue;
|
||||
}
|
||||
|
||||
$propertyAccess = $this->getPropertyAccess($tokens, $i);
|
||||
if ($propertyAccess !== null) {
|
||||
$sideEffects[] = SideEffect::SCOPE_POLLUTION;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->isNonLocalVariable($tokens, $i)) {
|
||||
$sideEffects[] = SideEffect::SCOPE_POLLUTION;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($sideEffects));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SideEffect::*|null
|
||||
*/
|
||||
private function getFunctionCallSideEffect(string $functionName): ?string { // @phpstan-ignore return.unusedType
|
||||
if (in_array($functionName, self::STANDARD_OUTPUT_FUNCTIONS, true)) {
|
||||
return SideEffect::STANDARD_OUTPUT;
|
||||
}
|
||||
|
||||
if (in_array($functionName, self::INPUT_OUTPUT_FUNCTIONS, true)) {
|
||||
return SideEffect::INPUT_OUTPUT;
|
||||
}
|
||||
|
||||
if (in_array($functionName, self::SCOPE_POLLUTING_FUNCTIONS, true)) {
|
||||
return SideEffect::SCOPE_POLLUTION;
|
||||
}
|
||||
|
||||
if (array_key_exists($functionName, $this->functionMetadata)) {
|
||||
if ($this->functionMetadata[$functionName]['hasSideEffects'] === true) {
|
||||
return SideEffect::UNKNOWN_CLASS;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$reflectionFunction = new \ReflectionFunction($functionName);
|
||||
$returnType = $reflectionFunction->getReturnType();
|
||||
if ($returnType === null) {
|
||||
return SideEffect::MAYBE; // no reflection information -> we don't know
|
||||
}
|
||||
if ((string)$returnType === 'void') {
|
||||
return SideEffect::UNKNOWN_CLASS; // functions with void return type must have side-effects
|
||||
}
|
||||
} catch (\ReflectionException $e) {
|
||||
return SideEffect::MAYBE; // function does not exist -> we don't know
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{0:int,1:string,2:int}|string|int> $tokens
|
||||
*/
|
||||
private function getFunctionCall(array $tokens, int $index): ?string {
|
||||
if (
|
||||
!array_key_exists($index, $tokens)
|
||||
|| !is_array($tokens[$index])
|
||||
|| $tokens[$index][0] !== T_STRING
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
$functionName = $tokens[$index][1];
|
||||
|
||||
$index++;
|
||||
$this->consumeWhitespaces($tokens, $index);
|
||||
|
||||
if (
|
||||
array_key_exists($index, $tokens)
|
||||
&& $tokens[$index] === '('
|
||||
) {
|
||||
return $functionName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{0:int,1:string,2:int}|string|int> $tokens
|
||||
*/
|
||||
private function getMethodCall(array $tokens, int $index): ?string {
|
||||
if (
|
||||
!array_key_exists($index, $tokens)
|
||||
|| !is_array($tokens[$index])
|
||||
|| !in_array($tokens[$index][0], [T_VARIABLE, T_STRING], true)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
$callee = $tokens[$index][1];
|
||||
|
||||
$index++;
|
||||
$this->consumeWhitespaces($tokens, $index);
|
||||
|
||||
if (
|
||||
!array_key_exists($index, $tokens)
|
||||
|| !is_array($tokens[$index])
|
||||
|| !in_array($tokens[$index][0], [T_OBJECT_OPERATOR , T_DOUBLE_COLON ], true)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
$operator = $tokens[$index][1];
|
||||
|
||||
$index++;
|
||||
$this->consumeWhitespaces($tokens, $index);
|
||||
|
||||
if (
|
||||
!array_key_exists($index, $tokens)
|
||||
|| !is_array($tokens[$index])
|
||||
|| !in_array($tokens[$index][0], [T_STRING], true)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
$method = $tokens[$index][1];
|
||||
|
||||
$index++;
|
||||
$this->consumeWhitespaces($tokens, $index);
|
||||
|
||||
if (
|
||||
array_key_exists($index, $tokens)
|
||||
&& $tokens[$index] !== '('
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $callee . $operator . $method;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{0:int,1:string,2:int}|string|int> $tokens
|
||||
*/
|
||||
private function getPropertyAccess(array $tokens, int $index): ?string {
|
||||
if (
|
||||
!array_key_exists($index, $tokens)
|
||||
|| !is_array($tokens[$index])
|
||||
|| !in_array($tokens[$index][0], [T_VARIABLE, T_STRING], true)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
$objectOrClass = $tokens[$index][1];
|
||||
|
||||
$index++;
|
||||
$this->consumeWhitespaces($tokens, $index);
|
||||
|
||||
if (
|
||||
!array_key_exists($index, $tokens)
|
||||
|| !is_array($tokens[$index])
|
||||
|| !in_array($tokens[$index][0], [T_OBJECT_OPERATOR , T_DOUBLE_COLON ], true)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
$operator = $tokens[$index][1];
|
||||
|
||||
$index++;
|
||||
$this->consumeWhitespaces($tokens, $index);
|
||||
|
||||
if (
|
||||
!array_key_exists($index, $tokens)
|
||||
|| !is_array($tokens[$index])
|
||||
|| !in_array($tokens[$index][0], [T_STRING, T_VARIABLE], true)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
$propName = $tokens[$index][1];
|
||||
|
||||
return $objectOrClass . $operator . $propName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{0:int,1:string,2:int}|string|int> $tokens
|
||||
*/
|
||||
private function isAnonymousFunction(array $tokens, int $index): bool
|
||||
{
|
||||
if (
|
||||
!array_key_exists($index, $tokens)
|
||||
|| !is_array($tokens[$index])
|
||||
|| $tokens[$index][0] !== T_FUNCTION
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$index++;
|
||||
$this->consumeWhitespaces($tokens, $index);
|
||||
|
||||
if (
|
||||
array_key_exists($index, $tokens)
|
||||
&& $tokens[$index] === '('
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{0:int,1:string,2:int}|string|int> $tokens
|
||||
*/
|
||||
private function isNonLocalVariable(array $tokens, int $index): bool
|
||||
{
|
||||
if (
|
||||
array_key_exists($index, $tokens)
|
||||
&& is_array($tokens[$index])
|
||||
&& $tokens[$index][0] === T_VARIABLE
|
||||
) {
|
||||
if (
|
||||
in_array(
|
||||
$tokens[$index][1],
|
||||
[
|
||||
'$this',
|
||||
'$GLOBALS', '$_SERVER', '$_GET', '$_POST', '$_FILES', '$_COOKIE', '$_SESSION', '$_REQUEST', '$_ENV',
|
||||
],
|
||||
true)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{0:int,1:string,2:int}|string|int> $tokens
|
||||
*/
|
||||
private function consumeWhitespaces(array $tokens, int &$index): void {
|
||||
while (
|
||||
array_key_exists($index, $tokens)
|
||||
&& is_array($tokens[$index])
|
||||
&& $tokens[$index][0] === T_WHITESPACE
|
||||
) {
|
||||
$index++;
|
||||
}
|
||||
}
|
||||
}
|
1588
vendor/staabm/side-effects-detector/lib/functionMetadata.php
vendored
Normal file
1588
vendor/staabm/side-effects-detector/lib/functionMetadata.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user