Skip to content

Commit 909b8ee

Browse files
committed
Merge branch 'master' into 2.x-dev
2 parents 8319a4d + bb69818 commit 909b8ee

File tree

9 files changed

+221
-161
lines changed

9 files changed

+221
-161
lines changed

monorepo/HydeStan/HydeStan.php

Lines changed: 76 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,29 @@
44

55
use Desilva\Console\Console;
66

7+
require_once __DIR__.'/includes/contracts.php';
8+
require_once __DIR__.'/includes/helpers.php';
9+
710
/**
8-
* @internal
11+
* @internal Custom static analysis tool for the HydePHP Development Monorepo.
912
*/
1013
final class HydeStan
1114
{
12-
const VERSION = '0.0.0-dev';
15+
private const FILE_ANALYSERS = [
16+
NoFixMeAnalyser::class,
17+
UnImportedFunctionAnalyser::class,
18+
];
19+
20+
private const TEST_FILE_ANALYSERS = [
21+
NoFixMeAnalyser::class,
22+
NoUsingAssertEqualsForScalarTypesTestAnalyser::class,
23+
];
24+
25+
private const LINE_ANALYSERS = [
26+
NoTestReferenceAnalyser::class,
27+
NoHtmlExtensionInHydePHPLinksAnalyser::class,
28+
NoExtraWhitespaceInCompressedPhpDocAnalyser::class,
29+
];
1330

1431
private array $files;
1532
private array $testFiles;
@@ -31,7 +48,7 @@ public function __construct(private readonly bool $debug = false)
3148

3249
$this->console = new Console();
3350

34-
$this->console->info(sprintf('HydeStan v%s is running!', self::VERSION));
51+
$this->console->info('HydeStan is running!');
3552
$this->console->newline();
3653
}
3754

@@ -105,64 +122,38 @@ public function addErrors(array $errors): void
105122

106123
private function getFiles(): array
107124
{
108-
$files = [];
109-
110-
$directory = new RecursiveDirectoryIterator(BASE_PATH.'/src');
111-
$iterator = new RecursiveIteratorIterator($directory);
112-
$regex = new RegexIterator($iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH);
113-
114-
foreach ($regex as $file) {
115-
$files[] = substr($file[0], strlen(BASE_PATH) + 1);
116-
}
117-
118-
return $files;
125+
return recursiveFileFinder('src');
119126
}
120127

121128
private function getTestFiles(): array
122129
{
123-
$files = [];
124-
125-
$directory = new RecursiveDirectoryIterator(BASE_PATH.'/tests');
126-
$iterator = new RecursiveIteratorIterator($directory);
127-
$regex = new RegexIterator($iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH);
128-
129-
foreach ($regex as $file) {
130-
$files[] = substr($file[0], strlen(BASE_PATH) + 1);
131-
}
132-
133-
return $files;
130+
return recursiveFileFinder('tests');
134131
}
135132

136133
private function analyseFile(string $file, string $contents): void
137134
{
138-
$fileAnalysers = [
139-
new NoFixMeAnalyser($file, $contents),
140-
new UnImportedFunctionAnalyser($file, $contents),
141-
];
135+
foreach (self::FILE_ANALYSERS as $fileAnalyserClass) {
136+
$fileAnalyser = new $fileAnalyserClass($file, $contents);
142137

143-
foreach ($fileAnalysers as $analyser) {
144138
if ($this->debug) {
145-
$this->console->debugComment('Running '.$analyser::class);
139+
$this->console->debugComment('Running '.$fileAnalyser::class);
146140
}
147141

148-
$analyser->run($file, $contents);
142+
$fileAnalyser->run($file, $contents);
149143
AnalysisStatisticsContainer::countedLines(substr_count($contents, "\n"));
150144

151145
foreach (explode("\n", $contents) as $lineNumber => $line) {
152-
$lineAnalysers = [
153-
new NoTestReferenceAnalyser($file, $lineNumber, $line),
154-
];
155-
156-
foreach ($lineAnalysers as $analyser) {
146+
foreach (self::LINE_ANALYSERS as $lineAnalyserClass) {
147+
$lineAnalyser = new $lineAnalyserClass($file, $lineNumber, $line);
157148
AnalysisStatisticsContainer::countedLine();
158-
$analyser->run($file, $lineNumber, $line);
149+
$lineAnalyser->run($file, $lineNumber, $line);
159150
$this->aggregateLines++;
160151
}
161152
}
162153
}
163154

164155
$this->scannedLines += substr_count($contents, "\n");
165-
$this->aggregateLines += (substr_count($contents, "\n") * count($fileAnalysers));
156+
$this->aggregateLines += (substr_count($contents, "\n") * count(self::FILE_ANALYSERS));
166157
}
167158

168159
private function getFileContents(string $file): string
@@ -197,58 +188,23 @@ protected function runTestStan(): void
197188

198189
private function analyseTestFile(string $file, string $contents): void
199190
{
200-
$fileAnalysers = [
201-
new NoFixMeAnalyser($file, $contents),
202-
new NoUsingAssertEqualsForScalarTypesTestAnalyser($file, $contents),
203-
];
191+
foreach (self::TEST_FILE_ANALYSERS as $fileAnalyserClass) {
192+
$fileAnalyser = new $fileAnalyserClass($file, $contents);
204193

205-
foreach ($fileAnalysers as $analyser) {
206194
if ($this->debug) {
207-
$this->console->debugComment('Running '.$analyser::class);
195+
$this->console->debugComment('Running '.$fileAnalyser::class);
208196
}
209197

210-
$analyser->run($file, $contents);
198+
$fileAnalyser->run($file, $contents);
211199
AnalysisStatisticsContainer::countedLines(substr_count($contents, "\n"));
212200

213201
foreach (explode("\n", $contents) as $lineNumber => $line) {
214-
$lineAnalysers = [
215-
//
216-
];
217-
218-
foreach ($lineAnalysers as $analyser) {
219-
AnalysisStatisticsContainer::countedLine();
220-
$analyser->run($file, $lineNumber, $line);
221-
$this->aggregateLines++;
222-
}
202+
// No line analysers defined for test files in the original code
223203
}
224204
}
225205

226206
$this->scannedLines += substr_count($contents, "\n");
227-
$this->aggregateLines += (substr_count($contents, "\n") * count($fileAnalysers));
228-
}
229-
}
230-
231-
abstract class Analyser
232-
{
233-
protected function fail(string $error): void
234-
{
235-
HydeStan::getInstance()->addError($error);
236-
}
237-
}
238-
239-
abstract class FileAnalyser extends Analyser implements FileAnalyserContract
240-
{
241-
public function __construct(protected string $file, protected string $contents)
242-
{
243-
//
244-
}
245-
}
246-
247-
abstract class LineAnalyser extends Analyser implements LineAnalyserContract
248-
{
249-
public function __construct(protected string $file, protected int $lineNumber, protected string $line)
250-
{
251-
//
207+
$this->aggregateLines += (substr_count($contents, "\n") * count(self::TEST_FILE_ANALYSERS));
252208
}
253209
}
254210

@@ -281,6 +237,40 @@ public function run(string $file, string $contents): void
281237
}
282238
}
283239

240+
class NoHtmlExtensionInHydePHPLinksAnalyser extends LineAnalyser
241+
{
242+
public function run(string $file, int $lineNumber, string $line): void
243+
{
244+
AnalysisStatisticsContainer::analysedExpressions(1);
245+
246+
if (str_contains($line, 'https://hydephp.com/') && str_contains($line, '.html')) {
247+
AnalysisStatisticsContainer::analysedExpressions(1);
248+
249+
$this->fail(sprintf('HTML extension used in URL at %s',
250+
fileLink(BASE_PATH.'/packages/framework/'.$file, $lineNumber + 1)
251+
));
252+
253+
HydeStan::addActionsMessage('warning', $file, $lineNumber + 1, 'HydeStan: NoHtmlExtensionError', 'URL contains .html extension. Consider removing it.');
254+
}
255+
}
256+
}
257+
258+
class NoExtraWhitespaceInCompressedPhpDocAnalyser extends LineAnalyser
259+
{
260+
public function run(string $file, int $lineNumber, string $line): void
261+
{
262+
AnalysisStatisticsContainer::analysedExpressions(1);
263+
264+
if (str_contains($line, '/** ')) {
265+
$this->fail(sprintf('Extra whitespace in compressed PHPDoc comment at %s',
266+
fileLink(BASE_PATH.'/packages/framework/'.$file, $lineNumber + 1)
267+
));
268+
269+
HydeStan::addActionsMessage('warning', $file, $lineNumber + 1, 'HydeStan: ExtraWhitespaceInPhpDocError', 'Extra whitespace found in compressed PHPDoc comment.');
270+
}
271+
}
272+
}
273+
284274
class NoUsingAssertEqualsForScalarTypesTestAnalyser extends FileAnalyser // Todo: Extend line analyser instead? Would allow for checking for more errors after the first error
285275
{
286276
public function run(string $file, string $contents): void
@@ -370,7 +360,7 @@ public function run(string $file, string $contents): void
370360
foreach ($calledFunctions as $calledFunction) {
371361
AnalysisStatisticsContainer::analysedExpression();
372362
if (! in_array($calledFunction, $functionImports)) {
373-
echo("Found unimported function '$calledFunction' in ".realpath(__DIR__.'/../../packages/framework/'.$file))."\n";
363+
echo sprintf("Found unimported function '$calledFunction' in %s\n", realpath(__DIR__.'/../../packages/framework/'.$file));
374364
}
375365
}
376366
}
@@ -384,80 +374,11 @@ public function run(string $file, int $lineNumber, string $line): void
384374

