Created
April 7, 2017 17:16
-
-
Save geerteltink/34ad30734f49f02f495b97bb77a09969 to your computer and use it in GitHub Desktop.
Zend Framework source file docblock license validation PoC
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @see https://github.com/zendframework/zend-coding-standard for the canonical source repository | |
* @copyright https://github.com/zendframework/zend-coding-standard/blob/master/COPYING.md Copyright | |
* @license https://github.com/zendframework/zend-coding-standard/blob/master/LICENSE.md New BSD License | |
*/ | |
class ZendCodingStandard_Sniffs_Commenting_FileLevelDocBlockSniff implements \PHP_CodeSniffer_Sniff | |
{ | |
private $changedFiles; | |
private $currentFile; | |
private $repo; | |
public function __construct() | |
{ | |
// Grab Travis CI environment variables | |
$travisBranch = getenv('TRAVIS_BRANCH'); | |
$travisPullRequest = getenv('TRAVIS_PULL_REQUEST'); | |
// TODO: Check how this works out when PR/branch is merged into master or develop | |
//$commitRange = 'HEAD~..HEAD'; | |
$commitRange = 'origin..HEAD'; | |
if (! empty($travisPullRequest) && $travisPullRequest !== 'false') { | |
$commitRange = sprintf('%s..FETCH_HEAD', $travisBranch); | |
} | |
// Get all changed files in current branch | |
exec('git diff --name-only --diff-filter=ACMRTUXB ' . escapeshellcmd($commitRange), $files); | |
$files = array_unique($files); | |
foreach ($files as $filename) { | |
$file = new SplFileObject($filename); | |
if ($file->getExtension() !== 'php' || ! $file->isReadable()) { | |
continue; | |
} | |
$this->changedFiles[] = $file->getRealPath(); | |
} | |
$content = file_get_contents('composer.json'); | |
$content = json_decode($content, true); | |
$this->repo = $content['name']; | |
} | |
/** | |
* Registers the tokens that this sniff wants to listen for. | |
* | |
* @return int[] | |
* @see Tokens.php | |
*/ | |
public function register() | |
{ | |
return [T_OPEN_TAG]; | |
} | |
/** | |
* Called when one of the token types that this sniff is listening for is | |
* found. | |
* | |
* @param PHP_CodeSniffer_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. Return (count($tokens) + 1) to skip | |
* the rest of the file. | |
*/ | |
public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) | |
{ | |
$this->currentFile = $phpcsFile; | |
$tokens = $phpcsFile->getTokens(); | |
// Only executed if file is changed | |
if (! in_array($phpcsFile->getFilename(), $this->changedFiles, false)) { | |
// Skip the rest of the file | |
return (count($tokens) + 1); | |
} | |
$commentStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); | |
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 doc comment', 'yes'); | |
return ($phpcsFile->numTokens + 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 | |
); | |
$ignore = [ | |
T_CLASS, | |
T_INTERFACE, | |
T_TRAIT, | |
T_FUNCTION, | |
T_CLOSURE, | |
T_PUBLIC, | |
T_PRIVATE, | |
T_PROTECTED, | |
T_FINAL, | |
T_STATIC, | |
T_ABSTRACT, | |
T_CONST, | |
T_PROPERTY, | |
T_INCLUDE, | |
T_INCLUDE_ONCE, | |
T_REQUIRE, | |
T_REQUIRE_ONCE, | |
]; | |
if (in_array($tokens[$nextToken]['code'], $ignore) === true) { | |
$phpcsFile->addError('Missing file-level DocBlock', $stackPtr, 'Missing'); | |
$phpcsFile->recordMetric($stackPtr, 'File has file-level DocBlock', 'no'); | |
return ($phpcsFile->numTokens + 1); | |
} | |
$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 comment. | |
$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'; | |
$phpcsFile->addError($error, $commentEnd, 'SpacingAfterComment'); | |
} | |
// 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 ($isRequired === false) { | |
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' && preg_match( | |
'|^https://github.com/' . $this->repo . ' for the canonical source repository$|', | |
$tokens[$string]['content'] | |
) === 0 | |
) { | |
$expected = sprintf('https://github.com/%s for the canonical source repository', $this->repo); | |
$error = 'Expected "%s" for @see tag'; | |
$fix = $phpcsFile->addFixableError($error, $tag, 'IncorrectFileLevelDocBlockLink', [$expected]); | |
if ($fix === true) { | |
$phpcsFile->fixer->replaceToken($string, $expected); | |
} | |
continue; | |
} | |
if ($name === '@copyright' && preg_match( | |
'|^Copyright \(c\) ([\d]{4}-)?' . gmdate('Y') . ' Zend Technologies USA Inc. \(http://www.zend.com\)$|', | |
$tokens[$string]['content'] | |
) === 0 | |
) { | |
// Grab license date range | |
$matches = []; | |
preg_match('|(?<start>[\d]{4})(-(?<end>[\d]{4}))?|', $tokens[$string]['content'], $matches); | |
$licenseEnd = gmdate('Y'); | |
$licenseStart = isset($matches['start']) ? $matches['start'] : null; | |
if ($licenseStart > $licenseEnd) { | |
$licenseStart = $licenseEnd; | |
} | |
if ($licenseStart === $licenseEnd) { | |
$licenseEnd = null; | |
} | |
$expected = sprintf( | |
'Copyright (c) %s%s%s Zend Technologies USA Inc. (http://www.zend.com)', | |
$licenseStart, | |
($licenseEnd ? '-' : ''), | |
$licenseEnd | |
); | |
$error = 'Expected "%s" for @copyright tag'; | |
$fix = $phpcsFile->addFixableError($error, $tag, 'IncorrectFileLevelDocBlockCopyright', [$expected]); | |
if ($fix === true) { | |
$phpcsFile->fixer->replaceToken($string, $expected); | |
} | |
continue; | |
} | |
if ($name === '@license' && preg_match( | |
'|^https://github.com/' . $this->repo . '/blob/master/LICENSE.md New BSD License$|', | |
$tokens[$string]['content'] | |
) === 0 | |
) { | |
$expected = sprintf('https://github.com/%s/blob/master/LICENSE.md New BSD License', $this->repo); | |
$error = 'Expected "%s" for @license tag'; | |
$fix = $phpcsFile->addFixableError( | |
$error, | |
$tag, | |
'IncorrectFileLevelDocBlockLicenseLink', | |
[$expected] | |
); | |
if ($fix === true) { | |
$phpcsFile->fixer->replaceToken($string, $expected); | |
} | |
continue; | |
} | |
} | |
// 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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/php | |
<?php | |
/** | |
* @see https://github.com/zendframework/zend-coding-standard for the canonical source repository | |
* @copyright https://github.com/zendframework/zend-coding-standard/blob/master/COPYING.md Copyright | |
* @license https://github.com/zendframework/zend-coding-standard/blob/master/LICENSE.md New BSD License | |
*/ | |
namespace Zend\CodingStandard; | |
use SplFileObject; | |
// Grab Travis CI environment variables | |
$TRAVIS_BRANCH = getenv('TRAVIS_BRANCH'); | |
$TRAVIS_PULL_REQUEST = getenv('TRAVIS_PULL_REQUEST'); | |
if (! empty($TRAVIS_PULL_REQUEST) && $TRAVIS_PULL_REQUEST !== 'false') { | |
$COMMIT_RANGE = sprintf('%s..FETCH_HEAD', $TRAVIS_BRANCH); | |
} else { | |
//$COMMIT_RANGE = 'HEAD~..HEAD'; | |
$COMMIT_RANGE = 'origin..HEAD'; | |
} | |
// Get all changed files | |
exec('git diff --name-only --diff-filter=ACMRTUXB ' . escapeshellcmd($COMMIT_RANGE), $files); | |
echo 'Testing php files for valid docheaders' . PHP_EOL; | |
echo PHP_EOL; | |
//$files = array_unique($argv); | |
$files = array_unique($files); | |
$errors = []; | |
foreach ($files as $filename) { | |
$file = new SplFileObject($filename); | |
if ($file->getExtension() !== 'php' || ! $file->isReadable()) { | |
continue; | |
} | |
$result = [ | |
'see' => false, | |
'copyright' => false, | |
'license' => false, | |
]; | |
while (! $file->eof()) { | |
$line = $file->fgets(); | |
if (preg_match( | |
'|@see[\s]+https://github.com/zendframework/zend-[a-z\-]+ for the canonical source repository|', | |
$line | |
) | |
) { | |
$result['see'] = true; | |
} | |
if (preg_match( | |
'|@copyright[\s]+Copyright \(c\) ([\d]{4}-)?' | |
. gmdate('Y') | |
. ' Zend Technologies USA Inc. \(http://www.zend.com\)|', | |
$line | |
) | |
) { | |
$result['copyright'] = true; | |
} | |
if (preg_match( | |
'|@license[\s]+https://github.com/zendframework/zend-[a-z\-]+/blob/master/LICENSE.md New BSD License|', | |
$line | |
) | |
) { | |
$result['license'] = true; | |
} | |
// Check the first 10 lines only | |
if ($file->key() > 10) { | |
break; | |
} | |
} | |
if ($result['see'] !== true || $result['copyright'] !== true || $result['license'] !== true) { | |
echo 'E'; | |
$errors[$filename] = $result; | |
} else { | |
echo '.'; | |
} | |
} | |
echo PHP_EOL; | |
echo PHP_EOL; | |
$totalErrors = count($errors); | |
if ($totalErrors > 0) { | |
echo '----------------------------------------' . PHP_EOL; | |
foreach ($errors as $filename => $result) { | |
echo sprintf( | |
'%s%s%s %s' . PHP_EOL, | |
($result['see'] !== true) ? 'S' : '.', | |
($result['copyright'] !== true) ? 'C' : '.', | |
($result['license'] !== true) ? 'L' : '.', | |
$filename | |
); | |
} | |
echo '----------------------------------------' . PHP_EOL; | |
echo PHP_EOL; | |
echo sprintf('Found %d docheader errors.', $totalErrors); | |
} else { | |
echo sprintf('No errors found.'); | |
} | |
echo PHP_EOL; | |
// TODO: Push status to GitHub | |
// https://developer.github.com/v3/repos/statuses/#create-a-status |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment