diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 00000000..bc71b62f --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,2 @@ +coverage_clover: clover.xml +json_path: coveralls-upload.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..b8424729 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +.coveralls.yml export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +phpcs.xml export-ignore +phpunit.xml.dist export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ac54a172 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +vendor/ +phpunit.xml +clover.xml +coveralls-upload.json diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..1aeaccab --- /dev/null +++ b/.travis.yml @@ -0,0 +1,55 @@ +sudo: false + +language: php + +cache: + directories: + - $HOME/.composer/cache + +env: + global: + - COMPOSER_ARGS="--no-interaction" + - COVERAGE_DEPS="php-coveralls/php-coveralls" + +matrix: + include: + - php: 7.1 + env: + - DEPS=lowest + - php: 7.1 + env: + - DEPS=locked + - CS_CHECK=true + - TEST_COVERAGE=true + - php: 7.1 + env: + - DEPS=latest + - php: 7.2 + env: + - DEPS=lowest + - php: 7.2 + env: + - DEPS=locked + - php: 7.2 + env: + - DEPS=latest + +before_install: + - if [[ $TEST_COVERAGE != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi + +install: + - travis_retry composer install $COMPOSER_ARGS + - if [[ $DEPS == 'latest' ]]; then travis_retry composer update $COMPOSER_ARGS ; fi + - if [[ $DEPS == 'lowest' ]]; then travis_retry composer update --prefer-lowest --prefer-stable $COMPOSER_ARGS ; fi + - if [[ $TEST_COVERAGE == 'true' ]]; then travis_retry composer require --dev $COMPOSER_ARGS $COVERAGE_DEPS ; fi + - stty cols 120 && composer show + +script: + - if [[ $TEST_COVERAGE == 'true' ]]; then composer test-coverage ; else composer test ; fi + - if [[ $CS_CHECK == 'true' ]]; then composer cs-check ; fi + +after_script: + - if [[ $TEST_COVERAGE == 'true' ]]; then travis_retry php vendor/bin/php-coveralls -v ; fi + +notifications: + email: false diff --git a/LICENSE.md b/LICENSE.md index badc26ea..bd188312 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,16 +1,15 @@ -Copyright (c) 2016, Zend Technologies USA, Inc. - +Copyright (c) 2016-2018, Zend Technologies USA, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -- Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. +- Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. -- Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. +- Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. - Neither the name of Zend Technologies USA, Inc. nor the names of its contributors may be used to endorse or promote products derived from this diff --git a/README.md b/README.md index 2a7e94a8..a9484c22 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ -Zend Framework Coding Standard -============================== +# Zend Framework Coding Standard + +[![Build Status](https://travis-ci.org/zendframework/zend-coding-standard.svg?branch=master)](https://travis-ci.org/zendframework/zend-coding-standard) +[![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-coding-standard/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-coding-standard?branch=master) Repository with all coding standard ruleset for Zend Framework repositories. -Installation ------------- +## Installation 1. Install the module via composer by running: @@ -26,7 +27,9 @@ Installation ```xml - + @@ -40,8 +43,7 @@ You can add or exclude some locations in that file. For a reference please see: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml -Usage ------ +## Usage * To run checks only: @@ -50,7 +52,7 @@ Usage ``` * To automatically fix many CS issues: - + ```bash $ composer cs-fix ``` diff --git a/composer.json b/composer.json index 89f82020..7209b89d 100644 --- a/composer.json +++ b/composer.json @@ -4,9 +4,38 @@ "license": "BSD-3-Clause", "keywords": [ "zf", + "zendframework", "coding standard" ], "require": { - "squizlabs/php_codesniffer": "^2.7" + "php": "^7.1", + "squizlabs/php_codesniffer": "3.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^7.0.1" + }, + "autoload": { + "psr-4": { + "ZendCodingStandard\\": "src/ZendCodingStandard/" + } + }, + "autoload-dev": { + "files": [ + "vendor/squizlabs/php_codesniffer/autoload.php" + ], + "psr-4": { + "PHP_CodeSniffer\\": "vendor/squizlabs/php_codesniffer/src/", + "ZendCodingStandardTest\\": "test/" + } + }, + "scripts": { + "check": [ + "@cs-check", + "@test" + ], + "cs-check": "phpcs", + "cs-fix": "phpcbf", + "test": "phpunit --colors=always", + "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..a3b622bf --- /dev/null +++ b/composer.lock @@ -0,0 +1,1526 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "2528f561888e57981d5f87311dcf666c", + "packages": [ + { + "name": "squizlabs/php_codesniffer", + "version": "3.2.3", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "4842476c434e375f9d3182ff7b89059583aa8b27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/4842476c434e375f9d3182ff7b89059583aa8b27", + "reference": "4842476c434e375f9d3182ff7b89059583aa8b27", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "http://www.squizlabs.com/php-codesniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2018-02-20T21:35:23+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "^6.2.3", + "squizlabs/php_codesniffer": "^3.0.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2017-07-22T11:58:36+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2017-10-19T19:58:43+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^1.0.1", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2017-03-05T18:14:27+00:00" + }, + { + "name": "phar-io/version", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2017-03-05T17:38:23+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2017-09-11T18:02:19+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "4.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "94fd0001232e47129dd3504189fa1c7225010d08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", + "reference": "94fd0001232e47129dd3504189fa1c7225010d08", + "shasum": "" + }, + "require": { + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0.0", + "phpdocumentor/type-resolver": "^0.4.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "doctrine/instantiator": "~1.0.5", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2017-11-30T07:14:17+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2017-07-14T14:27:02+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.7.5", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/dfd6be44111a7c41c2e884a336cc4f461b3b2401", + "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", + "sebastian/comparator": "^1.1|^2.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.5|^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2018-02-19T10:16:54+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "f8ca4b604baf23dab89d87773c28cc07405189ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f8ca4b604baf23dab89d87773c28cc07405189ba", + "reference": "f8ca4b604baf23dab89d87773c28cc07405189ba", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.1", + "phpunit/php-file-iterator": "^1.4.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "suggest": { + "ext-xdebug": "^2.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2018-02-02T07:01:41+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2017-11-27T13:52:08+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8b8454ea6958c3dee38453d3bd571e023108c91f", + "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2018-02-01T13:07:23+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "21ad88bbba7c3d93530d93994e0a33cd45f02ace" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/21ad88bbba7c3d93530d93994e0a33cd45f02ace", + "reference": "21ad88bbba7c3d93530d93994e0a33cd45f02ace", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2018-02-01T13:16:43+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "316555dbd0ed4097bbdd17c65ab416bf27a472e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/316555dbd0ed4097bbdd17c65ab416bf27a472e9", + "reference": "316555dbd0ed4097bbdd17c65ab416bf27a472e9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.6.1", + "phar-io/manifest": "^1.0.1", + "phar-io/version": "^1.0", + "php": "^7.1", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^6.0", + "phpunit/php-file-iterator": "^1.4.3", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.0", + "phpunit/phpunit-mock-objects": "^6.0", + "sebastian/comparator": "^2.1", + "sebastian/diff": "^3.0", + "sebastian/environment": "^3.1", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^1.0", + "sebastian/version": "^2.0.1" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-xdebug": "*", + "phpunit/php-invoker": "^2.0" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2018-02-13T06:08:08+00:00" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "e3249dedc2d99259ccae6affbc2684eac37c2e53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/e3249dedc2d99259ccae6affbc2684eac37c2e53", + "reference": "e3249dedc2d99259ccae6affbc2684eac37c2e53", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.5", + "php": "^7.1", + "phpunit/php-text-template": "^1.2.1", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2018-02-15T05:27:38+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04T06:30:41+00:00" + }, + { + "name": "sebastian/comparator", + "version": "2.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", + "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/diff": "^2.0 || ^3.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2018-02-01T13:46:46+00:00" + }, + { + "name": "sebastian/diff", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "e09160918c66281713f1c324c1f4c4c3037ba1e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/e09160918c66281713f1c324c1f4c4c3037ba1e8", + "reference": "e09160918c66281713f1c324c1f4c4c3037ba1e8", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0", + "symfony/process": "^2 || ^3.3 || ^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "time": "2018-02-01T13:45:15+00:00" + }, + { + "name": "sebastian/environment", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2017-07-01T08:51:00+00:00" + }, + { + "name": "sebastian/exporter", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", + "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2017-04-03T13:19:02+00:00" + }, + { + "name": "sebastian/global-state", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2017-04-27T15:39:26+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2017-08-03T12:35:26+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "time": "2017-03-29T09:07:27+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2017-03-03T06:23:57+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28T20:34:47+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "time": "2017-04-07T12:08:54+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "0df1908962e7a3071564e857d86874dad1ef204a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", + "reference": "0df1908962e7a3071564e857d86874dad1ef204a", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2018-01-29T19:49:41+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^7.1" + }, + "platform-dev": [] +} diff --git a/CONDUCT.md b/docs/CODE_OF_CONDUCT.md similarity index 96% rename from CONDUCT.md rename to docs/CODE_OF_CONDUCT.md index c663d2be..02fafcd1 100644 --- a/CONDUCT.md +++ b/docs/CODE_OF_CONDUCT.md @@ -1,6 +1,6 @@ # Contributor Code of Conduct -The Zend Framework project adheres to [The Code Manifesto](http://codemanifesto.com) +This project adheres to [The Code Manifesto](http://codemanifesto.com) as its guidelines for contributor interactions. ## The Code Manifesto diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 00000000..c0511a0f --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,189 @@ +# CONTRIBUTING + +## RESOURCES + +If you wish to contribute to this project, please be sure to +read/subscribe to the following resources: + + - [Coding Standards](https://github.com/zendframework/zend-coding-standard) + - [Forums](https://discourse.zendframework.com/c/contributors) + - [Slack](https://zendframework-slack.herokuapp.com) + - [Code of Conduct](CODE_OF_CONDUCT.md) + +If you are working on new features or refactoring +[create a proposal](https://github.com/zendframework/zend-coding-standard/issues/new). + +## RUNNING TESTS + +To run tests: + +- Clone the repository: + + ```console + $ git clone git://github.com/zendframework/zend-coding-standard.git + $ cd zend-coding-standard + ``` + +- Install dependencies via composer: + + ```console + $ composer install + ``` + + If you don't have `composer` installed, please download it from https://getcomposer.org/download/ + +- Run the tests using the "test" command shipped in the `composer.json`: + + ```console + $ composer test + ``` + +You can turn on conditional tests with the `phpunit.xml` file. +To do so: + + - Copy `phpunit.xml.dist` file to `phpunit.xml` + - Edit `phpunit.xml` to enable any specific functionality you + want to test, as well as to provide test values to utilize. + +## Running Coding Standards Checks + +First, ensure you've installed dependencies via composer, per the previous +section on running tests. + +To run CS checks only: + +```console +$ composer cs-check +``` + +To attempt to automatically fix common CS issues: + +```console +$ composer cs-fix +``` + +If the above fixes any CS issues, please re-run the tests to ensure +they pass, and make sure you add and commit the changes after verification. + +## Recommended Workflow for Contributions + +Your first step is to establish a public repository from which we can +pull your work into the master repository. We recommend using +[GitHub](https://github.com), as that is where the component is already hosted. + +1. Setup a [GitHub account](https://github.com/), if you haven't yet +2. Fork the repository (https://github.com/zendframework/zend-coding-standard) +3. Clone the canonical repository locally and enter it. + + ```console + $ git clone git://github.com/zendframework/zend-coding-standard.git + $ cd zend-coding-standard + ``` + +4. Add a remote to your fork; substitute your GitHub username in the command + below. + + ```console + $ git remote add {username} git@github.com:{username}/zend-coding-standard.git + $ git fetch {username} + ``` + +### Keeping Up-to-Date + +Periodically, you should update your fork or personal repository to +match the canonical ZF repository. Assuming you have setup your local repository +per the instructions above, you can do the following: + + +```console +$ git checkout master +$ git fetch origin +$ git rebase origin/master +# OPTIONALLY, to keep your remote up-to-date - +$ git push {username} master:master +``` + +If you're tracking other branches -- for example, the "develop" branch, where +new feature development occurs -- you'll want to do the same operations for that +branch; simply substitute "develop" for "master". + +### Working on a patch + +We recommend you do each new feature or bugfix in a new branch. This simplifies +the task of code review as well as the task of merging your changes into the +canonical repository. + +A typical workflow will then consist of the following: + +1. Create a new local branch based off either your master or develop branch. +2. Switch to your new local branch. (This step can be combined with the + previous step with the use of `git checkout -b`.) +3. Do some work, commit, repeat as necessary. +4. Push the local branch to your remote repository. +5. Send a pull request. + +The mechanics of this process are actually quite trivial. Below, we will +create a branch for fixing an issue in the tracker. + +```console +$ git checkout -b hotfix/9295 +Switched to a new branch 'hotfix/9295' +``` + +... do some work ... + + +```console +$ git commit +``` + +... write your log message ... + + +```console +$ git push {username} hotfix/9295:hotfix/9295 +Counting objects: 38, done. +Delta compression using up to 2 threads. +Compression objects: 100% (18/18), done. +Writing objects: 100% (20/20), 8.19KiB, done. +Total 20 (delta 12), reused 0 (delta 0) +To ssh://git@github.com/{username}/zend-coding-standard.git + b5583aa..4f51698 HEAD -> master +``` + +To send a pull request, you have two options. + +If using GitHub, you can do the pull request from there. Navigate to +your repository, select the branch you just created, and then select the +"Pull Request" button in the upper right. Select the user/organization +"zendframework" (or whatever the upstream organization is) as the recipient. + +#### What branch to issue the pull request against? + +Which branch should you issue a pull request against? + +- For fixes against the stable release, issue the pull request against the + "master" branch. +- For new features, or fixes that introduce new elements to the public API (such + as new public methods or properties), issue the pull request against the + "develop" branch. + +### Branch Cleanup + +As you might imagine, if you are a frequent contributor, you'll start to +get a ton of branches both locally and on your remote. + +Once you know that your changes have been accepted to the master +repository, we suggest doing some cleanup of these branches. + +- Local branch cleanup + + ```console + $ git branch -d + ``` + +- Remote branch removal + + ```console + $ git push {username} : + ``` diff --git a/docs/ISSUE_TEMPLATE.md b/docs/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..3bdc945e --- /dev/null +++ b/docs/ISSUE_TEMPLATE.md @@ -0,0 +1,19 @@ + - [ ] I was not able to find an [open](https://github.com/zendframework/zend-coding-standard/issues?q=is%3Aopen) or [closed](https://github.com/zendframework/zend-coding-standard/issues?q=is%3Aclosed) issue matching what I'm seeing. + - [ ] This is not a question. (Questions should be asked on [slack](https://zendframework.slack.com/) ([Signup for Slack here](https://zendframework-slack.herokuapp.com/)) or our [forums](https://discourse.zendframework.com/).) + +Provide a narrative description of what you are trying to accomplish. + +### Code to reproduce the issue + + + +```php +``` + +### Expected results + + + +### Actual results + + diff --git a/docs/PULL_REQUEST_TEMPLATE.md b/docs/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..f00d90c0 --- /dev/null +++ b/docs/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,25 @@ +Provide a narrative description of what you are trying to accomplish: + +- [ ] Are you fixing a bug? + - [ ] Detail how the bug is invoked currently. + - [ ] Detail the original, incorrect behavior. + - [ ] Detail the new, expected behavior. + - [ ] Base your feature on the `master` branch, and submit against that branch. + - [ ] Add a regression test that demonstrates the bug, and proves the fix. + - [ ] Add a `CHANGELOG.md` entry for the fix. + +- [ ] Are you creating a new feature? + - [ ] Why is the new feature needed? What purpose does it serve? + - [ ] How will users use the new feature? + - [ ] Base your feature on the `develop` branch, and submit against that branch. + - [ ] Add only one feature per pull request; split multiple features over multiple pull requests + - [ ] Add tests for the new feature. + - [ ] Add documentation for the new feature. + - [ ] Add a `CHANGELOG.md` entry for the new feature. + +- [ ] Is this related to quality assurance? + + +- [ ] Is this related to documentation? + + diff --git a/docs/SUPPORT.md b/docs/SUPPORT.md new file mode 100644 index 00000000..8ac51a29 --- /dev/null +++ b/docs/SUPPORT.md @@ -0,0 +1,25 @@ +# Getting Support + +Zend Framework offers three support channels: + +- For real-time questions, use our + [Slack](https://zendframework-slack.herokuapp.com) +- For detailed questions (e.g., those requiring examples) use our + [forums](https://discourse.zendframework.com/c/questions/components) +- To report issues, use this repository's + [issue tracker](https://github.com/zendframework/zend-coding-standard/issues/new) + +**DO NOT** use the issue tracker to ask questions; use Slack or the forums for +that. Questions posed to the issue tracker will be closed. + +When reporting an issue, please include the following details: + +- A narrative description of what you are trying to accomplish. +- The minimum code necessary to reproduce the issue. +- The expected results of exercising that code. +- The actual results received. + +We may ask for additional details: what version of the library you are using, +and what PHP version was used to reproduce the issue. + +You may also submit a failing test case as a pull request. diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 00000000..13388786 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,11 @@ + + + + + src + test + + test/*.inc + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..5fbd71f4 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,21 @@ + + + + + ./test + + + + + ./src + + + + + + + diff --git a/ruleset.xml b/ruleset.xml index 2e2536e0..35c6319a 100644 --- a/ruleset.xml +++ b/ruleset.xml @@ -1,23 +1,4 @@ - Zend Framework Coding Standard - - - - - - - - - - - - - - - - - - - + diff --git a/src/ZendCodingStandard/CodingStandard.php b/src/ZendCodingStandard/CodingStandard.php new file mode 100644 index 00000000..e1919fd4 --- /dev/null +++ b/src/ZendCodingStandard/CodingStandard.php @@ -0,0 +1,115 @@ + type2). + $matches = []; + $pattern = '/^array\(\s*([^\s^=^>]*)(\s*=>\s*(.*))?\s*\)/i'; + if (preg_match($pattern, $varType, $matches) !== 0) { + $type1 = ''; + if (isset($matches[1])) { + $type1 = $matches[1]; + } + + $type2 = ''; + if (isset($matches[3])) { + $type2 = $matches[3]; + } + + $type1 = self::suggestType($type1); + $type2 = self::suggestType($type2); + if ($type2 !== '') { + $type2 = ' => ' . $type2; + } + + if ($type1 || $type2) { + return sprintf('array(%s%s)', $type1, $type2); + } + } + + return 'array'; + } + + return Common::suggestType($varType); + } + + public static function isTraitUse(File $phpcsFile, int $stackPtr) : bool + { + $tokens = $phpcsFile->getTokens(); + + // Ignore USE keywords inside closures. + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + if ($tokens[$next]['code'] === T_OPEN_PARENTHESIS) { + return false; + } + + // Ignore global USE keywords. + if (! $phpcsFile->hasCondition($stackPtr, [T_CLASS, T_TRAIT, T_ANON_CLASS])) { + return false; + } + + return true; + } + + public static function isGlobalUse(File $phpcsFile, int $stackPtr) : bool + { + $tokens = $phpcsFile->getTokens(); + + // Ignore USE keywords inside closures. + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + if ($tokens[$next]['code'] === T_OPEN_PARENTHESIS) { + return false; + } + + // Ignore USE keywords for traits. + if ($phpcsFile->hasCondition($stackPtr, [T_CLASS, T_TRAIT, T_ANON_CLASS])) { + return false; + } + + return true; + } +} diff --git a/src/ZendCodingStandard/Helper/Methods.php b/src/ZendCodingStandard/Helper/Methods.php new file mode 100644 index 00000000..460bcdc0 --- /dev/null +++ b/src/ZendCodingStandard/Helper/Methods.php @@ -0,0 +1,404 @@ +currentFile !== $phpcsFile) { + $this->currentFile = $phpcsFile; + $this->currentNamespace = null; + } + + $namespace = $this->getNamespace($phpcsFile, $stackPtr); + if ($this->currentNamespace !== $namespace) { + $this->currentNamespace = $namespace; + $this->importedClasses = $this->getGlobalUses($phpcsFile, $stackPtr); + } + + $tokens = $phpcsFile->getTokens(); + + if ($tokens[$stackPtr]['code'] !== T_DOC_COMMENT_TAG) { + $this->methodName = $phpcsFile->getDeclarationName($stackPtr); + $this->isSpecialMethod = $this->methodName === '__construct' || $this->methodName === '__destruct'; + } + + // Get class name of the method, name of the parent class and implemented interfaces names + $this->className = null; + $this->parentClassName = null; + $this->implementedInterfaceNames = []; + if ($tokens[$stackPtr]['conditions']) { + $conditionCode = end($tokens[$stackPtr]['conditions']); + if (in_array($conditionCode, Tokens::$ooScopeTokens, true)) { + $conditionPtr = key($tokens[$stackPtr]['conditions']); + $this->className = $phpcsFile->getDeclarationName($conditionPtr); + if ($conditionCode !== T_INTERFACE) { + $this->parentClassName = $phpcsFile->findExtendedClassName($conditionPtr) ?: null; + } + $this->implementedInterfaceNames = $phpcsFile->findImplementedInterfaceNames($conditionPtr) ?: []; + } + } + } + + public function sortTypes(string $a, string $b) : int + { + $a = strtolower(str_replace('\\', ':', $a)); + $b = strtolower(str_replace('\\', ':', $b)); + + if ($a === 'null' || strpos($a, 'null[') === 0) { + return -1; + } + + if ($b === 'null' || strpos($b, 'null[') === 0) { + return 1; + } + + if ($a === 'true' || $a === 'false') { + return -1; + } + + if ($b === 'true' || $b === 'false') { + return 1; + } + + $aIsSimple = array_filter($this->simpleReturnTypes, function ($v) use ($a) { + return $v === $a || strpos($a, $v . '[') === 0; + }); + $bIsSimple = array_filter($this->simpleReturnTypes, function ($v) use ($b) { + return $v === $b || strpos($b, $v . '[') === 0; + }); + + if ($aIsSimple && $bIsSimple) { + return strcmp($a, $b); + } + + if ($aIsSimple) { + return -1; + } + + if ($bIsSimple) { + return 1; + } + + return strcmp( + preg_replace('/^:/', '', $a), + preg_replace('/^:/', '', $b) + ); + } + + private function getSuggestedType(string $class) : string + { + $prefix = $class[0] === '?' ? '?' : ''; + $suffix = strstr($class, '['); + $clear = strtolower(strtr($class, ['?' => '', '[' => '', ']' => ''])); + + if (in_array($clear, $this->simpleReturnTypes + ['static' => 'static'], true)) { + return $prefix . $clear . $suffix; + } + + $suggested = CodingStandard::suggestType($clear); + if ($suggested !== $clear) { + return $prefix . $suggested . $suffix; + } + + // Is it a current class? + if ($this->isClassName($clear)) { + return $prefix . 'self' . $suffix; + } + + // Is it a parent class? + if ($this->isParentClassName($clear)) { + return $prefix . 'parent' . $suffix; + } + + // Is the class imported? + if (isset($this->importedClasses[$clear])) { + return $prefix . $this->importedClasses[$clear]['alias'] . $suffix; + } + + if ($clear[0] === '\\') { + $ltrim = ltrim($clear, '\\'); + foreach ($this->importedClasses as $use) { + if (strtolower($use['class']) === $ltrim) { + return $prefix . $use['alias'] . $suffix; + } + + if (stripos($ltrim, $use['class'] . '\\') === 0) { + $clear = ltrim(strtr($class, ['?' => '', '[' => '', ']' => '']), '\\'); + $name = substr($clear, strlen($use['class'])); + + return $prefix . $use['alias'] . $name . $suffix; + } + } + } + + return $class; + } + + private function typesMatch(string $typeHint, string $typeStr) : bool + { + $isNullable = $typeHint[0] === '?'; + $lowerTypeHint = strtolower($isNullable ? substr($typeHint, 1) : $typeHint); + $lowerTypeStr = strtolower($typeStr); + + $types = explode('|', $lowerTypeStr); + $count = count($types); + + // For nullable types we expect null and type in PHPDocs + if ($isNullable && $count !== 2) { + return false; + } + + // If type is not nullable PHPDocs should just containt type name + if (! $isNullable && $count !== 1) { + return false; + } + + $fqcnTypeHint = strtolower($this->getFQCN($lowerTypeHint)); + foreach ($types as $key => $type) { + if ($type === 'null') { + continue; + } + + $types[$key] = strtolower($this->getFQCN($type)); + } + $fqcnTypes = implode('|', $types); + + return $fqcnTypeHint === $fqcnTypes + || ($isNullable + && ('null|' . $fqcnTypeHint === $fqcnTypes + || $fqcnTypeHint . '|null' === $fqcnTypes)); + } + + private function getFQCN(string $class) : string + { + // It is a simple type + if (in_array(strtolower($class), $this->simpleReturnTypes, true)) { + return $class; + } + + // It is already FQCN + if ($class[0] === '\\') { + return $class; + } + + // It is an imported class + if (isset($this->importedClasses[$class])) { + return '\\' . $this->importedClasses[$class]['class']; + } + + // It is a class from the current namespace + return ($this->currentNamespace ? '\\' . $this->currentNamespace : '') . '\\' . $class; + } + + private function isClassName(string $name) : bool + { + if (! $this->className) { + return false; + } + + $ns = strtolower($this->currentNamespace); + $lowerClassName = strtolower($this->className); + $lowerFQCN = ($ns ? '\\' . $ns : '') . '\\' . $lowerClassName; + $lower = strtolower($name); + + return $lower === $lowerFQCN + || $lower === $lowerClassName; + } + + private function isParentClassName(string $name) : bool + { + if (! $this->parentClassName) { + return false; + } + + $lowerParentClassName = strtolower($this->parentClassName); + $lowerFQCN = strtolower($this->getFQCN($lowerParentClassName)); + $lower = strtolower($name); + + return $lower === $lowerFQCN + || $lower === $lowerParentClassName; + } + + private function removeTag(File $phpcsFile, int $tagPtr) : void + { + $tokens = $phpcsFile->getTokens(); + + $phpcsFile->fixer->beginChangeset(); + if ($tokens[$tagPtr - 1]['code'] === T_DOC_COMMENT_WHITESPACE + && $tokens[$tagPtr + 3]['code'] === T_DOC_COMMENT_WHITESPACE + ) { + $phpcsFile->fixer->replaceToken($tagPtr - 1, ''); + } + + $phpcsFile->fixer->replaceToken($tagPtr, ''); + $phpcsFile->fixer->replaceToken($tagPtr + 1, ''); + $phpcsFile->fixer->replaceToken($tagPtr + 2, ''); + $phpcsFile->fixer->endChangeset(); + } + + private function getCommentStart(File $phpcsFile, int $stackPtr) : ?int + { + $tokens = $phpcsFile->getTokens(); + $skip = Tokens::$methodPrefixes + + [T_WHITESPACE => T_WHITESPACE]; + + $commentEnd = $phpcsFile->findPrevious($skip, $stackPtr - 1, null, true); + // There is no doc-comment for the function. + if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG) { + return null; + } + + return $tokens[$commentEnd]['comment_opener']; + } + + private function isThis(string $tag, string $type) : bool + { + if ($tag !== '@return') { + return false; + } + + return in_array(strtolower($type), ['$this', '$this|null', 'null|$this'], true); + } + + private function isVariable(string $str) : bool + { + return strpos($str, '$') === 0 + || strpos($str, '...$') === 0; + } + + private function isType(string $tag, string $type) : bool + { + if ($this->isThis($tag, $type)) { + return true; + } + + return (bool) preg_match('/^((?:\\\\?[a-z0-9]+)+(?:\[\])*)(\|(?:\\\\?[a-z0-9]+)+(?:\[\])*)*$/i', $type); + } +} diff --git a/src/ZendCodingStandard/Helper/Namespaces.php b/src/ZendCodingStandard/Helper/Namespaces.php new file mode 100644 index 00000000..52636f4c --- /dev/null +++ b/src/ZendCodingStandard/Helper/Namespaces.php @@ -0,0 +1,255 @@ +findPrevious(T_NAMESPACE, $stackPtr - 1)) { + $nsEnd = $phpcsFile->findNext([T_NS_SEPARATOR, T_STRING, T_WHITESPACE], $nsStart + 1, null, true); + return trim($phpcsFile->getTokensAsString($nsStart + 1, $nsEnd - $nsStart - 1)); + } + + return ''; + } + + /** + * @return array Array of imported classes { + * @var array $_ Key is lowercase class alias name { + * @var string $alias Original class alias name + * @var string $class FQCN + * } + * } + */ + private function getGlobalUses(File $phpcsFile, int $stackPtr = 0) : array + { + $first = 0; + $last = $phpcsFile->numTokens; + + $tokens = $phpcsFile->getTokens(); + + $nsStart = $phpcsFile->findPrevious(T_NAMESPACE, $stackPtr); + if ($nsStart && isset($tokens[$nsStart]['scope_opener'])) { + $first = $tokens[$nsStart]['scope_opener']; + $last = $tokens[$nsStart]['scope_closer']; + } + + $imports = []; + + $use = $first; + while ($use = $phpcsFile->findNext(T_USE, $use + 1, $last)) { + if (! empty($tokens[$use]['conditions'])) { + continue; + } + + if (isset($phpcsFile->getMetrics()[UnusedUseStatementSniff::class]['values'][$use])) { + continue; + } + + $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, $use + 1, null, true); + + if ($tokens[$nextToken]['code'] === T_STRING + && in_array(strtolower($tokens[$nextToken]['content']), ['const', 'function'], true) + ) { + continue; + } + + $end = $phpcsFile->findNext( + [T_NS_SEPARATOR, T_STRING], + $nextToken + 1, + null, + true + ); + + $class = trim($phpcsFile->getTokensAsString($nextToken, $end - $nextToken)); + + $endOfStatement = $phpcsFile->findEndOfStatement($use); + if ($aliasStart = $phpcsFile->findNext([T_WHITESPACE, T_AS], $end + 1, $endOfStatement, true)) { + $alias = trim($phpcsFile->getTokensAsString($aliasStart, $endOfStatement - $aliasStart)); + } else { + if (strrchr($class, '\\') !== false) { + $alias = substr(strrchr($class, '\\'), 1); + } else { + $alias = $class; + } + } + + $imports[strtolower($alias)] = ['alias' => $alias, 'class' => $class]; + } + + return $imports; + } + + /** + * @return false|int + */ + private function isFunctionUse(File $phpcsFile, int $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + + if ($tokens[$next]['code'] === T_STRING + && strtolower($tokens[$next]['content']) === 'function' + ) { + return $next; + } + + return false; + } + + /** + * @return false|int + */ + private function isConstUse(File $phpcsFile, int $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + + if ($tokens[$next]['code'] === T_STRING + && strtolower($tokens[$next]['content']) === 'const' + ) { + return $next; + } + + return false; + } + + /** + * @return array Array of imported constants { + * @var array $_ Key is lowercase constant name { + * @var string $name Original constant name + * @var string $fqn Fully qualified constant name without leading slashes + * } + * } + */ + private function getImportedConstants(File $phpcsFile, int $stackPtr, ?int &$lastUse) : array + { + $first = 0; + $last = $phpcsFile->numTokens; + + $tokens = $phpcsFile->getTokens(); + + $nsStart = $phpcsFile->findPrevious(T_NAMESPACE, $stackPtr); + if ($nsStart && isset($tokens[$nsStart]['scope_opener'])) { + $first = $tokens[$nsStart]['scope_opener']; + $last = $tokens[$nsStart]['scope_closer']; + } + + $lastUse = null; + $constants = []; + + $use = $first; + while ($use = $phpcsFile->findNext(T_USE, $use + 1, $last)) { + if (! CodingStandard::isGlobalUse($phpcsFile, $use)) { + continue; + } + + if (isset($phpcsFile->getMetrics()[UnusedUseStatementSniff::class]['values'][$use])) { + continue; + } + + if ($next = $this->isConstUse($phpcsFile, $use)) { + $start = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], $next + 1); + $end = $phpcsFile->findPrevious( + T_STRING, + $phpcsFile->findNext([T_AS, T_SEMICOLON], $start + 1) - 1 + ); + $endOfStatement = $phpcsFile->findEndOfStatement($next); + $name = $phpcsFile->findPrevious(T_STRING, $endOfStatement - 1); + $fullName = $phpcsFile->getTokensAsString($start, $end - $start + 1); + + $constants[strtoupper($tokens[$name]['content'])] = [ + 'name' => $tokens[$name]['content'], + 'fqn' => ltrim($fullName, '\\'), + ]; + } + + $lastUse = $use; + } + + return $constants; + } + + /** + * @return array Array of imported functions { + * @var array $_ Key is lowercase function name { + * @var string $name Original function name + * @var string $fqn Fully qualified function name without leading slashes + * } + * } + */ + private function getImportedFunctions(File $phpcsFile, int $stackPtr, ?int &$lastUse) : array + { + $first = 0; + $last = $phpcsFile->numTokens; + + $tokens = $phpcsFile->getTokens(); + + $nsStart = $phpcsFile->findPrevious(T_NAMESPACE, $stackPtr); + if ($nsStart && isset($tokens[$nsStart]['scope_opener'])) { + $first = $tokens[$nsStart]['scope_opener']; + $last = $tokens[$nsStart]['scope_closer']; + } + + $lastUse = null; + $functions = []; + + $use = $first; + while ($use = $phpcsFile->findNext(T_USE, $use + 1, $last)) { + if (! CodingStandard::isGlobalUse($phpcsFile, $use)) { + continue; + } + + if (isset($phpcsFile->getMetrics()[UnusedUseStatementSniff::class]['values'][$use])) { + continue; + } + + if ($next = $this->isFunctionUse($phpcsFile, $use)) { + $start = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], $next + 1); + $end = $phpcsFile->findPrevious( + T_STRING, + $phpcsFile->findNext([T_AS, T_SEMICOLON], $start + 1) - 1 + ); + $endOfStatement = $phpcsFile->findEndOfStatement($next); + $name = $phpcsFile->findPrevious(T_STRING, $endOfStatement - 1); + $fullName = $phpcsFile->getTokensAsString($start, $end - $start + 1); + + $functions[strtolower($tokens[$name]['content'])] = [ + 'name' => $tokens[$name]['content'], + 'fqn' => ltrim($fullName, '\\'), + ]; + } + + $lastUse = $use; + } + + return $functions; + } +} diff --git a/src/ZendCodingStandard/Sniffs/Arrays/DoubleArrowSniff.php b/src/ZendCodingStandard/Sniffs/Arrays/DoubleArrowSniff.php new file mode 100644 index 00000000..9f93c0de --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Arrays/DoubleArrowSniff.php @@ -0,0 +1,105 @@ +checkSpace($phpcsFile, $data); + } + } + + /** + * Processes a multi-line array definition. + * + * @param File $phpcsFile The current file being checked. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * @param int $arrayStart The token that starts the array definition. + * @param int $arrayEnd The token that ends the array definition. + * @param array $indices An array of token positions for the array keys, + * double arrows, and values. + */ + protected function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $arrayEnd, $indices) + { + $tokens = $phpcsFile->getTokens(); + + foreach ($indices as $data) { + if (! isset($data['arrow'])) { + continue; + } + + $arrow = $tokens[$data['arrow']]; + $value = $tokens[$data['value_start']]; + + if ($value['line'] > $arrow['line']) { + $error = 'Double arrow in array cannot be at the end of the line'; + $fix = $phpcsFile->addFixableError($error, $data['arrow'], 'AtTheEnd'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($data['arrow'], ''); + for ($i = $data['arrow'] - 1; $i > $data['index_end']; --$i) { + if ($tokens[$i]['code'] !== T_WHITESPACE) { + break; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->addContentBefore($data['value_start'], '=> '); + $phpcsFile->fixer->endChangeset(); + } + + continue; + } + + $index = $tokens[$data['index_end']]; + if ($index['line'] === $arrow['line']) { + $this->checkSpace($phpcsFile, $data); + } + } + } + + private function checkSpace(File $phpcsFile, array $element) : void + { + $tokens = $phpcsFile->getTokens(); + + $space = $tokens[$element['arrow'] - 1]; + if ($space['code'] === T_WHITESPACE && $space['content'] !== ' ') { + $error = 'Expected 1 space before "=>"; "%s" found'; + $data = [ + Common::prepareForOutput($space['content']), + ]; + $fix = $phpcsFile->addFixableError($error, $element['arrow'], 'SpaceBefore', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($element['arrow'] - 1, ' '); + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Arrays/FormatSniff.php b/src/ZendCodingStandard/Sniffs/Arrays/FormatSniff.php new file mode 100644 index 00000000..202bb1d0 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Arrays/FormatSniff.php @@ -0,0 +1,183 @@ +getTokens(); + + // Single-line array - spaces before first element + if ($tokens[$arrayStart + 1]['code'] === T_WHITESPACE) { + $error = 'Expected 0 spaces after array bracket opener; %d found'; + $data = [strlen($tokens[$arrayStart + 1]['content'])]; + $fix = $phpcsFile->addFixableError($error, $arrayStart + 1, 'SingleLineSpaceBefore', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($arrayStart + 1, ''); + } + } + + // Single-line array - spaces before last element + if ($tokens[$arrayEnd - 1]['code'] === T_WHITESPACE) { + $error = 'Expected 0 spaces before array bracket closer; %d found'; + $data = [strlen($tokens[$arrayEnd - 1]['content'])]; + $fix = $phpcsFile->addFixableError($error, $arrayEnd - 1, 'SingleLineSpaceAfter', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($arrayEnd - 1, ''); + } + } + } + + /** + * Processes a multi-line array definition. + * + * @param File $phpcsFile The current file being checked. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * @param int $arrayStart The token that starts the array definition. + * @param int $arrayEnd The token that ends the array definition. + * @param array $indices An array of token positions for the array keys, + * double arrows, and values. + */ + protected function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $arrayEnd, $indices) : void + { + $tokens = $phpcsFile->getTokens(); + + $firstContent = $phpcsFile->findNext(T_WHITESPACE, $arrayStart + 1, null, true); + if ($tokens[$firstContent]['code'] === T_CLOSE_SHORT_ARRAY) { + $error = 'Empty array must be in one line'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'EmptyArrayInOneLine'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($arrayStart + 1, ''); + } + + return; + } + + $lastContent = $phpcsFile->findPrevious(T_WHITESPACE, $arrayEnd - 1, null, true); + if ($tokens[$arrayEnd]['line'] > $tokens[$lastContent]['line'] + 1) { + $error = 'Blank line found at the end of the array'; + $fix = $phpcsFile->addFixableError($error, $arrayEnd - 1, 'BlankLineAtTheEnd'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $i = $lastContent + 1; + while ($tokens[$i]['line'] !== $tokens[$arrayEnd]['line']) { + $phpcsFile->fixer->replaceToken($i, ''); + ++$i; + } + $phpcsFile->fixer->addNewlineBefore($arrayEnd); + $phpcsFile->fixer->endChangeset(); + } + } + + $first = $phpcsFile->findFirstOnLine([], $arrayStart, true); + $indent = $tokens[$first]['code'] === T_WHITESPACE + ? strlen($tokens[$first]['content']) + : 0; + + $previousLine = $tokens[$arrayStart]['line']; + $next = $arrayStart; + while ($next = $phpcsFile->findNext(T_WHITESPACE, $next + 1, $arrayEnd, true)) { + if ($previousLine === $tokens[$next]['line']) { + if ($tokens[$next]['code'] !== T_COMMENT) { + $error = 'There must be one array element per line'; + $fix = $phpcsFile->addFixableError($error, $next, 'OneElementPerLine'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + if ($tokens[$next - 1]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($next - 1, ''); + } + $phpcsFile->fixer->addNewlineBefore($next); + $phpcsFile->fixer->endChangeset(); + } + } + } else { + if ($previousLine < $tokens[$next]['line'] - 1 + && (! empty($tokens[$stackPtr]['conditions']) + || $previousLine === $tokens[$arrayStart]['line']) + ) { + $firstOnLine = $phpcsFile->findFirstOnLine([], $next, true); + + $error = 'Blank line is not allowed here'; + $fix = $phpcsFile->addFixableError($error, $firstOnLine - 1, 'BlankLine'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($firstOnLine - 1, ''); + } + } + } + + if ($tokens[$next]['code'] === T_COMMENT + && (strpos($tokens[$next]['content'], '//') === 0 + || strpos($tokens[$next]['content'], '#') === 0) + ) { + $end = $next; + } else { + $end = $phpcsFile->findEndOfStatement($next); + if ($tokens[$end]['code'] === T_DOUBLE_ARROW + || $tokens[$end]['code'] === T_CLOSE_CURLY_BRACKET + ) { + $end = $phpcsFile->findEndOfStatement($end); + } + } + + $previousLine = $tokens[$end]['line']; + $next = $end; + } + + if ($first = $phpcsFile->findFirstOnLine([], $arrayEnd, true)) { + if ($first < $arrayEnd - 1) { + $error = 'Array closing bracket should be in new line'; + $fix = $phpcsFile->addFixableError($error, $arrayEnd, 'ClosingBracketInNewLine'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + if ($indent > 0) { + $phpcsFile->fixer->addContentBefore($arrayEnd, str_repeat(' ', $indent)); + } + $phpcsFile->fixer->addNewlineBefore($arrayEnd); + $phpcsFile->fixer->endChangeset(); + } + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Arrays/TrailingArrayCommaSniff.php b/src/ZendCodingStandard/Sniffs/Arrays/TrailingArrayCommaSniff.php new file mode 100644 index 00000000..269fbe9b --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Arrays/TrailingArrayCommaSniff.php @@ -0,0 +1,66 @@ +getTokens(); + $beforeClose = $phpcsFile->findPrevious(Tokens::$emptyTokens, $arrayEnd - 1, $arrayStart + 1, true); + + if ($beforeClose && $tokens[$beforeClose]['code'] === T_COMMA) { + $error = 'Single-line arrays must not have a trailing comma after the last element'; + $fix = $phpcsFile->addFixableError($error, $beforeClose, 'AdditionalTrailingComma'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($beforeClose, ''); + } + } + } + + /** + * Processes a multi-line array definition. + * + * @param File $phpcsFile The current file being checked. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * @param int $arrayStart The token that starts the array definition. + * @param int $arrayEnd The token that ends the array definition. + * @param array $indices An array of token positions for the array keys, + * double arrows, and values. + */ + protected function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $arrayEnd, $indices) : void + { + $tokens = $phpcsFile->getTokens(); + $beforeClose = $phpcsFile->findPrevious(Tokens::$emptyTokens, $arrayEnd - 1, $arrayStart + 1, true); + + if ($beforeClose && $tokens[$beforeClose]['code'] !== T_COMMA) { + $error = 'Multi-line arrays must have a trailing comma after the last element'; + $fix = $phpcsFile->addFixableError($error, $beforeClose, 'MissingTrailingComma'); + + if ($fix) { + $phpcsFile->fixer->addContent($beforeClose, ','); + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Classes/AlphabeticallySortedTraitsSniff.php b/src/ZendCodingStandard/Sniffs/Classes/AlphabeticallySortedTraitsSniff.php new file mode 100644 index 00000000..f670e2b5 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Classes/AlphabeticallySortedTraitsSniff.php @@ -0,0 +1,169 @@ +getTraits($phpcsFile, $stackPtr); + + $lastUse = null; + foreach ($uses as $use) { + if (! $lastUse) { + $lastUse = $use; + continue; + } + + $order = $this->compareUseStatements($use, $lastUse); + + if ($order < 0) { + $error = 'Traits are incorrectly ordered. The first wrong one is %s'; + $data = [$use['name']]; + $fix = $phpcsFile->addFixableError($error, $use['ptrUse'], 'IncorrectOrder', $data); + + if ($fix) { + $this->fixAlphabeticalOrder($phpcsFile, $uses); + } + + return; + } + + $lastUse = $use; + } + } + + /** + * @return string[][] + */ + private function getTraits(File $phpcsFile, int $scopePtr) : array + { + $tokens = $phpcsFile->getTokens(); + + $uses = []; + + $start = $tokens[$scopePtr]['scope_opener']; + $end = $tokens[$scopePtr]['scope_closer']; + while ($use = $phpcsFile->findNext(T_USE, $start + 1, $end)) { + if (! CodingStandard::isTraitUse($phpcsFile, $use) + || ! isset($tokens[$use]['conditions'][$scopePtr]) + || $tokens[$use]['level'] !== $tokens[$scopePtr]['level'] + 1 + ) { + $start = $use; + continue; + } + + // Find comma, semicolon or opening curly bracket, whatever is first. + $endOfName = $phpcsFile->findNext( + [T_COMMA, T_SEMICOLON, T_OPEN_CURLY_BRACKET], + $use + 1 + ); + + // Find end of scope - could be semicolon or closing curly bracket. + $endOfScope = $this->getEndOfTraitScope($phpcsFile, $endOfName); + + $uses[] = [ + 'ptrUse' => $use, + 'name' => trim($phpcsFile->getTokensAsString($use + 1, $endOfName - $use - 1)), + 'ptrEnd' => $endOfScope, + 'string' => trim($phpcsFile->getTokensAsString($use, $endOfScope - $use + 1)), + ]; + + $start = $endOfName; + } + + return $uses; + } + + private function getEndOfTraitScope(File $phpcsFile, int $stackPtr) : int + { + $tokens = $phpcsFile->getTokens(); + + if ($tokens[$stackPtr]['code'] === T_COMMA) { + $stackPtr = $phpcsFile->findNext([T_SEMICOLON, T_OPEN_CURLY_BRACKET], $stackPtr + 1); + } + + if ($tokens[$stackPtr]['code'] === T_SEMICOLON) { + return $stackPtr; + } + + return $phpcsFile->findNext(T_CLOSE_CURLY_BRACKET, $stackPtr + 1); + } + + /** + * @param string[] $a + * @param string[] $b + */ + private function compareUseStatements(array $a, array $b) : int + { + return strcasecmp( + $this->clearName($a['name']), + $this->clearName($b['name']) + ); + } + + private function clearName(string $name) : string + { + return str_replace('\\', ':', $name); + } + + /** + * @param string[][] $uses + */ + private function fixAlphabeticalOrder(File $phpcsFile, array $uses) : void + { + $first = reset($uses); + $last = end($uses); + $lastScopeCloser = $last['ptrEnd']; + + $phpcsFile->fixer->beginChangeset(); + for ($i = $first['ptrUse']; $i <= $lastScopeCloser; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + uasort($uses, function (array $a, array $b) { + return $this->compareUseStatements($a, $b); + }); + + $phpcsFile->fixer->addContent($first['ptrUse'], implode($phpcsFile->eolChar, array_map(function ($use) { + return $use['string']; + }, $uses))); + + $phpcsFile->fixer->endChangeset(); + } +} diff --git a/src/ZendCodingStandard/Sniffs/Classes/ConstVisibilitySniff.php b/src/ZendCodingStandard/Sniffs/Classes/ConstVisibilitySniff.php new file mode 100644 index 00000000..13bb1c5d --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Classes/ConstVisibilitySniff.php @@ -0,0 +1,47 @@ +getTokens(); + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true); + + if (! in_array($tokens[$prev]['code'], Tokens::$scopeModifiers, true)) { + $error = 'Missing constant visibility'; + $phpcsFile->addError($error, $stackPtr, 'MissingVisibility'); + } + } + + /** + * @param int $stackPtr + */ + protected function processTokenOutsideScope(File $phpcsFile, $stackPtr) : void + { + // do not process constant outside the scope + } +} diff --git a/src/ZendCodingStandard/Sniffs/Classes/NoNullValuesSniff.php b/src/ZendCodingStandard/Sniffs/Classes/NoNullValuesSniff.php new file mode 100644 index 00000000..0c3d1590 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Classes/NoNullValuesSniff.php @@ -0,0 +1,65 @@ +getTokens(); + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + if ($tokens[$next]['code'] !== T_EQUAL) { + return; + } + + $value = $phpcsFile->findNext(Tokens::$emptyTokens, $next + 1, null, true); + if ($tokens[$value]['code'] === T_NULL) { + $error = 'Default null value for the property is redundant.'; + $fix = $phpcsFile->addFixableError($error, $value, 'NullValue'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $stackPtr + 1; $i <= $value; ++$i) { + if (! in_array($tokens[$i]['code'], [T_WHITESPACE, T_EQUAL, T_NULL], true)) { + continue; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->endChangeset(); + } + } + } + + /** + * @param int $stackPtr + */ + protected function processVariable(File $phpcsFile, $stackPtr) : void + { + // Normal variables are not processed in this sniff. + } + + /** + * @param int $stackPtr + */ + protected function processVariableInString(File $phpcsFile, $stackPtr) : void + { + // Variables in string are not processed in this sniff. + } +} diff --git a/src/ZendCodingStandard/Sniffs/Classes/TraitUsageSniff.php b/src/ZendCodingStandard/Sniffs/Classes/TraitUsageSniff.php new file mode 100644 index 00000000..f9ce984a --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Classes/TraitUsageSniff.php @@ -0,0 +1,322 @@ +getTokens(); + + // No blank line before use keyword. + $prev = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true); + if ($tokens[$prev]['line'] + 1 !== $tokens[$stackPtr]['line']) { + $error = 'Blank line is not allowed before trait declaration'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'BlankLineBeforeTraits'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $prev + 1; $i < $stackPtr; ++$i) { + if ($tokens[$i]['line'] === $tokens[$stackPtr]['line']) { + break; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->addNewline($prev); + $phpcsFile->fixer->endChangeset(); + } + } + + // One space after the use keyword. + if ($tokens[$stackPtr + 1]['content'] !== ' ') { + $error = 'There must be a single space after USE keyword'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterUse'); + + if ($fix) { + if ($tokens[$stackPtr + 1]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($stackPtr + 1, ' '); + } else { + $phpcsFile->fixer->addContent($stackPtr, ' '); + } + } + } + + $scopeOpener = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET, T_SEMICOLON], $stackPtr + 1); + + $comma = $phpcsFile->findNext(T_COMMA, $stackPtr + 1, $scopeOpener - 1); + if ($comma) { + $error = 'There must be one USE per declaration.'; + $fix = $phpcsFile->addFixableError($error, $comma, 'OneUsePerDeclaration'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($comma, ';' . $phpcsFile->eolChar . 'use '); + } + } + + // Check for T_WHITESPACE in trait name. + $firstNotEmpty = $phpcsFile->findNext( + T_WHITESPACE, + $stackPtr + 1, + $comma ?: $scopeOpener, + true + ); + $lastNotEmpty = $phpcsFile->findPrevious( + T_WHITESPACE, + ($comma ?: $scopeOpener) - 1, + $stackPtr + 1, + true + ); + + if ($firstNotEmpty !== $lastNotEmpty) { + $emptyInName = $phpcsFile->findNext( + Tokens::$emptyTokens, + $firstNotEmpty + 1, + $lastNotEmpty + ); + if ($emptyInName) { + $error = 'Empty token %s is not allowed in trait name.'; + $data = [$tokens[$emptyInName]['type']]; + $fix = $phpcsFile->addFixableError($error, $emptyInName, 'EmptyToken', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($emptyInName, ''); + } + } + } + + if ($tokens[$scopeOpener]['code'] === T_OPEN_CURLY_BRACKET) { + $prevNonEmpty = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + $scopeOpener - 1, + null, + true + ); + if ($tokens[$prevNonEmpty]['line'] !== $tokens[$scopeOpener]['line']) { + $error = 'There must be a single space before curly bracket.'; + $fix = $phpcsFile->addFixableError($error, $scopeOpener, 'SpaceBeforeCurly'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $prevNonEmpty + 1; $i < $scopeOpener; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->addContentBefore($scopeOpener, ' '); + $phpcsFile->fixer->endChangeset(); + } + } elseif ($tokens[$scopeOpener - 1]['content'] !== ' ') { + $error = 'There must be a single space before curly bracket.'; + $fix = $phpcsFile->addFixableError($error, $scopeOpener, 'SpaceBeforeCurly'); + + if ($fix) { + if ($tokens[$scopeOpener - 1]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($scopeOpener - 1, ' '); + } else { + $phpcsFile->fixer->addContent($scopeOpener - 1, ' '); + } + } + } + + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $scopeOpener + 1, null, true); + if ($tokens[$nextNonEmpty]['line'] !== $tokens[$scopeOpener]['line'] + 1) { + $error = 'Content must be in next line after opening curly bracket.'; + $fix = $phpcsFile->addFixableError($error, $scopeOpener, 'OpeningCurlyBracket'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $scopeOpener + 1; $i < $nextNonEmpty; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->addContentBefore($nextNonEmpty, $phpcsFile->eolChar); + $phpcsFile->fixer->endChangeset(); + } + } + + $scopeCloser = $tokens[$scopeOpener]['scope_closer']; + $prevNonEmpty = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + $scopeCloser - 1, + null, + true + ); + if ($tokens[$prevNonEmpty]['line'] + 1 !== $tokens[$scopeCloser]['line']) { + $error = 'Close curly bracket must be in next line after content.'; + $fix = $phpcsFile->addFixableError($error, $scopeCloser, 'ClosingCurlyBracket'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $prevNonEmpty + 1; $i < $scopeCloser; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->addContentBefore($scopeCloser, $phpcsFile->eolChar); + $phpcsFile->fixer->endChangeset(); + } + } + + // Detect all statements inside curly brackets. + $statements = []; + $begin = $phpcsFile->findNext(Tokens::$emptyTokens, $scopeOpener + 1, null, true); + while ($end = $phpcsFile->findNext([T_SEMICOLON], $begin + 1, $scopeCloser)) { + $statements[] = [ + 'begin' => $begin, + 'end' => $end, + 'content' => $phpcsFile->getTokensAsString($begin, $end - $begin + 1), + ]; + $begin = $phpcsFile->findNext(Tokens::$emptyTokens, $end + 1, null, true); + } + + $lastStatement = null; + foreach ($statements as $statement) { + if (! $lastStatement) { + $lastStatement = $statement; + continue; + } + + $order = $this->compareStatements($statement, $lastStatement); + + if ($order < 0) { + $error = 'Statements in trait are incorrectly ordered. The first wrong is %s'; + $data = [$statement['content']]; + $fix = $phpcsFile->addFixableError($error, $statement['begin'], 'TraitStatementsOrder', $data); + + if ($fix) { + $this->fixAlphabeticalOrder($phpcsFile, $statements); + } + + break; + } + + $lastStatement = $statement; + } + } else { + $scopeCloser = $scopeOpener; + } + + $class = $phpcsFile->findPrevious([T_CLASS, T_TRAIT, T_ANON_CLASS], $stackPtr - 1); + // Only interested in the last USE statement from here onwards. + $nextUse = $stackPtr; + do { + $nextUse = $phpcsFile->findNext(T_USE, $nextUse + 1, $tokens[$class]['scope_closer']); + } while ($nextUse !== false + && (! CodingStandard::isTraitUse($phpcsFile, $nextUse) + || ! isset($tokens[$nextUse]['conditions'][$class]) + || $tokens[$nextUse]['level'] !== $tokens[$class]['level'] + 1) + ); + + if ($nextUse !== false) { + return; + } + + // Find next (after traits) non-whitespace token. + $next = $phpcsFile->findNext(T_WHITESPACE, $scopeCloser + 1, null, true); + + $diff = $tokens[$next]['line'] - $tokens[$scopeCloser]['line'] - 1; + if ($diff !== 1 + && $tokens[$next]['code'] !== T_CLOSE_CURLY_BRACKET + ) { + $error = 'There must be one blank line after the last USE statement; %s found;'; + $data = [$diff]; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterLastUse', $data); + + if ($fix) { + if ($diff === 0) { + $phpcsFile->fixer->addNewline($scopeCloser); + } else { + $phpcsFile->fixer->beginChangeset(); + for ($i = $scopeCloser + 1; $i < $next; ++$i) { + if ($tokens[$i]['line'] === $tokens[$next]['line']) { + break; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->addNewline($scopeCloser); + $phpcsFile->fixer->endChangeset(); + } + } + } + } + + /** + * Fix order of statements inside trait's curly brackets. + * + * @param string[] $statements + */ + private function fixAlphabeticalOrder(File $phpcsFile, array $statements) : void + { + $phpcsFile->fixer->beginChangeset(); + foreach ($statements as $statement) { + for ($i = $statement['begin']; $i <= $statement['end']; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + } + + usort($statements, function (array $a, array $b) { + return $this->compareStatements($a, $b); + }); + + $begins = array_column($statements, 'begin'); + sort($begins); + + foreach ($begins as $k => $begin) { + $phpcsFile->fixer->addContent($begin, $statements[$k]['content']); + } + + $phpcsFile->fixer->endChangeset(); + } + + /** + * @param string[] $a + * @param string[] $b + */ + private function compareStatements(array $a, array $b) : int + { + return strcasecmp( + $this->clearName($a['content']), + $this->clearName($b['content']) + ); + } + + private function clearName(string $name) : string + { + return str_replace('\\', ':', $name); + } +} diff --git a/src/ZendCodingStandard/Sniffs/Commenting/CodingStandardTagsSniff.php b/src/ZendCodingStandard/Sniffs/Commenting/CodingStandardTagsSniff.php new file mode 100644 index 00000000..dec28c8c --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Commenting/CodingStandardTagsSniff.php @@ -0,0 +1,126 @@ + '@phpcs:ignoreFile', + '@codingStandardsIgnoreStart' => '@phpcs:disable', + '@codingStandardsIgnoreEnd' => '@phpcs:enable', + '@codingStandardsIgnoreLine' => '@phpcs:ignore', + '@codingStandardsChangeSetting' => '@phpcs:set', + ]; + + /** + * @return int[] + */ + public function register() : array + { + return [T_OPEN_TAG]; + } + + /** + * @param int $stackPtr + * @return int + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $ignoredLines = $phpcsFile->tokenizer->ignoredLines; + $phpcsFile->tokenizer->ignoredLines = []; + + $next = $stackPtr; + while ($next = $phpcsFile->findNext([T_COMMENT, T_DOC_COMMENT_TAG], $next + 1)) { + if ($tokens[$next]['code'] === T_DOC_COMMENT_TAG) { + $lower = strtolower($tokens[$next]['content']); + if ($tag = key(array_filter($this->replacements, function ($key) use ($lower) { + return strtolower($key) === $lower; + }, ARRAY_FILTER_USE_KEY))) { + $this->overrideToken($phpcsFile, $next); + + $error = 'PHP_CodeSniffer tag %s in line %d is deprecated; use %s instead'; + $data = [ + $tag, + $tokens[$next]['line'], + $this->replacements[$tag], + ]; + $fix = $phpcsFile->addFixableError($error, $next, 'DeprecatedTag', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($next, $this->replacements[$tag]); + } + } + + continue; + } + + $content = ltrim($tokens[$next]['content'], ' /*'); + foreach ($this->replacements as $old => $new) { + if (stripos($content, $old) !== false) { + $this->overrideToken($phpcsFile, $next); + + $error = 'PHP_CodeSniffer tag %s in line is deprecated; use %s instead'; + $data = [ + $old, + $tokens[$next]['line'], + $new, + ]; + $fix = $phpcsFile->addFixableError($error, $next, 'DeprecatedTag', $data); + + if ($fix) { + $content = preg_replace( + '/' . preg_quote($old, '/') . '/i', + $new, + $tokens[$next]['content'] + ); + $phpcsFile->fixer->replaceToken($next, $content); + } + break; + } + } + } + + $phpcsFile->tokenizer->ignoredLines = $ignoredLines; + return $phpcsFile->numTokens + 1; + } + + private function overrideToken(File $phpcsFile, int $stackPtr) + { + $clear = function () use ($stackPtr) { + $this->tokens[$stackPtr]['content'] = 'ZF-CS'; + }; + + $clear->bindTo($phpcsFile, File::class)(); + } +} diff --git a/src/ZendCodingStandard/Sniffs/Commenting/DocCommentSniff.php b/src/ZendCodingStandard/Sniffs/Commenting/DocCommentSniff.php new file mode 100644 index 00000000..149db3f5 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Commenting/DocCommentSniff.php @@ -0,0 +1,867 @@ +getTokens(); + $commentStart = $stackPtr; + $commentEnd = $tokens[$stackPtr]['comment_closer']; + + if ($this->checkIfEmpty($phpcsFile, $commentStart, $commentEnd)) { + return; + } + + $this->checkBeforeOpen($phpcsFile, $commentStart); + $this->checkAfterClose($phpcsFile, $commentStart, $commentEnd); + $this->checkCommentIndents($phpcsFile, $commentStart, $commentEnd); + $this->checkTagsSpaces($phpcsFile, $commentStart, $commentEnd); + $this->checkInheritDoc($phpcsFile, $commentStart, $commentEnd); + + // Doc block comment in one line. + if ($tokens[$commentStart]['line'] === $tokens[$commentEnd]['line']) { + $this->checkSpacesInOneLineComment($phpcsFile, $commentStart, $commentEnd); + + return; + } + + $this->checkAfterOpen($phpcsFile, $commentStart); + $this->checkBeforeClose($phpcsFile, $commentEnd); + + $this->checkSpacesAfterStar($phpcsFile, $commentStart, $commentEnd); + $this->checkBlankLinesInComment($phpcsFile, $commentStart, $commentEnd); + + $this->checkBlankLineBeforeTags($phpcsFile, $commentStart); + } + + /** + * Checks if doc comment is empty. + */ + private function checkIfEmpty(File $phpcsFile, int $commentStart, int $commentEnd) : bool + { + $tokens = $phpcsFile->getTokens(); + + $empty = [ + T_DOC_COMMENT_WHITESPACE, + T_DOC_COMMENT_STAR, + ]; + + $next = $commentStart; + while ($next = $phpcsFile->findNext($empty, $next + 1, $commentEnd, true)) { + if ($tokens[$next]['code'] === T_DOC_COMMENT_STRING + && preg_match('/^[*\s]+$/', $tokens[$next]['content']) + ) { + continue; + } + + return false; + } + + $error = 'Doc comment is empty.'; + $fix = $phpcsFile->addFixableError($error, $commentStart, 'Empty'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $commentStart; $i <= $commentEnd; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + if ($tokens[$commentStart - 1]['code'] === T_WHITESPACE + && strpos($tokens[$commentStart - 1]['content'], $phpcsFile->eolChar) === false + ) { + $phpcsFile->fixer->replaceToken($commentStart - 1, ''); + if ($tokens[$commentStart - 2]['code'] === T_WHITESPACE + && strpos($tokens[$commentStart - 2]['content'], $phpcsFile->eolChar) !== false + && $tokens[$commentEnd + 1]['code'] === T_WHITESPACE + && strpos($tokens[$commentEnd + 1]['content'], $phpcsFile->eolChar) !== false + ) { + $phpcsFile->fixer->replaceToken($commentStart - 2, ''); + } + } elseif ($tokens[$commentStart - 1]['code'] === T_WHITESPACE + && strpos($tokens[$commentStart - 1]['content'], $phpcsFile->eolChar) !== false + && $tokens[$commentEnd + 1]['code'] === T_WHITESPACE + && strpos($tokens[$commentEnd + 1]['content'], $phpcsFile->eolChar) !== false + ) { + $phpcsFile->fixer->replaceToken($commentStart - 1, ''); + } elseif ($tokens[$commentStart - 1]['code'] === T_OPEN_TAG + && ($next = $phpcsFile->findNext(T_WHITESPACE, $commentEnd + 1, null, true)) + && $tokens[$next]['line'] > $tokens[$commentEnd]['line'] + 1 + ) { + $phpcsFile->fixer->replaceToken($commentEnd + 1, ''); + } + $phpcsFile->fixer->endChangeset(); + } + + return true; + } + + /** + * Checks if there is no any other content before doc comment opening tag, + * and if there is blank line before doc comment (for multiline doc comment). + */ + private function checkBeforeOpen(File $phpcsFile, int $commentStart) : void + { + $tokens = $phpcsFile->getTokens(); + + $previous = $phpcsFile->findPrevious(T_WHITESPACE, $commentStart - 1, null, true); + if ($tokens[$previous]['line'] === $tokens[$commentStart]['line']) { + $error = 'The open comment tag must be the only content on the line.'; + $fix = $phpcsFile->addFixableError($error, $commentStart, 'ContentBeforeOpeningTag'); + + if ($fix) { + $nonEmpty = $phpcsFile->findPrevious(T_WHITESPACE, $commentStart - 1, null, true); + $phpcsFile->fixer->beginChangeset(); + $prev = $commentStart; + while ($prev = $phpcsFile->findPrevious(T_WHITESPACE, $prev - 1, $nonEmpty)) { + $phpcsFile->fixer->replaceToken($prev, ''); + } + $phpcsFile->fixer->replaceToken($nonEmpty, trim($tokens[$nonEmpty]['content'])); + $phpcsFile->fixer->addNewline($commentStart - 1); + $phpcsFile->fixer->endChangeset(); + } + } elseif ($tokens[$previous]['line'] === $tokens[$commentStart]['line'] - 1 + && $tokens[$previous]['code'] !== T_OPEN_TAG + && $tokens[$previous]['code'] !== T_OPEN_CURLY_BRACKET + ) { + $error = 'Missing blank line before doc comment.'; + $fix = $phpcsFile->addFixableError($error, $commentStart, 'MissingBlankLine'); + + if ($fix) { + $phpcsFile->fixer->addNewlineBefore($commentStart); + } + } + } + + /** + * Checks if there is no any other content after doc comment opening tag (for multiline doc comment). + */ + private function checkAfterOpen(File $phpcsFile, int $commentStart) : void + { + $tokens = $phpcsFile->getTokens(); + + $next = $phpcsFile->findNext(T_DOC_COMMENT_WHITESPACE, $commentStart + 1, null, true); + if ($tokens[$next]['line'] === $tokens[$commentStart]['line']) { + $error = 'The open comment tag must be the only content on the line.'; + $fix = $phpcsFile->addFixableError($error, $commentStart, 'ContentAfterOpeningTag'); + + if ($fix) { + $indentToken = $tokens[$commentStart - 1]; + if ($indentToken['code'] === T_WHITESPACE + && $indentToken['line'] === $tokens[$commentStart]['line'] + ) { + $indent = strlen($indentToken['content']); + } else { + $indent = 0; + } + + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->addNewline($commentStart); + if ($tokens[$commentStart + 1]['code'] === T_DOC_COMMENT_WHITESPACE) { + $phpcsFile->fixer->replaceToken($commentStart + 1, str_repeat(' ', $indent)); + if ($tokens[$commentStart + 2]['code'] !== T_DOC_COMMENT_STAR) { + $phpcsFile->fixer->addContent($commentStart + 1, '* '); + } + } + $phpcsFile->fixer->endChangeset(); + } + } + } + + /** + * Checks if there is no any other content before doc comment closing tag (for multiline doc comment). + */ + private function checkBeforeClose(File $phpcsFile, int $commentEnd) : void + { + $tokens = $phpcsFile->getTokens(); + + $previous = $phpcsFile->findPrevious(T_DOC_COMMENT_WHITESPACE, $commentEnd - 1, null, true); + if ($tokens[$previous]['line'] === $tokens[$commentEnd]['line']) { + $error = 'The close comment tag must be the only content on the line.'; + $fix = $phpcsFile->addFixableError($error, $commentEnd, 'ContentBeforeClosingTag'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $content = $tokens[$commentEnd - 1]['content']; + if (trim($content) . ' ' !== $content) { + $phpcsFile->fixer->replaceToken($commentEnd - 1, trim($content)); + } + $phpcsFile->fixer->addNewlineBefore($commentEnd); + $phpcsFile->fixer->endChangeset(); + } + } + } + + /** + * Checks if there is no any other content after doc comment closing tag (for multiline doc comment). + */ + private function checkAfterClose(File $phpcsFile, int $commentStart, int $commentEnd) : void + { + $tokens = $phpcsFile->getTokens(); + + $allowEmptyLineBefore = [ + T_NAMESPACE, + T_USE, + ]; + + $prev = $phpcsFile->findPrevious(T_WHITESPACE, $commentStart - 1, null, true); + $next = $phpcsFile->findNext(T_WHITESPACE, $commentEnd + 1, null, true); + + if (! $next) { + $error = 'Doc comment is not allowed at the end of the file.'; + $phpcsFile->addError($error, $commentStart, 'DocCommentAtTheEndOfTheFile'); + return; + } + + if ($tokens[$commentEnd]['line'] === $tokens[$next]['line']) { + $error = 'The close comment tag must be the only content on the line.'; + $fix = $phpcsFile->addFixableError($error, $commentEnd, 'ContentAfterClosingTag'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $newLine = $commentEnd; + if ($tokens[$commentEnd + 1]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($commentEnd + 1, ''); + $newLine++; + } + $phpcsFile->fixer->addNewline($newLine); + $phpcsFile->fixer->endChangeset(); + } + } elseif ($tokens[$prev]['code'] === T_OPEN_TAG) { + if ($tokens[$next]['line'] === $tokens[$commentEnd]['line'] + 1) { + $error = 'Missing blank line after file doc comment.'; + $fix = $phpcsFile->addFixableError($error, $commentEnd, 'MissingBlankLineAfter'); + + if ($fix) { + $phpcsFile->fixer->addNewline($commentEnd); + } + } + } elseif ($tokens[$next]['line'] > $tokens[$commentEnd]['line'] + 1 + && ! in_array($tokens[$next]['code'], $allowEmptyLineBefore, true) + ) { + $error = 'Additional blank lines found after doc comment.'; + $fix = $phpcsFile->addFixableError($error, $commentEnd + 2, 'BlankLinesAfter'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $commentEnd + 1; $i < $next; $i++) { + if ($tokens[$i + 1]['line'] === $tokens[$next]['line']) { + break; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->endChangeset(); + } + } + } + + /** + * Checks if there is exactly one space after doc comment opening tag, + * and exactly one space before closing tag (for single line doc comment). + */ + private function checkSpacesInOneLineComment(File $phpcsFile, int $commentStart, int $commentEnd) : void + { + $tokens = $phpcsFile->getTokens(); + + // Check, if there is exactly one space after opening tag. + if ($tokens[$commentStart + 1]['code'] === T_DOC_COMMENT_WHITESPACE + && $tokens[$commentStart + 1]['content'] !== ' ' + ) { + $error = 'Expected 1 space after opening tag of one line doc block comment.'; + $fix = $phpcsFile->addFixableError($error, $commentStart + 1, 'InvalidSpacing'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($commentStart + 1, ' '); + } + } elseif ($tokens[$commentStart + 1]['code'] !== T_DOC_COMMENT_WHITESPACE) { + $error = 'Expected 1 space after opening tag of one line doc block comment.'; + $fix = $phpcsFile->addFixableError($error, $commentStart, 'InvalidSpacing'); + + if ($fix) { + $phpcsFile->fixer->addContent($commentStart, ' '); + } + } + + // Check, if there is exactly one space before closing tag. + $content = $tokens[$commentEnd - 1]['content']; + if (trim($content) . ' ' !== $content) { + $error = 'Expected 1 space before closing tag of one line doc block comment.'; + $fix = $phpcsFile->addFixableError($error, $commentEnd - 1, 'InvalidSpacing'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($commentEnd - 1, trim($content) . ' '); + } + } + } + + /** + * Checks if there is one space after star in multiline doc comment. + * More than one space is allowed, unless the line contains tag. + * + * @todo: needs to check with doctrine annotations + */ + private function checkSpacesAfterStar(File $phpcsFile, int $commentStart, int $commentEnd) : void + { + $tokens = $phpcsFile->getTokens(); + $firstTag = $tokens[$commentStart]['comment_tags'][0] ?? null; + + $replaces = []; + $next = $commentStart; + $search = [T_DOC_COMMENT_STAR, T_DOC_COMMENT_CLOSE_TAG]; + while ($next = $phpcsFile->findNext($search, $next + 1, $commentEnd + 1)) { + if (($tokens[$next + 1]['code'] !== T_DOC_COMMENT_WHITESPACE + && $tokens[$next]['code'] === T_DOC_COMMENT_STAR) + || ($tokens[$next + 1]['code'] === T_DOC_COMMENT_WHITESPACE + && strpos($tokens[$next + 1]['content'], $phpcsFile->eolChar) === false) + ) { + $nested = 0; + if ($firstTag) { + $prev = $next; + while ($prev = $phpcsFile->findPrevious(T_DOC_COMMENT_STRING, $prev - 1, $firstTag)) { + if ($tokens[$prev]['content'][0] === '}') { + --$nested; + } + if (substr($tokens[$prev]['content'], -1) === '{') { + ++$nested; + } + } + } + + $expectedSpaces = 1 + $this->indent * $nested; + $expected = str_repeat(' ', $expectedSpaces); + + if ($tokens[$next + 1]['code'] !== T_DOC_COMMENT_WHITESPACE) { + $error = 'There must be exactly %d space(s) between star and comment; found 0'; + $data = [ + $expectedSpaces, + ]; + $fix = $phpcsFile->addFixableError($error, $next, 'NoSpaceAfterStar', $data); + + if ($fix) { + $phpcsFile->fixer->addContent($next, $expected); + } + } elseif ($tokens[$next + 1]['content'] !== $expected + && ($tokens[$next + 2]['content'][0] === '@' + || $tokens[$next + 1]['line'] === $tokens[$commentStart]['line'] + 1) + ) { + $error = 'There must be exactly %d space(s) between star and comment'; + $data = [ + $expectedSpaces, + ]; + $fix = $phpcsFile->addFixableError($error, $next + 1, 'TooManySpacesAfterStar', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($next + 1, $expected); + } + } elseif ($tokens[$next + 2]['code'] !== T_DOC_COMMENT_TAG + && $tokens[$next + 2]['code'] !== T_DOC_COMMENT_CLOSE_TAG + ) { + $prev = $phpcsFile->findPrevious( + [ + T_DOC_COMMENT_WHITESPACE, + T_DOC_COMMENT_STRING, + T_DOC_COMMENT_STAR, + ], + $next - 1, + null, + true + ); + + if ($tokens[$next + 2]['content'][0] === '}') { + $expectedSpaces -= $this->indent; + } else { + $expectedSpaces += $this->indent; + } + + $spaces = strlen(preg_replace('/^( *).*$/', '\\1', $tokens[$next + 1]['content'])); + if (! isset($replaces[$prev][$spaces])) { + $replaces[$prev][$spaces] = $spaces; + } + + if ($tokens[$prev]['code'] === T_DOC_COMMENT_TAG + && ($spaces < $expectedSpaces + || (($spaces - 1) % $this->indent) !== 0 + || ($spaces > $expectedSpaces + && $tokens[$prev]['line'] === $tokens[$next + 1]['line'] - 1)) + ) { + // could be doc string or tag + $prev2 = $phpcsFile->findPrevious( + [ + T_DOC_COMMENT_WHITESPACE, + T_DOC_COMMENT_STAR, + ], + $next - 1, + $prev, + true + ); + + if ($tokens[$prev2]['line'] === $tokens[$next]['line'] - 1) { + if (isset($replaces[$prev][$spaces]) && $replaces[$prev][$spaces] !== $spaces) { + $expectedSpaces = $replaces[$prev][$spaces]; + } elseif ($tokens[$prev]['line'] !== $tokens[$next + 1]['line'] - 1) { + $expectedSpaces = 1 + (int) max( + round(($spaces - 1) / $this->indent) * $this->indent, + $this->indent + ); + } + $replaces[$prev][$spaces] = $expectedSpaces; + + $error = 'Invalid indent before description; expected %d spaces, found %d'; + $data = [ + $expectedSpaces, + $spaces, + ]; + $fix = $phpcsFile->addFixableError($error, $next + 1, 'InvalidDescriptionIndent', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($next + 1, str_repeat(' ', $expectedSpaces)); + } + } else { + $error = 'Additional description is not allowed after tag.' + . ' Please move description to the top of the PHPDocs' + . ' or remove empty line above if it is description for the tag.'; + $phpcsFile->addError($error, $next + 1, 'AdditionalDescription'); + } + } + } + } + } + } + + /** + * Doc comment cannot have empty line on the beginning of the comment, at the end of the comment, + * and there is allowed only one empty line between two comment sections. + */ + private function checkBlankLinesInComment(File $phpcsFile, int $commentStart, int $commentEnd) : void + { + $tokens = $phpcsFile->getTokens(); + + $empty = [ + T_DOC_COMMENT_WHITESPACE, + T_DOC_COMMENT_STAR, + ]; + + // Additional blank lines at the beginning of doc block. + $next = $phpcsFile->findNext($empty, $commentStart + 1, null, true); + if ($tokens[$next]['line'] > $tokens[$commentStart]['line'] + 1) { + $error = 'Additional blank lines found at the beginning of doc comment.'; + $fix = $phpcsFile->addFixableError($error, $commentStart + 2, 'SpacingBefore'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $commentStart + 1; $i < $next; $i++) { + if ($tokens[$i + 1]['line'] === $tokens[$next]['line']) { + break; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->endChangeset(); + } + } + + // Additional blank lines at the and of doc block. + $previous = $phpcsFile->findPrevious($empty, $commentEnd - 1, null, true); + if ($tokens[$previous]['line'] < $tokens[$commentEnd]['line'] - 1) { + $error = 'Additional blank lines found at the end of doc comment.'; + $fix = $phpcsFile->addFixableError($error, $previous + 2, 'SpacingAfter'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $previous + 1; $i < $commentEnd; $i++) { + if ($tokens[$i + 1]['line'] === $tokens[$commentEnd]['line']) { + break; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->endChangeset(); + } + } + + // Check for double blank lines. + $from = $phpcsFile->findNext($empty, $commentStart + 1, null, true); + $to = $phpcsFile->findPrevious($empty, $commentEnd - 1, null, true); + + while ($next = $phpcsFile->findNext($empty, $from + 1, $to, true)) { + if ($tokens[$next]['line'] > $tokens[$from]['line'] + 2) { + $error = 'More than one blank line between parts of doc block.'; + $i = 0; + while ($token = $phpcsFile->findNext(T_DOC_COMMENT_STAR, $from + 1, $next - 2)) { + if ($i++ > 0) { + $fix = $phpcsFile->addFixableError($error, $token, 'MultipleBlankLines'); + + if ($fix) { + $firstOnLine = $phpcsFile->findFirstOnLine($empty, $token); + for ($n = $firstOnLine; $n <= $token + 1; $n++) { + $phpcsFile->fixer->replaceToken($n, ''); + } + } + } + + $from = $token; + } + } + + $from = $next; + } + + // Check for blank lines without * + $from = $commentStart; + $to = $commentEnd; + + while ($next = $phpcsFile->findNext(T_DOC_COMMENT_WHITESPACE, $from + 1, $to + 1, true)) { + if ($tokens[$next]['line'] > $tokens[$from]['line'] + 1) { + $ptr = $from + 1; + while ($tokens[$ptr]['line'] === $tokens[$from]['line']) { + ++$ptr; + } + + $error = 'Blank line found in PHPDoc comment'; + $fix = $phpcsFile->addFixableError($error, $ptr, 'BlankLine'); + + if ($fix) { + $phpcsFile->fixer->addContentBefore($ptr, '*'); + } + } + + $from = $next; + } + } + + /** + * Checks indents of the comment (opening tag, lines with star, closing tag). + */ + private function checkCommentIndents(File $phpcsFile, int $commentStart, int $commentEnd) : void + { + $tokens = $phpcsFile->getTokens(); + + $allowEmptyLineBefore = [ + T_NAMESPACE, + T_USE, + ]; + + $next = $phpcsFile->findNext(T_WHITESPACE, $commentEnd + 1, null, true); + + // There is something exactly in the next line. + if ($next && $tokens[$next]['line'] === $tokens[$commentEnd]['line'] + 1) { + // Check indent of the next line. + if ($tokens[$next - 1]['code'] === T_WHITESPACE + && strpos($tokens[$next - 1]['content'], $phpcsFile->eolChar) === false + ) { + $indent = strlen($tokens[$next - 1]['content']); + } else { + $indent = 0; + } + } elseif (! $next + || ($tokens[$next]['line'] > $tokens[$commentEnd]['line'] + 1 + && in_array($tokens[$next]['code'], $allowEmptyLineBefore, true)) + ) { + $indent = 0; + } else { + return; + } + + // The open tag is alone in the line. + $previous = $phpcsFile->findPrevious(T_WHITESPACE, $commentStart - 1, null, true); + if ($tokens[$previous]['line'] < $tokens[$commentStart]['line']) { + // Check if comment starts with the same indent. + $spaces = $tokens[$commentStart - 1]; + if ($spaces['code'] === T_WHITESPACE + && strpos($spaces['content'], $phpcsFile->eolChar) === false + && strlen($spaces['content']) !== $indent + ) { + $error = 'Invalid doc comment indent. Expected %d spaces; %d found'; + $data = [ + $indent, + strlen($spaces['content']), + ]; + $fix = $phpcsFile->addFixableError($error, $commentStart, 'InvalidIndent', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($commentStart - 1, str_repeat(' ', $indent)); + } + } elseif ($spaces['code'] === T_WHITESPACE + && strpos($spaces['content'], $phpcsFile->eolChar) !== false + && $indent > 0 + ) { + $error = 'Invalid doc comment indent. Expected %d spaces; %d found'; + $data = [ + $indent, + 0, + ]; + $fix = $phpcsFile->addFixableError($error, $commentStart, 'InvalidIndent', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken( + $commentStart - 1, + $phpcsFile->eolChar . str_repeat(' ', $indent) + ); + } + } + } + + // This is one-line doc comment. + if ($tokens[$commentStart]['line'] === $tokens[$commentEnd]['line']) { + return; + } + + // Rest of the doc comment. + $from = $commentStart; + $search = [T_DOC_COMMENT_STAR, T_DOC_COMMENT_CLOSE_TAG]; + while ($next = $phpcsFile->findNext($search, $from + 1, $commentEnd + 1)) { + if ($tokens[$next]['line'] !== $tokens[$from]['line']) { + $spaces = $tokens[$next - 1]; + + if (strpos($spaces['content'], $phpcsFile->eolChar) !== false) { + $error = 'Invalid doc comment indent. Expected %d spaces; %d found'; + $data = [ + $indent + 1, + 0, + ]; + $fix = $phpcsFile->addFixableError($error, $next, 'InvalidIndent', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($next - 1, $phpcsFile->eolChar . ' '); + } + } elseif ($spaces['code'] === T_DOC_COMMENT_WHITESPACE + && strlen($spaces['content']) !== $indent + 1 + ) { + $error = 'Invalid doc comment indent. Expected %d spaces; %d found'; + $data = [ + $indent + 1, + strlen($spaces['content']), + ]; + $fix = $phpcsFile->addFixableError($error, $next, 'InvalidIndent', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($next - 1, str_repeat(' ', $indent + 1)); + } + } + } + + $from = $next; + } + } + + /** + * Check if there is one blank line before comment tags. + */ + private function checkBlankLineBeforeTags(File $phpcsFile, int $commentStart) : void + { + $tokens = $phpcsFile->getTokens(); + + if (! $tokens[$commentStart]['comment_tags']) { + return; + } + + $tag = $tokens[$commentStart]['comment_tags'][0]; + $beforeTag = $phpcsFile->findPrevious( + [T_DOC_COMMENT_WHITESPACE, T_DOC_COMMENT_STAR], + $tag - 1, + null, + true + ); + + if ($tokens[$beforeTag]['code'] === T_DOC_COMMENT_STRING + && $tokens[$beforeTag]['line'] === $tokens[$tag]['line'] - 1 + ) { + $firstOnLine = $phpcsFile->findFirstOnLine([], $tag, true); + + $error = 'Missing blank line before comment tags.'; + $fix = $phpcsFile->addFixableError($error, $firstOnLine, 'MissingBlankLine'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->addNewlineBefore($firstOnLine); + $phpcsFile->fixer->addContentBefore($firstOnLine, '*'); + $phpcsFile->fixer->endChangeset(); + } + } + } + + private function checkTagsSpaces(File $phpcsFile, int $commentStart, int $commentEnd) : void + { + $tokens = $phpcsFile->getTokens(); + + // Return when there is no tags in the comment. + if (empty($tokens[$commentStart]['comment_tags'])) { + return; + } + + // Return when comment contains one of the following tags. + $skipIfContains = ['@copyright', '@license']; + foreach ($tokens[$commentStart]['comment_tags'] as $tag) { + if (in_array(strtolower($tokens[$tag]['content']), $skipIfContains, true)) { + return; + } + } + + foreach ($tokens[$commentStart]['comment_tags'] as $tag) { + // Continue if next token is not a whitespace. + if ($tokens[$tag + 1]['code'] !== T_DOC_COMMENT_WHITESPACE) { + continue; + } + + if (in_array($tokens[$tag]['content'], $this->tagsWithContent, true)) { + $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd); + if (! $string || $tokens[$string]['line'] !== $tokens[$tag]['line']) { + $error = 'Content missing for %s tag in PHPDoc comment'; + $data = $tokens[$tag]['content']; + $phpcsFile->addError($error, $tag, 'EmptyTagContent', $data); + } + } + + // Continue if next token contains new line. + if (strpos($tokens[$tag + 1]['content'], $phpcsFile->eolChar) !== false) { + continue; + } + + // Continue if after next token the comment ends. + // It means end of the comment is in the same line as the tag. + if ($tokens[$tag + 2]['code'] === T_DOC_COMMENT_CLOSE_TAG) { + continue; + } + + // Check spaces after type for some tags. + if (in_array(strtolower($tokens[$tag]['content']), $this->tagWithType, true) + && $tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING + ) { + $this->checkSpacesAfterTag($phpcsFile, $tag); + } + + // Continue if next token is one space. + if ($tokens[$tag + 1]['content'] === ' ') { + continue; + } + + $error = 'There must be exactly one space after PHPDoc tag.'; + $fix = $phpcsFile->addFixableError($error, $tag + 1, 'SpaceAfterTag'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($tag + 1, ' '); + } + } + } + + private function checkSpacesAfterTag(File $phpcsFile, int $tag) : void + { + $tokens = $phpcsFile->getTokens(); + + $content = $tokens[$tag + 2]['content']; + $expected = implode(' ', array_filter(preg_split('/\s+/', $content))); + + if ($tokens[$tag + 3]['code'] === T_DOC_COMMENT_CLOSE_TAG) { + // In case when spacing between type and variable is correct. + // Space before closing comment tag are covered in another case. + if (trim($content) === $expected) { + return; + } + + $expected .= ' '; + } + + if ($content === $expected) { + return; + } + + $error = 'Invalid spacing in comment; expected: "%s", found "%s"'; + $data = [ + $expected, + $content, + ]; + $fix = $phpcsFile->addFixableError($error, $tag + 2, 'TagDescriptionSpacing', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($tag + 2, $expected); + } + } + + private function checkInheritDoc(File $phpcsFile, int $commentStart, int $commentEnd) : void + { + $tokens = $phpcsFile->getTokens(); + + $commentContent = $phpcsFile->getTokensAsString($commentStart + 1, $commentEnd - $commentStart - 1); + if (preg_match('/\*.*\{@inheritDoc\}/i', $commentContent, $m)) { + $error = 'Tag {@inheritDoc} is not allowed in doc-block comment. Please define explicitly types.'; + $phpcsFile->addError($error, $commentStart, 'InheritDoc'); + return; + } + + if (isset($tokens[$commentStart]['comment_tags'])) { + foreach ($tokens[$commentStart]['comment_tags'] as $tag) { + if (strtolower($tokens[$tag]['content']) === '@inheritdoc') { + $error = 'Tag @inheritDoc is not allowed in doc-block comment. Please define explicitly types.'; + $phpcsFile->addError($error, $tag, 'InheritDocTag'); + break; + } + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Commenting/FunctionCommentSniff.php b/src/ZendCodingStandard/Sniffs/Commenting/FunctionCommentSniff.php new file mode 100644 index 00000000..e434c060 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Commenting/FunctionCommentSniff.php @@ -0,0 +1,293 @@ +getTokens(); + $skip = Tokens::$methodPrefixes + + [T_WHITESPACE => T_WHITESPACE]; + + $commentEnd = $phpcsFile->findPrevious($skip, $stackPtr - 1, null, true); + if ($tokens[$commentEnd]['code'] === T_COMMENT) { + $error = 'You must use "/**" style comments for a function comment'; + $phpcsFile->addError($error, $stackPtr, 'WrongStyle'); + return; + } + + $commentStart = null; + if ($tokens[$commentEnd]['code'] === T_DOC_COMMENT_CLOSE_TAG) { + if ($tokens[$commentEnd]['line'] !== $tokens[$stackPtr]['line'] - 1) { + $error = 'There must be no blank lines after the function comment'; + $phpcsFile->addError($error, $commentEnd, 'SpacingAfter'); + } + + $commentStart = $tokens[$commentEnd]['comment_opener']; + + $this->processTagOrder($phpcsFile, $commentStart); + + if ($tokens[$commentStart]['line'] === $tokens[$commentEnd]['line']) { + $error = 'Function comment must be multiline comment'; + $fix = $phpcsFile->addFixableError($error, $commentStart, 'SingleLine'); + + if ($fix) { + $phpcsFile->fixer->addContent($commentStart, $phpcsFile->eolChar . ' *'); + } + } + } + } + + private function processTagOrder(File $phpcsFile, int $commentStart) : void + { + $tokens = $phpcsFile->getTokens(); + + $tags = $tokens[$commentStart]['comment_tags']; + + $nestedTags = []; + $data = []; + while ($tag = current($tags)) { + $key = key($tags); + if (isset($tags[$key + 1])) { + $lastFrom = $tags[$key + 1]; + } else { + $lastFrom = $tokens[$commentStart]['comment_closer']; + } + + $last = $phpcsFile->findPrevious( + [T_DOC_COMMENT_STAR, T_DOC_COMMENT_WHITESPACE], + $lastFrom - 1, + null, + true + ); + + // if the last character of the description is { + // we need to find closing curly bracket and treat the whole block + // as one, skip tags inside if there any + if (substr($tokens[$last]['content'], -1) === '{') { + $dep = 1; + $i = $last; + $max = $tokens[$commentStart]['comment_closer']; + while ($dep > 0 && $i < $max) { + $i = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $i + 1, $max); + + if (! $i) { + break; + } + + if ($tokens[$i]['content'][0] === '}') { + --$dep; + } + + if (substr($tokens[$i]['content'], -1) === '{') { + ++$dep; + } + } + + if ($dep > 0) { + $error = 'Tag contains nested description, but cannot find the closing bracket'; + $phpcsFile->addError($error, $last, 'NotClosed'); + return; + } + + $last = $i; + while (isset($tags[$key + 1]) && $tags[$key + 1] < $i) { + $tagName = strtolower($tokens[$tags[$key + 1]]['content']); + if (! array_filter($this->nestedTags, function ($v) use ($tagName) { + return strtolower($v) === $tagName; + })) { + $error = 'Tag %s cannot be nested.'; + $data = [ + $tokens[$tags[$key + 1]]['content'], + ]; + $phpcsFile->addError($error, $tags[$key + 1], 'NestedTag', $data); + return; + } + + $nestedTags[] = $tags[$key + 1]; + + next($tags); + ++$key; + } + } + + while ($tokens[$last + 1]['line'] === $tokens[$last]['line']) { + ++$last; + } + + $data[] = [ + 'tag' => strtolower($tokens[$tag]['content']), + 'token' => $tag, + 'first' => $phpcsFile->findFirstOnLine([], $tag, true), + 'last' => $last, + ]; + + next($tags); + } + + $firstTag = current($data); + + // Sorts values only and keep unchanged keys. + uasort($data, function (array $a, array $b) use ($data) { + $ai = array_search($a['tag'], $this->tagOrder, true); + $bi = array_search($b['tag'], $this->tagOrder, true); + + if ($ai !== false && $bi !== false && $ai !== $bi) { + return $ai > $bi ? 1 : -1; + } + + if ($ai !== false && $bi === false) { + return 1; + } + + if ($ai === false && $bi !== false) { + return -1; + } + + return array_search($a, $data, true) > array_search($b, $data, true) ? 1 : -1; + }); + + $last = key($data); + foreach ($data as $key => $val) { + if ($last <= $key) { + $last = $key; + continue; + } + + $error = 'Tags are ordered incorrectly. Here is the first in wrong order'; + $fix = $phpcsFile->addFixableError($error, $val['token'], 'InvalidOrder'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $toAdd = []; + foreach ($data as $k => $v) { + $toAdd[] = $phpcsFile->getTokensAsString($v['first'], $v['last'] - $v['first'] + 1); + for ($i = $v['first']; $i <= $v['last']; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + } + foreach ($toAdd as $content) { + $phpcsFile->fixer->addContent($firstTag['first'], $content); + } + $phpcsFile->fixer->endChangeset(); + } + return; + } + + // If order is correct check empty lines between tags. + $skip = [T_DOC_COMMENT_WHITESPACE, T_DOC_COMMENT_STAR]; + foreach ($tags as $key => $tag) { + // Skip the first tag. Empty line after description is added in other sniff. + if ($key === 0) { + continue; + } + + $i = $key; + do { + $prevTag = $tags[--$i]; + } while (in_array($prevTag, $nestedTags, true)); + if (in_array($tokens[$tag]['content'], $this->blankLineBefore, true) + && $tokens[$prevTag]['content'] !== $tokens[$tag]['content'] + ) { + $expected = 1; + } else { + $expected = 0; + } + + $prev = $phpcsFile->findPrevious($skip, $tag - 1, null, true); + $found = $tokens[$tag]['line'] - $tokens[$prev]['line'] - 1; + if ($found !== $expected) { + $error = 'Invalid number of empty lines between tags; expected %d, but found %d'; + $data = [ + $expected, + $found, + ]; + $fix = $phpcsFile->addFixableError($error, $prev + 2, 'BlankLine', $data); + + if ($fix) { + if ($found > $expected) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $prev + 1; $i < $tag; ++$i) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_WHITESPACE + && strpos($tokens[$i]['content'], $phpcsFile->eolChar) !== false + ) { + if ($found === $expected) { + break; + } + + --$found; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->endChangeset(); + } else { + $phpcsFile->fixer->addContent($prev, $phpcsFile->eolChar . '*'); + } + } + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Commenting/FunctionDataProviderTagSniff.php b/src/ZendCodingStandard/Sniffs/Commenting/FunctionDataProviderTagSniff.php new file mode 100644 index 00000000..f89f455b --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Commenting/FunctionDataProviderTagSniff.php @@ -0,0 +1,90 @@ +getTokens(); + $skip = Tokens::$methodPrefixes + + [T_WHITESPACE => T_WHITESPACE]; + + $commentEnd = $phpcsFile->findPrevious($skip, $stackPtr - 1, null, true); + // There is no doc-comment for the function. + if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG) { + return; + } + + $commentStart = $tokens[$commentEnd]['comment_opener']; + $tags = $tokens[$commentStart]['comment_tags']; + + // Checks @dataProvider tags + foreach ($tags as $pos => $tag) { + if (strtolower($tokens[$tag]['content']) !== '@dataprovider') { + continue; + } + + // Check if method name starts from "test". + $functionPtr = $phpcsFile->findNext(T_FUNCTION, $tag + 1); + $namePtr = $phpcsFile->findNext(T_STRING, $functionPtr + 1); + $functionName = $tokens[$namePtr]['content']; + + if (strpos($functionName, 'test') !== 0) { + $error = 'Tag @dataProvider is allowed only for test* methods.'; + $phpcsFile->addError($error, $tag, 'NoTestMethod'); + return; + } + + $params = $phpcsFile->getMethodParameters($functionPtr); + if (! $params) { + $error = 'Function "%s" does not accept any parameters.'; + $data = [$functionName]; + $phpcsFile->addError($error, $namePtr, 'MissingParameters', $data); + } + + // Check if data provider name is given and does not have "Provider" suffix. + if ($tokens[$tag + 1]['code'] !== T_DOC_COMMENT_WHITESPACE + || $tokens[$tag + 2]['code'] !== T_DOC_COMMENT_STRING + ) { + $error = 'Missing data provider name.'; + $phpcsFile->addError($error, $tag, 'MissingName'); + } else { + $providerName = $tokens[$tag + 2]['content']; + + if (preg_match('/Provider$/', $providerName)) { + $error = 'Data provider name should not have "Provider" suffix.'; + $phpcsFile->addError($error, $tag, 'DataProviderInvalidName'); + } + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Commenting/FunctionDisallowedTagSniff.php b/src/ZendCodingStandard/Sniffs/Commenting/FunctionDisallowedTagSniff.php new file mode 100644 index 00000000..15ed3edd --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Commenting/FunctionDisallowedTagSniff.php @@ -0,0 +1,96 @@ + 'Information about the author will be found with the commit.', + '@copyright' => 'Please see copyright notes on the top of the file.', + '@license' => 'Please see license notes on the top of the file.', + '@package' => '', + '@subpackage' => '', + '@version' => '', + '@inheritDoc' => 'Please define explicitly params, return type and throws for the method.', + '@expectedException' => 'Please use appropriate method instead just before call' + . ' which should throw the exception.', + '@expectedExceptionCode' => 'Please use appropriate method instead just before call' + . ' which should throw the exception.', + '@expectedExceptionMessage' => 'Please use appropriate method instead just before call' + . ' which should throw the exception.', + '@expectedExceptionMessageRegExp' => 'Please use appropriate method instead just before call' + . ' which should throw the exception.', + ]; + + /** + * @return int[] + */ + public function register() : array + { + return [T_FUNCTION]; + } + + /** + * @param int $stackPtr + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $skip = Tokens::$methodPrefixes + + [T_WHITESPACE => T_WHITESPACE]; + + $commentEnd = $phpcsFile->findPrevious($skip, $stackPtr - 1, null, true); + // There is no doc-comment for the function. + if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG) { + return; + } + + $commentStart = $tokens[$commentEnd]['comment_opener']; + foreach ($tokens[$commentStart]['comment_tags'] as $tag) { + $content = strtolower($tokens[$tag]['content']); + $result = array_filter($this->disallowedTags, function ($key) use ($content) { + return strtolower($key) === $content; + }, ARRAY_FILTER_USE_KEY); + + if (! $result) { + continue; + } + + $tagName = key($result); + $tagError = current($result); + $error = 'Tag %s is not allowed. %s'; + $errorCode = sprintf('%sTagNotAllowed', ucfirst(substr($tagName, 1))); + $data = [ + $tagName, + $tagError, + ]; + + $phpcsFile->addError($error, $tag, $errorCode, $data); + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseSniff.php b/src/ZendCodingStandard/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseSniff.php new file mode 100644 index 00000000..2001db83 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseSniff.php @@ -0,0 +1,47 @@ +getTokens(); + + $next = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true); + if (! $next) { + return; + } + + if ($tokens[$next]['code'] !== T_COMMENT) { + return; + } + + if ($tokens[$next]['line'] > $tokens[$stackPtr]['line']) { + return; + } + + $error = 'Inline comment is not allowed after closing curly bracket.'; + $phpcsFile->addError($error, $next, 'NotAllowed'); + } +} diff --git a/src/ZendCodingStandard/Sniffs/Commenting/PhpcsAnnotationSniff.php b/src/ZendCodingStandard/Sniffs/Commenting/PhpcsAnnotationSniff.php new file mode 100644 index 00000000..19acc52a --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Commenting/PhpcsAnnotationSniff.php @@ -0,0 +1,66 @@ +getTokens(); + $ignoredLines = $phpcsFile->tokenizer->ignoredLines; + $phpcsFile->tokenizer->ignoredLines = []; + + $next = $stackPtr; + while ($next = $phpcsFile->findNext(Tokens::$phpcsCommentTokens, $next + 1)) { + $this->overrideToken($phpcsFile, $next); + + if ($tokens[$next - 1]['content'] !== '@' + && ! preg_match('/@phpcs:/i', $tokens[$next]['content']) + ) { + $error = 'Missing @ before phpcs annotation'; + $fix = $phpcsFile->addFixableError($error, $next, 'MissingAt'); + + if ($fix) { + $content = preg_replace('/phpcs:/i', '@\\0', $tokens[$next]['content']); + $phpcsFile->fixer->replaceToken($next, $content); + } + } + } + + $phpcsFile->tokenizer->ignoredLines = $ignoredLines; + + return $phpcsFile->numTokens + 1; + } + + private function overrideToken(File $phpcsFile, int $stackPtr) + { + $clear = function () use ($stackPtr) { + $this->tokens[$stackPtr]['content'] = 'ZF-CS'; + }; + + $clear->bindTo($phpcsFile, File::class)(); + } +} diff --git a/src/ZendCodingStandard/Sniffs/Commenting/TagCaseSniff.php b/src/ZendCodingStandard/Sniffs/Commenting/TagCaseSniff.php new file mode 100644 index 00000000..3cd5a02a --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Commenting/TagCaseSniff.php @@ -0,0 +1,124 @@ + '@api', + '@author' => '@author', + '@category' => '@category', + '@copyright' => '@copyright', + '@deprecated' => '@deprecated', + '@example' => '@example', + '@filesource' => '@filesource', + '@global' => '@global', + '@ignore' => '@ignore', + '@inheritdoc' => '@inheritDoc', + '@internal' => '@internal', + '@license' => '@license', + '@link' => '@link', + '@method' => '@method', + '@package' => '@package', + '@param' => '@param', + '@property' => '@property', + '@property-read' => '@property-read', + '@property-write' => '@property-write', + '@return' => '@return', + '@see' => '@see', + '@since' => '@since', + '@source' => '@source', + '@subpackage' => '@subpackage', + '@throws' => '@throws', + '@todo' => '@todo', + '@uses' => '@uses', + '@used-by' => '@used-by', + '@var' => '@var', + '@version' => '@version', + // PHPUnit annotations + '@after' => '@after', + '@afterclass' => '@afterClass', + '@backupglobals' => '@backupGlobals', + '@backupstaticattributes' => '@backupStaticAttributes', + '@before' => '@before', + '@beforeclass' => '@beforeClass', + '@codecoverageignore' => '@codeCoverageIgnore', + '@codecoverageignorestart' => '@codeCoverageIgnoreStart', + '@codecoverageignoreend' => '@codeCoverageIgnoreEnd', + '@covers' => '@covers', + '@coversdefaultclass' => '@coversDefaultClass', + '@coversnothing' => '@coversNothing', + '@dataprovider' => '@dataProvider', + '@depends' => '@depends', + '@expectedexception' => '@expectedException', + '@expectedexceptioncode' => '@expectedExceptionCode', + '@expectedexceptionmessage' => '@expectedExceptionMessage', + '@expectedexceptionmessageregexp' => '@expectedExceptionMessageRegExp', + '@group' => '@group', + '@large' => '@large', + '@medium' => '@medium', + '@preserveglobalstate' => '@preserveGlobalState', + '@requires' => '@requires', + '@runtestsinseparateprocesses' => '@runTestsInSeparateProcesses', + '@runinseparateprocess' => '@runInSeparateProcess', + '@small' => '@small', + '@test' => '@test', + '@testdox' => '@testdox', + '@ticket' => '@ticket', + ]; + + /** + * @return int[] + */ + public function register() : array + { + return [T_DOC_COMMENT_TAG]; + } + + /** + * @param int $stackPtr + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + $content = $tokens[$stackPtr]['content']; + $lower = strtolower($content); + if (! isset($this->tags[$lower])) { + return; + } + + if ($this->tags[$lower] === $content) { + return; + } + + $tagName = $this->tags[$lower]; + $error = 'Invalid case tag. Expected "%s", but found "%s"'; + $errorCode = sprintf('%sTagWrongCase', ucfirst($tagName)); + $data = [ + $tagName, + $content, + ]; + + $fix = $phpcsFile->addFixableError($error, $stackPtr, $errorCode, $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr, $tagName); + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Commenting/TagWithTypeSniff.php b/src/ZendCodingStandard/Sniffs/Commenting/TagWithTypeSniff.php new file mode 100644 index 00000000..fb8bb1f5 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Commenting/TagWithTypeSniff.php @@ -0,0 +1,480 @@ +initScope($phpcsFile, $stackPtr); + + $this->type = null; + $this->types = []; + $this->description = null; + + $tokens = $phpcsFile->getTokens(); + + $tag = strtolower($tokens[$stackPtr]['content']); + if (! in_array($tag, $this->tags, true)) { + return; + } + + $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $stackPtr + 1); + if ($string !== $stackPtr + 2 + || $tokens[$string]['line'] !== $tokens[$stackPtr]['line'] + ) { + if ($tag === '@param') { + $error = 'Missing param type and name with tag %s'; + } else { + $error = 'Missing type with tag %s'; + } + $data = [$tag]; + $phpcsFile->addError($error, $stackPtr, 'MissingType', $data); + return; + } + + $split = preg_split('/\s/', $tokens[$stackPtr + 2]['content'], 2); + $this->type = array_shift($split); + $this->description = trim(array_shift($split) ?: '') ?: null; + + if ($tag === '@return' && ! $this->processReturnTag($phpcsFile, $stackPtr)) { + return; + } + + if ($tag === '@param' && ! $this->processParamTag($phpcsFile, $stackPtr)) { + return; + } + + if ($tag === '@var' && ! $this->processVarTag($phpcsFile, $stackPtr)) { + return; + } + + if (! $this->isType($tag, $this->type)) { + $error = 'Invalid type format with tag %s'; + $data = [$tag]; + $phpcsFile->addError($error, $stackPtr + 2, 'InvalidTypeFormat', $data); + return; + } + + if ($this->isThis($tag, $this->type) + && strtolower($this->type) !== $this->type + ) { + $error = 'Invalid case of type with tag %s; expected "%s" but found "%s"'; + $data = [ + $tag, + strtolower($this->type), + $this->type, + ]; + $fix = $phpcsFile->addFixableError($error, $stackPtr + 2, 'InvalidThisCase', $data); + + if ($fix) { + $content = trim(strtolower($this->type) . ' ' . $this->description); + $phpcsFile->fixer->replaceToken($stackPtr + 2, $content); + } + } + + // Type with tag contains only null, null[], null[][], null|null[], ... + $cleared = array_unique(explode('|', strtolower(strtr($this->type, ['[' => '', ']' => ''])))); + if (count($cleared) === 1 && $cleared[0] === 'null') { + $error = 'Type with tag %s contains only "null". Please specify all possible types'; + $data = [ + $tag, + $this->type, + ]; + $phpcsFile->addError($error, $stackPtr + 2, 'OnlyNullType', $data); + return; + } + + $this->checkTypes($phpcsFile, $tag, $stackPtr); + } + + /** + * @return bool True if can continue further processing. + */ + private function processReturnTag(File $phpcsFile, int $tagPtr) : bool + { + if (strtolower($this->type) === 'void') { + if ($this->description) { + $error = 'Description for return "void" type is not allowed.' + . ' Please move it to method description.'; + $phpcsFile->addError($error, $tagPtr, 'ReturnVoidDescription'); + return false; + } + + $error = 'Return tag with "void" type is redundant.'; + $fix = $phpcsFile->addFixableError($error, $tagPtr, 'ReturnVoid'); + + if ($fix) { + $this->removeTag($phpcsFile, $tagPtr); + } + + return false; + } + + if (isset($this->description[0]) && $this->description[0] === '$') { + $error = 'Return tag description cannot start from variable name.'; + $phpcsFile->addError($error, $tagPtr + 2, 'ReturnVariable'); + } + + return true; + } + + private function processParamTag(File $phpcsFile, int $tagPtr) : bool + { + $tokens = $phpcsFile->getTokens(); + + $split = preg_split('/\s/', $tokens[$tagPtr + 2]['content'], 3); + + if (! isset($split[1])) { + if ($this->isVariable($split[0])) { + $error = 'Missing param type for param %s'; + $data = [ + $split[0], + ]; + $phpcsFile->addError($error, $tagPtr + 2, 'MissingParamType', $data); + } else { + $error = 'Missing parameter name in PHPDocs'; + $phpcsFile->addError($error, $tagPtr + 2, 'MissingParamName'); + } + + return false; + } + + if (! $this->isVariable($split[1])) { + $error = empty($split[1]) ? 'Missing parameter name in PHPDocs' : 'Invalid parameter name'; + $phpcsFile->addError($error, $tagPtr + 2, 'InvalidParamName'); + return false; + } + + return true; + } + + private function processVarTag(File $phpcsFile, int $tagPtr) : bool + { + $tokens = $phpcsFile->getTokens(); + + $nested = 0; + $commentStart = $phpcsFile->findPrevious(T_DOC_COMMENT_OPEN_TAG, $tagPtr - 1); + $i = $tagPtr; + while ($i = $phpcsFile->findPrevious(T_DOC_COMMENT_STRING, $i - 1, $commentStart)) { + if ($tokens[$i]['content'][0] === '}') { + --$nested; + } + + if (substr($tokens[$i]['content'], -1) === '{') { + ++$nested; + } + + $i = $phpcsFile->findPrevious([T_DOC_COMMENT_TAG, T_DOC_COMMENT_OPEN_TAG], $i - 1); + } + + $condition = end($tokens[$tagPtr]['conditions']); + $isMemberVar = isset(Tokens::$ooScopeTokens[$condition]); + + $split = preg_split('/\s/', $tokens[$tagPtr + 2]['content'], 3); + if ($nested > 0 || ! $isMemberVar) { + if (! isset($split[1])) { + if ($this->isVariable($split[0])) { + $error = 'Missing variable type'; + $phpcsFile->addError($error, $tagPtr + 2, 'MissingVarType'); + } else { + $error = 'Missing variable name in PHPDocs'; + $phpcsFile->addError($error, $tagPtr + 2, 'MissingVarName'); + } + + return false; + } + + if (! $this->isVariable($split[1])) { + $error = empty($split[1]) ? 'Missing variable name in PHPDocs' : 'Invalid variable name'; + $phpcsFile->addError($error, $tagPtr + 2, 'InvalidVarName'); + return false; + } + + return true; + } + + if (! empty($split[0][0]) && $split[0][0] === '$') { + $error = 'Variable name should not be included in the tag'; + $fix = $phpcsFile->addFixableError($error, $tagPtr + 2, 'VariableName'); + + if ($fix) { + unset($split[0]); + $content = trim(implode(' ', $split)); + if ($phpcsFile->getTokens()[$tagPtr + 3]['code'] !== T_DOC_COMMENT_WHITESPACE) { + $content .= ' '; + } + $phpcsFile->fixer->beginChangeset(); + if (trim($content) === '') { + $phpcsFile->fixer->replaceToken($tagPtr + 1, ''); + } + $phpcsFile->fixer->replaceToken($tagPtr + 2, $content); + $phpcsFile->fixer->endChangeset(); + } + return false; + } + + if (! empty($split[1][0]) && $split[1][0] === '$') { + $error = 'Variable name should not be included in the tag'; + $fix = $phpcsFile->addFixableError($error, $tagPtr + 2, 'VariableName'); + + if ($fix) { + unset($split[1]); + $phpcsFile->fixer->replaceToken($tagPtr + 2, implode(' ', $split)); + } + } + + return true; + } + + private function checkTypes(File $phpcsFile, string $tag, int $tagPtr) : void + { + $hasInvalidType = false; + $this->types = explode('|', $this->type); + + // Check if types are unique. + $uniq = array_unique($this->types); + if ($uniq !== $this->types) { + $expected = implode('|', $uniq); + $error = 'Duplicated types with tag; expected "%s", but found "%s"'; + $data = [ + $expected, + $this->type, + ]; + $fix = $phpcsFile->addFixableError($error, $tagPtr + 2, 'DuplicateTypes', $data); + + if ($fix) { + $content = trim($expected . ' ' . $this->description); + $phpcsFile->fixer->replaceToken($tagPtr + 2, $content); + } + + return; + } + + $count = count($this->types); + foreach ($this->types as $key => $type) { + $lower = strtolower($type); + + if ($count > 1 + && ($lower === 'mixed' || strpos($lower, 'mixed[') === 0) + ) { + $error = 'Type %s cannot be mixed with other types.'; + $data = [ + $type, + ]; + $phpcsFile->addError($error, $tagPtr + 2, 'TypeMixed', $data); + + $hasInvalidType = true; + continue; + } + + $clearType = strtr($lower, ['[' => '', ']' => '']); + if ($tag === '@param' || $tag === '@var') { + if (in_array($clearType, ['void', 'true', 'false'], true)) { + $error = 'Invalid param type: "%s"'; + $code = sprintf('InvalidType%s', ucfirst($clearType)); + $data = [ + $type, + ]; + $phpcsFile->addError($error, $tagPtr + 2, $code, $data); + + $hasInvalidType = true; + continue; + } + } + + // todo: what with void[] ? + if ($clearType === 'void') { + // If void is mixed up with other return types. + $error = 'Type "void" is mixed with other types.'; + $phpcsFile->addError($error, $tagPtr + 2, 'VoidMixed'); + + $hasInvalidType = true; + continue; + } + + if (in_array(strtolower($type), ['null', 'true', 'false'], true)) { + $suggestedType = strtolower($type); + } else { + $suggestedType = $this->getSuggestedType($type); + } + if ($suggestedType !== $type) { + if (strpos($suggestedType, 'self') === 0 + && strtolower($type) !== $suggestedType + ) { + if ($tag === '@param' || $tag === '@var') { + $error = 'The type cannot be class name. Please use "self" or "static" instead'; + } else { + $error = 'Return type cannot be class name. Please use "self", "static" or "$this" instead' + . ' depends what you expect to be returned'; + } + $phpcsFile->addError($error, $tagPtr + 2, 'InvalidReturnClassName'); + + $hasInvalidType = true; + continue; + } + + $error = 'Invalid type with tag; expected "%s", but found "%s"'; + $data = [ + $suggestedType, + $type, + ]; + $fix = $phpcsFile->addFixableError($error, $tagPtr + 2, 'InvalidType', $data); + + if ($fix) { + $this->types[$key] = $suggestedType; + $content = trim(implode('|', $this->types) . ' ' . $this->description); + if ($phpcsFile->getTokens()[$tagPtr + 3]['code'] !== T_DOC_COMMENT_WHITESPACE) { + $content .= ' '; + } + $phpcsFile->fixer->replaceToken($tagPtr + 2, $content); + } + + $hasInvalidType = true; + continue; + } + } + + if ($hasInvalidType) { + return; + } + + // Check boolean values with tag + $lowerReturnDocTypes = explode('|', strtolower($this->type)); + $hasTrue = in_array('true', $lowerReturnDocTypes, true); + $hasFalse = in_array('false', $lowerReturnDocTypes, true); + if (in_array('bool', $lowerReturnDocTypes, true)) { + if ($hasTrue) { + $error = 'Type with tag contains "bool" and "true". Please use just "bool"'; + $fix = $phpcsFile->addFixableError($error, $tagPtr + 2, 'BoolAndTrue'); + + if ($fix) { + $types = array_filter($this->types, function ($v) { + return strtolower($v) !== 'true'; + }); + $content = trim(implode('|', $types) . ' ' . $this->description); + $phpcsFile->fixer->replaceToken($tagPtr + 2, $content); + } + + return; + } + + if ($hasFalse) { + $error = 'Type with tag contains "bool" and "false". Please use just "bool"'; + $fix = $phpcsFile->addFixableError($error, $tagPtr + 2, 'BoolAndFalse'); + + if ($fix) { + $types = array_filter($this->types, function ($v) { + return strtolower($v) !== 'false'; + }); + $content = trim(implode('|', $types) . ' ' . $this->description); + $phpcsFile->fixer->replaceToken($tagPtr + 2, $content); + } + + return; + } + } elseif ($hasTrue && $hasFalse) { + $error = 'Return tag contains "true" and "false". Please use "bool" instead.'; + $fix = $phpcsFile->addFixableError($error, $tagPtr + 2, 'TrueAndFalse'); + + if ($fix) { + $types = array_filter($this->types, function ($v) { + return ! in_array(strtolower($v), ['true', 'false'], true); + }); + $types[] = 'bool'; + $content = trim(implode('|', $types) . ' ' . $this->description); + $phpcsFile->fixer->replaceToken($tagPtr + 2, $content); + } + + return; + } + + // todo: here was previously uniqueness check + + // Check if order of types is as expected: first null, then simple types, and then complex. + usort($this->types, function ($a, $b) { + return $this->sortTypes($a, $b); + }); + $content = implode('|', $this->types); + if ($content !== $this->type) { + $error = 'Invalid order of types with tag; expected "%s" but found "%s"'; + $data = [ + $content, + $this->type, + ]; + $fix = $phpcsFile->addFixableError($error, $tagPtr + 2, 'InvalidOrder', $data); + + if ($fix) { + $content = trim($content . ' ' . $this->description); + $phpcsFile->fixer->replaceToken($tagPtr + 2, $content); + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Commenting/VariableCommentSniff.php b/src/ZendCodingStandard/Sniffs/Commenting/VariableCommentSniff.php new file mode 100644 index 00000000..237ea8e3 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Commenting/VariableCommentSniff.php @@ -0,0 +1,179 @@ +getTokens(); + $ignore = [ + T_PUBLIC, + T_PRIVATE, + T_PROTECTED, + T_VAR, + T_STATIC, + T_WHITESPACE, + ]; + + $commentEnd = $phpcsFile->findPrevious($ignore, $stackPtr - 1, null, true); + if ($commentEnd === false + || ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG + && $tokens[$commentEnd]['code'] !== T_COMMENT) + ) { + $error = 'Missing member variable doc comment'; + $phpcsFile->addError($error, $stackPtr, 'Missing'); + return; + } + + if ($tokens[$commentEnd]['code'] === T_COMMENT) { + if ($tokens[$commentEnd]['line'] === $tokens[$stackPtr]['line'] - 1) { + $error = 'You must use "/**" style comments for a member variable comment'; + $phpcsFile->addError($error, $commentEnd, 'WrongStyle'); + } else { + $error = 'Missing member variable doc comment'; + $phpcsFile->addError($error, $stackPtr, 'Missing'); + } + + return; + } + + $commentStart = $tokens[$commentEnd]['comment_opener']; + + $foundVar = null; + + $tags = $tokens[$commentStart]['comment_tags']; + while ($tag = current($tags)) { + $key = key($tags); + if (isset($tags[$key + 1])) { + $lastFrom = $tags[$key + 1]; + } else { + $lastFrom = $tokens[$commentStart]['comment_closer']; + } + + $last = $phpcsFile->findPrevious( + [T_DOC_COMMENT_STAR, T_DOC_COMMENT_WHITESPACE], + $lastFrom - 1, + null, + true + ); + + if (substr($tokens[$last]['content'], -1) === '{') { + $dep = 1; + $i = $last; + $max = $tokens[$commentStart]['comment_closer']; + while ($dep > 0 && $i < $max) { + $i = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $i + 1, $max); + + if (! $i) { + break; + } + + if ($tokens[$i]['content'][0] === '}') { + --$dep; + } + + if (substr($tokens[$i]['content'], -1) === '{') { + ++$dep; + } + } + + if ($dep > 0) { + $error = 'Tag contains nested description, but cannot find the closing bracket'; + $phpcsFile->addError($error, $last, 'NotClosed'); + return; + } + + while (isset($tags[$key + 1]) && $tags[$key + 1] < $i) { + $tagName = strtolower($tokens[$tags[$key + 1]]['content']); + if (! array_filter($this->nestedTags, function ($v) use ($tagName) { + return strtolower($v) === $tagName; + })) { + $error = 'Tag %s cannot be nested.'; + $data = [ + $tokens[$tags[$key + 1]]['content'], + ]; + $phpcsFile->addError($error, $tags[$key + 1], 'NestedTag', $data); + return; + } + + $nestedTags[] = $tags[$key + 1]; + + next($tags); + ++$key; + } + } + + if (strtolower($tokens[$tag]['content']) === '@var') { + if ($foundVar !== null) { + $error = 'Only one @var tag is allowed in a member variable comment'; + $phpcsFile->addError($error, $tag, 'DuplicateVar'); + } else { + $foundVar = $tag; + } + } else { + $error = '%s tag is not allowed in member variable comment'; + $data = [$tokens[$tag]['content']]; + $phpcsFile->addError($error, $tag, 'TagNotAllowed', $data); + } + + next($tags); + } + + // The @var tag is the only one we require. + if ($foundVar === null) { + $error = 'Missing @var tag in member variable comment'; + $phpcsFile->addError($error, $commentEnd, 'MissingVar'); + } + } + + /** + * @param int $stackPtr + */ + protected function processVariable(File $phpcsFile, $stackPtr) : void + { + // Sniff process only class member vars. + } + + /** + * @param int $stackPtr + */ + protected function processVariableInString(File $phpcsFile, $stackPtr) : void + { + // Sniff process only class member vars. + } +} diff --git a/src/ZendCodingStandard/Sniffs/Files/DeclareStrictTypesSniff.php b/src/ZendCodingStandard/Sniffs/Files/DeclareStrictTypesSniff.php new file mode 100644 index 00000000..ddf542a7 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Files/DeclareStrictTypesSniff.php @@ -0,0 +1,124 @@ +getTokens(); + + $string = $phpcsFile->findNext( + T_STRING, + $tokens[$stackPtr]['parenthesis_opener'] + 1, + $tokens[$stackPtr]['parenthesis_closer'] + ); + + // It is no strict type declaration. + if ($string === false + || stripos($tokens[$string]['content'], 'strict_types') === false + ) { + return; + } + + $prev = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true); + + if ($tokens[$prev]['code'] === T_DOC_COMMENT_CLOSE_TAG) { + if ($this->checkTags($phpcsFile, $tokens[$tokens[$prev]['comment_opener']])) { + return; + } + } + + $eos = $phpcsFile->findEndOfStatement($stackPtr); + + if ($tokens[$prev]['code'] !== T_OPEN_TAG) { + $error = 'Wrong place of strict type declaration statement; must be above comment'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'BelowComment'); + + if ($fix) { + $prev = $phpcsFile->findPrevious([T_OPEN_TAG, T_DOC_COMMENT_CLOSE_TAG], $prev - 1); + $this->fix($phpcsFile, $stackPtr, $eos, $prev); + } + + return; + } + + $next = $phpcsFile->findNext(T_WHITESPACE, $eos + 1, null, true); + if ($tokens[$next]['code'] === T_DOC_COMMENT_OPEN_TAG + && $this->checkTags($phpcsFile, $tokens[$next]) + ) { + $error = 'Wrong place of strict type declaration statement; must be below comment'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'AboveComment'); + + if ($fix) { + $this->fix($phpcsFile, $stackPtr, $eos, $tokens[$next]['comment_closer']); + } + } + } + + private function fix(File $phpcsFile, int $start, int $eos, int $after) : void + { + $declaration = $phpcsFile->getTokensAsString($start, $eos - $start + 1); + + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->addContent($after, $phpcsFile->eolChar . $declaration . $phpcsFile->eolChar); + for ($i = $start; $i <= $eos; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->endChangeset(); + } + + private function checkTags(File $phpcsFile, array $tag) : bool + { + $tokens = $phpcsFile->getTokens(); + + $tags = array_map(function ($value) { + return strtolower($value); + }, $this->containsTags); + + foreach ($tag['comment_tags'] ?? [] as $token) { + if (false !== ($i = array_search(strtolower($tokens[$token]['content']), $tags, true))) { + unset($tags[$i]); + } + } + + return ! $tags; + } +} diff --git a/src/ZendCodingStandard/Sniffs/Formatting/DoubleColonSniff.php b/src/ZendCodingStandard/Sniffs/Formatting/DoubleColonSniff.php new file mode 100644 index 00000000..a7c0f328 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Formatting/DoubleColonSniff.php @@ -0,0 +1,48 @@ +getTokens(); + + if ($tokens[$stackPtr - 1]['code'] === T_WHITESPACE) { + $error = 'A double colon must not be preceded by a whitespace.'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceBefore'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr - 1, ''); + } + } + + if ($tokens[$stackPtr + 1]['code'] === T_WHITESPACE) { + $error = 'A double colon must not be followed by a whitespace.'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfter'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr + 1, ''); + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Formatting/HeredocSniff.php b/src/ZendCodingStandard/Sniffs/Formatting/HeredocSniff.php new file mode 100644 index 00000000..7516b4e2 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Formatting/HeredocSniff.php @@ -0,0 +1,77 @@ +getTokens(); + $content = $tokens[$stackPtr]['content']; + + $expected = preg_replace('/<<<\s+/', '<<<', $tokens[$stackPtr]['content']); + if ($content !== $expected) { + $error = 'Heredoc start tag cannot contain any whitespaces; found "%s", expected "%s"'; + $data = [ + trim($content), + trim($expected), + ]; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Space', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr, $expected); + } + } + + $expected = strtoupper($content); + if ($content !== $expected) { + if (preg_match('/[a-z][A-Z]/', $content)) { + $error = 'Heredoc tag must be uppercase underscore separated, cannot be camel case'; + $phpcsFile->addError($error, $stackPtr, 'CamelCase'); + } else { + $error = 'Heredoc tag must be uppercase; found "%s"; expected "%s"'; + $data = [ + trim($content), + trim($expected), + ]; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Uppercase', $data); + + if ($fix) { + $closer = $tokens[$stackPtr]['scope_closer']; + + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($stackPtr, $expected); + $phpcsFile->fixer->replaceToken($closer, strtoupper($tokens[$closer]['content'])); + $phpcsFile->fixer->endChangeset(); + } + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Formatting/NewKeywordSniff.php b/src/ZendCodingStandard/Sniffs/Formatting/NewKeywordSniff.php new file mode 100644 index 00000000..d8975152 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Formatting/NewKeywordSniff.php @@ -0,0 +1,46 @@ +getTokens(); + + if ($tokens[$stackPtr + 1]['code'] !== T_WHITESPACE) { + $error = 'A "new" keyword must be followed by a single space.'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'MissingSpace'); + + if ($fix) { + $phpcsFile->fixer->addContent($stackPtr, ' '); + } + } elseif ($tokens[$stackPtr + 1]['content'] !== ' ') { + $error = 'A "new" keyword must be followed by a single space.'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'TooManySpaces'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr + 1, ' '); + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Formatting/NoSpaceAfterSplatSniff.php b/src/ZendCodingStandard/Sniffs/Formatting/NoSpaceAfterSplatSniff.php new file mode 100644 index 00000000..44fcba9f --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Formatting/NoSpaceAfterSplatSniff.php @@ -0,0 +1,39 @@ +getTokens(); + + if ($tokens[$stackPtr + 1]['code'] === T_WHITESPACE) { + $error = 'A splat operator must not be followed by a space'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceFound'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr + 1, ''); + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Formatting/ReferenceSniff.php b/src/ZendCodingStandard/Sniffs/Formatting/ReferenceSniff.php new file mode 100644 index 00000000..f32076c0 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Formatting/ReferenceSniff.php @@ -0,0 +1,69 @@ +getTokens(); + + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true); + + $tokenCodes = Tokens::$assignmentTokens + [ + T_COMMA => T_COMMA, + T_OPEN_PARENTHESIS => T_OPEN_PARENTHESIS, + T_OPEN_SHORT_ARRAY => T_OPEN_SHORT_ARRAY, + ]; + if (! in_array($tokens[$prev]['code'], $tokenCodes, true)) { + return; + } + + // One space before & + if ($tokens[$prev]['line'] === $tokens[$stackPtr]['line'] + && ! in_array($tokens[$stackPtr - 1]['code'], [T_WHITESPACE, T_OPEN_PARENTHESIS, T_OPEN_SHORT_ARRAY], true) + ) { + $error = 'Missing space before reference character'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'MissingSpace'); + + if ($fix) { + $phpcsFile->fixer->addContentBefore($stackPtr, ' '); + } + } + + // No space after & + if ($tokens[$stackPtr + 1]['code'] === T_WHITESPACE) { + $error = 'Unexpected space after reference character'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'UnexpectedSpace'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr + 1, ''); + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Formatting/ReturnTypeSniff.php b/src/ZendCodingStandard/Sniffs/Formatting/ReturnTypeSniff.php new file mode 100644 index 00000000..1dbc4be6 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Formatting/ReturnTypeSniff.php @@ -0,0 +1,261 @@ +spacesBeforeColon = (int) $this->spacesBeforeColon; + $this->spacesAfterColon = (int) $this->spacesAfterColon; + $this->spacesAfterNullable = (int) $this->spacesAfterNullable; + $tokens = $phpcsFile->getTokens(); + + // Check if between the closing parenthesis and return type are only allowed tokens. + $parenthesisCloser = $phpcsFile->findPrevious( + [ + T_COLON, + T_NS_SEPARATOR, + T_NULLABLE, + T_STRING, + T_WHITESPACE, + ], + $stackPtr - 1, + null, + true + ); + if ($tokens[$parenthesisCloser]['code'] !== T_CLOSE_PARENTHESIS) { + $error = 'Return type declaration contains invalid token %s'; + $data = [$tokens[$parenthesisCloser]['type']]; + $phpcsFile->addError($error, $parenthesisCloser, 'InvalidToken', $data); + + return; + } + + $colon = $phpcsFile->findPrevious(T_COLON, $stackPtr - 1); + $nullable = $phpcsFile->findNext(T_NULLABLE, $colon + 1, $stackPtr); + + $this->checkSpacesBeforeColon($phpcsFile, $colon); + $this->checkSpacesAfterColon($phpcsFile, $colon); + if ($nullable) { + $this->checkSpacesAfterNullable($phpcsFile, $nullable); + } + + $first = $phpcsFile->findNext(Tokens::$emptyTokens, ($nullable ?: $colon) + 1, null, true); + $end = $phpcsFile->findNext([T_SEMICOLON, T_OPEN_CURLY_BRACKET], $stackPtr + 1); + $last = $phpcsFile->findPrevious(Tokens::$emptyTokens, $end - 1, null, true); + + $space = $phpcsFile->findNext(T_WHITESPACE, $first, $last + 1); + if ($space) { + $error = 'Return type declaration contains invalid token %s'; + $data = [$tokens[$space]['type']]; + $fix = $phpcsFile->addFixableError($error, $space, 'SpaceInReturnType', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($space, ''); + } + + return; + } + + $returnType = trim($phpcsFile->getTokensAsString($first, $last - $first + 1)); + + if ($first === $last + && in_array(strtolower($returnType), $this->simpleReturnTypes, true) + && ! in_array($returnType, $this->simpleReturnTypes, true) + ) { + $error = 'Simple return type must be lowercase. Found "%s", expected "%s"'; + $data = [ + $returnType, + strtolower($returnType), + ]; + $fix = $phpcsFile->addFixableError($error, $first, 'LowerCaseSimpleType', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr, strtolower($returnType)); + } + } + } + + /** + * Check if token before colon match configured number of spaces. + */ + private function checkSpacesBeforeColon(File $phpcsFile, int $colon) : void + { + $tokens = $phpcsFile->getTokens(); + + // The whitespace before colon is not expected and it is not present. + if ($this->spacesBeforeColon === 0 + && $tokens[$colon - 1]['code'] !== T_WHITESPACE + ) { + return; + } + + $expected = str_repeat(' ', $this->spacesBeforeColon); + + // Previous token contains expected number of spaces, + // and before whitespace there is close parenthesis token. + if ($this->spacesBeforeColon > 0 + && $tokens[$colon - 1]['content'] === $expected + && $tokens[$colon - 2]['code'] === T_CLOSE_PARENTHESIS + ) { + return; + } + + $error = 'There must be exactly %d space(s) between the closing parenthesis and the colon' + . ' when declaring a return type for a function'; + $data = [$this->spacesBeforeColon]; + $fix = $phpcsFile->addFixableError($error, $colon - 1, 'SpacesBeforeColon', $data); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + if ($tokens[$colon - 1]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($colon - 1, $expected); + if (isset($tokens[$colon - 2]) && $tokens[$colon - 2]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($colon - 2, ''); + } + } else { + $phpcsFile->fixer->addContentBefore($colon, $expected); + } + $phpcsFile->fixer->endChangeset(); + } + } + + /** + * Check if token after colon match configured number of spaces. + */ + private function checkSpacesAfterColon(File $phpcsFile, int $colon) : void + { + $tokens = $phpcsFile->getTokens(); + + // The whitespace after colon is not expected and it is not present. + if ($this->spacesAfterColon === 0 + && $tokens[$colon + 1]['code'] !== T_WHITESPACE + ) { + return; + } + + $expected = str_repeat(' ', $this->spacesAfterColon); + + // Next token contains expected number of spaces. + if ($this->spacesAfterColon > 0 + && $tokens[$colon + 1]['content'] === $expected + ) { + return; + } + + $error = 'There must be exactly %d space(s) between the colon and return type' + . ' when declaring a return type for a function'; + $data = [$this->spacesAfterColon]; + $fix = $phpcsFile->addFixableError($error, $colon, 'SpacesAfterColon', $data); + + if ($fix) { + if ($tokens[$colon + 1]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($colon + 1, $expected); + } else { + $phpcsFile->fixer->addContent($colon, $expected); + } + } + } + + /** + * Checks if token after nullable operator match configured number of spaces. + */ + private function checkSpacesAfterNullable(File $phpcsFile, int $nullable) : void + { + $tokens = $phpcsFile->getTokens(); + + // The whitespace after nullable operator is not expected and it is not present. + if ($this->spacesAfterNullable === 0 + && $tokens[$nullable + 1]['code'] !== T_WHITESPACE + ) { + return; + } + + $expected = str_repeat(' ', $this->spacesAfterNullable); + + // Next token contains expected number of spaces. + if ($this->spacesAfterNullable > 0 + && $tokens[$nullable + 1]['content'] === $expected + ) { + return; + } + + $error = 'There must be exactly %d space(s) between the nullable operator and return type' + . ' when declaring a return type for a function'; + $data = [$this->spacesAfterNullable]; + $fix = $phpcsFile->addFixableError($error, $nullable + 1, 'SpacesAfterNullable', $data); + + if ($fix) { + if ($tokens[$nullable + 1]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($nullable + 1, $expected); + } else { + $phpcsFile->fixer->addContent($nullable, $expected); + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Formatting/UnnecessaryParenthesesSniff.php b/src/ZendCodingStandard/Sniffs/Formatting/UnnecessaryParenthesesSniff.php new file mode 100644 index 00000000..e764c13d --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Formatting/UnnecessaryParenthesesSniff.php @@ -0,0 +1,287 @@ +getTokens(); + + if (isset($tokens[$stackPtr]['parenthesis_owner'])) { + return; + } + + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true); + if (in_array($tokens[$prev]['code'], $this->parenthesesAllowedTokens, true)) { + return; + } + + $closePtr = $tokens[$stackPtr]['parenthesis_closer']; + + // Skip when method call on new instance i.e.: (new DateTime())->modify(...) + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $closePtr + 1, null, true); + if ($tokens[$next]['code'] === T_OBJECT_OPERATOR) { + return; + } + + $firstInside = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, $closePtr, true); + $lastInside = $phpcsFile->findPrevious(Tokens::$emptyTokens, $closePtr - 1, $stackPtr + 1, true); + + if ($firstInside === $lastInside) { + $this->error($phpcsFile, $stackPtr, $closePtr, 'SingleExpression'); + return; + } + + if (! in_array($tokens[$prev]['code'], Tokens::$castTokens, true)) { + $instanceOf = $phpcsFile->findNext(T_INSTANCEOF, $stackPtr + 1, $closePtr); + if ($instanceOf !== false) { + $op = $phpcsFile->findNext(Tokens::$booleanOperators, $stackPtr + 1, $closePtr); + if ($op === false) { + $this->error($phpcsFile, $stackPtr, $closePtr, 'SingleInstanceOf'); + return; + } + } + } + + // Skip when operator before the parenthesis + if (in_array($tokens[$prev]['code'], Tokens::$operators + Tokens::$booleanOperators, true)) { + return; + } + + // Skip when operator after the parenthesis + if (in_array($tokens[$next]['code'], Tokens::$operators + Tokens::$booleanOperators, true)) { + return; + } + + // Check single expression casting + if (in_array($tokens[$prev]['code'], Tokens::$castTokens, true)) { + $op = $phpcsFile->findNext( + Tokens::$assignmentTokens + + Tokens::$booleanOperators + + Tokens::$equalityTokens + + Tokens::$operators + + [ + T_INLINE_ELSE => T_INLINE_ELSE, + T_INLINE_THEN => T_INLINE_THEN, + T_INSTANCEOF => T_INSTANCEOF, + ], + $stackPtr + 1, + $closePtr + ); + + if ($op === false) { + $this->error($phpcsFile, $stackPtr, $closePtr, 'SingleCast'); + } + return; + } + + // Check single expression negation, concatenation or arithmetic operation + $prevTokens = Tokens::$arithmeticTokens + [ + T_BOOLEAN_NOT => T_BOOLEAN_NOT, + T_STRING_CONCAT => T_STRING_CONCAT, + ]; + if (in_array($tokens[$prev]['code'], $prevTokens, true)) { + $op = $phpcsFile->findNext( + Tokens::$assignmentTokens + + Tokens::$booleanOperators + + Tokens::$equalityTokens + + Tokens::$operators + + [ + T_INLINE_ELSE => T_INLINE_ELSE, + T_INLINE_THEN => T_INLINE_THEN, + ], + $stackPtr + 1, + $closePtr + ); + + if ($op === false) { + $this->error($phpcsFile, $stackPtr, $closePtr, 'SingleNot'); + } + return; + } + + // Check single expression comparision + if (in_array($tokens[$prev]['code'], Tokens::$equalityTokens, true)) { + $op = $phpcsFile->findNext( + Tokens::$arithmeticTokens + + Tokens::$assignmentTokens + + Tokens::$booleanOperators + + [ + T_BITWISE_AND => T_BITWISE_AND, + T_BITWISE_OR => T_BITWISE_OR, + T_BITWISE_XOR => T_BITWISE_XOR, + T_COALESCE => T_COALESCE, + T_INLINE_ELSE => T_INLINE_ELSE, + T_INLINE_THEN => T_INLINE_THEN, + ], + $stackPtr + 1, + $closePtr + ); + + if ($op === false) { + $this->error($phpcsFile, $stackPtr, $closePtr, 'SingleEquality'); + } + return; + } + + $endPtr = $phpcsFile->findNext($this->endTokens, $closePtr + 1); + $lastPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $endPtr - 1, null, true); + + if ($lastPtr === $closePtr) { + // Nested ternary operator + if (in_array($tokens[$prev]['code'], [T_INLINE_THEN, T_INLINE_ELSE], true)) { + $op = $phpcsFile->findNext( + Tokens::$assignmentTokens + + Tokens::$booleanOperators + + [ + T_COALESCE => T_COALESCE, + T_INLINE_ELSE => T_INLINE_ELSE, + T_INLINE_THEN => T_INLINE_THEN, + ], + $stackPtr + 1, + $closePtr + ); + + if ($op === false) { + $this->error($phpcsFile, $stackPtr, $closePtr, 'NestedTernary'); + } + return; + } + + $this->error($phpcsFile, $stackPtr, $closePtr, 'MultipleExpression'); + } + } + + private function error(File $phpcsFile, int $openPtr, int $closePtr, string $errorCode) : void + { + $tokens = $phpcsFile->getTokens(); + + $error = 'Parentheses around expression "%s" are redundant.'; + $data = [$phpcsFile->getTokensAsString($openPtr + 1, $closePtr - $openPtr - 1)]; + $fix = $phpcsFile->addFixableError($error, $openPtr, $errorCode, $data); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + if (in_array($tokens[$openPtr - 1]['code'], $this->spaceTokens, true)) { + $phpcsFile->fixer->replaceToken($openPtr, ' '); + } else { + $phpcsFile->fixer->replaceToken($openPtr, ''); + } + $phpcsFile->fixer->replaceToken($closePtr, ''); + $phpcsFile->fixer->endChangeset(); + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Functions/ParamSniff.php b/src/ZendCodingStandard/Sniffs/Functions/ParamSniff.php new file mode 100644 index 00000000..fadc9b6b --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Functions/ParamSniff.php @@ -0,0 +1,497 @@ +initScope($phpcsFile, $stackPtr); + + $this->processedParams = []; + $this->params = $phpcsFile->getMethodParameters($stackPtr); + + if ($commentStart = $this->getCommentStart($phpcsFile, $stackPtr)) { + $this->processParamDoc($phpcsFile, $commentStart); + } + $this->processParamSpec($phpcsFile); + } + + private function processParamDoc(File $phpcsFile, int $commentStart) : void + { + $params = []; + $paramsMap = []; + $tokens = $phpcsFile->getTokens(); + + foreach ($tokens[$commentStart]['comment_tags'] as $tag) { + if (strtolower($tokens[$tag]['content']) !== '@param') { + continue; + } + + $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag + 1); + if ($string !== $tag + 2 + || $tokens[$string]['line'] !== $tokens[$tag]['line'] + ) { + // Missing param type and name + continue; + } + + $split = preg_split('/\s/', $tokens[$tag + 2]['content'], 3); + if (! isset($split[1]) || ! $this->isVariable($split[1])) { + // Missing param type or it's not a variable + continue; + } + + $name = $split[1]; + + $clearName = strtolower(preg_replace('/^\.{3}/', '', $name)); + if (in_array($clearName, $params, true)) { + $error = 'Param tag is duplicated for parameter %s'; + $data = [ + $name, + ]; + $phpcsFile->addError($error, $tag + 2, 'DuplicatedParamTag', $data); + continue; + } + $params[] = $clearName; + + $param = array_filter($this->params, function (array $param) use ($clearName) { + return strtolower($param['name']) === $clearName; + }); + + if (! $param) { + $error = 'Parameter %s has not been found in function declaration'; + $data = [ + $name, + ]; + $phpcsFile->addError($error, $tag + 2, 'NoParameter', $data); + continue; + } + + // Add param to processed list, even if it may not be checked. + $this->processedParams[] = key($param); + $paramsMap[key($param)] = ['token' => $tag, 'name' => $name]; + + if (! $this->isType('@param', $split[0])) { + // The type definition is invalid + continue; + } + $description = $split[2] ?? null; + $type = $split[0]; + + $this->checkParam($phpcsFile, current($param), $tag, $name, $type, $description); + } + + $last = current($this->processedParams); + foreach ($this->processedParams as $current) { + if ($last > $current) { + $error = 'Wrong param order, the first wrong is %s'; + $data = [ + $paramsMap[$current]['name'], + ]; + $fix = $phpcsFile->addFixableError($error, $paramsMap[$current]['token'], 'WrongParamOrder', $data); + + if ($fix) { + $this->fixParamOrder($phpcsFile, $paramsMap, $current); + } + + break; + } + + $last = $current; + } + } + + /** + * @param string[] $map + */ + private function fixParamOrder(File $phpcsFile, array $map, int $wrong) : void + { + $tokens = $phpcsFile->getTokens(); + + $tagPtr = $map[$wrong]['token']; + + $line = $tokens[$tagPtr]['line']; + // Find first element in line with token, all it will be moved. + $start = $phpcsFile->findFirstOnLine([], $tagPtr, true); + + $end = $tagPtr; + while (true) { + while ($tokens[$end + 1]['line'] === $line) { + ++$end; + } + + $next = $phpcsFile->findNext( + [T_DOC_COMMENT_WHITESPACE, T_DOC_COMMENT_STAR], + $end, + null, + true + ); + + if ($tokens[$next]['code'] !== T_DOC_COMMENT_STRING + || $tokens[$next]['line'] !== $line + 1 + ) { + break; + } + + ++$line; + $end = $next; + } + + $contentToMove = $phpcsFile->getTokensAsString($start, $end - $start + 1); + + // Where to move? + foreach ($map as $key => $data) { + if ($key > $wrong) { + $moveBefore = $phpcsFile->findFirstOnLine([], $data['token'], true); + break; + } + } + + $phpcsFile->fixer->beginChangeset(); + // Remove param from the old position. + for ($i = $start; $i <= $end; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + // Put param in the new position. + $phpcsFile->fixer->addContentBefore($moveBefore, $contentToMove); + $phpcsFile->fixer->endChangeset(); + } + + private function replaceParamTypeHint(File $phpcsFile, int $varPtr, string $newTypeHint) : void + { + $last = $phpcsFile->findPrevious([T_ARRAY_HINT, T_CALLABLE, T_STRING], $varPtr - 1); + $first = $phpcsFile->findPrevious([T_NULLABLE, T_STRING, T_NS_SEPARATOR], $last - 1, null, true); + + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($last, $newTypeHint); + for ($i = $last - 1; $i > $first; --$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->endChangeset(); + } + + /** + * @param array $param Real param function details.z + * @param null|int $tagPtr Position of the @param tag. + * @param null|string $name Name of the param in the @param tag. + * @param null|string $typeStr Type of the param in the @param tag. + * @param null|string $description Description of the param in the @param tag. + */ + private function checkParam( + File $phpcsFile, + array $param, + int $tagPtr = null, + string $name = null, + string $typeStr = null, + string $description = null + ) : void { + $typeHint = $param['type_hint']; + + if ($typeHint) { + $suggestedType = $this->getSuggestedType($typeHint); + + if ($suggestedType !== $typeHint) { + $error = 'Invalid type hint for param %s; expected "%s", but found "%s"'; + $data = [ + $param['name'], + $suggestedType, + $typeHint, + ]; + $fix = $phpcsFile->addFixableError($error, $param['token'], 'InvalidTypeHint', $data); + + if ($fix) { + $this->replaceParamTypeHint( + $phpcsFile, + $param['token'], + $suggestedType + ); + } + + $typeHint = $suggestedType; + } + } + $lowerTypeHint = strtolower($typeHint); + + // There is no param tag for the parameter + if (! $tagPtr) { + if (! $typeHint) { + $error = 'Parameter %s needs specification in PHPDocs'; + $data = [ + $param['name'], + ]; + $phpcsFile->addError($error, $param['token'], 'MissingSpecification', $data); + } + + return; + } + + $clearName = preg_replace('/^\.{3}/', '', $name); + $isVariadic = $name !== $clearName; + + if ($param['name'] !== $clearName) { + $error = 'Parameter name is not consistent, found: "%s" and "%s"'; + $data = [ + $clearName, + $param['name'], + ]; + $phpcsFile->addError($error, $tagPtr, 'InconsistentParamName', $data); + } + + $isSpecVariadic = $param['variable_length'] === true; + if ($isVariadic xor $isSpecVariadic) { + $error = 'Parameter variadic inconsistent'; + $phpcsFile->addError($error, $tagPtr, 'InconsistentVariadic'); + } + + $types = explode('|', $typeStr); + + // Check if null is one of the types + if (($param['nullable_type'] + || (isset($param['default']) && strtolower($param['default']) === 'null')) + && ! preg_grep('/^null$/i', $types) + ) { + $error = 'Missing type "null" for nullable parameter %s'; + $data = [ + $param['name'], + ]; + $fix = $phpcsFile->addFixableError($error, $tagPtr + 2, 'ParamDocMissingNull', $data); + + if ($fix) { + $content = trim('null|' . implode('|', $types) . ' ' . $name . ' ' . $description); + $phpcsFile->fixer->replaceToken($tagPtr + 2, $content); + } + } + + $count = count($types); + $break = false; + foreach ($types as $key => $type) { + $lower = strtolower($type); + + if ($lower === 'null' + && $typeHint + && ! $param['nullable_type'] + && (! isset($param['default']) + || $param['default'] !== 'null') + ) { + $error = 'Param %s cannot have "null" value'; + $data = [ + $name, + ]; + $fix = $phpcsFile->addFixableError($error, $tagPtr + 2, 'ParamDocNull', $data); + + if ($fix) { + unset($types[$key]); + $content = trim(implode('|', $types) . ' ' . $name . ' ' . $description); + $phpcsFile->fixer->replaceToken($tagPtr + 2, $content); + } + + $break = true; + continue; + } + + if ($typeHint) { + $simpleTypes = array_merge($this->simpleReturnTypes, ['mixed']); + + // array + if (in_array($lowerTypeHint, ['array', '?array'], true) + && ! in_array($lower, ['null', 'array'], true) + && strpos($type, '[]') === false + ) { + $error = 'Param type contains "%s" which is not an array type'; + $data = [ + $type, + ]; + $phpcsFile->addError($error, $tagPtr + 2, 'NotArrayType', $data); + + $break = true; + continue; + } + + // iterable + if (in_array($lowerTypeHint, ['iterable', '?iterable'], true) + && in_array($lower, $simpleTypes, true) + ) { + $error = 'Param type contains "%s" which is not an iterable type'; + $data = [ + $type, + ]; + $phpcsFile->addError($error, $tagPtr + 2, 'NotIterableType', $data); + + $break = true; + continue; + } + + // traversable + if (in_array($lowerTypeHint, [ + 'traversable', + '?traversable', + '\traversable', + '?\traversable', + ], true) + && ! in_array($lower, ['null', 'traversable', '\traversable'], true) + && in_array($lower, $simpleTypes, true) + ) { + $error = 'Param type contains "%s" which is not a traversable type'; + $data = [ + $type, + ]; + $phpcsFile->addError($error, $tagPtr + 2, 'NotTraversableType', $data); + + $break = true; + continue; + } + + // generator + if (in_array($lowerTypeHint, [ + 'generator', + '?generator', + '\generator', + '?\generator', + ], true) + && ! in_array($lower, ['null', 'generator', '\generator'], true) + && in_array($lower, array_merge($simpleTypes, ['mixed']), true) + ) { + $error = 'Param type contains %s which is not a generator type'; + $data = [ + $type, + ]; + $phpcsFile->addError($error, $tagPtr + 2, 'NotGeneratorType', $data); + + $break = true; + continue; + } + + $needSpecificationTypes = [ + 'array', + '?array', + 'iterable', + '?iterable', + 'traversable', + '?traversable', + '\traversable', + '?\traversable', + 'generator', + '?generator', + '\generator', + '?\generator', + ]; + + if (! in_array($lowerTypeHint, $needSpecificationTypes, true) + && ((in_array($lowerTypeHint, $simpleTypes, true) + && $lower !== 'null' + && $lower !== $lowerTypeHint + && '?' . $lower !== $lowerTypeHint) + || (! in_array($lowerTypeHint, $simpleTypes, true) + && array_filter($simpleTypes, function ($v) use ($lower) { + return $v === $lower || strpos($lower, $v . '[') === 0; + }))) + ) { + $error = 'Invalid type "%s" for parameter %s'; + $data = [ + $type, + $name, + ]; + $phpcsFile->addError($error, $tagPtr, 'ParamDocInvalidType', $data); + + $break = true; + continue; + } + } + } + + // If some parameter is invalid, we don't want to preform other checks + if ($break) { + return; + } + + // Check if PHPDocs param is required + if ($typeHint && ! $description) { + $tmpTypeHint = $typeHint; + if (isset($param['default']) + && strtolower($param['default']) === 'null' + && $tmpTypeHint[0] !== '?' + ) { + $tmpTypeHint = '?' . $tmpTypeHint; + } + + if ($this->typesMatch($tmpTypeHint, $typeStr)) { + $error = 'Param tag is redundant'; + $fix = $phpcsFile->addFixableError($error, $tagPtr, 'RedundantParamDoc'); + + if ($fix) { + $this->removeTag($phpcsFile, $tagPtr); + } + } + } + } + + private function processParamSpec(File $phpcsFile) : void + { + foreach ($this->params as $k => $param) { + if (in_array($k, $this->processedParams, true)) { + continue; + } + + $this->checkParam($phpcsFile, $param); + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Functions/ReturnTypeSniff.php b/src/ZendCodingStandard/Sniffs/Functions/ReturnTypeSniff.php new file mode 100644 index 00000000..6ef69c56 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Functions/ReturnTypeSniff.php @@ -0,0 +1,876 @@ +initScope($phpcsFile, $stackPtr); + + $this->returnDoc = null; + $this->returnDocTypes = []; + $this->returnDocValue = null; + $this->returnDocDescription = null; + $this->returnDocIsValid = true; + + $this->returnType = null; + $this->returnTypeValue = null; + $this->returnTypeIsValid = true; + + if ($commentStart = $this->getCommentStart($phpcsFile, $stackPtr)) { + $this->processReturnDoc($phpcsFile, $commentStart); + } + $this->processReturnType($phpcsFile, $stackPtr); + $this->processReturnStatements($phpcsFile, $stackPtr); + } + + /** + * @return bool|int + */ + private function getReturnType(File $phpcsFile, int $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]['scope_opener'])) { + $to = $tokens[$stackPtr]['scope_opener']; + } else { + $to = $phpcsFile->findEndOfStatement($stackPtr, [T_COLON]); + } + + return $phpcsFile->findNext(T_RETURN_TYPE, $stackPtr + 1, $to); + } + + private function processReturnDoc(File $phpcsFile, int $commentStart) : void + { + $tokens = $phpcsFile->getTokens(); + + $returnDoc = null; + foreach ($tokens[$commentStart]['comment_tags'] as $tag) { + if (strtolower($tokens[$tag]['content']) !== '@return') { + continue; + } + + if ($returnDoc !== null) { + $error = 'Only 1 @return tag is allowed in a function comment'; + $phpcsFile->addError($error, $tag, 'DuplicateReturn'); + + $this->returnDoc = $returnDoc; + $this->returnDocIsValid = false; + return; + } + + if ($this->isSpecialMethod) { + $error = sprintf('@return tag is not allowed for "%s" method', $this->methodName); + $phpcsFile->addError($error, $tag, 'SpecialMethodReturnTag'); + } + + $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag + 1); + if ($string !== $tag + 2 + || $tokens[$string]['line'] !== $tokens[$tag]['line'] + ) { + $this->returnDoc = $tag; + $this->returnDocIsValid = false; + return; + } + + $returnDoc = $tag; + } + + if (! $returnDoc || $this->isSpecialMethod) { + return; + } + + $this->returnDoc = $returnDoc; + + $split = preg_split('/\s/', $tokens[$returnDoc + 2]['content'], 2); + $this->returnDocValue = $split[0]; + $this->returnDocDescription = isset($split[1]) ? trim($split[1]) : null; + + if (strtolower($this->returnDocValue) === 'void') { + $this->returnDocIsValid = false; + return; + } + + if (! $this->isType('@return', $this->returnDocValue)) { + $this->returnDocIsValid = false; + return; + } + + // Return tag contains only null, null[], null[][], ... + $cleared = strtolower(strtr($this->returnDocValue, ['[' => '', ']' => ''])); + if ($cleared === 'null') { + $this->returnDocIsValid = false; + return; + } + + $this->returnDocTypes = explode('|', $this->returnDocValue); + } + + private function processReturnType(File $phpcsFile, int $stackPtr) : void + { + // Get return type from method signature + $returnType = $this->getReturnType($phpcsFile, $stackPtr); + if (! $returnType) { + return; + } + + $this->returnType = $returnType; + + if ($this->isSpecialMethod) { + $error = 'Method "%s" cannot declare return type'; + $data = [$this->methodName]; + $phpcsFile->addError($error, $stackPtr, 'SpecialMethodReturnType', $data); + + $this->returnTypeIsValid = false; + return; + } + + $colon = $phpcsFile->findPrevious(T_COLON, $returnType - 1, $stackPtr + 1); + $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $colon + 1, null, true); + + $this->returnTypeValue = preg_replace( + '/\s/', + '', + $phpcsFile->getTokensAsString($firstNonEmpty, $returnType - $firstNonEmpty + 1) + ); + $lowerReturnTypeValue = strtolower($this->returnTypeValue); + + $suggestedType = $this->getSuggestedType($this->returnTypeValue); + if ($suggestedType !== $this->returnTypeValue) { + $error = 'Invalid return type; expected %s, but found %s'; + $data = [ + $suggestedType, + $this->returnTypeValue, + ]; + $fix = $phpcsFile->addFixableError($error, $returnType, 'InvalidReturnType', $data); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $firstNonEmpty; $i < $returnType; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->replaceToken($returnType, $suggestedType); + $phpcsFile->fixer->endChangeset(); + } + + return; + } + + if (! $this->returnDoc || ! $this->returnDocIsValid) { + return; + } + + $hasNullInDoc = preg_grep('/^null$/i', $this->returnDocTypes); + + if (! $hasNullInDoc && $this->returnTypeValue[0] === '?') { + $error = 'Missing "null" as possible return type in PHPDocs.' + . ' Nullable type has been found in return type declaration.'; + $fix = $phpcsFile->addFixableError($error, $this->returnDoc + 2, 'MissingNull'); + + if ($fix) { + $content = trim('null|' . $this->returnDocValue . ' ' . $this->returnDocDescription); + $phpcsFile->fixer->replaceToken($this->returnDoc + 2, $content); + } + + return; + } + + if ($hasNullInDoc && $this->returnTypeValue[0] !== '?') { + $error = 'Null type has been found in PHPDocs for return type.' + . ' It is not declared with function return type.'; + $fix = $phpcsFile->addFixableError($error, $this->returnDoc + 2, 'AdditionalNull'); + + if ($fix) { + foreach ($this->returnDocTypes as $key => $type) { + if (strtolower($type) === 'null') { + unset($this->returnDocTypes[$key]); + break; + } + } + + $content = trim(implode('|', $this->returnDocTypes) . ' ' . $this->returnDocDescription); + $phpcsFile->fixer->replaceToken($this->returnDoc + 2, $content); + } + + return; + } + + $needSpecificationTypes = [ + 'array', + '?array', + 'iterable', + '?iterable', + 'traversable', + '?traversable', + '\traversable', + '?\traversable', + 'generator', + '?generator', + '\generator', + '?\generator', + ]; + + if (! in_array($lowerReturnTypeValue, $needSpecificationTypes, true)) { + if ($this->typesMatch($this->returnTypeValue, $this->returnDocValue)) { + // There is no description and values are the same so PHPDoc tag is redundant. + if (! $this->returnDocDescription) { + $error = 'Return tag is redundant'; + $fix = $phpcsFile->addFixableError($error, $this->returnDoc, 'RedundantReturnDoc'); + + if ($fix) { + $this->removeTag($phpcsFile, $this->returnDoc); + } + } + + return; + } + + if (in_array($lowerReturnTypeValue, ['parent', '?parent'], true)) { + if (! in_array(strtolower($this->returnDocValue), [ + 'parent', + 'null|parent', + 'parent|null', + 'self', + 'null|self', + 'self|null', + 'static', + 'null|static', + 'static|null', + '$this', + 'null|$this', + '$this|null', + ], true)) { + $error = 'Return type is "parent" so return tag must be one of:' + . ' "parent", "self", "static" or "$this"'; + $phpcsFile->addError($error, $this->returnDoc + 2, 'ReturnParent'); + } + + return; + } + + if (in_array($lowerReturnTypeValue, ['self', '?self'], true)) { + if (! in_array(strtolower($this->returnDocValue), [ + 'self', + 'null|self', + 'self|null', + 'static', + 'null|static', + 'static|null', + '$this', + 'null|$this', + '$this|null', + ], true)) { + $error = 'Return type is "self" so return tag must be one of: "self", "static" or "$this"'; + $phpcsFile->addError($error, $this->returnDoc + 2, 'ReturnSelf'); + } + + return; + } + + if (! in_array($lowerReturnTypeValue, $this->simpleReturnTypes, true)) { + foreach ($this->returnDocTypes as $type) { + $lower = strtolower($type); + if (array_filter($this->simpleReturnTypes, function ($v) use ($lower) { + return $v === $lower || strpos($lower, $v . '[') === 0; + })) { + $error = 'Unexpected type "%s" found in return tag'; + $data = [ + $type, + ]; + $phpcsFile->addError($error, $this->returnDoc + 2, 'ReturnComplexType', $data); + } + } + + return; + } + + $error = 'Return type in PHPDoc tag is different than declared type in method declaration: "%s" and "%s"'; + $data = [ + $this->returnDocValue, + $this->returnTypeValue, + ]; + $phpcsFile->addError($error, $this->returnDoc + 2, 'DifferentTagAndDeclaration', $data); + + return; + } + + $simpleTypes = array_merge($this->simpleReturnTypes, ['mixed']); + + switch ($lowerReturnTypeValue) { + case 'array': + case '?array': + foreach ($this->returnDocTypes as $type) { + if (in_array(strtolower($type), ['null', 'array'], true)) { + continue; + } + + if (strpos($type, '[]') === false) { + $error = 'Return type contains "%s" which is not an array type'; + $data = [ + $type, + ]; + $phpcsFile->addError($error, $this->returnDoc + 2, 'NotArrayType', $data); + } + } + break; + + case 'iterable': + case '?iterable': + foreach ($this->returnDocTypes as $type) { + $lower = strtolower($type); + if ($lower === 'iterable') { + continue; + } + + if (in_array($lower, $simpleTypes, true)) { + $error = 'Return type contains "%s" which is not an iterable type'; + $data = [ + $type, + ]; + $phpcsFile->addError($error, $this->returnDoc + 2, 'NotIterableType', $data); + } + } + break; + + case 'traversable': + case '?traversable': + case '\traversable': + case '?\traversable': + foreach ($this->returnDocTypes as $type) { + $lower = strtolower($type); + if (in_array($lower, ['null', 'traversable', '\traversable'], true)) { + continue; + } + + if (in_array($lower, $simpleTypes, true)) { + $error = 'Return type contains "%s" which is not a traversable type'; + $data = [ + $type, + ]; + $phpcsFile->addError($error, $this->returnDoc + 2, 'NotTraversableType', $data); + } + } + break; + + case 'generator': + case '?generator': + case '\generator': + case '?\generator': + foreach ($this->returnDocTypes as $type) { + $lower = strtolower($type); + if (in_array($lower, ['null', 'generator', '\generator'], true)) { + continue; + } + + if (in_array($lower, $simpleTypes, true)) { + $error = 'Return type contains "%s" which is not a generator type'; + $data = [ + $type, + ]; + $phpcsFile->addError($error, $this->returnDoc + 2, 'NotGeneratorType', $data); + } + } + break; + } + } + + private function processReturnStatements(File $phpcsFile, int $stackPtr) : void + { + $tokens = $phpcsFile->getTokens(); + + // Method does not have a body. + if (! isset($tokens[$stackPtr]['scope_opener'])) { + return; + } + + $returnValues = []; + + // Search all return/yield/yield from in the method. + for ($i = $tokens[$stackPtr]['scope_opener'] + 1; $i < $tokens[$stackPtr]['scope_closer']; ++$i) { + // Skip closures and anonymous classes. + if ($tokens[$i]['code'] === T_CLOSURE + || $tokens[$i]['code'] === T_ANON_CLASS + ) { + $i = $tokens[$i]['scope_closer']; + continue; + } + + if ($tokens[$i]['code'] !== T_RETURN + && $tokens[$i]['code'] !== T_YIELD + && $tokens[$i]['code'] !== T_YIELD_FROM + ) { + continue; + } + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $i + 1, null, true); + if ($tokens[$next]['code'] === T_SEMICOLON) { + $this->returnCodeVoid($phpcsFile, $i); + } else { + $this->returnCodeValue($phpcsFile, $i); + $returnValues[$next] = $this->getReturnValue($phpcsFile, $next); + + if ($this->returnDoc + && $this->returnDocIsValid + && in_array(strtolower($this->returnDocValue), ['$this', 'null|$this', '$this|null'], true) + ) { + $isThis = ! in_array($tokens[$next]['code'], [ + T_CLOSURE, + T_CONSTANT_ENCAPSED_STRING, + T_DIR, + T_DOUBLE_QUOTED_STRING, + T_FILE, + T_NEW, + T_NS_SEPARATOR, + T_OPEN_PARENTHESIS, + T_OBJECT_CAST, + T_SELF, + T_STATIC, + T_STRING_CAST, + T_PARENT, + ], true); + + if ($isThis + && $tokens[$next]['code'] === T_VARIABLE + && (strtolower($tokens[$next]['content']) !== '$this' + || (($next = $phpcsFile->findNext(Tokens::$emptyTokens, $next + 1, null, true)) + && $tokens[$next]['code'] !== T_SEMICOLON)) + ) { + $isThis = false; + } + + if (! $isThis) { + $error = 'Return type of "%s" function is "$this",' + . ' but function is returning not $this here'; + $data = [$this->methodName]; + $phpcsFile->addError($error, $i, 'InvalidReturnNotThis', $data); + } + } + } + } + + if (! $returnValues + && (($this->returnDoc && $this->returnDocIsValid) + || ($this->returnType && $this->returnTypeIsValid && strtolower($this->returnTypeValue) !== 'void')) + ) { + $error = 'Return type of "%s" function is not void, but function has no return statement'; + $data = [$this->methodName]; + $phpcsFile->addError( + $error, + $this->returnDoc && $this->returnDocIsValid ? $this->returnDoc : $this->returnType, + 'InvalidNoReturn', + $data + ); + } + + if (! $returnValues || ! $this->returnDoc || ! $this->returnDocIsValid) { + return; + } + + $uniq = array_unique($returnValues); + if (count($uniq) === 1) { + // We have to use current because index in the array is $ptr + switch (current($uniq)) { + case 'array': + if ($matches = array_udiff( + preg_grep('/[^\]]$/', $this->returnDocTypes), + ['null', 'array', 'iterable'], + function ($a, $b) { + return strcasecmp($a, $b); + } + )) { + $error = 'Function returns only array, but return type contains not array types: %s'; + $data = [ + implode(', ', $matches), + ]; + $phpcsFile->addError($error, $this->returnDoc, 'ReturnArrayOnly', $data); + } + break; + + case 'bool': + if (! in_array(strtolower($this->returnDocValue), ['bool', 'boolean'], true)) { + $error = 'Functions returns only boolean value, but return type is not only bool'; + $phpcsFile->addError($error, $this->returnDoc, 'ReturnBoolOnly'); + } + break; + + case 'false': + if (strtolower($this->returnDocValue) !== 'false') { + $error = 'Function returns only boolean false, but return type is not only false'; + $phpcsFile->addError($error, $this->returnDoc, 'ReturnFalseOnly'); + } + break; + + case 'true': + if (strtolower($this->returnDocValue) !== 'true') { + $error = 'Function returns only boolean true, but return type is not only true'; + $phpcsFile->addError($error, $this->returnDoc, 'ReturnTrueOnly'); + } + break; + + case 'new': + $instances = []; + foreach ($returnValues as $ptr => $new) { + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $ptr + 1, null, true); + if ($tokens[$next]['code'] !== T_STRING + && $tokens[$next]['code'] !== T_NS_SEPARATOR + ) { + // It is unknown instance, break switch. + break 2; + } + + $after = $phpcsFile->findNext( + Tokens::$emptyTokens + + [T_NS_SEPARATOR => T_NS_SEPARATOR, T_STRING => T_STRING], + $next + 1, + null, + true + ); + + $last = $phpcsFile->findPrevious(T_STRING, $after - 1, $next); + $content = $this->getSuggestedType( + $phpcsFile->getTokensAsString($next, $last - $next + 1) + ); + + $instances[strtolower($content)] = $content; + } + + // If function returns instances of different types, break. + if (count($instances) !== 1) { + break; + } + + $className = current($instances); + if ($this->returnDocValue !== $className) { + $error = 'Function returns only new instance of %s, but return type is not only %s'; + $data = [ + $className, + $className, + ]; + $phpcsFile->addError($error, $this->returnDoc + 2, 'ReturnNewInstanceOnly', $data); + } + break; + + case '$this': + if (($isClassName = $this->isClassName($this->returnDocValue)) + || strtolower($this->returnDocValue) === 'self' + ) { + $error = 'Function returns only $this so return type should be $this instead of ' + . ($isClassName ? 'class name' : 'self'); + $fix = $phpcsFile->addFixableError($error, $this->returnDoc + 2, 'ReturnThisOnly'); + + if ($fix) { + $content = trim('$this ' . $this->returnDocDescription); + $phpcsFile->fixer->replaceToken($this->returnDoc + 2, $content); + } + } + break; + } + } + } + + private function getReturnValue(File $phpcsFile, int $ptr) : string + { + $tokens = $phpcsFile->getTokens(); + + switch ($tokens[$ptr]['code']) { + case T_ARRAY: + case T_ARRAY_CAST: + case T_OPEN_SHORT_ARRAY: + if (! $this->hasCorrectType(['array', '?array', 'iterable', '?iterable'], []) + || ($this->returnDoc + && $this->returnDocIsValid + && strpos($this->returnDocValue, '[]') === false + && ! array_intersect( + explode('|', strtolower($this->returnDocValue)), + ['array', 'iterable'] + )) + ) { + $error = 'Function return type is array nor iterable, but function returns array here'; + $phpcsFile->addError($error, $ptr, 'ReturnArray'); + } + return 'array'; + + case T_BOOL_CAST: + case T_BOOLEAN_NOT: + $end = $ptr; + while (++$end) { + if ($tokens[$end]['code'] === T_OPEN_PARENTHESIS) { + $end = $tokens[$end]['parenthesis_closer']; + continue; + } + if ($tokens[$end]['code'] === T_OPEN_SQUARE_BRACKET + || $tokens[$end]['code'] === T_OPEN_CURLY_BRACKET + || $tokens[$end]['code'] === T_OPEN_SHORT_ARRAY + ) { + $end = $tokens[$end]['bracket_closer']; + continue; + } + + if (in_array($tokens[$end]['code'], [T_SEMICOLON, T_INLINE_THEN], true)) { + break; + } + } + if ($tokens[$end]['code'] !== T_SEMICOLON) { + return 'unknown'; + } + + if (! $this->hasCorrectType(['bool', '?bool'], ['bool', 'boolean'])) { + $error = 'Function return type is not bool, but function returns boolean value here'; + $phpcsFile->addError($error, $ptr, 'ReturnFloat'); + } + return 'bool'; + + case T_FALSE: + if (! $this->hasCorrectType(['bool', '?bool'], ['bool', 'boolean', 'false'])) { + $error = 'Function return type is not bool, but function returns boolean false here'; + $phpcsFile->addError($error, $ptr, 'ReturnFalse'); + } + return 'false'; + + case T_TRUE: + if (! $this->hasCorrectType(['bool', '?bool'], ['bool', 'boolean', 'true'])) { + $error = 'Function return type is not bool, but function returns boolean true here'; + $phpcsFile->addError($error, $ptr, 'ReturnTrue'); + } + return 'true'; + + // integer value or integer cast + case T_LNUMBER: + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $ptr + 1, null, true); + if ($tokens[$next]['code'] !== T_SEMICOLON) { + return 'unknown'; + } + // no break + case T_INT_CAST: + if (! $this->hasCorrectType(['int', '?int'], ['int', 'integer'])) { + $error = 'Function return type is not int, but function return int here'; + $phpcsFile->addError($error, $ptr, 'ReturnInt'); + } + return 'int'; + + // float value or float cast + case T_DNUMBER: + case T_DOUBLE_CAST: + if (! $this->hasCorrectType(['float', '?float'], ['double', 'float', 'real'])) { + $error = 'Function return type is not float, but function returns float here'; + $phpcsFile->addError($error, $ptr, 'ReturnFloat'); + } + return 'float'; + + case T_NEW: + return 'new'; + + case T_NULL: + if (! $this->hasCorrectType([], ['null']) + || ($this->returnType + && $this->returnTypeIsValid + && strpos($this->returnTypeValue, '?') !== 0) + ) { + $error = 'Function return type is not nullable, but function returns null here'; + $phpcsFile->addError($error, $ptr, 'ReturnNull'); + } + return 'null'; + + case T_VARIABLE: + if (strtolower($tokens[$ptr]['content']) !== '$this') { + return 'variable'; + } + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $ptr + 1, null, true); + if ($tokens[$next]['code'] !== T_SEMICOLON) { + // This is not "$this" return but something else. + return 'unknown'; + } + return '$this'; + } + + return 'unknown'; + } + + /** + * @param string[] $expectedType + * @param string[] $expectedDoc + */ + private function hasCorrectType(array $expectedType, array $expectedDoc) : bool + { + if ($expectedType + && $this->returnType + && $this->returnTypeIsValid + && ! in_array(strtolower($this->returnTypeValue), $expectedType, true) + ) { + return false; + } + + if ($expectedDoc + && $this->returnDoc + && $this->returnDocIsValid + && ! array_filter($this->returnDocTypes, function ($v) use ($expectedDoc) { + return in_array(strtolower($v), $expectedDoc, true); + }) + ) { + return false; + } + + return true; + } + + private function returnCodeVoid(File $phpcsFile, int $ptr) : void + { + if (($this->returnDoc && $this->returnDocIsValid) + || ($this->returnType && $this->returnTypeIsValid && strtolower($this->returnTypeValue) !== 'void') + ) { + $error = 'Return type of "%s" function is not void, but function is returning void here'; + $data = [$this->methodName]; + $phpcsFile->addError($error, $ptr, 'InvalidReturnNotVoid', $data); + } + } + + private function returnCodeValue(File $phpcsFile, int $ptr) : void + { + // Special method cannot return any values. + if ($this->isSpecialMethod) { + $error = 'Method "%s" cannot return any value, but returns it here'; + $data = [$this->methodName]; + $phpcsFile->addError($error, $ptr, 'SpecialMethodReturnValue', $data); + + return; + } + + // Function is void but return a value. + if ((! $this->returnType + || ! $this->returnTypeIsValid + || $this->returnTypeValue === 'void') + && (! $this->returnDoc + || ! $this->returnDocIsValid + || $this->returnDocValue === 'void') + ) { + $error = 'Function "%s" returns value but it is not specified.' + . ' Please add return tag or declare return type.'; + $data = [ + $this->methodName, + ]; + $phpcsFile->addError($error, $ptr, 'ReturnValue', $data); + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Functions/ThrowsSniff.php b/src/ZendCodingStandard/Sniffs/Functions/ThrowsSniff.php new file mode 100644 index 00000000..78a04d28 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Functions/ThrowsSniff.php @@ -0,0 +1,380 @@ +initScope($phpcsFile, $stackPtr); + + $this->throwTags = []; + + if ($commentStart = $this->getCommentStart($phpcsFile, $stackPtr)) { + $this->processThrowsDoc($phpcsFile, $commentStart); + } + $this->processThrowStatements($phpcsFile, $stackPtr); + } + + private function processThrowsDoc(File $phpcsFile, int $commentStart) : void + { + $tokens = $phpcsFile->getTokens(); + + foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { + if (strtolower($tokens[$tag]['content']) !== '@throws') { + continue; + } + + $exception = null; + if ($tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING) { + $split = preg_split('/\s/', $tokens[$tag + 2]['content'], 2); + $exception = $split[0]; + $description = isset($split[1]) ? trim($split[1]) : null; + $suggested = $this->getSuggestedType($exception); + + if ($exception !== $suggested) { + $error = 'Invalid exception type; expected %s, but found %s'; + $data = [ + $suggested, + $exception, + ]; + $fix = $phpcsFile->addFixableError($error, $tag + 2, 'InvalidType', $data); + + if ($fix) { + $content = trim($suggested . ' ' . $description); + $phpcsFile->fixer->replaceToken($tag + 2, $content); + } + } + + $this->throwTags[$tag] = $suggested; + } + + if (! $exception) { + $error = 'Exception type missing for @throws tag in function comment'; + $phpcsFile->addError($error, $tag, 'MissingType'); + } + } + } + + protected function processThrowStatements(File $phpcsFile, int $stackPtr) : void + { + $tokens = $phpcsFile->getTokens(); + + // Skip function without body + if (! isset($tokens[$stackPtr]['scope_opener'])) { + return; + } + + $scopeBegin = $tokens[$stackPtr]['scope_opener']; + $scopeEnd = $tokens[$stackPtr]['scope_closer']; + + $thrownExceptions = []; + $thrownVariables = 0; + $foundThrows = false; + + $throw = $scopeBegin; + while (true) { + $throw = $phpcsFile->findNext(T_THROW, $throw + 1, $scopeEnd); + + // Throw statement not found. + if (! $throw) { + break; + } + + // The throw statement is in another scope. + if (! $this->isLastScope($phpcsFile, $tokens[$throw]['conditions'], $stackPtr)) { + continue; + } + + $foundThrows = true; + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $throw + 1, null, true); + if ($tokens[$next]['code'] === T_NEW) { + $currException = $phpcsFile->findNext(Tokens::$emptyTokens, $next + 1, null, true); + + if (in_array($tokens[$currException]['code'], $this->nameTokens, true)) { + $end = $phpcsFile->findNext($this->nameTokens, $currException + 1, null, true); + + $class = $phpcsFile->getTokensAsString($currException, $end - $currException); + $suggested = $this->getSuggestedType($class); + + if ($class !== $suggested) { + $error = 'Invalid exception class name; expected %s, but found %s'; + $data = [ + $suggested, + $class, + ]; + $fix = $phpcsFile->addFixableError($error, $currException, 'InvalidExceptionClassName', $data); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($currException, $suggested); + for ($i = $currException + 1; $i < $end; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->endChangeset(); + } + } + + $thrownExceptions[] = $suggested; + continue; + } + } elseif ($tokens[$next]['code'] === T_VARIABLE) { + $catch = $phpcsFile->findPrevious(T_CATCH, $throw, $scopeBegin); + + if ($catch) { + $thrownVar = $phpcsFile->findPrevious( + T_VARIABLE, + $tokens[$catch]['parenthesis_closer'] - 1, + $tokens[$catch]['parenthesis_opener'] + ); + + if ($tokens[$thrownVar]['content'] === $tokens[$next]['content']) { + $exceptions = $this->getExceptions( + $phpcsFile, + $tokens[$catch]['parenthesis_opener'] + 1, + $thrownVar - 1 + ); + + foreach ($exceptions as $exception) { + $thrownExceptions[] = $exception; + } + } + + continue; + } + } + + ++$thrownVariables; + } + + if (! $foundThrows) { + // It should be disabled if we want to declare implicit throws + foreach ($this->throwTags as $ptr => $class) { + $error = 'Function does not throw any exception but has @throws tag'; + $phpcsFile->addError($error, $ptr, 'AdditionalThrowTag'); + } + + return; + } + + // Only need one @throws tag for each type of exception thrown. + $thrownExceptions = array_unique($thrownExceptions); + + // Make sure @throws tag count matches thrown count. + $thrownCount = count($thrownExceptions) ?: 1; + $tagCount = count(array_unique($this->throwTags)); + + if ($thrownVariables > 0) { + if ($thrownCount > $tagCount) { + $error = 'Expected at least %d @throws tag(s) in function comment; %d found'; + $data = [ + $thrownCount, + $tagCount, + ]; + $phpcsFile->addError($error, $stackPtr, 'WrongNumberAtLeast', $data); + return; + } + } else { + if ($thrownCount !== $tagCount) { + $error = 'Expected %d @throws tag(s) in function comment; %d found'; + $data = [ + $thrownCount, + $tagCount, + ]; + $phpcsFile->addError($error, $stackPtr, 'WrongNumberExact', $data); + return; + } + } + + foreach ($thrownExceptions as $throw) { + if (! in_array($throw, $this->throwTags, true)) { + $error = 'Missing @throws tag for "%s" exception'; + $data = [$throw]; + $phpcsFile->addError($error, $stackPtr, 'Missing', $data); + } + } + } + + /** + * @return string[] + */ + private function getExceptions(File $phpcsFile, int $from, int $to) : array + { + $tokens = $phpcsFile->getTokens(); + + $exceptions = []; + $currName = ''; + $start = null; + $end = null; + + for ($i = $from; $i <= $to; ++$i) { + if (in_array($tokens[$i]['code'], $this->nameTokens, true)) { + if ($currName === '') { + $start = $i; + } + + $end = $i; + $currName .= $tokens[$i]['content']; + } + + if ($tokens[$i]['code'] === T_BITWISE_OR || $i === $to) { + $suggested = $this->getSuggestedType($currName); + + if ($suggested !== $currName) { + $error = 'Invalid exception class name in catch; expected %s, but found %s'; + $data = [ + $suggested, + $currName, + ]; + $fix = $phpcsFile->addFixableError($error, $start, 'InvalidCatchClassName', $data); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($start, $suggested); + for ($j = $start + 1; $j <= $end; ++$j) { + $phpcsFile->fixer->replaceToken($j, ''); + } + $phpcsFile->fixer->endChangeset(); + } + } + + $exceptions[] = $suggested; + $currName = ''; + $start = null; + $end = null; + } + } + + return $exceptions; + } + + /** + * Check if $scope is the last closure/function/try condition. + * + * @param string[] $conditions + * @param int $scope Scope to check in conditions. + */ + private function isLastScope(File $phpcsFile, array $conditions, int $scope) : bool + { + $tokens = $phpcsFile->getTokens(); + + foreach (array_reverse($conditions, true) as $ptr => $code) { + if ($code !== T_FUNCTION && $code !== T_CLOSURE && $code !== T_TRY) { + continue; + } + + if ($code === T_CLOSURE && $ptr !== $scope) { + // Check if closure is called. + $afterClosure = $phpcsFile->findNext( + Tokens::$emptyTokens, + $tokens[$ptr]['scope_closer'] + 1, + null, + true + ); + if ($afterClosure && $tokens[$afterClosure]['code'] === T_CLOSE_PARENTHESIS) { + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $afterClosure + 1, null, true); + if ($next && $tokens[$next]['code'] === T_OPEN_PARENTHESIS) { + return true; + } + } + + // Check if closure is passed to function/class. + if (($token = $this->findPrevious($phpcsFile, $ptr)) + && in_array($tokens[$token]['code'], [T_STRING, T_VARIABLE], true) + ) { + return true; + } + } + + return $ptr === $scope; + } + + return false; + } + + private function findPrevious(File $phpcsFile, int $ptr) : ?int + { + $tokens = $phpcsFile->getTokens(); + + while (--$ptr) { + if ($tokens[$ptr]['code'] === T_CLOSE_PARENTHESIS) { + $ptr = $tokens[$ptr]['parenthesis_opener']; + } elseif ($tokens[$ptr]['code'] === T_CLOSE_CURLY_BRACKET + || $tokens[$ptr]['code'] === T_CLOSE_SHORT_ARRAY + || $tokens[$ptr]['code'] === T_CLOSE_SQUARE_BRACKET + ) { + $ptr = $tokens[$ptr]['bracket_opener']; + } elseif ($tokens[$ptr]['code'] === T_OPEN_PARENTHESIS) { + return $phpcsFile->findPrevious(Tokens::$emptyTokens, $ptr - 1, null, true); + } elseif (in_array( + $tokens[$ptr]['code'], + [T_SEMICOLON, T_OPEN_CURLY_BRACKET, T_OPEN_SHORT_ARRAY, T_OPEN_SQUARE_BRACKET], + true + )) { + break; + } + } + + return null; + } +} diff --git a/src/ZendCodingStandard/Sniffs/Methods/LineAfterSniff.php b/src/ZendCodingStandard/Sniffs/Methods/LineAfterSniff.php new file mode 100644 index 00000000..787faa06 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Methods/LineAfterSniff.php @@ -0,0 +1,73 @@ +getTokens(); + + // Methods with body. + if (isset($tokens[$stackPtr]['scope_closer'])) { + $closer = $tokens[$stackPtr]['scope_closer']; + } else { + $closer = $phpcsFile->findNext(T_SEMICOLON, $tokens[$stackPtr]['parenthesis_closer'] + 1); + } + + $contentAfter = $phpcsFile->findNext(T_WHITESPACE, $closer + 1, null, true); + if ($contentAfter !== false + && $tokens[$contentAfter]['line'] - $tokens[$closer]['line'] !== 2 + && $tokens[$contentAfter]['code'] !== T_CLOSE_CURLY_BRACKET + ) { + $error = 'Expected 1 blank line after method; %d found'; + $found = max($tokens[$contentAfter]['line'] - $tokens[$closer]['line'] - 1, 0); + $data = [$found]; + $fix = $phpcsFile->addFixableError($error, $closer, 'BlankLinesAfter', $data); + + if ($fix) { + if ($found) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $closer + 1; $i < $contentAfter - 1; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->endChangeset(); + } else { + $phpcsFile->fixer->addNewline($closer); + } + } + } + } + + /** + * @param int $stackPtr + */ + protected function processTokenOutsideScope(File $phpcsFile, $stackPtr) : void + { + } +} diff --git a/src/ZendCodingStandard/Sniffs/Namespaces/AlphabeticallySortedUsesSniff.php b/src/ZendCodingStandard/Sniffs/Namespaces/AlphabeticallySortedUsesSniff.php new file mode 100644 index 00000000..c9c59997 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Namespaces/AlphabeticallySortedUsesSniff.php @@ -0,0 +1,251 @@ +getUseStatements($phpcsFile, $stackPtr); + $tokens = $phpcsFile->getTokens(); + + $lastUse = null; + foreach ($uses as $use) { + if (! $lastUse) { + $lastUse = $use; + continue; + } + + $order = $this->compareUseStatements($use, $lastUse); + + if ($order < 0) { + $error = 'Use statements are incorrectly ordered. The first wrong one is %s'; + $data = [$use['name']]; + + $fix = $phpcsFile->addFixableError($error, $use['ptrUse'], 'IncorrectOrder', $data); + + if ($fix) { + $this->fixAlphabeticalOrder($phpcsFile, $uses); + } + + return; + } + + // Check empty lines between use statements. + // There must be exactly one empty line between use statements of different type + // and no empty lines between use statements of the same type. + $lineDiff = $tokens[$use['ptrUse']]['line'] - $tokens[$lastUse['ptrUse']]['line']; + if ($lastUse['type'] === $use['type']) { + if ($lineDiff > 1) { + $error = 'There must not be any empty line between use statement of the same type'; + $fix = $phpcsFile->addFixableError($error, $use['ptrUse'], 'EmptyLine'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $lastUse['ptrEnd'] + 1; $i < $use['ptrUse']; ++$i) { + if (strpos($tokens[$i]['content'], $phpcsFile->eolChar) !== false) { + $phpcsFile->fixer->replaceToken($i, ''); + --$lineDiff; + + if ($lineDiff === 1) { + break; + } + } + } + $phpcsFile->fixer->endChangeset(); + } + } elseif ($lineDiff === 0) { + $error = 'Each use statement must be in new line'; + $fix = $phpcsFile->addFixableError($error, $use['ptrUse'], 'TheSameLine'); + + if ($fix) { + $phpcsFile->fixer->addNewline($lastUse['ptrEnd']); + } + } + } else { + if ($lineDiff > 2) { + $error = 'There must be exactly one empty line between use statements of different type'; + $fix = $phpcsFile->addFixableError($error, $use['ptrUse'], 'TooManyEmptyLines'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $lastUse['ptrEnd'] + 1; $i < $use['ptrUse']; ++$i) { + if (strpos($tokens[$i]['content'], $phpcsFile->eolChar) !== false) { + $phpcsFile->fixer->replaceToken($i, ''); + --$lineDiff; + + if ($lineDiff === 2) { + break; + } + } + } + $phpcsFile->fixer->endChangeset(); + } + } elseif ($lineDiff <= 1) { + $error = 'There must be exactly one empty line between use statements of different type'; + $fix = $phpcsFile->addFixableError($error, $use['ptrUse'], 'MissingEmptyLine'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $lineDiff; $i < 2; ++$i) { + $phpcsFile->fixer->addNewline($lastUse['ptrEnd']); + } + $phpcsFile->fixer->endChangeset(); + } + } + } + + $lastUse = $use; + } + } + + /** + * @return string[][] + */ + private function getUseStatements(File $phpcsFile, int $scopePtr) : array + { + $tokens = $phpcsFile->getTokens(); + + $uses = []; + + if (isset($tokens[$scopePtr]['scope_opener'])) { + $start = $tokens[$scopePtr]['scope_opener']; + $end = $tokens[$scopePtr]['scope_closer']; + } else { + $start = $scopePtr; + $end = null; + } + while ($use = $phpcsFile->findNext(T_USE, $start + 1, $end)) { + if (! CodingStandard::isGlobalUse($phpcsFile, $use) + || ($end !== null + && (! isset($tokens[$use]['conditions'][$scopePtr]) + || $tokens[$use]['level'] !== $tokens[$scopePtr]['level'] + 1)) + ) { + $start = $use; + continue; + } + + // find semicolon as the end of the global use scope + $endOfScope = $phpcsFile->findNext([T_SEMICOLON], $use + 1); + + $startOfName = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], $use + 1, $endOfScope); + + $type = 'class'; + if ($tokens[$startOfName]['code'] === T_STRING) { + $lowerContent = strtolower($tokens[$startOfName]['content']); + if ($lowerContent === 'function' + || $lowerContent === 'const' + ) { + $type = $lowerContent; + + $startOfName = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], $startOfName + 1, $endOfScope); + } + } + + $uses[] = [ + 'ptrUse' => $use, + 'name' => trim($phpcsFile->getTokensAsString($startOfName, $endOfScope - $startOfName)), + 'ptrEnd' => $endOfScope, + 'string' => trim($phpcsFile->getTokensAsString($use, $endOfScope - $use + 1)), + 'type' => $type, + ]; + + $start = $endOfScope; + } + + return $uses; + } + + /** + * @param string[] $a + * @param string[] $b + */ + private function compareUseStatements(array $a, array $b) : int + { + if ($a['type'] === $b['type']) { + return strcasecmp( + $this->clearName($a['name']), + $this->clearName($b['name']) + ); + } + + if ($a['type'] === 'class' + || ($a['type'] === 'function' && $b['type'] === 'const') + ) { + return -1; + } + + return 1; + } + + private function clearName(string $name) : string + { + return str_replace('\\', ':', $name); + } + + /** + * @param string[][] $uses + */ + private function fixAlphabeticalOrder(File $phpcsFile, array $uses) : void + { + $first = reset($uses); + $last = end($uses); + $lastScopeCloser = $last['ptrEnd']; + + $phpcsFile->fixer->beginChangeset(); + for ($i = $first['ptrUse']; $i <= $lastScopeCloser; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + uasort($uses, function (array $a, array $b) { + return $this->compareUseStatements($a, $b); + }); + + $lastType = reset($uses)['type']; + $content = []; + foreach ($uses as $use) { + if ($lastType !== $use['type']) { + $content[] = $phpcsFile->eolChar . $use['string']; + $lastType = $use['type']; + } else { + $content[] = $use['string']; + } + } + + $phpcsFile->fixer->addContent($first['ptrUse'], implode($phpcsFile->eolChar, $content)); + $phpcsFile->fixer->endChangeset(); + } +} diff --git a/src/ZendCodingStandard/Sniffs/Namespaces/ConstAndFunctionKeywordsSniff.php b/src/ZendCodingStandard/Sniffs/Namespaces/ConstAndFunctionKeywordsSniff.php new file mode 100644 index 00000000..c02bd433 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Namespaces/ConstAndFunctionKeywordsSniff.php @@ -0,0 +1,75 @@ +getTokens(); + $classPtr = $phpcsFile->findNext( + Tokens::$emptyTokens, + $stackPtr + 1, + null, + true + ); + + $lowerContent = strtolower($tokens[$classPtr]['content']); + if ($lowerContent === 'function' || $lowerContent === 'const') { + if ($lowerContent !== $tokens[$classPtr]['content']) { + $error = 'PHP keywords must be lowercase; expected "%s" but found "%s"'; + $data = [$lowerContent, $tokens[$classPtr]['content']]; + $fix = $phpcsFile->addFixableError($error, $classPtr, 'NotLowerCase', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($classPtr, $lowerContent); + } + } + + if ($tokens[$classPtr + 1]['code'] !== T_WHITESPACE) { + $error = 'There must be single space after %s keyword'; + $data = [$lowerContent]; + $fix = $phpcsFile->addFixableError($error, $classPtr, 'NoSpace', $data); + + if ($fix) { + $phpcsFile->fixer->addContent($classPtr, ' '); + } + } elseif ($tokens[$classPtr + 1]['content'] !== ' ') { + $error = 'There must be single space after %s keyword'; + $data = [$lowerContent]; + $fix = $phpcsFile->addFixableError($error, $classPtr + 1, 'NoSpace', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($classPtr + 1, ' '); + } + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Namespaces/UnusedUseStatementSniff.php b/src/ZendCodingStandard/Sniffs/Namespaces/UnusedUseStatementSniff.php new file mode 100644 index 00000000..7e35581c --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Namespaces/UnusedUseStatementSniff.php @@ -0,0 +1,215 @@ +getTokens(); + + // Only check use statements in the global scope. + if (! CodingStandard::isGlobalUse($phpcsFile, $stackPtr)) { + return; + } + + // Seek to the end of the statement and get the string before the semi colon. + // It works only with one USE keyword per declaration. + $semiColon = $phpcsFile->findEndOfStatement($stackPtr); + if ($tokens[$semiColon]['code'] !== T_SEMICOLON) { + return; + } + + $classPtr = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + $semiColon - 1, + null, + true + ); + + // Search where the class name is used. PHP treats class names case + // insensitive, that's why we cannot search for the exact class name string + // and need to iterate over all T_STRING tokens in the file. + $classUsed = $phpcsFile->findNext($this->checkInTokens, $classPtr + 1); + $className = $tokens[$classPtr]['content']; + $lowerClassName = strtolower($className); + + // Check if the referenced class is in the same namespace as the current + // file. If it is then the use statement is not necessary. + $namespacePtr = $phpcsFile->findPrevious(T_NAMESPACE, $stackPtr); + + // Check if the use statement does aliasing with the "as" keyword. Aliasing + // is allowed even in the same namespace. + $aliasUsed = $phpcsFile->findPrevious(T_AS, $classPtr - 1, $stackPtr); + + if ($namespacePtr !== false && $aliasUsed === false) { + $nsEnd = $phpcsFile->findNext( + [ + T_NS_SEPARATOR, + T_STRING, + T_DOC_COMMENT_STRING, + T_WHITESPACE, + ], + $namespacePtr + 1, + null, + true + ); + $namespace = trim($phpcsFile->getTokensAsString($namespacePtr + 1, $nsEnd - $namespacePtr - 1)); + + $useNamespacePtr = $phpcsFile->findNext(T_STRING, $stackPtr + 1); + $useNamespaceEnd = $phpcsFile->findNext( + [ + T_NS_SEPARATOR, + T_STRING, + ], + $useNamespacePtr + 1, + null, + true + ); + $useNamespace = rtrim( + $phpcsFile->getTokensAsString( + $useNamespacePtr, + $useNamespaceEnd - $useNamespacePtr - 1 + ), + '\\' + ); + + if (strcasecmp($namespace, $useNamespace) === 0) { + $classUsed = false; + } + } + + $emptyTokens = Tokens::$emptyTokens; + unset($emptyTokens[T_DOC_COMMENT_TAG]); + + while ($classUsed !== false) { + if ((in_array($tokens[$classUsed]['code'], [T_STRING, T_RETURN_TYPE], true) + && strtolower($tokens[$classUsed]['content']) === $lowerClassName) + || ($tokens[$classUsed]['code'] === T_DOC_COMMENT_STRING + && preg_match( + '/(\s|\||^)' . preg_quote($lowerClassName) . '(\s|\||\\\\|$|\[\])/i', + $tokens[$classUsed]['content'] + )) + || ($tokens[$classUsed]['code'] === T_DOC_COMMENT_TAG + && preg_match( + '/@' . preg_quote($lowerClassName) . '(\(|\\\\|$)/i', + $tokens[$classUsed]['content'] + )) + ) { + $beforeUsage = $phpcsFile->findPrevious( + $emptyTokens, + $classUsed - 1, + null, + true + ); + + if (in_array($tokens[$classUsed]['code'], [T_STRING, T_RETURN_TYPE], true)) { + // If a backslash is used before the class name then this is some other + // use statement. + if ($tokens[$beforeUsage]['code'] !== T_USE + && $tokens[$beforeUsage]['code'] !== T_NS_SEPARATOR + && $tokens[$beforeUsage]['code'] !== T_OBJECT_OPERATOR + ) { + return; + } + + // Trait use statement within a class. + if ($tokens[$beforeUsage]['code'] === T_USE + && ! empty($tokens[$beforeUsage]['conditions']) + ) { + return; + } + } elseif ($tokens[$beforeUsage]['code'] === T_DOC_COMMENT_TAG + && in_array( + $tokens[$beforeUsage]['content'], + ['@var', '@param', '@return', '@throws', '@method'], + true + ) + ) { + return; + } else { + return; + } + } + + $classUsed = $phpcsFile->findNext($this->checkInTokens, $classUsed + 1); + } + + $warning = 'Unused use statement "%s"'; + $data = [$className]; + $fix = $phpcsFile->addFixableWarning($warning, $stackPtr, 'UnusedUse', $data); + + if ($fix) { + // Remove the whole use statement line. + $phpcsFile->fixer->beginChangeset(); + for ($i = $stackPtr; $i <= $semiColon; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + // Also remove whitespace after the semicolon (new lines). + if (isset($tokens[$i]) && $tokens[$i]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->endChangeset(); + + $phpcsFile->recordMetric($stackPtr, __CLASS__, $stackPtr); + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Namespaces/UseDoesNotStartWithBackslashSniff.php b/src/ZendCodingStandard/Sniffs/Namespaces/UseDoesNotStartWithBackslashSniff.php new file mode 100644 index 00000000..c82de103 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Namespaces/UseDoesNotStartWithBackslashSniff.php @@ -0,0 +1,72 @@ +getTokens(); + $classPtr = $phpcsFile->findNext( + Tokens::$emptyTokens, + $stackPtr + 1, + null, + true + ); + + $lowerContent = strtolower($tokens[$classPtr]['content']); + if ($lowerContent === 'function' || $lowerContent === 'const') { + $classPtr = $phpcsFile->findNext( + Tokens::$emptyTokens, + $classPtr + 1, + null, + true + ); + } + + if ($tokens[$classPtr]['code'] === T_NS_SEPARATOR + || ($tokens[$classPtr]['code'] === T_STRING + && $tokens[$classPtr]['content'] === '\\') + ) { + $error = 'Use statement cannot start with a backslash'; + $fix = $phpcsFile->addFixableError($error, $classPtr, 'BackslashAtStart'); + + if ($fix) { + if ($tokens[$classPtr - 1]['code'] !== T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($classPtr, ' '); + } else { + $phpcsFile->fixer->replaceToken($classPtr, ''); + } + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/NamingConventions/ValidVariableNameSniff.php b/src/ZendCodingStandard/Sniffs/NamingConventions/ValidVariableNameSniff.php new file mode 100644 index 00000000..395bac6d --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/NamingConventions/ValidVariableNameSniff.php @@ -0,0 +1,73 @@ + true, + '_GET' => true, + '_POST' => true, + '_REQUEST' => true, + '_SESSION' => true, + '_ENV' => true, + '_COOKIE' => true, + '_FILES' => true, + 'GLOBALS' => true, + ]; + + /** + * @param int $stackPtr + */ + protected function processVariable(File $phpcsFile, $stackPtr) : void + { + $tokens = $phpcsFile->getTokens(); + $varName = ltrim($tokens[$stackPtr]['content'], '$'); + + // If it's a php reserved var, then its ok. + if (isset($this->phpReservedVars[$varName])) { + return; + } + + $objOperator = $phpcsFile->findPrevious([T_WHITESPACE], $stackPtr - 1, null, true); + if ($tokens[$objOperator]['code'] === T_DOUBLE_COLON) { + return; // skip MyClass::$variable, there might be no control over the declaration + } + + if (! Common::isCamelCaps($varName, false, true, false)) { + $error = 'Variable "%s" is not in valid camel caps format'; + $data = [$varName]; + $phpcsFile->addError($error, $stackPtr, 'NotCamelCaps', $data); + } + } + + /** + * @param int $stackPtr + */ + protected function processMemberVar(File $phpcsFile, $stackPtr) : void + { + // handled by PSR2.Classes.PropertyDeclaration + } + + /** + * @param int $stackPtr + */ + protected function processVariableInString(File $phpcsFile, $stackPtr) : void + { + // handled by Squiz.Strings.DoubleQuoteUsage + } +} diff --git a/src/ZendCodingStandard/Sniffs/Operators/BooleanOperatorSniff.php b/src/ZendCodingStandard/Sniffs/Operators/BooleanOperatorSniff.php new file mode 100644 index 00000000..dca7a119 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Operators/BooleanOperatorSniff.php @@ -0,0 +1,63 @@ +getTokens(); + + $prev = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + $stackPtr - 1, + null, + true + ); + $next = $phpcsFile->findNext( + Tokens::$emptyTokens, + $stackPtr + 1, + null, + true + ); + + if ($tokens[$prev]['line'] === $tokens[$stackPtr]['line'] + && $tokens[$next]['line'] !== $tokens[$stackPtr]['line'] + ) { + $error = 'Logical operator cannot be at the end of the line.'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'OperatorAtTheEnd'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($stackPtr, ''); + for ($i = $stackPtr - 1; $i > $prev; $i--) { + if ($tokens[$i]['code'] !== T_WHITESPACE) { + break; + } + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->addContentBefore($next, $tokens[$stackPtr]['content'] . ' '); + $phpcsFile->fixer->endChangeset(); + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Operators/TernaryOperatorSniff.php b/src/ZendCodingStandard/Sniffs/Operators/TernaryOperatorSniff.php new file mode 100644 index 00000000..8ad5d3b7 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Operators/TernaryOperatorSniff.php @@ -0,0 +1,136 @@ +getTokens(); + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + if ($tokens[$next]['line'] > $tokens[$stackPtr]['line']) { + $error = 'Invalid position of ternary operator "%s"'; + $data = [$tokens[$stackPtr]['content']]; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Invalid', $data); + + if ($fix) { + $isShortTernary = $tokens[$stackPtr]['code'] === T_INLINE_THEN + && $tokens[$next]['code'] === T_INLINE_ELSE; + + $phpcsFile->fixer->beginChangeset(); + if ($tokens[$stackPtr - 1]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($stackPtr - 1, ''); + } + $phpcsFile->fixer->replaceToken($stackPtr, ''); + if ($isShortTernary) { + if ($tokens[$stackPtr + 1]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($stackPtr + 1, ''); + } + $phpcsFile->fixer->addContentBefore($next, $tokens[$stackPtr]['content']); + } else { + $phpcsFile->fixer->addContentBefore($next, $tokens[$stackPtr]['content'] . ' '); + } + $phpcsFile->fixer->endChangeset(); + } + } + + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true); + if ($tokens[$prev]['line'] < $tokens[$stackPtr]['line']) { + $isThen = $tokens[$stackPtr]['code'] === T_INLINE_THEN; + + $token = $isThen + ? $this->findElse($phpcsFile, $stackPtr) + : $this->findThen($phpcsFile, $stackPtr); + + if ($token === $prev || $token === $next) { + return; + } + + $tokenNext = $phpcsFile->findNext(Tokens::$emptyTokens, $token + 1, null, true); + $tokenPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $token - 1, null, true); + if ($tokens[$tokenNext]['line'] === $tokens[$token]['line'] + && $tokens[$tokenPrev]['line'] === $tokens[$token]['line'] + ) { + $error = 'Invalid position of ternary operator "%s"'; + $data = [$tokens[$token]['content']]; + $fix = $phpcsFile->addFixableError($error, $token, 'Invalid', $data); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + if ($tokens[$token - 1]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($token - 1, ''); + } + $phpcsFile->fixer->addNewlineBefore($token); + $phpcsFile->fixer->endChangeset(); + } + } + } + } + + protected function findThen(File $phpcsFile, int $stackPtr) : ?int + { + $tokens = $phpcsFile->getTokens(); + $count = 0; + + $i = $stackPtr; + while ($i = $phpcsFile->findPrevious([T_INLINE_ELSE, T_INLINE_THEN], $i - 1)) { + if ($tokens[$i]['code'] === T_INLINE_ELSE) { + ++$count; + } else { + --$count; + + if ($count < 0) { + return $i; + } + } + } + + return null; + } + + protected function findElse(File $phpcsFile, int $stackPtr) : ?int + { + $tokens = $phpcsFile->getTokens(); + $count = 0; + + $i = $stackPtr; + while ($i = $phpcsFile->findNext([T_INLINE_ELSE, T_INLINE_THEN], $i + 1)) { + if ($tokens[$i]['code'] === T_INLINE_THEN) { + ++$count; + } else { + --$count; + + if ($count < 0) { + return $i; + } + } + } + + return null; + } +} diff --git a/src/ZendCodingStandard/Sniffs/PHP/CorrectClassNameCaseSniff.php b/src/ZendCodingStandard/Sniffs/PHP/CorrectClassNameCaseSniff.php new file mode 100644 index 00000000..6f368561 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/PHP/CorrectClassNameCaseSniff.php @@ -0,0 +1,441 @@ +declaredClasses = array_merge( + get_declared_classes(), + get_declared_interfaces(), + get_declared_traits() + ); + } + + /** + * @return int[] + */ + public function register() : array + { + return [ + T_NEW, + T_USE, + T_DOUBLE_COLON, + T_IMPLEMENTS, + T_EXTENDS, + // params of function/closures + T_FUNCTION, + T_CLOSURE, + // return type (PHP 7) + T_RETURN_TYPE, + // PHPDocs tags + T_DOC_COMMENT_TAG, + ]; + } + + /** + * @param int $stackPtr + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + switch ($tokens[$stackPtr]['code']) { + case T_DOUBLE_COLON: + $this->checkDoubleColon($phpcsFile, $stackPtr); + return; + case T_NEW: + $this->checkNew($phpcsFile, $stackPtr); + return; + case T_USE: + $this->checkUse($phpcsFile, $stackPtr); + return; + case T_FUNCTION: + case T_CLOSURE: + $this->checkFunctionParams($phpcsFile, $stackPtr); + return; + case T_RETURN_TYPE: + $this->checkReturnType($phpcsFile, $stackPtr); + return; + case T_DOC_COMMENT_TAG: + $this->checkTag($phpcsFile, $stackPtr); + return; + } + + $this->checkExtendsAndImplements($phpcsFile, $stackPtr); + } + + /** + * Checks statement before double colon - "ClassName::". + */ + private function checkDoubleColon(File $phpcsFile, int $stackPtr) : void + { + $tokens = $phpcsFile->getTokens(); + + $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, null, true); + + // When "static::", "self::", "parent::" or "$var::", skip. + if ($tokens[$prevToken]['code'] === T_STATIC + || $tokens[$prevToken]['code'] === T_SELF + || $tokens[$prevToken]['code'] === T_PARENT + || $tokens[$prevToken]['code'] === T_VARIABLE + ) { + return; + } + + $start = $phpcsFile->findPrevious( + [T_NS_SEPARATOR, T_STRING], + $prevToken - 1, + null, + true + ); + + $this->checkClass($phpcsFile, $start + 1, $prevToken + 1);//$prevToken - $start); + } + + /** + * Checks "new ClassName" statements. + */ + private function checkNew(File $phpcsFile, int $stackPtr) : void + { + $tokens = $phpcsFile->getTokens(); + + $nextToken = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true); + + // When "new static", "new self" or "new $var", skip. + if ($tokens[$nextToken]['code'] === T_STATIC + || $tokens[$nextToken]['code'] === T_SELF + || $tokens[$nextToken]['code'] === T_VARIABLE + ) { + return; + } + + $end = $phpcsFile->findNext( + [T_NS_SEPARATOR, T_STRING], + $nextToken + 1, + null, + true + ); + + $this->checkClass($phpcsFile, $nextToken, $end); + } + + /** + * Checks "use" statements - global and traits. + */ + private function checkUse(File $phpcsFile, int $stackPtr) : void + { + $tokens = $phpcsFile->getTokens(); + + // Ignore USE keywords inside closures. + $next = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true); + if ($tokens[$next]['code'] === T_OPEN_PARENTHESIS) { + return; + } + + $nextToken = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true); + + $end = $phpcsFile->findNext( + [T_NS_SEPARATOR, T_STRING], + $nextToken + 1, + null, + true + ); + + // Global use statements. + if (empty($tokens[$stackPtr]['conditions'])) { + $this->checkClass($phpcsFile, $nextToken, $end, true); + return; + } + + // Traits. + $this->checkClass($phpcsFile, $nextToken, $end); + } + + /** + * Checks params type hints + */ + private function checkFunctionParams(File $phpcsFile, int $stackPtr) : void + { + $params = $phpcsFile->getMethodParameters($stackPtr); + + foreach ($params as $param) { + if (! $param['type_hint']) { + continue; + } + + $end = $phpcsFile->findPrevious(Tokens::$emptyTokens, $param['token'] - 1, null, true); + $before = $phpcsFile->findPrevious([T_COMMA, T_OPEN_PARENTHESIS, T_WHITESPACE], $end - 1); + $first = $phpcsFile->findNext(Tokens::$emptyTokens, $before + 1, null, true); + + $this->checkClass($phpcsFile, $first, $end + 1); + } + } + + /** + * Checks return type + */ + private function checkReturnType(File $phpcsFile, int $stackPtr) : void + { + $before = $phpcsFile->findPrevious([T_COLON, T_NULLABLE], $stackPtr - 1); + $first = $phpcsFile->findNext(Tokens::$emptyTokens, $before + 1, null, true); + + $this->checkClass($phpcsFile, $first, $stackPtr + 1); + } + + /** + * Checks PHPDocs tags + */ + private function checkTag(File $phpcsFile, int $stackPtr) : void + { + $tokens = $phpcsFile->getTokens(); + + if (! in_array($tokens[$stackPtr]['content'], ['@var', '@param', '@return', '@throws'], true) + || $tokens[$stackPtr + 1]['code'] !== T_DOC_COMMENT_WHITESPACE + || $tokens[$stackPtr + 2]['code'] !== T_DOC_COMMENT_STRING + ) { + return; + } + + $string = $tokens[$stackPtr + 2]['content']; + [$types] = explode(' ', $string); + $typesArr = explode('|', $types); + + $newTypesArr = []; + foreach ($typesArr as $type) { + $expected = $this->getExpectedName($phpcsFile, $type, $stackPtr + 2); + + $newTypesArr[] = $expected; + } + + $newTypes = implode('|', $newTypesArr); + + if ($newTypes !== $types) { + $error = 'Invalid class name case: expected %s; found %s'; + $data = [ + $newTypes, + $types, + ]; + $fix = $phpcsFile->addFixableError($error, $stackPtr + 2, 'InvalidInPhpDocs', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken( + $stackPtr + 2, + preg_replace('/^' . preg_quote($types) . '/', $newTypes, $string) + ); + } + } + } + + /** + * Returns expected class name for given $class. + */ + private function getExpectedName(File $phpcsFile, string $class, int $stackPtr) : string + { + $suffix = strstr($class, '['); + $class = strtr($class, ['[' => '', ']' => '']); + + if ($class[0] === '\\') { + $result = $this->hasDifferentCase(ltrim($class, '\\')); + if ($result) { + return '\\' . $result . $suffix; + } + + return $class . $suffix; + } + + $imports = $this->getGlobalUses($phpcsFile); + + // Check if class is imported. + if (isset($imports[strtolower($class)])) { + if ($imports[strtolower($class)]['alias'] !== $class) { + return $imports[strtolower($class)]['alias'] . $suffix; + } + } else { + // Class from the same namespace. + $namespace = $this->getNamespace($phpcsFile, $stackPtr); + $fullClassName = ltrim($namespace . '\\' . $class, '\\'); + + $result = $this->hasDifferentCase(ltrim($fullClassName, '\\')); + if ($result) { + return ltrim(substr($result, strlen($namespace)), '\\') . $suffix; + } + } + + return $class . $suffix; + } + + /** + * Checks "extends" and "implements" classes/interfaces. + */ + private function checkExtendsAndImplements(File $phpcsFile, int $stackPtr) : void + { + $tokens = $phpcsFile->getTokens(); + + $search = $stackPtr; + while ($nextToken = $phpcsFile->findNext([T_WHITESPACE, T_COMMA], $search + 1, null, true)) { + if ($tokens[$nextToken]['code'] !== T_NS_SEPARATOR + && $tokens[$nextToken]['code'] !== T_STRING + ) { + break; + } + + $end = $phpcsFile->findNext( + [T_NS_SEPARATOR, T_STRING], + $nextToken + 1, + null, + true + ); + + $this->checkClass($phpcsFile, $nextToken, $end); + + $search = $end; + } + } + + /** + * Checks if class is used correctly. + */ + private function checkClass(File $phpcsFile, int $start, int $end, bool $isGlobalUse = false) : void + { + $class = trim($phpcsFile->getTokensAsString($start, $end - $start)); + if ($class[0] === '\\') { + $result = $this->hasDifferentCase(ltrim($class, '\\')); + if ($result) { + $this->error($phpcsFile, $start, $end, '\\' . $result, $class); + } + + return; + } + + if (! $isGlobalUse) { + $imports = $this->getGlobalUses($phpcsFile); + + // Check if class is imported. + if (isset($imports[strtolower($class)])) { + if ($imports[strtolower($class)]['alias'] !== $class) { + $this->error($phpcsFile, $start, $end, $imports[strtolower($class)]['alias'], $class); + } + } else { + // Class from the same namespace. + $namespace = $this->getNamespace($phpcsFile, $start); + $fullClassName = ltrim($namespace . '\\' . $class, '\\'); + + $result = $this->hasDifferentCase(ltrim($fullClassName, '\\')); + if ($result) { + $this->error($phpcsFile, $start, $end, ltrim(substr($result, strlen($namespace)), '\\'), $class); + } + } + } else { + // Global use statement. + $result = $this->hasDifferentCase($class); + if ($result) { + $this->error($phpcsFile, $start, $end, $result, $class); + } + } + } + + /** + * Reports new fixable error. + */ + private function error(File $phpcsFile, int $start, int $end, string $expected, string $actual) : void + { + $error = 'Invalid class name case: expected %s; found %s'; + $data = [ + $expected, + $actual, + ]; + $fix = $phpcsFile->addFixableError($error, $start + 1, 'Invalid', $data); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $start; $i < $end - 1; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->replaceToken($end - 1, $expected); + $phpcsFile->fixer->endChangeset(); + } + } + + /** + * Checks if class is defined and has different case - then returns class name + * with correct case. Otherwise returns false. + */ + private function hasDifferentCase(string $class) : ?string + { + $index = array_search(strtolower($class), array_map('strtolower', $this->declaredClasses), true); + + if ($index === false) { + // Not defined? + return null; + } + + if ($this->declaredClasses[$index] === $class) { + // Exactly the same. + return null; + } + + return $this->declaredClasses[$index]; + } +} diff --git a/src/ZendCodingStandard/Sniffs/PHP/DeclareStrictTypesSniff.php b/src/ZendCodingStandard/Sniffs/PHP/DeclareStrictTypesSniff.php new file mode 100644 index 00000000..48dea4e9 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/PHP/DeclareStrictTypesSniff.php @@ -0,0 +1,379 @@ +spacingBefore = (int) $this->spacingBefore; + $this->spacingAfter = (int) $this->spacingAfter; + + $tokens = $phpcsFile->getTokens(); + + if ($stackPtr > 0) { + $before = trim($phpcsFile->getTokensAsString(0, $stackPtr)); + + if ($before === '') { + $error = 'Unexpected whitespace before PHP opening tag'; + $fix = $phpcsFile->addFixableError($error, 0, 'Whitespace'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = 0; $i < $stackPtr; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->endChangeset(); + } + } else { + $error = 'Missing strict type declaration as first statement in the script'; + $fix = $phpcsFile->addFixableError($error, 0, 'Missing'); + + if ($fix) { + $phpcsFile->fixer->addContentBefore( + 0, + sprintf('%s', $this->format, $phpcsFile->eolChar) + ); + } + } + + $this->checkOtherDeclarations($phpcsFile); + + return $phpcsFile->numTokens + 1; + } + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + + if ($tokens[$next]['code'] === T_DECLARE) { + $string = $phpcsFile->findNext( + T_STRING, + $tokens[$next]['parenthesis_opener'] + 1, + $tokens[$next]['parenthesis_closer'] + ); + + if ($string !== false + && stripos($tokens[$string]['content'], 'strict_types') !== false + ) { + $eos = $phpcsFile->findEndOfStatement($next); + $prev = $phpcsFile->findPrevious(T_WHITESPACE, $next - 1, null, true); + $after = $phpcsFile->findNext(T_WHITESPACE, $eos + 1, null, true); + + if ($after !== false + && $tokens[$prev]['code'] === T_OPEN_TAG + && $tokens[$after]['code'] === T_CLOSE_TAG + ) { + if ($tokens[$prev]['line'] !== $tokens[$next]['line']) { + $error = 'PHP open tag must be on the same line as strict type declaration.'; + $fix = $phpcsFile->addFixableError($error, $prev, 'OpenTag'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($prev, 'fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->endChangeset(); + } + + $prev = false; + } + + if ($prev !== false && ($prev < ($next - 1) || $tokens[$prev]['content'] !== 'addFixableError($error, $prev, 'OpenTagSpace'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($prev, 'fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->endChangeset(); + } + } + + if ($tokens[$after]['line'] !== $tokens[$eos]['line']) { + $error = 'PHP close tag must be on the same line as strict type declaration.'; + $fix = $phpcsFile->addFixableError($error, $after, 'CloseTag'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $eos + 1; $i < $after; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->addContentBefore($after, ' '); + $phpcsFile->fixer->endChangeset(); + } + + $after = false; + } + + if ($after !== false && ($after > $eos + 2 || $tokens[$eos + 1]['content'] !== ' ')) { + $error = 'Expected single space before PHP close tag and after declaration.'; + $fix = $phpcsFile->addFixableError($error, $after, 'CloseTagSpace'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $eos + 1; $i < $after; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->addContentBefore($after, ' '); + $phpcsFile->fixer->endChangeset(); + } + } + + $prev = false; + $after = false; + } + + // Check how many blank lines there are before declare statement. + if ($prev !== false) { + $linesBefore = $tokens[$next]['line'] - $tokens[$prev]['line'] - 1; + if ($linesBefore !== $this->spacingBefore) { + if ($linesBefore < 0) { + $error = 'Strict type declaration must be in new line'; + $data = []; + } else { + $error = 'Invalid number of blank lines before declare statement;' + . ' expected %d, but found %d'; + $data = [ + $this->spacingBefore, + $linesBefore, + ]; + } + + $fix = $phpcsFile->addFixableError($error, $next, 'LinesBefore', $data); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + if ($linesBefore > $this->spacingBefore) { + // Remove additional blank line(s). + for ($i = $prev + 1; $i < $next; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + if ($tokens[$next]['line'] - $tokens[$i + 1]['line'] - 1 === $this->spacingBefore) { + break; + } + } + } else { + // Clear whitespaces between prev and next, but no new lines. + if ($linesBefore < 0) { + for ($i = $prev + 1; $i < $next; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + } + + // Add new blank line(s). + while ($linesBefore < $this->spacingBefore) { + $phpcsFile->fixer->addNewlineBefore($next); + ++$linesBefore; + } + } + + $phpcsFile->fixer->endChangeset(); + } + } + } + + // Check number of blank lines after the declare statement. + if ($after !== false) { + if ($tokens[$after]['code'] === T_CLOSE_TAG) { + $this->spacingAfter = 0; + } + + $linesAfter = $tokens[$after]['line'] - $tokens[$eos]['line'] - 1; + if ($linesAfter !== $this->spacingAfter) { + if ($linesAfter < 0) { + $error = 'Strict type declaration must be the only statement in the line'; + $data = []; + } else { + $error = 'Invalid number of blank lines after declare statement; expected %d, but found %d'; + $data = [ + $this->spacingAfter, + $linesAfter, + ]; + } + + $fix = $phpcsFile->addFixableError($error, $eos, 'LinesAfter', $data); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + if ($linesAfter > $this->spacingAfter) { + for ($i = $eos + 1; $i < $after; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + if ($tokens[$after]['line'] - $tokens[$i + 1]['line'] - 1 === $this->spacingAfter) { + break; + } + } + } else { + // Remove whitespaces between EOS and after token. + if ($linesAfter < 0) { + for ($i = $eos + 1; $i < $after; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + } + + // Add new lines after the statement. + while ($linesAfter < $this->spacingAfter) { + $phpcsFile->fixer->addNewline($eos); + ++$linesAfter; + } + } + + $phpcsFile->fixer->endChangeset(); + } + } + } + + // Check if declare statement match provided format. + $string = $phpcsFile->getTokensAsString($next, $eos - $next + 1); + if ($string !== $this->format) { + $error = 'Invalid format of strict type declaration; expected "%s", but found "%s"'; + $data = [ + $this->format, + $string, + ]; + + if ($this->normalize($string) === $this->normalize($this->format)) { + $fix = $phpcsFile->addFixableError($error, $next, 'InvalidFormat', $data); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $next; $i < $eos; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->replaceToken($eos, $this->format); + $phpcsFile->fixer->endChangeset(); + } + } else { + $phpcsFile->addError($error, $next, 'InvalidFormatNotFixable', $data); + } + } + + $this->checkOtherDeclarations($phpcsFile, $next); + + return $phpcsFile->numTokens + 1; + } + } + + $this->checkOtherDeclarations($phpcsFile, $next); + + $error = 'Missing strict type declaration at the beginning of the file'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NotFound'); + + if ($fix) { + $phpcsFile->fixer->addContent($stackPtr, $this->format . $phpcsFile->eolChar); + } + + return $phpcsFile->numTokens + 1; + } + + /** + * @param string $string String to be normalized. + */ + private function normalize(string $string) : string + { + return strtolower(preg_replace('/\s/', '', $string)); + } + + /** + * Process other strict_type declaration in the file and remove them. + * The declaration has to be the very first statement in the script. + * + * @param File $phpcsFile The file being scanned. + * @param int $declare The position of the first declaration. + */ + private function checkOtherDeclarations(File $phpcsFile, int $declare = 0) : void + { + $tokens = $phpcsFile->getTokens(); + + while ($declare = $phpcsFile->findNext(T_DECLARE, $declare + 1)) { + $string = $phpcsFile->findNext( + T_STRING, + $tokens[$declare]['parenthesis_opener'] + 1, + $tokens[$declare]['parenthesis_closer'] + ); + + if ($string !== false + && stripos($tokens[$string]['content'], 'strict_types') !== false + ) { + $error = 'Strict type declaration must be the very first statement in the script'; + $fix = $phpcsFile->addFixableError($error, $declare, 'NotFirstStatement'); + + if ($fix) { + $end = $phpcsFile->findNext( + Tokens::$emptyTokens + [T_SEMICOLON => T_SEMICOLON], + $tokens[$declare]['parenthesis_closer'] + 1, + null, + true + ); + + if ($end === false) { + $end = $phpcsFile->numTokens; + } + + for ($i = $declare; $i < $end; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + } + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/PHP/ImportInternalConstantSniff.php b/src/ZendCodingStandard/Sniffs/PHP/ImportInternalConstantSniff.php new file mode 100644 index 00000000..2d68bbbc --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/PHP/ImportInternalConstantSniff.php @@ -0,0 +1,212 @@ +builtInConstants = $arr; + } + + /** + * @return int[] + */ + public function register() : array + { + return [T_STRING]; + } + + /** + * @param int $stackPtr + */ + public function process(File $phpcsFile, $stackPtr) + { + if ($this->currentFile !== $phpcsFile) { + $this->currentFile = $phpcsFile; + $this->currentNamespace = null; + } + + $namespace = $this->getNamespace($phpcsFile, $stackPtr); + if ($namespace && $this->currentNamespace !== $namespace) { + $this->currentNamespace = $namespace; + $this->importedConstants = $this->getImportedConstants($phpcsFile, $stackPtr, $this->lastUse); + } + + $tokens = $phpcsFile->getTokens(); + + $content = strtoupper($tokens[$stackPtr]['content']); + if ($content !== $tokens[$stackPtr]['content']) { + return; + } + + if (! isset($this->builtInConstants[$content])) { + return; + } + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + if ($next && $tokens[$next]['code'] === T_OPEN_PARENTHESIS) { + return; + } + + $prev = $phpcsFile->findPrevious( + Tokens::$emptyTokens + [T_BITWISE_AND => T_BITWISE_AND, T_NS_SEPARATOR => T_NS_SEPARATOR], + $stackPtr - 1, + null, + true + ); + if ($tokens[$prev]['code'] === T_FUNCTION + || $tokens[$prev]['code'] === T_NEW + || $tokens[$prev]['code'] === T_STRING + || $tokens[$prev]['code'] === T_DOUBLE_COLON + || $tokens[$prev]['code'] === T_OBJECT_OPERATOR + ) { + return; + } + + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true); + if ($tokens[$prev]['code'] === T_NS_SEPARATOR) { + if (! $namespace) { + $error = 'FQN for PHP internal constant "%s" is not needed here, file does not have defined namespace'; + $data = [ + $content, + ]; + + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NoNamespace', $data); + if ($fix) { + $phpcsFile->fixer->replaceToken($prev, ''); + } + } elseif (isset($this->importedConstants[$content])) { + if (strtoupper($this->importedConstants[$content]['fqn']) === $content) { + $error = 'FQN for PHP internal constant "%s" is not needed here, constant is already imported'; + $data = [ + $content, + ]; + + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'RedundantFQN', $data); + if ($fix) { + $phpcsFile->fixer->replaceToken($prev, ''); + } + } + } else { + $error = 'PHP internal constant "%s" must be imported'; + $data = [ + $content, + ]; + + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'ImportFQN', $data); + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($prev, ''); + $this->importConstant($phpcsFile, $stackPtr, $content); + $phpcsFile->fixer->endChangeset(); + } + } + } elseif ($namespace) { + if (! isset($this->importedConstants[$content])) { + $error = 'PHP internal constant "%s" must be imported'; + $data = [ + $content, + ]; + + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Import', $data); + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $this->importConstant($phpcsFile, $stackPtr, $content); + $phpcsFile->fixer->endChangeset(); + } + } + } + } + + private function importConstant(File $phpcsFile, int $stackPtr, string $constantName) : void + { + if ($this->lastUse) { + $ptr = $phpcsFile->findEndOfStatement($this->lastUse); + } else { + $nsStart = $phpcsFile->findPrevious(T_NAMESPACE, $stackPtr); + if ($nsStart) { + $tokens = $phpcsFile->getTokens(); + if (isset($tokens[$nsStart]['scope_opener'])) { + $ptr = $tokens[$nsStart]['scope_opener']; + } else { + $ptr = $phpcsFile->findEndOfStatement($nsStart); + $phpcsFile->fixer->addNewline($ptr); + } + } else { + $ptr = $phpcsFile->findPrevious(T_OPEN_TAG, $stackPtr - 1); + } + } + + $phpcsFile->fixer->addNewline($ptr); + $phpcsFile->fixer->addContent($ptr, sprintf('use const %s;', $constantName)); + if (! $this->lastUse && (! $nsStart || isset($tokens[$nsStart]['scope_opener']))) { + $phpcsFile->fixer->addNewline($ptr); + } + + $this->importedConstants[$constantName] = [ + 'name' => $constantName, + 'fqn' => $constantName, + ]; + } +} diff --git a/src/ZendCodingStandard/Sniffs/PHP/ImportInternalFunctionSniff.php b/src/ZendCodingStandard/Sniffs/PHP/ImportInternalFunctionSniff.php new file mode 100644 index 00000000..566c0ccd --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/PHP/ImportInternalFunctionSniff.php @@ -0,0 +1,200 @@ +builtInFunctions = array_flip($allFunctions['internal']); + } + + /** + * @return int[] + */ + public function register() : array + { + return [T_STRING]; + } + + /** + * @param int $stackPtr + */ + public function process(File $phpcsFile, $stackPtr) + { + if ($this->currentFile !== $phpcsFile) { + $this->currentFile = $phpcsFile; + $this->currentNamespace = null; + } + + $namespace = $this->getNamespace($phpcsFile, $stackPtr); + if ($namespace && $this->currentNamespace !== $namespace) { + $this->currentNamespace = $namespace; + $this->importedFunctions = $this->getImportedFunctions($phpcsFile, $stackPtr, $this->lastUse); + } + + $tokens = $phpcsFile->getTokens(); + + // Make sure this is a function call. + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + if (! $next || $tokens[$next]['code'] !== T_OPEN_PARENTHESIS) { + return; + } + + $content = strtolower($tokens[$stackPtr]['content']); + if (! isset($this->builtInFunctions[$content])) { + return; + } + + $prev = $phpcsFile->findPrevious( + Tokens::$emptyTokens + [T_BITWISE_AND => T_BITWISE_AND, T_NS_SEPARATOR => T_NS_SEPARATOR], + $stackPtr - 1, + null, + true + ); + if ($tokens[$prev]['code'] === T_FUNCTION + || $tokens[$prev]['code'] === T_NEW + || $tokens[$prev]['code'] === T_STRING + || $tokens[$prev]['code'] === T_DOUBLE_COLON + || $tokens[$prev]['code'] === T_OBJECT_OPERATOR + ) { + return; + } + + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true); + if ($tokens[$prev]['code'] === T_NS_SEPARATOR) { + if (! $namespace) { + $error = 'FQN for PHP internal function "%s" is not needed here, file does not have defined namespace'; + $data = [ + $content, + ]; + + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NoNamespace', $data); + if ($fix) { + $phpcsFile->fixer->replaceToken($prev, ''); + } + } elseif (isset($this->importedFunctions[$content])) { + if (strtolower($this->importedFunctions[$content]['fqn']) === $content) { + $error = 'FQN for PHP internal function "%s" is not needed here, function is already imported'; + $data = [ + $content, + ]; + + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'RedundantFQN', $data); + if ($fix) { + $phpcsFile->fixer->replaceToken($prev, ''); + } + } + } else { + $error = 'PHP internal function "%s" must be imported'; + $data = [ + $content, + ]; + + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'ImportFQN', $data); + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($prev, ''); + $this->importFunction($phpcsFile, $stackPtr, $content); + $phpcsFile->fixer->endChangeset(); + } + } + } elseif ($namespace) { + if (! isset($this->importedFunctions[$content])) { + $error = 'PHP internal function "%s" must be imported'; + $data = [ + $content, + ]; + + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Import', $data); + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $this->importFunction($phpcsFile, $stackPtr, $content); + $phpcsFile->fixer->endChangeset(); + } + } + } + } + + private function importFunction(File $phpcsFile, int $stackPtr, string $functionName) : void + { + if ($this->lastUse) { + $ptr = $phpcsFile->findEndOfStatement($this->lastUse); + } else { + $nsStart = $phpcsFile->findPrevious(T_NAMESPACE, $stackPtr); + if ($nsStart) { + $tokens = $phpcsFile->getTokens(); + if (isset($tokens[$nsStart]['scope_opener'])) { + $ptr = $tokens[$nsStart]['scope_opener']; + } else { + $ptr = $phpcsFile->findEndOfStatement($nsStart); + $phpcsFile->fixer->addNewline($ptr); + } + } else { + $ptr = $phpcsFile->findPrevious(T_OPEN_TAG, $stackPtr - 1); + } + } + + $phpcsFile->fixer->addNewline($ptr); + $phpcsFile->fixer->addContent($ptr, sprintf('use function %s;', $functionName)); + if (! $this->lastUse && (! $nsStart || isset($tokens[$nsStart]['scope_opener']))) { + $phpcsFile->fixer->addNewline($ptr); + } + + $this->importedFunctions[$functionName] = [ + 'name' => $functionName, + 'fqn' => $functionName, + ]; + } +} diff --git a/src/ZendCodingStandard/Sniffs/PHP/InstantiatingParenthesisSniff.php b/src/ZendCodingStandard/Sniffs/PHP/InstantiatingParenthesisSniff.php new file mode 100644 index 00000000..0d64da31 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/PHP/InstantiatingParenthesisSniff.php @@ -0,0 +1,74 @@ +getTokens(); + + $end = $phpcsFile->findNext( + array_merge(Tokens::$emptyTokens, [ + T_ANON_CLASS, + T_NS_SEPARATOR, + T_SELF, + T_STATIC, + T_STRING, + T_VARIABLE, + ]), + $stackPtr + 1, + null, + true + ); + + while ($tokens[$end]['code'] === T_OPEN_SQUARE_BRACKET) { + $end = $phpcsFile->findNext(Tokens::$emptyTokens, $tokens[$end]['bracket_closer'] + 1, null, true); + } + + if ($tokens[$end]['code'] !== T_OPEN_PARENTHESIS) { + $last = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + $end - 1, + $stackPtr + 1, + true + ); + + $error = 'Missing parenthesis on instantiating a new class.'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'MissingParenthesis'); + + if ($fix) { + $phpcsFile->fixer->addContent($last, '()'); + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/PHP/RedundantSemicolonSniff.php b/src/ZendCodingStandard/Sniffs/PHP/RedundantSemicolonSniff.php new file mode 100644 index 00000000..b1daa068 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/PHP/RedundantSemicolonSniff.php @@ -0,0 +1,62 @@ +getTokens(); + + if (! isset($tokens[$stackPtr]['scope_condition'])) { + return; + } + + $scopeCondition = $tokens[$stackPtr]['scope_condition']; + if (in_array($tokens[$scopeCondition]['code'], [T_ANON_CLASS, T_CLOSURE], true)) { + return; + } + + $nextNonEmpty = $phpcsFile->findNext( + Tokens::$emptyTokens, + $stackPtr + 1, + null, + true + ); + + if ($tokens[$nextNonEmpty]['code'] === T_SEMICOLON) { + $error = 'Redundant semicolon after control structure "%s".'; + $data = [strtolower($tokens[$scopeCondition]['content'])]; + $fix = $phpcsFile->addFixableError($error, $nextNonEmpty, 'SemicolonFound', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($nextNonEmpty, ''); + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/PHP/SingleSemicolonSniff.php b/src/ZendCodingStandard/Sniffs/PHP/SingleSemicolonSniff.php new file mode 100644 index 00000000..032d63ea --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/PHP/SingleSemicolonSniff.php @@ -0,0 +1,40 @@ +getTokens(); + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true); + + if ($next && $tokens[$next]['code'] === T_SEMICOLON) { + $error = 'Redundant semicolon'; + $fix = $phpcsFile->addFixableError($error, $next, 'Semicolon'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($next, ''); + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/PHP/TypeCastingSniff.php b/src/ZendCodingStandard/Sniffs/PHP/TypeCastingSniff.php new file mode 100644 index 00000000..33afebd9 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/PHP/TypeCastingSniff.php @@ -0,0 +1,87 @@ + '(bool)', + '(integer)' => '(int)', + '(real)' => '(float)', + '(double)' => '(float)', + ]; + + /** + * @return int[] + */ + public function register() : array + { + return Tokens::$castTokens + + [T_BOOLEAN_NOT => T_BOOLEAN_NOT]; + } + + /** + * @param int $stackPtr + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if ($tokens[$stackPtr]['code'] === T_BOOLEAN_NOT) { + $nextToken = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true); + if (! $nextToken || $tokens[$nextToken]['code'] !== T_BOOLEAN_NOT) { + return; + } + $error = 'Double negation casting is not allowed. Please use (bool) instead.'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'DoubleNot'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($stackPtr, '(bool)'); + $phpcsFile->fixer->replaceToken($nextToken, ''); + $phpcsFile->fixer->endChangeset(); + } + + return; + } + + if ($tokens[$stackPtr]['code'] === T_UNSET_CAST) { + $phpcsFile->addError('(unset) casting is not allowed.', $stackPtr, 'UnsetCast'); + return; + } + + $content = $tokens[$stackPtr]['content']; + $expected = preg_replace('/\s/', '', strtolower($content)); + if ($content !== $expected || isset($this->castMap[$expected])) { + $error = 'Invalid casting used. Expected %s, found %s'; + $expected = $this->castMap[$expected] ?? $expected; + $data = [ + $expected, + $content, + ]; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Invalid', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr, $expected); + } + + return; + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/Strings/NoConcatenationAtTheEndSniff.php b/src/ZendCodingStandard/Sniffs/Strings/NoConcatenationAtTheEndSniff.php new file mode 100644 index 00000000..8dd25e30 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Strings/NoConcatenationAtTheEndSniff.php @@ -0,0 +1,49 @@ +getTokens(); + + $next = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true); + + if ($tokens[$stackPtr]['line'] === $tokens[$next]['line']) { + return; + } + + $error = 'String concatenation character is not allowed at the end of the line.'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'ConcatenationAtTheEnd'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + if ($tokens[$stackPtr - 1]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($stackPtr - 1, ''); + } + $phpcsFile->fixer->replaceToken($stackPtr, ''); + $phpcsFile->fixer->addContentBefore($next, '. '); + $phpcsFile->fixer->endChangeset(); + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/WhiteSpace/BlankLineSniff.php b/src/ZendCodingStandard/Sniffs/WhiteSpace/BlankLineSniff.php new file mode 100644 index 00000000..2ae6779c --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/WhiteSpace/BlankLineSniff.php @@ -0,0 +1,45 @@ +getTokens(); + + $next = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true); + if ($next && $tokens[$stackPtr]['line'] < $tokens[$next]['line'] - 2) { + $error = 'Unexpected blank line found.'; + $fix = $phpcsFile->addFixableError($error, $stackPtr + 1, 'BlankLine'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr + 1, ''); + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/WhiteSpace/CommaSpacingSniff.php b/src/ZendCodingStandard/Sniffs/WhiteSpace/CommaSpacingSniff.php new file mode 100644 index 00000000..19ee2af9 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/WhiteSpace/CommaSpacingSniff.php @@ -0,0 +1,113 @@ +getTokens(); + + // Check spaces before comma. + $prevToken = $tokens[$stackPtr - 1]; + if ($prevToken['code'] === T_WHITESPACE) { + $error = 'Expected 0 spaces before comma; found %d'; + $data = [strlen($prevToken['content'])]; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceBeforeComma', $data); + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr - 1, ''); + } + } + + $nextToken = $tokens[$stackPtr + 1]; + if ($nextToken['code'] !== T_WHITESPACE) { + // There is no space after comma. + + $error = 'Expected 1 space after comma; found 0'; + $fix = $phpcsFile->addFixableError($error, $stackPtr + 1, 'NoSpaceAfterComma'); + if ($fix) { + $phpcsFile->fixer->addContent($stackPtr, ' '); + } + } elseif ($nextToken['content'] !== ' ') { + // There is more than one space after comma. + + $nonSpace = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true); + if ($tokens[$nonSpace]['line'] !== $tokens[$stackPtr]['line']) { + // Next non-space token is in new line, return. + + return; + } + + // Check if this is multidimensional array. + $openArray = $phpcsFile->findPrevious([T_OPEN_SHORT_ARRAY], $stackPtr); + $beforeOpening = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + $openArray - 1, + null, + true + ); + $closeArray = $phpcsFile->findNext([T_CLOSE_SHORT_ARRAY], $stackPtr); + $afterClosing = $phpcsFile->findNext( + array_merge(Tokens::$emptyTokens, [T_COMMA]), + $closeArray + 1, + null, + true + ); + + if ($tokens[$openArray]['line'] !== $tokens[$closeArray]['line'] + || ($tokens[$beforeOpening]['line'] === $tokens[$openArray]['line'] + && $tokens[$beforeOpening]['code'] !== T_DOUBLE_ARROW) + || $tokens[$afterClosing]['line'] === $tokens[$closeArray]['line'] + ) { + $error = 'Expected 1 space after comma; found %d'; + $data = [ + strlen($nextToken['content']), + ]; + $fix = $phpcsFile->addFixableError($error, $stackPtr + 1, 'SpacingAfterComma', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr + 1, ' '); + } + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/WhiteSpace/NoBlankLineAtStartSniff.php b/src/ZendCodingStandard/Sniffs/WhiteSpace/NoBlankLineAtStartSniff.php new file mode 100644 index 00000000..366df920 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/WhiteSpace/NoBlankLineAtStartSniff.php @@ -0,0 +1,66 @@ +getTokens(); + $token = $tokens[$stackPtr]; + + // Skip function without body. + if (! isset($token['scope_opener'])) { + return; + } + + $scopeOpener = $tokens[$stackPtr]['scope_opener']; + $firstContent = $phpcsFile->findNext(T_WHITESPACE, $scopeOpener + 1, null, true); + + if ($tokens[$firstContent]['line'] > $tokens[$scopeOpener]['line'] + 1) { + $error = 'Blank line found at start of %s'; + $data = [$token['content']]; + $fix = $phpcsFile->addFixableError($error, $scopeOpener + 1, 'BlankLine', $data); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + $i = $scopeOpener + 1; + while ($tokens[$i]['line'] !== $tokens[$firstContent]['line']) { + $phpcsFile->fixer->replaceToken($i, ''); + ++$i; + } + $phpcsFile->fixer->addNewline($scopeOpener); + $phpcsFile->fixer->endChangeset(); + } + } + } +} diff --git a/src/ZendCodingStandard/Sniffs/WhiteSpace/ScopeIndentSniff.php b/src/ZendCodingStandard/Sniffs/WhiteSpace/ScopeIndentSniff.php new file mode 100644 index 00000000..face60d9 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/WhiteSpace/ScopeIndentSniff.php @@ -0,0 +1,910 @@ + T_IF, + T_ELSEIF => T_ELSEIF, + T_SWITCH => T_SWITCH, + T_WHILE => T_WHILE, + T_FOR => T_FOR, + T_FOREACH => T_FOREACH, + T_CATCH => T_CATCH, + ]; + + /** + * @var int[] + */ + private $endOfStatement = [ + T_SEMICOLON, + T_CLOSE_CURLY_BRACKET, + T_OPEN_CURLY_BRACKET, + T_OPEN_TAG, + T_COLON, + T_GOTO_LABEL, + T_COMMA, + T_OPEN_PARENTHESIS, + T_OPEN_SHORT_ARRAY, + ]; + + /** + * @var int[] + */ + private $caseEndToken = [ + T_BREAK, + T_CONTINUE, + T_RETURN, + T_THROW, + T_EXIT, + ]; + + /** + * @var int[] + */ + private $breakToken; + + /** + * @var int[] + */ + private $functionToken; + + public function __construct() + { + $this->breakToken = Tokens::$operators + + Tokens::$assignmentTokens + + Tokens::$booleanOperators + + Tokens::$comparisonTokens + + [ + T_SEMICOLON => T_SEMICOLON, + T_OPEN_PARENTHESIS => T_OPEN_PARENTHESIS, + T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET, + T_OPEN_SHORT_ARRAY => T_OPEN_SHORT_ARRAY, + T_ARRAY => T_ARRAY, + T_COMMA => T_COMMA, + T_INLINE_ELSE => T_INLINE_ELSE, + T_INLINE_THEN => T_INLINE_THEN, + T_STRING_CONCAT => T_STRING_CONCAT, + ]; + + $this->functionToken = Tokens::$functionNameTokens + + $this->controlStructures + + [ + T_SELF => T_SELF, + T_STATIC => T_STATIC, + T_VARIABLE => T_VARIABLE, + T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET, + T_CLOSE_PARENTHESIS => T_CLOSE_PARENTHESIS, + T_USE => T_USE, + T_CLOSURE => T_CLOSURE, + T_ARRAY => T_ARRAY, + ]; + } + + /** + * @return int[] + */ + public function register() : array + { + return [T_OPEN_TAG]; + } + + /** + * @param int $stackPtr + * @return null|int + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + $depth = 0; + $extras = []; + $previousIndent = null; + + // calculate indent of php open tag + $html = $phpcsFile->findFirstOnLine(T_INLINE_HTML, $stackPtr); + $trimmed = ltrim($tokens[$html]['content']); + if ($html === false || $trimmed === '') { + $extraIndent = $tokens[$stackPtr]['column'] - 1; + } else { + $extraIndent = strlen($tokens[$html]['content']) - strlen($trimmed); + } + + for ($i = $stackPtr + 1; $i < $phpcsFile->numTokens; ++$i) { + if (in_array($tokens[$i]['code'], Tokens::$booleanOperators, true)) { + $next = $phpcsFile->findNext( + Tokens::$emptyTokens + [T_OPEN_PARENTHESIS => T_OPEN_PARENTHESIS], + $i + 1, + null, + true + ); + + if ($tokens[$next]['line'] > $tokens[$i]['line']) { + $error = 'Boolean operator found at the end of the line.'; + $fix = $phpcsFile->addFixableError($error, $i, 'BooleanOperatorAtTheEnd'); + + if ($fix) { + $lastNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, $next - 1, null, true); + $string = $phpcsFile->getTokensAsString($i, $lastNonEmpty - $i + 1); + + if (substr($string, -1) !== '(') { + $string .= ' '; + } + + $phpcsFile->fixer->beginChangeset(); + $j = $i - 1; + while ($tokens[$j]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($j, ''); + --$j; + } + for ($j = $i; $j <= $lastNonEmpty; ++$j) { + $phpcsFile->fixer->replaceToken($j, ''); + } + $phpcsFile->fixer->addContentBefore($next, $string); + $phpcsFile->fixer->endChangeset(); + } + + continue; + } + } + + // skip some tags + if ($tokens[$i]['code'] === T_INLINE_HTML) { + // || $tokens[$i]['code'] === T_CLOSE_TAG + // || $tokens[$i]['code'] === T_OPEN_TAG + continue; + } + + if (($tokens[$i]['code'] === T_CONSTANT_ENCAPSED_STRING + || $tokens[$i]['code'] === T_DOUBLE_QUOTED_STRING) + && $tokens[$i - 1]['code'] === $tokens[$i]['code'] + ) { + continue; + } + + // || $tokens[$i]['code'] === T_ANON_CLASS + if ($tokens[$i]['code'] === T_CLASS) { + $i = $tokens[$i]['scope_opener']; + continue; + } + + // @todo: multi-open-tags + if ($tokens[$i]['code'] === T_OPEN_TAG) { + // $error = 'This sniff does not support files with multiple PHP open tags.'; + // $phpcsFile->addError($error, $i, 'UnsupportedFile'); + // return $phpcsFile->numTokens + 1; + // if ($depth === 0) { + $extraIndent = max($tokens[$i]['column'] - 1 - ($depth * $this->indent), 0); + $extraIndent = (int) (ceil($extraIndent / $this->indent) * $this->indent); + // } + continue; + } + + // skip doc block comment + if ($tokens[$i]['code'] === T_DOC_COMMENT_OPEN_TAG) { + $i = $tokens[$i]['comment_closer']; + continue; + } + + // skip heredoc/nowdoc + if ($tokens[$i]['code'] === T_START_HEREDOC + || $tokens[$i]['code'] === T_START_NOWDOC + ) { + $i = $tokens[$i]['scope_closer']; + continue; + } + + if (isset($extras[$i])) { + $extraIndent -= $extras[$i]; + unset($extras[$i]); + } + + // check if closing parenthesis is in the same line as control structure + if ($tokens[$i]['code'] === T_OPEN_CURLY_BRACKET + && isset($tokens[$i]['scope_condition']) + && ($scopeCondition = $tokens[$tokens[$i]['scope_condition']]) + && ! in_array($scopeCondition['code'], [T_FUNCTION, T_CLOSURE], true) + && ($parenthesis = $phpcsFile->findPrevious(Tokens::$emptyTokens, $i - 1, null, true)) + && $tokens[$parenthesis]['code'] === T_CLOSE_PARENTHESIS + && $tokens[$parenthesis]['line'] > $scopeCondition['line'] + ) { + $prev = $phpcsFile->findPrevious( + Tokens::$emptyTokens + [T_CLOSE_PARENTHESIS => T_CLOSE_PARENTHESIS], + $parenthesis - 1, + null, + true + ); + if ($scopeCondition['line'] === $tokens[$prev]['line']) { + $error = 'Closing parenthesis must be in the same line as control structure.'; + $fix = $phpcsFile->addFixableError($error, $parenthesis, 'UnnecessaryLineBreak'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($j = $prev + 1; $j < $parenthesis; ++$j) { + if ($tokens[$j]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($j, ''); + } + } + $phpcsFile->fixer->endChangeset(); + } + } + } + + // closing parenthesis in next line when multi-line control structure + if ($tokens[$i]['code'] === T_CLOSE_PARENTHESIS + && $tokens[$i]['line'] > $tokens[$tokens[$i]['parenthesis_opener']]['line'] + ) { + $prev = $phpcsFile->findPrevious( + Tokens::$emptyTokens + + [ + T_CLOSE_SHORT_ARRAY => T_CLOSE_SHORT_ARRAY, + T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET, + T_CLOSE_PARENTHESIS => T_CLOSE_PARENTHESIS, + ], + $i - 1, + null, + true + ); + + $owner = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + $tokens[$i]['parenthesis_opener'] - 1, + null, + true + ); + if ($tokens[$prev]['line'] === $tokens[$i]['line'] + && in_array($tokens[$owner]['code'], $this->functionToken, true) + && $this->hasContainNewLine( + $phpcsFile, + $tokens[$i]['parenthesis_opener'], + $tokens[$i]['parenthesis_closer'] + ) + ) { + $error = 'Closing parenthesis must be in next line'; + $fix = $phpcsFile->addFixableError($error, $i, 'ClosingParenthesis'); + + if ($fix) { + $phpcsFile->fixer->addNewlineBefore($i); + } + } + + if (isset($tokens[$owner]['scope_condition'])) { + $scopeCondition = $tokens[$owner]; + $prev = $i; + while (($prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $prev - 1, null, true)) + && $tokens[$prev]['code'] === T_CLOSE_PARENTHESIS + && $tokens[$prev]['line'] > $scopeCondition['line'] + && $tokens[$tokens[$prev]['parenthesis_opener']]['line'] === $scopeCondition['line'] + && ! $phpcsFile->findFirstOnLine( + Tokens::$emptyTokens + [T_CLOSE_PARENTHESIS => T_CLOSE_PARENTHESIS], + $prev, + true + ) + ) { + if ($tokens[$prev]['line'] <= $tokens[$i]['line'] - 1) { + $error = 'Invalid closing parenthesis position.'; + $fix = $phpcsFile->addFixableError($error, $prev, 'InvalidClosingParenthesisPosition'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($j = $prev + 1; $j < $i; ++$j) { + if ($tokens[$j]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($j, ''); + } + } + $phpcsFile->fixer->endChangeset(); + } + } elseif ($tokens[$prev + 1]['code'] === T_WHITESPACE) { + $error = 'Unexpected whitespace before closing parenthesis.'; + $fix = $phpcsFile->addFixableError( + $error, + $prev + 1, + 'UnexpectedSpacesBeforeClosingParenthesis' + ); + + if ($fix) { + $phpcsFile->fixer->replaceToken($prev + 1, ''); + } + } + } + } + } + + if ($tokens[$i]['code'] === T_DOUBLE_ARROW) { + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $i - 1, null, true); + + if ($tokens[$prev]['line'] === $tokens[$i]['line']) { + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $i + 1, null, true); + + if ($tokens[$next]['code'] === T_OPEN_PARENTHESIS + && $tokens[$tokens[$next]['parenthesis_closer']]['line'] > $tokens[$next]['line'] + ) { + $after = $phpcsFile->findNext( + Tokens::$emptyTokens, + $tokens[$next]['parenthesis_closer'] + 1, + null, + true + ); + if ($tokens[$after]['code'] === T_OBJECT_OPERATOR) { + $newEI = $this->indent; + $extraIndent += $newEI; + if (isset($extras[$after])) { + $extras[$after] += $newEI; + } else { + $extras[$after] = $newEI; + } + } + } + } + } + + if ($tokens[$i]['code'] === T_OBJECT_OPERATOR) { + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $i - 1, null, true); + + if ($tokens[$prev]['line'] === $tokens[$i]['line']) { + if (($prevObjectOperator = $this->hasPrevObjectOperator($phpcsFile, $i)) + && $tokens[$prevObjectOperator]['line'] < $tokens[$i]['line'] + ) { + // add line break before + $error = 'Object operator must be in new line'; + $fix = $phpcsFile->addFixableError($error, $i, 'ObjectOperator'); + + if ($fix) { + $phpcsFile->fixer->addNewlineBefore($i); + } + } + + $next = $phpcsFile->findNext( + Tokens::$emptyTokens + [ + T_STRING, + T_VARIABLE, + T_OPEN_CURLY_BRACKET, + T_CLOSE_CURLY_BRACKET, + ], + $i + 1, + null, + true + ); + + if ($tokens[$next]['code'] === T_OPEN_PARENTHESIS + && $tokens[$tokens[$next]['parenthesis_closer']]['line'] > $tokens[$next]['line'] + ) { + $after = $phpcsFile->findNext( + Tokens::$emptyTokens, + $tokens[$next]['parenthesis_closer'] + 1, + null, + true + ); + if ($tokens[$after]['code'] === T_OBJECT_OPERATOR) { + $column = $tokens[$i]['column']; + $newEI = $column - 1 - $extraIndent - $tokens[$i]['level'] * $this->indent; + $extraIndent += $newEI; + if (isset($extras[$after])) { + $extras[$after] += $newEI; + } else { + $extras[$after] = $newEI; + } + } + } + } + } + + if ($tokens[$i]['column'] === 1 + && ($next = $phpcsFile->findNext(T_WHITESPACE, $i, null, true)) + && $tokens[$next]['line'] === $tokens[$i]['line'] + ) { + $depth = $tokens[$next]['level']; + + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, $i - 1, null, true); + + $expectedIndent = $depth * $this->indent; + if (in_array($tokens[$next]['code'], $this->caseEndToken, true) + && isset($tokens[$next]['scope_closer']) + && $tokens[$next]['scope_closer'] === $next + ) { + $endOfStatement = $phpcsFile->findEndOfStatement($next); + if (isset($extras[$endOfStatement])) { + $extras[$endOfStatement] += $this->indent; + } else { + $extras[$endOfStatement] = $this->indent; + } + + $extraIndent += $this->indent; + } elseif ($tokens[$next]['code'] === T_CLOSE_PARENTHESIS) { + if (isset($extras[$next])) { + $extraIndent -= $extras[$next]; + unset($extras[$next]); + } + + $opener = $tokens[$next]['parenthesis_opener']; + $owner = $phpcsFile->findPrevious(Tokens::$emptyTokens, $opener - 1, null, true); + + // if it is not a function call + // and not a control structure + if (! in_array($tokens[$owner]['code'], $this->functionToken, true)) { + $error = 'Closing parenthesis must be in previous line.'; + $fix = $phpcsFile->addFixableError($error, $next, 'ClosingParenthesis'); + + if ($fix) { + $semicolon = $phpcsFile->findNext(Tokens::$emptyTokens, $next + 1, null, true); + + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($next, ''); + $phpcsFile->fixer->addContent($prev, $tokens[$next]['content']); + if ($tokens[$semicolon]['code'] === T_SEMICOLON) { + $phpcsFile->fixer->addContent($prev, ';'); + $phpcsFile->fixer->replaceToken($semicolon, ''); + $j = $semicolon + 1; + } else { + $j = $next + 1; + } + while ($tokens[$j]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($j, ''); + ++$j; + + if ($tokens[$j]['line'] > $tokens[$next]['line']) { + break; + } + } + $phpcsFile->fixer->endChangeset(); + } + + continue; + } + } elseif ($tokens[$next]['code'] === T_CLOSE_SHORT_ARRAY) { + if (isset($extras[$next])) { + $extraIndent -= $extras[$next]; + unset($extras[$next]); + } + } elseif ($tokens[$next]['code'] === T_OBJECT_OPERATOR) { + if (isset($extras[$next])) { + $extraIndent -= $extras[$next]; + unset($extras[$next]); + } + + $np = $this->np($phpcsFile, $next); + if ($fp = $this->fp($phpcsFile, $next)) { + $newEI = $fp - 1 - $expectedIndent - $extraIndent; + $extraIndent += $newEI; + if (isset($extras[$np])) { + $extras[$np] += $newEI; + } else { + $extras[$np] = $newEI; + } + } else { + $extraIndent += $this->indent; + if (isset($extras[$np])) { + $extras[$np] += $this->indent; + } else { + $extras[$np] = $this->indent; + } + } + } elseif ($tokens[$next]['code'] === T_INLINE_THEN) { + $expectedIndent = $previousIndent - $extraIndent + $this->indent; + } elseif ($tokens[$next]['code'] === T_INLINE_ELSE) { + $count = 0; + $t = $i; + while ($t = $phpcsFile->findPrevious([T_INLINE_THEN, T_INLINE_ELSE], $t - 1)) { + if ($tokens[$t]['code'] === T_INLINE_ELSE) { + ++$count; + } else { + --$count; + + if ($count < 0) { + break; + } + } + } + + $first = $phpcsFile->findFirstOnLine([], $t, true); + if ($tokens[$first]['code'] !== T_WHITESPACE) { + $expectedIndent = $this->indent; + } else { + $expectedIndent = strlen($tokens[$first]['content']) - $extraIndent; + + $firstNonEmpty = $phpcsFile->findFirstOnLine(Tokens::$emptyTokens, $t, true); + if ($t !== $firstNonEmpty) { + $expectedIndent += $this->indent; + } + } + } elseif (! in_array($tokens[$prev]['code'], $this->endOfStatement, true) + && $tokens[$next]['code'] !== T_OPEN_CURLY_BRACKET + ) { + if ($this->getControlStructurePtr($phpcsFile, $next) !== false) { + $addIndent = $expectedIndent + $extraIndent - $this->indent <= $previousIndent + && ! in_array($tokens[$next]['code'], Tokens::$booleanOperators, true); + } else { + $addIndent = $expectedIndent + $extraIndent <= $previousIndent; + } + + if ($addIndent) { + $expectedIndent = ($depth + 1) * $this->indent; + } + } + + $expectedIndent += $extraIndent; + $previousIndent = $expectedIndent; + + if ($tokens[$i]['code'] === T_WHITESPACE + && strpos($tokens[$i]['content'], $phpcsFile->eolChar) === false + && strlen($tokens[$i]['content']) !== $expectedIndent + ) { + $error = 'Invalid indent. Expected %d spaces, found %d'; + $data = [ + $expectedIndent, + strlen($tokens[$i]['content']), + ]; + $fix = $phpcsFile->addFixableError($error, $i, 'InvalidIndent', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken($i, str_repeat(' ', max($expectedIndent, 0))); + } + } elseif ($tokens[$i]['code'] === T_COMMENT + && preg_match('/^(\s*)\*/', $tokens[$i]['content'], $match) + ) { + if (strlen($match[1]) !== $expectedIndent + 1) { + $error = 'Invalid comment indent. Expected %d spaces, found %d'; + $data = [ + $expectedIndent + 1, + strlen($match[1]), + ]; + $fix = $phpcsFile->addFixableError($error, $i, 'CommentIndent', $data); + + if ($fix) { + $phpcsFile->fixer->replaceToken( + $i, + str_repeat(' ', max($expectedIndent, 0) + 1) . ltrim($tokens[$i]['content']) + ); + } + } + } elseif ($tokens[$i]['code'] !== T_WHITESPACE + && $expectedIndent + && ($tokens[$i]['code'] !== T_COMMENT + || preg_match('/^\s*(\/\/|#)/', $tokens[$i]['content'])) + ) { + $error = 'Missing indent. Expected %d spaces'; + $data = [$expectedIndent]; + $fix = $phpcsFile->addFixableError($error, $i, 'MissingIndent', $data); + + if ($fix) { + $phpcsFile->fixer->addContentBefore($i, str_repeat(' ', max($expectedIndent, 0))); + } + } + } + + // count extra indent + if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS + || $tokens[$i]['code'] === T_OPEN_SHORT_ARRAY + || ($tokens[$i]['code'] === T_OPEN_CURLY_BRACKET + && isset($tokens[$i]['scope_closer'])) + ) { + switch ($tokens[$i]['code']) { + case T_OPEN_PARENTHESIS: + $key = 'parenthesis_closer'; + break; + case T_OPEN_SHORT_ARRAY: + $key = 'bracket_closer'; + break; + default: + $key = 'scope_closer'; + break; + } + $xEnd = $tokens[$i][$key]; + + // no extra indent if there is no new line between open and close brackets + if (! $this->hasContainNewLine($phpcsFile, $i, $xEnd)) { + continue; + } + + // If open parenthesis belongs to control structure + if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS + && isset($tokens[$i]['parenthesis_owner']) + && in_array($tokens[$tokens[$i]['parenthesis_owner']]['code'], $this->controlStructures, true) + ) { + // search for first non-empty token in line, + // where is the closing parenthesis of the control structure + $firstOnLine = $phpcsFile->findFirstOnLine(Tokens::$emptyTokens, $xEnd, true); + + $extraIndent += $this->indent; + if (isset($extras[$firstOnLine])) { + $extras[$firstOnLine] += $this->indent; + } else { + $extras[$firstOnLine] = $this->indent; + } + + $controlStructure[$tokens[$i]['line']] = $tokens[$i]['parenthesis_closer']; + + continue; + } + + // If there is another open bracket in the current line, + // and closing bracket is in the same line as closing bracket of the current token + // (or there is no no line break between them) + // skip the current token to count indent. + $another = $i; + $openTags = [T_OPEN_PARENTHESIS, T_OPEN_SHORT_ARRAY, T_OPEN_CURLY_BRACKET]; + while (($another = $phpcsFile->findNext($openTags, $another + 1)) + && $tokens[$another]['line'] === $tokens[$i]['line'] + ) { + if (($tokens[$another]['code'] === T_OPEN_PARENTHESIS + && $tokens[$tokens[$another]['parenthesis_closer']]['line'] > $tokens[$another]['line'] + && ! $this->hasContainNewLine($phpcsFile, $tokens[$another]['parenthesis_closer'], $xEnd)) + || ($tokens[$another]['code'] === T_OPEN_SHORT_ARRAY + && $tokens[$tokens[$another]['bracket_closer']]['line'] > $tokens[$another]['line'] + && ! $this->hasContainNewLine($phpcsFile, $tokens[$another]['bracket_closer'], $xEnd)) + || ($tokens[$another]['code'] === T_OPEN_CURLY_BRACKET + && isset($tokens[$another]['scope_closer']) + && $tokens[$tokens[$another]['scope_closer']]['line'] > $tokens[$another]['line'] + && ! $this->hasContainNewLine($phpcsFile, $tokens[$another]['scope_closer'], $xEnd)) + ) { + continue 2; + } + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $i, true); + + $firstInNextLine = $i; + while ($tokens[$firstInNextLine]['line'] === $tokens[$i]['line'] + || $tokens[$firstInNextLine]['code'] === T_WHITESPACE + ) { + ++$firstInNextLine; + } + + $ei1 = 0; + if ($tokens[$first]['level'] === $tokens[$firstInNextLine]['level'] + && $tokens[$firstInNextLine]['code'] !== T_CLOSE_CURLY_BRACKET + ) { + $ei1 = $this->indent; + if (isset($extras[$xEnd])) { + $extras[$xEnd] += $ei1; + } else { + $extras[$xEnd] = $ei1; + } + } + + $ei2 = 0; + $next = $phpcsFile->findNext(Tokens::$emptyTokens, $i + 1, null, true); + if ($tokens[$next]['line'] > $tokens[$i]['line']) { + // current line indent + $whitespace = $phpcsFile->findFirstOnLine([], $i, true); + if ($tokens[$whitespace]['code'] === T_WHITESPACE) { + $sum = strlen($tokens[$whitespace]['content']) + - $tokens[$first]['level'] * $this->indent + - $extraIndent; + + if ($sum > 0) { + $ei2 = $sum; + if (isset($extras[$xEnd + 1])) { + $extras[$xEnd + 1] += $ei2; + } else { + $extras[$xEnd + 1] = $ei2; + } + } + } + } + + $extraIndent += $ei1 + $ei2; + } + } + + return $phpcsFile->numTokens + 1; + } + + /** + * @todo: need name refactor and method description + */ + private function fp(File $phpcsFile, int $ptr) : ?int + { + if ($this->alignObjectOperators) { + $tokens = $phpcsFile->getTokens(); + + while (--$ptr) { + if ($tokens[$ptr]['code'] === T_CLOSE_PARENTHESIS) { + $ptr = $tokens[$ptr]['parenthesis_opener']; + } elseif ($tokens[$ptr]['code'] === T_CLOSE_CURLY_BRACKET + || $tokens[$ptr]['code'] === T_CLOSE_SHORT_ARRAY + || $tokens[$ptr]['code'] === T_CLOSE_SQUARE_BRACKET + ) { + $ptr = $tokens[$ptr]['bracket_opener']; + } elseif ($tokens[$ptr]['code'] === T_OBJECT_OPERATOR) { + return $tokens[$ptr]['column']; + } elseif (in_array( + $tokens[$ptr]['code'], + [T_SEMICOLON, T_OPEN_CURLY_BRACKET, T_OPEN_PARENTHESIS, T_OPEN_SHORT_ARRAY, T_OPEN_SQUARE_BRACKET], + true + )) { + break; + } + } + } + + return null; + } + + /** + * @todo: need name refactor and method description + */ + private function np(File $phpcsFile, int $ptr) : ?int + { + $tokens = $phpcsFile->getTokens(); + + while (++$ptr) { + if ($tokens[$ptr]['code'] === T_OPEN_PARENTHESIS) { + $ptr = $tokens[$ptr]['parenthesis_closer']; + } elseif ($tokens[$ptr]['code'] === T_OPEN_CURLY_BRACKET) { + $ptr = $tokens[$ptr]['bracket_closer']; + } elseif (in_array( + $tokens[$ptr]['code'], + [T_OBJECT_OPERATOR, T_SEMICOLON, T_CLOSE_PARENTHESIS, T_CLOSE_SHORT_ARRAY], + true + )) { + return $ptr; + } + } + + return null; + } + + /** + * Checks if there is another object operator + * before $ptr token. + */ + private function hasPrevObjectOperator(File $phpcsFile, int $ptr) : ?int + { + $tokens = $phpcsFile->getTokens(); + + while (--$ptr) { + if ($tokens[$ptr]['code'] === T_CLOSE_PARENTHESIS) { + $ptr = $tokens[$ptr]['parenthesis_opener']; + } elseif ($tokens[$ptr]['code'] === T_CLOSE_CURLY_BRACKET) { + $ptr = $tokens[$ptr]['bracket_opener']; + } elseif ($tokens[$ptr]['code'] === T_OBJECT_OPERATOR) { + return $ptr; + } elseif (in_array($tokens[$ptr]['code'], $this->breakToken, true)) { + break; + } + } + + return null; + } + + /** + * Checks if between $fromPtr and $toPtr is any new line + * excluding scopes (arrays, closures, multiline function calls). + */ + private function hasContainNewLine(File $phpcsFile, int $fromPtr, int $toPtr) : bool + { + $tokens = $phpcsFile->getTokens(); + + for ($j = $fromPtr + 1; $j < $toPtr; ++$j) { + switch ($tokens[$j]['code']) { + case T_OPEN_PARENTHESIS: + case T_ARRAY: + $j = $tokens[$j]['parenthesis_closer']; + continue 2; + case T_OPEN_CURLY_BRACKET: + if (isset($tokens[$j]['scope_closer'])) { + $j = $tokens[$j]['scope_closer']; + } + continue 2; + case T_OPEN_SHORT_ARRAY: + $j = $tokens[$j]['bracket_closer']; + continue 2; + case T_WHITESPACE: + if (strpos($tokens[$j]['content'], $phpcsFile->eolChar) !== false) { + return true; + } + } + } + + return false; + } + + /** + * Checks if the $ptr token is inside control structure + * and returns the control structure pointer; + * otherwise returns boolean `false`. + * + * @return false|int + */ + private function getControlStructurePtr(File $phpcsFile, int $ptr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$ptr]['nested_parenthesis'])) { + foreach ($tokens[$ptr]['nested_parenthesis'] as $start => $end) { + // find expression before + $prev = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + $start - 1, + null, + true + ); + + if (in_array($tokens[$prev]['code'], $this->controlStructures, true)) { + return $prev; + } + } + } + + return false; + } +} diff --git a/src/ZendCodingStandard/ruleset.xml b/src/ZendCodingStandard/ruleset.xml new file mode 100755 index 00000000..87771e2e --- /dev/null +++ b/src/ZendCodingStandard/ruleset.xml @@ -0,0 +1,76 @@ + + + Zend Framework Coding Standard + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Variable "%s" not allowed in double quoted string; use sprintf() instead. + + + + + + + + + + + + + + diff --git a/test/Ruleset.php b/test/Ruleset.php new file mode 100644 index 00000000..59f7e27d --- /dev/null +++ b/test/Ruleset.php @@ -0,0 +1,26 @@ + $bool) { + $newClassName = str_replace('php_codesniffer\\standards\\', '', $className); + unset($restrictions[$className]); + $restrictions[$newClassName] = $bool; + } + + parent::registerSniffs($files, $restrictions, $exclusions); + } +} diff --git a/test/Sniffs/Arrays/DoubleArrowUnitTest.inc b/test/Sniffs/Arrays/DoubleArrowUnitTest.inc new file mode 100644 index 00000000..ef2fb4e2 --- /dev/null +++ b/test/Sniffs/Arrays/DoubleArrowUnitTest.inc @@ -0,0 +1,10 @@ + 'b', 'c'=>'d']; +$m = [ + 'a' => + 'b', + 'c' + => 'd', + 'e' => 'f', +]; diff --git a/test/Sniffs/Arrays/DoubleArrowUnitTest.inc.fixed b/test/Sniffs/Arrays/DoubleArrowUnitTest.inc.fixed new file mode 100644 index 00000000..ce1f6919 --- /dev/null +++ b/test/Sniffs/Arrays/DoubleArrowUnitTest.inc.fixed @@ -0,0 +1,10 @@ + 'b', 'c'=>'d']; +$m = [ + 'a' + => 'b', + 'c' + => 'd', + 'e' => 'f', +]; diff --git a/test/Sniffs/Arrays/DoubleArrowUnitTest.php b/test/Sniffs/Arrays/DoubleArrowUnitTest.php new file mode 100644 index 00000000..05bb16ff --- /dev/null +++ b/test/Sniffs/Arrays/DoubleArrowUnitTest.php @@ -0,0 +1,24 @@ + 1, + 5 => 1, + 9 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Arrays/FormatUnitTest.inc b/test/Sniffs/Arrays/FormatUnitTest.inc new file mode 100644 index 00000000..4e33020f --- /dev/null +++ b/test/Sniffs/Arrays/FormatUnitTest.inc @@ -0,0 +1,82 @@ + 1, 2 => 1, + 3 => 2, 4 => 3, + 5 => 5, +]; + +$arr = [ + + 'elem', 'elem', + +]; + +$a = array_merge([ + ['a' => 'b'], + ['c' => 'd'], + ]); diff --git a/test/Sniffs/Arrays/FormatUnitTest.inc.fixed b/test/Sniffs/Arrays/FormatUnitTest.inc.fixed new file mode 100644 index 00000000..aa8fb246 --- /dev/null +++ b/test/Sniffs/Arrays/FormatUnitTest.inc.fixed @@ -0,0 +1,95 @@ + 1, +2 => 1, + 3 => 2, +4 => 3, + 5 => 5, +]; + +$arr = [ + 'elem', +'elem', +]; + +$a = array_merge([ + ['a' => 'b'], + ['c' => 'd'], + ]); diff --git a/test/Sniffs/Arrays/FormatUnitTest.php b/test/Sniffs/Arrays/FormatUnitTest.php new file mode 100644 index 00000000..273b89d6 --- /dev/null +++ b/test/Sniffs/Arrays/FormatUnitTest.php @@ -0,0 +1,44 @@ + 2, + 14 => 2, + 16 => 2, + 19 => 1, + 20 => 1, + 22 => 1, + 25 => 1, + 31 => 3, + 33 => 1, + 38 => 1, + 39 => 1, + 47 => 2, + 49 => 1, + 53 => 2, + 55 => 1, + 56 => 1, + 62 => 2, + 63 => 1, + 68 => 1, + 69 => 1, + 74 => 1, + 75 => 1, + 76 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Arrays/TrailingArrayCommaUnitTest.inc b/test/Sniffs/Arrays/TrailingArrayCommaUnitTest.inc new file mode 100644 index 00000000..719c5769 --- /dev/null +++ b/test/Sniffs/Arrays/TrailingArrayCommaUnitTest.inc @@ -0,0 +1,26 @@ + 'bar' +]; +$a6 = [ + 'foo', 'bar', 'baz' +]; +$a7 = [ + ['a1'], + ['a2'], + ['a3'] +]; + +$s = ['a',/**/]; +$s1 = ['a' => 'b', ]; diff --git a/test/Sniffs/Arrays/TrailingArrayCommaUnitTest.inc.fixed b/test/Sniffs/Arrays/TrailingArrayCommaUnitTest.inc.fixed new file mode 100644 index 00000000..ef3a8a6b --- /dev/null +++ b/test/Sniffs/Arrays/TrailingArrayCommaUnitTest.inc.fixed @@ -0,0 +1,26 @@ + 'bar', +]; +$a6 = [ + 'foo', 'bar', 'baz', +]; +$a7 = [ + ['a1'], + ['a2'], + ['a3'], +]; + +$s = ['a'/**/]; +$s1 = ['a' => 'b' ]; diff --git a/test/Sniffs/Arrays/TrailingArrayCommaUnitTest.php b/test/Sniffs/Arrays/TrailingArrayCommaUnitTest.php new file mode 100644 index 00000000..064a88ae --- /dev/null +++ b/test/Sniffs/Arrays/TrailingArrayCommaUnitTest.php @@ -0,0 +1,27 @@ + 1, + 14 => 1, + 17 => 1, + 22 => 1, + 25 => 1, + 26 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Classes/AlphabeticallySortedTraitsUnitTest.inc b/test/Sniffs/Classes/AlphabeticallySortedTraitsUnitTest.inc new file mode 100644 index 00000000..6ac1d5e3 --- /dev/null +++ b/test/Sniffs/Classes/AlphabeticallySortedTraitsUnitTest.inc @@ -0,0 +1,41 @@ + 1, + 36 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Classes/ConstVisibilityUnitTest.inc b/test/Sniffs/Classes/ConstVisibilityUnitTest.inc new file mode 100644 index 00000000..f8bed8f9 --- /dev/null +++ b/test/Sniffs/Classes/ConstVisibilityUnitTest.inc @@ -0,0 +1,30 @@ + 1, + 15 => 1, + 23 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Classes/NoNullValuesUnitTest.inc b/test/Sniffs/Classes/NoNullValuesUnitTest.inc new file mode 100644 index 00000000..daa6ef9f --- /dev/null +++ b/test/Sniffs/Classes/NoNullValuesUnitTest.inc @@ -0,0 +1,28 @@ + 1, + 7 => 1, + 9 => 1, + 11 => 1, + 13 => 1, + 23 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Classes/TraitUsageUnitTest.inc b/test/Sniffs/Classes/TraitUsageUnitTest.inc new file mode 100644 index 00000000..b04e049a --- /dev/null +++ b/test/Sniffs/Classes/TraitUsageUnitTest.inc @@ -0,0 +1,40 @@ + 1, + 11 => 1, + 12 => 1, + 15 => 2, + 17 => 1, + 20 => 2, + 22 => 1, + 25 => 3, + 26 => 4, + 35 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Commenting/CodingStandardTagsUnitTest.inc b/test/Sniffs/Commenting/CodingStandardTagsUnitTest.inc new file mode 100644 index 00000000..906a8b1f --- /dev/null +++ b/test/Sniffs/Commenting/CodingStandardTagsUnitTest.inc @@ -0,0 +1,40 @@ + 1, + 4 => 1, + 5 => 1, + 6 => 1, + 7 => 1, + 11 => 1, + 15 => 1, + 19 => 1, + 23 => 1, + 27 => 1, + 30 => 1, + 31 => 1, + 32 => 1, + 33 => 1, + 34 => 1, + 36 => 1, + 37 => 1, + 38 => 1, + 39 => 1, + 40 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Commenting/DocCommentUnitTest.1.inc b/test/Sniffs/Commenting/DocCommentUnitTest.1.inc new file mode 100644 index 00000000..b2bd77c9 --- /dev/null +++ b/test/Sniffs/Commenting/DocCommentUnitTest.1.inc @@ -0,0 +1,44 @@ + 1, + 8 => 1, + 13 => 1, + 18 => 1, + 23 => 1, + 26 => 1, + 27 => 1, + 28 => 1, + 31 => 1, + 38 => 1, + 42 => 1, + ]; + case 'DocCommentUnitTest.2.inc': + return [ + 3 => 1, + 9 => 1, + 14 => 1, + 19 => 1, + ]; + case 'DocCommentUnitTest.3.inc': + return [ + 1 => 1, + 5 => 1, + 9 => 1, + 14 => 1, + 18 => 1, + ]; + case 'DocCommentUnitTest.4.inc': + return [ + 4 => 1, + 10 => 1, + 14 => 1, + 16 => 1, + 21 => 1, + 25 => 1, + 30 => 1, + ]; + case 'DocCommentUnitTest.5.inc': + return [ + 3 => 1, + 4 => 2, + 9 => 1, + 11 => 1, + 14 => 1, + 15 => 1, + 16 => 1, + 19 => 1, + ]; + case 'DocCommentUnitTest.6.inc': + return [ + 2 => 2, + 8 => 1, + 11 => 1, + 14 => 2, + 17 => 1, + 20 => 2, + ]; + case 'DocCommentUnitTest.7.inc': + return [ + 2 => 1, + 9 => 1, + 13 => 1, + ]; + case 'DocCommentUnitTest.8.inc': + return [ + 3 => 1, + 8 => 1, + 12 => 1, + 16 => 1, + ]; + case 'DocCommentUnitTest.9.inc': + return [ + 4 => 1, + 10 => 1, + 13 => 1, + 18 => 1, + 25 => 1, + 27 => 1, + 32 => 1, + ]; + } + + return [ + 3 => 1, + 8 => 1, + 10 => 1, + 11 => 1, + 12 => 1, + 13 => 1, + 16 => 1, + 17 => 1, + 25 => 1, + 26 => 1, + 29 => 1, + 32 => 1, + 35 => 1, + 36 => 1, + 37 => 1, + 42 => 1, + 45 => 1, + 48 => 1, + 53 => 2, + 55 => 1, + 59 => 1, + 62 => 1, + 65 => 1, + 70 => 1, + 73 => 1, + 75 => 1, + 79 => 1, + 80 => 1, + 81 => 1, + 84 => 1, + 89 => 1, + 90 => 1, + 94 => 1, + 96 => 1, + 99 => 1, + 103 => 1, + 104 => 1, + 110 => 1, + 111 => 1, + 127 => 1, + 133 => 1, + 138 => 1, + 142 => 2, + 143 => 2, + 145 => 2, + 146 => 2, + 150 => 2, + 158 => 2, + 161 => 1, + 162 => 1, + 164 => 1, + 170 => 1, + 179 => 1, + 180 => 1, + 183 => 1, + 184 => 1, + 185 => 1, + 192 => 1, + 203 => 1, + 214 => 1, + 215 => 1, + 216 => 1, + 217 => 1, + 218 => 1, + 223 => 1, + 229 => 1, + 236 => 1, + 241 => 1, + 246 => 1, + 252 => 1, + 253 => 1, + 255 => 1, + 256 => 1, + 258 => 1, + 259 => 1, + 260 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Commenting/FunctionCommentUnitTest.inc b/test/Sniffs/Commenting/FunctionCommentUnitTest.inc new file mode 100644 index 00000000..63c7fcc2 --- /dev/null +++ b/test/Sniffs/Commenting/FunctionCommentUnitTest.inc @@ -0,0 +1,98 @@ + 1, + 11 => 1, + 17 => 1, + 30 => 1, + 31 => 1, + 34 => 1, + 37 => 1, + 44 => 1, + 52 => 1, + 67 => 1, + 74 => 1, + 80 => 1, + 96 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Commenting/FunctionDataProviderTagUnitTest.inc b/test/Sniffs/Commenting/FunctionDataProviderTagUnitTest.inc new file mode 100644 index 00000000..2ff0e4eb --- /dev/null +++ b/test/Sniffs/Commenting/FunctionDataProviderTagUnitTest.inc @@ -0,0 +1,37 @@ + 1, + 16 => 1, + 19 => 1, + 25 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Commenting/FunctionDisallowedTagUnitTest.inc b/test/Sniffs/Commenting/FunctionDisallowedTagUnitTest.inc new file mode 100644 index 00000000..6042a343 --- /dev/null +++ b/test/Sniffs/Commenting/FunctionDisallowedTagUnitTest.inc @@ -0,0 +1,53 @@ + 1, + 13 => 1, + 14 => 1, + 15 => 1, + 16 => 1, + 17 => 1, + 19 => 1, + 20 => 1, + 30 => 1, + 31 => 1, + 32 => 1, + 33 => 1, + 35 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseUnitTest.inc b/test/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseUnitTest.inc new file mode 100644 index 00000000..4519fa8c --- /dev/null +++ b/test/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseUnitTest.inc @@ -0,0 +1,28 @@ + 0) { + if ($a) { + --$a; + } // end if + } // end while + } // end __construct + + public function method($a) + { + switch ($a) { + case 1: + if ($a > 1) { + ++$a; + } + // no break + case 2: + default: + } // end switch + + return $a; + } +} \ No newline at end of file diff --git a/test/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseUnitTest.php b/test/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseUnitTest.php new file mode 100644 index 00000000..038c01b8 --- /dev/null +++ b/test/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseUnitTest.php @@ -0,0 +1,25 @@ + 1, + 11 => 1, + 12 => 1, + 24 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Commenting/PhpcsAnnotationUnitTest.inc b/test/Sniffs/Commenting/PhpcsAnnotationUnitTest.inc new file mode 100644 index 00000000..3b9ca9af --- /dev/null +++ b/test/Sniffs/Commenting/PhpcsAnnotationUnitTest.inc @@ -0,0 +1,40 @@ + 1, + 4 => 1, + 5 => 1, + 6 => 1, + 7 => 1, + 11 => 1, + 15 => 1, + 19 => 1, + 23 => 1, + 27 => 1, + 30 => 1, + 31 => 1, + 32 => 1, + 33 => 1, + 34 => 1, + 36 => 1, + 37 => 1, + 38 => 1, + 39 => 1, + 40 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Commenting/TagCaseUnitTest.inc b/test/Sniffs/Commenting/TagCaseUnitTest.inc new file mode 100644 index 00000000..f4362085 --- /dev/null +++ b/test/Sniffs/Commenting/TagCaseUnitTest.inc @@ -0,0 +1,75 @@ + 1, + 5 => 1, + 6 => 1, + 7 => 1, + 8 => 1, + 9 => 1, + 10 => 1, + 11 => 1, + 12 => 1, + 13 => 1, + 14 => 1, + 15 => 1, + 16 => 1, + 17 => 1, + 18 => 1, + 19 => 1, + 20 => 1, + 21 => 1, + 22 => 1, + 23 => 1, + 24 => 1, + 25 => 1, + 26 => 1, + 27 => 1, + 28 => 1, + 29 => 1, + 30 => 1, + 31 => 1, + 32 => 1, + 33 => 1, + // PHPUnit Annotations + 37 => 1, + 38 => 1, + 39 => 1, + 40 => 1, + 41 => 1, + 42 => 1, + 43 => 1, + 44 => 1, + 45 => 1, + 46 => 1, + 47 => 1, + 48 => 1, + 49 => 1, + 50 => 1, + 51 => 1, + 52 => 1, + 53 => 1, + 54 => 1, + 55 => 1, + 56 => 1, + 57 => 1, + 58 => 1, + 59 => 1, + 60 => 1, + 61 => 1, + 62 => 1, + 63 => 1, + 64 => 1, + 65 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Commenting/TagWithTypeUnitTest.1.inc b/test/Sniffs/Commenting/TagWithTypeUnitTest.1.inc new file mode 100644 index 00000000..36e48549 --- /dev/null +++ b/test/Sniffs/Commenting/TagWithTypeUnitTest.1.inc @@ -0,0 +1,322 @@ + 1, + 17 => 1, + 22 => 1, + 27 => 1, + 32 => 1, + 47 => 2, + 52 => 1, + 57 => 1, + 62 => 1, + 67 => 1, + 72 => 1, + 77 => 1, + 82 => 1, + 87 => 1, + 92 => 1, + 107 => 1, + 112 => 1, + 117 => 1, + 122 => 2, + 127 => 1, + 132 => 1, + 137 => 1, + 142 => 1, + 147 => 1, + 152 => 1, + 157 => 1, + 162 => 1, + 167 => 1, + 172 => 1, + 177 => 1, + 182 => 1, + 187 => 1, + 192 => 1, + 202 => 1, + 207 => 1, + 212 => 1, + 217 => 1, + 222 => 1, + 227 => 1, + 232 => 1, + 237 => 1, + 242 => 1, + 252 => 1, + 272 => 1, + 277 => 1, + 282 => 1, + 301 => 1, + 309 => 1, + ]; + case 'TagWithTypeUnitTest.2.inc': + return [ + 12 => 1, + 22 => 1, + 27 => 1, + 32 => 1, + 47 => 2, + 52 => 1, + 57 => 1, + 62 => 1, + 67 => 1, + 72 => 1, + 77 => 1, + 82 => 1, + 87 => 1, + 92 => 1, + 107 => 1, + 112 => 1, + 117 => 1, + 122 => 1, + 127 => 1, + 132 => 1, + 137 => 1, + 142 => 1, + 147 => 1, + 152 => 1, + 157 => 1, + 162 => 1, + 167 => 1, + 172 => 1, + 177 => 1, + 182 => 1, + 187 => 1, + 202 => 1, + 207 => 1, + 212 => 2, + 222 => 1, + 227 => 1, + 232 => 1, + 237 => 1, + 242 => 1, + 247 => 1, + 252 => 1, + 257 => 1, + 262 => 1, + 267 => 1, + 277 => 1, + 282 => 1, + 287 => 1, + 292 => 1, + 297 => 2, + 302 => 2, + 307 => 1, + 327 => 1, + 332 => 1, + 337 => 1, + 353 => 1, + 361 => 1, + ]; + case 'TagWithTypeUnitTest.3.inc': + return [ + 12 => 1, + 22 => 1, + 27 => 1, + 32 => 1, + 47 => 2, + 52 => 1, + 57 => 1, + 62 => 1, + 67 => 1, + 72 => 1, + 77 => 1, + 82 => 1, + 87 => 1, + 92 => 1, + 107 => 1, + 112 => 1, + 117 => 1, + 122 => 2, + 127 => 1, + 132 => 1, + 137 => 1, + 142 => 1, + 147 => 1, + 152 => 1, + 157 => 1, + 162 => 1, + 167 => 1, + 172 => 1, + 177 => 1, + 182 => 1, + 192 => 1, + 197 => 1, + 202 => 1, + 207 => 1, + 212 => 1, + 217 => 1, + 222 => 1, + 227 => 1, + 232 => 1, + 242 => 1, + 255 => 1, + 258 => 1, + 261 => 1, + 265 => 1, + 269 => 1, + 272 => 1, + 281 => 1, + 282 => 1, + 290 => 1, + 295 => 1, + 299 => 1, + 304 => 1, + ]; + } + + return []; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Commenting/VariableCommentUnitTest.inc b/test/Sniffs/Commenting/VariableCommentUnitTest.inc new file mode 100644 index 00000000..3732309c --- /dev/null +++ b/test/Sniffs/Commenting/VariableCommentUnitTest.inc @@ -0,0 +1,56 @@ + 1, + 9 => 1, + 11 => 1, + 29 => 1, + 34 => 1, + 43 => 1, + 51 => 2, + 54 => 2, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Files/DeclareStrictTypesUnitTest.1.inc b/test/Sniffs/Files/DeclareStrictTypesUnitTest.1.inc new file mode 100644 index 00000000..89fa6c62 --- /dev/null +++ b/test/Sniffs/Files/DeclareStrictTypesUnitTest.1.inc @@ -0,0 +1,9 @@ + 1]; + case 'DeclareStrictTypesUnitTest.3.inc': + return [7 => 1]; + case 'DeclareStrictTypesUnitTest.4.inc': + return [7 => 1]; + case 'DeclareStrictTypesUnitTest.5.inc': + return [8 => 1]; + case 'DeclareStrictTypesUnitTest.6.inc': + return [12 => 1]; + } + + return []; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Formatting/DoubleColonUnitTest.inc b/test/Sniffs/Formatting/DoubleColonUnitTest.inc new file mode 100644 index 00000000..7aa29d57 --- /dev/null +++ b/test/Sniffs/Formatting/DoubleColonUnitTest.inc @@ -0,0 +1,33 @@ + 2, + 7 => 2, + 10 => 2, + // 14 => 2, // double colon is preceded by and followed by comments + 18 => 2, + 24 => 2, + 31 => 2, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Formatting/HeredocUnitTest.inc b/test/Sniffs/Formatting/HeredocUnitTest.inc new file mode 100644 index 00000000..68a8dd6b --- /dev/null +++ b/test/Sniffs/Formatting/HeredocUnitTest.inc @@ -0,0 +1,31 @@ + 1, + 6 => 1, + 9 => 1, + 12 => 1, + 21 => 1, + 24 => 1, + 27 => 2, + 30 => 2, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Formatting/NewKeywordUnitTest.inc b/test/Sniffs/Formatting/NewKeywordUnitTest.inc new file mode 100644 index 00000000..df26b507 --- /dev/null +++ b/test/Sniffs/Formatting/NewKeywordUnitTest.inc @@ -0,0 +1,18 @@ + 1, // not checking next character after space + 6 => 1, + 8 => 1, + 10 => 1, + 14 => 1, + 16 => 1, + 18 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Formatting/NoSpaceAfterSplatUnitTest.inc b/test/Sniffs/Formatting/NoSpaceAfterSplatUnitTest.inc new file mode 100644 index 00000000..b0f8cd14 --- /dev/null +++ b/test/Sniffs/Formatting/NoSpaceAfterSplatUnitTest.inc @@ -0,0 +1,18 @@ + 1, + 5 => 1, + 11 => 1, + 13 => 1, + // 18 => 1, // we are not checking what it the next character after splat op + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Formatting/ReferenceUnitTest.inc b/test/Sniffs/Formatting/ReferenceUnitTest.inc new file mode 100644 index 00000000..dd5bfd87 --- /dev/null +++ b/test/Sniffs/Formatting/ReferenceUnitTest.inc @@ -0,0 +1,15 @@ + 1, + 5 => 1, + 6 => 2, + 7 => 1, + 8 => 1, + 13 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Formatting/ReturnTypeUnitTest.1.inc b/test/Sniffs/Formatting/ReturnTypeUnitTest.1.inc new file mode 100644 index 00000000..1880e09e --- /dev/null +++ b/test/Sniffs/Formatting/ReturnTypeUnitTest.1.inc @@ -0,0 +1,22 @@ + 1, + 7 => 3, + 9 => 2, + 10 => 2, + 17 => 2, + 18 => 3, + 20 => 1, + 21 => 2, + 22 => 1, + ]; + } + + return [ + 8 => 1, + 12 => 1, + 30 => 2, + 39 => 1, + 45 => 2, + 53 => 2, + 58 => 2, + 59 => 2, + 60 => 2, + 61 => 3, + 62 => 3, + 63 => 2, + 64 => 1, + 65 => 2, + 66 => 1, + 67 => 2, + 68 => 1, + 69 => 2, + 70 => 1, + 71 => 2, + 72 => 1, + 73 => 2, + 74 => 1, + 75 => 2, + 76 => 1, + 77 => 2, + 78 => 1, + 80 => 2, + 81 => 1, + 83 => 1, + 84 => 3, + 89 => 1, + 92 => 1, + 94 => 1, + 96 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Formatting/UnnecessaryParenthesesUnitTest.inc b/test/Sniffs/Formatting/UnnecessaryParenthesesUnitTest.inc new file mode 100644 index 00000000..d3099646 --- /dev/null +++ b/test/Sniffs/Formatting/UnnecessaryParenthesesUnitTest.inc @@ -0,0 +1,168 @@ +method2((1 + 2), 2, 3); + $arr = [ + (1 + 2) => 14, + (2 + 3) * 2 => (1 + (4 * 3)), + 34 % (2) => 1 / (2 * 3), + 8 - (1) => 7 * (4), + 1 * (4 - 2) => 2, + (11) << (2) => (5) | (6 - 2), + ]; + sort($arr); + + $i1 = new self(); + $i2 = new static(); + $i3 = new DateTime(); + + $anonym = new class() extends UnnecessaryParentheses { + }; + + $v1 = new $anonym(); + $v2 = $arr(); + $v3 = static::method4(); + $v5 = self::method4(); + $v6 = $this->{$arr}($i1); + + $i = (32 * 1); + $k = ($i + 1) << 3; + $j = ($k) ? ($i + 2) : ($i - 2) / 2; + + return('hello'); + } + + public function method2($x, $y, $z) + { + switch ($x) { + case (true): + break; + case (1): + break; + } + + return (($x || $y) && ($z)); + } + + public function method3($x, $y, $z) + { + return ($x && $y) || ($z); + } + + public static function method4() + { + for ($i = (1 + 2); ($i < 15); (++$i)) { + $i /= (2.5 + 2); + echo($i << 3); + } + + echo ($i), ($i - 1), ($i + 1), ($i - 1) * 2; + + return (function() use ($i) { + return ('foo'); + }); + } + + public function method5() + { + return (function() { + return('bar'); + })(); + } + + public function method6() + { + if (isset($x)) { + if (empty($y)) { + unset($x); + unset($a, $b); + } + + exit($x); + } + + $c = eval('function(){}'); + + return (1 + 2); + } + + private function method7($x) + { + while (($x)) { + $x = abs((--$x)); + if (($x) % 19) { + die($x); + } + } + + return (1 + 2) * 3; + } + + protected function method8($x, $y) + { + if (! ($x || $y)) { + if (! ($x) || ! ($x + $y)) { + echo $x, ($y); + } + } + + $z = ! ($x instanceof DateTime || ($x) instanceof ArrayAccess); + + return(1 / ($z ? 2 : 3)); + } + + public function method9($x, $y) + { + if (false === ($a = strpos($x, $y))) { + return (3 ^ $a); + } + + if (($b = strpos($x, $y)) != false) { + return [$a, ($b - 2)]; + } + + $h = (int) ($a instanceof DateTime); + $m = (int) ($a + $b); + $n = (float) ($b); + $w = (bool) ($x ^ $y); + $z = (null === ($x ?: null)); + $q = 'string' + . (! empty($x) ? ' ' . $x : '') + . (! empty($y) ? ' ' . $y : ''); + $r = 1 + ($x ? 2 : 3); + $s = (! (($x instanceof DateTime) || ($x instanceof ArrayAccess))); + $p = $x ?: ($y ?: (2 + 1)); + $o = $x ? ($y ?: 1) : ($x - 1); + + return(! ($b || $a) || ($a - $b)); + } + + public function method10($a, $b, $c) + { + if (($a || $z = strpos($b, $c)) === false) { + $z = clone($a); + } + + $x = (int) ($a++); + $y = ! ($z instanceof DateTime); + $z = 'string ' . (--$a); + $w = 7 !== (++$b); + + list($var) = explode(',', '1,2,3'); + + $date = (new DateTime())->modify('+1 year'); + + $a = $b ? ($c ?? 1) : 0; + $a = $b ? 0 : ($c ?? 1); + + new $arr['abc']['def'](); + + $r = $a === ($b + $c) * $d; + $r = $a === $d * ($b + $c); + $r = $a === $b + $c; + $r = $a === ($b + $c); + } +} diff --git a/test/Sniffs/Formatting/UnnecessaryParenthesesUnitTest.inc.fixed b/test/Sniffs/Formatting/UnnecessaryParenthesesUnitTest.inc.fixed new file mode 100644 index 00000000..843ddcbb --- /dev/null +++ b/test/Sniffs/Formatting/UnnecessaryParenthesesUnitTest.inc.fixed @@ -0,0 +1,168 @@ +method2(1 + 2, 2, 3); + $arr = [ + 1 + 2 => 14, + (2 + 3) * 2 => 1 + (4 * 3), + 34 % 2 => 1 / (2 * 3), + 8 - 1 => 7 * 4, + 1 * (4 - 2) => 2, + 11 << 2 => 5 | (6 - 2), + ]; + sort($arr); + + $i1 = new self(); + $i2 = new static(); + $i3 = new DateTime(); + + $anonym = new class() extends UnnecessaryParentheses { + }; + + $v1 = new $anonym(); + $v2 = $arr(); + $v3 = static::method4(); + $v5 = self::method4(); + $v6 = $this->{$arr}($i1); + + $i = 32 * 1; + $k = ($i + 1) << 3; + $j = $k ? $i + 2 : ($i - 2) / 2; + + return 'hello'; + } + + public function method2($x, $y, $z) + { + switch ($x) { + case true: + break; + case 1: + break; + } + + return ($x || $y) && $z; + } + + public function method3($x, $y, $z) + { + return ($x && $y) || $z; + } + + public static function method4() + { + for ($i = 1 + 2; $i < 15; ++$i) { + $i /= 2.5 + 2; + echo $i << 3; + } + + echo $i, $i - 1, $i + 1, ($i - 1) * 2; + + return function() use ($i) { + return 'foo'; + }; + } + + public function method5() + { + return (function() { + return 'bar'; + })(); + } + + public function method6() + { + if (isset($x)) { + if (empty($y)) { + unset($x); + unset($a, $b); + } + + exit($x); + } + + $c = eval('function(){}'); + + return 1 + 2; + } + + private function method7($x) + { + while ($x) { + $x = abs(--$x); + if ($x % 19) { + die($x); + } + } + + return (1 + 2) * 3; + } + + protected function method8($x, $y) + { + if (! ($x || $y)) { + if (! $x || ! ($x + $y)) { + echo $x, $y; + } + } + + $z = ! ($x instanceof DateTime || $x instanceof ArrayAccess); + + return 1 / ($z ? 2 : 3); + } + + public function method9($x, $y) + { + if (false === ($a = strpos($x, $y))) { + return 3 ^ $a; + } + + if (($b = strpos($x, $y)) != false) { + return [$a, $b - 2]; + } + + $h = (int) ($a instanceof DateTime); + $m = (int) ($a + $b); + $n = (float) $b; + $w = (bool) ($x ^ $y); + $z = null === ($x ?: null); + $q = 'string' + . (! empty($x) ? ' ' . $x : '') + . (! empty($y) ? ' ' . $y : ''); + $r = 1 + ($x ? 2 : 3); + $s = ! ($x instanceof DateTime || $x instanceof ArrayAccess); + $p = $x ?: ($y ?: 2 + 1); + $o = $x ? ($y ?: 1) : $x - 1; + + return ! ($b || $a) || ($a - $b); + } + + public function method10($a, $b, $c) + { + if (($a || $z = strpos($b, $c)) === false) { + $z = clone $a; + } + + $x = (int) $a++; + $y = ! $z instanceof DateTime; + $z = 'string ' . --$a; + $w = 7 !== ++$b; + + list($var) = explode(',', '1,2,3'); + + $date = (new DateTime())->modify('+1 year'); + + $a = $b ? ($c ?? 1) : 0; + $a = $b ? 0 : ($c ?? 1); + + new $arr['abc']['def'](); + + $r = $a === ($b + $c) * $d; + $r = $a === $d * ($b + $c); + $r = $a === $b + $c; + $r = $a === ($b + $c); + } +} diff --git a/test/Sniffs/Formatting/UnnecessaryParenthesesUnitTest.php b/test/Sniffs/Formatting/UnnecessaryParenthesesUnitTest.php new file mode 100644 index 00000000..4fc1f99c --- /dev/null +++ b/test/Sniffs/Formatting/UnnecessaryParenthesesUnitTest.php @@ -0,0 +1,62 @@ + 1, + 9 => 1, + 10 => 1, + 11 => 1, + 12 => 2, + 14 => 3, + 31 => 1, + 33 => 2, + 35 => 1, + 41 => 1, + 43 => 1, + 47 => 2, + 52 => 1, + 57 => 3, + 58 => 1, + 59 => 1, + 62 => 3, + 64 => 1, + 65 => 1, + 72 => 1, + 89 => 1, + 94 => 1, + 95 => 1, + 96 => 1, + 107 => 1, + 108 => 1, + 112 => 1, + 114 => 1, + 120 => 1, + 124 => 1, + 129 => 1, + 131 => 1, + 136 => 3, + 137 => 1, + 138 => 1, + 140 => 1, + 146 => 1, + 149 => 1, + 150 => 1, + 151 => 1, + 152 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Functions/ParamUnitTest.1.inc b/test/Sniffs/Functions/ParamUnitTest.1.inc new file mode 100644 index 00000000..0525d05d --- /dev/null +++ b/test/Sniffs/Functions/ParamUnitTest.1.inc @@ -0,0 +1,141 @@ + 1, + 13 => 1, + 18 => 1, + 23 => 1, + 33 => 1, + 43 => 1, + 53 => 1, + 63 => 1, + 73 => 1, + 83 => 1, + 93 => 1, + 98 => 1, + 130 => 1, + 135 => 1, + ]; + } + return [ + 18 => 1, + 33 => 1, + 50 => 1, + 67 => 1, + 81 => 1, + 85 => 3, + 93 => 1, + 98 => 1, + 107 => 1, + 109 => 1, + 112 => 1, + 114 => 1, + 117 => 1, + 121 => 2, + 123 => 2, + 126 => 1, + 127 => 1, + 128 => 1, + 129 => 1, + 130 => 1, + 131 => 1, + 132 => 1, + 133 => 1, + 138 => 1, + 139 => 1, + 142 => 1, + 143 => 1, + 147 => 1, + 151 => 1, + 153 => 1, + 157 => 1, + 159 => 2, + 163 => 1, + 168 => 1, + 169 => 1, + 170 => 1, + 175 => 1, + 180 => 1, + 201 => 1, + 202 => 1, + 203 => 1, + 204 => 1, + 219 => 1, + 220 => 1, + 229 => 1, + 243 => 1, + 248 => 1, + 259 => 2, + 261 => 1, + 266 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Functions/ReturnTypeUnitTest.1.inc b/test/Sniffs/Functions/ReturnTypeUnitTest.1.inc new file mode 100644 index 00000000..d13ad345 --- /dev/null +++ b/test/Sniffs/Functions/ReturnTypeUnitTest.1.inc @@ -0,0 +1,309 @@ +method($a) && $a[0]; + } + + /** + * @return array|\DateTime[] + */ + public function returnBoolOnlyHasNonBooleanType4() + { + return ! ['array']; + } + + /** + * @return int + */ + public function returnBoolOnlyHasNonBooleanType5($a) + { + if ($a) { + return true; + } + + return false; + } + + /** + * @return int|string + */ + public function returnPossiblyNotBoolAndDoesNotHaveBoolType($a) + { + return ! $a ? true : null; + } + + /** + * @return bool + */ + public function returnBoolAndHasBooleanType1($a) + { + return ! $a; + } + + /** + * @return boolean + */ + public function returnBoolAndHasBooleanType2($a) + { + return (bool) $a; + } + + public function returnBoolAndHasBooleanReturnType1($a) : bool + { + return ! $a; + } + + public function returnBoolAndHasBooleanReturnType2($a) : ?bool + { + return (bool) $a; + } + + public function returnBoolAndHasBooleanReturnType3($a) : bool + { + if ($a) { + return true; + } + + return false; + } + + public function returnBooleanAndHasNotBooleanReturnType($a) : int + { + if ($a) { + return true; + } + + return 1; + } + + /** + * @return false + */ + public function returnTrueOnlyHasNotTrueValue() + { + return true; + } + + /** + * @return true + */ + public function returnFalseOnlyHasNotFalseValue() + { + return false; + } + + /** + * @return string + */ + public function returnIntAndHasNoIntType1($a) + { + if ($a) { + return 2 * $a; + } + + return 0; + } + + /** + * @return float + */ + public function returnIntAndHasNoIntType2($a) + { + if (! is_float($a)) { + return 1; + } + + return $a; + } + + public function returnIntAndHasNoReturnTypeInt1() : string + { + return 1; + } + + public function returnIntAndHasNoReturnTypeInt2() : float + { + return 1; + } + + public function returnIntAndHasIntReturnType1() : int + { + return 1; + } + + public function returnIntAndHasIntReturnType2() : ?int + { + return 0; + } + + public function returnPossiblyNotIntAndHasIntReturnType($a) : ?int + { + return 5 * $a; + } + + /** + * @return float + */ + public function returnPossiblyNotIntAndDoesNotHaveIntType($a) + { + return 5 * $a; + } + + /** + * @return float + */ + public function returnNullButDoesNotHaveNullType($a) + { + if ($a) { + return $a; + } + + return null; + } + + public function returnNullButReturnTypeIsNotNullable($a) : int + { + if ($a) { + return $a; + } + + return null; + } + + /** + * @return string + */ + public function returnFloatAndHasNoFloatType1($a) + { + if ($a) { + return 2 * $a; + } + + return 0.0; + } + + /** + * @return int + */ + public function returnFloatAndHasNoFloatType2($a) + { + if (! is_int($a)) { + return 1.0; + } + + return $a; + } + + public function returnFloatAndHasNoReturnTypeFloat1() : string + { + return 1.0; + } + + public function returnFloatAndHasNoReturnTypeFloat2() : int + { + return 1.0; + } + + public function returnFloatAndHasFloatReturnType1() : float + { + return 1.0; + } + + public function returnFloatAndHasFloatReturnType2() : ?float + { + return 0.0; + } + + public function returnFloatExpressionAndDoesNotHaveFloatReturnType($a) : ?int + { + return 5.0 * $a; + } + + /** + * @return int + */ + public function returnFloatExpressionAndDoesNotHaveFloatType($a) + { + return 5.0 * $a; + } + + /** + * @return object + */ + public function returnNewInstanceFromVariable($a) + { + return new $a; + } + + /** + * @return OtherClass + */ + public function returnNewInstance() + { + return new MyClass; + } + + /** + * @return MyInterface2 + */ + public function returnTwoDifferentInstances($a) : MyNamespace\MyInterface1 + { + if ($a) { + return new MyNamespace\ClassA(); + } + + return new MyNamespace\ClassX(); + } + + /** + * @return self + */ + public function returnThisOnlyAndHasSelfInTag() + { + return $this; + } + + /** + * @return FunctionCommentReturn + */ + public function returnThisOnlyAndHasClassNameInTag() + { + return $this; + } + + /** + * @return \MyNamespace\Test\Functions\FunctionCommentReturn + */ + public function returnThisOnlyAndHasFCQNClassNameInTag() + { + return $this; + } +} diff --git a/test/Sniffs/Functions/ReturnTypeUnitTest.2.inc.fixed b/test/Sniffs/Functions/ReturnTypeUnitTest.2.inc.fixed new file mode 100644 index 00000000..34526e1b --- /dev/null +++ b/test/Sniffs/Functions/ReturnTypeUnitTest.2.inc.fixed @@ -0,0 +1,473 @@ +method($a) && $a[0]; + } + + /** + * @return array|\DateTime[] + */ + public function returnBoolOnlyHasNonBooleanType4() + { + return ! ['array']; + } + + /** + * @return int + */ + public function returnBoolOnlyHasNonBooleanType5($a) + { + if ($a) { + return true; + } + + return false; + } + + /** + * @return int|string + */ + public function returnPossiblyNotBoolAndDoesNotHaveBoolType($a) + { + return ! $a ? true : null; + } + + /** + * @return bool + */ + public function returnBoolAndHasBooleanType1($a) + { + return ! $a; + } + + /** + * @return boolean + */ + public function returnBoolAndHasBooleanType2($a) + { + return (bool) $a; + } + + public function returnBoolAndHasBooleanReturnType1($a) : bool + { + return ! $a; + } + + public function returnBoolAndHasBooleanReturnType2($a) : ?bool + { + return (bool) $a; + } + + public function returnBoolAndHasBooleanReturnType3($a) : bool + { + if ($a) { + return true; + } + + return false; + } + + public function returnBooleanAndHasNotBooleanReturnType($a) : int + { + if ($a) { + return true; + } + + return 1; + } + + /** + * @return false + */ + public function returnTrueOnlyHasNotTrueValue() + { + return true; + } + + /** + * @return true + */ + public function returnFalseOnlyHasNotFalseValue() + { + return false; + } + + /** + * @return string + */ + public function returnIntAndHasNoIntType1($a) + { + if ($a) { + return 2 * $a; + } + + return 0; + } + + /** + * @return float + */ + public function returnIntAndHasNoIntType2($a) + { + if (! is_float($a)) { + return 1; + } + + return $a; + } + + public function returnIntAndHasNoReturnTypeInt1() : string + { + return 1; + } + + public function returnIntAndHasNoReturnTypeInt2() : float + { + return 1; + } + + public function returnIntAndHasIntReturnType1() : int + { + return 1; + } + + public function returnIntAndHasIntReturnType2() : ?int + { + return 0; + } + + public function returnPossiblyNotIntAndHasIntReturnType($a) : ?int + { + return 5 * $a; + } + + /** + * @return float + */ + public function returnPossiblyNotIntAndDoesNotHaveIntType($a) + { + return 5 * $a; + } + + /** + * @return float + */ + public function returnNullButDoesNotHaveNullType($a) + { + if ($a) { + return $a; + } + + return null; + } + + public function returnNullButReturnTypeIsNotNullable($a) : int + { + if ($a) { + return $a; + } + + return null; + } + + /** + * @return string + */ + public function returnFloatAndHasNoFloatType1($a) + { + if ($a) { + return 2 * $a; + } + + return 0.0; + } + + /** + * @return int + */ + public function returnFloatAndHasNoFloatType2($a) + { + if (! is_int($a)) { + return 1.0; + } + + return $a; + } + + public function returnFloatAndHasNoReturnTypeFloat1() : string + { + return 1.0; + } + + public function returnFloatAndHasNoReturnTypeFloat2() : int + { + return 1.0; + } + + public function returnFloatAndHasFloatReturnType1() : float + { + return 1.0; + } + + public function returnFloatAndHasFloatReturnType2() : ?float + { + return 0.0; + } + + public function returnFloatExpressionAndDoesNotHaveFloatReturnType($a) : ?int + { + return 5.0 * $a; + } + + /** + * @return int + */ + public function returnFloatExpressionAndDoesNotHaveFloatType($a) + { + return 5.0 * $a; + } + + /** + * @return object + */ + public function returnNewInstanceFromVariable($a) + { + return new $a; + } + + /** + * @return OtherClass + */ + public function returnNewInstance() + { + return new MyClass; + } + + /** + * @return MyInterface2 + */ + public function returnTwoDifferentInstances($a) : MyNamespace\MyInterface1 + { + if ($a) { + return new MyNamespace\ClassA(); + } + + return new MyNamespace\ClassX(); + } + + /** + * @return $this + */ + public function returnThisOnlyAndHasSelfInTag() + { + return $this; + } + + /** + * @return $this + */ + public function returnThisOnlyAndHasClassNameInTag() + { + return $this; + } + + /** + * @return $this + */ + public function returnThisOnlyAndHasFCQNClassNameInTag() + { + return $this; + } +} diff --git a/test/Sniffs/Functions/ReturnTypeUnitTest.3.inc b/test/Sniffs/Functions/ReturnTypeUnitTest.3.inc new file mode 100644 index 00000000..5eadd98e --- /dev/null +++ b/test/Sniffs/Functions/ReturnTypeUnitTest.3.inc @@ -0,0 +1,185 @@ +missingReturnTypeYield($a); + } + + public function returnType() : int + { + return 1; + } + + public function returnTypeYield() : int + { + yield 1; + } + + public function returnTypeYieldFrom() : int + { + yield from $this->returnTypeYieldFrom(); + } + + public function withClosure() + { + $a = function () { + return 1; + }; + } + + public function withClosureYield() + { + $a = function () { + yield 1; + }; + } + + public function withClosureYieldFrom() + { + $a = function () { + yield from $this->returnTypeYield(); + }; + } + + public function returnTypeWithClosure() + { + $a = function (&$x) { + if ($x) { + return; + } + + ++$x; + }; + + return $a; + } + + /** + * @return int + */ + public function invalidReturnTagWithClosure() + { + $a = function () { + return 1; + }; + } + + public function invalidReturnTypeWithClosure() : int + { + $a = function () { + return 1; + }; + } + + public function withAnonClass() + { + $a = new class { + public function a() { + return 1; + } + }; + } + + public function withAnonClassYield() + { + $a = new class { + public function a() { + yield 1; + } + }; + } + + public function withAnonClassYieldFrom() + { + $a = new class { + public function a() { + yield from b(); + } + }; + } + + public function withAnonClassReturnType() + { + $a = new class { + public function a() { + return; + } + }; + + return 1; + } + + /** + * @return int Description. + */ + public function tabSeparatedDoc() + { + return 1; + } + + /** + * @return int Description. + */ + public function moreSpacesInDoc() + { + return 1; + } + + abstract public function returnTypeArrayDoesNotNeedSpecification() : array; + + abstract public function returnTypeNullableArrayDoesNotNeedSpecification() : ?array; + + abstract public function returnTypeTraversableDoesNotNeedSpecification() : \Traversable; + + abstract public function returnTypeNullableTraversableDoesNotNeedSpecificaiton() : ?\Traversable; + + abstract public function returnTypeTraversableWithoutNSDoesNotNeedSpecification() : Traversable; + + abstract public function returnTypeNullableTraversableWithoutNSDoesNotNeedSpecification() : ?Traversable; + + abstract public function returnTypeGeneratorDoesNotNeedSpecification() : \Generator; + + abstract public function returnTypeNullableGeneratorDoesNotNeedSpecificaiton() : ?\Generator; + + abstract public function returnTypeGeneratorWithoutNSDoesNotNeedSpecification() : Generator; + + abstract public function returnTypeNullableGeneratorWithoutNSDoesNotNeedSpecification() : ?Generator; + + abstract public function returnTypeIterableDoesNotNeedSpecification() : iterable; + + abstract public function returnTypeNullableIterableDoesNotNeedSpecification() : ?iterable; + + /** + * @return bool|int We don't know the exact type here. + */ + public function returnNullOrTrueWrongCaseInTag() + { + return mt_rand(0, 1) ? true : null; + } + + /** + * @return string We don't know the exact type here. + */ + public function returnNullOrFalseWrongCaseInTag() + { + return mt_rand(0, 1) ? null : false; + } + + /** + * @return array|callable|iterable|\Generator|\Traversable + */ + public function returnArrayOrSomethingElse($x) + { + if ($x) { + return function() { + }; + } + + return []; + } + + /** + * @return + */ + abstract public function missingType(); + + /** + * @return int,string + */ + abstract public function invalidType(); + + /** + * @return null + */ + abstract public function returnNull(); + + /** + * @return null[] + */ + abstract public function returnNullArray(); +} diff --git a/test/Sniffs/Functions/ReturnTypeUnitTest.php b/test/Sniffs/Functions/ReturnTypeUnitTest.php new file mode 100644 index 00000000..8b88268c --- /dev/null +++ b/test/Sniffs/Functions/ReturnTypeUnitTest.php @@ -0,0 +1,186 @@ + 1, + 12 => 1, + 15 => 1, + 18 => 1, + 20 => 1, + 22 => 1, + 25 => 1, + 28 => 1, + 30 => 1, + 32 => 1, + 34 => 1, + 37 => 1, + 42 => 1, + 47 => 1, + 52 => 1, + 57 => 1, + 62 => 1, + 67 => 1, + 77 => 1, + 82 => 1, + 92 => 1, + 97 => 1, + 107 => 1, + 112 => 1, + 117 => 1, // in theory we can return here another class of the same parent type... + 122 => 1, + 127 => 1, + // 132 => 1, // There is no error, because return type is invalid + 134 => 1, + 137 => 1, + 142 => 1, + 147 => 1, + 152 => 1, + 167 => 1, + 182 => 1, + 197 => 1, + 202 => 1, + 207 => 1, + // 212 => 1, // There is no error, because return type is invalid + 214 => 1, + 217 => 1, + 222 => 1, + 227 => 1, + 237 => 1, + 242 => 1, + 252 => 1, + 257 => 1, + 262 => 1, + 267 => 1, + 272 => 1, + // 277 => 1, // There is no error, because return type is invalid + 279 => 1, + 282 => 1, + 287 => 2, + 292 => 1, + 297 => 1, + 304 => 1, + ]; + case 'ReturnTypeUnitTest.2.inc': + return [ + 8 => 2, + 13 => 1, + 18 => 1, + 23 => 1, + 28 => 1, + 33 => 2, + 42 => 1, + 43 => 1, + 44 => 1, + 45 => 1, + 46 => 1, + 47 => 1, + 48 => 1, + 49 => 1, + 50 => 1, + 51 => 1, + 52 => 1, + 53 => 1, + 54 => 1, + 55 => 1, + 56 => 1, + 57 => 1, + 58 => 1, + 59 => 1, + 60 => 1, + 61 => 1, + 62 => 1, + 63 => 1, + 64 => 1, + 65 => 1, + 66 => 1, + 70 => 1, + 77 => 1, + 84 => 1, + 96 => 1, + 104 => 1, + 112 => 1, + 116 => 1, + 124 => 1, + 132 => 1, + 176 => 1, + 184 => 1, + 188 => 1, + 192 => 1, + 196 => 1, + 200 => 1, + 204 => 1, + 213 => 1, + 216 => 1, + 265 => 1, + 272 => 1, + 276 => 1, + 280 => 1, + 284 => 1, + 296 => 1, + 305 => 1, + 313 => 1, + 318 => 1, + 353 => 1, + 362 => 1, + 374 => 1, + 383 => 1, + 391 => 1, + 396 => 1, + 411 => 1, + 419 => 1, + 431 => 1, + 451 => 1, + 459 => 1, + 467 => 1, + ]; + case 'ReturnTypeUnitTest.3.inc': + return [ + 8 => 1, + 24 => 1, + 40 => 1, + 56 => 1, + 72 => 1, + 96 => 1, + 120 => 1, + 144 => 1, + 160 => 1, + ]; + } + + return [ + 8 => 1, + 10 => 1, + 12 => 1, + 16 => 1, + 18 => 1, + 20 => 1, + 27 => 1, + 36 => 1, + 41 => 1, + 46 => 1, + 95 => 1, + 99 => 1, + 108 => 1, + 119 => 1, + 128 => 1, + 137 => 1, + 150 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Functions/ThrowsUnitTest.inc b/test/Sniffs/Functions/ThrowsUnitTest.inc new file mode 100644 index 00000000..4a6204cd --- /dev/null +++ b/test/Sniffs/Functions/ThrowsUnitTest.inc @@ -0,0 +1,313 @@ +getMessage()); + } + } + + /** + * @throws Exception + */ + public function hasThrowsTagsButDoesNotThrowAnything() + { + } + + /** + * @throws Exception + */ + abstract public function abstractCanHaveThrowsTags(); + + public function throwNewClassFromVariable() + { + $e = Exception::class; + throw new $e; + } + + /** + * @throws + */ + public function missingExceptionTypeInTag() + { + } + + public function throwExceptionFromFactory() + { + throw ExceptionFacotry::create(); + } + + public function throwVariableAndException() + { + $e = new Exception(); + throw $e; + throw new Exception(); + } + + /** + * @throws Exception + */ + public function throwVariableAndExceptionOK1() + { + $e = ExceptionFactory::createException(); + throw $e; + throw new Exception(); + } + + /** + * @throws Ex + * @throws Exception + */ + public function throwVariableAndExceptionOK2() + { + $e = ExceptionFactory::createException(); + throw $e; + throw new Exception(); + } + + /** + * @throws Exception + * @throws Ex + * @throws \InvalidArgumentException + */ + public function valid1() + { + throw new Ex(); + throw new Exception(); + throw new Ex(); + throw new \InvalidArgumentException(); + throw new Exception(); + } + + public function valid2() + { + try { + throw new Ex(); + } catch (Exception $e) { + } + } + + public function valid3() + { + return function() { + throw new Ex(); + }; + } + + /** + * @throws UnknownExceptionType + */ + public function valid4() + { + throw $this->throwException(); + } + + /** + * @throws UnknownException It cannot be determined. + */ + public function valid5() + { + $ex = ExceptionFactory::create(); + throw $ex; + } + + public function valid6() + { + return function () { + return new class { + /** + * @throws Exception + */ + public function x() { + throw new Exception(); + } + }; + }; + } + + abstract public function abstractMethod(); + + /** + * @throws \RuntimeException\Exception + */ + public function throwException() + { + throw new Ex\Exception; + } + + public function closure() + { + return function () { + throw new Exception(); + }; + } + + /** + * @throws Exception + */ + public function calledClosure() + { + (function () { + throw new Exception(); + })(); + } + + /** + * @throws Exception + */ + public function callback(array $arr) + { + return array_filter([] + array_filter($arr), function () { + throw new Exception(); + }); + } +} diff --git a/test/Sniffs/Functions/ThrowsUnitTest.inc.fixed b/test/Sniffs/Functions/ThrowsUnitTest.inc.fixed new file mode 100644 index 00000000..b05e1a2f --- /dev/null +++ b/test/Sniffs/Functions/ThrowsUnitTest.inc.fixed @@ -0,0 +1,313 @@ +getMessage()); + } + } + + /** + * @throws Exception + */ + public function hasThrowsTagsButDoesNotThrowAnything() + { + } + + /** + * @throws Exception + */ + abstract public function abstractCanHaveThrowsTags(); + + public function throwNewClassFromVariable() + { + $e = Exception::class; + throw new $e; + } + + /** + * @throws + */ + public function missingExceptionTypeInTag() + { + } + + public function throwExceptionFromFactory() + { + throw ExceptionFacotry::create(); + } + + public function throwVariableAndException() + { + $e = new Exception(); + throw $e; + throw new Exception(); + } + + /** + * @throws Exception + */ + public function throwVariableAndExceptionOK1() + { + $e = ExceptionFactory::createException(); + throw $e; + throw new Exception(); + } + + /** + * @throws Ex + * @throws Exception + */ + public function throwVariableAndExceptionOK2() + { + $e = ExceptionFactory::createException(); + throw $e; + throw new Exception(); + } + + /** + * @throws Exception + * @throws Ex + * @throws \InvalidArgumentException + */ + public function valid1() + { + throw new Ex(); + throw new Exception(); + throw new Ex(); + throw new \InvalidArgumentException(); + throw new Exception(); + } + + public function valid2() + { + try { + throw new Ex(); + } catch (Exception $e) { + } + } + + public function valid3() + { + return function() { + throw new Ex(); + }; + } + + /** + * @throws UnknownExceptionType + */ + public function valid4() + { + throw $this->throwException(); + } + + /** + * @throws UnknownException It cannot be determined. + */ + public function valid5() + { + $ex = ExceptionFactory::create(); + throw $ex; + } + + public function valid6() + { + return function () { + return new class { + /** + * @throws Exception + */ + public function x() { + throw new Exception(); + } + }; + }; + } + + abstract public function abstractMethod(); + + /** + * @throws Ex\Exception + */ + public function throwException() + { + throw new Ex\Exception; + } + + public function closure() + { + return function () { + throw new Exception(); + }; + } + + /** + * @throws Exception + */ + public function calledClosure() + { + (function () { + throw new Exception(); + })(); + } + + /** + * @throws Exception + */ + public function callback(array $arr) + { + return array_filter([] + array_filter($arr), function () { + throw new Exception(); + }); + } +} diff --git a/test/Sniffs/Functions/ThrowsUnitTest.php b/test/Sniffs/Functions/ThrowsUnitTest.php new file mode 100644 index 00000000..d64384c0 --- /dev/null +++ b/test/Sniffs/Functions/ThrowsUnitTest.php @@ -0,0 +1,39 @@ + 1, + 18 => 1, + 28 => 1, + 40 => 1, + 46 => 1, + 50 => 1, + 60 => 2, + 68 => 1, + 78 => 1, + 97 => 1, + 127 => 1, + 142 => 1, + 160 => 1, + 171 => 1, + 178 => 1, + 184 => 1, + 189 => 1, + 280 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Methods/LineAfterUnitTest.1.inc b/test/Sniffs/Methods/LineAfterUnitTest.1.inc new file mode 100644 index 00000000..7f98da91 --- /dev/null +++ b/test/Sniffs/Methods/LineAfterUnitTest.1.inc @@ -0,0 +1,16 @@ + 1, + 8 => 1, + 13 => 1, + 14 => 2, + 15 => 1, + ]; + case 'LineAfterUnitTest.2.inc': + return [ + 7 => 1, + 10 => 1, + 15 => 1, + 20 => 1, + 21 => 2, + 22 => 1, + 24 => 1, + ]; + case 'LineAfterUnitTest.3.inc': + return [ + 6 => 1, + 9 => 1, + 14 => 1, + 19 => 1, + 20 => 2, + 21 => 1, + ]; + } + + return [ + 7 => 1, + 10 => 1, + 15 => 1, + 20 => 1, + 21 => 2, + 22 => 1, + 24 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Namespaces/AlphabeticallySortedUsesUnitTest.1.inc b/test/Sniffs/Namespaces/AlphabeticallySortedUsesUnitTest.1.inc new file mode 100644 index 00000000..b1f261bb --- /dev/null +++ b/test/Sniffs/Namespaces/AlphabeticallySortedUsesUnitTest.1.inc @@ -0,0 +1,33 @@ + $foo; + }; + } + + public function anonym() + { + return new class() { + use AnotherTrait; + }; + } +} diff --git a/test/Sniffs/Namespaces/AlphabeticallySortedUsesUnitTest.1.inc.fixed b/test/Sniffs/Namespaces/AlphabeticallySortedUsesUnitTest.1.inc.fixed new file mode 100644 index 00000000..2a8a7adc --- /dev/null +++ b/test/Sniffs/Namespaces/AlphabeticallySortedUsesUnitTest.1.inc.fixed @@ -0,0 +1,35 @@ + $foo; + }; + } + + public function anonym() + { + return new class() { + use AnotherTrait; + }; + } +} diff --git a/test/Sniffs/Namespaces/AlphabeticallySortedUsesUnitTest.inc b/test/Sniffs/Namespaces/AlphabeticallySortedUsesUnitTest.inc new file mode 100644 index 00000000..2e83e8c1 --- /dev/null +++ b/test/Sniffs/Namespaces/AlphabeticallySortedUsesUnitTest.inc @@ -0,0 +1,38 @@ + 1, + ]; + } + + return [ + 5 => 1, + 18 => 1, + 19 => 1, + 20 => 1, + 32 => 1, + 33 => 1, + 37 => 2, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Namespaces/ConstAndFunctionKeywordsUnitTest.inc b/test/Sniffs/Namespaces/ConstAndFunctionKeywordsUnitTest.inc new file mode 100644 index 00000000..581d05f7 --- /dev/null +++ b/test/Sniffs/Namespaces/ConstAndFunctionKeywordsUnitTest.inc @@ -0,0 +1,17 @@ + 1, + 4 => 1, + 6 => 2, + 7 => 2, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Namespaces/UnusedUseStatementUnitTest.inc b/test/Sniffs/Namespaces/UnusedUseStatementUnitTest.inc new file mode 100644 index 00000000..295bc00c --- /dev/null +++ b/test/Sniffs/Namespaces/UnusedUseStatementUnitTest.inc @@ -0,0 +1,95 @@ +unused2 = new \Unused1(); + } +} diff --git a/test/Sniffs/Namespaces/UnusedUseStatementUnitTest.inc.fixed b/test/Sniffs/Namespaces/UnusedUseStatementUnitTest.inc.fixed new file mode 100644 index 00000000..899b0331 --- /dev/null +++ b/test/Sniffs/Namespaces/UnusedUseStatementUnitTest.inc.fixed @@ -0,0 +1,89 @@ +unused2 = new \Unused1(); + } +} diff --git a/test/Sniffs/Namespaces/UnusedUseStatementUnitTest.php b/test/Sniffs/Namespaces/UnusedUseStatementUnitTest.php new file mode 100644 index 00000000..e62fb953 --- /dev/null +++ b/test/Sniffs/Namespaces/UnusedUseStatementUnitTest.php @@ -0,0 +1,27 @@ + 1, + 11 => 1, + 13 => 1, + 19 => 1, + 20 => 1, + 21 => 1, + ]; + } +} diff --git a/test/Sniffs/Namespaces/UseDoesNotStartWithBackslashUnitTest.inc b/test/Sniffs/Namespaces/UseDoesNotStartWithBackslashUnitTest.inc new file mode 100644 index 00000000..ce184500 --- /dev/null +++ b/test/Sniffs/Namespaces/UseDoesNotStartWithBackslashUnitTest.inc @@ -0,0 +1,17 @@ + 1, + 5 => 1, + 6 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/NamingConventions/ValidVariableNameUnitTest.inc b/test/Sniffs/NamingConventions/ValidVariableNameUnitTest.inc new file mode 100644 index 00000000..1fa89be9 --- /dev/null +++ b/test/Sniffs/NamingConventions/ValidVariableNameUnitTest.inc @@ -0,0 +1,29 @@ + 1, + 16 => 1, + 28 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Operators/BooleanOperatorUnitTest.inc b/test/Sniffs/Operators/BooleanOperatorUnitTest.inc new file mode 100644 index 00000000..9df23d51 --- /dev/null +++ b/test/Sniffs/Operators/BooleanOperatorUnitTest.inc @@ -0,0 +1,28 @@ + 1, + 6 => 1, + 9 => 1, + 12 => 1, + 15 => 1, + 16 => 1, + 19 => 1, + 27 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Operators/TernaryOperatorUnitTest.inc b/test/Sniffs/Operators/TernaryOperatorUnitTest.inc new file mode 100644 index 00000000..da96a6bf --- /dev/null +++ b/test/Sniffs/Operators/TernaryOperatorUnitTest.inc @@ -0,0 +1,39 @@ + 1, + 7 => 1, + 9 => 1, + 12 => 1, + 16 => 1, + 19 => 1, + 24 => 1, + 31 => 1, + 35 => 1, + 37 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/PHP/CorrectClassNameCaseUnitTest.1.inc b/test/Sniffs/PHP/CorrectClassNameCaseUnitTest.1.inc new file mode 100644 index 00000000..f0833a44 --- /dev/null +++ b/test/Sniffs/PHP/CorrectClassNameCaseUnitTest.1.inc @@ -0,0 +1,11 @@ + 1, + 5 => 1, + 8 => 1, + 9 => 1, + 11 => 1, + ]; + } + + return [ + 5 => 1, + 6 => 1, + 7 => 1, + 8 => 1, + 15 => 1, + 17 => 1, + // 18 => 0, + 21 => 1, + // 25 => 0, + 26 => 1, + 27 => 1, + 28 => 1, + 31 => 1, + 33 => 1, + // 38 => 0, + 40 => 1, + 43 => 1, + 48 => 1, + 55 => 1, + 59 => 1, + 60 => 1, + 61 => 1, + // 63 => 0, + 64 => 1, + 66 => 2, + 73 => 1, + 75 => 1, + 76 => 1, + 77 => 1, + 83 => 1, + 84 => 1, + 89 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/PHP/DeclareStrictTypesUnitTest.1.inc b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.1.inc new file mode 100644 index 00000000..ed04879a --- /dev/null +++ b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.1.inc @@ -0,0 +1,7 @@ + + + + <?php + declare(strict_types=1); + echo 'Title'; + ?> + + + + + diff --git a/test/Sniffs/PHP/DeclareStrictTypesUnitTest.11.inc.fixed b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.11.inc.fixed new file mode 100644 index 00000000..cfad003b --- /dev/null +++ b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.11.inc.fixed @@ -0,0 +1,12 @@ + + + + + <?php + echo 'Title'; + ?> + + + + + diff --git a/test/Sniffs/PHP/DeclareStrictTypesUnitTest.12.inc b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.12.inc new file mode 100644 index 00000000..5ab5fb76 --- /dev/null +++ b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.12.inc @@ -0,0 +1,6 @@ + + + + diff --git a/test/Sniffs/PHP/DeclareStrictTypesUnitTest.12.inc.fixed b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.12.inc.fixed new file mode 100644 index 00000000..eb1c6a15 --- /dev/null +++ b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.12.inc.fixed @@ -0,0 +1,8 @@ + + + + diff --git a/test/Sniffs/PHP/DeclareStrictTypesUnitTest.13.inc b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.13.inc new file mode 100644 index 00000000..b5c390ae --- /dev/null +++ b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.13.inc @@ -0,0 +1,2 @@ + + + + <?php echo 'Title'; ?> + + + + diff --git a/test/Sniffs/PHP/DeclareStrictTypesUnitTest.6.inc.fixed b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.6.inc.fixed new file mode 100644 index 00000000..468d36c7 --- /dev/null +++ b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.6.inc.fixed @@ -0,0 +1,8 @@ + + + + <?php echo 'Title'; ?> + + + + diff --git a/test/Sniffs/PHP/DeclareStrictTypesUnitTest.7.inc b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.7.inc new file mode 100644 index 00000000..0dd39dd4 --- /dev/null +++ b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.7.inc @@ -0,0 +1,13 @@ + + + + <?php echo 'Title'; ?> + + + + diff --git a/test/Sniffs/PHP/DeclareStrictTypesUnitTest.7.inc.fixed b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.7.inc.fixed new file mode 100644 index 00000000..468d36c7 --- /dev/null +++ b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.7.inc.fixed @@ -0,0 +1,8 @@ + + + + <?php echo 'Title'; ?> + + + + diff --git a/test/Sniffs/PHP/DeclareStrictTypesUnitTest.8.inc b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.8.inc new file mode 100644 index 00000000..7d570e97 --- /dev/null +++ b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.8.inc @@ -0,0 +1,15 @@ + + + + <?php echo 'Title'; ?> + + + + diff --git a/test/Sniffs/PHP/DeclareStrictTypesUnitTest.8.inc.fixed b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.8.inc.fixed new file mode 100644 index 00000000..ec8ce723 --- /dev/null +++ b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.8.inc.fixed @@ -0,0 +1,14 @@ + + + + <?php echo 'Title'; ?> + + + + diff --git a/test/Sniffs/PHP/DeclareStrictTypesUnitTest.9.inc b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.9.inc new file mode 100644 index 00000000..6d6f8774 --- /dev/null +++ b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.9.inc @@ -0,0 +1 @@ + diff --git a/test/Sniffs/PHP/DeclareStrictTypesUnitTest.9.inc.fixed b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.9.inc.fixed new file mode 100644 index 00000000..2e240679 --- /dev/null +++ b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.9.inc.fixed @@ -0,0 +1,5 @@ + diff --git a/test/Sniffs/PHP/DeclareStrictTypesUnitTest.inc b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.inc new file mode 100644 index 00000000..7438d056 --- /dev/null +++ b/test/Sniffs/PHP/DeclareStrictTypesUnitTest.inc @@ -0,0 +1,8 @@ + + + 1]; + case 'DeclareStrictTypesUnitTest.2.inc': + return [2 => 1]; + case 'DeclareStrictTypesUnitTest.3.inc': + return [8 => 1]; + case 'DeclareStrictTypesUnitTest.4.inc': + return [1 => 1]; + case 'DeclareStrictTypesUnitTest.5.inc': + return [2 => 2]; + case 'DeclareStrictTypesUnitTest.6.inc': + return [1 => 2]; + case 'DeclareStrictTypesUnitTest.7.inc': + return [ + 1 => 1, + 6 => 1, + ]; + case 'DeclareStrictTypesUnitTest.8.inc': + return [6 => 1]; + case 'DeclareStrictTypesUnitTest.9.inc': + return [1 => 2]; + case 'DeclareStrictTypesUnitTest.10.inc': + return [ + 1 => 1, + 4 => 1, + ]; + case 'DeclareStrictTypesUnitTest.11.inc': + return [ + 1 => 1, + 5 => 1, + ]; + case 'DeclareStrictTypesUnitTest.12.inc': + return [3 => 2]; + case 'DeclareStrictTypesUnitTest.13.inc': + return [2 => 2]; + } + + return [ + 1 => 1, + 5 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/PHP/ImportInternalConstantUnitTest.1.inc b/test/Sniffs/PHP/ImportInternalConstantUnitTest.1.inc new file mode 100644 index 00000000..7e0522f9 --- /dev/null +++ b/test/Sniffs/PHP/ImportInternalConstantUnitTest.1.inc @@ -0,0 +1,19 @@ +E_ERROR; +$z = $obj::ATOM; + +function SEEK_CUR() { +} + +SEEK_CUR(); + +$b = &T_WHITESPACE; + +define('MY_CONST', 'value'); +$myConst = MY_CONST; + +$c = \T_VAR; diff --git a/test/Sniffs/PHP/ImportInternalConstantUnitTest.inc.fixed b/test/Sniffs/PHP/ImportInternalConstantUnitTest.inc.fixed new file mode 100644 index 00000000..06adae35 --- /dev/null +++ b/test/Sniffs/PHP/ImportInternalConstantUnitTest.inc.fixed @@ -0,0 +1,33 @@ +E_ERROR; +$z = $obj::ATOM; + +function SEEK_CUR() { +} + +SEEK_CUR(); + +$b = &T_WHITESPACE; + +define('MY_CONST', 'value'); +$myConst = MY_CONST; + +$c = T_VAR; diff --git a/test/Sniffs/PHP/ImportInternalConstantUnitTest.php b/test/Sniffs/PHP/ImportInternalConstantUnitTest.php new file mode 100644 index 00000000..669a0e98 --- /dev/null +++ b/test/Sniffs/PHP/ImportInternalConstantUnitTest.php @@ -0,0 +1,51 @@ + 1, + 5 => 1, + 11 => 1, + 12 => 1, + 18 => 1, + ]; + case 'ImportInternalConstantUnitTest.2.inc': + return [ + 5 => 1, + 6 => 1, + ]; + case 'ImportInternalConstantUnitTest.3.inc': + return [ + 6 => 1, + ]; + case 'ImportInternalConstantUnitTest.4.inc': + return [ + 5 => 1, + 8 => 1, + ]; + } + + return [ + 5 => 1, + 7 => 1, + 8 => 2, + 21 => 1, + 26 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/PHP/ImportInternalFunctionUnitTest.1.inc b/test/Sniffs/PHP/ImportInternalFunctionUnitTest.1.inc new file mode 100644 index 00000000..556dd6cf --- /dev/null +++ b/test/Sniffs/PHP/ImportInternalFunctionUnitTest.1.inc @@ -0,0 +1,50 @@ +count(); +$count = $obj::count(); + +function myFunction() { +} + +if (isset($x) || empty($x)) { + unset($x); + + reset($keys); +} + +$count = &count($array); +$count = \count($array); + +$count = Count($array); + +$keys = Array_Keys($array); +$values = Array_Rand($array); + +$rand = mt_rand(1, 29); +$rand = \MT_rand(1, 100); diff --git a/test/Sniffs/PHP/ImportInternalFunctionUnitTest.inc.fixed b/test/Sniffs/PHP/ImportInternalFunctionUnitTest.inc.fixed new file mode 100644 index 00000000..18c78085 --- /dev/null +++ b/test/Sniffs/PHP/ImportInternalFunctionUnitTest.inc.fixed @@ -0,0 +1,40 @@ +count(); +$count = $obj::count(); + +function myFunction() { +} + +if (isset($x) || empty($x)) { + unset($x); + + reset($keys); +} + +$count = &count($array); +$count = count($array); + +$count = Count($array); + +$keys = Array_Keys($array); +$values = Array_Rand($array); + +$rand = mt_rand(1, 29); +$rand = MT_rand(1, 100); diff --git a/test/Sniffs/PHP/ImportInternalFunctionUnitTest.php b/test/Sniffs/PHP/ImportInternalFunctionUnitTest.php new file mode 100644 index 00000000..59470a88 --- /dev/null +++ b/test/Sniffs/PHP/ImportInternalFunctionUnitTest.php @@ -0,0 +1,66 @@ + 1, + 5 => 1, + 11 => 1, + 12 => 1, + 18 => 1, + 19 => 1, + 26 => 1, + 32 => 1, + 41 => 1, + 49 => 1, + ]; + case 'ImportInternalFunctionUnitTest.2.inc': + return [ + 5 => 1, + 6 => 1, + 8 => 1, + 9 => 1, + ]; + case 'ImportInternalFunctionUnitTest.3.inc': + return [ + 6 => 1, + ]; + case 'ImportInternalFunctionUnitTest.4.inc': + return [ + 5 => 1, + 6 => 1, + 9 => 1, + 10 => 1, + ]; + } + + return [ + 5 => 1, + 7 => 1, + 11 => 1, + 21 => 1, + 24 => 1, + 25 => 1, + 27 => 1, + 29 => 1, + 30 => 1, + 32 => 1, + 33 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/PHP/InstantiatingParenthesisUnitTest.inc b/test/Sniffs/PHP/InstantiatingParenthesisUnitTest.inc new file mode 100644 index 00000000..ef462093 --- /dev/null +++ b/test/Sniffs/PHP/InstantiatingParenthesisUnitTest.inc @@ -0,0 +1,41 @@ +format('Y-m-d') +); + +class MyClass { + public function __construct(DateTime $dt = null, $param = '') {} + + static public function instance() { + return new self; + } + + static public function staticInstance() { + return new static; + } +} + +new MyClass; +new MyClass(new DateTime); +new MyClass(new DateTime, $_GET); + +$var = DateTime::class; +new $var; + +[new DateTime]; +array(new DateTime); + +$anonymousClass = new class{}; +$anonymousClass = new class {}; + +new $arr['val']; +new $arr['val'](); +(new $arr)['val']; +(new $arr())['val']; + +new $arr['abc']['def']; +new $arr['key']['inx'](); diff --git a/test/Sniffs/PHP/InstantiatingParenthesisUnitTest.inc.fixed b/test/Sniffs/PHP/InstantiatingParenthesisUnitTest.inc.fixed new file mode 100644 index 00000000..d1242638 --- /dev/null +++ b/test/Sniffs/PHP/InstantiatingParenthesisUnitTest.inc.fixed @@ -0,0 +1,41 @@ +format('Y-m-d') +); + +class MyClass { + public function __construct(DateTime $dt = null, $param = '') {} + + static public function instance() { + return new self(); + } + + static public function staticInstance() { + return new static(); + } +} + +new MyClass(); +new MyClass(new DateTime()); +new MyClass(new DateTime(), $_GET); + +$var = DateTime::class; +new $var(); + +[new DateTime()]; +array(new DateTime()); + +$anonymousClass = new class(){}; +$anonymousClass = new class() {}; + +new $arr['val'](); +new $arr['val'](); +(new $arr())['val']; +(new $arr())['val']; + +new $arr['abc']['def'](); +new $arr['key']['inx'](); diff --git a/test/Sniffs/PHP/InstantiatingParenthesisUnitTest.php b/test/Sniffs/PHP/InstantiatingParenthesisUnitTest.php new file mode 100644 index 00000000..d7df5dfa --- /dev/null +++ b/test/Sniffs/PHP/InstantiatingParenthesisUnitTest.php @@ -0,0 +1,37 @@ + 1, + 4 => 1, + 7 => 1, + 14 => 1, + 18 => 1, + 22 => 1, + 23 => 1, + 24 => 1, + 27 => 1, + 29 => 1, + 30 => 1, + 32 => 1, + 33 => 1, + 35 => 1, + 37 => 1, + 40 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/PHP/RedundantSemicolonUnitTest.inc b/test/Sniffs/PHP/RedundantSemicolonUnitTest.inc new file mode 100644 index 00000000..a149ae40 --- /dev/null +++ b/test/Sniffs/PHP/RedundantSemicolonUnitTest.inc @@ -0,0 +1,24 @@ + 0) { +}; + +while (0) { +}; + +for (;;) { +}; + +switch (true) { +}; + +$closure = function() { +}; + +$class = new class { +}; + +$class = new class() extends \DateTime implements \ArrayAccess { +}; + +$a = $b{0}; diff --git a/test/Sniffs/PHP/RedundantSemicolonUnitTest.inc.fixed b/test/Sniffs/PHP/RedundantSemicolonUnitTest.inc.fixed new file mode 100644 index 00000000..0ded4c9e --- /dev/null +++ b/test/Sniffs/PHP/RedundantSemicolonUnitTest.inc.fixed @@ -0,0 +1,24 @@ + 0) { +} + +while (0) { +} + +for (;;) { +} + +switch (true) { +} + +$closure = function() { +}; + +$class = new class { +}; + +$class = new class() extends \DateTime implements \ArrayAccess { +}; + +$a = $b{0}; diff --git a/test/Sniffs/PHP/RedundantSemicolonUnitTest.php b/test/Sniffs/PHP/RedundantSemicolonUnitTest.php new file mode 100644 index 00000000..b662d94d --- /dev/null +++ b/test/Sniffs/PHP/RedundantSemicolonUnitTest.php @@ -0,0 +1,25 @@ + 1, + 7 => 1, + 10 => 1, + 13 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/PHP/SingleSemicolonUnitTest.inc b/test/Sniffs/PHP/SingleSemicolonUnitTest.inc new file mode 100644 index 00000000..252e804d --- /dev/null +++ b/test/Sniffs/PHP/SingleSemicolonUnitTest.inc @@ -0,0 +1,5 @@ + 1, + 5 => 3, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/PHP/TypeCastingUnitTest.inc b/test/Sniffs/PHP/TypeCastingUnitTest.inc new file mode 100644 index 00000000..fc997a97 --- /dev/null +++ b/test/Sniffs/PHP/TypeCastingUnitTest.inc @@ -0,0 +1,49 @@ + 1, + 5 => 1, + 7 => 1, + 8 => 1, + 14 => 1, + 16 => 1, + 17 => 1, + 19 => 1, + 20 => 1, + 26 => 1, + 28 => 1, + 29 => 1, + 31 => 1, + 32 => 1, + 33 => 1, + 35 => 1, + 36 => 1, + 37 => 1, + 39 => 1, + 40 => 1, + 41 => 1, + 42 => 1, + 43 => 1, + 44 => 1, + 45 => 1, + 46 => 1, + 47 => 1, + 48 => 1, + 49 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Strings/NoConcatenationAtTheEndUnitTest.inc b/test/Sniffs/Strings/NoConcatenationAtTheEndUnitTest.inc new file mode 100644 index 00000000..a818100a --- /dev/null +++ b/test/Sniffs/Strings/NoConcatenationAtTheEndUnitTest.inc @@ -0,0 +1,10 @@ + 1, + 9 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/TestCase.php b/test/Sniffs/TestCase.php new file mode 100644 index 00000000..4653dd65 --- /dev/null +++ b/test/Sniffs/TestCase.php @@ -0,0 +1,392 @@ +getPathname(); + if (strpos($path, $testFileBase) === 0) { + if ($path !== $testFileBase . 'php' + && substr($path, -5) !== 'fixed' + ) { + $testFiles[] = $path; + } + } + } + + // Put them in order. + sort($testFiles); + + return $testFiles; + } + + /** + * Should this test be skipped for some reason. + */ + protected function shouldSkipTest() : bool + { + return false; + } + + /** + * Tests the extending classes Sniff class. + */ + final public function testSniff() : void + { + // Skip this test if we can't run in this environment. + if ($this->shouldSkipTest()) { + $this->markTestSkipped(); + } + + $sniffCode = Common::getSniffCode(get_class($this)); + $sniffCode = str_replace('Test.', '.', $sniffCode); + [$standardName, $categoryName, $sniffName] = explode('.', $sniffCode); + + $testFileBase = $this->testsDir . $categoryName . DIRECTORY_SEPARATOR . $sniffName . 'UnitTest.'; + + // Get a list of all test files to check. + $testFiles = $this->getTestFiles($testFileBase); + + $config = new Config([], false); + $config->cache = false; + $config->standards = [sprintf('%s/%s', $this->standardsDir, $standardName)]; + $config->sniffs = [$sniffCode]; + $config->ignored = []; + + $ruleset = new Ruleset($config); + + $failureMessages = []; + foreach ($testFiles as $testFile) { + $filename = basename($testFile); + $oldConfig = $config->getSettings(); + + try { + $this->setCliValues($filename, $config); + $phpcsFile = new LocalFile($testFile, $ruleset, $config); + $phpcsFile->process(); + } catch (RuntimeException $e) { + $this->fail(sprintf('An unexpected exception has been caught: %s', $e->getMessage())); + } + + $failures = $this->generateFailureMessages($phpcsFile); + $failureMessages = array_merge($failureMessages, $failures); + + if ($phpcsFile->getFixableCount() > 0) { + // Attempt to fix the errors. + $phpcsFile->fixer->fixFile(); + $fixable = $phpcsFile->getFixableCount(); + if ($fixable > 0) { + $failureMessages[] = sprintf('Failed to fix %d fixable violations in %s', $fixable, $filename); + } + + // Check for a .fixed file to check for accuracy of fixes. + $fixedFile = $testFile . '.fixed'; + if (file_exists($fixedFile)) { + $diff = $phpcsFile->fixer->generateDiff($fixedFile); + if (trim($diff) !== '') { + $filename = basename($testFile); + $fixedFilename = basename($fixedFile); + $failureMessages[] = sprintf( + 'Fixed version of %s does not match expected version in %s; the diff is%s%s', + $filename, + $fixedFilename, + PHP_EOL, + $diff + ); + } + } + } + + // Restore the config. + $config->setSettings($oldConfig); + } + + if ($failureMessages) { + $this->fail(implode(PHP_EOL, $failureMessages)); + } + } + + /** + * Generate a list of test failures for a given sniffed file. + * + * @param LocalFile $file The file being tested. + * @return string[] + * @throws RuntimeException + */ + private function generateFailureMessages(LocalFile $file) : array + { + $testFile = $file->getFilename(); + + $foundErrors = $file->getErrors(); + $foundWarnings = $file->getWarnings(); + $expectedErrors = $this->getErrorList(basename($testFile)); + $expectedWarnings = $this->getWarningList(basename($testFile)); + + if (! is_array($expectedErrors)) { + throw new RuntimeException('getErrorList() must return an array'); + } + + if (! is_array($expectedWarnings)) { + throw new RuntimeException('getWarningList() must return an array'); + } + + // We merge errors and warnings together to make it easier + // to iterate over them and produce the errors string. In this way, + // we can report on errors and warnings in the same line even though + // it's not really structured to allow that. + + $allProblems = []; + $failureMessages = []; + + foreach ($foundErrors as $line => $lineErrors) { + foreach ($lineErrors as $column => $errors) { + if (! isset($allProblems[$line])) { + $allProblems[$line] = [ + 'expected_errors' => 0, + 'expected_warnings' => 0, + 'found_errors' => [], + 'found_warnings' => [], + ]; + } + + $foundErrorsTemp = []; + foreach ($allProblems[$line]['found_errors'] as $foundError) { + $foundErrorsTemp[] = $foundError; + } + + $errorsTemp = []; + foreach ($errors as $foundError) { + $errorsTemp[] = $foundError['message'] . ' (' . $foundError['source'] . ')'; + } + + $allProblems[$line]['found_errors'] = array_merge($foundErrorsTemp, $errorsTemp); + } + + if (isset($expectedErrors[$line])) { + $allProblems[$line]['expected_errors'] = $expectedErrors[$line]; + } else { + $allProblems[$line]['expected_errors'] = 0; + } + + unset($expectedErrors[$line]); + } + + foreach ($expectedErrors as $line => $numErrors) { + if (! isset($allProblems[$line])) { + $allProblems[$line] = [ + 'expected_errors' => 0, + 'expected_warnings' => 0, + 'found_errors' => [], + 'found_warnings' => [], + ]; + } + + $allProblems[$line]['expected_errors'] = $numErrors; + } + + foreach ($foundWarnings as $line => $lineWarnings) { + foreach ($lineWarnings as $column => $warnings) { + if (! isset($allProblems[$line])) { + $allProblems[$line] = [ + 'expected_errors' => 0, + 'expected_warnings' => 0, + 'found_errors' => [], + 'found_warnings' => [], + ]; + } + + $foundWarningsTemp = []; + foreach ($allProblems[$line]['found_warnings'] as $foundWarning) { + $foundWarningsTemp[] = $foundWarning; + } + + $warningsTemp = []; + foreach ($warnings as $warning) { + $warningsTemp[] = $warning['message'] . ' (' . $warning['source'] . ')'; + } + + $allProblems[$line]['found_warnings'] = array_merge($foundWarningsTemp, $warningsTemp); + } + + if (isset($expectedWarnings[$line])) { + $allProblems[$line]['expected_warnings'] = $expectedWarnings[$line]; + } else { + $allProblems[$line]['expected_warnings'] = 0; + } + + unset($expectedWarnings[$line]); + } + + foreach ($expectedWarnings as $line => $numWarnings) { + if (! isset($allProblems[$line])) { + $allProblems[$line] = [ + 'expected_errors' => 0, + 'expected_warnings' => 0, + 'found_errors' => [], + 'found_warnings' => [], + ]; + } + + $allProblems[$line]['expected_warnings'] = $numWarnings; + } + + // Order the messages by line number. + ksort($allProblems); + + foreach ($allProblems as $line => $problems) { + $numErrors = count($problems['found_errors']); + $numWarnings = count($problems['found_warnings']); + $expectedErrors = $problems['expected_errors']; + $expectedWarnings = $problems['expected_warnings']; + + $errors = ''; + $foundString = ''; + + if ($expectedErrors !== $numErrors || $expectedWarnings !== $numWarnings) { + $lineMessage = '[LINE ' . $line . ']'; + $expectedMessage = 'Expected '; + $foundMessage = 'in ' . basename($testFile) . ' but found '; + + if ($expectedErrors !== $numErrors) { + $expectedMessage .= $expectedErrors . ' error(s)'; + $foundMessage .= $numErrors . ' error(s)'; + if ($numErrors !== 0) { + $foundString .= 'error(s)'; + $errors .= implode(PHP_EOL . ' -> ', $problems['found_errors']); + } + + if ($expectedWarnings !== $numWarnings) { + $expectedMessage .= ' and '; + $foundMessage .= ' and '; + if ($numWarnings !== 0) { + if ($foundString !== '') { + $foundString .= ' and '; + } + } + } + } + + if ($expectedWarnings !== $numWarnings) { + $expectedMessage .= $expectedWarnings . ' warning(s)'; + $foundMessage .= $numWarnings . ' warning(s)'; + if ($numWarnings !== 0) { + $foundString .= 'warning(s)'; + if ($errors) { + $errors .= PHP_EOL . ' -> '; + } + + $errors .= implode(PHP_EOL . ' -> ', $problems['found_warnings']); + } + } + + $fullMessage = sprintf('%s %s %s.', $lineMessage, $expectedMessage, $foundMessage); + if ($errors !== '') { + $fullMessage .= sprintf(' The %s found were:%s -> %s', $foundString, PHP_EOL, $errors); + } + + $failureMessages[] = $fullMessage; + } + } + + return $failureMessages; + } + + /** + * Get a list of CLI values to set before the file is tested. + * + * @param string $filename The name of the file being tested. + * @param Config $config The config data for the run. + * @return string[] + */ + public function setCliValues(string $filename, Config $config) : array + { + return []; + } + + /** + * Returns the lines where errors should occur. + * + * The key of the array should represent the line number and the value + * should represent the number of errors that should occur on that line. + * + * @return int[] + */ + abstract protected function getErrorList(string $testFile = '') : array; + + /** + * Returns the lines where warnings should occur. + * + * The key of the array should represent the line number and the value + * should represent the number of warnings that should occur on that line. + * + * @return int[] + */ + abstract protected function getWarningList(string $testFile = '') : array; +} diff --git a/test/Sniffs/WhiteSpace/BlankLineUnitTest.inc b/test/Sniffs/WhiteSpace/BlankLineUnitTest.inc new file mode 100644 index 00000000..a0097035 --- /dev/null +++ b/test/Sniffs/WhiteSpace/BlankLineUnitTest.inc @@ -0,0 +1,30 @@ + 1, + 6 => 1, + 10 => 1, + 11 => 1, + 18 => 1, + 26 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/WhiteSpace/CommaSpacingUnitTest.inc b/test/Sniffs/WhiteSpace/CommaSpacingUnitTest.inc new file mode 100644 index 00000000..d4263ad3 --- /dev/null +++ b/test/Sniffs/WhiteSpace/CommaSpacingUnitTest.inc @@ -0,0 +1,54 @@ + [1, 2, 3], + 'longKey' => [123, 456, 789], +]; + +abstract class MyClass { + public function method($x,$y, $z){} + + abstract public function absMethod($foo,$bar, $baz); +} + +trait MyTrait { + protected function method($a,$b, $c){} +} + +interface MyInterface { + public function method($x,$y, $z); +} + +echo $x /* some comment here */ , + $y; + +echo $x,// comment here + $y; + +echo $foo /** @var $bar */ + ,$bar; + +echo $foo, /** @var $bar */ + $bar; + +$arr = [[1, 2, 3]]; +$arr = array_unique([1, 2], [3, 4]); diff --git a/test/Sniffs/WhiteSpace/CommaSpacingUnitTest.inc.fixed b/test/Sniffs/WhiteSpace/CommaSpacingUnitTest.inc.fixed new file mode 100644 index 00000000..9030afa8 --- /dev/null +++ b/test/Sniffs/WhiteSpace/CommaSpacingUnitTest.inc.fixed @@ -0,0 +1,53 @@ + [1, 2, 3], + 'longKey' => [123, 456, 789], +]; + +abstract class MyClass { + public function method($x, $y, $z){} + + abstract public function absMethod($foo, $bar, $baz); +} + +trait MyTrait { + protected function method($a, $b, $c){} +} + +interface MyInterface { + public function method($x, $y, $z); +} + +echo $x /* some comment here */, + $y; + +echo $x, // comment here + $y; + +echo $foo /** @var $bar */, $bar; + +echo $foo, /** @var $bar */ + $bar; + +$arr = [[1, 2, 3]]; +$arr = array_unique([1, 2], [3, 4]); diff --git a/test/Sniffs/WhiteSpace/CommaSpacingUnitTest.php b/test/Sniffs/WhiteSpace/CommaSpacingUnitTest.php new file mode 100644 index 00000000..dbb8fa13 --- /dev/null +++ b/test/Sniffs/WhiteSpace/CommaSpacingUnitTest.php @@ -0,0 +1,36 @@ + 1, + 5 => 1, + 7 => 1, + 10 => 2, + 12 => 1, + 14 => 2, + 28 => 2, + 30 => 2, + 34 => 2, + 38 => 2, + 41 => 1, + 44 => 1, + 48 => 2, + 53 => 2, + 54 => 3, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/WhiteSpace/NoBlankLineAtStartUnitTest.inc b/test/Sniffs/WhiteSpace/NoBlankLineAtStartUnitTest.inc new file mode 100644 index 00000000..4da5d7b6 --- /dev/null +++ b/test/Sniffs/WhiteSpace/NoBlankLineAtStartUnitTest.inc @@ -0,0 +1,57 @@ + 1, + 9 => 1, + 17 => 1, + 19 => 1, + 30 => 1, + 36 => 1, + 42 => 1, + 45 => 1, + 47 => 1, + 54 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.1.inc b/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.1.inc new file mode 100644 index 00000000..de2e3388 --- /dev/null +++ b/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.1.inc @@ -0,0 +1,228 @@ +getMessage(); +} + +class MyClass +{ +} + +function y($a, $b) { + if ($a + || $b) { + return $a; + } elseif ($a + && $b + ) { + return $b; + } elseif ($a + xor $b +) { + return $a - $b; + } elseif ($a + || $b) { + return $a; + } elseif ($a + && $b) { + return $b; + } + + return $a + $b; +} + +while (1 +|| 2) { + continue; +} + +if ($a +&& ($b +|| $c) +&& preg_match( + '/a/', + 'b' +) +) { + return 12; +} + +if (false === strpos( + 'haystack', + 'needle' +) +) { + echo 0; +} + +if (false === $method( + 'value' +) +) { + echo 1; +} + +if ($a +!== $b + && $c +instanceof \DateTime +) { + echo 1; +} + +if ('x' === trim(sprintf( + 'a %s', + 'b' +) +) +) { + echo 2; +} + +if (true === strpos( + 'a', + 'b' +) ) { + echo 3; +} + +if ($a && ($b || $c +)) { + echo 4; +} + +if ($a && ($b + || $c +)) { + echo 5; +} + +$a = 'string'; +$v = $a{0}; +$z = $a->{$v}; + +if ($a + && ($b + || $c + ) +) { + echo 1; +} + +if ($a + && ($b + || $c + ) && ($d + || $e + ) +) { + echo 2; +} + +if ($a + && ($b + || $c + ) + && ($d + || $e + ) +) { + echo 3; +} + +while (in_array($a, [ + $b, + $c, + $d, +], true)) { +} + +if (in_array($a, array( + $b, + $c +), true)) { +} elseif (myFunc($a, anotherFunc( + $d, + $e +), true)) { +} + +do { +} while (myFunc($a, function ( + $b, + $c +) { + return $b <=> $c; +}, true)); + +if (myFunc([ + 'elem1', + 'elem2', +], function ( + $a, + $b +) { + return $a > $b; +}, [ + 'param' => 'val' +])) { +} elseif (($a + && $b) + || ($c + && $d) + || ($e + && $f + && $g) +) { +} + +do { +} while ($a + && ($b + || $c + || $d) +); + +if ($a + && (($b + || $c + || $d) + && $e + || ($f && $g)) +) { +} elseif ($a + && ((($b + || $c + || $d) + && $e) + || ($f && $g)) +) { +} elseif ($a + && (($x && ($b + || $c + || $d) + && $e) + || ($f && $g)) +) { +} diff --git a/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.1.inc.fixed b/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.1.inc.fixed new file mode 100644 index 00000000..429cc2b0 --- /dev/null +++ b/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.1.inc.fixed @@ -0,0 +1,222 @@ +getMessage(); +} + +class MyClass +{ +} + +function y($a, $b) { + if ($a + || $b + ) { + return $a; + } elseif ($a + && $b + ) { + return $b; + } elseif ($a + xor $b + ) { + return $a - $b; + } elseif ($a + || $b + ) { + return $a; + } elseif ($a + && $b + ) { + return $b; + } + + return $a + $b; +} + +while (1 + || 2 +) { + continue; +} + +if ($a + && ($b + || $c) + && preg_match( + '/a/', + 'b' + ) +) { + return 12; +} + +if (false === strpos( + 'haystack', + 'needle' +)) { + echo 0; +} + +if (false === $method( + 'value' +)) { + echo 1; +} + +if ($a + !== $b + && $c + instanceof \DateTime +) { + echo 1; +} + +if ('x' === trim(sprintf( + 'a %s', + 'b' +))) { + echo 2; +} + +if (true === strpos( + 'a', + 'b' +)) { + echo 3; +} + +if ($a && ($b || $c)) { + echo 4; +} + +if ($a && ($b + || $c) +) { + echo 5; +} + +$a = 'string'; +$v = $a{0}; +$z = $a->{$v}; + +if ($a + && ($b + || $c) +) { + echo 1; +} + +if ($a + && ($b + || $c) + && ($d + || $e) +) { + echo 2; +} + +if ($a + && ($b + || $c) + && ($d + || $e) +) { + echo 3; +} + +while (in_array($a, [ + $b, + $c, + $d, +], true)) { +} + +if (in_array($a, array( + $b, + $c +), true)) { +} elseif (myFunc($a, anotherFunc( + $d, + $e +), true)) { +} + +do { +} while (myFunc($a, function ( + $b, + $c +) { + return $b <=> $c; +}, true)); + +if (myFunc([ + 'elem1', + 'elem2', +], function ( + $a, + $b +) { + return $a > $b; +}, [ + 'param' => 'val' +])) { +} elseif (($a + && $b) + || ($c + && $d) + || ($e + && $f + && $g) +) { +} + +do { +} while ($a + && ($b + || $c + || $d) +); + +if ($a + && (($b + || $c + || $d) + && $e + || ($f && $g)) +) { +} elseif ($a + && ((($b + || $c + || $d) + && $e) + || ($f && $g)) +) { +} elseif ($a + && (($x && ($b + || $c + || $d) + && $e) + || ($f && $g)) +) { +} diff --git a/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.inc b/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.inc new file mode 100644 index 00000000..b05d45ce --- /dev/null +++ b/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.inc @@ -0,0 +1,558 @@ + $b) { +return 1; +} + +return [ + 1, + 2, + ]; +} +} + +switch (true) { +case '1': +echo 1; +break; +case '2': +echo 2; +break; +default: +switch ($a) { +default: +case 0: +echo 9; +return 17; +case 1: +break; +} +break; +} + +usort($a, function($x, $y) { + return $x > $y; +}); + +function x($a) { + /* + * some comment + */ + $y = $a; + + return $y; +} + +class Test2 { + public $var = <<call($a, $b) + ->another($x) + ->oneMore($d); + +(new DateTime()) + ->modify('-1 day') + ->modify('-1 hour'); + +$m = (new DateTime())->modify('-1 second') + ->modify('+1 second'); + +$val = $class->{$method}($value) + ->{$value}; + +$v = $a->{$m}(1) + ->{$d} + ->date + ->modify('-1 day') + ->format('Y-m-d'); + +$f = function () use ( + $foo, + $bar +) { + return $foo + $bar; +}; + +LABEL: + + $something = 'porter'; + +OTHER: + + $other = 'label'; + +$g = $t->method() + ->another(function () { + $x = 13; + + return $x; + }); + +$concatenation = 'string' + . 'next part'; + +$sum = $a + + $b; + +class B { + public function foo() + { + return function ($stream, $streamIndex) { + return preg_match('#(.*?)\r?\n#', $stream, $matches, null, $streamIndex) === 1 + ? $matches[1] + : substr($stream, $streamIndex); + }; + } + + public function bar($parameters) + { + return array_filter( + $parameters, + function (array $parameter) { + return PHP_VERSION_ID >= 70100 + || ( + false === strpos($parameter[2], '?') + && ! in_array(strtolower($parameter[2]), ['void', 'iterable']) + ); + } + ); + } + + public function tar($abc, $def) + { + /* + this part is commented out + and doesn't matter + what + indent +do we have here. + + */ + return PHP_VERSION_ID >= 70100 && ( + $abc + || $def + ); + } + + private function test2($services) + { + return function () use ($services) { + return $services->get('Application') + ->method1() + ->method2(); + }; + } +} + +switch ($a) { + // POST + case 'POST': + $do = 'something'; + break; + // GET + case 'GET': + // my comment here + break; +// All others + default: + $do = 'nothing'; + break; +} + +echo "This is a long +string that spans $numLines lines + without indenting. +"; + +$oldArray = array( + 'the', 'sniff', + 'does', + 'check', 'indents', 'in', + 'arrays' +); + +class Test3 +{ + public static $a; + + public static function xyz($b) + { + if (static::$a[$b] === null) { + static::$a[$b] = new static( + 'it', + 'is', + 'not checked in that sniff' + ); + } + } +} + +$obj->{$m}( + 'skip', + 'it' +); + +if ($a == 5) : + echo "a equals 5"; + echo "..."; +elseif ($a == 6) : + echo "a equals 6"; + echo "!!!"; +else : + echo "a is neither 5 nor 6"; +endif; + +if ($foo): + if ($bar) $foo = 1; + elseif ($baz) $foo = 2; +endif; + +usort( + $a, + function ( + $x, + $y + ) { + return $x > $y; + } +); + +$var = $foo + && $bar + && ($baz + || $key); + +$num = $multi + * ($val1 + + $val2) + + $int; + +$closure = function ( + $a, + $b, + $c +) { + if ($a + $b > $c) { + return $a + && ($b + || $c); + } + + return $b && ($a + || $c); +}; + +switch ($r) { +case self::TYPE: +$var = null; +if ($x) { + return 1; +} +exit; +case self::GO; +return function () use ($r) { +if ($r) { +throw new \InvalidArgumentException( +sprintf('Error %s', 'value') +); +} + +return function ($a, $b) { +return $a <=> $b; +}; +}; +case self::DEF: + default: +$var = 1; +throw new \Exception(sprintf( +'Type "%s" is unknown or cannot be used as property default value.', +get_class($value) +)); +} + +(function ($a, $b) { +return function ($c, $d) use ($a, $b) { +echo $a, $b, $c, $d; +}; +})( + 'a', +'b' +)( + 'c', +'d' +); + +(new \DateTime([ + 'something', +]))->format('ymd'); + +$myVariable + ->call(sprintf( + 'Text %s', + 'value' + )); + +class MyClass +{ + public function test() + { + } +} + +$mock->expects( + $this->any() +) +->method('get') +->will($this->throwException()); + +$mock->expects($this->any()) +->method( +'get' +) +->will($this->throwException()); + +$mock +->expects($this->any()) +->method( +'get' +) +->will($this->throwException()); + +$mock +->expects( +$this->any() +) +->method('get') +->will($this->throwException()); + +class Test3 +{ + public function method() + { + $mock->expects( + $this->any() + ) + ->method('get') + ->will($this->throwException()); + + $mock->expects($this->any()) + ->method( + 'get' + ) + ->will($this->throwException()); + + $mock + ->expects($this->any()) + ->method( + 'get' + ) + ->will($this->throwException()); + + $mock + ->expects( + $this->any() + ) + ->method('get') + ->will($this->throwException()); + } + + public function method2() + { + $mock->method( + 'param' + )->shouldBeCalled(); + } + +public function method3() +{ +$list = [ +'fn' => function ($a) { +if ($a === true) { +echo 'hi'; +} +}, +'call' => sprintf( +'Text %s', +'param' +), +]; +} + +public function method4() +{ +$this->console +->writeln( +Argument::that(function ($arg) { +if (false === strpos($arg, 'src/ErrorMiddleware.php')) { +return false; +} +if (false === strpos( +$arg, +sprintf('implementing %s', ErrorMiddlewareInterface::class) +)) { +return false; +} +return true; +}) +) +->shouldBeCalled(); +} +} + +$foo = $bar ++ [ +'foo', +'bar', +]; + +$foo = $bar ++ func( +'foo', +'bar' +); + +$f->foo( +'foo' +) +->bar( +'bar', +$foo ++ [ +'bar', +] +); + +$this->abc($a, [ + 'param1', + 'param2', +]) + ->def(); + +$this->def($a, [ + 'abc', + 'def', + ]); + +throw new MyException(sprintf( +'Type "%s" is unknown or cannot be used as property default value.', +get_class($value) +), $param, [ +'foo' => 'bar', +'bar' => 'baz', +]); + +function test1(array $a, $b) +{ + if (array_filter($a, function ($v) use ($b) { + return $v === $b * 2; + })) { + return 1; + } + + return 0; +} + +function test2() +{ + if (false === strpos( + 'haystack', + 'needle' + )) { + return 1; + } + + return 0; +} + +$response = $handler->handle( + $request + ->withMethod(RequestMethod::METHOD_GET) + ->withAttribute(self::FORWARDED_HTTP_METHOD_ATTRIBUTE, RequestMethod::METHOD_HEAD) +); + +yield 'key' => (new DateTime( + (function () { + yield 'key1' => (new X( + 'hi' + )); + + yield 'key2' => (new X( + 'hi', + )) + ->m() + ->r(); + + yield 'key3' => function () { + return [ + 'arr' => (new X( + 'hello' + )) + ->m() + ->s(), + ]; + }; + })() +)) +->format('Y'); + +$abc = [ + $test + ->abc() + ->def(), +]; + +$def = function ($a) { + $a + ->a() + ->b(); +}; + +$ghi = [ + [ + function () { + }, + $hey + ->a() + ->b(), + ], + $hola + ->c() + ->d(), +]; diff --git a/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.inc.fixed b/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.inc.fixed new file mode 100644 index 00000000..b084bba0 --- /dev/null +++ b/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.inc.fixed @@ -0,0 +1,556 @@ + $b) { + return 1; + } + + return [ + 1, + 2, + ]; + } +} + +switch (true) { + case '1': + echo 1; + break; + case '2': + echo 2; + break; + default: + switch ($a) { + default: + case 0: + echo 9; + return 17; + case 1: + break; + } + break; +} + +usort($a, function($x, $y) { + return $x > $y; +}); + +function x($a) { + /* + * some comment + */ + $y = $a; + + return $y; +} + +class Test2 { + public $var = <<call($a, $b) + ->another($x) + ->oneMore($d); + +(new DateTime()) + ->modify('-1 day') + ->modify('-1 hour'); + +$m = (new DateTime())->modify('-1 second') + ->modify('+1 second'); + +$val = $class->{$method}($value) + ->{$value}; + +$v = $a->{$m}(1) + ->{$d} + ->date + ->modify('-1 day') + ->format('Y-m-d'); + +$f = function () use ( + $foo, + $bar +) { + return $foo + $bar; +}; + +LABEL: + +$something = 'porter'; + +OTHER: + +$other = 'label'; + +$g = $t->method() + ->another(function () { + $x = 13; + + return $x; + }); + +$concatenation = 'string' + . 'next part'; + +$sum = $a + + $b; + +class B { + public function foo() + { + return function ($stream, $streamIndex) { + return preg_match('#(.*?)\r?\n#', $stream, $matches, null, $streamIndex) === 1 + ? $matches[1] + : substr($stream, $streamIndex); + }; + } + + public function bar($parameters) + { + return array_filter( + $parameters, + function (array $parameter) { + return PHP_VERSION_ID >= 70100 + || (false === strpos($parameter[2], '?') + && ! in_array(strtolower($parameter[2]), ['void', 'iterable'])); + } + ); + } + + public function tar($abc, $def) + { + /* + this part is commented out + and doesn't matter + what + indent +do we have here. + + */ + return PHP_VERSION_ID >= 70100 + && ($abc + || $def); + } + + private function test2($services) + { + return function () use ($services) { + return $services->get('Application') + ->method1() + ->method2(); + }; + } +} + +switch ($a) { + // POST + case 'POST': + $do = 'something'; + break; + // GET + case 'GET': + // my comment here + break; + // All others + default: + $do = 'nothing'; + break; +} + +echo "This is a long +string that spans $numLines lines + without indenting. +"; + +$oldArray = array( + 'the', 'sniff', + 'does', + 'check', 'indents', 'in', + 'arrays' +); + +class Test3 +{ + public static $a; + + public static function xyz($b) + { + if (static::$a[$b] === null) { + static::$a[$b] = new static( + 'it', + 'is', + 'not checked in that sniff' + ); + } + } +} + +$obj->{$m}( + 'skip', + 'it' +); + +if ($a == 5) : + echo "a equals 5"; + echo "..."; +elseif ($a == 6) : + echo "a equals 6"; + echo "!!!"; +else : + echo "a is neither 5 nor 6"; +endif; + +if ($foo): + if ($bar) $foo = 1; + elseif ($baz) $foo = 2; +endif; + +usort( + $a, + function ( + $x, + $y + ) { + return $x > $y; + } +); + +$var = $foo + && $bar + && ($baz + || $key); + +$num = $multi + * ($val1 + + $val2) + + $int; + +$closure = function ( + $a, + $b, + $c +) { + if ($a + $b > $c) { + return $a + && ($b + || $c); + } + + return $b && ($a + || $c); +}; + +switch ($r) { + case self::TYPE: + $var = null; + if ($x) { + return 1; + } + exit; + case self::GO; + return function () use ($r) { + if ($r) { + throw new \InvalidArgumentException( + sprintf('Error %s', 'value') + ); + } + + return function ($a, $b) { + return $a <=> $b; + }; + }; + case self::DEF: + default: + $var = 1; + throw new \Exception(sprintf( + 'Type "%s" is unknown or cannot be used as property default value.', + get_class($value) + )); +} + +(function ($a, $b) { + return function ($c, $d) use ($a, $b) { + echo $a, $b, $c, $d; + }; +})( + 'a', + 'b' +)( + 'c', + 'd' +); + +(new \DateTime([ + 'something', +]))->format('ymd'); + +$myVariable + ->call(sprintf( + 'Text %s', + 'value' + )); + +class MyClass +{ + public function test() + { + } +} + +$mock->expects( + $this->any() + ) + ->method('get') + ->will($this->throwException()); + +$mock->expects($this->any()) + ->method( + 'get' + ) + ->will($this->throwException()); + +$mock + ->expects($this->any()) + ->method( + 'get' + ) + ->will($this->throwException()); + +$mock + ->expects( + $this->any() + ) + ->method('get') + ->will($this->throwException()); + +class Test3 +{ + public function method() + { + $mock->expects( + $this->any() + ) + ->method('get') + ->will($this->throwException()); + + $mock->expects($this->any()) + ->method( + 'get' + ) + ->will($this->throwException()); + + $mock + ->expects($this->any()) + ->method( + 'get' + ) + ->will($this->throwException()); + + $mock + ->expects( + $this->any() + ) + ->method('get') + ->will($this->throwException()); + } + + public function method2() + { + $mock->method( + 'param' + ) + ->shouldBeCalled(); + } + + public function method3() + { + $list = [ + 'fn' => function ($a) { + if ($a === true) { + echo 'hi'; + } + }, + 'call' => sprintf( + 'Text %s', + 'param' + ), + ]; + } + + public function method4() + { + $this->console + ->writeln( + Argument::that(function ($arg) { + if (false === strpos($arg, 'src/ErrorMiddleware.php')) { + return false; + } + if (false === strpos( + $arg, + sprintf('implementing %s', ErrorMiddlewareInterface::class) + )) { + return false; + } + return true; + }) + ) + ->shouldBeCalled(); + } +} + +$foo = $bar + + [ + 'foo', + 'bar', + ]; + +$foo = $bar + + func( + 'foo', + 'bar' + ); + +$f->foo( + 'foo' + ) + ->bar( + 'bar', + $foo + + [ + 'bar', + ] + ); + +$this->abc($a, [ + 'param1', + 'param2', + ]) + ->def(); + +$this->def($a, [ + 'abc', + 'def', +]); + +throw new MyException(sprintf( + 'Type "%s" is unknown or cannot be used as property default value.', + get_class($value) +), $param, [ + 'foo' => 'bar', + 'bar' => 'baz', +]); + +function test1(array $a, $b) +{ + if (array_filter($a, function ($v) use ($b) { + return $v === $b * 2; + })) { + return 1; + } + + return 0; +} + +function test2() +{ + if (false === strpos( + 'haystack', + 'needle' + )) { + return 1; + } + + return 0; +} + +$response = $handler->handle( + $request + ->withMethod(RequestMethod::METHOD_GET) + ->withAttribute(self::FORWARDED_HTTP_METHOD_ATTRIBUTE, RequestMethod::METHOD_HEAD) +); + +yield 'key' => (new DateTime( + (function () { + yield 'key1' => (new X( + 'hi' + )); + + yield 'key2' => (new X( + 'hi', + )) + ->m() + ->r(); + + yield 'key3' => function () { + return [ + 'arr' => (new X( + 'hello' + )) + ->m() + ->s(), + ]; + }; + })() + )) + ->format('Y'); + +$abc = [ + $test + ->abc() + ->def(), +]; + +$def = function ($a) { + $a + ->a() + ->b(); +}; + +$ghi = [ + [ + function () { + }, + $hey + ->a() + ->b(), + ], + $hola + ->c() + ->d(), +]; diff --git a/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.php b/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.php new file mode 100644 index 00000000..a0d8bb0d --- /dev/null +++ b/test/Sniffs/WhiteSpace/ScopeIndentUnitTest.php @@ -0,0 +1,263 @@ + 1, + 34 => 2, + 38 => 1, + 39 => 1, // + 40 => 1, // + 41 => 1, + 42 => 1, + 45 => 2, + 46 => 1, // + 47 => 1, // + 48 => 2, + 49 => 1, // + 50 => 1, // + 56 => 1, + 61 => 1, + 62 => 1, + 63 => 1, + 64 => 1, + 65 => 1, + 66 => 1, + 72 => 1, // + 73 => 1, // + 74 => 2, // 1 + 80 => 1, // + 81 => 2, // 1 + 87 => 1, + 89 => 1, + 95 => 1, // + 96 => 1, // + 97 => 2, // 1 + 98 => 2, // 1 + 106 => 1, + 111 => 2, // 1 -- todo: the same errors + 117 => 1, + 128 => 1, + 136 => 1, + 138 => 1, + 146 => 1, + 149 => 1, + ]; + } + + return [ + 10 => 1, + 11 => 1, + 12 => 1, + 13 => 1, + 14 => 1, + 16 => 1, + 20 => 1, + 24 => 1, + 25 => 1, + 26 => 1, + 27 => 1, + 28 => 1, + 29 => 1, + 30 => 1, + 31 => 1, + 32 => 1, + 33 => 1, + 34 => 1, + 35 => 1, + 36 => 1, + 37 => 1, + 38 => 1, + 39 => 1, + 43 => 1, + 47 => 1, + 48 => 1, + 49 => 1, + 73 => 1, + 74 => 1, + 77 => 1, + 82 => 1, + 87 => 1, + 91 => 1, + 95 => 1, + 98 => 1, + 101 => 1, + 110 => 1, + 115 => 1, + 119 => 1, + 123 => 1, + 125 => 1, + 129 => 1, + 132 => 1, + 139 => 1, + 150 => 1, + 152 => 1, + 153 => 1, + 168 => 1, + 169 => 1, + 171 => 1, + 178 => 1, + 185 => 1, + 189 => 1, + 191 => 1, + 193 => 1, + 205 => 1, + 207 => 1, + 208 => 1, + 219 => 1, + 220 => 1, + 221 => 1, + 228 => 1, + 233 => 1, + 237 => 1, + 239 => 1, + 244 => 1, + 250 => 1, + 253 => 1, + 260 => 1, + 264 => 1, + 269 => 1, + 279 => 1, + 283 => 1, + 284 => 1, + 285 => 1, + 286 => 1, + 287 => 1, + 288 => 1, + 289 => 1, + 290 => 1, + 291 => 1, + 292 => 1, + 293 => 1, + 294 => 1, + 295 => 1, + 297 => 1, + 298 => 1, + 299 => 1, + 300 => 1, + 301 => 1, + 302 => 1, + 303 => 1, + 304 => 1, + 305 => 1, + 306 => 1, + 307 => 1, + 311 => 1, + 312 => 1, + 313 => 1, + 315 => 1, + 316 => 1, + 318 => 1, + 319 => 1, + 340 => 1, + 341 => 1, + 342 => 1, + 346 => 1, + 347 => 1, + 348 => 1, + 352 => 1, + 354 => 1, + 359 => 1, + 360 => 1, + 361 => 1, + 370 => 1, + 371 => 1, + 372 => 1, + 376 => 1, + 377 => 1, + 378 => 1, + 399 => 1, + 400 => 2, + 403 => 1, + 404 => 1, + 405 => 1, + 406 => 1, + 407 => 1, + 408 => 1, + 409 => 1, + 410 => 1, + 411 => 1, + 412 => 1, + 413 => 1, + 414 => 1, + 415 => 1, + 416 => 1, + 418 => 1, + 419 => 1, + 420 => 1, + 421 => 1, + 422 => 1, + 423 => 1, + 424 => 1, + 425 => 1, + 426 => 1, + 427 => 1, + 428 => 1, + 429 => 1, + 430 => 1, + 431 => 1, + 432 => 1, + 433 => 1, + 434 => 1, + 436 => 1, + 440 => 1, + 441 => 1, + 442 => 1, + 446 => 1, + 447 => 1, + 448 => 1, + 452 => 1, + 453 => 1, + 454 => 1, + 455 => 1, + 456 => 1, + 457 => 1, + 458 => 1, + 459 => 1, + 460 => 1, + 463 => 1, + 464 => 1, + 465 => 1, + 469 => 1, + 470 => 1, + 471 => 1, + 474 => 1, + 475 => 1, + 477 => 1, + 478 => 1, + 511 => 1, + 512 => 1, + 513 => 1, + 514 => 1, + 516 => 1, + 517 => 1, + 518 => 1, + 519 => 1, + 522 => 1, + 523 => 1, + 524 => 1, + 525 => 1, + 526 => 1, + 527 => 1, + 529 => 1, + 530 => 1, + 531 => 1, + 532 => 1, + 533 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +}