clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,172 @@
<?php
/**
* Parses and verifies the class doc comment.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Checks that comment doc blocks exist on classes, interfaces and traits. Largely
* copied from PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting\ClassCommentSniff.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class ClassCommentSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [
T_CLASS,
T_INTERFACE,
T_TRAIT,
T_ENUM,
];
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$find = Tokens::$methodPrefixes;
$find[T_WHITESPACE] = T_WHITESPACE;
$find[T_READONLY] = T_READONLY;
$name = $tokens[$stackPtr]['content'];
$classCodeStart = $stackPtr;
$previousContent = null;
for ($commentEnd = ($stackPtr - 1); $commentEnd >= 0; $commentEnd--) {
if (isset($find[$tokens[$commentEnd]['code']]) === true) {
continue;
}
if ($previousContent === null) {
$previousContent = $commentEnd;
}
if ($tokens[$commentEnd]['code'] === T_ATTRIBUTE_END
&& isset($tokens[$commentEnd]['attribute_opener']) === true
) {
$commentEnd = $classCodeStart = $tokens[$commentEnd]['attribute_opener'];
continue;
}
break;
}
if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
&& $tokens[$commentEnd]['code'] !== T_COMMENT
) {
$fix = $phpcsFile->addFixableError('Missing %s doc comment', $classCodeStart, 'Missing', [$name]);
if ($fix === true) {
$phpcsFile->fixer->addContent($commentEnd, "\n\n/**\n *\n */");
}
return;
}
// Try and determine if this is a file comment instead of a class comment.
if ($tokens[$commentEnd]['code'] === T_DOC_COMMENT_CLOSE_TAG) {
$start = ($tokens[$commentEnd]['comment_opener'] - 1);
} else {
$start = ($commentEnd - 1);
}
$fileTag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($start + 1), $commentEnd, false, '@file');
if ($fileTag !== false) {
// This is a file comment.
$fix = $phpcsFile->addFixableError('Missing %s doc comment', $classCodeStart, 'Missing', [$name]);
if ($fix === true) {
$phpcsFile->fixer->addContent($commentEnd, "\n/**\n *\n */");
}
return;
}
if ($tokens[$commentEnd]['code'] === T_COMMENT) {
$fix = $phpcsFile->addFixableError('You must use "/**" style comments for a %s comment', $classCodeStart, 'WrongStyle', [$name]);
if ($fix === true) {
// Convert the comment into a doc comment.
$phpcsFile->fixer->beginChangeset();
$comment = '';
for ($i = $commentEnd; $tokens[$i]['code'] === T_COMMENT; $i--) {
$comment = ' *'.ltrim($tokens[$i]['content'], '/* ').$comment;
$phpcsFile->fixer->replaceToken($i, '');
}
$phpcsFile->fixer->replaceToken($commentEnd, "/**\n".rtrim($comment, "*/\n")."\n */");
$phpcsFile->fixer->endChangeset();
}
return;
}
if ($tokens[$commentEnd]['line'] !== ($tokens[$classCodeStart]['line'] - 1)) {
$error = 'There must be exactly one newline after the %s comment';
$fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfter', [$name]);
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
for ($i = ($commentEnd + 1); $tokens[$i]['code'] === T_WHITESPACE && $i < $classCodeStart; $i++) {
$phpcsFile->fixer->replaceToken($i, '');
}
$phpcsFile->fixer->addContent($commentEnd, "\n");
$phpcsFile->fixer->endChangeset();
}
}
$comment = [];
for ($i = $start; $i < $commentEnd; $i++) {
if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) {
break;
}
if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
$comment[] = $tokens[$i]['content'];
}
}
$words = explode(' ', implode(' ', $comment));
if (count($words) <= 2) {
$className = $phpcsFile->getDeclarationName($stackPtr);
foreach ($words as $word) {
// Check if the comment contains the class name.
if (strpos($word, $className) !== false) {
$error = 'The class short comment should describe what the class does and not simply repeat the class name';
$phpcsFile->addWarning($error, $commentEnd, 'Short');
break;
}
}
}
}//end process()
}//end class

View File

@@ -0,0 +1,116 @@
<?php
/**
* \Drupal\Sniffs\Commenting\DataTypeNamespaceSniff.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Checks that data types in param, return, var, and throws tags are fully namespaced.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class DataTypeNamespaceSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_USE];
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in
* the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
// Only check use statements in the global scope.
if (empty($tokens[$stackPtr]['conditions']) === false) {
return;
}
// Seek to the end of the statement and get the string before the semi colon.
$semiColon = $phpcsFile->findEndOfStatement($stackPtr);
if ($tokens[$semiColon]['code'] !== T_SEMICOLON) {
return;
}
$classPtr = $phpcsFile->findPrevious(
Tokens::$emptyTokens,
($semiColon - 1),
null,
true
);
if ($tokens[$classPtr]['code'] !== T_STRING) {
return;
}
// Replace @var data types in doc comments with the fully qualified class
// name.
$useNamespacePtr = $phpcsFile->findNext([T_STRING], ($stackPtr + 1));
$useNamespaceEnd = $phpcsFile->findNext(
[
T_NS_SEPARATOR,
T_STRING,
],
($useNamespacePtr + 1),
null,
true
);
$fullNamespace = $phpcsFile->getTokensAsString($useNamespacePtr, ($useNamespaceEnd - $useNamespacePtr));
$tag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($stackPtr + 1));
while ($tag !== false) {
if (($tokens[$tag]['content'] === '@var'
|| $tokens[$tag]['content'] === '@return'
|| $tokens[$tag]['content'] === '@param'
|| $tokens[$tag]['content'] === '@throws')
&& isset($tokens[($tag + 1)]) === true
&& $tokens[($tag + 1)]['code'] === T_DOC_COMMENT_WHITESPACE
&& isset($tokens[($tag + 2)]) === true
&& $tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING
&& strpos($tokens[($tag + 2)]['content'], $tokens[$classPtr]['content']) === 0
) {
$error = 'Data types in %s tags need to be fully namespaced';
$data = [$tokens[$tag]['content']];
$fix = $phpcsFile->addFixableError($error, ($tag + 2), 'DataTypeNamespace', $data);
if ($fix === true) {
$replacement = '\\'.$fullNamespace.substr($tokens[($tag + 2)]['content'], strlen($tokens[$classPtr]['content']));
$phpcsFile->fixer->replaceToken(($tag + 2), $replacement);
}
}
$tag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($tag + 1));
}//end while
}//end process()
}//end class

View File

@@ -0,0 +1,231 @@
<?php
/**
* \Drupal\Sniffs\Commenting\DeprecatedSniff.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Config;
/**
* Ensures standard format of @ deprecated tag text in docblock.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class DeprecatedSniff implements Sniff
{
/**
* Show debug output for this sniff.
*
* Use phpcs --runtime-set deprecated_debug true
*
* @var boolean
*/
private $debug = false;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
if (defined('PHP_CODESNIFFER_IN_TESTS') === true) {
$this->debug = false;
}
return [T_DOC_COMMENT_TAG];
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$debug = Config::getConfigData('deprecated_debug');
if ($debug !== null) {
$this->debug = (bool) $debug;
}
$tokens = $phpcsFile->getTokens();
// Only process @deprecated tags.
if (strcasecmp($tokens[$stackPtr]['content'], '@deprecated') !== 0) {
return;
}
// Get the end point of the comment block which has the deprecated tag.
$commentEnd = $phpcsFile->findNext(T_DOC_COMMENT_CLOSE_TAG, ($stackPtr + 1));
// Get the full @deprecated text which may cover multiple lines.
$textItems = [];
$lastLine = $tokens[($stackPtr + 1)]['line'];
for ($i = ($stackPtr + 1); $i < $commentEnd; $i++) {
if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
if ($tokens[$i]['line'] <= ($lastLine + 1)) {
$textItems[$i] = $tokens[$i]['content'];
$lastLine = $tokens[$i]['line'];
} else {
break;
}
}
// Found another tag, so we have all the deprecation text.
if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) {
break;
}
}
// The standard format for the deprecation text is:
// @deprecated in %in-version% and is removed from %removal-version%. %extra-info%.
$standardFormat = "@deprecated in %%deprecation-version%% and is removed from %%removal-version%%. %%extra-info%%.";
// Use (?U) 'ungreedy' before the removal-version so that only the text
// up to the first dot+space is matched, as there may be more than one
// sentence in the extra-info part.
$fullText = trim(implode(' ', $textItems));
$matches = [];
preg_match('/^in (.+) and is removed from (?U)(.+)(?:\. | |\.$|$)(.*)$/', $fullText, $matches);
// There should be 4 items in $matches: 0 is full text, 1 = in-version,
// 2 = removal-version, 3 = extra-info (can be blank at this stage).
if (count($matches) !== 4) {
// The full text does not match the standard. Try to find fixes by
// testing with a relaxed set of criteria, based on common
// formatting variations. This is designed for Core fixes only.
$error = "The text '@deprecated %s' does not match the standard format: ".$standardFormat;
// All of the standard text should be on the first comment line, so
// try to match with common formatting errors to allow an automatic
// fix. If not possible then report a normal error.
$matchesFix = [];
$fix = null;
if (count($textItems) > 0) {
// Get just the first line of the text.
$key = array_keys($textItems)[0];
$text1 = $textItems[$key];
// Matching on (drupal|) here says that we are only attempting to provide
// automatic fixes for Drupal core, and if the project is missing we are
// assuming it is Drupal core. Deprecations for contrib projects are much
// less frequent and faults can be corrected manually.
// cspell:ignore xdev
preg_match('/^(.*)(as of|in) (drupal|)( |:|)+([\d\.\-xdev\?]+)(,| |. |)(.*)(removed|removal)([ |from|before|in|the]*) (drupal|)( |:|)([\d\-\.xdev]+)( |,|$)+(?:release|)(?:[\.,])*(.*)$/i', $text1, $matchesFix);
if (count($matchesFix) >= 12) {
// It is a Drupal core deprecation and is fixable.
if (empty($matchesFix[1]) === false && $this->debug === true) {
// For info, to check it is acceptable to remove the text in [1].
echo('DEBUG: File: '.$phpcsFile->path.', line '.$tokens[($stackPtr)]['line'].PHP_EOL);
echo('DEBUG: "@deprecated '.$text1.'"'.PHP_EOL);
echo('DEBUG: Fix will remove: "'.$matchesFix[1].'"'.PHP_EOL);
}
$ver1 = str_Replace(['-dev', 'x'], ['', '0'], trim($matchesFix[5], '.'));
$ver2 = str_Replace(['-dev', 'x'], ['', '0'], trim($matchesFix[12], '.'));
// If the version is short, add enough '.0' to correct it.
while (substr_count($ver1, '.') < 2) {
$ver1 .= '.0';
}
while (substr_count($ver2, '.') < 2) {
$ver2 .= '.0';
}
$correctedText = trim('in drupal:'.$ver1.' and is removed from drupal:'.$ver2.'. '.trim($matchesFix[14]));
// If $correctedText is longer than 65 this will make the whole line
// exceed 80 so give a warning if running with debug.
if (strlen($correctedText) > 65 && $this->debug === true) {
echo('WARNING: File '.$phpcsFile->path.', line '.$tokens[($stackPtr)]['line'].PHP_EOL);
echo('WARNING: Original = * @deprecated '.$text1.PHP_EOL);
echo('WARNING: Corrected = * @deprecated '.$correctedText.PHP_EOL);
echo('WARNING: New line length '.(strlen($correctedText) + 15).' exceeds standard 80 character limit'.PHP_EOL);
}
$fix = $phpcsFile->addFixableError($error, $key, 'IncorrectTextLayout', [$fullText]);
if ($fix === true) {
$phpcsFile->fixer->replaceToken($key, $correctedText);
}
}//end if
}//end if
if ($fix === null) {
// There was no automatic fix, so give a normal error.
$phpcsFile->addError($error, $stackPtr, 'IncorrectTextLayout', [$fullText]);
}
} else {
// The text follows the basic layout. Now check that the versions
// match drupal:n.n.n or project:n.x-n.n or project:n.x-n.n-label[n]
// or project:n.n.n or project:n.n.n-label[n].
// The text must be all lower case and numbers can be one or two digits.
foreach (['deprecation-version' => $matches[1], 'removal-version' => $matches[2]] as $name => $version) {
if (preg_match('/^[a-z\d_]+:(\d{1,2}\.\d{1,2}\.\d{1,2}|\d{1,2}\.x\-\d{1,2}\.\d{1,2})(-[a-z]{1,5}\d{1,2})?$/', $version) === 0) {
$error = "The %s '%s' does not match the lower-case machine-name standard: drupal:n.n.n or project:n.x-n.n or project:n.x-n.n-label[n] or project:n.n.n or project:n.n.n-label[n]";
$phpcsFile->addWarning($error, $stackPtr, 'DeprecatedVersionFormat', [$name, $version]);
}
}
// The 'IncorrectTextLayout' above is designed to pass if all is ok
// except for missing extra info. This is a common fault so provide
// a separate check and message for this.
if ($matches[3] === '') {
$error = 'The @deprecated tag must have %extra-info%. The standard format is: '.str_replace('%%', '%', $standardFormat);
$phpcsFile->addError($error, $stackPtr, 'MissingExtraInfo', []);
}
}//end if
// The next tag in this comment block after @deprecated must be @see.
$seeTag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($stackPtr + 1), $commentEnd, false, '@see');
if ($seeTag === false) {
$error = 'Each @deprecated tag must have a @see tag immediately following it';
$phpcsFile->addError($error, $stackPtr, 'DeprecatedMissingSeeTag');
return;
}
// Check the format of the @see url.
$string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, ($seeTag + 1), $commentEnd);
// If the @see tag exists but has no content then $string will be empty
// and $tokens[$string]['content'] will return '<?php' which makes the
// standards message confusing. Better to set crLink to blank here.
if ($string === false) {
$crLink = ' ';
} else {
$crLink = $tokens[$string]['content'];
}
// Allow for the alternative 'node' or 'project/aaa/issues' format.
preg_match('[^http(s*)://www.drupal.org/(node|project/\w+/issues)/(\d+)([\.\?\;!]*)$]', $crLink, $matches);
if (isset($matches[4]) === true && empty($matches[4]) === false) {
// If matches[4] is not blank it means that the url is OK but it
// ends with punctuation. This is a common and fixable mistake.
$error = "The @see url '%s' should not end with punctuation";
$fix = $phpcsFile->addFixableError($error, $string, 'DeprecatedPeriodAfterSeeUrl', [$crLink]);
if ($fix === true) {
// Remove all of the the trailing punctuation.
$content = substr($crLink, 0, -(strlen($matches[4])));
$phpcsFile->fixer->replaceToken($string, $content);
}//end if
} else if (empty($matches) === true) {
$error = "The @see url '%s' does not match the standard: http(s)://www.drupal.org/node/n or http(s)://www.drupal.org/project/aaa/issues/n";
$phpcsFile->addWarning($error, $seeTag, 'DeprecatedWrongSeeUrlFormat', [$crLink]);
}
}//end process()
}//end class

View File

@@ -0,0 +1,164 @@
<?php
/**
* \Drupal\Sniffs\Commenting\DocCommentAlignmentSniff.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Largely copied from
* \PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting\DocCommentAlignmentSniff to also
* handle the "var" keyword. See
* https://github.com/squizlabs/PHP_CodeSniffer/pull/1212
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class DocCommentAlignmentSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_DOC_COMMENT_OPEN_TAG];
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
// We are only interested in function/class/interface doc block comments.
$ignore = Tokens::$emptyTokens;
if ($phpcsFile->tokenizerType === 'JS') {
$ignore[] = T_EQUAL;
$ignore[] = T_STRING;
$ignore[] = T_OBJECT_OPERATOR;
}
$nextToken = $phpcsFile->findNext($ignore, ($stackPtr + 1), null, true);
$ignore = [
T_CLASS => true,
T_INTERFACE => true,
T_FUNCTION => true,
T_PUBLIC => true,
T_PRIVATE => true,
T_PROTECTED => true,
T_STATIC => true,
T_ABSTRACT => true,
T_PROPERTY => true,
T_OBJECT => true,
T_PROTOTYPE => true,
T_VAR => true,
];
if (isset($ignore[$tokens[$nextToken]['code']]) === false) {
// Could be a file comment.
$prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($tokens[$prevToken]['code'] !== T_OPEN_TAG) {
return;
}
}
// There must be one space after each star (unless it is an empty comment line)
// and all the stars must be aligned correctly.
$requiredColumn = ($tokens[$stackPtr]['column'] + 1);
$endComment = $tokens[$stackPtr]['comment_closer'];
for ($i = ($stackPtr + 1); $i <= $endComment; $i++) {
if ($tokens[$i]['code'] !== T_DOC_COMMENT_STAR
&& $tokens[$i]['code'] !== T_DOC_COMMENT_CLOSE_TAG
) {
continue;
}
if ($tokens[$i]['code'] === T_DOC_COMMENT_CLOSE_TAG) {
// Can't process the close tag if it is not the first thing on the line.
$prev = $phpcsFile->findPrevious(T_DOC_COMMENT_WHITESPACE, ($i - 1), $stackPtr, true);
if ($tokens[$prev]['line'] === $tokens[$i]['line']) {
continue;
}
}
if ($tokens[$i]['column'] !== $requiredColumn) {
$error = 'Expected %s space(s) before asterisk; %s found';
$data = [
($requiredColumn - 1),
($tokens[$i]['column'] - 1),
];
$fix = $phpcsFile->addFixableError($error, $i, 'SpaceBeforeStar', $data);
if ($fix === true) {
$padding = str_repeat(' ', ($requiredColumn - 1));
if ($tokens[$i]['column'] === 1) {
$phpcsFile->fixer->addContentBefore($i, $padding);
} else {
$phpcsFile->fixer->replaceToken(($i - 1), $padding);
}
}
}
if ($tokens[$i]['code'] !== T_DOC_COMMENT_STAR) {
continue;
}
if ($tokens[($i + 2)]['line'] !== $tokens[$i]['line']) {
// Line is empty.
continue;
}
if ($tokens[($i + 1)]['code'] !== T_DOC_COMMENT_WHITESPACE) {
$error = 'Expected 1 space after asterisk; 0 found';
$fix = $phpcsFile->addFixableError($error, $i, 'NoSpaceAfterStar');
if ($fix === true) {
$phpcsFile->fixer->addContent($i, ' ');
}
} else if ($tokens[($i + 2)]['code'] === T_DOC_COMMENT_TAG
&& $tokens[($i + 1)]['content'] !== ' '
// Special @code/@endcode/@see tags can have more than 1 space.
&& in_array(
$tokens[($i + 2)]['content'],
[
'@param',
'@return',
'@throws',
'@ingroup',
'@var',
]
) === true
) {
$error = 'Expected 1 space after asterisk; %s found';
$data = [strlen($tokens[($i + 1)]['content'])];
$fix = $phpcsFile->addFixableError($error, $i, 'SpaceAfterStar', $data);
if ($fix === true) {
$phpcsFile->fixer->replaceToken(($i + 1), ' ');
}
}//end if
}//end for
}//end process()
}//end class

View File

@@ -0,0 +1,77 @@
<?php
/**
* Ensures @code annotations in doc blocks don't contain long array syntax.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
/**
* Ensures @code annotations in doc blocks don't contain long array syntax.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class DocCommentLongArraySyntaxSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_DOC_COMMENT_OPEN_TAG];
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$commentEnd = $phpcsFile->findNext(T_DOC_COMMENT_CLOSE_TAG, ($stackPtr + 1));
// Look for @code annotations.
$codeEnd = $stackPtr;
do {
$codeStart = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($codeEnd + 1), $commentEnd, false, '@code');
if ($codeStart !== false) {
$codeEnd = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($codeStart + 1), $commentEnd, false, '@endcode');
// If the code block never ends then simply ignore this
// docblock, it is probably malformed.
if ($codeEnd === false) {
break;
} else {
// Check for long array syntax use inside this @code annotation.
for ($i = ($codeStart + 1); $i < $codeEnd; $i++) {
if (preg_match('/\barray\s*\(/', $tokens[$i]['content']) === 1) {
$error = 'Long array syntax must not be used in doc comment code annotations';
$phpcsFile->addError($error, $i, 'DocLongArray');
}
}
}
}
} while ($codeStart !== false);
}//end process()
}//end class

View File

@@ -0,0 +1,546 @@
<?php
/**
* Ensures doc blocks follow basic formatting.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
/**
* Ensures doc blocks follow basic formatting.
*
* Largely copied from
* \PHP_CodeSniffer\Standards\Generic\Sniffs\Commenting\DocCommentSniff,
* but Drupal @file comments are different.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class DocCommentSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_DOC_COMMENT_OPEN_TAG];
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$commentEnd = $phpcsFile->findNext(T_DOC_COMMENT_CLOSE_TAG, ($stackPtr + 1));
$commentStart = $tokens[$commentEnd]['comment_opener'];
$empty = [
T_DOC_COMMENT_WHITESPACE,
T_DOC_COMMENT_STAR,
];
$short = $phpcsFile->findNext($empty, ($stackPtr + 1), $commentEnd, true);
if ($short === false) {
// No content at all.
$error = 'Doc comment is empty';
$phpcsFile->addError($error, $stackPtr, 'Empty');
return;
}
// Ignore doc blocks in functions, this is handled by InlineCommentSniff.
if (empty($tokens[$stackPtr]['conditions']) === false && in_array(T_FUNCTION, $tokens[$stackPtr]['conditions']) === true) {
return;
}
// The first line of the comment should just be the /** code.
// In JSDoc there are cases with @lends that are on the same line as code.
if ($tokens[$short]['line'] === $tokens[$stackPtr]['line'] && $phpcsFile->tokenizerType !== 'JS') {
$error = 'The open comment tag must be the only content on the line';
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'ContentAfterOpen');
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->addNewline($stackPtr);
$phpcsFile->fixer->addContentBefore($short, '* ');
$phpcsFile->fixer->endChangeset();
}
}
// The last line of the comment should just be the */ code.
$prev = $phpcsFile->findPrevious($empty, ($commentEnd - 1), $stackPtr, true);
if ($tokens[$commentEnd]['content'] !== '*/') {
$error = 'Wrong function doc comment end; expected "*/", found "%s"';
$phpcsFile->addError($error, $commentEnd, 'WrongEnd', [$tokens[$commentEnd]['content']]);
}
// Check for additional blank lines at the end of the comment.
if ($tokens[$prev]['line'] < ($tokens[$commentEnd]['line'] - 1)) {
$error = 'Additional blank lines found at end of doc comment';
$fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfter');
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
for ($i = ($prev + 1); $i < $commentEnd; $i++) {
if ($tokens[($i + 1)]['line'] === $tokens[$commentEnd]['line']) {
break;
}
$phpcsFile->fixer->replaceToken($i, '');
}
$phpcsFile->fixer->endChangeset();
}
}
// The short description of @file comments is one line below.
if ($tokens[$short]['code'] === T_DOC_COMMENT_TAG && $tokens[$short]['content'] === '@file') {
$next = $phpcsFile->findNext($empty, ($short + 1), $commentEnd, true);
if ($next !== false) {
$fileShort = $short;
$short = $next;
}
}
// Do not check defgroup sections, they have no short description. Also don't
// check PHPUnit tests doc blocks because they might not have a description.
if (in_array($tokens[$short]['content'], ['@defgroup', '@addtogroup', '@}', '@coversDefaultClass']) === true) {
return;
}
// Check for a comment description.
if ($tokens[$short]['code'] !== T_DOC_COMMENT_STRING) {
// JSDoc has many cases of @type declaration that don't have a
// description.
if ($phpcsFile->tokenizerType === 'JS') {
return;
}
// PHPUnit test methods are allowed to skip the short description and
// only provide an @covers annotation.
if ($tokens[$short]['content'] === '@covers') {
return;
}
// If inheritDoc is found without curly braces it is identified as a T_DOC_COMMENT_TAG not a
// T_DOC_COMMENT_STRING. It would be misleading to give the 'Missing short description' error
// below, hence we give a more useful message and can fix it automatically.
if (stripos($tokens[$short]['content'], '@inheritdoc') === 0) {
$error = "{$tokens[$short]['content']} found. Did you mean {{$tokens[$short]['content']}}?";
$fix = $phpcsFile->addFixableError($error, $short, 'InheritDocWithoutBraces');
if ($fix === true) {
$phpcsFile->fixer->replaceToken($short, "{{$tokens[$short]['content']}}");
}
return;
}
$error = 'Missing short description in doc comment';
$phpcsFile->addError($error, $stackPtr, 'MissingShort');
return;
}//end if
if (isset($fileShort) === true) {
$start = $fileShort;
} else {
$start = $stackPtr;
}
// No extra newline before short description.
if ($tokens[$short]['line'] !== ($tokens[$start]['line'] + 1)) {
$error = 'Doc comment short description must be on the first line';
$fix = $phpcsFile->addFixableError($error, $short, 'SpacingBeforeShort');
if ($fix === true) {
// Move file comment short description to the next line.
if (isset($fileShort) === true && $tokens[$short]['line'] === $tokens[$start]['line']) {
$phpcsFile->fixer->addContentBefore($fileShort, "\n *");
} else {
$phpcsFile->fixer->beginChangeset();
for ($i = $start; $i < $short; $i++) {
if ($tokens[$i]['line'] === $tokens[$start]['line']) {
continue;
} else if ($tokens[$i]['line'] === $tokens[$short]['line']) {
break;
}
$phpcsFile->fixer->replaceToken($i, '');
}
$phpcsFile->fixer->endChangeset();
}
}
}//end if
if ($tokens[($short - 1)]['content'] !== ' '
&& strpos($tokens[($short - 1)]['content'], $phpcsFile->eolChar) === false
) {
$error = 'Function comment short description must start with exactly one space';
$fix = $phpcsFile->addFixableError($error, $short, 'ShortStartSpace');
if ($fix === true) {
if ($tokens[($short - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) {
$phpcsFile->fixer->replaceToken(($short - 1), ' ');
} else {
$phpcsFile->fixer->addContent(($short - 1), ' ');
}
}
}
// Account for the fact that a short description might cover
// multiple lines.
$shortContent = $tokens[$short]['content'];
$shortEnd = $short;
for ($i = ($short + 1); $i < $commentEnd; $i++) {
if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
if ($tokens[$i]['line'] === ($tokens[$shortEnd]['line'] + 1)) {
$shortContent .= $tokens[$i]['content'];
$shortEnd = $i;
} else {
break;
}
}
if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) {
break;
}
}
// Remove any trailing white spaces which are detected by other sniffs.
$shortContent = trim($shortContent);
if ($shortContent !== ''
&& preg_match('|\p{Lu}|u', $shortContent[0]) === 0
// Allow both variants of inheritdoc comments.
&& $shortContent !== '{@inheritdoc}'
&& $shortContent !== '{@inheritDoc}'
// Ignore Features module export files that just use the file name as
// comment.
&& $shortContent !== basename($phpcsFile->getFilename())
) {
$error = 'Doc comment short description must start with a capital letter';
// If we cannot capitalize the first character then we don't have a
// fixable error.
if ($tokens[$short]['content'] === ucfirst($tokens[$short]['content'])) {
$phpcsFile->addError($error, $short, 'ShortNotCapital');
} else {
$fix = $phpcsFile->addFixableError($error, $short, 'ShortNotCapital');
if ($fix === true) {
$phpcsFile->fixer->replaceToken($short, ucfirst($tokens[$short]['content']));
}
}
}
$lastChar = substr($shortContent, -1);
// Allow these characters as valid line-ends not requiring to be fixed.
if (in_array($lastChar, ['.', '!', '?', ')']) === false
// Allow both variants of inheritdoc comments.
&& $shortContent !== '{@inheritdoc}'
&& $shortContent !== '{@inheritDoc}'
// Ignore Features module export files that just use the file name as
// comment.
&& $shortContent !== basename($phpcsFile->getFilename())
) {
$error = 'Doc comment short description must end with a full stop';
// If the last character is alphanumeric and the content is all on one line then fix it.
if (preg_match('/[a-zA-Z0-9]/', $lastChar) === 1
&& $tokens[$short]['line'] === $tokens[$shortEnd]['line']
) {
$fix = $phpcsFile->addFixableError($error, $shortEnd, 'ShortFullStop');
if ($fix === true) {
$phpcsFile->fixer->addContent($shortEnd, '.');
}
} else {
// The correct fix is not obvious, so report an error and leave for manual correction.
$phpcsFile->addError($error, $shortEnd, 'ShortFullStop');
}
}
if ($tokens[$short]['line'] !== $tokens[$shortEnd]['line']) {
$error = 'Doc comment short description must be on a single line, further text should be a separate paragraph';
$phpcsFile->addError($error, $shortEnd, 'ShortSingleLine');
}
$long = $phpcsFile->findNext($empty, ($shortEnd + 1), ($commentEnd - 1), true);
if ($long === false) {
return;
}
if ($tokens[$long]['code'] === T_DOC_COMMENT_STRING) {
if ($tokens[$long]['line'] !== ($tokens[$shortEnd]['line'] + 2)) {
$error = 'There must be exactly one blank line between descriptions in a doc comment';
$fix = $phpcsFile->addFixableError($error, $long, 'SpacingBetween');
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
for ($i = ($shortEnd + 1); $i < $long; $i++) {
if ($tokens[$i]['line'] === $tokens[$shortEnd]['line']) {
continue;
} else if ($tokens[$i]['line'] === ($tokens[$long]['line'] - 1)) {
break;
}
$phpcsFile->fixer->replaceToken($i, '');
}
$phpcsFile->fixer->endChangeset();
}
}
if (preg_match('|\p{Lu}|u', $tokens[$long]['content'][0]) === 0
&& $tokens[$long]['content'] !== ucfirst($tokens[$long]['content'])
) {
$error = 'Doc comment long description must start with a capital letter';
$fix = $phpcsFile->addFixableError($error, $long, 'LongNotCapital');
if ($fix === true) {
$phpcsFile->fixer->replaceToken($long, ucfirst($tokens[$long]['content']));
}
}
// Account for the fact that a description might cover multiple lines.
$longContent = $tokens[$long]['content'];
$longEnd = $long;
for ($i = ($long + 1); $i < $commentEnd; $i++) {
if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
if ($tokens[$i]['line'] <= ($tokens[$longEnd]['line'] + 1)) {
$longContent .= $tokens[$i]['content'];
$longEnd = $i;
} else {
break;
}
}
if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) {
if ($tokens[$i]['line'] <= ($tokens[$longEnd]['line'] + 1)
// Allow link tags within the long comment itself.
&& ($tokens[$i]['content'] === '@link' || $tokens[$i]['content'] === '@endlink')
) {
$longContent .= $tokens[$i]['content'];
$longEnd = $i;
} else {
break;
}
}
}//end for
// Remove any trailing white spaces which are detected by other sniffs.
$longContent = trim($longContent);
if (preg_match('/[a-zA-Z]$/', $longContent) === 1) {
$error = 'Doc comment long description must end with a full stop';
$fix = $phpcsFile->addFixableError($error, $longEnd, 'LongFullStop');
if ($fix === true) {
$phpcsFile->fixer->addContent($longEnd, '.');
}
}
}//end if
if (empty($tokens[$commentStart]['comment_tags']) === true) {
// No tags in the comment.
return;
}
$firstTag = $tokens[$commentStart]['comment_tags'][0];
$prev = $phpcsFile->findPrevious($empty, ($firstTag - 1), $stackPtr, true);
// This does not apply to @file, @code, @link and @endlink tags.
if ($tokens[$firstTag]['line'] !== ($tokens[$prev]['line'] + 2)
&& isset($fileShort) === false
&& in_array($tokens[$firstTag]['content'], ['@code', '@link', '@endlink']) === false
) {
$error = 'There must be exactly one blank line before the tags in a doc comment';
$fix = $phpcsFile->addFixableError($error, $firstTag, 'SpacingBeforeTags');
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
for ($i = ($prev + 1); $i < $firstTag; $i++) {
if ($tokens[$i]['line'] === $tokens[$firstTag]['line']) {
break;
}
$phpcsFile->fixer->replaceToken($i, '');
}
$indent = str_repeat(' ', $tokens[$stackPtr]['column']);
$phpcsFile->fixer->addContent($prev, $phpcsFile->eolChar.$indent.'*'.$phpcsFile->eolChar);
$phpcsFile->fixer->endChangeset();
}
}
// Break out the tags into groups and check alignment within each.
// A tag group is one where there are no blank lines between tags.
// The param tag group is special as it requires all @param tags to be inside.
$tagGroups = [];
// cspell:ignore groupid
$groupid = 0;
$paramGroupid = null;
$currentTag = null;
$previousTag = null;
$isNewGroup = null;
$checkTags = [
'@param',
'@return',
'@throws',
];
foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
if ($pos > 0) {
// If this tag is not in the same column as the initial tag then
// it must be an inline comment tag and should be ignored here.
if ($tokens[$tag]['column'] !== $tokens[$firstTag]['column']) {
continue;
}
$prev = $phpcsFile->findPrevious(
T_DOC_COMMENT_STRING,
($tag - 1),
$tokens[$commentStart]['comment_tags'][($pos - 1)]
);
if ($prev === false) {
$prev = $tokens[$commentStart]['comment_tags'][($pos - 1)];
}
$isNewGroup = $tokens[$prev]['line'] !== ($tokens[$tag]['line'] - 1);
if ($isNewGroup === true) {
$groupid++;
}
}//end if
$currentTag = $tokens[$tag]['content'];
if ($currentTag === '@param') {
if (($paramGroupid === null
&& empty($tagGroups[$groupid]) === false)
|| ($paramGroupid !== null
&& $paramGroupid !== $groupid)
) {
$error = 'Parameter tags must be grouped together in a doc comment';
$phpcsFile->addError($error, $tag, 'ParamGroup');
}
if ($paramGroupid === null) {
$paramGroupid = $groupid;
}
// All of the $checkTags sections should be separated by a blank
// line both before and after the sections.
} else if ($isNewGroup === false
&& (in_array($currentTag, $checkTags) === true || in_array($previousTag, $checkTags) === true)
&& $previousTag !== $currentTag
) {
$error = 'Separate the %s and %s sections by a blank line.';
$fix = $phpcsFile->addFixableError($error, $tag, 'TagGroupSpacing', [$previousTag, $currentTag]);
if ($fix === true) {
$phpcsFile->fixer->replaceToken(($tag - 1), "\n".str_repeat(' ', ($tokens[$tag]['column'] - 3)).'* ');
}
}//end if
$previousTag = $currentTag;
$tagGroups[$groupid][] = $tag;
}//end foreach
foreach ($tagGroups as $group) {
$maxLength = 0;
$paddings = [];
$pos = 0;
foreach ($group as $pos => $tag) {
$tagLength = strlen($tokens[$tag]['content']);
if ($tagLength > $maxLength) {
$maxLength = $tagLength;
}
// Check for a value. No value means no padding needed.
$string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd);
if ($string !== false && $tokens[$string]['line'] === $tokens[$tag]['line']) {
$paddings[$tag] = strlen($tokens[($tag + 1)]['content']);
}
}
// Check that there was single blank line after the tag block
// but account for a multi-line tag comments.
$lastTag = $group[$pos];
$next = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($lastTag + 3), $commentEnd);
if ($next !== false && $tokens[$next]['column'] === $tokens[$firstTag]['column']) {
$prev = $phpcsFile->findPrevious([T_DOC_COMMENT_TAG, T_DOC_COMMENT_STRING], ($next - 1), $commentStart);
if ($tokens[$next]['line'] !== ($tokens[$prev]['line'] + 2)) {
$error = 'There must be a single blank line after a tag group';
$fix = $phpcsFile->addFixableError($error, $lastTag, 'SpacingAfterTagGroup');
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
for ($i = ($prev + 1); $i < $next; $i++) {
if ($tokens[$i]['line'] === $tokens[$next]['line']) {
break;
}
$phpcsFile->fixer->replaceToken($i, '');
}
$indent = str_repeat(' ', $tokens[$stackPtr]['column']);
$phpcsFile->fixer->addContent($prev, $phpcsFile->eolChar.$indent.'*'.$phpcsFile->eolChar);
$phpcsFile->fixer->endChangeset();
}
}
}//end if
// Now check paddings.
foreach ($paddings as $tag => $padding) {
if ($padding !== 1) {
$error = 'Tag value indented incorrectly; expected 1 space but found %s';
$data = [$padding];
$fix = $phpcsFile->addFixableError($error, ($tag + 1), 'TagValueIndent', $data);
if ($fix === true) {
$phpcsFile->fixer->replaceToken(($tag + 1), ' ');
}
}
}
}//end foreach
// If there is a param group, it needs to be first; with the exception
// of @code, @todo and link tags.
if ($paramGroupid !== null && $paramGroupid !== 0
&& in_array($tokens[$tokens[$commentStart]['comment_tags'][0]]['content'], ['@code', '@todo', '@link', '@endlink', '@codingStandardsIgnoreStart']) === false
// In JSDoc we can have many other valid tags like @function or
// tags like @constructor before the param tags.
&& $phpcsFile->tokenizerType !== 'JS'
) {
$error = 'Parameter tags must be defined first in a doc comment';
$phpcsFile->addError($error, $tagGroups[$paramGroupid][0], 'ParamNotFirst');
}
$foundTags = [];
$lastPos = 0;
foreach ($tokens[$stackPtr]['comment_tags'] as $pos => $tag) {
$tagName = $tokens[$tag]['content'];
// Skip code tags, they can be anywhere.
if (in_array($tagName, $checkTags) === false) {
continue;
}
if (isset($foundTags[$tagName]) === true) {
$lastTag = $tokens[$stackPtr]['comment_tags'][$lastPos];
if ($tokens[$lastTag]['content'] !== $tagName) {
$error = 'Tags must be grouped together in a doc comment';
$phpcsFile->addError($error, $tag, 'TagsNotGrouped');
}
}
$foundTags[$tagName] = true;
$lastPos = $pos;
}
}//end process()
}//end class

