phpphpcs

phpcs: How to enforce class inheritance?


I want to make sure all classes in some dir (src/Controller/CP) extend some other class AbstractCPController.

So that

class SupportTicketTagController extends AbstractController

would show PHPCS error, and

class SupportTicketTagController extends AbstractCPController

would be fine.

Is that possible with PHPCS?


Solution

  • I ended up creating my own Sniff. Its far from perfect but may serve as a base for somebody in the future:

    <?php
    
    declare(strict_types=1);
    
    namespace MyCodingStandard\Sniffs\Classes;
    
    use PHP_CodeSniffer\Fixer;
    use PHP_CodeSniffer\Sniffs\Sniff;
    use PHP_CodeSniffer\Files\File;
    
    /**
     * Sniff for catching classes not marked as abstract or final
     */
    final class ClassInheritanceSniff implements Sniff
    {
        private ?Fixer $fixer;
    
        private $position;
    
        public function register(): array
        {
            return [T_CLASS];
        }
    
        public function process(File $file, $position): void
        {
            $this->fixer = $file->fixer;
            $this->position = $position;
    
            $tokens = $file->getTokens();
    
            $className = $tokens[$file->findNext(T_STRING, $position + 1)]['content'];
    
            if ($className === 'AbstractCPController') {
                return;
            }
    
            $curlyOpenLine = $file->findNext(T_OPEN_CURLY_BRACKET, $position + 1);
    
            $extendsLine = $file->findNext(T_EXTENDS, $position + 1, $curlyOpenLine - 1);
            $implementsLine = $file->findNext(T_IMPLEMENTS, $position + 1, $curlyOpenLine - 1);
    
            $extendedClasses = [];
            $implementedInterfaces = [];
    
            if ($extendsLine) {
                $extendsLineMax = $curlyOpenLine - 1;
    
                if ($implementsLine > $extendsLine) {
                    $extendsLineMax = $implementsLine - 1;
                }
    
                $lastClassName = '';
    
                for ($i = $extendsLine + 1; $i < $extendsLineMax; $i++) {
                    if ($tokens[$i]['code'] == T_STRING || $tokens[$i]['code'] == T_NS_SEPARATOR) {
                        $lastClassName .= $tokens[$i]['content'];
                    } else {
                        if ($lastClassName) {
                            $extendedClasses[] = $lastClassName;
                            $lastClassName = '';
                        }
                    }
                }
    
                if ($lastClassName) {
                    $extendedClasses[] = $lastClassName;
                }
            }
    
            if ($implementsLine) {
                $implementsLineMax = $curlyOpenLine - 1;
    
                if ($extendsLine > $implementsLine) {
                    $implementsLineMax = $extendsLine - 1;
                }
    
                $lastClassName = '';
    
                for ($i = $implementsLine + 1; $i < $implementsLineMax; $i++) {
                    if ($tokens[$i]['code'] == T_STRING || $tokens[$i]['code'] == T_NS_SEPARATOR) {
                        $lastClassName .= $tokens[$i]['content'];
                    } else {
                        if ($lastClassName) {
                            $implementedInterfaces[] = $lastClassName;
                            $lastClassName = '';
                        }
                    }
                }
    
                if ($lastClassName) {
                    $implementedInterfaces[] = $lastClassName;
                }
            }
    
            foreach ($extendedClasses as $class) {
                if (str_ends_with($class, 'AbstractCPController')) {
                    return;
                }
            }
    
            $file->addError(
                'All CP Controller classes should extend from AbstractCPController',
                $position - 1,
                self::class
            );
        }
    }