Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ composer.lock
.php-version
composer.phar
.phpunit.cache/
.vscode
.vscode
.phparkitect.cache
71 changes: 71 additions & 0 deletions src/Analyzer/CachedFileParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace Arkitect\Analyzer;

class CachedFileParser implements Parser
{
/** @var array<string, array{hash: string, result: ParserResult}> */
private array $entries = [];

private bool $dirty = false;

private string $filePath;

private Parser $innerParser;

public function __construct(Parser $innerParser, string $cacheFilePath)
{
$this->filePath = $cacheFilePath;
$this->innerParser = $innerParser;

if (file_exists($cacheFilePath)) {
$data = unserialize((string) file_get_contents($cacheFilePath));
if (\is_array($data)) {
$this->entries = $data;
}
}
}

public function __destruct()
{
if ($this->dirty) {
file_put_contents($this->filePath, serialize($this->entries));
}
}

public function parse(string $fileContent, string $filename): ParserResult
{
$cachedResult = $this->get($filename, md5($fileContent));

if (null !== $cachedResult) {
return $cachedResult;
}

$result = $this->innerParser->parse($fileContent, $filename);

$this->set($filename, md5($fileContent), $result);

return $result;
}

public function get(string $filename, string $contentHash): ?ParserResult
{
if (!isset($this->entries[$filename])) {
return null;
}

if ($this->entries[$filename]['hash'] !== $contentHash) {
return null;
}

return $this->entries[$filename]['result'];
}

public function set(string $filename, string $contentHash, ParserResult $result): void
{
$this->entries[$filename] = ['hash' => $contentHash, 'result' => $result];
$this->dirty = true;
}
}
23 changes: 18 additions & 5 deletions src/Analyzer/FileParserFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,32 @@

class FileParserFactory
{
public static function createFileParser(TargetPhpVersion $targetPhpVersion, bool $parseCustomAnnotations = true): FileParser
{
return new FileParser(
public static function createFileParser(
TargetPhpVersion $targetPhpVersion,
bool $parseCustomAnnotations = true,
?string $cacheFilePath = null,
): Parser {
$fp = new FileParser(
new NodeTraverser(),
new FileVisitor(new ClassDescriptionBuilder()),
new NameResolver(),
new DocblockTypesResolver($parseCustomAnnotations),
$targetPhpVersion
);

if (null !== $cacheFilePath) {
$fp = new CachedFileParser($fp, $cacheFilePath);
}

return $fp;
}

public static function forPhpVersion(string $targetPhpVersion): FileParser
public static function forPhpVersion(string $targetPhpVersion): Parser
{
return self::createFileParser(TargetPhpVersion::create($targetPhpVersion), true);
return self::createFileParser(
TargetPhpVersion::create($targetPhpVersion),
true,
null
);
}
}
26 changes: 26 additions & 0 deletions src/Analyzer/FilesToParse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Arkitect\Analyzer;

use Symfony\Component\Finder\SplFileInfo;

/**
* @template-implements \IteratorAggregate<SplFileInfo>
*/
class FilesToParse implements \IteratorAggregate
{
/** @var array<SplFileInfo> */
private array $files = [];

public function add(SplFileInfo $file): void
{
$this->files[] = $file;
}

public function getIterator(): \Traversable
{
return new \ArrayIterator($this->files);
}
}
20 changes: 20 additions & 0 deletions src/Analyzer/ParsedFiles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Arkitect\Analyzer;

class ParsedFiles
{
private array $data = [];

public function add(string $relativeFilePath, ParserResult $result): void
{
$this->data[$relativeFilePath] = $result;
}

public function get(string $relativeFilePath): ?ParserResult
{
return $this->data[$relativeFilePath] ?? null;
}
}
11 changes: 10 additions & 1 deletion src/CLI/Command/Check.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class Check extends Command
private const IGNORE_BASELINE_LINENUMBERS_PARAM = 'ignore-baseline-linenumbers';
private const FORMAT_PARAM = 'format';
private const AUTOLOAD_PARAM = 'autoload';
private const NO_CACHE_PARAM = 'no-cache';

private const GENERATE_BASELINE_PARAM = 'generate-baseline';
private const DEFAULT_RULES_FILENAME = 'phparkitect.php';
Expand Down Expand Up @@ -105,6 +106,12 @@ protected function configure(): void
'a',
InputOption::VALUE_REQUIRED,
'Specify an autoload file to use',
)
->addOption(
self::NO_CACHE_PARAM,
'o',
InputOption::VALUE_NONE,
'Disable cache'
);
}

Expand All @@ -124,6 +131,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$generateBaseline = $input->getOption(self::GENERATE_BASELINE_PARAM);
$phpVersion = $input->getOption('target-php-version');
$format = $input->getOption(self::FORMAT_PARAM);
$noCache = (bool) $input->getOption(self::NO_CACHE_PARAM);

// we write everything on STDERR apart from the list of violations which goes on STDOUT
// this allows to pipe the output of this command to a file while showing output on the terminal
Expand All @@ -139,7 +147,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
->baselineFilePath(Baseline::resolveFilePath($useBaseline, self::DEFAULT_BASELINE_FILENAME))
->ignoreBaselineLinenumbers($ignoreBaselineLinenumbers)
->skipBaseline($skipBaseline)
->format($format);
->format($format)
->noCache($noCache);