View File

@@ -0,0 +1,89 @@
<?php
/**
* \Drupal\Sniffs\Commenting\DocCommentStarSniff
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
/**
* Checks that a doc comment block has a doc comment star on every line.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class DocCommentStarSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_DOC_COMMENT_OPEN_TAG];
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$lastLineChecked = $tokens[$stackPtr]['line'];
for ($i = ($stackPtr + 1); $i < ($tokens[$stackPtr]['comment_closer'] - 1); $i++) {
// We are only interested in the beginning of the line.
if ($tokens[$i]['line'] === $lastLineChecked) {
continue;
}
// The first token on the line must be a whitespace followed by a star.
if ($tokens[$i]['code'] === T_DOC_COMMENT_WHITESPACE) {
if ($tokens[($i + 1)]['code'] !== T_DOC_COMMENT_STAR) {
$error = 'Doc comment star missing';
$fix = $phpcsFile->addFixableError($error, $i, 'StarMissing');
if ($fix === true) {
if (strpos($tokens[$i]['content'], $phpcsFile->eolChar) !== false) {
$phpcsFile->fixer->replaceToken($i, str_repeat(' ', $tokens[$stackPtr]['column'])."* \n");
} else {
$phpcsFile->fixer->replaceToken($i, str_repeat(' ', $tokens[$stackPtr]['column']).'* ');
}
// Ordering of lines might have changed - stop here. The
// fixer will restart the sniff if there are remaining fixes.
return;
}
}
} else if ($tokens[$i]['code'] !== T_DOC_COMMENT_STAR) {
$error = 'Doc comment star missing';
$fix = $phpcsFile->addFixableError($error, $i, 'StarMissing');
if ($fix === true) {
$phpcsFile->fixer->addContentBefore($i, str_repeat(' ', $tokens[$stackPtr]['column']).'* ');
}
}//end if
$lastLineChecked = $tokens[$i]['line'];
}//end for
}//end process()
}//end class

View File

@@ -0,0 +1,249 @@
<?php
/**
* Parses and verifies the doc comments for files.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
/**
* Parses and verifies the doc comments for files.
*
* Verifies that :
* <ul>
* <li>A doc comment exists.</li>
* <li>There is a blank newline after the @file statement.</li>
* </ul>
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class FileCommentSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_OPEN_TAG];
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return int
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$commentStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
// Files containing exactly one class, interface or trait are allowed to
// omit a file doc block. If a namespace is used then the file comment must
// be omitted.
$oopKeyword = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], $stackPtr);
if ($oopKeyword !== false) {
$namespace = $phpcsFile->findNext(T_NAMESPACE, $stackPtr);
// Check if the file contains multiple classes/interfaces/traits - then a
// file doc block is allowed.
$secondOopKeyword = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], ($oopKeyword + 1));
// Namespaced classes, interfaces and traits should not have an @file doc
// block.
if (($tokens[$commentStart]['code'] === T_DOC_COMMENT_OPEN_TAG
|| $tokens[$commentStart]['code'] === T_COMMENT)
&& $secondOopKeyword === false
&& $namespace !== false
) {
if ($tokens[$commentStart]['code'] === T_COMMENT) {
$phpcsFile->addError('Namespaced classes, interfaces and traits should not begin with a file doc comment', $commentStart, 'NamespaceNoFileDoc');
} else {
$fix = $phpcsFile->addFixableError('Namespaced classes, interfaces and traits should not begin with a file doc comment', $commentStart, 'NamespaceNoFileDoc');
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
for ($i = $commentStart; $i <= ($tokens[$commentStart]['comment_closer'] + 1); $i++) {
$phpcsFile->fixer->replaceToken($i, '');
}
// If, after removing the comment, there are two new lines
// remove them.
if ($tokens[($commentStart - 1)]['content'] === "\n" && $tokens[$i]['content'] === "\n") {
$phpcsFile->fixer->replaceToken($i, '');
}
$phpcsFile->fixer->endChangeset();
}
}
}//end if
if ($namespace !== false) {
return ($phpcsFile->numTokens + 1);
}
// Search for global functions before and after the class.
$function = $phpcsFile->findPrevious(T_FUNCTION, ($oopKeyword - 1));
if ($function === false) {
$function = $phpcsFile->findNext(T_FUNCTION, ($tokens[$oopKeyword]['scope_closer'] + 1));
}
$fileTag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($commentStart + 1), null, false, '@file');
// No other classes, no other global functions and no explicit @file tag
// anywhere means it is ok to skip the file comment.
if ($secondOopKeyword === false && $function === false && $fileTag === false) {
return ($phpcsFile->numTokens + 1);
}
}//end if
if ($tokens[$commentStart]['code'] === T_COMMENT) {
$fix = $phpcsFile->addFixableError('You must use "/**" style comments for a file comment', $commentStart, 'WrongStyle');
if ($fix === true) {
$content = $tokens[$commentStart]['content'];
// If the comment starts with something like "/**" then we just
// insert a space after the stars.
if (strpos($content, '/**') === 0) {
$phpcsFile->fixer->replaceToken($commentStart, str_replace('/**', '/** ', $content));
} else if (strpos($content, '/*') === 0) {
// Just turn the /* ... */ style comment into a /** ... */ style
// comment.
$phpcsFile->fixer->replaceToken($commentStart, str_replace('/*', '/**', $content));
} else {
$content = trim(ltrim($tokens[$commentStart]['content'], '/# '));
$phpcsFile->fixer->replaceToken($commentStart, "/**\n * @file\n * $content\n */\n");
}
}
return ($phpcsFile->numTokens + 1);
} else if ($commentStart === false || $tokens[$commentStart]['code'] !== T_DOC_COMMENT_OPEN_TAG) {
$fix = $phpcsFile->addFixableError('Missing file doc comment', 0, 'Missing');
if ($fix === true) {
// Only PHP has a real opening tag, additional newline at the
// beginning here.
if ($phpcsFile->tokenizerType === 'PHP') {
// In templates add the file doc block to the very beginning of
// the file.
if ($tokens[0]['code'] === T_INLINE_HTML) {
$phpcsFile->fixer->addContentBefore(0, "<?php\n\n/**\n * @file\n */\n?>\n");
} else {
$phpcsFile->fixer->addContent($stackPtr, "\n/**\n * @file\n */\n");
}
} else {
$phpcsFile->fixer->addContent($stackPtr, "/**\n * @file\n */\n");
}
}
return ($phpcsFile->numTokens + 1);
}//end if
$commentEnd = $tokens[$commentStart]['comment_closer'];
$fileTag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($commentStart + 1), $commentEnd, false, '@file');
$next = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), null, true);
// If there is no @file tag and the next line is a function or class
// definition then the file docblock is missing.
if ($tokens[$next]['line'] === ($tokens[$commentEnd]['line'] + 1)
&& $tokens[$next]['code'] === T_FUNCTION
) {
if ($fileTag === false) {
$fix = $phpcsFile->addFixableError('Missing file doc comment', $stackPtr, 'Missing');
if ($fix === true) {
// Only PHP has a real opening tag, additional newline at the
// beginning here.
if ($phpcsFile->tokenizerType === 'PHP') {
$phpcsFile->fixer->addContent($stackPtr, "\n/**\n * @file\n */\n");
} else {
$phpcsFile->fixer->addContent($stackPtr, "/**\n * @file\n */\n");
}
}
return ($phpcsFile->numTokens + 1);
}
}//end if
if ($fileTag === false || $tokens[$fileTag]['line'] !== ($tokens[$commentStart]['line'] + 1)) {
$secondLine = $phpcsFile->findNext([T_DOC_COMMENT_STAR, T_DOC_COMMENT_CLOSE_TAG], ($commentStart + 1), $commentEnd);
$fix = $phpcsFile->addFixableError('The second line in the file doc comment must be "@file"', $secondLine, 'FileTag');
if ($fix === true) {
if ($fileTag === false) {
$phpcsFile->fixer->addContent($commentStart, "\n * @file");
} else {
// Delete the @file tag at its current position and insert one
// after the beginning of the comment.
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->addContent($commentStart, "\n * @file");
$phpcsFile->fixer->replaceToken($fileTag, '');
$phpcsFile->fixer->endChangeset();
}
}
return ($phpcsFile->numTokens + 1);
}
// Exactly one blank line after the file comment.
if ($tokens[$next]['line'] !== ($tokens[$commentEnd]['line'] + 2)
&& $next !== false && $tokens[$next]['code'] !== T_CLOSE_TAG
) {
$error = 'There must be exactly one blank line after the file comment';
$fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfterComment');
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
$uselessLine = ($commentEnd + 1);
while ($uselessLine < $next) {
$phpcsFile->fixer->replaceToken($uselessLine, '');
$uselessLine++;
}
$phpcsFile->fixer->addContent($commentEnd, "\n\n");
$phpcsFile->fixer->endChangeset();
}
return ($phpcsFile->numTokens + 1);
}
// Template file: no blank line after the file comment.
if ($tokens[$next]['line'] !== ($tokens[$commentEnd]['line'] + 1)
&& $tokens[$next]['line'] > $tokens[$commentEnd]['line']
&& $tokens[$next]['code'] === T_CLOSE_TAG
) {
$error = 'There must be no blank line after the file comment in a template';
// cspell:ignore TeamplateSpacingAfterComment
$fix = $phpcsFile->addFixableError($error, $commentEnd, 'TeamplateSpacingAfterComment');
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
$uselessLine = ($commentEnd + 1);
while ($uselessLine < $next) {
$phpcsFile->fixer->replaceToken($uselessLine, '');
$uselessLine++;
}
$phpcsFile->fixer->addContent($commentEnd, "\n");
$phpcsFile->fixer->endChangeset();
}
}
// Ignore the rest of the file.
return ($phpcsFile->numTokens + 1);
}//end process()
}//end class

View File

@@ -0,0 +1,995 @@
<?php
/**
* Parses and verifies the doc comments for functions.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Parses and verifies the doc comments for functions. Largely copied from
* PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting\FunctionCommentSniff.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class FunctionCommentSniff implements Sniff
{
/**
* A map of invalid data types to valid ones for param and return documentation.
*
* @var array<string, string>
*/
public static $invalidTypes = [
'Array' => 'array',
'array()' => 'array',
'[]' => 'array',
'boolean' => 'bool',
'Boolean' => 'bool',
'integer' => 'int',
'str' => 'string',
'number' => 'int',
'String' => 'string',
'type' => 'mixed',
'NULL' => 'null',
'FALSE' => 'false',
'TRUE' => 'true',
'Bool' => 'bool',
'Int' => 'int',
'Integer' => 'int',
// cspell:ignore TRUEFALSE
'TRUEFALSE' => 'bool',
];
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_FUNCTION];
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$ignore = Tokens::$methodPrefixes;
$ignore[T_WHITESPACE] = T_WHITESPACE;
$functionCodeStart = $stackPtr;
for ($commentEnd = ($stackPtr - 1); $commentEnd >= 0; $commentEnd--) {
if (isset($ignore[$tokens[$commentEnd]['code']]) === true) {
continue;
}
if ($tokens[$commentEnd]['code'] === T_ATTRIBUTE_END
&& isset($tokens[$commentEnd]['attribute_opener']) === true
) {
$commentEnd = $functionCodeStart = $tokens[$commentEnd]['attribute_opener'];
continue;
}
break;
}
// Constructor methods are exempt from requiring a docblock.
// @see https://www.drupal.org/project/coder/issues/3400560.
$methodName = $phpcsFile->getDeclarationName($stackPtr);
if ($methodName === '__construct'
&& $tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
&& $tokens[$commentEnd]['code'] !== T_COMMENT
) {
return;
}
$beforeCommentEnd = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($commentEnd - 1), null, true);
if (($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
&& $tokens[$commentEnd]['code'] !== T_COMMENT)
|| ($beforeCommentEnd !== false
// If there is something more on the line than just the comment then the
// comment does not belong to the function.
&& $tokens[$beforeCommentEnd]['line'] === $tokens[$commentEnd]['line'])
) {
$fix = $phpcsFile->addFixableError('Missing function doc comment', $stackPtr, 'Missing');
if ($fix === true) {
$before = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), ($stackPtr + 1), true);
$phpcsFile->fixer->addContentBefore($before, "/**\n *\n */\n");
}
return;
}
if ($tokens[$commentEnd]['code'] === T_COMMENT) {
$fix = $phpcsFile->addFixableError('You must use "/**" style comments for a function comment', $stackPtr, 'WrongStyle');
if ($fix === true) {
// Convert the comment into a doc comment.
$phpcsFile->fixer->beginChangeset();
$comment = '';
for ($i = $commentEnd; $tokens[$i]['code'] === T_COMMENT; $i--) {
$comment = ' *'.ltrim($tokens[$i]['content'], '/* ').$comment;
$phpcsFile->fixer->replaceToken($i, '');
}
$phpcsFile->fixer->replaceToken($commentEnd, "/**\n".rtrim($comment, "*/\n")."\n */\n");
$phpcsFile->fixer->endChangeset();
}
return;
}
$commentStart = $tokens[$commentEnd]['comment_opener'];
foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
// This is a file comment, not a function comment.
if ($tokens[$tag]['content'] === '@file') {
$fix = $phpcsFile->addFixableError('Missing function doc comment', $stackPtr, 'Missing');
if ($fix === true) {
$before = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), ($stackPtr + 1), true);
$phpcsFile->fixer->addContentBefore($before, "/**\n *\n */\n");
}
return;
}
if ($tokens[$tag]['content'] === '@see') {
// Make sure the tag isn't empty.
$string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd);
if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) {
$error = 'Content missing for @see tag in function comment';
$phpcsFile->addError($error, $tag, 'EmptySees');
}
}
}//end foreach
if ($tokens[$commentEnd]['line'] !== ($tokens[$functionCodeStart]['line'] - 1)) {
$error = 'There must be no blank lines after the function comment';
$fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfter');
if ($fix === true) {
$phpcsFile->fixer->replaceToken(($commentEnd + 1), '');
}
}
$this->processReturn($phpcsFile, $stackPtr, $commentStart);
$this->processThrows($phpcsFile, $stackPtr, $commentStart);
$this->processParams($phpcsFile, $stackPtr, $commentStart);
$this->processSees($phpcsFile, $stackPtr, $commentStart);
}//end process()
/**
* Process the return comment of this function comment.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
* @param int $commentStart The position in the stack where the comment started.
*
* @return void
*/
protected function processReturn(File $phpcsFile, $stackPtr, $commentStart)
{
$tokens = $phpcsFile->getTokens();
$return = null;
$end = $stackPtr;
foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
if ($tokens[$tag]['content'] === '@return') {
if ($return !== null) {
$error = 'Only 1 @return tag is allowed in a function comment';
$phpcsFile->addError($error, $tag, 'DuplicateReturn');
return;
}
$return = $tag;
// Any strings until the next tag belong to this comment.
if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) {
$skipTags = [
'@code',
'@endcode',
];
$skipPos = ($pos + 1);
while (isset($tokens[$commentStart]['comment_tags'][$skipPos]) === true
&& in_array($tokens[$commentStart]['comment_tags'][$skipPos], $skipTags) === true
) {
$skipPos++;
}
$end = $tokens[$commentStart]['comment_tags'][$skipPos];
} else {
$end = $tokens[$commentStart]['comment_closer'];
}
}//end if
}//end foreach
if ($return !== null) {
$returnType = trim($tokens[($return + 2)]['content']);
if (empty($returnType) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) {
$error = 'Return type missing for @return tag in function comment';
$phpcsFile->addError($error, $return, 'MissingReturnType');
} else if (strpos($returnType, ' ') === false) {
// Check return type (can be multiple, separated by '|').
$typeNames = explode('|', $returnType);
$suggestedNames = [];
$hasNull = false;
foreach ($typeNames as $i => $typeName) {
if (strtolower($typeName) === 'null') {
$hasNull = true;
}
$suggestedName = $this->suggestType($typeName);
if (in_array($suggestedName, $suggestedNames, true) === false) {
$suggestedNames[] = $suggestedName;
}
}
$suggestedType = implode('|', $suggestedNames);
if ($returnType !== $suggestedType) {
$error = 'Expected "%s" but found "%s" for function return type';
$data = [
$suggestedType,
$returnType,
];
$fix = $phpcsFile->addFixableError($error, $return, 'InvalidReturn', $data);
if ($fix === true) {
$replacement = $suggestedType;
$phpcsFile->fixer->replaceToken(($return + 2), $replacement);
unset($replacement);
}
}
// If the return type is void, make sure there is
// no return statement in the function.
if ($returnType === 'void') {
if (isset($tokens[$stackPtr]['scope_closer']) === true) {
$endToken = $tokens[$stackPtr]['scope_closer'];
for ($returnToken = $stackPtr; $returnToken < $endToken; $returnToken++) {
if ($tokens[$returnToken]['code'] === T_CLOSURE
|| $tokens[$returnToken]['code'] === T_ANON_CLASS
) {
$returnToken = $tokens[$returnToken]['scope_closer'];
continue;
}
if ($tokens[$returnToken]['code'] === T_RETURN
|| $tokens[$returnToken]['code'] === T_YIELD
|| $tokens[$returnToken]['code'] === T_YIELD_FROM
) {
break;
}
}
if ($returnToken !== $endToken) {
// If the function is not returning anything, just
// exiting, then there is no problem.
$semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true);
if ($tokens[$semicolon]['code'] !== T_SEMICOLON) {
$error = 'Function return type is void, but function contains return statement';
$phpcsFile->addError($error, $return, 'InvalidReturnVoid');
}
}
}//end if
} else if ($returnType !== 'mixed'
&& $returnType !== 'never'
&& in_array('void', $typeNames, true) === false
) {
// If return type is not void, never, or mixed, there needs to be a
// return statement somewhere in the function that returns something.
if (isset($tokens[$stackPtr]['scope_closer']) === true) {
$endToken = $tokens[$stackPtr]['scope_closer'];
for ($returnToken = $stackPtr; $returnToken < $endToken; $returnToken++) {
if ($tokens[$returnToken]['code'] === T_CLOSURE
|| $tokens[$returnToken]['code'] === T_ANON_CLASS
) {
$returnToken = $tokens[$returnToken]['scope_closer'];
continue;
}
if ($tokens[$returnToken]['code'] === T_RETURN
|| $tokens[$returnToken]['code'] === T_YIELD
|| $tokens[$returnToken]['code'] === T_YIELD_FROM
) {
break;
}
}
if ($returnToken === $endToken) {
$error = 'Function return type is not void, but function has no return statement';
$phpcsFile->addError($error, $return, 'InvalidNoReturn');
} else {
$semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true);
// Void return is allowed if the @return type has null in it.
if ($tokens[$semicolon]['code'] === T_SEMICOLON && $hasNull === false) {
$error = 'Function return type is not void, but function is returning void here';
$phpcsFile->addError($error, $returnToken, 'InvalidReturnNotVoid');
}
}
}//end if
}//end if
}//end if
$comment = '';
for ($i = ($return + 3); $i < $end; $i++) {
if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
$indent = 0;
if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) {
$indent = strlen($tokens[($i - 1)]['content']);
}
$comment .= ' '.$tokens[$i]['content'];
$commentLines[] = [
'comment' => $tokens[$i]['content'],
'token' => $i,
'indent' => $indent,
];
if ($indent < 3) {
$error = 'Return comment indentation must be 3 spaces, found %s spaces';
$fix = $phpcsFile->addFixableError($error, $i, 'ReturnCommentIndentation', [$indent]);
if ($fix === true) {
$phpcsFile->fixer->replaceToken(($i - 1), ' ');
}
}
}
}//end for
// The first line of the comment must be indented no more than 3
// spaces, the following lines can be more so we only check the first
// line.
if (empty($commentLines[0]['indent']) === false && $commentLines[0]['indent'] > 3) {
$error = 'Return comment indentation must be 3 spaces, found %s spaces';
$fix = $phpcsFile->addFixableError($error, ($commentLines[0]['token'] - 1), 'ReturnCommentIndentation', [$commentLines[0]['indent']]);
if ($fix === true) {
$phpcsFile->fixer->replaceToken(($commentLines[0]['token'] - 1), ' ');
}
}
if ($comment === '' && $returnType !== '$this' && $returnType !== 'static') {
if (strpos($returnType, ' ') !== false) {
$error = 'Description for the @return value must be on the next line';
} else {
$error = 'Description for the @return value is missing';
}
$phpcsFile->addError($error, $return, 'MissingReturnComment');
} else if (strpos($returnType, ' ') !== false) {
if (preg_match('/^([^\s]+)[\s]+(\$[^\s]+)[\s]*$/', $returnType, $matches) === 1) {
$error = 'Return type must not contain variable name "%s"';
$data = [$matches[2]];
$fix = $phpcsFile->addFixableError($error, ($return + 2), 'ReturnVarName', $data);
if ($fix === true) {
$phpcsFile->fixer->replaceToken(($return + 2), $matches[1]);
}
// Do not check PHPStan types that contain any kind of brackets.
// See https://phpstan.org/writing-php-code/phpdoc-types#general-arrays .
} else if (preg_match('/[<\[\{\(]/', $returnType) === 0) {
$error = 'Return type "%s" must not contain spaces';
$data = [$returnType];
$phpcsFile->addError($error, $return, 'ReturnTypeSpaces', $data);
}
}//end if
}//end if
}//end processReturn()
/**
* Process any throw tags that this function comment has.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
* @param int $commentStart The position in the stack where the comment started.
*
* @return void
*/
protected function processThrows(File $phpcsFile, $stackPtr, $commentStart)
{
$tokens = $phpcsFile->getTokens();
foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
if ($tokens[$tag]['content'] !== '@throws') {
continue;
}
if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
$error = 'Exception type missing for @throws tag in function comment';
$phpcsFile->addError($error, $tag, 'InvalidThrows');
} else {
// Any strings until the next tag belong to this comment.
if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) {
$end = $tokens[$commentStart]['comment_tags'][($pos + 1)];
} else {
$end = $tokens[$commentStart]['comment_closer'];
}
$comment = '';
$throwStart = null;
for ($i = ($tag + 3); $i < $end; $i++) {
if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
if ($throwStart === null) {
$throwStart = $i;
}
$indent = 0;
if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) {
$indent = strlen($tokens[($i - 1)]['content']);
}
$comment .= ' '.$tokens[$i]['content'];
if ($indent < 3) {
$error = 'Throws comment indentation must be 3 spaces, found %s spaces';
// cspell:ignore TrhowsCommentIndentation
$phpcsFile->addError($error, $i, 'TrhowsCommentIndentation', [$indent]);
}
}
}
$comment = trim($comment);
if ($comment === '') {
if (str_word_count($tokens[($tag + 2)]['content'], 0, '\\_') > 1) {
$error = '@throws comment must be on the next line';
$phpcsFile->addError($error, $tag, 'ThrowsComment');
}
return;
}
// Starts with a capital letter and ends with a full stop.
$firstChar = $comment[0];
if (strtoupper($firstChar) !== $firstChar) {
$error = '@throws tag comment must start with a capital letter';
$phpcsFile->addError($error, $throwStart, 'ThrowsNotCapital');
}
$lastChar = substr($comment, -1);
if (in_array($lastChar, ['.', '!', '?']) === false) {
$error = '@throws tag comment must end with a full stop';
$phpcsFile->addError($error, $throwStart, 'ThrowsNoFullStop');
}
}//end if
}//end foreach
}//end processThrows()
/**
* Process the function parameter comments.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
* @param int $commentStart The position in the stack where the comment started.
*
* @return void
*/
protected function processParams(File $phpcsFile, $stackPtr, $commentStart)
{
$tokens = $phpcsFile->getTokens();
$params = [];
$maxType = 0;
$maxVar = 0;
foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
if ($tokens[$tag]['content'] !== '@param') {
continue;
}
$type = '';
$typeSpace = 0;
$var = '';
$varSpace = 0;
$comment = '';
$commentLines = [];
if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) {
$matches = [];
preg_match('/((?:(?![$.]|&(?=\$)).)*)(?:((?:\.\.\.)?(?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/', $tokens[($tag + 2)]['content'], $matches);
$typeLen = strlen($matches[1]);
$type = trim($matches[1]);
$typeSpace = ($typeLen - strlen($type));
$typeLen = strlen($type);
if ($typeLen > $maxType) {
$maxType = $typeLen;
}
// If there is more than one word then it is a comment that should be
// on the next line.
if (isset($matches[4]) === true && ($typeLen > 0
|| preg_match('/[^\s]+[\s]+[^\s]+/', $matches[4]) === 1)
) {
$comment = $matches[4];
$error = 'Parameter comment must be on the next line';
$fix = $phpcsFile->addFixableError($error, ($tag + 2), 'ParamCommentNewLine');
if ($fix === true) {
$parts = $matches;
unset($parts[0]);
$parts[3] = "\n * ";
$phpcsFile->fixer->replaceToken(($tag + 2), implode('', $parts));
}
}
if (isset($matches[2]) === true) {
$var = $matches[2];
} else {
$var = '';
}
if (substr($var, -1) === '.') {
$error = 'Doc comment parameter name "%s" must not end with a dot';
$fix = $phpcsFile->addFixableError($error, ($tag + 2), 'ParamNameDot', [$var]);
if ($fix === true) {
$content = $type.' '.substr($var, 0, -1);
$phpcsFile->fixer->replaceToken(($tag + 2), $content);
}
// Continue with the next parameter to avoid confusing
// overlapping errors further down.
continue;
}
$varLen = strlen($var);
if ($varLen > $maxVar) {
$maxVar = $varLen;
}
// Any strings until the next tag belong to this comment.
if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) {
// Ignore code tags and include them within this comment.
$skipTags = [
'@code',
'@endcode',
'@link',
];
$skipPos = $pos;
while (isset($tokens[$commentStart]['comment_tags'][($skipPos + 1)]) === true) {
$skipPos++;
if (in_array($tokens[$tokens[$commentStart]['comment_tags'][$skipPos]]['content'], $skipTags) === false
// Stop when we reached the next tag on the outer @param level.
&& $tokens[$tokens[$commentStart]['comment_tags'][$skipPos]]['column'] === $tokens[$tag]['column']
) {
break;
}
}
if ($tokens[$tokens[$commentStart]['comment_tags'][$skipPos]]['column'] === ($tokens[$tag]['column'] + 2)) {
$end = $tokens[$commentStart]['comment_closer'];
} else {
$end = $tokens[$commentStart]['comment_tags'][$skipPos];
}
} else {
$end = $tokens[$commentStart]['comment_closer'];
}//end if
for ($i = ($tag + 3); $i < $end; $i++) {
if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
$indent = 0;
if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) {
$indent = strlen($tokens[($i - 1)]['content']);
// There can be @code or @link tags within an @param comment.
if ($tokens[($i - 2)]['code'] === T_DOC_COMMENT_TAG) {
$indent = 0;
if ($tokens[($i - 3)]['code'] === T_DOC_COMMENT_WHITESPACE) {
$indent = strlen($tokens[($i - 3)]['content']);
}
}
}
$comment .= ' '.$tokens[$i]['content'];
$commentLines[] = [
'comment' => $tokens[$i]['content'],
'token' => $i,
'indent' => $indent,
];
if ($indent < 3) {
$error = 'Parameter comment indentation must be 3 spaces, found %s spaces';
$fix = $phpcsFile->addFixableError($error, $i, 'ParamCommentIndentation', [$indent]);
if ($fix === true) {
$phpcsFile->fixer->replaceToken(($i - 1), ' ');
}
}
}//end if
}//end for
// The first line of the comment must be indented no more than 3
// spaces, the following lines can be more so we only check the first
// line.
if (empty($commentLines[0]['indent']) === false && $commentLines[0]['indent'] > 3) {
$error = 'Parameter comment indentation must be 3 spaces, found %s spaces';
$fix = $phpcsFile->addFixableError($error, ($commentLines[0]['token'] - 1), 'ParamCommentIndentation', [$commentLines[0]['indent']]);
if ($fix === true) {
$phpcsFile->fixer->replaceToken(($commentLines[0]['token'] - 1), ' ');
}
}
if ($comment === '') {
$error = 'Missing parameter comment';
$phpcsFile->addError($error, $tag, 'MissingParamComment');
$commentLines[] = ['comment' => ''];
}//end if
$variableArguments = false;
// Allow the "..." @param doc for a variable number of parameters.
// This could happen with type defined as @param array ... or
// without type defined as @param ...
if ($tokens[($tag + 2)]['content'] === '...'
|| (substr($tokens[($tag + 2)]['content'], -3) === '...'
&& count(explode(' ', $tokens[($tag + 2)]['content'])) === 2)
) {
$variableArguments = true;
}
if ($typeLen === 0 && $variableArguments === false) {
$error = 'Missing parameter type';
// If there is just one word as comment at the end of the line
// then this is probably the data type. Move it before the
// variable name.
if (isset($matches[4]) === true && preg_match('/[^\s]+[\s]+[^\s]+/', $matches[4]) === 0) {
$fix = $phpcsFile->addFixableError($error, $tag, 'MissingParamType');
if ($fix === true) {
$phpcsFile->fixer->replaceToken(($tag + 2), $matches[4].' '.$var);
}
} else {
$phpcsFile->addError($error, $tag, 'MissingParamType');
}
}
if (empty($matches[2]) === true && $variableArguments === false) {
$error = 'Missing parameter name';
$phpcsFile->addError($error, $tag, 'MissingParamName');
}
} else {
$error = 'Missing parameter type';
$phpcsFile->addError($error, $tag, 'MissingParamType');
}//end if
$params[] = [
'tag' => $tag,
'type' => $type,
'var' => $var,
'comment' => $comment,
'commentLines' => $commentLines,
'type_space' => $typeSpace,
'var_space' => $varSpace,
];
}//end foreach
$realParams = $phpcsFile->getMethodParameters($stackPtr);
$foundParams = [];
$checkPos = 0;
foreach ($params as $pos => $param) {
if ($param['var'] === '') {
continue;
}
$foundParams[] = $param['var'];
// If the type is empty, the whole line is empty.
if ($param['type'] === '') {
continue;
}
// Make sure the param name is correct.
$matched = false;
// Parameter documentation can be omitted for some parameters, so we have
// to search the rest for a match.
$realName = '<undefined>';
while (isset($realParams[($checkPos)]) === true) {
$realName = $realParams[$checkPos]['name'];
if ($realName === $param['var']
|| ($realParams[$checkPos]['pass_by_reference'] === true
&& ('&'.$realName) === $param['var'])
|| ($realParams[$checkPos]['variable_length'] === true
&& ('...'.$realName) === $param['var'])
) {
$matched = true;
break;
}
$checkPos++;
}
// Support variadic arguments.
if (preg_match('/(\s+)\.{3}$/', $param['type'], $matches) === 1) {
$param['type_space'] = strlen($matches[1]);
$param['type'] = preg_replace('/\s+\.{3}$/', '', $param['type']);
}
// Check the param type value. This could also be multiple parameter
// types separated by '|'.
$typeNames = explode('|', $param['type']);
$suggestedNames = [];
foreach ($typeNames as $i => $typeName) {
$suggestedNames[] = static::suggestType($typeName);
}
$suggestedType = implode('|', $suggestedNames);
if (preg_match('/\s/', $param['type']) === 1) {
// Do not check PHPStan types that contain any kind of brackets.
// See https://phpstan.org/writing-php-code/phpdoc-types#general-arrays .
if (preg_match('/[<\[\{\(]/', $param['type']) === 0) {
$error = 'Parameter type "%s" must not contain spaces';
$data = [$param['type']];
$phpcsFile->addError($error, $param['tag'], 'ParamTypeSpaces', $data);
}
} else if ($param['type'] !== $suggestedType) {
$error = 'Expected "%s" but found "%s" for parameter type';
$data = [
$suggestedType,
$param['type'],
];
$fix = $phpcsFile->addFixableError($error, $param['tag'], 'IncorrectParamVarName', $data);
if ($fix === true) {
$content = $suggestedType;
$content .= str_repeat(' ', $param['type_space']);
$content .= $param['var'];
$phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content);
}
}//end if
// Check number of spaces after the type.
$spaces = 1;
if ($param['type_space'] !== $spaces) {
$error = 'Expected %s spaces after parameter type; %s found';
$data = [
$spaces,
$param['type_space'],
];
$fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data);
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
$content = $param['type'];
$content .= str_repeat(' ', $spaces);
$content .= $param['var'];
$content .= str_repeat(' ', $param['var_space']);
// At this point there is no description expected in the
// param line so no need to append comment.
$phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content);
// Fix up the indent of additional comment lines.
foreach ($param['commentLines'] as $lineNum => $line) {
if ($lineNum === 0
|| $param['commentLines'][$lineNum]['indent'] === 0
) {
continue;
}
$newIndent = max(($param['commentLines'][$lineNum]['indent'] + $spaces - $param['type_space']), 0);
$phpcsFile->fixer->replaceToken(
($param['commentLines'][$lineNum]['token'] - 1),
str_repeat(' ', $newIndent)
);
}
$phpcsFile->fixer->endChangeset();
}//end if
}//end if
if ($matched === false) {
if ($checkPos >= $pos) {
$code = 'ParamNameNoMatch';
$data = [
$param['var'],
$realName,
];
$error = 'Doc comment for parameter %s does not match ';
if (strtolower($param['var']) === strtolower($realName)) {
$error .= 'case of ';
$code = 'ParamNameNoCaseMatch';
}
$error .= 'actual variable name %s';
$phpcsFile->addError($error, $param['tag'], $code, $data);
// Reset the parameter position to check for following
// parameters.
$checkPos = ($pos - 1);
} else if (substr($param['var'], -4) !== ',...') {
// We must have an extra parameter comment.
$error = 'Superfluous parameter comment';
$phpcsFile->addError($error, $param['tag'], 'ExtraParamComment');
}//end if
}//end if
$checkPos++;
if ($param['comment'] === '') {
continue;
}
// Param comments must start with a capital letter and end with the full stop.
if (isset($param['commentLines'][0]['comment']) === true) {
$firstChar = $param['commentLines'][0]['comment'];
} else {
$firstChar = $param['comment'];
}
if (preg_match('|\p{Lu}|u', $firstChar) === 0) {
$error = 'Parameter comment must start with a capital letter';
if (isset($param['commentLines'][0]['token']) === true) {
$commentToken = $param['commentLines'][0]['token'];
} else {
$commentToken = $param['tag'];
}
$phpcsFile->addError($error, $commentToken, 'ParamCommentNotCapital');
}
$lastChar = substr($param['comment'], -1);
if (in_array($lastChar, ['.', '!', '?', ')']) === false) {
$error = 'Parameter comment must end with a full stop';
if (empty($param['commentLines']) === true) {
$commentToken = ($param['tag'] + 2);
} else {
$lastLine = end($param['commentLines']);
$commentToken = $lastLine['token'];
}
// Don't show an error if the end of the comment is in a code
// example.
if ($this->isInCodeExample($phpcsFile, $commentToken, $param['tag']) === false) {
$fix = $phpcsFile->addFixableError($error, $commentToken, 'ParamCommentFullStop');
if ($fix === true) {
// Add a full stop as the last character of the comment.
$phpcsFile->fixer->addContent($commentToken, '.');
}
}
}
}//end foreach
// Missing parameters only apply to methods and not function because on
// functions it is allowed to leave out param comments for form constructors
// for example.
// It is also allowed to omit param tags completely, in which case we don't
// throw errors. Only throw errors if param comments exists but are
// incomplete on class methods.
if ($tokens[$stackPtr]['level'] > 0 && empty($foundParams) === false) {
foreach ($realParams as $realParam) {
$realParamKeyName = $realParam['name'];
if (in_array($realParamKeyName, $foundParams) === false
&& (($realParam['pass_by_reference'] === true
&& in_array("&$realParamKeyName", $foundParams) === true)
|| ($realParam['variable_length'] === true
&& in_array("...$realParamKeyName", $foundParams) === true)) === false
) {
$error = 'Parameter %s is not described in comment';
$phpcsFile->addError($error, $commentStart, 'ParamMissingDefinition', [$realParam['name']]);
}
}
}//end if
}//end processParams()
/**
* Process the function "see" comments.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
* @param int $commentStart The position in the stack where the comment started.
*
* @return void
*/
protected function processSees(File $phpcsFile, $stackPtr, $commentStart)
{
$tokens = $phpcsFile->getTokens();
foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
if ($tokens[$tag]['content'] !== '@see') {
continue;
}
if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) {
$comment = $tokens[($tag + 2)]['content'];
if (strpos($comment, ' ') !== false) {
$error = 'The @see reference should not contain any additional text';
$phpcsFile->addError($error, $tag, 'SeeAdditionalText');
continue;
}
if (preg_match('/[\.!\?]$/', $comment) === 1) {
$error = 'Trailing punctuation for @see references is not allowed.';
$fix = $phpcsFile->addFixableError($error, $tag, 'SeePunctuation');
if ($fix === true) {
// Replace the last character from the comment which is
// already tested to be a punctuation.
$content = substr($comment, 0, -1);
$phpcsFile->fixer->replaceToken(($tag + 2), $content);
}//end if
}
}
}//end foreach
}//end processSees()
/**
* Returns a valid variable type for param/var tag.
*
* @param string $type The variable type to process.
*
* @return string
*/
public static function suggestType($type)
{
if (isset(static::$invalidTypes[$type]) === true) {
return static::$invalidTypes[$type];
}
if ($type === '$this') {
return $type;
}
// Also allow some more characters for special type hints supported by
// PHPStan:
// https://phpstan.org/writing-php-code/phpdoc-types#basic-types .
$type = preg_replace('/[^a-zA-Z0-9_\\\[\]\-<> ,"\{\}\?\':\*\|\&]/', '', $type);
return $type;
}//end suggestType()
/**
* Determines if a comment line is part of an @code/@endcode example.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
* @param int $commentStart The position of the start of the comment
* in the stack passed in $tokens.
*
* @return boolean Returns true if the comment line is within a @code block,
* false otherwise.
*/
protected function isInCodeExample(File $phpcsFile, $stackPtr, $commentStart)
{
$tokens = $phpcsFile->getTokens();
if (strpos($tokens[$stackPtr]['content'], '@code') !== false) {
return true;
}
$prevTag = $phpcsFile->findPrevious([T_DOC_COMMENT_TAG], ($stackPtr - 1), $commentStart);
if ($prevTag === false) {
return false;
}
if ($tokens[$prevTag]['content'] === '@code') {
return true;
}
return false;
}//end isInCodeExample()
}//end class

View File

@@ -0,0 +1,60 @@
<?php
/**
* Parses and verifies comment language.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
/**
* Parses and verifies that comments use gender neutral language.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class GenderNeutralCommentSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [
T_COMMENT,
T_DOC_COMMENT_STRING,
];
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if ((bool) preg_match('/(^|\W)(he|her|hers|him|his|she)($|\W)/i', $tokens[$stackPtr]['content']) === true) {
$phpcsFile->addError('Unnecessarily gendered language in a comment', $stackPtr, 'GenderNeutral');
}
}//end process()
}//end class

View File

@@ -0,0 +1,129 @@
<?php
/**
* Ensures hook comments on function are correct.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
/**
* Ensures hook comments on function are correct.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class HookCommentSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_FUNCTION];
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
// We are only interested in the most outer scope, ignore methods in classes for example.
if (empty($tokens[$stackPtr]['conditions']) === false) {
return;
}
$commentEnd = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG) {
return;
}
$commentStart = $tokens[$commentEnd]['comment_opener'];
$empty = [
T_DOC_COMMENT_WHITESPACE,
T_DOC_COMMENT_STAR,
];
$short = $phpcsFile->findNext($empty, ($commentStart + 1), $commentEnd, true);
if ($short === false) {
// No content at all.
return;
}
// Account for the fact that a short description might cover
// multiple lines.
$shortContent = $tokens[$short]['content'];
$shortEnd = $short;
for ($i = ($short + 1); $i < $commentEnd; $i++) {
if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
if ($tokens[$i]['line'] === ($tokens[$shortEnd]['line'] + 1)) {
$shortContent .= $tokens[$i]['content'];
$shortEnd = $i;
} else {
break;
}
}
}
// Check if a hook implementation doc block is formatted correctly.
if (preg_match('/^[\s]*Implement[^\n]+?hook_[^\n]+/i', $shortContent, $matches) === 1) {
if (strstr($matches[0], 'Implements ') === false || strstr($matches[0], 'Implements of') !== false
|| preg_match('/ (drush_)?hook_[a-zA-Z0-9_]+\(\)( for .+)?\.$/', $matches[0]) !== 1
) {
$phpcsFile->addWarning('Format should be "* Implements hook_foo().", "* Implements hook_foo_BAR_ID_bar() for xyz_bar().",, "* Implements hook_foo_BAR_ID_bar() for xyz-bar.html.twig.", "* Implements hook_foo_BAR_ID_bar() for xyz-bar.tpl.php.", or "* Implements hook_foo_BAR_ID_bar() for block templates."', $short, 'HookCommentFormat');
} else {
// Check that a hook implementation does not duplicate param and
// return documentation.
foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
if ($tokens[$tag]['content'] === '@param') {
$warn = 'Hook implementations should not duplicate @param documentation';
$phpcsFile->addWarning($warn, $tag, 'HookParamDoc');
}
if ($tokens[$tag]['content'] === '@return') {
$warn = 'Hook implementations should not duplicate @return documentation';
$phpcsFile->addWarning($warn, $tag, 'HookReturnDoc');
}
}
}//end if
return;
}//end if
// Check if the doc block just repeats the function name with
// "Implements example_hook_name()".
$functionName = $phpcsFile->getDeclarationName($stackPtr);
if ($functionName !== null && preg_match("/^[\s]*Implements $functionName\(\)\.$/i", $shortContent) === 1) {
$error = 'Hook implementations must be documented with "Implements hook_example()."';
$fix = $phpcsFile->addFixableError($error, $short, 'HookRepeat');
if ($fix === true) {
$newComment = preg_replace('/Implements [^_]+/', 'Implements hook', $shortContent);
$phpcsFile->fixer->replaceToken($short, $newComment);
}
}
}//end process()
}//end class

View File

@@ -0,0 +1,497 @@
<?php
/**
* \Drupal\Sniffs\Commenting\InlineCommentSniff.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* \Drupal\Sniffs\Commenting\InlineCommentSniff.
*
* Checks that no perl-style comments are used. Checks that inline comments ("//")
* have a space after //, start capitalized and end with proper punctuation.
* Largely copied from
* \PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting\InlineCommentSniff.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class InlineCommentSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [
T_COMMENT,
T_DOC_COMMENT_OPEN_TAG,
];
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the
* stack passed in $tokens.
*
* @return int|void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
// If this is a function/class/interface doc block comment, skip it.
// We are only interested in inline doc block comments, which are
// not allowed.
if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_OPEN_TAG) {
$nextToken = $phpcsFile->findNext(
Tokens::$emptyTokens,
($stackPtr + 1),
null,
true
);
$ignore = [
T_ATTRIBUTE,
T_CLASS,
T_INTERFACE,
T_TRAIT,
T_ENUM,
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,
T_VAR,
];
// Also ignore all doc blocks defined in the outer scope (no scope
// conditions are set).
if (in_array($tokens[$nextToken]['code'], $ignore, true) === true
|| empty($tokens[$stackPtr]['conditions']) === true
) {
return;
}
if ($phpcsFile->tokenizerType === 'JS') {
// We allow block comments if a function or object
// is being assigned to a variable.
$ignore = Tokens::$emptyTokens;
$ignore[] = T_EQUAL;
$ignore[] = T_STRING;
$ignore[] = T_OBJECT_OPERATOR;
$nextToken = $phpcsFile->findNext($ignore, ($nextToken + 1), null, true);
if ($tokens[$nextToken]['code'] === T_FUNCTION
|| $tokens[$nextToken]['code'] === T_CLOSURE
|| $tokens[$nextToken]['code'] === T_OBJECT
|| $tokens[$nextToken]['code'] === T_PROTOTYPE
) {
return;
}
}
$prevToken = $phpcsFile->findPrevious(
Tokens::$emptyTokens,
($stackPtr - 1),
null,
true
);
if ($tokens[$prevToken]['code'] === T_OPEN_TAG) {
return;
}
// Inline doc blocks are allowed in JSDoc.
if ($tokens[$stackPtr]['content'] === '/**' && $phpcsFile->tokenizerType !== 'JS') {
// The only exception to inline doc blocks is the /** @var */
// declaration. Allow that in any form.
$varTag = $phpcsFile->findNext([T_DOC_COMMENT_TAG], ($stackPtr + 1), $tokens[$stackPtr]['comment_closer'], false, '@var');
if ($varTag === false) {
$error = 'Inline doc block comments are not allowed; use "/* Comment */" or "// Comment" instead';
$phpcsFile->addError($error, $stackPtr, 'DocBlock');
}
}
}//end if
if ($tokens[$stackPtr]['content'][0] === '#') {
$error = 'Perl-style comments are not allowed; use "// Comment" instead';
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'WrongStyle');
if ($fix === true) {
$comment = ltrim($tokens[$stackPtr]['content'], "# \t");
$phpcsFile->fixer->replaceToken($stackPtr, "// $comment");
}
}
// We don't want end of block comments. Check if the last token before the
// comment is a closing curly brace.
$previousContent = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
if ($tokens[$previousContent]['line'] === $tokens[$stackPtr]['line']) {
if ($tokens[$previousContent]['code'] === T_CLOSE_CURLY_BRACKET) {
return;
}
// Special case for JS files.
if ($tokens[$previousContent]['code'] === T_COMMA
|| $tokens[$previousContent]['code'] === T_SEMICOLON
) {
$lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($previousContent - 1), null, true);
if ($tokens[$lastContent]['code'] === T_CLOSE_CURLY_BRACKET) {
return;
}
}
}
// Only want inline comments.
if (substr($tokens[$stackPtr]['content'], 0, 2) !== '//') {
return;
}
// Ignore code example lines.
if ($this->isInCodeExample($phpcsFile, $stackPtr) === true) {
return;
}
$commentTokens = [$stackPtr];
$nextComment = $stackPtr;
$lastComment = $stackPtr;
while (($nextComment = $phpcsFile->findNext(T_COMMENT, ($nextComment + 1), null, false)) !== false) {
if ($tokens[$nextComment]['line'] !== ($tokens[$lastComment]['line'] + 1)) {
break;
}
// Only want inline comments.
if (substr($tokens[$nextComment]['content'], 0, 2) !== '//') {
break;
}
// There is a comment on the very next line. If there is
// no code between the comments, they are part of the same
// comment block.
$prevNonWhitespace = $phpcsFile->findPrevious(T_WHITESPACE, ($nextComment - 1), $lastComment, true);
if ($prevNonWhitespace !== $lastComment) {
break;
}
// A comment starting with "@" means a new comment section.
if (preg_match('|^//[\s]*@|', $tokens[$nextComment]['content']) === 1) {
break;
}
$commentTokens[] = $nextComment;
$lastComment = $nextComment;
}//end while
$commentText = '';
$lastCommentToken = $stackPtr;
foreach ($commentTokens as $lastCommentToken) {
$comment = rtrim($tokens[$lastCommentToken]['content']);
if (trim(substr($comment, 2)) === '') {
continue;
}
$spaceCount = 0;
$tabFound = false;
$commentLength = strlen($comment);
for ($i = 2; $i < $commentLength; $i++) {
if ($comment[$i] === "\t") {
$tabFound = true;
break;
}
if ($comment[$i] !== ' ') {
break;
}
$spaceCount++;
}
$fix = false;
if ($tabFound === true) {
$error = 'Tab found before comment text; expected "// %s" but found "%s"';
$data = [
ltrim(substr($comment, 2)),
$comment,
];
$fix = $phpcsFile->addFixableError($error, $lastCommentToken, 'TabBefore', $data);
} else if ($spaceCount === 0) {
$error = 'No space found before comment text; expected "// %s" but found "%s"';
$data = [
substr($comment, 2),
$comment,
];
$fix = $phpcsFile->addFixableError($error, $lastCommentToken, 'NoSpaceBefore', $data);
}//end if
if ($fix === true) {
$newComment = '// '.ltrim($tokens[$lastCommentToken]['content'], "/\t ");
$phpcsFile->fixer->replaceToken($lastCommentToken, $newComment);
}
if ($spaceCount > 1) {
// Check if there is a comment on the previous line that justifies the
// indentation.
$prevComment = $phpcsFile->findPrevious([T_COMMENT], ($lastCommentToken - 1), null, false);
if (($prevComment !== false) && (($tokens[$prevComment]['line']) === ($tokens[$lastCommentToken]['line'] - 1))) {
$prevCommentText = rtrim($tokens[$prevComment]['content']);
$prevSpaceCount = 0;
for ($i = 2; $i < strlen($prevCommentText); $i++) {
if ($prevCommentText[$i] !== ' ') {
break;
}
$prevSpaceCount++;
}
if ($spaceCount > $prevSpaceCount && $prevSpaceCount > 0) {
// A previous comment could be a list item or @todo.
$indentationStarters = [
'-',
'@todo',
];
$words = preg_split('/\s+/', $prevCommentText);
$numberedList = (bool) preg_match('/^[0-9]+\./', $words[1]);
if (in_array($words[1], $indentationStarters) === true) {
if ($spaceCount !== ($prevSpaceCount + 2)) {
$error = 'Comment indentation error after %s element, expected %s spaces';
$fix = $phpcsFile->addFixableError($error, $lastCommentToken, 'SpacingBefore', [$words[1], ($prevSpaceCount + 2)]);
if ($fix === true) {
$newComment = '//'.str_repeat(' ', ($prevSpaceCount + 2)).ltrim($tokens[$lastCommentToken]['content'], "/\t ");
$phpcsFile->fixer->replaceToken($lastCommentToken, $newComment);
}
}
} else if ($numberedList === true) {
$expectedSpaceCount = ($prevSpaceCount + strlen($words[1]) + 1);
if ($spaceCount !== $expectedSpaceCount) {
$error = 'Comment indentation error, expected %s spaces';
$fix = $phpcsFile->addFixableError($error, $lastCommentToken, 'SpacingBefore', [$expectedSpaceCount]);
if ($fix === true) {
$newComment = '//'.str_repeat(' ', $expectedSpaceCount).ltrim($tokens[$lastCommentToken]['content'], "/\t ");
$phpcsFile->fixer->replaceToken($lastCommentToken, $newComment);
}
}
} else {
$error = 'Comment indentation error, expected only %s spaces';
$phpcsFile->addError($error, $lastCommentToken, 'SpacingBefore', [$prevSpaceCount]);
}//end if
}//end if
} else {
$error = '%s spaces found before inline comment; expected "// %s" but found "%s"';
$data = [
$spaceCount,
substr($comment, (2 + $spaceCount)),
$comment,
];
$fix = $phpcsFile->addFixableError($error, $lastCommentToken, 'SpacingBefore', $data);
if ($fix === true) {
$phpcsFile->fixer->replaceToken($lastCommentToken, '// '.substr($comment, (2 + $spaceCount)).$phpcsFile->eolChar);
}
}//end if
}//end if
$commentText .= trim(substr($tokens[$lastCommentToken]['content'], 2));
}//end foreach
if ($commentText === '') {
$error = 'Blank comments are not allowed';
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'Empty');
if ($fix === true) {
$phpcsFile->fixer->replaceToken($stackPtr, '');
}
return ($lastCommentToken + 1);
}
$words = preg_split('/\s+/', $commentText);
if (preg_match('/^\p{Ll}/u', $commentText) === 1) {
// Allow special lower cased words that contain non-alpha characters
// (function references, machine names with underscores etc.).
$matches = [];
preg_match('/[a-z]+/', $words[0], $matches);
if (isset($matches[0]) === true && $matches[0] === $words[0]) {
$error = 'Inline comments must start with a capital letter';
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'NotCapital');
if ($fix === true) {
$newComment = preg_replace("/$words[0]/", ucfirst($words[0]), $tokens[$stackPtr]['content'], 1);
$phpcsFile->fixer->replaceToken($stackPtr, $newComment);
}
}
}
// Only check the end of comment character if the start of the comment
// is a letter, indicating that the comment is just standard text.
// Also, when the comment starts with cspell: don't check the end of the
// comment.
if (preg_match('/^\p{L}/u', $commentText) === 1
&& preg_match('/(cspell|spell\-checker|spellchecker):/i', $commentText) === 0
) {
$commentCloser = $commentText[(strlen($commentText) - 1)];
$acceptedClosers = [
'full-stops' => '.',
'exclamation marks' => '!',
'question marks' => '?',
'colons' => ':',
'or closing parentheses' => ')',
];
// Allow special last words like URLs or function references
// without punctuation.
$lastWord = $words[(count($words) - 1)];
$matches = [];
preg_match('/https?:\/\/.+/', $lastWord, $matches);
$isUrl = isset($matches[0]) === true;
preg_match('/[$a-zA-Z_]+\([$a-zA-Z_]*\)/', $lastWord, $matches);
$isFunction = isset($matches[0]) === true;
// Also allow closing tags like @endlink or @endcode.
$isEndTag = $lastWord[0] === '@';
if (in_array($commentCloser, $acceptedClosers, true) === false
&& $isUrl === false && $isFunction === false && $isEndTag === false
) {
$error = 'Inline comments must end in %s';
$ender = '';
foreach ($acceptedClosers as $closerName => $symbol) {
$ender .= ' '.$closerName.',';
}
$ender = trim($ender, ' ,');
$data = [$ender];
$fix = $phpcsFile->addFixableError($error, $lastCommentToken, 'InvalidEndChar', $data);
if ($fix === true) {
$newContent = preg_replace('/(\s+)$/', '.$1', $tokens[$lastCommentToken]['content']);
$phpcsFile->fixer->replaceToken($lastCommentToken, $newContent);
}
}
}//end if
// Finally, the line below the last comment cannot be empty if this inline
// comment is on a line by itself.
if ($tokens[$previousContent]['line'] < $tokens[$stackPtr]['line']) {
$next = $phpcsFile->findNext(T_WHITESPACE, ($lastCommentToken + 1), null, true);
if ($next === false) {
// Ignore if the comment is the last non-whitespace token in a file.
return ($lastCommentToken + 1);
}
if ($tokens[$next]['code'] === T_DOC_COMMENT_OPEN_TAG) {
// If this inline comment is followed by a docblock,
// ignore spacing as docblock/function etc spacing rules
// are likely to conflict with our rules.
return ($lastCommentToken + 1);
}
$errorCode = 'SpacingAfter';
if (isset($tokens[$stackPtr]['conditions']) === true) {
$conditions = $tokens[$stackPtr]['conditions'];
$type = end($conditions);
$conditionPtr = key($conditions);
if (($type === T_FUNCTION || $type === T_CLOSURE)
&& $tokens[$conditionPtr]['scope_closer'] === $next
) {
$errorCode = 'SpacingAfterAtFunctionEnd';
}
}
for ($i = ($lastCommentToken + 1); $i < $phpcsFile->numTokens; $i++) {
if ($tokens[$i]['line'] === ($tokens[$lastCommentToken]['line'] + 1)) {
if ($tokens[$i]['code'] !== T_WHITESPACE) {
return ($lastCommentToken + 1);
}
} else if ($tokens[$i]['line'] > ($tokens[$lastCommentToken]['line'] + 1)) {
break;
}
}
$error = 'There must be no blank line following an inline comment';
$fix = $phpcsFile->addFixableWarning($error, $lastCommentToken, $errorCode);
if ($fix === true) {
$phpcsFile->fixer->beginChangeset();
for ($i = ($lastCommentToken + 1); $i < $next; $i++) {
if ($tokens[$i]['line'] === $tokens[$next]['line']) {
break;
}
$phpcsFile->fixer->replaceToken($i, '');
}
$phpcsFile->fixer->endChangeset();
}
}//end if
return ($lastCommentToken + 1);
}//end process()
/**
* Determines if a comment line is part of an @code/@endcode example.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return boolean Returns true if the comment line is within a @code block,
* false otherwise.
*/
protected function isInCodeExample(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if ($tokens[$stackPtr]['content'] === '// @code'.$phpcsFile->eolChar) {
return true;
}
$prevComment = $stackPtr;
$lastComment = $stackPtr;
while (($prevComment = $phpcsFile->findPrevious([T_COMMENT], ($lastComment - 1), null, false)) !== false) {
if ($tokens[$prevComment]['line'] !== ($tokens[$lastComment]['line'] - 1)) {
return false;
}
if ($tokens[$prevComment]['content'] === '// @code'.$phpcsFile->eolChar) {
return true;
}
if ($tokens[$prevComment]['content'] === '// @endcode'.$phpcsFile->eolChar) {
return false;
}
$lastComment = $prevComment;
}
return false;
}//end isInCodeExample()
}//end class

