Skip to content

Instantly share code, notes, and snippets.

@geerteltink
Created April 7, 2017 17:16
Show Gist options
  • Save geerteltink/34ad30734f49f02f495b97bb77a09969 to your computer and use it in GitHub Desktop.
Save geerteltink/34ad30734f49f02f495b97bb77a09969 to your computer and use it in GitHub Desktop.
Zend Framework source file docblock license validation PoC
<?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);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment