diff --git a/src/Checkers/FileSystemChecker.php b/src/Checkers/FileSystemChecker.php index 97747341..c586be26 100644 --- a/src/Checkers/FileSystemChecker.php +++ b/src/Checkers/FileSystemChecker.php @@ -53,7 +53,7 @@ public function check(array $parameters): array $misspelling, $fileOrDirectory->getRealPath(), 0, - ), $this->spellchecker->check($name), + ), $this->spellchecker->check($name, $fileOrDirectory->getRealPath()), ); $issues = [ diff --git a/src/Checkers/SourceCodeChecker.php b/src/Checkers/SourceCodeChecker.php index 7dc7ac39..451beecf 100644 --- a/src/Checkers/SourceCodeChecker.php +++ b/src/Checkers/SourceCodeChecker.php @@ -118,7 +118,7 @@ private function getIssuesFromSourceFile(SplFileInfo $file): array $misspelling, $file->getRealPath(), $this->getErrorLine($file, $name), - ), $this->spellchecker->check(SpellcheckFormatter::format($name))), + ), $this->spellchecker->check(SpellcheckFormatter::format($name), $file->getRealPath())), ]; } diff --git a/src/Config.php b/src/Config.php index 8861b098..7439f906 100644 --- a/src/Config.php +++ b/src/Config.php @@ -35,14 +35,20 @@ final class Config * * @param array $whitelistedWords * @param array $whitelistedPaths + * @param array> $fileSpecificIgnores */ public function __construct( public array $whitelistedWords = [], public array $whitelistedPaths = [], + public array $fileSpecificIgnores = [], public ?string $preset = null, public ?string $language = null, ) { $this->whitelistedWords = array_map(strtolower(...), $whitelistedWords); + $this->fileSpecificIgnores = array_map( + fn (array $words): array => array_map(strtolower(...), $words), + $fileSpecificIgnores + ); } /** @@ -96,7 +102,8 @@ public static function instance(): self * language?: string, * ignore?: array{ * words?: array, - * paths?: array + * paths?: array, + * files?: array> * } * } $jsonAsArray */ @@ -105,6 +112,7 @@ public static function instance(): self return self::$instance = new self( $jsonAsArray['ignore']['words'] ?? [], $jsonAsArray['ignore']['paths'] ?? [], + $jsonAsArray['ignore']['files'] ?? [], $jsonAsArray['preset'] ?? null, $jsonAsArray['language'] ?? null, ); @@ -152,14 +160,40 @@ public function ignoreWords(array $words): void } /** - * Checks if the word is ignored. + * Checks if the word is ignored globally or for a specific file. */ - public function isWordIgnored(string $word): bool + public function isWordIgnored(string $word, ?string $filePath = null): bool { - return in_array(strtolower($word), [ + $word = strtolower($word); + + // Check global ignores + $globalIgnores = [ ...$this->whitelistedWords, ...array_map(strtolower(...), PresetProvider::whitelistedWords($this->preset)), - ]); + ]; + + if (in_array($word, $globalIgnores)) { + return true; + } + + // Check file-specific ignores + if ($filePath !== null) { + $projectPath = ProjectPath::get(); + + // Normalize the file path to be relative to project root + $normalizedFilePath = $filePath; + if (str_starts_with($filePath, $projectPath.'/')) { + $normalizedFilePath = substr($filePath, strlen($projectPath) + 1); + } + + foreach ($this->fileSpecificIgnores as $path => $words) { + if ($normalizedFilePath === $path && in_array($word, $words)) { + return true; + } + } + } + + return false; } /** @@ -183,6 +217,7 @@ private function persist(): void 'ignore' => [ 'words' => $this->whitelistedWords, 'paths' => $this->whitelistedPaths, + 'files' => $this->fileSpecificIgnores, ], ], JSON_PRETTY_PRINT)); } diff --git a/src/Contracts/Services/Spellchecker.php b/src/Contracts/Services/Spellchecker.php index e530a369..98f44d57 100644 --- a/src/Contracts/Services/Spellchecker.php +++ b/src/Contracts/Services/Spellchecker.php @@ -16,5 +16,5 @@ interface Spellchecker * * @return array */ - public function check(string $text): array; + public function check(string $text, ?string $filePath = null): array; } diff --git a/src/Services/Spellcheckers/Aspell.php b/src/Services/Spellcheckers/Aspell.php index 8afbc668..ef5634c5 100644 --- a/src/Services/Spellcheckers/Aspell.php +++ b/src/Services/Spellcheckers/Aspell.php @@ -43,7 +43,7 @@ public static function default(): self * * @return array */ - public function check(string $text): array + public function check(string $text, ?string $filePath = null): array { /** @var array|null $misspellings */ $misspellings = $this->cache->has($text) ? $this->cache->get($text) : $this->getMisspellings($text); @@ -54,7 +54,7 @@ public function check(string $text): array return array_filter( $misspellings, - fn (Misspelling $misspelling): bool => ! $this->config->isWordIgnored($misspelling->word), + fn (Misspelling $misspelling): bool => ! $this->config->isWordIgnored($misspelling->word, $filePath), ); } diff --git a/tests/Unit/ConfigTest.php b/tests/Unit/ConfigTest.php index 722d9f3c..4cf4824b 100644 --- a/tests/Unit/ConfigTest.php +++ b/tests/Unit/ConfigTest.php @@ -12,7 +12,7 @@ 'php', ])->and($config->whitelistedPaths)->toBe([ 'tests', - ]); + ])->and($config->fileSpecificIgnores)->toBe([]); }); it('should to be a singleton', function (): void { @@ -30,7 +30,8 @@ $config = Config::instance(); expect($config->whitelistedWords)->toBe([]) - ->and($config->whitelistedPaths)->toBe([]); + ->and($config->whitelistedPaths)->toBe([]) + ->and($config->fileSpecificIgnores)->toBe([]); }); it('should be able to create a peck.json config file', function (): void { @@ -58,7 +59,7 @@ ]) ->and($config->whitelistedPaths)->toBe([ 'tests', - ]); + ])->and($config->fileSpecificIgnores)->toBe([]); }); describe('language', function (): void { diff --git a/tests/Unit/Services/AspellTest.php b/tests/Unit/Services/AspellTest.php index 5f66b057..e8147379 100644 --- a/tests/Unit/Services/AspellTest.php +++ b/tests/Unit/Services/AspellTest.php @@ -128,3 +128,53 @@ ->and($misspellings[0]->word)->toBe('Xxxxxxxxxxxxxxxxxx') ->and($misspellings[0]->suggestions)->toBeEmpty(); }); + +it('ignores words for specific files', function (): void { + $config = new Config([], [], [ + 'src/Test.php' => ['testword'], + 'tests/SomeTest.php' => ['unittest'], + ]); + $cache = new Cache(__DIR__.'/../../.peck-test.cache'); + $spellchecker = new Aspell($config, $cache); + + // Should ignore 'testword' in src/Test.php + $issues = $spellchecker->check('testword', 'src/Test.php'); + expect($issues)->toBeEmpty(); + + // Should not ignore 'testword' in other files + $issues = $spellchecker->check('testword', 'src/Other.php'); + expect($issues)->toHaveCount(1) + ->and($issues[0]->word)->toBe('testword'); + + // Should ignore 'unittest' in the specific test file + $issues = $spellchecker->check('unittest', 'tests/SomeTest.php'); + expect($issues)->toBeEmpty(); + + // Should not ignore 'unittest' in non-test files + $issues = $spellchecker->check('unittest', 'src/Service.php'); + expect($issues)->toHaveCount(1) + ->and($issues[0]->word)->toBe('unittest'); +}); + +it('combines global and file-specific ignores', function (): void { + $config = new Config(['globalword'], [], [ + 'src/Test.php' => ['specificword'], + ]); + $cache = new Cache(__DIR__.'/../../.peck-test.cache'); + $spellchecker = new Aspell($config, $cache); + + // Global word should be ignored everywhere + $issues = $spellchecker->check('globalword', 'src/Test.php'); + expect($issues)->toBeEmpty(); + + $issues = $spellchecker->check('globalword', 'src/Other.php'); + expect($issues)->toBeEmpty(); + + // File-specific word should only be ignored in that file + $issues = $spellchecker->check('specificword', 'src/Test.php'); + expect($issues)->toBeEmpty(); + + $issues = $spellchecker->check('specificword', 'src/Other.php'); + expect($issues)->toHaveCount(1) + ->and($issues[0]->word)->toBe('specificword'); +});