diff --git a/monorepo/HydeStan/HydeStan.php b/monorepo/HydeStan/HydeStan.php index 570f5fea072..05aa7eafe55 100644 --- a/monorepo/HydeStan/HydeStan.php +++ b/monorepo/HydeStan/HydeStan.php @@ -4,12 +4,29 @@ use Desilva\Console\Console; +require_once __DIR__.'/includes/contracts.php'; +require_once __DIR__.'/includes/helpers.php'; + /** - * @internal + * @internal Custom static analysis tool for the HydePHP Development Monorepo. */ final class HydeStan { - const VERSION = '0.0.0-dev'; + private const FILE_ANALYSERS = [ + NoFixMeAnalyser::class, + UnImportedFunctionAnalyser::class, + ]; + + private const TEST_FILE_ANALYSERS = [ + NoFixMeAnalyser::class, + NoUsingAssertEqualsForScalarTypesTestAnalyser::class, + ]; + + private const LINE_ANALYSERS = [ + NoTestReferenceAnalyser::class, + NoHtmlExtensionInHydePHPLinksAnalyser::class, + NoExtraWhitespaceInCompressedPhpDocAnalyser::class, + ]; private array $files; private array $testFiles; @@ -31,7 +48,7 @@ public function __construct(private readonly bool $debug = false) $this->console = new Console(); - $this->console->info(sprintf('HydeStan v%s is running!', self::VERSION)); + $this->console->info('HydeStan is running!'); $this->console->newline(); } @@ -105,64 +122,38 @@ public function addErrors(array $errors): void private function getFiles(): array { - $files = []; - - $directory = new RecursiveDirectoryIterator(BASE_PATH.'/src'); - $iterator = new RecursiveIteratorIterator($directory); - $regex = new RegexIterator($iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH); - - foreach ($regex as $file) { - $files[] = substr($file[0], strlen(BASE_PATH) + 1); - } - - return $files; + return recursiveFileFinder('src'); } private function getTestFiles(): array { - $files = []; - - $directory = new RecursiveDirectoryIterator(BASE_PATH.'/tests'); - $iterator = new RecursiveIteratorIterator($directory); - $regex = new RegexIterator($iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH); - - foreach ($regex as $file) { - $files[] = substr($file[0], strlen(BASE_PATH) + 1); - } - - return $files; + return recursiveFileFinder('tests'); } private function analyseFile(string $file, string $contents): void { - $fileAnalysers = [ - new NoFixMeAnalyser($file, $contents), - new UnImportedFunctionAnalyser($file, $contents), - ]; + foreach (self::FILE_ANALYSERS as $fileAnalyserClass) { + $fileAnalyser = new $fileAnalyserClass($file, $contents); - foreach ($fileAnalysers as $analyser) { if ($this->debug) { - $this->console->debugComment('Running '.$analyser::class); + $this->console->debugComment('Running '.$fileAnalyser::class); } - $analyser->run($file, $contents); + $fileAnalyser->run($file, $contents); AnalysisStatisticsContainer::countedLines(substr_count($contents, "\n")); foreach (explode("\n", $contents) as $lineNumber => $line) { - $lineAnalysers = [ - new NoTestReferenceAnalyser($file, $lineNumber, $line), - ]; - - foreach ($lineAnalysers as $analyser) { + foreach (self::LINE_ANALYSERS as $lineAnalyserClass) { + $lineAnalyser = new $lineAnalyserClass($file, $lineNumber, $line); AnalysisStatisticsContainer::countedLine(); - $analyser->run($file, $lineNumber, $line); + $lineAnalyser->run($file, $lineNumber, $line); $this->aggregateLines++; } } } $this->scannedLines += substr_count($contents, "\n"); - $this->aggregateLines += (substr_count($contents, "\n") * count($fileAnalysers)); + $this->aggregateLines += (substr_count($contents, "\n") * count(self::FILE_ANALYSERS)); } private function getFileContents(string $file): string @@ -197,58 +188,23 @@ protected function runTestStan(): void private function analyseTestFile(string $file, string $contents): void { - $fileAnalysers = [ - new NoFixMeAnalyser($file, $contents), - new NoUsingAssertEqualsForScalarTypesTestAnalyser($file, $contents), - ]; + foreach (self::TEST_FILE_ANALYSERS as $fileAnalyserClass) { + $fileAnalyser = new $fileAnalyserClass($file, $contents); - foreach ($fileAnalysers as $analyser) { if ($this->debug) { - $this->console->debugComment('Running '.$analyser::class); + $this->console->debugComment('Running '.$fileAnalyser::class); } - $analyser->run($file, $contents); + $fileAnalyser->run($file, $contents); AnalysisStatisticsContainer::countedLines(substr_count($contents, "\n")); foreach (explode("\n", $contents) as $lineNumber => $line) { - $lineAnalysers = [ - // - ]; - - foreach ($lineAnalysers as $analyser) { - AnalysisStatisticsContainer::countedLine(); - $analyser->run($file, $lineNumber, $line); - $this->aggregateLines++; - } + // No line analysers defined for test files in the original code } } $this->scannedLines += substr_count($contents, "\n"); - $this->aggregateLines += (substr_count($contents, "\n") * count($fileAnalysers)); - } -} - -abstract class Analyser -{ - protected function fail(string $error): void - { - HydeStan::getInstance()->addError($error); - } -} - -abstract class FileAnalyser extends Analyser implements FileAnalyserContract -{ - public function __construct(protected string $file, protected string $contents) - { - // - } -} - -abstract class LineAnalyser extends Analyser implements LineAnalyserContract -{ - public function __construct(protected string $file, protected int $lineNumber, protected string $line) - { - // + $this->aggregateLines += (substr_count($contents, "\n") * count(self::TEST_FILE_ANALYSERS)); } } @@ -281,6 +237,40 @@ public function run(string $file, string $contents): void } } +class NoHtmlExtensionInHydePHPLinksAnalyser extends LineAnalyser +{ + public function run(string $file, int $lineNumber, string $line): void + { + AnalysisStatisticsContainer::analysedExpressions(1); + + if (str_contains($line, 'https://hydephp.com/') && str_contains($line, '.html')) { + AnalysisStatisticsContainer::analysedExpressions(1); + + $this->fail(sprintf('HTML extension used in URL at %s', + fileLink(BASE_PATH.'/packages/framework/'.$file, $lineNumber + 1) + )); + + HydeStan::addActionsMessage('warning', $file, $lineNumber + 1, 'HydeStan: NoHtmlExtensionError', 'URL contains .html extension. Consider removing it.'); + } + } +} + +class NoExtraWhitespaceInCompressedPhpDocAnalyser extends LineAnalyser +{ + public function run(string $file, int $lineNumber, string $line): void + { + AnalysisStatisticsContainer::analysedExpressions(1); + + if (str_contains($line, '/** ')) { + $this->fail(sprintf('Extra whitespace in compressed PHPDoc comment at %s', + fileLink(BASE_PATH.'/packages/framework/'.$file, $lineNumber + 1) + )); + + HydeStan::addActionsMessage('warning', $file, $lineNumber + 1, 'HydeStan: ExtraWhitespaceInPhpDocError', 'Extra whitespace found in compressed PHPDoc comment.'); + } + } +} + class NoUsingAssertEqualsForScalarTypesTestAnalyser extends FileAnalyser // Todo: Extend line analyser instead? Would allow for checking for more errors after the first error { public function run(string $file, string $contents): void @@ -370,7 +360,7 @@ public function run(string $file, string $contents): void foreach ($calledFunctions as $calledFunction) { AnalysisStatisticsContainer::analysedExpression(); if (! in_array($calledFunction, $functionImports)) { - echo("Found unimported function '$calledFunction' in ".realpath(__DIR__.'/../../packages/framework/'.$file))."\n"; + echo sprintf("Found unimported function '$calledFunction' in %s\n", realpath(__DIR__.'/../../packages/framework/'.$file)); } } } @@ -384,80 +374,11 @@ public function run(string $file, int $lineNumber, string $line): void if (str_starts_with($line, ' * @see') && str_ends_with($line, 'Test')) { AnalysisStatisticsContainer::analysedExpressions(1); - $this->fail(sprintf('Test class %s is referenced in %s:%s', trim(substr($line, 7)), - realpath(__DIR__.'/../../packages/framework/'.$file) ?: $file, $lineNumber + 1)); - } - } -} - -class AnalysisStatisticsContainer -{ - private static int $linesCounted = 0; - private static float $expressionsAnalysed = 0; - - public static function countedLine(): void - { - self::$linesCounted++; - } - - public static function countedLines(int $count): void - { - self::$linesCounted += $count; - } - - public static function analysedExpression(): void - { - self::$expressionsAnalysed++; - } - - public static function analysedExpressions(float $countOrEstimate): void - { - self::$expressionsAnalysed += $countOrEstimate; - } - - public static function getLinesCounted(): int - { - return self::$linesCounted; - } - - public static function getExpressionsAnalysed(): int - { - return (int) round(self::$expressionsAnalysed); - } -} - -interface FileAnalyserContract -{ - public function __construct(string $file, string $contents); - - public function run(string $file, string $contents): void; -} - -interface LineAnalyserContract -{ - public function __construct(string $file, int $lineNumber, string $line); - - public function run(string $file, int $lineNumber, string $line): void; -} - -function check_str_contains_any(array $searches, string $line): bool -{ - $strContainsAny = false; - foreach ($searches as $search) { - AnalysisStatisticsContainer::analysedExpression(); - if (str_contains($line, $search)) { - $strContainsAny = true; + $this->fail(sprintf('Test class %s is referenced in %s:%s', + trim(substr($line, 7)), + realpath(__DIR__.'/../../packages/framework/'.$file) ?: $file, + $lineNumber + 1 + )); } } - - return $strContainsAny; -} - -function fileLink(string $file, ?int $line = null): string -{ - $path = (realpath(__DIR__.'/../../packages/framework/'.$file) ?: $file).($line ? ':'.$line : ''); - $trim = strlen(getcwd()) + 2; - $path = substr($path, $trim); - - return str_replace('\\', '/', $path); } diff --git a/monorepo/HydeStan/README.md b/monorepo/HydeStan/README.md index 5d9b407be69..2e7d18da8ab 100644 --- a/monorepo/HydeStan/README.md +++ b/monorepo/HydeStan/README.md @@ -1 +1,23 @@ -# HydeStan - Experimental Custom Static Analysis Tool for the HydePHP Monorepo +# HydeStan - Internal Custom Static Analysis for the HydePHP Monorepo + +## About + +HydeStan is a custom static analysis tool in the HydePHP monorepo, designed to provide additional static analysis and code quality checks for the HydePHP framework. +It is in continuous development and is highly specialized, and cannot be relied upon for any outside this repository. + +## Usage + +The analyser is called through the `run.php` script, and is automatically run on all commits through the GitHub Actions CI/CD pipeline. + +### Running HydeStan + +It can also be run manually from the monorepo root: + +```bash +php ./monorepo/HydeStan/run.php +``` + +### GitHub Integration + +A subset of HydeStan is also run on the Git patches sent to our custom [CI Server](https://ci.hydephp.com) to provide near-instant immediate feedback on commits. +Example: https://ci.hydephp.com/api/hydestan/status/e963e2b1c8637ed5d1114e98b32ee698a821c74f diff --git a/monorepo/HydeStan/includes/contracts.php b/monorepo/HydeStan/includes/contracts.php new file mode 100644 index 00000000000..52f64b4dd90 --- /dev/null +++ b/monorepo/HydeStan/includes/contracts.php @@ -0,0 +1,41 @@ +addError($error); + } +} + +abstract class FileAnalyser extends Analyser implements FileAnalyserContract +{ + public function __construct(protected string $file, protected string $contents) + { + // + } +} + +abstract class LineAnalyser extends Analyser implements LineAnalyserContract +{ + public function __construct(protected string $file, protected int $lineNumber, protected string $line) + { + // + } +} diff --git a/monorepo/HydeStan/includes/helpers.php b/monorepo/HydeStan/includes/helpers.php new file mode 100644 index 00000000000..f3a9c8dcb31 --- /dev/null +++ b/monorepo/HydeStan/includes/helpers.php @@ -0,0 +1,76 @@ + */ + /** @return \Hyde\Foundation\Kernel\FileCollection */ public function files(): FileCollection { $this->needsToBeBooted(); diff --git a/packages/framework/src/Foundation/Facades/Files.php b/packages/framework/src/Foundation/Facades/Files.php index 39bbebb6da2..79d2acdc005 100644 --- a/packages/framework/src/Foundation/Facades/Files.php +++ b/packages/framework/src/Foundation/Facades/Files.php @@ -13,7 +13,7 @@ */ class Files extends Facade { - /** @return \Hyde\Foundation\Kernel\FileCollection */ + /** @return \Hyde\Foundation\Kernel\FileCollection */ public static function getFacadeRoot(): FileCollection { return HydeKernel::getInstance()->files(); diff --git a/packages/framework/src/Framework/Features/Blogging/Models/PostAuthor.php b/packages/framework/src/Framework/Features/Blogging/Models/PostAuthor.php index 79778bc4799..1d50af8700e 100644 --- a/packages/framework/src/Framework/Features/Blogging/Models/PostAuthor.php +++ b/packages/framework/src/Framework/Features/Blogging/Models/PostAuthor.php @@ -102,7 +102,7 @@ public static function create(array $data): PostAuthor ], $data)); } - /** Get a Post Author instance by username, or null if not found. */ + /** Get a Post Author instance by username, or null if not found. */ public static function get(string $username): ?static { return static::all()->get(static::normalizeUsername($username));