Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x
  2. FileHeaderSniff.php

class FileHeaderSniff

Hierarchy

  • class \PHP_CodeSniffer\Standards\PSR12\Sniffs\Files\FileHeaderSniff implements \PHP_CodeSniffer\Sniffs\Sniff

Expanded class hierarchy of FileHeaderSniff

File

vendor/squizlabs/php_codesniffer/src/Standards/PSR12/Sniffs/Files/FileHeaderSniff.php, line 16

Namespace

PHP_CodeSniffer\Standards\PSR12\Sniffs\Files
View source
class FileHeaderSniff 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 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.
     *
     * @return int|void
     */
    public function process(File $phpcsFile, $stackPtr) {
        $tokens = $phpcsFile->getTokens();
        $possibleHeaders = [];
        $searchFor = Tokens::$ooScopeTokens;
        $searchFor[T_OPEN_TAG] = T_OPEN_TAG;
        $openTag = $stackPtr;
        do {
            $headerLines = $this->getHeaderLines($phpcsFile, $openTag);
            if (empty($headerLines) === true && $openTag === $stackPtr) {
                // No content in the file.
                return;
            }
            $possibleHeaders[$openTag] = $headerLines;
            if (count($headerLines) > 1) {
                break;
            }
            $next = $phpcsFile->findNext($searchFor, $openTag + 1);
            if (isset(Tokens::$ooScopeTokens[$tokens[$next]['code']]) === true) {
                // Once we find an OO token, the file content has
                // definitely started.
                break;
            }
            $openTag = $next;
        } while ($openTag !== false);
        if ($openTag === false) {
            // We never found a proper file header.
            // If the file has multiple PHP open tags, we know
            // that it must be a mix of PHP and HTML (or similar)
            // so the header rules do not apply.
            if (count($possibleHeaders) > 1) {
                return $phpcsFile->numTokens;
            }
            // There is only one possible header.
            // If it is the first content in the file, it technically
            // serves as the file header, and the open tag needs to
            // have a newline after it. Otherwise, ignore it.
            if ($stackPtr > 0) {
                return $phpcsFile->numTokens;
            }
            $openTag = $stackPtr;
        }
        else {
            if (count($possibleHeaders) > 1) {
                // There are other PHP blocks before the file header.
                $error = 'The file header must be the first content in the file';
                $phpcsFile->addError($error, $openTag, 'HeaderPosition');
            }
            else {
                // The first possible header was the file header block,
                // so make sure it is the first content in the file.
                if ($openTag !== 0) {
                    // Allow for hashbang lines.
                    $hashbang = false;
                    if ($tokens[$openTag - 1]['code'] === T_INLINE_HTML) {
                        $content = trim($tokens[$openTag - 1]['content']);
                        if (substr($content, 0, 2) === '#!') {
                            $hashbang = true;
                        }
                    }
                    if ($hashbang === false) {
                        $error = 'The file header must be the first content in the file';
                        $phpcsFile->addError($error, $openTag, 'HeaderPosition');
                    }
                }
            }
        }
        
        //end if
        $this->processHeaderLines($phpcsFile, $possibleHeaders[$openTag]);
        return $phpcsFile->numTokens;
    }
    
    //end process()
    
    /**
     * Gather information about the statements inside a possible file header.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
     * @param int                         $stackPtr  The position of the current
     *                                               token in the stack.
     *
     * @return array
     */
    public function getHeaderLines(File $phpcsFile, $stackPtr) {
        $tokens = $phpcsFile->getTokens();
        $next = $phpcsFile->findNext(T_WHITESPACE, $stackPtr + 1, null, true);
        if ($next === false) {
            return [];
        }
        $headerLines = [];
        $headerLines[] = [
            'type' => 'tag',
            'start' => $stackPtr,
            'end' => $stackPtr,
        ];
        $foundDocblock = false;
        $commentOpeners = Tokens::$scopeOpeners;
        unset($commentOpeners[T_NAMESPACE]);
        unset($commentOpeners[T_DECLARE]);
        unset($commentOpeners[T_USE]);
        unset($commentOpeners[T_IF]);
        unset($commentOpeners[T_WHILE]);
        unset($commentOpeners[T_FOR]);
        unset($commentOpeners[T_FOREACH]);
        unset($commentOpeners[T_DO]);
        unset($commentOpeners[T_TRY]);
        do {
            switch ($tokens[$next]['code']) {
                case T_DOC_COMMENT_OPEN_TAG:
                    if ($foundDocblock === true) {
                        // Found a second docblock, so start of code.
                        break 2;
                    }
                    // Make sure this is not a code-level docblock.
                    $end = $tokens[$next]['comment_closer'];
                    for ($docToken = $end + 1; $docToken < $phpcsFile->numTokens; $docToken++) {
                        if (isset(Tokens::$emptyTokens[$tokens[$docToken]['code']]) === true) {
                            continue;
                        }
                        if ($tokens[$docToken]['code'] === T_ATTRIBUTE && isset($tokens[$docToken]['attribute_closer']) === true) {
                            $docToken = $tokens[$docToken]['attribute_closer'];
                            continue;
                        }
                        break;
                    }
                    if ($docToken === $phpcsFile->numTokens) {
                        $docToken--;
                    }
                    if (isset($commentOpeners[$tokens[$docToken]['code']]) === false && isset(Tokens::$methodPrefixes[$tokens[$docToken]['code']]) === false && $tokens[$docToken]['code'] !== T_READONLY) {
                        // Check for an @var annotation.
                        $annotation = false;
                        for ($i = $next; $i < $end; $i++) {
                            if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG && strtolower($tokens[$i]['content']) === '@var') {
                                $annotation = true;
                                break;
                            }
                        }
                        if ($annotation === false) {
                            $foundDocblock = true;
                            $headerLines[] = [
                                'type' => 'docblock',
                                'start' => $next,
                                'end' => $end,
                            ];
                        }
                    }
                    
                    //end if
                    $next = $end;
                    break;
                case T_DECLARE:
                case T_NAMESPACE:
                    if (isset($tokens[$next]['scope_opener']) === true) {
                        // If this statement is using bracketed syntax, it doesn't
                        // apply to the entire files and so is not part of header.
                        // The header has now ended and the main code block begins.
                        break 2;
                    }
                    $end = $phpcsFile->findEndOfStatement($next);
                    $headerLines[] = [
                        'type' => substr(strtolower($tokens[$next]['type']), 2),
                        'start' => $next,
                        'end' => $end,
                    ];
                    $next = $end;
                    break;
                case T_USE:
                    $type = 'use';
                    $useType = $phpcsFile->findNext(Tokens::$emptyTokens, $next + 1, null, true);
                    if ($useType !== false && $tokens[$useType]['code'] === T_STRING) {
                        $content = strtolower($tokens[$useType]['content']);
                        if ($content === 'function' || $content === 'const') {
                            $type .= ' ' . $content;
                        }
                    }
                    $end = $phpcsFile->findEndOfStatement($next);
                    $headerLines[] = [
                        'type' => $type,
                        'start' => $next,
                        'end' => $end,
                    ];
                    $next = $end;
                    break;
                default:
                    // Skip comments as PSR-12 doesn't say if these are allowed or not.
                    if (isset(Tokens::$commentTokens[$tokens[$next]['code']]) === true) {
                        $next = $phpcsFile->findNext(Tokens::$commentTokens, $next + 1, null, true);
                        if ($next === false) {
                            // We reached the end of the file.
                            break 2;
                        }
                        $next--;
                        break;
                    }
                    // We found the start of the main code block.
                    break 2;
            }
            
            //end switch
            $next = $phpcsFile->findNext(T_WHITESPACE, $next + 1, null, true);
        } while ($next !== false);
        return $headerLines;
    }
    
    //end getHeaderLines()
    
    /**
     * Check the spacing and grouping of the statements inside each header block.
     *
     * @param \PHP_CodeSniffer\Files\File $phpcsFile   The file being scanned.
     * @param array                       $headerLines Header information, as sourced
     *                                                 from getHeaderLines().
     *
     * @return void
     */
    public function processHeaderLines(File $phpcsFile, $headerLines) {
        $tokens = $phpcsFile->getTokens();
        $found = [];
        foreach ($headerLines as $i => $line) {
            if (isset($headerLines[$i + 1]) === false || $headerLines[$i + 1]['type'] !== $line['type']) {
                // We're at the end of the current header block.
                // Make sure there is a single blank line after
                // this block.
                $next = $phpcsFile->findNext(T_WHITESPACE, $line['end'] + 1, null, true);
                if ($next !== false && $tokens[$next]['line'] !== $tokens[$line['end']]['line'] + 2) {
                    $error = 'Header blocks must be separated by a single blank line';
                    $fix = $phpcsFile->addFixableError($error, $line['end'], 'SpacingAfterBlock');
                    if ($fix === true) {
                        if ($tokens[$next]['line'] === $tokens[$line['end']]['line']) {
                            $phpcsFile->fixer
                                ->addContentBefore($next, $phpcsFile->eolChar . $phpcsFile->eolChar);
                        }
                        else {
                            if ($tokens[$next]['line'] === $tokens[$line['end']]['line'] + 1) {
                                $phpcsFile->fixer
                                    ->addNewline($line['end']);
                            }
                            else {
                                $phpcsFile->fixer
                                    ->beginChangeset();
                                for ($i = $line['end'] + 1; $i < $next; $i++) {
                                    if ($tokens[$i]['line'] === $tokens[$line['end']]['line'] + 2) {
                                        break;
                                    }
                                    $phpcsFile->fixer
                                        ->replaceToken($i, '');
                                }
                                $phpcsFile->fixer
                                    ->endChangeset();
                            }
                        }
                    }
                    
                    //end if
                }
                
                //end if
                // Make sure we haven't seen this next block before.
                if (isset($headerLines[$i + 1]) === true && isset($found[$headerLines[$i + 1]['type']]) === true) {
                    $error = 'Similar statements must be grouped together inside header blocks; ';
                    $error .= 'the first "%s" statement was found on line %s';
                    $data = [
                        $headerLines[$i + 1]['type'],
                        $tokens[$found[$headerLines[$i + 1]['type']]['start']]['line'],
                    ];
                    $phpcsFile->addError($error, $headerLines[$i + 1]['start'], 'IncorrectGrouping', $data);
                }
            }
            else {
                if ($headerLines[$i + 1]['type'] === $line['type']) {
                    // Still in the same block, so make sure there is no
                    // blank line after this statement.
                    $next = $phpcsFile->findNext(T_WHITESPACE, $line['end'] + 1, null, true);
                    if ($tokens[$next]['line'] > $tokens[$line['end']]['line'] + 1) {
                        $error = 'Header blocks must not contain blank lines';
                        $fix = $phpcsFile->addFixableError($error, $line['end'], 'SpacingInsideBlock');
                        if ($fix === true) {
                            $phpcsFile->fixer
                                ->beginChangeset();
                            for ($i = $line['end'] + 1; $i < $next; $i++) {
                                if ($tokens[$i]['line'] === $tokens[$line['end']]['line']) {
                                    continue;
                                }
                                if ($tokens[$i]['line'] === $tokens[$next]['line']) {
                                    break;
                                }
                                $phpcsFile->fixer
                                    ->replaceToken($i, '');
                            }
                            $phpcsFile->fixer
                                ->endChangeset();
                        }
                    }
                }
            }
            
            //end if
            if (isset($found[$line['type']]) === false) {
                $found[$line['type']] = $line;
            }
        }
        
        //end foreach
        
        /*
            Next, check that the order of the header blocks
            is correct:
                Opening php tag.
                File-level docblock.
                One or more declare statements.
                The namespace declaration of the file.
                One or more class-based use import statements.
                One or more function-based use import statements.
                One or more constant-based use import statements.
        */
        $blockOrder = [
            'tag' => 'opening PHP tag',
            'docblock' => 'file-level docblock',
            'declare' => 'declare statements',
            'namespace' => 'namespace declaration',
            'use' => 'class-based use imports',
            'use function' => 'function-based use imports',
            'use const' => 'constant-based use imports',
        ];
        foreach (array_keys($found) as $type) {
            if ($type === 'tag') {
                // The opening tag is always in the correct spot.
                continue;
            }
            do {
                $orderedType = next($blockOrder);
            } while ($orderedType !== false && key($blockOrder) !== $type);
            if ($orderedType === false) {
                // We didn't find the block type in the rest of the
                // ordered array, so it is out of place.
                // Error and reset the array to the correct position
                // so we can check the next block.
                reset($blockOrder);
                $prevValidType = 'tag';
                do {
                    $orderedType = next($blockOrder);
                    if (isset($found[key($blockOrder)]) === true && key($blockOrder) !== $type) {
                        $prevValidType = key($blockOrder);
                    }
                } while ($orderedType !== false && key($blockOrder) !== $type);
                $error = 'The %s must follow the %s in the file header';
                $data = [
                    $blockOrder[$type],
                    $blockOrder[$prevValidType],
                ];
                $phpcsFile->addError($error, $found[$type]['start'], 'IncorrectOrder', $data);
            }
            
            //end if
        }
        
        //end foreach
    }
    
    //end processHeaderLines()

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title
FileHeaderSniff::getHeaderLines public function Gather information about the statements inside a possible file header.
FileHeaderSniff::process public function Processes this sniff when one of its tokens is encountered. Overrides Sniff::process
FileHeaderSniff::processHeaderLines public function Check the spacing and grouping of the statements inside each header block.
FileHeaderSniff::register public function Returns an array of tokens this test wants to listen for. Overrides Sniff::register
RSS feed
Powered by Drupal