diff --git a/BigBite/Docs/Classes/StringableStandard.xml b/BigBite/Docs/Classes/StringableStandard.xml new file mode 100644 index 0000000..dc46959 --- /dev/null +++ b/BigBite/Docs/Classes/StringableStandard.xml @@ -0,0 +1,27 @@ + + + + + + + implements Stringable { + public function __toString(): string { + return __CLASS__; + } +} + ]]> + + + { + public function __toString(): string { + return __CLASS__; + } +} + ]]> + + + diff --git a/BigBite/Sniffs/Classes/StringableSniff.php b/BigBite/Sniffs/Classes/StringableSniff.php new file mode 100644 index 0000000..8888732 --- /dev/null +++ b/BigBite/Sniffs/Classes/StringableSniff.php @@ -0,0 +1,107 @@ + + */ + public $supportedTokenizers = array( + 'PHP', + ); + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() { + if ( version_compare( phpversion(), '8.0.0', '<' ) ) { + return array(); + } + + return array( \T_FUNCTION ); + } + + /** + * 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(); + $fnName = $phpcsFile->findNext( T_STRING, $stackPtr, ( $stackPtr + 5 ), false, null, true ); + + if ( false === $fnName || '__toString' !== $tokens[ $fnName ]['content'] ) { + return; + } + + $classDecl = $phpcsFile->findPrevious( T_CLASS, $stackPtr, 0 ); + + // this __toString function isn't a class member? + if ( false === $classDecl ) { + return; + } + + $interfaces = ObjectDeclarations::findImplementedInterfaceNames( $phpcsFile, $classDecl ); + + // we're good - class containing __toString implements the interface. + if ( is_array( $interfaces ) && in_array( 'Stringable', $interfaces, true ) ) { + return; + } + + $message = 'Classes that declare "__toString" should implement the Stringable interface.'; + $doWeFix = $phpcsFile->addFixableError( $message, $classDecl, 'NotImplemented', array(), 0 ); + + if ( true !== $doWeFix ) { + return; + } + + $openingCurly = $tokens[ $classDecl ]['scope_opener']; + + if ( $tokens[ $openingCurly ]['line'] === $tokens[ $classDecl ]['line'] ) { + $prevToken = $phpcsFile->findPrevious( T_STRING, $openingCurly, $classDecl ); + + if ( false === $prevToken ) { + return; + } + + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->addContent( $prevToken, ' implements Stringable' ); + $phpcsFile->fixer->endChangeset(); + + return; + } + + $endOfLine = $phpcsFile->findPrevious( T_WHITESPACE, $openingCurly, $classDecl ); + + if ( false === $endOfLine ) { + return; + } + + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->addContentBefore( $endOfLine, ' implements Stringable' ); + $phpcsFile->fixer->endChangeset(); + } +} diff --git a/BigBite/Tests/Classes/StringableUnitTest.1.inc b/BigBite/Tests/Classes/StringableUnitTest.1.inc new file mode 100644 index 0000000..b15eae2 --- /dev/null +++ b/BigBite/Tests/Classes/StringableUnitTest.1.inc @@ -0,0 +1,29 @@ + + */ + public function getErrorList( $testFile = '' ) { + if ( version_compare( phpversion(), '8.0.0', '<' ) ) { + return array(); + } + + switch ( $testFile ) { + case 'StringableUnitTest.1.inc': + return array( + 12 => 1, + 18 => 1, + 24 => 1, + ); + default: + return array(); + } + } + + /** + * Returns the lines where warnings should occur. + * + * The key of the array should represent the line number and the value + * should represent the number of warnings that should occur on that line. + * + * @param string $testFile The name of the file being tested. + * + * @return array + */ + public function getWarningList( $testFile = '' ) { + return array(); + } +}