View File

@@ -0,0 +1,152 @@
<?php
/**
* \Drupal\Sniffs\Commenting\InlineVariableCommentSniff
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
/**
* Checks for the correct usage of inline variable type declarations.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class InlineVariableCommentSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [
T_COMMENT,
T_DOC_COMMENT_TAG,
];
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$ignore = [
T_CLASS,
T_INTERFACE,
T_TRAIT,
T_ENUM,
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,
T_VAR,
];
// If this is a function/class/interface doc block comment, skip it.
$nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
if (in_array($tokens[$nextToken]['code'], $ignore, true) === true) {
return;
}
if ($tokens[$stackPtr]['code'] === T_COMMENT) {
if (strpos($tokens[$stackPtr]['content'], '@var') !== false) {
$warning = 'Inline @var declarations should use the /** */ delimiters';
if (strpos($tokens[$stackPtr]['content'], '#') === 0 || strpos($tokens[$stackPtr]['content'], '//') === 0) {
// If this comment contains '*/' then the developer is mixing
// inline comment styles. This could be commented out code,
// so leave this line alone completely.
if (strpos($tokens[$stackPtr]['content'], '*/') !== false) {
return;
}
if ($phpcsFile->addFixableWarning($warning, $stackPtr, 'VarInline') === true) {
// Hashtag and slash based comments contain a trailing
// new line.
$varContent = rtrim($tokens[$stackPtr]['content']);
// Remove all leading hashtags and slashes.
$varContent = ltrim($varContent, '/# ');
$phpcsFile->fixer->replaceToken($stackPtr, ('/** '.$varContent." */\n"));
}
} else {
if ($phpcsFile->addFixableWarning($warning, $stackPtr, 'VarInline') === true) {
$phpcsFile->fixer->replaceToken($stackPtr, substr_replace($tokens[$stackPtr]['content'], '/**', 0, 2));
}
}//end if
}//end if
return;
}//end if
// Skip if it's not a variable declaration.
if ($tokens[$stackPtr]['content'] !== '@var') {
return;
}
// Get the content of the @var tag to determine the order.
$varContent = '';
$varContentPtr = $phpcsFile->findNext(T_DOC_COMMENT_STRING, ($stackPtr + 1));
if ($varContentPtr !== false) {
$varContent = $tokens[$varContentPtr]['content'];
}
if (strpos($varContent, '$') === 0) {
$warning = 'The variable name should be defined after the type';
$parts = explode(' ', $varContent, 3);
if (isset($parts[1]) === true) {
if ($phpcsFile->addFixableWarning($warning, $varContentPtr, 'VarInlineOrder') === true) {
// Switch type and variable name.
$replace = [
$parts[1],
$parts[0],
];
if (isset($parts[2]) === true) {
$replace[] = $parts[2];
}
$phpcsFile->fixer->replaceToken($varContentPtr, implode(' ', $replace));
}
} else {
$phpcsFile->addWarning($warning, $varContentPtr, 'VarInlineOrder');
}
}//end if
}//end process()
}//end class

