From 62096e00db2cb1e245d92a744a580aa4a839d06f Mon Sep 17 00:00:00 2001 From: Alessandro Lai <alessandro.lai85@gmail.com> Date: Thu, 21 Nov 2024 15:42:57 +0100 Subject: [PATCH 1/7] Bump requirements --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index d9962b9..939e443 100644 --- a/composer.json +++ b/composer.json @@ -10,12 +10,12 @@ } ], "require": { - "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^1.0", + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0", "thecodingmachine/safe": "^1.0 || ^2.0" }, "require-dev": { - "phpunit/phpunit": "^7.5.2 || ^8.0", + "phpunit/phpunit": "^9.6", "php-coveralls/php-coveralls": "^2.1", "squizlabs/php_codesniffer": "^3.4" }, @@ -30,13 +30,13 @@ } }, "scripts": { - "phpstan": "phpstan analyse src -c phpstan.neon --level=7 --no-progress -vvv", + "phpstan": "phpstan analyse src -c phpstan.neon --level=max --no-progress -vvv", "cs-fix": "phpcbf", "cs-check": "phpcs" }, "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "2.0-dev" }, "phpstan": { "includes": [ From b99a89546b900f94bab55608c31772c9db773488 Mon Sep 17 00:00:00 2001 From: Alessandro Lai <alessandro.lai85@gmail.com> Date: Thu, 21 Nov 2024 15:45:56 +0100 Subject: [PATCH 2/7] Upgrade PHPUnit config --- .gitignore | 3 ++- phpunit.xml.dist | 65 +++++++++++++++++++++++------------------------- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index 9b8adcf..bcada90 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ /composer.lock /phpunit.xml /tmp -/build/ \ No newline at end of file +/build/ +*.cache diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 245ec37..ac73057 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,36 +1,33 @@ -<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" +<?xml version="1.0"?> +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" + 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> + <testsuites> + <testsuite name="Test suite"> + <directory>./tests/</directory> + </testsuite> + </testsuites> + <coverage> + <include> + <directory suffix=".php">./src</directory> + </include> + <report> + <clover outputFile="build/logs/clover.xml"/> + <html outputDirectory="build/coverage"/> + <text outputFile="php://stdout" showUncoveredFiles="true" showOnlySummary="true"/> + </report> + </coverage> </phpunit> From e30b538d6664af29499e0fdec5619cd4387af932 Mon Sep 17 00:00:00 2001 From: Alessandro Lai <alessandro.lai85@gmail.com> Date: Thu, 21 Nov 2024 22:47:08 +0100 Subject: [PATCH 3/7] Refactor and adapt the rules to the new PHPStan APIs --- src/Rules/Error/SafeClassRuleError.php | 21 ++++++++ src/Rules/Error/SafeFunctionRuleError.php | 23 +++++++++ src/Rules/Error/SafeRuleError.php | 31 ++++++++++++ src/Rules/UseSafeClassesRule.php | 17 ++----- src/Rules/UseSafeFunctionsRule.php | 60 ++++++++++------------- 5 files changed, 106 insertions(+), 46 deletions(-) create mode 100644 src/Rules/Error/SafeClassRuleError.php create mode 100644 src/Rules/Error/SafeFunctionRuleError.php create mode 100644 src/Rules/Error/SafeRuleError.php diff --git a/src/Rules/Error/SafeClassRuleError.php b/src/Rules/Error/SafeClassRuleError.php new file mode 100644 index 0000000..ad48573 --- /dev/null +++ b/src/Rules/Error/SafeClassRuleError.php @@ -0,0 +1,21 @@ +<?php + +namespace TheCodingMachine\Safe\PHPStan\Rules\Error; + +use PhpParser\Node\Name; + +class SafeClassRuleError extends SafeRuleError +{ + public function __construct(Name $className, int $line) + { + parent::__construct( + "Class $className is unsafe to use. Its methods can return FALSE instead of throwing an exception. Please add 'use Safe\\$className;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library.", + $line, + ); + } + + public function getIdentifier(): string + { + return self::IDENTIFIER_PREFIX . 'class'; + } +} diff --git a/src/Rules/Error/SafeFunctionRuleError.php b/src/Rules/Error/SafeFunctionRuleError.php new file mode 100644 index 0000000..ef27c81 --- /dev/null +++ b/src/Rules/Error/SafeFunctionRuleError.php @@ -0,0 +1,23 @@ +<?php + +namespace TheCodingMachine\Safe\PHPStan\Rules\Error; + +use PhpParser\Node\Name; + +class SafeFunctionRuleError extends SafeRuleError +{ + public function __construct(Name $nodeName, int $line) + { + $functionName = $nodeName->toString(); + + parent::__construct( + "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.", + $line, + ); + } + + public function getIdentifier(): string + { + return self::IDENTIFIER_PREFIX . 'class'; + } +} diff --git a/src/Rules/Error/SafeRuleError.php b/src/Rules/Error/SafeRuleError.php new file mode 100644 index 0000000..5e82745 --- /dev/null +++ b/src/Rules/Error/SafeRuleError.php @@ -0,0 +1,31 @@ +<?php + +namespace TheCodingMachine\Safe\PHPStan\Rules\Error; + +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; + +abstract class SafeRuleError implements RuleError, LineRuleError, IdentifierRuleError +{ + protected const IDENTIFIER_PREFIX = 'theCodingMachineSafe.'; + + private string $message; + private int $line; + + public function __construct(string $message, int $line) + { + $this->message = $message; + $this->line = $line; + } + + public function getMessage(): string + { + return $this->message; + } + + public function getLine(): int + { + return $this->line; + } +} diff --git a/src/Rules/UseSafeClassesRule.php b/src/Rules/UseSafeClassesRule.php index 6dfab48..3db731d 100644 --- a/src/Rules/UseSafeClassesRule.php +++ b/src/Rules/UseSafeClassesRule.php @@ -5,15 +5,9 @@ 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\Rules\Error\SafeClassRuleError; use TheCodingMachine\Safe\PHPStan\Utils\ClassListLoader; -use TheCodingMachine\Safe\PHPStan\Utils\FunctionListLoader; -use PhpParser\Node\Arg; -use PhpParser\Node\Expr; -use PhpParser\Node\Scalar; /** * This rule checks that no "unsafe" classes are instantiated in code. @@ -27,11 +21,6 @@ public function getNodeType(): string return Node\Expr\New_::class; } - /** - * @param Node\Expr\New_ $node - * @param \PHPStan\Analyser\Scope $scope - * @return string[] - */ public function processNode(Node $node, Scope $scope): array { $classNode = $node->class; @@ -43,7 +32,9 @@ public function processNode(Node $node, Scope $scope): array $unsafeClasses = ClassListLoader::getClassList(); if (isset($unsafeClasses[$className])) { - return ["Class $className is unsafe to use. Its methods can return FALSE instead of throwing an exception. Please add 'use Safe\\$className;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library."]; + return [ + new SafeClassRuleError($classNode, $node->getStartLine()), + ]; } return []; diff --git a/src/Rules/UseSafeFunctionsRule.php b/src/Rules/UseSafeFunctionsRule.php index cdbf205..d649184 100644 --- a/src/Rules/UseSafeFunctionsRule.php +++ b/src/Rules/UseSafeFunctionsRule.php @@ -4,15 +4,13 @@ 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; use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Scalar; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; +use TheCodingMachine\Safe\PHPStan\Rules\Error\SafeFunctionRuleError; +use TheCodingMachine\Safe\PHPStan\Utils\FunctionListLoader; /** * This rule checks that no "unsafe" functions are used in code. @@ -26,11 +24,6 @@ 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) { @@ -40,37 +33,38 @@ public function processNode(Node $node, Scope $scope): array $unsafeFunctions = FunctionListLoader::getFunctionList(); if (isset($unsafeFunctions[$functionName])) { - if (version_compare(PHP_VERSION, '7.3.0', '>=')) { - if ($functionName === "json_decode") { - if (count($node->args) == 4) { - if ($this->argValueIncludeJSONTHROWONERROR($node->args[3])) { - return []; - } - } - } - if ($functionName === "json_encode") { - if (count($node->args) >= 2) { - if ($this->argValueIncludeJSONTHROWONERROR($node->args[1])) { - return []; - } - } - } + if ( + $functionName === "json_decode" + && $this->argValueIncludeJSONTHROWONERROR($node->getArgs()[3] ?? null) + ) { + return []; + } + + if ( + $functionName === "json_encode" + && $this->argValueIncludeJSONTHROWONERROR($node->getArgs()[1] ?? null) + ) { + return []; } - 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 [new SafeFunctionRuleError($node->name, $node->getStartLine())]; } return []; } - private function argValueIncludeJSONTHROWONERROR(Arg $arg): bool + private function argValueIncludeJSONTHROWONERROR(?Arg $arg): bool { - $parseValue = function ($expr, array $options) use (&$parseValue): array { + if ($arg === null) { + return false; + } + + $parseValue = static function ($expr, array $options) use (&$parseValue): array { if ($expr instanceof Expr\BinaryOp\BitwiseOr) { return array_merge($parseValue($expr->left, $options), $parseValue($expr->right, $options)); } elseif ($expr instanceof Expr\ConstFetch) { - return array_merge($options, $expr->name->parts); - } elseif ($expr instanceof Scalar\LNumber) { + return array_merge($options, $expr->name->getParts()); + } elseif ($expr instanceof Scalar\Int_) { return array_merge($options, [$expr->value]); } else { return $options; @@ -78,7 +72,7 @@ private function argValueIncludeJSONTHROWONERROR(Arg $arg): bool }; $options = $parseValue($arg->value, []); - if (in_array("JSON_THROW_ON_ERROR", $options)) { + if (in_array("JSON_THROW_ON_ERROR", $options, true)) { return true; } @@ -87,6 +81,6 @@ private function argValueIncludeJSONTHROWONERROR(Arg $arg): bool return ($element & 4194304) == 4194304; }, array_filter($options, function ($element) { return is_int($element); - }))); + })), true); } } From 23fe3a82cf3caa3cdaffce5d793e01933656e381 Mon Sep 17 00:00:00 2001 From: Alessandro Lai <alessandro.lai85@gmail.com> Date: Thu, 21 Nov 2024 22:59:39 +0100 Subject: [PATCH 4/7] Refactor and adapt the TypeExtension to the new PHPStan APIs --- ...afeFunctionsDynamicReturnTypeExtension.php | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php index 529b157..02711e1 100644 --- a/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php @@ -3,6 +3,7 @@ namespace TheCodingMachine\Safe\PHPStan\Type\Php; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; @@ -35,7 +36,12 @@ public function getTypeFromFunctionCall( ): Type { $type = $this->getPreliminarilyResolvedTypeFromFunctionCall($functionReflection, $functionCall, $scope); - $possibleTypes = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + $possibleTypes = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants() + ) + ->getReturnType(); if (TypeCombinator::containsNull($possibleTypes)) { $type = TypeCombinator::addNull($type); @@ -50,28 +56,36 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( Scope $scope ): Type { $argumentPosition = $this->functions[$functionReflection->getName()]; + $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $functionCall->getArgs(), + $functionReflection->getVariants() + ) + ->getReturnType(); + if (count($functionCall->args) <= $argumentPosition) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return $defaultReturnType; } - $subjectArgumentType = $scope->getType($functionCall->args[$argumentPosition]->value); - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if ($subjectArgumentType instanceof MixedType) { + $subjectArgument = $functionCall->args[$argumentPosition]; + if (!$subjectArgument instanceof Arg) { + return $defaultReturnType; + } + + $subjectArgumentType = $scope->getType($subjectArgument->value); + $mixedType = new MixedType(); + if ($subjectArgumentType->isSuperTypeOf($mixedType)->yes()) { 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) { + $stringType = new StringType(); + if ($stringType->isSuperTypeOf($subjectArgumentType)->yes()) { return $stringType; - } elseif ($compareSuperTypes === $isArraySuperType) { - if ($subjectArgumentType instanceof ArrayType) { - return $subjectArgumentType->generalizeValues(); - } - return $subjectArgumentType; + } + + $arrayType = new ArrayType($mixedType, $mixedType); + if ($arrayType->isSuperTypeOf($subjectArgumentType)->yes()) { + return $arrayType; } return $defaultReturnType; From 69f7d412c7f80772626bd9c3a44314d0cd5080fc Mon Sep 17 00:00:00 2001 From: Alessandro Lai <alessandro.lai85@gmail.com> Date: Thu, 21 Nov 2024 23:14:39 +0100 Subject: [PATCH 5/7] Refactor FunctionListLoader to avoid PHPStan errors --- src/Utils/FunctionListLoader.php | 48 +++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/Utils/FunctionListLoader.php b/src/Utils/FunctionListLoader.php index 2f1ad2a..ed351e7 100644 --- a/src/Utils/FunctionListLoader.php +++ b/src/Utils/FunctionListLoader.php @@ -3,33 +3,49 @@ namespace TheCodingMachine\Safe\PHPStan\Utils; -use PHPStan\Analyser\Scope; -use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\MethodReflection; - class FunctionListLoader { /** - * @var string[] + * @var array<string, string> */ - private static $functions; + private static array $functions; /** - * @return string[] + * @return array<string, 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.'); + return self::$functions ??= self::fetchIndexedFunctions(); + } + + /** + * @return array<string, string> + */ + private static function fetchIndexedFunctions(): array + { + 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.'); + } + + if (!is_array($functions)) { + throw new \RuntimeException('The functions list should be an array.'); + } + + $indexedFunctions = []; + + foreach ($functions as $function) { + if (!is_string($function)) { + throw new \RuntimeException('The functions list should contain only strings, got ' . get_debug_type($function)); } + // Let's index these functions by their name - self::$functions = \Safe\array_combine($functions, $functions); + $indexedFunctions[$function] = $function; } - return self::$functions; + + return $indexedFunctions; } } From 755777541b9edb4592362cb011f2843f3c060818 Mon Sep 17 00:00:00 2001 From: Alessandro Lai <alessandro.lai85@gmail.com> Date: Thu, 21 Nov 2024 23:14:59 +0100 Subject: [PATCH 6/7] Reconfigure PHPStan --- composer.json | 2 +- phpstan.neon | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 939e443..14771e8 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ } }, "scripts": { - "phpstan": "phpstan analyse src -c phpstan.neon --level=max --no-progress -vvv", + "phpstan": "phpstan analyse -c phpstan.neon --no-progress -vvv", "cs-fix": "phpcbf", "cs-check": "phpcs" }, diff --git a/phpstan.neon b/phpstan.neon index 3a54d92..46b5aa0 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,4 +1,24 @@ parameters: + level: max + paths: + - src ignoreErrors: + - + message: '#^Implementing PHPStan\\Rules\\IdentifierRuleError is not covered by backward compatibility promise\. The interface might change in a minor PHPStan version\.$#' + identifier: phpstanApi.interface + count: 1 + path: src/Rules/Error/SafeRuleError.php + + - + message: '#^Implementing PHPStan\\Rules\\LineRuleError is not covered by backward compatibility promise\. The interface might change in a minor PHPStan version\.$#' + identifier: phpstanApi.interface + count: 1 + path: src/Rules/Error/SafeRuleError.php + + - + message: '#^Implementing PHPStan\\Rules\\RuleError is not covered by backward compatibility promise\. The interface might change in a minor PHPStan version\.$#' + identifier: phpstanApi.interface + count: 1 + path: src/Rules/Error/SafeRuleError.php includes: - phpstan-safe-rule.neon From 4c802d07d668171bd09292e3bdbd1d8d637444ae Mon Sep 17 00:00:00 2001 From: Alessandro Lai <alessandro.lai85@gmail.com> Date: Thu, 21 Nov 2024 23:22:17 +0100 Subject: [PATCH 7/7] Migrate CI from Travis to Github Actions --- .github/workflows/tests.yaml | 50 ++++++++++++++++++++++++++++++++++++ .travis.yml | 34 ------------------------ 2 files changed, 50 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/tests.yaml delete mode 100644 .travis.yml diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..0669b89 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,50 @@ +name: CI + +on: + pull_request: ~ + push: + branches: + - master + schedule: + - cron: "47 6 * * 1" # once a month, to surface issues with newer dependencies + +jobs: + Tests: + runs-on: 'ubuntu-latest' + strategy: + matrix: + php: + - '7.4' + - '8.0' + - '8.1' + - '8.2' + - '8.3' + - '8.4' + dependencies: ['highest'] + include: + - description: '(lowest)' + php: '7.4' + dependencies: 'lowest' + + name: PHP ${{ matrix.php }} ${{ matrix.description }} + steps: + - name: "Checkout" + uses: actions/checkout@v4 + - name: "Install PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + - name: "Install dependencies" + uses: ramsey/composer-install@v3 + with: + dependency-versions: ${{ matrix.dependencies }} + - name: "Run PHPStan analysis" + run: composer phpstan + - name: "Run tests" + run: vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always + - name: "Upload test coverage" + uses: codecov/codecov-action@v5 + with: + files: './coverage.xml' + fail_ci_if_error: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 43967a5..0000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -language: php -sudo: false -jobs: - include: - - php: 7.4 - env: PREFER_LOWEST="" - before_script: - - &composerupdate - composer update --no-interaction --no-progress --optimize-autoloader $PREFER_LOWEST - script: - - &phpunit - "./vendor/bin/phpunit" - - composer phpstan - - composer cs-check - after_script: - - travis_retry php vendor/bin/php-coveralls -v - - php: 7.1 - env: PREFER_LOWEST="" - before_script: - - *composerupdate - script: - - *phpunit - - php: 7.1 - env: PREFER_LOWEST="--prefer-lowest" - before_script: - - *composerupdate - script: - - *phpunit - -cache: - directories: - - "$HOME/.composer/cache" -after_success: -- vendor/bin/coveralls -v