Skip to content
Merged
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
12 changes: 11 additions & 1 deletion src/Analyze/FileScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@
*/
final class FileScopeResolver
{
/**
* Directories never reviewed by a broad scan — dependencies and generated
* output. Excluded relative to each scanned root, so an explicit
* `--path=vendor/foo` still works; only `--all` / directory scans skip them.
*
* @var list<string>
*/
private const EXCLUDED_DIRS = ['vendor', 'node_modules', 'storage', 'bootstrap/cache'];

public function __construct(
private readonly CommandExecutor $executor,
private readonly string $workingDirectory,
Expand Down Expand Up @@ -110,7 +119,8 @@ private function toExistingPhpFiles(array $files): array
private function phpFilesIn(string $dir): array
{
$files = [];
foreach (Finder::create()->files()->in($dir)->name('*.php')->sortByName() as $file) {
$finder = Finder::create()->files()->in($dir)->name('*.php')->exclude(self::EXCLUDED_DIRS)->sortByName();
foreach ($finder as $file) {
$files[] = $file->getRealPath() ?: $file->getPathname();
}

Expand Down
24 changes: 15 additions & 9 deletions src/Commands/CodeguardAnalyzeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Henryavila\Codeguard\Analyze\AnalyzeResult;
use Henryavila\Codeguard\Analyze\AnalyzeRunner;
use Henryavila\Codeguard\Analyze\FileScopeResolver;
use Henryavila\Codeguard\Analyze\LlmClient;
use Henryavila\Codeguard\Analyze\Severity;
use Henryavila\Codeguard\Telemetry\EventName;
use Henryavila\Codeguard\Telemetry\EventStatus;
Expand Down Expand Up @@ -40,6 +41,7 @@ public function handle(
FileScopeResolver $scope,
Recorder $recorder,
AnalyzeBaseline $baseline,
LlmClient $llm,
): int {
if ((bool) $this->option('emit')) {
return $this->handleEmit($config, $runner, $scope);
Expand All @@ -50,6 +52,19 @@ public function handle(
return $this->handleIngest($config, $runner, $scope, $recorder, $baseline, $ingest);
}

// No real adjudicating driver → context-emit is the supported transport.
// Inform and fall back to writing a work order for /codeguard-review,
// instead of a dead-end notice. The synchronous path below runs only
// when a driver (e.g. an API client) is bound in place of NullLlmClient.
if (! $llm->isConfigured()) {
$this->components->info(
'No LLM driver configured — emitting a work order for context-emit review '
.'(run /codeguard-review, or --ingest its findings). Uses your Claude Code subscription, no metered API.',
);

return $this->handleEmit($config, $runner, $scope);
}

$context = $this->resolveContext();
$failOn = $this->resolveFailOn();

Expand All @@ -67,15 +82,6 @@ public function handle(
$files = $this->resolveFiles($scope);
$result = $runner->run($files, $config->enabledPresets, $failOn, $context);

if (! $result->adjudicated) {
$this->components->warn(
'LLM driver not configured — set config(\'codeguard.patterns.driver\'). No patterns adjudicated.',
);
$this->emitCommandEnd($recorder, self::SUCCESS, $startHrtime);

return self::SUCCESS;
}

$this->maybeAccept($baseline, $result);
$this->renderFindings($result);

Expand Down
43 changes: 34 additions & 9 deletions tests/Feature/CodeguardAnalyzeCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,25 +189,32 @@ function analyzeReadEvents(string $path): array
}
});

it('does not adjudicate or fake a clean repo when no driver is configured', function (): void {
it('falls back to context-emit (work order) when no LLM driver is configured', function (): void {
$telemetry = analyzeTelemetryPath();
$file = analyzeFixtureFile();
$out = sys_get_temp_dir().DIRECTORY_SEPARATOR.'codeguard-fallback-'.uniqid().'.json';
$fake = new FakeLlmClient(analyzeFindingHandler('critical'), configured: false);
analyzeBind($telemetry, $fake);

try {
$exit = Artisan::call('codeguard:analyze', ['--path' => $file, '--context' => 'ci']);

$events = analyzeReadEvents($telemetry);
$analyzeEnded = array_values(array_filter(
$events,
static fn (array $event): bool => ($event['event'] ?? '') === 'analyze.ended',
));
$exit = Artisan::call('codeguard:analyze', ['--path' => $file, '--out' => $out, '--context' => 'ci']);
$output = Artisan::output();

// No synchronous adjudication is attempted, and instead of a dead-end
// notice the command emits a work order for the context-emit review path.
expect($exit)->toBe(0)
->and($fake->calls)->toHaveCount(0)
->and($analyzeEnded[0]['status'] ?? null)->toBe('skip');
->and(is_file($out))->toBeTrue()
->and($output)->toContain('context-emit');

$decoded = json_decode((string) file_get_contents($out), true);
$units = (is_array($decoded) && is_array($decoded['units'] ?? null)) ? $decoded['units'] : [];

expect($units)->toHaveCount(1);
} finally {
if (is_file($out)) {
unlink($out);
}
analyzeCleanup($file, $telemetry);
}
});
Expand Down Expand Up @@ -236,6 +243,24 @@ function analyzeReadEvents(string $path): array
}
});

it('fails with a clear error when the --ingest findings file does not exist', function (): void {
$telemetry = analyzeTelemetryPath();
$file = analyzeFixtureFile();
$missing = sys_get_temp_dir().DIRECTORY_SEPARATOR.'codeguard-missing-'.uniqid().'.json';
$fake = new FakeLlmClient(fn (AnalysisUnit $unit): array => []);
analyzeBind($telemetry, $fake);

try {
$exit = Artisan::call('codeguard:analyze', ['--ingest' => $missing, '--path' => $file, '--context' => 'ci']);

expect($exit)->toBe(1)
->and(Artisan::output())->toContain('not found')
->and($fake->calls)->toHaveCount(0);
} finally {
analyzeCleanup($file, $telemetry);
}
});

it('emits a work order JSON with units and prompt-ready patterns', function (): void {
$file = analyzeFixtureFile();
$out = sys_get_temp_dir().DIRECTORY_SEPARATOR.'codeguard-workorder-'.uniqid().'.json';
Expand Down
33 changes: 33 additions & 0 deletions tests/Unit/Analyze/FileScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,36 @@ function fsrExecutor(string $gitOutput): FakeCommandExecutor
fsrCleanup($base);
}
});

it('excludes vendor, node_modules and generated dirs from --all', function (): void {
$base = fsrBase();

try {
fsrWrite($base, 'app/Real.php');
fsrWrite($base, 'vendor/acme/lib/Dep.php');
fsrWrite($base, 'node_modules/pkg/index.php');
fsrWrite($base, 'storage/framework/views/cached.php');
fsrWrite($base, 'bootstrap/cache/packages.php');
$resolver = new FileScopeResolver(fsrExecutor(''), $base);

$all = $resolver->all();

expect($all)->toHaveCount(1)
->and($all[0])->toContain('Real.php');
} finally {
fsrCleanup($base);
}
});

it('still scans an explicitly-requested vendor subtree (exclusion is for broad scans)', function (): void {
$base = fsrBase();

try {
fsrWrite($base, 'vendor/acme/lib/Dep.php');
$resolver = new FileScopeResolver(fsrExecutor(''), $base);

expect($resolver->path('vendor/acme/lib'))->toHaveCount(1);
} finally {
fsrCleanup($base);
}
});
Loading