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/.gitignore b/.gitignore new file mode 100644 index 00000000..d5e33e78 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/clover.xml +/composer.lock +/coveralls-upload.json +/phpunit.xml +/vendor/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..c454a821 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,77 @@ +sudo: false + +language: php + +cache: + directories: + - $HOME/.composer/cache + - vendor + +env: + global: + - COMPOSER_ARGS="--no-interaction" + - COVERAGE_DEPS="satooshi/php-coveralls" + - LEGACY_DEPS="phpunit/phpunit" + +matrix: + include: + - php: 5.6 + env: + - DEPS=lowest + - php: 5.6 + env: + - DEPS=locked + - php: 5.6 + env: + - DEPS=latest + - php: 7 + env: + - DEPS=lowest + - php: 7 + env: + - DEPS=locked + - php: 7 + env: + - DEPS=latest + - php: 7.1 + env: + - DEPS=lowest + - php: 7.1 + env: + - DEPS=locked + - php: 7.1 + env: + - DEPS=latest + #- TEST_COVERAGE=true + +before_install: + - if [[ $TEST_COVERAGE != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi + - travis_retry composer self-update + +install: + - travis_retry composer install $COMPOSER_ARGS --ignore-platform-reqs + - if [[ $TRAVIS_PHP_VERSION =~ ^5.6 ]]; then travis_retry composer update $COMPOSER_ARGS --with-dependencies $LEGACY_DEPS ; fi + - 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 + - composer show + +script: + - if [[ $TEST_COVERAGE == 'true' ]]; then composer test-coverage ; else composer test ; fi + - composer cs-check + +after_script: + - if [[ $TEST_COVERAGE == 'true' ]]; then composer upload-coverage ; fi + +notifications: + email: false + irc: + channels: + - "irc.freenode.org#zftalk.dev" + on_success: change + on_failure: always + slack: + rooms: + - secure: "fh+J7c7A9f7Sje2h9M00mw9UyeKQ2l1FyGvOwQ50CPHvf0u7bb1OV3sEqTukiwSNEQNmJ5C3QuJpaxkIeDjI8LJpeNderWeu6NH2O5OedSEElHmc7RsBygfiHM05hWnv10ddDxJ+YtmuNjpkXIoXcBdHby+eRBJ09YStVhnIwQakBbKBH7Idlitn23QYl4VZeA3jTcGsHhCtGjpjDt4sohs/RJWgGfAYTSKcjSLdFWWdg2G8PRPKTyQkR+nFd92lvVeRteg0VzxGJqKXoeJP3B0WYB7emQJho+ly4DZFkL+wJZPtcEHCi/ne9l/OaVy6XGhbiDVXxxpyexD4cmGySdjpsYirXqxjS6V8kfWn3JVbCxipI518zJq5Rb3JOCVcdoo7P/xXqYj+fihMbGfxBorUqwm8uBlCblRGXJ1QcVRsi52u0zMJN+QQ/gYHf0gBoF9IbYvQDeshZT80TnYUy2/om/j9xTUfZMdEKTRQWrj9LbsNKY619gZt2u/b2tbcWdjFiPA/Uxa2VjphHz8LFuHaU43/km4swLhthkPowdZLozuyJjksbJcjH7izh3Hxd8oRxBFrqdfuPgh3owQyW75wvG/TGWnNXMqO66OuXXaJKAomBnVBjGB6DBo3TnPzEyKUkLi54AVQIfaDPqQH8yec5ugogOYrrQDLIT1kdVY=" + on_success: change + on_failure: always diff --git a/COPYRIGHT.md b/COPYRIGHT.md new file mode 100644 index 00000000..5837b4b1 --- /dev/null +++ b/COPYRIGHT.md @@ -0,0 +1 @@ +Copyright (c) 2016-2017, Zend Technologies USA, Inc. diff --git a/LICENSE.md b/LICENSE.md index badc26ea..4095e570 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2016, Zend Technologies USA, Inc. +Copyright (c) 2016-2017, Zend Technologies USA, Inc. All rights reserved. diff --git a/README.md b/README.md index 2a7e94a8..caa8b276 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -Zend Framework Coding Standard -============================== +# Zend Framework Coding Standard -Repository with all coding standard ruleset for Zend Framework repositories. +[![Build Status](https://secure.travis-ci.org/zendframework/zend-coding-standard.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-coding-standard) +[![Coverage Status](https://coveralls.io/repos/zendframework/zend-coding-standard/badge.svg?branch=master)](https://coveralls.io/r/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: @@ -39,9 +39,7 @@ Installation 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: diff --git a/composer.json b/composer.json index 89f82020..e7b58df0 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,32 @@ "coding standard" ], "require": { - "squizlabs/php_codesniffer": "^2.7" + "php": "^5.6 || ^7.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "require-dev": { + "mikey179/vfsStream": "^1.6", + "phpunit/phpunit": "^6.1 || ^5.7.15" + }, + "autoload": { + "psr-4": { + "ZendCodingStandard\\": "src/ZendCodingStandard/" + } + }, + "autoload-dev": { + "psr-4": { + "ZendCodingStandardTest\\": "test/" + } + }, + "scripts": { + "check": [ + "@cs-check", + "@test" + ], + "cs-check": "phpcs -s", + "cs-fix": "phpcbf", + "test": "phpunit --colors=always", + "test-coverage": "phpunit --coverage-clover clover.xml", + "upload-coverage": "coveralls -v" } } diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 00000000..d24dc220 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,17 @@ + + + + + + + + + ./COPYRIGHT.md + ./LICENSE.md + + ./src + ./test + + + */TestAsset/* + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..be783ebf --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,17 @@ + + + + + test/ + + + + + + src + + + diff --git a/ruleset.xml b/ruleset.xml deleted file mode 100644 index 2e2536e0..00000000 --- a/ruleset.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - Zend Framework Coding Standard - - - - - - - - - - - - - - - - - - - - diff --git a/src/ZendCodingStandard/Sniffs/Commenting/FileLevelDocBlockSniff.php b/src/ZendCodingStandard/Sniffs/Commenting/FileLevelDocBlockSniff.php new file mode 100644 index 00000000..3ba49245 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Commenting/FileLevelDocBlockSniff.php @@ -0,0 +1,261 @@ +repo = $content['name']; + } + + /** + * Registers the tokens that this sniff wants to listen for. + * + * @return int[] + */ + public function register() + { + return [T_OPEN_TAG]; + } + + /** + * Called when one of the token types that this sniff is listening for is + * found. + * + * @param File $phpcsFile The PHP_CodeSniffer file where the token was found. + * @param int $stackPtr The position in the PHP_CodeSniffer file's token stack where the token was found. + * + * @return int Optionally returns a stack pointer. The sniff will not be called again on the current file until the + * returned stack pointer is reached. + */ + public function process(File $phpcsFile, $stackPtr) + { + // Skip license and copyright file + if (in_array(substr($phpcsFile->getFilename(), -10), ['LICENSE.md', 'COPYRIGHT.md'])) { + return ($phpcsFile->numTokens + 1); + } + + $tokens = $phpcsFile->getTokens(); + $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + + // Valid file-level DocBlock style + if ($tokens[$commentStart]['code'] === T_COMMENT) { + $phpcsFile->addError( + 'You must use "/**" style comments for a file-level DocBlock', + $commentStart, + 'WrongStyle' + ); + $phpcsFile->recordMetric($stackPtr, 'File has file-level DocBlock', 'yes'); + + return ($phpcsFile->numTokens + 1); + } + + // File-level DocBlock exists, part 1 + if ($commentStart === false || $tokens[$commentStart]['code'] !== T_DOC_COMMENT_OPEN_TAG) { + $phpcsFile->addError('Missing file-level DocBlock', $stackPtr, 'Missing'); + $phpcsFile->recordMetric($stackPtr, 'File has file-level DocBlock', 'no'); + + return ($phpcsFile->numTokens + 1); + } + + $commentEnd = $tokens[$commentStart]['comment_closer']; + $nextToken = $phpcsFile->findNext(T_WHITESPACE, $commentEnd + 1, null, true); + + // File-level DocBlock exists, part 2 + if (in_array($tokens[$nextToken]['code'], self::IGNORE) === true) { + $phpcsFile->addError('Missing file-level DocBlock', $stackPtr, 'Missing'); + $phpcsFile->recordMetric($stackPtr, 'File has file-level DocBlock', 'no'); + + return ($phpcsFile->numTokens + 1); + } + + // File-level DocBlock does exist + $phpcsFile->recordMetric($stackPtr, 'File has file-level DocBlock', 'yes'); + + // No blank line between the open tag and the file comment. + if ($tokens[$commentStart]['line'] > ($tokens[$stackPtr]['line'] + 1)) { + $error = 'There must be no blank lines before the file-level DocBlock'; + $phpcsFile->addError($error, $stackPtr, 'SpacingAfterOpen'); + } + + // Exactly one blank line after the file-level DocBlock + $next = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), null, true); + if ($tokens[$next]['line'] !== ($tokens[$commentEnd]['line'] + 2)) { + $error = 'There must be exactly one blank line after the file-level DocBlock'; + $fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfterComment'); + if ($fix === true) { + $phpcsFile->fixer->addNewline($commentEnd); + } + } + + // Required tags in correct order. + $required = [ + '@see' => true, + '@copyright' => true, + '@license' => true, + ]; + + $foundTags = []; + foreach ($tokens[$commentStart]['comment_tags'] as $tag) { + $name = $tokens[$tag]['content']; + $isRequired = isset($required[$name]); + + if ($isRequired === true && in_array($name, $foundTags) === true) { + $error = 'Only one %s tag is allowed in a file-level DocBlock'; + $data = [$name]; + $phpcsFile->addError($error, $tag, 'Duplicate' . ucfirst(substr($name, 1)) . 'Tag', $data); + } + + $foundTags[] = $name; + + if ($name === '@link') { + $error = 'Deprecated @link tag is used, use @see tag instead'; + $fix = $phpcsFile->addFixableError($error, $tag, 'DeprecatedLinkTag'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($tag, '@see '); + } + } + + if ($isRequired === false && $name !== '@link') { + continue; + } + + $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd); + if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) { + $error = 'Content missing for %s tag in file-level DocBlock'; + $data = [$name]; + $phpcsFile->addError($error, $tag, 'Empty' . ucfirst(substr($name, 1)) . 'Tag', $data); + continue; + } + + if ($name === '@see' || $name === '@link') { + $expected = sprintf('https://github.com/%s for the canonical source repository', $this->repo); + if (preg_match('|^' . $expected . '$|', $tokens[$string]['content']) === 0) { + $error = 'Expected "%s" for %s tag'; + $fix = $phpcsFile->addFixableError($error, $tag, 'IncorrectSourceLink', [$expected, $name]); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($string, $expected); + } + } + continue; + } + + if ($name === '@copyright') { + // Grab copyright date range + list($firstYear, $lastYear) = LicenseUtils::detectDateRange($tokens[$string]['content']); + + $expected = sprintf('https://github.com/%s/blob/master/COPYRIGHT.md Copyright', $this->repo); + if (preg_match('|^' . $expected . '$|', $tokens[$string]['content']) === 0) { + $error = 'Expected "%s" for @copyright tag'; + $fix = $phpcsFile->addFixableError($error, $tag, 'IncorrectCopyrightLink', [$expected]); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($string, $expected); + if ($firstYear !== null) { + LicenseUtils::buildFiles($firstYear, $lastYear); + } + } + } + continue; + } + + if ($name === '@license') { + $expected = sprintf('https://github.com/%s/blob/master/LICENSE.md New BSD License', $this->repo); + if (preg_match('|^' . $expected . '$|', $tokens[$string]['content']) === 0) { + $error = 'Expected "%s" for @license tag'; + $fix = $phpcsFile->addFixableError($error, $tag, 'IncorrectLicenseLink', [$expected]); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($string, $expected); + } + } + continue; + } + } + + // If a @link tag was detected, it already triggered errors at this + // point. Treat @link as @see to suppress even more errors and warnings + // which should have been fixed by renaming the tag. + if ($foundTags[0] === '@link') { + $foundTags[0] = '@see'; + } + + // Check if the tags are in the correct position. + $pos = 0; + foreach ($required as $tag => $true) { + if (in_array($tag, $foundTags) === false) { + $error = 'Missing %s tag in file-level DocBlock'; + $data = [$tag]; + $phpcsFile->addError($error, $commentEnd, 'Missing' . ucfirst(substr($tag, 1)) . 'Tag', $data); + } + + if (isset($foundTags[$pos]) === false) { + break; + } + + if ($foundTags[$pos] !== $tag) { + $error = 'The file-level DocBlock tag in position %s should be the %s tag'; + $data = [ + ($pos + 1), + $tag, + ]; + $phpcsFile->addWarning( + $error, + $tokens[$commentStart]['comment_tags'][$pos], + ucfirst(substr($tag, 1)) . 'TagOrder', + $data + ); + } + + $pos++; + } + + // Ignore the rest of the file. + return ($phpcsFile->numTokens + 1); + } +} diff --git a/src/ZendCodingStandard/Sniffs/Files/CopyrightSniff.php b/src/ZendCodingStandard/Sniffs/Files/CopyrightSniff.php new file mode 100644 index 00000000..59c2bf5e --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Files/CopyrightSniff.php @@ -0,0 +1,93 @@ +copyrightFile = LicenseUtils::getCopyrightFile(); + } + + /** + * Registers the tokens that this sniff wants to listen for. + * + * @return int[] + */ + public function register() + { + return [T_INLINE_HTML]; + } + + /** + * Called when one of the token types that this sniff is listening for is + * found. + * + * @param File $phpcsFile The PHP_CodeSniffer file where the token was found. + * @param int $stackPtr The position in the PHP_CodeSniffer file's token stack where the token was found. + * + * @return int Optionally returns a stack pointer. The sniff will not be called again on the current file until the + * returned stack pointer is reached. + */ + public function process(File $phpcsFile, $stackPtr) + { + // Skip all files except the copyright file + if (substr($phpcsFile->getFilename(), -10) !== 'COPYRIGHT.md') { + return ($phpcsFile->numTokens + 1); + } + + if (! $this->copyrightFile->getRealPath()) { + $error = 'Missing COPYRIGHT.md file in the component root dir'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'MissingLicense'); + if ($fix === true) { + LicenseUtils::buildFiles(); + } + + // Ignore the rest of the file. + return ($phpcsFile->numTokens + 1); + } + + // Get copyright dates + list($firstYear, $lastYear) = LicenseUtils::detectDateRange( + file_get_contents($this->copyrightFile->getRealPath()) + ); + + // Check copyright year + if (($lastYear === null && $firstYear !== gmdate('Y')) + || ($lastYear !== null && $lastYear !== gmdate('Y')) + ) { + $error = sprintf( + 'Expected "Copyright (c) %s" in COPYRIGHT.md', + LicenseUtils::formatDateRange($firstYear, gmdate('Y')) + ); + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'InvalidCopyrightDate'); + if ($fix === true) { + LicenseUtils::buildFiles($firstYear, $lastYear); + } + } + + // Ignore the rest of the file. + return ($phpcsFile->numTokens + 1); + } +} diff --git a/src/ZendCodingStandard/Sniffs/Files/LicenseSniff.php b/src/ZendCodingStandard/Sniffs/Files/LicenseSniff.php new file mode 100644 index 00000000..7de60570 --- /dev/null +++ b/src/ZendCodingStandard/Sniffs/Files/LicenseSniff.php @@ -0,0 +1,93 @@ +licenseFile = LicenseUtils::getLicenseFile(); + } + + /** + * Registers the tokens that this sniff wants to listen for. + * + * @return int[] + */ + public function register() + { + return [T_INLINE_HTML]; + } + + /** + * Called when one of the token types that this sniff is listening for is + * found. + * + * @param File $phpcsFile The PHP_CodeSniffer file where the token was found. + * @param int $stackPtr The position in the PHP_CodeSniffer file's token stack where the token was found. + * + * @return int Optionally returns a stack pointer. The sniff will not be called again on the current file until the + * returned stack pointer is reached. + */ + public function process(File $phpcsFile, $stackPtr) + { + // Skip all files except the license file + if (substr($phpcsFile->getFilename(), -10) !== $this->licenseFile->getFilename()) { + return ($phpcsFile->numTokens + 1); + } + + if (! $this->licenseFile->getRealPath()) { + $error = 'Missing LICENSE.md file in the component root dir'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'MissingLicense'); + if ($fix === true) { + LicenseUtils::buildFiles(); + } + + // Ignore the rest of the file. + return ($phpcsFile->numTokens + 1); + } + + // Get license dates + list($firstYear, $lastYear) = LicenseUtils::detectDateRange( + file_get_contents($this->licenseFile->getRealPath()) + ); + + // Check license year + if (($lastYear === null && $firstYear !== gmdate('Y')) + || ($lastYear !== null && $lastYear !== gmdate('Y')) + ) { + $error = sprintf( + 'Expected "Copyright (c) %s" in LICENSE.md', + LicenseUtils::formatDateRange($firstYear, gmdate('Y')) + ); + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'InvalidCopyrightDate'); + if ($fix === true) { + LicenseUtils::buildFiles($firstYear, $lastYear); + } + } + + // Ignore the rest of the file. + return ($phpcsFile->numTokens + 1); + } +} diff --git a/src/ZendCodingStandard/Utils/LicenseUtils.php b/src/ZendCodingStandard/Utils/LicenseUtils.php new file mode 100644 index 00000000..dc88e76c --- /dev/null +++ b/src/ZendCodingStandard/Utils/LicenseUtils.php @@ -0,0 +1,187 @@ +[\d]{4})(-(?[\d]{4}))?|', $string, $matches); + + $detectedFirstYear = isset($matches['start']) ? $matches['start'] : null; + $detectedLastYear = isset($matches['end']) ? $matches['end'] : null; + + if ($firstYear === null || $detectedFirstYear < $firstYear) { + $firstYear = $detectedFirstYear; + } + + if ($lastYear === null || $detectedLastYear > $lastYear) { + $lastYear = $detectedLastYear; + } + + return [$firstYear, $lastYear]; + } + + /** + * Format date range + * + * Returns a formatted date range from a given first and last year. If the + * last year is not set or the same as the first year it returns a single + * year. Otherwise it returns `-`. + * + * @param string|null $firstYear + * @param string|null $lastYear + * @return string + */ + public static function formatDateRange($firstYear = null, $lastYear = null) + { + $currentYear = gmdate('Y'); + if ($lastYear === null || $lastYear > $currentYear) { + $lastYear = $currentYear; + } + + $dateRange = $lastYear; + if ($firstYear !== null && $firstYear < $lastYear) { + $dateRange = sprintf('%s-%s', $firstYear, $lastYear); + } + + return $dateRange; + } + + /** + * Build copyright and license files + * + * This detects the current date range if any of the two files exist. And + * updates their content in case of any detected changes. + * + * @param null $firstYear + * @param null $lastYear + * @param SplFileInfo|null $copyrightFile + * @param SplFileInfo|null $licenseFile + */ + public static function buildFiles( + $firstYear = null, + $lastYear = null, + SplFileInfo $copyrightFile = null, + SplFileInfo $licenseFile = null + ) { + if ($copyrightFile === null) { + $copyrightFile = self::getCopyrightFile(); + } + + if ($licenseFile === null) { + $licenseFile = self::getLicenseFile(); + } + + // Get copyright dates + $oldCopyright = null; + if ($copyrightFile->isReadable()) { + $oldCopyright = file_get_contents($copyrightFile->getPathname()); + list($firstYear, $lastYear) = self::detectDateRange($oldCopyright, $firstYear, $lastYear); + } + + // Get license dates + $oldLicense = null; + if ($licenseFile->isReadable()) { + $oldLicense = file_get_contents($licenseFile->getPathname()); + list($firstYear, $lastYear) = self::detectDateRange($oldLicense, $firstYear, $lastYear); + } + + // Format date range, enforce current year + $copyrightDateRange = self::formatDateRange($firstYear, gmdate('Y')); + + // Save new copyright content if it's updated + $newCopyright = sprintf(self::$copyright, $copyrightDateRange); + if ($oldCopyright === null || $oldCopyright !== $newCopyright) { + file_put_contents($copyrightFile->getPathname(), $newCopyright); + } + + // Save new license if it's updated + $newLicense = sprintf(self::$license, $copyrightDateRange); + if ($oldLicense === null || $oldLicense !== $newLicense) { + file_put_contents($licenseFile->getPathname(), $newLicense); + } + } +} diff --git a/src/ZendCodingStandard/ruleset.xml b/src/ZendCodingStandard/ruleset.xml new file mode 100644 index 00000000..2b3e2c75 --- /dev/null +++ b/src/ZendCodingStandard/ruleset.xml @@ -0,0 +1,39 @@ + + + Zend Framework Coding Standard + + + + + + + + + + + + + + + + + + + + + + + + + *.md + + + + + *.md + + + *.md + + + diff --git a/test/SniffTestCase.php b/test/SniffTestCase.php new file mode 100644 index 00000000..365a4cd5 --- /dev/null +++ b/test/SniffTestCase.php @@ -0,0 +1,163 @@ +config = new Config(); + + // Don't cache files + $this->config->cache = false; + + // Set ruleset + $this->config->standards = ['src/ZendCodingStandard/ruleset.xml']; + } + + public function processAsset($asset) + { + $ruleset = new Ruleset($this->config); + + $file = new LocalFile($asset, $ruleset, $this->config); + $file->process(); + + return $file; + } + + public function assertErrorCount($expectedCount, File $file) + { + if (! is_int($expectedCount)) { + throw InvalidArgumentHelper::factory(1, 'integer'); + } + + $message = sprintf( + 'Failed asserting that "%s" has %d violations.', + str_replace(__DIR__, 'test', $file->getFilename()), + $expectedCount + ); + $this->assertEquals($expectedCount, $file->getErrorCount(), $message); + } + + public function assertHasError($expectedError, File $file) + { + foreach ($file->getErrors() as $line => $lineErrors) { + foreach ($lineErrors as $column => $errors) { + foreach ($errors as $error) { + if (isset($error['source']) && $error['source'] === $expectedError) { + $this->assertTrue(true); + + return; + } + } + } + } + + $message = sprintf( + 'Failed asserting that "%s" has "%s" error.', + str_replace(__DIR__, 'test', $file->getFilename()), + $expectedError + ); + $this->assertTrue(false, $message); + } + + public function assertWarningCount($expectedCount, File $file) + { + if (! is_int($expectedCount)) { + throw InvalidArgumentHelper::factory(1, 'integer'); + } + + $message = sprintf( + 'Failed asserting that "%s" has %d warnings.', + str_replace(__DIR__, 'test', $file->getFilename()), + $expectedCount + ); + $this->assertEquals($expectedCount, $file->getWarningCount(), $message); + } + + public function assertHasWarning($expectedError, File $file) + { + foreach ($file->getWarnings() as $line => $lineErrors) { + foreach ($lineErrors as $column => $errors) { + foreach ($errors as $error) { + if (isset($error['source']) && $error['source'] === $expectedError) { + $this->assertTrue(true); + + return; + } + } + } + } + + $message = sprintf( + 'Failed asserting that "%s" has "%s" warning.', + str_replace(__DIR__, 'test', $file->getFilename()), + $expectedError + ); + $this->assertTrue(false, $message); + } + + public function assertAssetCanBeFixed($fixed, File $file) + { + if ($fixed === null) { + $message = sprintf( + 'Failed asserting that "%s" has no fixable violations.', + str_replace(__DIR__, 'test', $file->getFilename()) + ); + $this->assertEquals(0, $file->getFixableCount(), $message); + + return; + } + + // Try to fix the file + $file->fixer->fixFile(); + $message = sprintf( + 'Failed to fix %d fixable violations in "%s".', + $file->getFixableCount(), + str_replace(__DIR__, 'test', $file->getFilename()) + ); + $this->assertEquals(0, $file->getFixableCount(), $message); + + // Validate fixes + $message = sprintf( + 'Failed asserting that "%s" has all fixable violations fixed.', + str_replace(__DIR__, 'test', $file->getFilename()) + ); + $this->assertEquals('', trim($file->fixer->generateDiff($fixed)), $message); + } +} diff --git a/test/Sniffs/Commenting/FileLevelDocBlockSniffTest.php b/test/Sniffs/Commenting/FileLevelDocBlockSniffTest.php new file mode 100644 index 00000000..b55518ca --- /dev/null +++ b/test/Sniffs/Commenting/FileLevelDocBlockSniffTest.php @@ -0,0 +1,189 @@ +processAsset($asset); + + $this->assertErrorCount($errorCount, $file); + $this->assertWarningCount($warningCount, $file); + + foreach ($errors as $error) { + $this->assertHasError($error, $file); + } + + foreach ($warnings as $warning) { + $this->assertHasWarning($warning, $file); + } + + $this->assertAssetCanBeFixed($fixed, $file); + } + + public function assetsProvider() + { + return [ + 'FileLevelDocBlockPass' => [ + 'asset' => __DIR__ . '/TestAsset/FileLevelDocBlockPass.php', + 'fixed' => null, + 'errorCount' => 0, + 'errors' => [], + 'warningCount' => 0, + 'warnings' => [], + ], + + 'FileLevelDocBlockMissing' => [ + 'asset' => __DIR__ . '/TestAsset/FileLevelDocBlockMissing.php', + 'fixed' => null, + 'errorCount' => 1, + 'errors' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.Missing', + ], + 'warningCount' => 0, + 'warnings' => [], + ], + + 'FileLevelDocBlockMissingSeeTag' => [ + 'asset' => __DIR__ . '/TestAsset/FileLevelDocBlockMissingSeeTag.php', + 'fixed' => null, + 'errorCount' => 1, + 'errors' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.MissingSeeTag', + ], + 'warningCount' => 1, + 'warnings' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.SeeTagOrder', + ], + ], + + 'FileLevelDocBlockMissingCopyrightTag' => [ + 'asset' => __DIR__ . '/TestAsset/FileLevelDocBlockMissingCopyrightTag.php', + 'fixed' => null, + 'errorCount' => 1, + 'errors' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.MissingCopyrightTag', + ], + 'warningCount' => 1, + 'warnings' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.CopyrightTagOrder', + ], + ], + + 'FileLevelDocBlockMissingLicenseTag' => [ + 'asset' => __DIR__ . '/TestAsset/FileLevelDocBlockMissingLicenseTag.php', + 'fixed' => null, + 'errorCount' => 1, + 'errors' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.MissingLicenseTag', + ], + 'warningCount' => 1, + 'warnings' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.LicenseTagOrder', + ], + ], + + 'FileLevelDocBlockWrongStyle' => [ + 'asset' => __DIR__ . '/TestAsset/FileLevelDocBlockWrongStyle.php', + 'fixed' => null, + 'errorCount' => 1, + 'errors' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.WrongStyle', + ], + 'warningCount' => 0, + 'warnings' => [], + ], + + 'FileLevelDocBlockSpacingAfterOpen' => [ + 'asset' => __DIR__ . '/TestAsset/FileLevelDocBlockSpacingAfterOpen.php', + 'fixed' => null, + 'errorCount' => 1, + 'errors' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.SpacingAfterOpen', + ], + 'warningCount' => 0, + 'warnings' => [], + ], + + 'FileLevelDocBlockSpacingAfterComment' => [ + 'asset' => __DIR__ . '/TestAsset/FileLevelDocBlockSpacingAfterComment.php', + 'fixed' => __DIR__ . '/TestAsset/FileLevelDocBlockSpacingAfterComment.fixed.php', + 'errorCount' => 1, + 'errors' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.SpacingAfterComment', + ], + 'warningCount' => 0, + 'warnings' => [], + ], + + 'FileLevelDocBlockIncorrectSourceLink' => [ + 'asset' => __DIR__ . '/TestAsset/FileLevelDocBlockIncorrectSourceLink.php', + 'fixed' => __DIR__ . '/TestAsset/FileLevelDocBlockIncorrectSourceLink.fixed.php', + 'errorCount' => 1, + 'errors' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.IncorrectSourceLink', + ], + 'warningCount' => 0, + 'warnings' => [], + ], + + 'FileLevelDocBlockIncorrectCopyrightLink' => [ + 'asset' => __DIR__ . '/TestAsset/FileLevelDocBlockIncorrectCopyrightLink.php', + 'fixed' => __DIR__ . '/TestAsset/FileLevelDocBlockIncorrectCopyrightLink.fixed.php', + 'errorCount' => 1, + 'errors' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.IncorrectCopyrightLink', + ], + 'warningCount' => 0, + 'warnings' => [], + ], + + 'FileLevelDocBlockIncorrectLicenseLink' => [ + 'asset' => __DIR__ . '/TestAsset/FileLevelDocBlockIncorrectLicenseLink.php', + 'fixed' => __DIR__ . '/TestAsset/FileLevelDocBlockIncorrectLicenseLink.fixed.php', + 'errorCount' => 1, + 'errors' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.IncorrectLicenseLink', + ], + 'warningCount' => 0, + 'warnings' => [], + ], + + 'FileLevelDocBlockEmptyTags' => [ + 'asset' => __DIR__ . '/TestAsset/FileLevelDocBlockEmptyTags.php', + 'fixed' => null, + 'errorCount' => 3, + 'errors' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.EmptySeeTag', + 'ZendCodingStandard.Commenting.FileLevelDocBlock.EmptyCopyrightTag', + 'ZendCodingStandard.Commenting.FileLevelDocBlock.EmptyLicenseTag', + ], + 'warningCount' => 0, + 'warnings' => [], + ], + + 'FileLevelDocBlockDeprecatedLinkTag' => [ + 'asset' => __DIR__ . '/TestAsset/FileLevelDocBlockDeprecatedLinkTag.php', + 'fixed' => __DIR__ . '/TestAsset/FileLevelDocBlockDeprecatedLinkTag.fixed.php', + 'errorCount' => 2, + 'errors' => [ + 'ZendCodingStandard.Commenting.FileLevelDocBlock.DeprecatedLinkTag', + 'ZendCodingStandard.Commenting.FileLevelDocBlock.IncorrectSourceLink', + ], + 'warningCount' => 0, + 'warnings' => [], + ], + ]; + } +} diff --git a/test/Sniffs/Commenting/TestAsset/FileLevelDocBlockDeprecatedLinkTag.fixed.php b/test/Sniffs/Commenting/TestAsset/FileLevelDocBlockDeprecatedLinkTag.fixed.php new file mode 100644 index 00000000..6baac6ec --- /dev/null +++ b/test/Sniffs/Commenting/TestAsset/FileLevelDocBlockDeprecatedLinkTag.fixed.php @@ -0,0 +1,12 @@ +root = vfsStream::setup('tmp'); + } + + /** + * @dataProvider dateRangeDetectionProvider + */ + public function testDateRangeDetection($string, $firstYear, $lastYear, $expectedFirstYear, $expectedLastYear) + { + list($actualFirstYear, $actualLastYear) = LicenseUtils::detectDateRange($string, $firstYear, $lastYear); + + $this->assertEquals($expectedFirstYear, $actualFirstYear); + $this->assertEquals($expectedLastYear, $actualLastYear); + } + + public function dateRangeDetectionProvider() + { + return [ + 'empty' => ['Copyright (c) Foo', null, null, null, null], + '2014' => ['(c) 2014 Foo', null, null, 2014, null], + '2015-2016' => ['(c) 2015-2016 Bar', null, null, 2015, 2016], + '2016-current' => [sprintf('(c) 2016-%s', gmdate('Y')), null, null, 2016, gmdate('Y')], + 'current' => [sprintf('(c) %s', gmdate('Y')), null, null, gmdate('Y'), null], + 'o2012' => ['(c) 2014 Foo', 2012, null, 2012, null], + 'o2016' => ['(c) 2014 Foo', 2016, null, 2014, null], + 'o2012-o2015' => ['(c) 2014 Foo', 2012, 2015, 2012, 2015], + 'o2012-o2016' => ['(c) 2014-2015', 2012, 2016, 2012, 2016], + 'o2016-oCurrent' => [sprintf('(c) 2016-%s', gmdate('Y')), 2012, 2016, 2012, gmdate('Y')], + 'oCurrent' => [sprintf('(c) %s', gmdate('Y')), 2012, 2014, 2012, 2014], + ]; + } + + /** + * @dataProvider dateRangeFormatProvider + */ + public function testFormatDateRange($firstYear, $lastYear, $expected) + { + $this->assertEquals($expected, LicenseUtils::formatDateRange($firstYear, $lastYear)); + } + + public function dateRangeFormatProvider() + { + return [ + '2014' => ['2014', null, '2014-' . gmdate('Y')], + '2015-2016' => ['2015', '2016', '2015-2016'], + '2016-current' => ['2016', gmdate('Y'), '2016-' . gmdate('Y')], + 'current' => [gmdate('Y'), null, gmdate('Y')], + ]; + } + + public function testBuildNewFiles() + { + $firstYear = null; + $lastYear = null; + $copyrightFile = new SplFileInfo($this->root->url() . '/COPYRIGHT.tmp'); + $licenseFile = new SplFileInfo($this->root->url() . '/LICENSE.tmp'); + + LicenseUtils::buildFiles($firstYear, $lastYear, $copyrightFile, $licenseFile); + + $this->assertTrue($this->root->hasChild('COPYRIGHT.tmp')); + $this->assertEquals( + sprintf(LicenseUtils::$copyright, LicenseUtils::formatDateRange(gmdate('Y'))), + $this->root->getChild('COPYRIGHT.tmp')->getContent() + ); + + $this->assertTrue($this->root->hasChild('LICENSE.tmp')); + $this->assertEquals( + sprintf(LicenseUtils::$license, LicenseUtils::formatDateRange(gmdate('Y'))), + $this->root->getChild('LICENSE.tmp')->getContent() + ); + } + + public function testUpdateBothFilesWithSameDates() + { + $copyrightFile = new SplFileInfo($this->root->url() . '/COPYRIGHT.tmp'); + $licenseFile = new SplFileInfo($this->root->url() . '/LICENSE.tmp'); + + file_put_contents( + $copyrightFile->getPathname(), + sprintf(LicenseUtils::$copyright, LicenseUtils::formatDateRange('2015')) + ); + + file_put_contents( + $licenseFile->getPathname(), + sprintf(LicenseUtils::$license, LicenseUtils::formatDateRange('2016-2017')) + ); + + LicenseUtils::buildFiles('2016', '2016', $copyrightFile, $licenseFile); + + $this->assertTrue($this->root->hasChild('COPYRIGHT.tmp')); + $this->assertEquals( + sprintf(LicenseUtils::$copyright, LicenseUtils::formatDateRange('2015', gmdate('Y'))), + $this->root->getChild('COPYRIGHT.tmp')->getContent() + ); + + $this->assertTrue($this->root->hasChild('LICENSE.tmp')); + $this->assertEquals( + sprintf(LicenseUtils::$license, LicenseUtils::formatDateRange('2015', gmdate('Y'))), + $this->root->getChild('LICENSE.tmp')->getContent() + ); + } +}