$this->requireAutoload($output, $config->getAutoloadFilePath());
$printer = $this->createPrinter($output, $config->getFormat());
Expand Down
4 changes: 2 additions & 2 deletions src/CLI/Command/DebugExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
/**
* @throws \Arkitect\Exceptions\PhpVersionNotValidException
*/
private function getParser(InputInterface $input): \Arkitect\Analyzer\FileParser
private function getParser(InputInterface $input): \Arkitect\Analyzer\Parser
{
$phpVersion = $input->getOption('target-php-version');
$targetPhpVersion = TargetPhpVersion::create($phpVersion);
$fileParser = FileParserFactory::createFileParser($targetPhpVersion);
$fileParser = FileParserFactory::createFileParser($targetPhpVersion, true, null);

return $fileParser;
}
Expand Down
17 changes: 17 additions & 0 deletions src/CLI/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class Config

private TargetPhpVersion $targetPhpVersion;

private ?string $cacheFilePath = null;

public function __construct()
{
$this->classSetRules = [];
Expand All @@ -43,6 +45,7 @@ public function __construct()
$this->format = PrinterFactory::default();
$this->autoloadFilePath = null;
$this->targetPhpVersion = TargetPhpVersion::latest();
$this->cacheFilePath = '.phparkitect.cache';
}

public function add(ClassSet $classSet, ArchRule ...$rules): self
Expand Down Expand Up @@ -167,4 +170,18 @@ public function getAutoloadFilePath(): ?string
{
return $this->autoloadFilePath;
}

public function noCache(bool $noCache): self
{
if ($noCache) {
$this->cacheFilePath = null;
}

return $this;
}

public function getCacheFilePath(): ?string
{
return $this->cacheFilePath;
}
}
79 changes: 73 additions & 6 deletions src/CLI/Runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Arkitect\Analyzer\ClassDescription;
use Arkitect\Analyzer\FileParserFactory;
use Arkitect\Analyzer\FilesToParse;
use Arkitect\Analyzer\ParsedFiles;
use Arkitect\Analyzer\Parser;
use Arkitect\Analyzer\ParsingErrors;
use Arkitect\ClassSetRules;
Expand Down Expand Up @@ -45,17 +47,46 @@ public function check(
Violations $violations,
ParsingErrors $parsingErrors,
bool $stopOnFailure,
): void {
// first step: collect all files to parse
$filesToParse = $this->collectFilesToParse($classSetRule);

// second step: parse all files and collect results
$parsedFiles = $this->collectParsedFiles(
$filesToParse,
$fileParser,
$progress
);

// third step: check all rules on all files
$this->checkRulesOnParsedFiles(
$classSetRule,
$parsedFiles,
$violations,
$parsingErrors,
$stopOnFailure
);
}

public function checkRulesOnParsedFiles(
ClassSetRules $classSetRule,
ParsedFiles $parsedFiles,
Violations $violations,
ParsingErrors $parsingErrors,
bool $stopOnFailure,
): void {
/** @var SplFileInfo $file */
foreach ($classSetRule->getClassSet() as $file) {
$fileViolations = new Violations();

$progress->startParsingFile($file->getRelativePathname());
$result = $parsedFiles->get($file->getRelativePathname());

$result = $fileParser->parse($file->getContents(), $file->getRelativePathname());
if (null === $result) {
continue; // this should not happen
}

$parsingErrors->merge($result->parsingErrors());

$fileViolations = new Violations();

/** @var ClassDescription $classDescription */
foreach ($result->classDescriptions() as $classDescription) {
foreach ($classSetRule->getRules() as $rule) {
Expand All @@ -70,9 +101,37 @@ public function check(
}

$violations->merge($fileViolations);
}
}

protected function collectFilesToParse(ClassSetRules $classSetRule): FilesToParse
{
$filesToParse = new FilesToParse();

/** @var SplFileInfo $file */
foreach ($classSetRule->getClassSet() as $file) {
$filesToParse->add($file);
}

return $filesToParse;
}

protected function collectParsedFiles(FilesToParse $filesToParse, Parser $fileParser, Progress $progress): ParsedFiles
{
$parsedFiles = new ParsedFiles();

/** @var SplFileInfo $file */
foreach ($filesToParse as $file) {
$progress->startParsingFile($file->getRelativePathname());

$result = $fileParser->parse($file->getContents(), $file->getRelativePathname());

$parsedFiles->add($file->getRelativePathname(), $result);

$progress->endParsingFile($file->getRelativePathname());
}

return $parsedFiles;
}

protected function doRun(Config $config, Progress $progress): array
Expand All @@ -82,15 +141,23 @@ protected function doRun(Config $config, Progress $progress): array

$fileParser = FileParserFactory::createFileParser(
$config->getTargetPhpVersion(),
$config->isParseCustomAnnotationsEnabled()
$config->isParseCustomAnnotationsEnabled(),
$config->getCacheFilePath()
);

/** @var ClassSetRules $classSetRule */
foreach ($config->getClassSetRules() as $classSetRule) {
$progress->startFileSetAnalysis($classSetRule->getClassSet());

try {
$this->check($classSetRule, $progress, $fileParser, $violations, $parsingErrors, $config->isStopOnFailure());
$this->check(
$classSetRule,
$progress,
$fileParser,
$violations,
$parsingErrors,
$config->isStopOnFailure()
);
} catch (FailOnFirstViolationException $e) {
break;
} finally {
Expand Down
Loading
Loading