View File

@@ -0,0 +1,102 @@
<?php
/**
* \Drupal\Sniffs\Commenting\PostStatementCommentSniff.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
/**
* Largely copied from
* \PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting\PostStatementCommentSniff
* but we want the fixer to move the comment to the previous line.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class PostStatementCommentSniff implements Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
return [T_COMMENT];
}//end register()
/**
* Processes this sniff, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token in the
* stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
if (substr($tokens[$stackPtr]['content'], 0, 2) !== '//') {
return;
}
$commentLine = $tokens[$stackPtr]['line'];
$lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
if ($tokens[$lastContent]['line'] !== $commentLine) {
return;
}
if ($tokens[$lastContent]['code'] === T_CLOSE_CURLY_BRACKET) {
return;
}
// Special case for JS files.
if ($tokens[$lastContent]['code'] === T_COMMA
|| $tokens[$lastContent]['code'] === T_SEMICOLON
) {
$lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($lastContent - 1), null, true);
if ($tokens[$lastContent]['code'] === T_CLOSE_CURLY_BRACKET) {
return;
}
}
$error = 'Comments may not appear after statements';
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'Found');
if ($fix === true) {
if ($tokens[$lastContent]['code'] === T_OPEN_TAG) {
$phpcsFile->fixer->addNewlineBefore($stackPtr);
return;
}
$lineStart = $stackPtr;
while ($tokens[$lineStart]['line'] === $tokens[$stackPtr]['line']
&& $tokens[$lineStart]['code'] !== T_OPEN_TAG
) {
$lineStart--;
}
$phpcsFile->fixer->beginChangeset();
$phpcsFile->fixer->addContent($lineStart, $tokens[$stackPtr]['content']);
$phpcsFile->fixer->replaceToken($stackPtr, $phpcsFile->eolChar);
$phpcsFile->fixer->endChangeset();
}
}//end process()
}//end class

View File

@@ -0,0 +1,191 @@
<?php
/**
* Parses and verifies comment language.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Config;
/**
* Parses and verifies that comments use the correct @todo format.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class TodoCommentSniff implements Sniff
{
/**
* Show debug output for this sniff.
*
* Use phpcs --runtime-set todo_debug true
*
* @var boolean
*/
private $debug = false;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array<int|string>
*/
public function register()
{
if (defined('PHP_CODESNIFFER_IN_TESTS') === true) {
$this->debug = false;
}
return [
T_COMMENT,
T_DOC_COMMENT_TAG,
T_DOC_COMMENT_STRING,
];
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$debug = Config::getConfigData('todo_debug');
if ($debug !== null) {
$this->debug = (bool) $debug;
}
$tokens = $phpcsFile->getTokens();
if ($this->debug === true) {
echo "\n------\n\$tokens[$stackPtr] = ".print_r($tokens[$stackPtr], true).PHP_EOL;
echo 'code = '.$tokens[$stackPtr]['code'].', type = '.$tokens[$stackPtr]['type']."\n";
}
// Standard comments and multi-line comments where the "@" is missing so
// it does not register as a T_DOC_COMMENT_TAG.
if ($tokens[$stackPtr]['code'] === T_COMMENT || $tokens[$stackPtr]['code'] === T_DOC_COMMENT_STRING) {
$comment = $tokens[$stackPtr]['content'];
if ($this->debug === true) {
echo "Getting \$comment from \$tokens[$stackPtr]['content']\n";
}
$this->checkTodoFormat($phpcsFile, $stackPtr, $comment, $tokens);
} else if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_TAG) {
// Document comment tag (i.e. comments that begin with "@").
// Determine if this is related at all and build the full comment line
// from the various segments that the line is parsed into.
$expression = '/^@to/i';
$comment = $tokens[$stackPtr]['content'];
if ((bool) preg_match($expression, $comment) === true) {
if ($this->debug === true) {
echo "Attempting to build comment\n";
}
$index = ($stackPtr + 1);
while ($tokens[$index]['line'] === $tokens[$stackPtr]['line']) {
$comment .= $tokens[$index]['content'];
$index++;
}
if ($this->debug === true) {
echo "Result comment = $comment\n";
}
$this->checkTodoFormat($phpcsFile, $stackPtr, $comment, $tokens);
}//end if
}//end if
}//end process()
/**
* Checks a comment string for the correct syntax.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
* @param string $comment The comment text.
* @param array<int, mixed> $tokens The token data.
*
* @return void
*/
private function checkTodoFormat(File $phpcsFile, int $stackPtr, string $comment, array $tokens)
{
if ($this->debug === true) {
echo "Checking \$comment = '$comment'\n";
}
$expression = '/(?x) # Set free-space mode to allow this commenting.
^(\/|\s)* # At the start optionally match any forward slashes and spaces
(?i) # set case-insensitive mode.
(?=( # Start a positive non-consuming look-ahead to find all possible todos
@+to(-|\s|)+do # if one or more @ allow spaces and - between the to and do.
\h*(-|:)* # Also match the trailing "-" or ":" so they can be replaced.
| # or
to(-)*do # If no @ then only accept todo or to-do or to--do, etc, no spaces.
(\s-|:)* # Also match the trailing "-" or ":" so they can be replaced.
))
(?-i) # Reset to case-sensitive
(?! # Start another non-consuming look-ahead, this time negative
@todo\s # It has to match lower-case @todo followed by one space
(?!-|:)\S # and then any non-space except "-" or ":".
)/m';
if ((bool) preg_match($expression, $comment, $matches) === true) {
if ($this->debug === true) {
echo "Failed regex - give message\n";
}
$commentTrimmed = trim($comment, " /\r\n");
if ($commentTrimmed === '@todo') {
// We can't fix a comment that doesn't have any text.
$phpcsFile->addWarning("'%s' should match the format '@todo Fix problem X here.'", $stackPtr, 'TodoFormat', [$commentTrimmed]);
$fix = false;
} else {
// Comments with description text are fixable.
$fix = $phpcsFile->addFixableWarning("'%s' should match the format '@todo Fix problem X here.'", $stackPtr, 'TodoFormat', [$commentTrimmed]);
}
if ($fix === true) {
if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_TAG) {
// Rewrite the comment past the token content to an empty
// string as part of it may be part of the match, but not in
// the token content. Then replace the token content with
// the fixed comment from the matched content.
$phpcsFile->fixer->beginChangeset();
$index = ($stackPtr + 1);
while ($tokens[$index]['line'] === $tokens[$stackPtr]['line']) {
$phpcsFile->fixer->replaceToken($index, '');
$index++;
}
$fixedTodo = str_replace($matches[2], '@todo ', $comment);
$phpcsFile->fixer->replaceToken($stackPtr, $fixedTodo);
$phpcsFile->fixer->endChangeset();
} else {
// The full comment line text is available here, so the
// replacement is fairly straightforward.
$fixedTodo = str_replace($matches[2], '@todo ', $tokens[$stackPtr]['content']);
$phpcsFile->fixer->replaceToken($stackPtr, $fixedTodo);
}//end if
}//end if
}//end if
}//end checkTodoFormat()
}//end class

