diff --git a/COPYRIGHT.md b/COPYRIGHT.md new file mode 100644 index 00000000..42bc88cf --- /dev/null +++ b/COPYRIGHT.md @@ -0,0 +1 @@ +Copyright (c) 2016-2018, Zend Technologies USA, Inc. diff --git a/composer.json b/composer.json index 7209b89d..b32e13f7 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "phpunit/phpunit": "^7.0.1" }, "autoload": { + "files": ["src/ZendCodingStandard/helper.php"], "psr-4": { "ZendCodingStandard\\": "src/ZendCodingStandard/" } diff --git a/phpcs.xml b/phpcs.xml index 13388786..849465f6 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -4,8 +4,11 @@ xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd"> - src - test - + . test/*.inc + vendor/ + + + src/ZendCodingStandard/helper.php + diff --git a/ruleset.xml b/ruleset.xml index 35c6319a..92248c0d 100644 --- a/ruleset.xml +++ b/ruleset.xml @@ -1,4 +1,8 @@ + + ../../../ + vendor/ + test/**/TestAsset/ diff --git a/src/ZendCodingStandard/CodingStandard.php b/src/ZendCodingStandard/CodingStandard.php index e1919fd4..a72729f7 100644 --- a/src/ZendCodingStandard/CodingStandard.php +++ b/src/ZendCodingStandard/CodingStandard.php @@ -1,4 +1,9 @@ getTokens(); + $next = $phpcsFile->findNext(T_WHITESPACE, $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 the first statement in the file is strict type declaration. + if ($string && stripos($tokens[$string]['content'], 'strict_types') !== false) { + $eos = $phpcsFile->findEndOfStatement($next); + $next = $phpcsFile->findNext(T_WHITESPACE, $eos + 1, null, true); + } + } + + if ($next && $tokens[$next]['code'] === T_DOC_COMMENT_OPEN_TAG) { + $prev = $phpcsFile->findPrevious(T_WHITESPACE, $next - 1, null, true); + if ($tokens[$prev]['code'] === T_OPEN_TAG + && $tokens[$prev]['line'] + 1 !== $tokens[$next]['line'] + ) { + $error = 'License header must be in next line after opening PHP tag'; + $fix = $phpcsFile->addFixableError($error, $next, 'Line'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $next - 1; $i > $prev; --$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->endChangeset(); + } + } + + $content = $phpcsFile->getTokensAsString($next, $tokens[$next]['comment_closer'] - $next + 1); + $comment = $this->getComment(); + + if ($comment === $content) { + return $phpcsFile->numTokens + 1; + } + + if ($this->hasTags($phpcsFile, $next)) { + $error = 'Invalid doc license header'; + $fix = $phpcsFile->addFixableError($error, $next, 'Invalid'); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $next; $i < $tokens[$next]['comment_closer']; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->replaceToken($tokens[$next]['comment_closer'], $comment); + $phpcsFile->fixer->endChangeset(); + } + + return $phpcsFile->numTokens + 1; + } + } + + $error = 'Missing license header'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Missing'); + + if ($fix) { + $phpcsFile->fixer->addContent($stackPtr, $this->getComment() . $phpcsFile->eolChar); + } + + return $phpcsFile->numTokens + 1; + } + + private function getComment() : string + { + return strtr($this->comment, array_merge($this->getDefaultVariables(), $this->variables)); + } + + private function hasTags(File $phpcsFile, int $comment) : bool + { + $tokens = $phpcsFile->getTokens(); + $tags = ['@copyright' => true, '@license' => true]; + + foreach ($tokens[$comment]['comment_tags'] ?? [] as $token) { + $content = strtolower($tokens[$token]['content']); + unset($tags[$content]); + } + + return ! $tags; + } + + private function getDefaultVariables() : array + { + return [ + '{org}' => Config::getConfigData('zfcs:org') ?: 'zendframework', + '{repo}' => Config::getConfigData('zfcs:repo'), + ]; + } +} diff --git a/src/ZendCodingStandard/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseSniff.php b/src/ZendCodingStandard/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseSniff.php index 2001db83..60af2c68 100644 --- a/src/ZendCodingStandard/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseSniff.php +++ b/src/ZendCodingStandard/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseSniff.php @@ -1,4 +1,9 @@ run) { + return $phpcsFile->numTokens + 1; + } + $this->run = true; + + foreach ($this->files as $file) { + if (! file_exists($file)) { + $error = 'File %s does not exist'; + $data = [$file]; + $fix = $phpcsFile->addFixableError($error, 0, 'NotExists', $data); + + if ($fix) { + touch($file); + } + } + } + + return $phpcsFile->numTokens + 1; + } +} diff --git a/src/ZendCodingStandard/Sniffs/Files/DeclareStrictTypesSniff.php b/src/ZendCodingStandard/Sniffs/Files/DeclareStrictTypesSniff.php index ddf542a7..a473142e 100644 --- a/src/ZendCodingStandard/Sniffs/Files/DeclareStrictTypesSniff.php +++ b/src/ZendCodingStandard/Sniffs/Files/DeclareStrictTypesSniff.php @@ -1,4 +1,9 @@ 'https://raw.githubusercontent.com/zendframework/maintainers/master/template/LICENSE.md', + 'COPYRIGHT.md' => 'Copyright (c) {year}, Zend Technologies USA, Inc.' . "\n", + 'CODE_OF_CONDUCT.md' => 'https://raw.githubusercontent.com/zendframework/maintainers/master/template/docs/CODE_OF_CONDUCT.md', + 'CONTRIBUTING.md' => 'https://raw.githubusercontent.com/zendframework/maintainers/master/template/docs/CONTRIBUTING.md', + 'ISSUE_TEMPLATE.md' => 'https://raw.githubusercontent.com/zendframework/maintainers/master/template/docs/ISSUE_TEMPLATE.md', + 'PULL_REQUEST_TEMPLATE.md' => 'https://raw.githubusercontent.com/zendframework/maintainers/master/template/docs/PULL_REQUEST_TEMPLATE.md', + 'SUPPORT.md' => 'https://raw.githubusercontent.com/zendframework/maintainers/master/template/docs/SUPPORT.md', + // @phpcs:enable + ]; + + /** + * @var string[] + */ + public $variables = []; + + /** + * @var string + */ + public $yearTimezone = 'GMT'; + + /** + * @var null|string + */ + private $yearRange; + + /** + * @return int[] + */ + public function register() : array + { + return [T_MD_LINE]; + } + + /** + * @param int $stackPtr + * @return int + */ + public function process(File $phpcsFile, $stackPtr) + { + $template = $this->getTemplate($phpcsFile->getFilename()); + if ($template === null) { + $tokens = $phpcsFile->getTokens(); + + do { + $content = $tokens[$stackPtr]['content']; + $expected = rtrim($content) . (substr($content, -1) === "\n" ? "\n" : ''); + + if ($content !== $expected) { + $error = 'White characters found at the end of the line'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'WhiteChars'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($stackPtr, $expected); + } + } + } while ($stackPtr = $phpcsFile->findNext(T_MD_LINE, $stackPtr + 1)); + + if (trim($tokens[$phpcsFile->numTokens - 1]['content']) !== '') { + $error = 'Missing empty line at the end of the file'; + $fix = $phpcsFile->addFixableError($error, $phpcsFile->numTokens - 1, 'MissingEmptyLine'); + + if ($fix) { + $phpcsFile->fixer->addNewline($phpcsFile->numTokens - 1); + } + } elseif ($phpcsFile->numTokens > 1 && trim($tokens[$phpcsFile->numTokens - 2]['content']) === '') { + $error = 'Redundant empty line at the end of the file'; + $fix = $phpcsFile->addFixableError($error, $phpcsFile->numTokens - 1, 'RedundantEmptyLine'); + + if ($fix) { + $phpcsFile->fixer->replaceToken($phpcsFile->numTokens - 2, ''); + } + } + + return $phpcsFile->numTokens + 1; + } + + $content = $phpcsFile->getTokensAsString(0, $phpcsFile->numTokens); + + $newContent = strtr($template, array_merge($this->getDefaultVariables(), $this->variables)); + + if ($content !== $newContent) { + $error = 'Content is outdated; found %s; expected %s'; + $data = [$content, $newContent]; + $code = ucfirst(strtolower(strstr(basename($phpcsFile->getFilename()), '.', true))); + $fix = $phpcsFile->addFixableError($error, $stackPtr, $code, $data); + + if ($fix) { + $phpcsFile->fixer->beginChangeset(); + for ($i = 0; $i < $phpcsFile->numTokens; ++$i) { + $phpcsFile->fixer->replaceToken($i, ''); + } + $phpcsFile->fixer->addContent(0, $newContent); + $phpcsFile->fixer->endChangeset(); + } + } + + return $phpcsFile->numTokens + 1; + } + + private function getTemplate(string $filename) : ?string + { + foreach ($this->templates as $name => $template) { + if (strpos($filename, '/' . $name) !== false) { + if (filter_var($template, FILTER_VALIDATE_URL)) { + return file_get_contents($template); + } + + return $template; + } + } + + return null; + } + + private function getDefaultVariables() : array + { + return [ + '{category}' => Config::getConfigData('zfcs:category') ?: 'components', + '{org}' => Config::getConfigData('zfcs:org') ?: 'zendframework', + '{repo}' => Config::getConfigData('zfcs:repo'), + '{year}' => $this->getYearRange(), + ]; + } + + private function getYearRange() : string + { + if (! $this->yearRange) { + $timezone = new DateTimeZone($this->yearTimezone); + + exec('git tag -l --format=\'%(taggerdate)\' | head -n 1', $output, $return); + $date = new DateTime($return === 0 && isset($output[0]) ? $output[0] : 'now'); + $date->setTimezone($timezone); + $this->yearRange = $date->format('Y'); + + $currentYear = (new DateTime('now', $timezone))->format('Y'); + if ($this->yearRange < $currentYear) { + $this->yearRange .= '-' . $currentYear; + } + } + + return $this->yearRange; + } +} diff --git a/src/ZendCodingStandard/Sniffs/Formatting/DoubleColonSniff.php b/src/ZendCodingStandard/Sniffs/Formatting/DoubleColonSniff.php index a7c0f328..1a7749c6 100644 --- a/src/ZendCodingStandard/Sniffs/Formatting/DoubleColonSniff.php +++ b/src/ZendCodingStandard/Sniffs/Formatting/DoubleColonSniff.php @@ -1,4 +1,9 @@ &$line) { + $line = [ + 'content' => $line . "\n", + 'code' => T_MD_LINE, + 'type' => 'T_MD_LINE', + ]; + } + + $line['content'] = preg_replace('/\n$/', '', $line['content']); + + return $lines; + } + + /** + * Performs additional processing after main tokenizing. + */ + protected function processAdditional() + { + } +} diff --git a/src/ZendCodingStandard/helper.php b/src/ZendCodingStandard/helper.php new file mode 100644 index 00000000..facc4ea6 --- /dev/null +++ b/src/ZendCodingStandard/helper.php @@ -0,0 +1,34 @@ + Zend Framework Coding Standard + + + diff --git a/test/Ruleset.php b/test/Ruleset.php index 59f7e27d..dbdabc3b 100644 --- a/test/Ruleset.php +++ b/test/Ruleset.php @@ -1,4 +1,9 @@ 1]; + case 'LicenseHeaderUnitTest.3.inc': + return [3 => 2]; + } + + return [ + 1 => 1, + ]; + } + + public function getWarningList(string $testFile = '') : array + { + return []; + } +} diff --git a/test/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseUnitTest.php b/test/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseUnitTest.php index 038c01b8..e7b2cdc2 100644 --- a/test/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseUnitTest.php +++ b/test/Sniffs/Commenting/NoInlineCommentAfterCurlyCloseUnitTest.php @@ -1,4 +1,9 @@