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.
+[](https://secure.travis-ci.org/zendframework/zend-coding-standard)
+[](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()
+ );
+ }
+}