View File

@@ -0,0 +1,229 @@
<?php
/**
* Parses and verifies class property doc comments.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
namespace Drupal\Sniffs\Commenting;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\AbstractVariableSniff;
/**
* Parses and verifies class property doc comments.
*
* Largely copied from
* \PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting\VariableCommentSniff.
*
* @category PHP
* @package PHP_CodeSniffer
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class VariableCommentSniff extends AbstractVariableSniff
{
/**
* Called to process class member vars.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token
* in the stack passed in $tokens.
*
* @return void
*/
public function processMemberVar(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$ignore = [
T_PUBLIC => T_PUBLIC,
T_PRIVATE => T_PRIVATE,
T_PROTECTED => T_PROTECTED,
T_VAR => T_VAR,
T_STATIC => T_STATIC,
T_READONLY => T_READONLY,
T_WHITESPACE => T_WHITESPACE,
T_STRING => T_STRING,
T_NS_SEPARATOR => T_NS_SEPARATOR,
T_NAMESPACE => T_NAMESPACE,
T_NULLABLE => T_NULLABLE,
T_TYPE_UNION => T_TYPE_UNION,
T_TYPE_INTERSECTION => T_TYPE_INTERSECTION,
T_NULL => T_NULL,
T_TRUE => T_TRUE,
T_FALSE => T_FALSE,
T_SELF => T_SELF,
T_PARENT => T_PARENT,
];
for ($commentEnd = ($stackPtr - 1); $commentEnd >= 0; $commentEnd--) {
if (isset($ignore[$tokens[$commentEnd]['code']]) === true) {
continue;
}
if ($tokens[$commentEnd]['code'] === T_ATTRIBUTE_END
&& isset($tokens[$commentEnd]['attribute_opener']) === true
) {
$commentEnd = $tokens[$commentEnd]['attribute_opener'];
continue;
}
break;
}
if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
&& $tokens[$commentEnd]['code'] !== T_COMMENT
) {
$phpcsFile->addError('Missing member variable doc comment', $stackPtr, 'Missing');
return;
}
if ($tokens[$commentEnd]['code'] === T_COMMENT) {
$fix = $phpcsFile->addFixableError('You must use "/**" style comments for a member variable comment', $stackPtr, 'WrongStyle');
if ($fix === true) {
// Convert the comment into a doc comment.
$phpcsFile->fixer->beginChangeset();
$comment = '';
for ($i = $commentEnd; $tokens[$i]['code'] === T_COMMENT; $i--) {
$comment = ' *'.ltrim($tokens[$i]['content'], '/* ').$comment;
$phpcsFile->fixer->replaceToken($i, '');
}
$phpcsFile->fixer->replaceToken($commentEnd, "/**\n".rtrim($comment, "*/\n")."\n */\n");
$phpcsFile->fixer->endChangeset();
}
return;
}//end if
$commentStart = $tokens[$commentEnd]['comment_opener'];
// Ignore variable comments that use inheritdoc, allow both variants.
$commentContent = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart));
if (strpos($commentContent, '{@inheritdoc}') !== false
|| strpos($commentContent, '{@inheritDoc}') !== false
) {
return;
}
$foundVar = null;
foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
if ($tokens[$tag]['content'] === '@var') {
if ($foundVar !== null) {
$error = 'Only one @var tag is allowed in a member variable comment';
$phpcsFile->addError($error, $tag, 'DuplicateVar');
} else {
$foundVar = $tag;
}
} else if ($tokens[$tag]['content'] === '@see') {
// Make sure the tag isn't empty.
$string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd);
if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) {
$error = 'Content missing for @see tag in member variable comment';
$phpcsFile->addError($error, $tag, 'EmptySees');
}
}//end if
}//end foreach
// The @var tag is the only one we require.
if ($foundVar === null) {
// If there's an inline type argument then you may omit the @var comment.
// Check if there's a type between the variable name and the comment end.
if ($phpcsFile->findPrevious([T_STRING], $stackPtr, $commentEnd) !== false) {
return;
}
$error = 'Missing @var tag in member variable comment';
$phpcsFile->addError($error, $commentEnd, 'MissingVar');
return;
}
$firstTag = $tokens[$commentStart]['comment_tags'][0];
if ($foundVar !== null && $tokens[$firstTag]['content'] !== '@var') {
$error = 'The @var tag must be the first tag in a member variable comment';
$phpcsFile->addError($error, $foundVar, 'VarOrder');
}
// Make sure the tag isn't empty and has the correct padding.
$string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $foundVar, $commentEnd);
if ($string === false || $tokens[$string]['line'] !== $tokens[$foundVar]['line']) {
$error = 'Content missing for @var tag in member variable comment';
$phpcsFile->addError($error, $foundVar, 'EmptyVar');
return;
}
$varType = $tokens[($foundVar + 2)]['content'];
// There may be multiple types separated by pipes.
$suggestedTypes = [];
foreach (explode('|', $varType) as $type) {
$suggestedTypes[] = FunctionCommentSniff::suggestType($type);
}
$suggestedType = implode('|', $suggestedTypes);
// Detect and auto-fix the common mistake that the variable name is
// appended to the type declaration.
$matches = [];
if (preg_match('/^([^\s]+)(\s+\$.+)$/', $varType, $matches) === 1) {
$error = 'Do not append variable name "%s" to the type declaration in a member variable comment';
$data = [
trim($matches[2]),
];
$fix = $phpcsFile->addFixableError($error, ($foundVar + 2), 'InlineVariableName', $data);
if ($fix === true) {
$phpcsFile->fixer->replaceToken(($foundVar + 2), $matches[1]);
}
} else if ($varType !== $suggestedType) {
$error = 'Expected "%s" but found "%s" for @var tag in member variable comment';
$data = [
$suggestedType,
$varType,
];
$fix = $phpcsFile->addFixableError($error, ($foundVar + 2), 'IncorrectVarType', $data);
if ($fix === true) {
$phpcsFile->fixer->replaceToken(($foundVar + 2), $suggestedType);
}
}//end if
}//end processMemberVar()
/**
* Called to process a normal variable.
*
* Not required for this sniff.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where this token was found.
* @param int $stackPtr The position where the double quoted
* string was found.
*
* @return void
*/
protected function processVariable(File $phpcsFile, $stackPtr)
{
}//end processVariable()
/**
* Called to process variables found in double quoted strings.
*
* Not required for this sniff.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where this token was found.
* @param int $stackPtr The position where the double quoted
* string was found.
*
* @return void
*/
protected function processVariableInString(File $phpcsFile, $stackPtr)
{
}//end processVariableInString()
}//end class