Skip to content

Global Exclusion Pattern System #212

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 3, 2025
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
28 changes: 28 additions & 0 deletions docs/example/config/exclude-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Example configuration with exclusion patterns

# Global exclusion patterns
exclude:
# File patterns to exclude globally (glob patterns with wildcards)
patterns:
- "**/.env*"
- "**/config/secrets.yaml"
- "**/*.pem"
- "**/*.key"
- "**/id_rsa"
- "**/credentials.json"

# Paths to exclude globally (exact directories or files)
paths:
- ".secrets/"
- "config/credentials/"
- "node_modules"
- "vendor"

# Regular configuration continues
documents:
- description: "Project Documentation"
outputPath: "docs/project.md"
sources:
- type: file
sourcePaths:
- "src/"
22 changes: 21 additions & 1 deletion json-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,26 @@
"$ref": "#/definitions/prompt"
}
},
"exclude": {
"type": "object",
"description": "Global exclusion patterns for filtering files from being included in documents",
"properties": {
"patterns": {
"type": "array",
"description": "Glob patterns to exclude files globally (e.g., '**/.env*', '**/*.pem')",
"items": {
"type": "string"
}
},
"paths": {
"type": "array",
"description": "Specific paths to exclude globally (directories or files)",
"items": {
"type": "string"
}
}
}
},
"variables": {
"type": "object",
"description": "Custom variables to use throughout the configuration",
Expand Down Expand Up @@ -705,7 +725,7 @@
}
}
},
"allOf": [
"anyOf": [
{
"if": {
"properties": {
Expand Down
29 changes: 29 additions & 0 deletions src/Application/Bootloader/ExcludeBootloader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Butschster\ContextGenerator\Application\Bootloader;

use Butschster\ContextGenerator\Config\Exclude\ExcludeParserPlugin;
use Butschster\ContextGenerator\Config\Exclude\ExcludeRegistry;
use Butschster\ContextGenerator\Config\Exclude\ExcludeRegistryInterface;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Core\Attribute\Singleton;

#[Singleton]
final class ExcludeBootloader extends Bootloader
{
#[\Override]
public function defineSingletons(): array
{
return [
ExcludeRegistryInterface::class => ExcludeRegistry::class,
];
}

public function boot(ConfigLoaderBootloader $configLoader, ExcludeParserPlugin $excludeParser): void
{
// Register the exclude parser plugin
$configLoader->registerParserPlugin($excludeParser);
}
}
4 changes: 3 additions & 1 deletion src/Application/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Butschster\ContextGenerator\Application\Bootloader\ConsoleBootloader;
use Butschster\ContextGenerator\Application\Bootloader\ContentRendererBootloader;
use Butschster\ContextGenerator\Application\Bootloader\CoreBootloader;
use Butschster\ContextGenerator\Application\Bootloader\ExcludeBootloader;
use Butschster\ContextGenerator\Application\Bootloader\GithubClientBootloader;
use Butschster\ContextGenerator\Application\Bootloader\GitlabClientBootloader;
use Butschster\ContextGenerator\Application\Bootloader\HttpClientBootloader;
Expand Down Expand Up @@ -58,9 +59,10 @@ protected function defineBootloaders(): array
GithubClientBootloader::class,
ComposerClientBootloader::class,
ConfigLoaderBootloader::class,
VariableBootloader::class,
ExcludeBootloader::class,
ModifierBootloader::class,
ContentRendererBootloader::class,
VariableBootloader::class,
SourceFetcherBootloader::class,
SourceRegistryBootloader::class,

Expand Down
42 changes: 42 additions & 0 deletions src/Config/Exclude/AbstractExclusion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace Butschster\ContextGenerator\Config\Exclude;

/**
* Base class for all exclusion patterns
*/
abstract readonly class AbstractExclusion implements ExclusionPatternInterface
{
/**
* @param string $pattern Exclusion pattern value
*/
public function __construct(
protected string $pattern,
) {}

/**
* Get the raw pattern string
*/
public function getPattern(): string
{
return $this->pattern;
}

/**
* Abstract method to check if a path matches this pattern
*/
abstract public function matches(string $path): bool;

/**
* Normalize a pattern for consistent comparison
*/
protected function normalizePattern(string $pattern): string
{
$pattern = \preg_replace('#^\./#', '', $pattern);

// Remove trailing slash
return \rtrim((string) $pattern, '/');
}
}
98 changes: 98 additions & 0 deletions src/Config/Exclude/ExcludeParserPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace Butschster\ContextGenerator\Config\Exclude;

use Butschster\ContextGenerator\Application\Logger\LoggerPrefix;
use Butschster\ContextGenerator\Config\Parser\ConfigParserPluginInterface;
use Butschster\ContextGenerator\Config\Registry\RegistryInterface;
use Psr\Log\LoggerInterface;

/**
* Parser plugin for the 'exclude' section in configuration
*/
final readonly class ExcludeParserPlugin implements ConfigParserPluginInterface
{
public function __construct(
private ExcludeRegistryInterface $registry,
#[LoggerPrefix(prefix: 'exclude-parser')]
private ?LoggerInterface $logger = null,
) {}

public function getConfigKey(): string

Check warning on line 23 in src/Config/Exclude/ExcludeParserPlugin.php

View check run for this annotation

Codecov / codecov/patch

src/Config/Exclude/ExcludeParserPlugin.php#L23

Added line #L23 was not covered by tests
{
return 'exclude';

Check warning on line 25 in src/Config/Exclude/ExcludeParserPlugin.php

View check run for this annotation

Codecov / codecov/patch

src/Config/Exclude/ExcludeParserPlugin.php#L25

Added line #L25 was not covered by tests
}

public function supports(array $config): bool
{
return isset($config['exclude']) && \is_array($config['exclude']);
}

public function parse(array $config, string $rootPath): ?RegistryInterface
{
if (!$this->supports($config)) {
return null;

Check warning on line 36 in src/Config/Exclude/ExcludeParserPlugin.php

View check run for this annotation

Codecov / codecov/patch

src/Config/Exclude/ExcludeParserPlugin.php#L36

Added line #L36 was not covered by tests
}

\assert($this->registry instanceof RegistryInterface);
$excludeConfig = $config['exclude'];

// Parse patterns
if (isset($excludeConfig['patterns']) && \is_array($excludeConfig['patterns'])) {
$this->parsePatterns($excludeConfig['patterns']);
}

// Parse paths
if (isset($excludeConfig['paths']) && \is_array($excludeConfig['paths'])) {
$this->parsePaths($excludeConfig['paths']);
}

$this->logger?->info('Parsed exclusion configuration', [
'patternCount' => \count($this->registry->getPatterns()),
]);

return $this->registry;
}

public function updateConfig(array $config, string $rootPath): array
{
// We don't need to modify the config, just return it as is
return $config;
}

/**
* Parse glob pattern exclusions
*/
private function parsePatterns(array $patterns): void
{
foreach ($patterns as $pattern) {
if (!\is_string($pattern) || empty($pattern)) {
$this->logger?->warning('Invalid exclusion pattern, skipping', [
'pattern' => $pattern,
]);
continue;

Check warning on line 75 in src/Config/Exclude/ExcludeParserPlugin.php

View check run for this annotation

Codecov / codecov/patch

src/Config/Exclude/ExcludeParserPlugin.php#L72-L75

Added lines #L72 - L75 were not covered by tests
}

$this->registry->addPattern(new PatternExclusion($pattern));
}
}

/**
* Parse path exclusions
*/
private function parsePaths(array $paths): void
{
foreach ($paths as $path) {
if (!\is_string($path) || empty($path)) {
$this->logger?->warning('Invalid exclusion path, skipping', [
'path' => $path,
]);
continue;

Check warning on line 92 in src/Config/Exclude/ExcludeParserPlugin.php

View check run for this annotation

Codecov / codecov/patch

src/Config/Exclude/ExcludeParserPlugin.php#L89-L92

Added lines #L89 - L92 were not covered by tests
}

$this->registry->addPattern(new PathExclusion($path));
}
}
}
105 changes: 105 additions & 0 deletions src/Config/Exclude/ExcludeRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

declare(strict_types=1);

namespace Butschster\ContextGenerator\Config\Exclude;

use Butschster\ContextGenerator\Application\Logger\LoggerPrefix;
use Butschster\ContextGenerator\Config\Registry\RegistryInterface;
use Psr\Log\LoggerInterface;
use Spiral\Core\Attribute\Singleton;

/**
* Registry for file exclusion patterns
*
* @implements RegistryInterface<ExclusionPatternInterface>
*/
#[Singleton]
final class ExcludeRegistry implements ExcludeRegistryInterface, RegistryInterface
{
/** @var array<ExclusionPatternInterface> */
private array $patterns = [];

public function __construct(
#[LoggerPrefix(prefix: 'exclude-registry')]
private readonly ?LoggerInterface $logger = null,
) {}

/**
* Add a new exclusion pattern
*/
public function addPattern(ExclusionPatternInterface $pattern): self
{
$this->patterns[] = $pattern;

$this->logger?->debug('Added exclusion pattern', [
'pattern' => $pattern->getPattern(),
]);

return $this;
}

/**
* Check if a path should be excluded
*/
public function shouldExclude(string $path): bool
{
foreach ($this->patterns as $pattern) {
if ($pattern->matches($path)) {
$this->logger?->debug('Path excluded by pattern', [
'path' => $path,
'pattern' => $pattern->getPattern(),
]);

return true;
}
}

return false;
}

/**
* Get all registered exclusion patterns
*/
public function getPatterns(): array
{
return $this->patterns;
}

/**
* Get the registry type
*/
public function getType(): string
{
return 'exclude';
}

/**
* Get all items in the registry
*/
public function getItems(): array

Check warning on line 80 in src/Config/Exclude/ExcludeRegistry.php

View check run for this annotation

Codecov / codecov/patch

src/Config/Exclude/ExcludeRegistry.php#L80

Added line #L80 was not covered by tests
{
return $this->patterns;

Check warning on line 82 in src/Config/Exclude/ExcludeRegistry.php

View check run for this annotation

Codecov / codecov/patch

src/Config/Exclude/ExcludeRegistry.php#L82

Added line #L82 was not covered by tests
}

/**
* Make the registry iterable
*/
public function getIterator(): \Traversable

Check warning on line 88 in src/Config/Exclude/ExcludeRegistry.php

View check run for this annotation

Codecov / codecov/patch

src/Config/Exclude/ExcludeRegistry.php#L88

Added line #L88 was not covered by tests
{
return new \ArrayIterator($this->patterns);

Check warning on line 90 in src/Config/Exclude/ExcludeRegistry.php

View check run for this annotation

Codecov / codecov/patch

src/Config/Exclude/ExcludeRegistry.php#L90

Added line #L90 was not covered by tests
}

/**
* JSON serialization
*/
public function jsonSerialize(): array

Check warning on line 96 in src/Config/Exclude/ExcludeRegistry.php

View check run for this annotation

Codecov / codecov/patch

src/Config/Exclude/ExcludeRegistry.php#L96

Added line #L96 was not covered by tests
{
return [
'patterns' => \array_map(
static fn(ExclusionPatternInterface $pattern) => $pattern->jsonSerialize(),
$this->patterns,
),
];

Check warning on line 103 in src/Config/Exclude/ExcludeRegistry.php

View check run for this annotation

Codecov / codecov/patch

src/Config/Exclude/ExcludeRegistry.php#L98-L103

Added lines #L98 - L103 were not covered by tests
}
}
28 changes: 28 additions & 0 deletions src/Config/Exclude/ExcludeRegistryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Butschster\ContextGenerator\Config\Exclude;

/**
* Interface for exclusion pattern registry
*/
interface ExcludeRegistryInterface
{
/**
* Add an exclusion pattern
*/
public function addPattern(ExclusionPatternInterface $pattern): self;

/**
* Check if a path should be excluded
*/
public function shouldExclude(string $path): bool;

/**
* Get all registered exclusion patterns
*
* @return array<ExclusionPatternInterface>
*/
public function getPatterns(): array;
}
Loading
Loading