Skip to content

Commit

Permalink
Merge branch 'master' into 2.x-dev
Browse files Browse the repository at this point in the history
  • Loading branch information
caendesilva committed Jul 10, 2024
2 parents 8319a4d + bb69818 commit 909b8ee
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 161 deletions.
231 changes: 76 additions & 155 deletions monorepo/HydeStan/HydeStan.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
}
}
Expand All @@ -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);
}
24 changes: 23 additions & 1 deletion monorepo/HydeStan/README.md
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions monorepo/HydeStan/includes/contracts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

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;
}

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)
{
//
}
}
Loading

0 comments on commit 909b8ee

Please sign in to comment.