diff --git a/.gitattributes b/.gitattributes index 51b431136..9cd1bb7da 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,3 +15,4 @@ .travis.yml export-ignore appveyor.yml export-ignore phpunit.xml.dist export-ignore +/phpstan/ export-ignore diff --git a/.travis.yml b/.travis.yml index 0cc13f91a..49822707f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,9 @@ matrix: - php: 7.1 - php: 7.2 - php: 7.3 - env: PHPSTAN=1 + env: + - deps=high + - PHPSTAN=1 - php: 7.3 env: - deps=high @@ -64,8 +66,8 @@ script: - ls -d tests/Composer/Test/* | grep -v TestCase.php | parallel --gnu --keep-order 'echo "Running {} tests"; ./vendor/bin/phpunit -c tests/complete.phpunit.xml --colors=always {} || (echo -e "\e[41mFAILED\e[0m {}" && exit 1);' # Run PHPStan - if [[ $PHPSTAN == "1" ]]; then - bin/composer require --dev phpstan/phpstan-shim:^0.11 --ignore-platform-reqs && - vendor/bin/phpstan.phar analyse --configuration=phpstan/config.neon; + bin/composer require --dev phpstan/phpstan:^0.12 && + vendor/bin/phpstan analyse --configuration=phpstan/config.neon; fi before_deploy: diff --git a/composer.json b/composer.json index d1f98a56a..435aa5c01 100644 --- a/composer.json +++ b/composer.json @@ -66,8 +66,13 @@ }, "autoload-dev": { "psr-4": { - "Composer\\Test\\": "tests/Composer/Test" - } + "Composer\\Test\\": "tests/Composer/Test", + "Composer\\PHPStanRules\\": "phpstan/Rules/src", + "Composer\\PHPStanRulesTests\\": "phpstan/Rules/tests" + }, + "classmap": [ + "phpstan/Rules/tests/data" + ] }, "bin": [ "bin/composer" diff --git a/phpstan/Rules/src/AnonymousFunctionWithThisRule.php b/phpstan/Rules/src/AnonymousFunctionWithThisRule.php new file mode 100644 index 000000000..d9a44ecb5 --- /dev/null +++ b/phpstan/Rules/src/AnonymousFunctionWithThisRule.php @@ -0,0 +1,46 @@ + + */ +final class AnonymousFunctionWithThisRule implements Rule +{ + /** + * @inheritDoc + */ + public function getNodeType(): string + { + return \PhpParser\Node\Expr\Variable::class; + } + + /** + * @inheritDoc + */ + public function processNode(Node $node, Scope $scope): array + { + if (!\is_string($node->name) || $node->name !== 'this') { + return []; + } + + if ($scope->isInClosureBind()) { + return []; + } + + if (!$scope->isInClass()) { + // reported in other standard rule on level 0 + return []; + } + + if ($scope->isInAnonymousFunction()) { + return ['Using $this inside anonymous function is prohibited because of PHP 5.3 support.']; + } + + return []; + } +} diff --git a/phpstan/Rules/tests/AnonymousFunctionWithThisRuleTest.php b/phpstan/Rules/tests/AnonymousFunctionWithThisRuleTest.php new file mode 100644 index 000000000..add285d17 --- /dev/null +++ b/phpstan/Rules/tests/AnonymousFunctionWithThisRuleTest.php @@ -0,0 +1,28 @@ + + */ +final class AnonymousFunctionWithThisRuleTest extends RuleTestCase +{ + /** + * @inheritDoc + */ + protected function getRule(): \PHPStan\Rules\Rule + { + return new AnonymousFunctionWithThisRule(); + } + + public function testWithThis(): void + { + $this->analyse([__DIR__ . '/data/method-with-this.php'], [ + ['Using $this inside anonymous function is prohibited because of PHP 5.3 support.', 13], + ['Using $this inside anonymous function is prohibited because of PHP 5.3 support.', 17], + ]); + } +} diff --git a/phpstan/Rules/tests/data/method-with-this.php b/phpstan/Rules/tests/data/method-with-this.php new file mode 100644 index 000000000..e0b15bffa --- /dev/null +++ b/phpstan/Rules/tests/data/method-with-this.php @@ -0,0 +1,34 @@ +firstProp; + }; + + call_user_func(function() { + $this->funMethod(); + }, $this); + + $bind = 'bind'; + function() use($bind) { + + }; + } +} + +function global_ok() { + $_SERVER['REMOTE_ADDR']; +} + +function global_this() { + // not checked by our rule, it is checked with standard phpstan rule on level 0 + $this['REMOTE_ADDR']; +} diff --git a/phpstan/config.neon b/phpstan/config.neon index ae53ac531..599d27d51 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -1,24 +1,12 @@ parameters: autoload_files: - - phpstan/autoload.php + - autoload.php level: 0 excludes_analyse: - - 'tests/Composer/Test/Fixtures' - - 'tests/Composer/Test/Autoload/Fixtures' - - 'tests/Composer/Test/Plugin/Fixtures' + - '../tests/Composer/Test/Fixtures/*' + - '../tests/Composer/Test/Autoload/Fixtures/*' + - '../tests/Composer/Test/Plugin/Fixtures/*' ignoreErrors: - # unused parameters - - '~^Constructor of class Composer\\Repository\\VcsRepository has an unused parameter \$dispatcher\.$~' - - '~^Constructor of class Composer\\Repository\\PearRepository has an unused parameter \$dispatcher\.$~' - - '~^Constructor of class Composer\\Util\\Http\\CurlDownloader has an unused parameter \$disableTls\.$~' - - '~^Constructor of class Composer\\Util\\Http\\CurlDownloader has an unused parameter \$options\.$~' - - '~^Constructor of class Composer\\Repository\\PearRepository has an unused parameter \$config\.$~' - - # unused uses - - '~^Anonymous function has an unused use \$io\.$~' - - '~^Anonymous function has an unused use \$cache\.$~' - - '~^Anonymous function has an unused use \$path\.$~' - # ion cube is not installed - '~^Function ioncube_loader_\w+ not found\.$~' # rar is not installed @@ -33,14 +21,17 @@ parameters: # variable defined in eval - '~^Undefined variable: \$res$~' - # always checked whether the class exists - - '~^Instantiated class Symfony\\Component\\Console\\Terminal not found\.$~' - - '~^Class Symfony\\Component\\Console\\Input\\StreamableInputInterface not found\.$~' - - '~^Call to an undefined static method Symfony\\Component\\Process\\Process::fromShellCommandline\(\).$~' + # we don't have different constructors for parent/child + - '~^Unsafe usage of new static\(\)\.$~' - # parent call in test mocks - - '~^Composer\\Test\\Mock\\HttpDownloaderMock::__construct\(\) does not call parent constructor from Composer\\Util\\HttpDownloader\.$~' - - '~^Composer\\Test\\Mock\\InstallationManagerMock::__construct\(\) does not call parent constructor from Composer\\Installer\\InstallationManager\.$~' + # hhvm should have support for $this in closures + - + count: 1 + message: '~^Using \$this inside anonymous function is prohibited because of PHP 5\.3 support\.$~' + path: '../tests/Composer/Test/Repository/PlatformRepositoryTest.php' paths: - - src - - tests + - ../src + - ../tests + +rules: + - Composer\PHPStanRules\AnonymousFunctionWithThisRule diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 7c7e45354..9775c27d9 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -113,7 +113,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $accept = null; $reject = null; - $download = function () use ($io, $output, $httpDownloader, $cache, $eventDispatcher, $package, $fileName, $path, &$urls, &$accept, &$reject) { + $download = function () use ($io, $output, $httpDownloader, $cache, $eventDispatcher, $package, $fileName, &$urls, &$accept, &$reject) { $url = reset($urls); if ($eventDispatcher) { @@ -160,7 +160,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface }); }; - $accept = function ($response) use ($io, $cache, $package, $fileName, $path, $self, &$urls) { + $accept = function ($response) use ($cache, $package, $fileName, $self, &$urls) { $url = reset($urls); $cacheKey = $url['cacheKey']; @@ -174,7 +174,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface return $fileName; }; - $reject = function ($e) use ($io, &$urls, $download, $fileName, $path, $package, &$retries, $filesystem, $self) { + $reject = function ($e) use ($io, &$urls, $download, $fileName, $package, &$retries, $filesystem, $self) { // clean up if (file_exists($fileName)) { $filesystem->unlink($fileName); diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 9c3fb6884..8d1579b63 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -246,7 +246,7 @@ class InstallationManager $dispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation); } }, function ($e) use ($jobType, $installer, $package, $initialPackage, $loop, $io) { - $this->io->writeError(' ' . ucfirst($jobType) .' of '.$package->getPrettyName().' failed'); + $io->writeError(' ' . ucfirst($jobType) .' of '.$package->getPrettyName().' failed'); $promise = $installer->cleanup($jobType, $package, $initialPackage); if ($promise) { diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index e4cfbba83..02f2a05fe 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -1119,7 +1119,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return $data; }; - $reject = function ($e) use (&$retries, $httpDownloader, $filename, $options, &$reject, $accept, $io, $url, $cache, &$degradedMode) { + $reject = function ($e) use (&$retries, $httpDownloader, $filename, $options, &$reject, $accept, $io, $url, &$degradedMode) { if ($e instanceof TransportException && $e->getStatusCode() === 404) { return false; } diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index c4b6c5424..8a117a232 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -190,7 +190,7 @@ class HttpDownloader $downloader->scheduleNextJob(); return $response; - }, function ($e) use ($io, &$job, $downloader) { + }, function ($e) use (&$job, $downloader) { $job['status'] = HttpDownloader::STATUS_FAILED; $job['exception'] = $e;