385375
if (str_starts_with($line, ' * @see') && str_ends_with($line, 'Test')) {
386376
AnalysisStatisticsContainer::analysedExpressions(1);
387-
$this->fail(sprintf('Test class %s is referenced in %s:%s', trim(substr($line, 7)),
388-
realpath(__DIR__.'/../../packages/framework/'.$file) ?: $file, $lineNumber + 1));
389-
}
390-
}
391-
}
392-
393-
class AnalysisStatisticsContainer
394-
{
395-
private static int $linesCounted = 0;
396-
private static float $expressionsAnalysed = 0;
397-
398-
public static function countedLine(): void
399-
{
400-
self::$linesCounted++;
401-
}
402-
403-
public static function countedLines(int $count): void
404-
{
405-
self::$linesCounted += $count;
406-
}
407-
408-
public static function analysedExpression(): void
409-
{
410-
self::$expressionsAnalysed++;
411-
}
412-
413-
public static function analysedExpressions(float $countOrEstimate): void
414-
{
415-
self::$expressionsAnalysed += $countOrEstimate;
416-
}
417-
418-
public static function getLinesCounted(): int
419-
{
420-
return self::$linesCounted;
421-
}
422-
423-
public static function getExpressionsAnalysed(): int
424-
{
425-
return (int) round(self::$expressionsAnalysed);
426-
}
427-
}
428-
429-
interface FileAnalyserContract
430-
{
431-
public function __construct(string $file, string $contents);
432-
433-
public function run(string $file, string $contents): void;
434-
}
435-
436-
interface LineAnalyserContract
437-
{
438-
public function __construct(string $file, int $lineNumber, string $line);
439-
440-
public function run(string $file, int $lineNumber, string $line): void;
441-
}
442-
443-
function check_str_contains_any(array $searches, string $line): bool
444-
{
445-
$strContainsAny = false;
446-
foreach ($searches as $search) {
447-
AnalysisStatisticsContainer::analysedExpression();
448-
if (str_contains($line, $search)) {
449-
$strContainsAny = true;
377+
$this->fail(sprintf('Test class %s is referenced in %s:%s',
378+
trim(substr($line, 7)),
379+
realpath(__DIR__.'/../../packages/framework/'.$file) ?: $file,
380+
$lineNumber + 1
381+
));
450382
}
451383
}
452-
453-
return $strContainsAny;
454-
}
455-
456-
function fileLink(string $file, ?int $line = null): string
457-
{
458-
$path = (realpath(__DIR__.'/../../packages/framework/'.$file) ?: $file).($line ? ':'.$line : '');
459-
$trim = strlen(getcwd()) + 2;
460-
$path = substr($path, $trim);
461-
462-
return str_replace('\\', '/', $path);
463384
}

monorepo/HydeStan/README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,23 @@
1-
# HydeStan - Experimental Custom Static Analysis Tool for the HydePHP Monorepo
1+
# HydeStan - Internal Custom Static Analysis for the HydePHP Monorepo
2+
3+
## About
4+
5+
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.
6+
It is in continuous development and is highly specialized, and cannot be relied upon for any outside this repository.
7+
8+
## Usage
9+
10+
The analyser is called through the `run.php` script, and is automatically run on all commits through the GitHub Actions CI/CD pipeline.
11+
12+
### Running HydeStan
13+
14+
It can also be run manually from the monorepo root:
15+
16+
```bash
17+
php ./monorepo/HydeStan/run.php
18+
```
19+
20+
### GitHub Integration
21+
22+
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.
23+
Example: https://ci.hydephp.com/api/hydestan/status/e963e2b1c8637ed5d1114e98b32ee698a821c74f
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
interface FileAnalyserContract
6+
{
7+
public function __construct(string $file, string $contents);
8+
9+
public function run(string $file, string $contents): void;
10+
}
11+
12+
interface LineAnalyserContract
13+
{
14+
public function __construct(string $file, int $lineNumber, string $line);
15+
16+
public function run(string $file, int $lineNumber, string $line): void;
17+
}
18+
19+
abstract class Analyser
20+
{
21+
protected function fail(string $error): void
22+
{
23+
HydeStan::getInstance()->addError($error);
24+
}
25+
}
26+
27+
abstract class FileAnalyser extends Analyser implements FileAnalyserContract
28+
{
29+
public function __construct(protected string $file, protected string $contents)
30+
{
31+
//
32+
}
33+
}
34+
35+
abstract class LineAnalyser extends Analyser implements LineAnalyserContract
36+
{
37+
public function __construct(protected string $file, protected int $lineNumber, protected string $line)
38+
{
39+
//
40+
}
41+
}

0 commit comments

Comments
 (0)