diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index 457c6fa..7c3cf37 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -13,9 +13,9 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.2 coverage: none - tools: php-cs-fixer:3.54.x, cs2pr + tools: php-cs-fixer:3.67.x, cs2pr - name: Restore PHP-CS-Fixer cache uses: actions/cache@v4 diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 855f53c..c7d6f2d 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ "8.1", "8.3" ] + php: [ "8.2", "8.4" ] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index c04172c..a4c3e4b 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -7,8 +7,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ["8.1", "8.2", "8.3"] - stability: [--prefer-lowest, --prefer-stable] + include: + - php: "8.4" + stability: --prefer-stable + - php: "8.2" + stability: --prefer-lowest env: PHP_VERSION: ${{ matrix.php }} DEPS_STRATEGY: ${{ matrix.stability }} @@ -46,7 +49,7 @@ jobs: - name: Run tests run: | - export WITH_COVERAGE=$(if [[ ("${{ matrix.php }}" = "8.3") && ("${{ matrix.stability }}" = "--prefer-stable") ]]; then echo "true"; else echo "false"; fi) + export WITH_COVERAGE=$(if [[ ("${{ matrix.php }}" = "8.4") && ("${{ matrix.stability }}" = "--prefer-stable") ]]; then echo "true"; else echo "false"; fi) echo "WITH_COVERAGE=${WITH_COVERAGE}" >> $GITHUB_ENV make vendor make tests diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index e57561f..a3e8f44 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -24,8 +24,11 @@ 'lowercase_cast' => true, 'method_chaining_indentation' => true, 'native_function_casing' => true, - 'native_function_invocation' => ['include' => ['@compiler_optimized']], - 'new_with_braces' => true, + 'native_function_invocation' => [ + 'include' => ['@compiler_optimized'], + 'strict' => false + ], + 'new_with_parentheses' => true, 'modernize_types_casting' => true, 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], 'no_empty_statement' => true, diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 54c9558..b5802ce 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -3,7 +3,7 @@ build: analysis: environment: php: - version: 8.1 + version: 8.4 cache: disabled: false directories: diff --git a/Makefile b/Makefile index 392853b..7316c21 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,13 @@ DOCKER_COMPOSE ?= docker compose -EXEC_PHP = $(DOCKER_COMPOSE) run --rm -T php -PHP_VERSION ?= 8.2 +EXEC_PHP = $(DOCKER_COMPOSE) run --rm php +PHP_VERSION ?= 8.4 DEPS_STRATEGY ?= --prefer-stable COMPOSER = $(EXEC_PHP) composer WITH_COVERAGE ?= "FALSE" EXAMPLES_DIR ?= "examples" pull: - @$(DOCKER_COMPOSE) pull languagetools jamspell php + @$(DOCKER_COMPOSE) pull languagetools jamspell build: $(DOCKER_COMPOSE) build --no-cache php @@ -25,13 +25,16 @@ setup: build .PHONY: build kill setup +PHPUNIT_FLAGS = $(if $(filter 8.4,$(PHP_VERSION)),--display-deprecations,) \ + $(if $(filter true,$(WITH_COVERAGE)),--coverage-clover clover.xml,) + tests: ## Run all tests tests: - if [ $(WITH_COVERAGE) = true ]; then $(EXEC_PHP) vendor/bin/phpunit --coverage-clover clover.xml; else $(EXEC_PHP) vendor/bin/phpunit; fi + $(EXEC_PHP) vendor/bin/phpunit $(PHPUNIT_FLAGS) tests-dox: ## Run all tests in dox format tests-dox: - if [ $(WITH_COVERAGE) = true ]; then $(EXEC_PHP) vendor/bin/phpunit --coverage-clover clover.xml --testdox; else $(EXEC_PHP) vendor/bin/phpunit --testdox; fi + $(EXEC_PHP) vendor/bin/phpunit $(PHPUNIT_FLAGS) --testdox # @TODO not optimized, it recreates a container for each example examples-test: @@ -45,11 +48,11 @@ examples-test: tu: ## Run unit tests tu: vendor - $(EXEC_PHP) vendor/bin/phpunit --exclude-group integration + $(EXEC_PHP) vendor/bin/phpunit --display-deprecations --exclude-group integration ti: ## Run functional tests ti: vendor - $(EXEC_PHP) vendor/bin/phpunit --group integration + $(EXEC_PHP) vendor/bin/phpunit --display-deprecations --group integration .PHONY: tests tests-dox examples-test tu ti @@ -59,12 +62,12 @@ vendor: PHP_CS_FIXER = docker-compose run --rm -T php tools/php-cs-fixer/vendor/bin/php-cs-fixer fix -vv --allow-risky=yes phpcs: - PHP_VERSION=8.1 $(EXEC_PHP) composer -d tools/php-cs-fixer update - PHP_VERSION=8.1 $(PHP_CS_FIXER) --dry-run + PHP_VERSION=8.2 $(EXEC_PHP) composer -d tools/php-cs-fixer update + PHP_VERSION=8.2 $(PHP_CS_FIXER) --dry-run phpcbf: - PHP_VERSION=8.1 $(EXEC_PHP) composer -d tools/php-cs-fixer update - PHP_VERSION=8.1 $(PHP_CS_FIXER) + PHP_VERSION=8.2 $(EXEC_PHP) composer -d tools/php-cs-fixer update + PHP_VERSION=8.2 $(PHP_CS_FIXER) phpstan: vendor $(EXEC_PHP) vendor/bin/phpstan analyse src -c phpstan.neon -a vendor/autoload.php diff --git a/composer.json b/composer.json index bdaf151..d1c4c88 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,7 @@ { "name": "tigitz/php-spellchecker", "type": "library", + "version": "0.8.0", "description": "Provides an easy way to spellcheck multiple text source by many spellcheckers, directly from PHP", "keywords": [ "spelling", @@ -21,11 +22,10 @@ } ], "require": { - "php": "^8.1", + "php": "^8.2", "nyholm/psr7": "^1.3", "psr/http-client": "^1.0", - "symfony/process": "^4.4.30 | ^5.0 |^6.0 | ^7.0", - "thecodingmachine/safe": "^1.0 | ^2.0", + "symfony/process": "^6.4 | ^7", "webmozart/assert": "^1.11" }, "require-dev": { @@ -33,16 +33,15 @@ "cocur/slugify": "^3.2 || ^4.0", "erusev/parsedown": "^1.7", "erusev/parsedown-extra": "^0.8", - "phpstan/phpstan": "^1.2.0", - "phpstan/phpstan-strict-rules": "^1.1.0", - "phpstan/phpstan-webmozart-assert": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.0.0", - "phpunit/phpunit": "^9.5", - "pixelrobin/php-feather": "^1.0", - "symfony/filesystem": "^4.4 || ^5.0 || ^6.0", - "symfony/finder": "^4.4 || ^5.0 || ^6.0", - "symfony/http-client": "^5.0 || ^6.0", - "thecodingmachine/phpstan-safe-rule": "^1.1" + "phpstan/phpstan": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpstan/phpstan-webmozart-assert": "^2", + "phpstan/phpstan-phpunit": "^2", + "phpunit/phpunit": "^11.0", + "pixelrobin/php-feather": "^2", + "symfony/filesystem": "^5 |^6 | ^7", + "symfony/finder": "^5 |^6 | ^7", + "symfony/http-client": "^5 |^6 | ^7" }, "suggest": { "symfony/http-client": "A PSR-18 Client implementation to use spellcheckers that relies on HTTP APIs" @@ -51,7 +50,7 @@ "psr-4": { "PhpSpellcheck\\": "src" }, - "files": [ "src/Text/functions.php" ] + "files": [ "src/Text/functions.php" , "src/Utils/php-functions.php" ] }, "autoload-dev": { "psr-4": { diff --git a/docker-compose.yml b/docker-compose.yml index 350e8c4..5fe6fe3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,10 @@ services: php: - image: tigitz/phpspellchecker:${PHP_VERSION:-8.1} + image: tigitz/phpspellchecker:${PHP_VERSION:-8.4} build: context: docker/php args: - PHP_VERSION: ${PHP_VERSION:-8.1} + PHP_VERSION: ${PHP_VERSION:-8.4} volumes: - .:/usr/src/myapp - ./cache:/root/composer/cache diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index 1194792..90dd230 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -26,16 +26,19 @@ RUN apt-get update \ aspell-en \ aspell-ru \ libpspell-dev -RUN set -eux; \ - case "$PHP_VERSION" in \ - 8.1*) pecl install xdebug-3.1.1;; \ - *) pecl install xdebug-3.3.2;; \ - esac -RUN docker-php-ext-configure pspell \ - && docker-php-ext-enable xdebug \ - && docker-php-ext-install pspell \ - && docker-php-ext-install zip \ - && rm -r /var/lib/apt/lists/* + +RUN pecl channel-update pecl.php.net && \ + pecl install xdebug-3.4.0 && \ + docker-php-ext-enable xdebug + +RUN if [ "${PHP_VERSION}" = "8.4" ]; then \ + pecl install pspell; \ + else \ + docker-php-ext-configure pspell && \ + docker-php-ext-install pspell; \ + fi && \ + docker-php-ext-enable pspell && \ + rm -r /var/lib/apt/lists/* RUN cp /usr/share/hunspell/en_US.aff /usr/share/hunspell/en_US.aff.orig \ && cp /usr/share/hunspell/en_US.dic /usr/share/hunspell/en_US.dic.orig \ diff --git a/docs/generate-docs.php b/docs/generate-docs.php index f268171..882a66b 100644 --- a/docs/generate-docs.php +++ b/docs/generate-docs.php @@ -107,11 +107,11 @@ } // Generate index.html doc file from the readme while stripping some sections -$readme = \Safe\file_get_contents(__DIR__.'/../README.md'); +$readme = \PhpSpellcheck\file_get_contents(__DIR__.'/../README.md'); -$readme = \Safe\preg_replace('/(# Install[\s\S]+?)^# /m', '# ', $readme); -$readme = \Safe\preg_replace('/(# Usage[\s\S]+?)^# /m', '# ', $readme); -$readme = \Safe\preg_replace('/(# Testing[\s\S]+?)^# /m', '# ', $readme); +$readme = \PhpSpellcheck\preg_replace('/(# Install[\s\S]+?)^# /m', '# ', $readme); +$readme = \PhpSpellcheck\preg_replace('/(# Usage[\s\S]+?)^# /m', '# ', $readme); +$readme = \PhpSpellcheck\preg_replace('/(# Testing[\s\S]+?)^# /m', '# ', $readme); $fs->dumpFile( __DIR__.'/index.html', diff --git a/phpstan.neon b/phpstan.neon index 9ce79cb..93705cf 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,6 @@ includes: - vendor/phpstan/phpstan-webmozart-assert/extension.neon - vendor/phpstan/phpstan-strict-rules/rules.neon - - vendor/thecodingmachine/phpstan-safe-rule/phpstan-safe-rule.neon - vendor/phpstan/phpstan-phpunit/extension.neon parameters: @@ -24,41 +23,19 @@ parameters: path: src/MisspellingHandler/MisspellingHandlerInterface.php - - message: "#^Function pspell_config_create is unsafe to use\\. It can return FALSE instead of throwing an exception\\. Please add 'use function Safe\\\\pspell_config_create;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library\\.$#" + message: '#^Default value of the parameter \#2 \$flags \(0\) of function PhpSpellcheck\\json_encode\(\) is incompatible with type int\<1, max\>\.$#' + identifier: parameter.defaultValue count: 1 - path: src/Spellchecker/PHPPspell.php + path: src/Utils/php-functions.php - - message: "#^Function pspell_config_ignore is unsafe to use\\. It can return FALSE instead of throwing an exception\\. Please add 'use function Safe\\\\pspell_config_ignore;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library\\.$#" + message: '#^Parameter \#3 \$depth of function json_decode expects int\<1, max\>, int given\.$#' + identifier: argument.type count: 1 - path: src/Spellchecker/PHPPspell.php + path: src/Utils/php-functions.php - - message: "#^Function pspell_config_mode is unsafe to use\\. It can return FALSE instead of throwing an exception\\. Please add 'use function Safe\\\\pspell_config_mode;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library\\.$#" + message: '#^Parameter \#3 \$depth of function json_encode expects int\<1, max\>, int given\.$#' + identifier: argument.type count: 1 - path: src/Spellchecker/PHPPspell.php - - - - message: "#^Function pspell_new_config is unsafe to use\\. It can return FALSE instead of throwing an exception\\. Please add 'use function Safe\\\\pspell_new_config;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library\\.$#" - count: 1 - path: src/Spellchecker/PHPPspell.php - - - - message: "#^Parameter \\#1 \\$dictionary of function pspell_check expects PSpell\\\\Dictionary, PSpell\\\\Dictionary\\|false given\\.$#" - count: 1 - path: src/Spellchecker/PHPPspell.php - - - - message: "#^Parameter \\#1 \\$dictionary of function pspell_check expects PSpell\\\\Dictionary, int given\\.$#" - count: 1 - path: src/Spellchecker/PHPPspell.php - - - - message: "#^Parameter \\#1 \\$dictionary of function pspell_suggest expects PSpell\\\\Dictionary, PSpell\\\\Dictionary\\|false given\\.$#" - count: 1 - path: src/Spellchecker/PHPPspell.php - - - - message: "#^Parameter \\#1 \\$dictionary of function pspell_suggest expects PSpell\\\\Dictionary, int given\\.$#" - count: 1 - path: src/Spellchecker/PHPPspell.php + path: src/Utils/php-functions.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ecacd03..3a0995c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,23 +2,16 @@ - - - src/ - - @@ -35,4 +28,10 @@ tests + + + + src + + diff --git a/src/Exception/FilesystemException.php b/src/Exception/FilesystemException.php new file mode 100644 index 0000000..372f0d1 --- /dev/null +++ b/src/Exception/FilesystemException.php @@ -0,0 +1,15 @@ + 'PREG_INTERNAL_ERROR: Internal error', + PREG_BACKTRACK_LIMIT_ERROR => 'PREG_BACKTRACK_LIMIT_ERROR: Backtrack limit reached', + PREG_RECURSION_LIMIT_ERROR => 'PREG_RECURSION_LIMIT_ERROR: Recursion limit reached', + PREG_BAD_UTF8_ERROR => 'PREG_BAD_UTF8_ERROR: Invalid UTF8 character', + PREG_BAD_UTF8_OFFSET_ERROR => 'PREG_BAD_UTF8_OFFSET_ERROR', + PREG_JIT_STACKLIMIT_ERROR => 'PREG_JIT_STACKLIMIT_ERROR', + ]; + $errMsg = $errorMap[preg_last_error()] ?? 'Unknown PCRE error: '.preg_last_error(); + + return new self($errMsg, preg_last_error()); + } +} diff --git a/src/Exception/ProcessFailedException.php b/src/Exception/ProcessFailedException.php index d60e438..7419f7e 100644 --- a/src/Exception/ProcessFailedException.php +++ b/src/Exception/ProcessFailedException.php @@ -15,13 +15,13 @@ class ProcessFailedException extends \RuntimeException implements ExceptionInter public function __construct( Process $process, - \Throwable $previous = null, + ?\Throwable $previous = null, string $failureReason = '', int $code = 0 ) { $this->process = $process; - $message = \Safe\sprintf( + $message = \sprintf( 'Process with command "%s" has failed%s with exit code %d(%s)%s', $process->getCommandLine(), $process->isStarted() ? ' running' : '', diff --git a/src/Exception/ProcessHasErrorOutputException.php b/src/Exception/ProcessHasErrorOutputException.php index 7f34303..e2642a7 100644 --- a/src/Exception/ProcessHasErrorOutputException.php +++ b/src/Exception/ProcessHasErrorOutputException.php @@ -20,7 +20,7 @@ public function __construct(string $errorOutput, string $parsedText, string $com MSG; parent::__construct( - \Safe\sprintf( + \sprintf( $exceptionTemplateMessage, $errorOutput, $command, diff --git a/src/Exception/PspellException.php b/src/Exception/PspellException.php new file mode 100644 index 0000000..235d8a3 --- /dev/null +++ b/src/Exception/PspellException.php @@ -0,0 +1,15 @@ +suggestions); + $existingSuggestionsAsKeys = array_flip($this->suggestions); foreach ($suggestionsToAdd as $suggestionToAdd) { if (!isset($existingSuggestionsAsKeys[$suggestionToAdd])) { $this->suggestions[] = $suggestionToAdd; diff --git a/src/MisspellingHandler/EchoHandler.php b/src/MisspellingHandler/EchoHandler.php index a28b1f4..bb9fe32 100644 --- a/src/MisspellingHandler/EchoHandler.php +++ b/src/MisspellingHandler/EchoHandler.php @@ -14,13 +14,13 @@ class EchoHandler implements MisspellingHandlerInterface public function handle(iterable $misspellings): void { foreach ($misspellings as $misspelling) { - $output = sprintf( + $output = \sprintf( 'word: %s | line: %d | offset: %d | suggestions: %s | context: %s' . PHP_EOL, $misspelling->getWord(), $misspelling->getLineNumber(), $misspelling->getOffset(), $misspelling->hasSuggestions() ? implode(',', $misspelling->getSuggestions()) : '', - \Safe\json_encode($misspelling->getContext()) + \PhpSpellcheck\json_encode($misspelling->getContext()) ); echo $output; diff --git a/src/Source/Directory.php b/src/Source/Directory.php index 2206e65..2170fc2 100644 --- a/src/Source/Directory.php +++ b/src/Source/Directory.php @@ -62,7 +62,7 @@ private function getContents(): iterable // When regex pattern is used, an array containing the file path in its first element is returned $file = new \SplFileInfo(current($file)); } else { - throw new RuntimeException(\Safe\sprintf('Couldn\'t create "%s" object from the given file', \SplFileInfo::class)); + throw new RuntimeException(\sprintf('Couldn\'t create "%s" object from the given file', \SplFileInfo::class)); } if (!$file->isDir() && $file->getRealPath() !== false) { diff --git a/src/Source/File.php b/src/Source/File.php index f094a4e..71c8066 100644 --- a/src/Source/File.php +++ b/src/Source/File.php @@ -25,13 +25,13 @@ public function __construct(string $filePath) */ public function toTexts(array $context = []): iterable { - $context['filePath'] = \Safe\realpath($this->filePath); + $context['filePath'] = \PhpSpellcheck\realpath($this->filePath); yield new Text($this->getFileContent(), $context); } private function getFileContent(): string { - return \Safe\file_get_contents($this->filePath); + return \PhpSpellcheck\file_get_contents($this->filePath); } } diff --git a/src/Spellchecker/Aspell.php b/src/Spellchecker/Aspell.php index 70aead7..dc1591a 100644 --- a/src/Spellchecker/Aspell.php +++ b/src/Spellchecker/Aspell.php @@ -69,7 +69,7 @@ public function getSupportedLanguages(): iterable $languages[$name] = true; } $languages = array_keys($languages); - \Safe\sort($languages); + sort($languages); return $languages; } diff --git a/src/Spellchecker/Hunspell.php b/src/Spellchecker/Hunspell.php index 7b0cc7e..7ced24e 100644 --- a/src/Spellchecker/Hunspell.php +++ b/src/Spellchecker/Hunspell.php @@ -68,7 +68,7 @@ public function getSupportedLanguages(): iterable foreach ($output as $line) { $line = trim($line); if ('' === $line // Skip empty lines - || \Safe\substr($line, -1) === ':' // Skip headers + || substr($line, -1) === ':' // Skip headers || strpos($line, ':') !== false // Skip search path ) { continue; @@ -78,11 +78,13 @@ public function getSupportedLanguages(): iterable // Skip MySpell hyphen files continue; } - $name = \Safe\preg_replace('/\.(aff|dic)$/', '', $name); - $languages[$name] = true; + $name = \PhpSpellcheck\preg_replace('/\.(aff|dic)$/', '', $name); + if (\is_string($name)) { + $languages[$name] = true; + } } $languages = array_keys($languages); - \Safe\sort($languages); + sort($languages); return $languages; } diff --git a/src/Spellchecker/Ispell.php b/src/Spellchecker/Ispell.php index f235155..ae459b2 100644 --- a/src/Spellchecker/Ispell.php +++ b/src/Spellchecker/Ispell.php @@ -101,7 +101,7 @@ public function getSupportedLanguages(): iterable continue; } - yield \Safe\substr($file, 0, -4); + yield substr($file, 0, -4); } } diff --git a/src/Spellchecker/JamSpell.php b/src/Spellchecker/JamSpell.php index e195564..342eea4 100644 --- a/src/Spellchecker/JamSpell.php +++ b/src/Spellchecker/JamSpell.php @@ -10,7 +10,6 @@ use PhpSpellcheck\Misspelling; use PhpSpellcheck\Utils\LineAndOffset; use Psr\Http\Client\ClientInterface; -use Webmozart\Assert\Assert; class JamSpell implements SpellcheckerInterface { @@ -36,15 +35,15 @@ public function check(string $text, array $languages, array $context): iterable ->createRequest('POST', $this->endpoint) ->withBody(Stream::create($text)); - $spellcheckResponseAsArray = \Safe\json_decode($spellcheckResponse = $this->httpClient->sendRequest($request)->getBody()->getContents(), true); - Assert::isArray($spellcheckResponseAsArray); + /** @var array{results: array} $spellcheckResponseAsArray */ + $spellcheckResponseAsArray = json_decode($spellcheckResponse = $this->httpClient->sendRequest($request)->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); - // @TODO use json api validation schema if (!isset($spellcheckResponseAsArray['results'])) { throw new RuntimeException('Jamspell spellcheck HTTP response must include a "results" key. Response given: "'.$spellcheckResponse.'"'); } foreach ($spellcheckResponseAsArray['results'] as $result) { + [$line, $offset] = LineAndOffset::findFromFirstCharacterOffset($text, $result['pos_from']); yield new Misspelling( diff --git a/src/Spellchecker/LanguageTool.php b/src/Spellchecker/LanguageTool.php index 4b4dd17..680e371 100644 --- a/src/Spellchecker/LanguageTool.php +++ b/src/Spellchecker/LanguageTool.php @@ -42,6 +42,15 @@ public function check( } $check = $this->apiClient->spellCheck($text, $languages, $options ?? []); + /** @var array{matches: list, + * sentence: string, + * message: string, + * rule: string + * }>} $check */ + if (!\is_array($check['matches'])) { throw new RuntimeException('LanguageTool spellcheck response must contain a "matches" array'); } diff --git a/src/Spellchecker/LanguageTool/LanguageToolApiClient.php b/src/Spellchecker/LanguageTool/LanguageToolApiClient.php index 345be65..2da4fb5 100644 --- a/src/Spellchecker/LanguageTool/LanguageToolApiClient.php +++ b/src/Spellchecker/LanguageTool/LanguageToolApiClient.php @@ -23,7 +23,14 @@ public function __construct(string $baseUrl) * @param array $languages * @param array $options * - * @return array + * @return array{matches: array, + * sentence: string, + * message: string, + * rule: string + * }>} */ public function spellCheck(string $text, array $languages, array $options): array { @@ -34,6 +41,7 @@ public function spellCheck(string $text, array $languages, array $options): arra $options['altLanguages'] = implode(',', $languages); } + /** @var array{matches: array, sentence: string, message: string, rule: string}>} */ return $this->requestAPI( '/v2/check', 'POST', @@ -47,14 +55,15 @@ public function spellCheck(string $text, array $languages, array $options): arra */ public function getSupportedLanguages(): array { - return array_column( + /** @var array */ + return array_values(array_unique(array_column( $this->requestAPI( '/v2/languages', 'GET', 'Accept: application/json' ), 'longCode' - ); + ))); } /** @@ -75,9 +84,9 @@ public function requestAPI(string $endpoint, string $method, string $header, arr $httpData['content'] = http_build_query($queryParams); } - $content = \Safe\file_get_contents($this->baseUrl . $endpoint, false, stream_context_create(['http' => $httpData])); + $content = \PhpSpellcheck\file_get_contents($this->baseUrl . $endpoint, false, stream_context_create(['http' => $httpData])); /** @var array $contentAsArray */ - $contentAsArray = \Safe\json_decode($content, true); + $contentAsArray = \PhpSpellcheck\json_decode($content, true); return $contentAsArray; } diff --git a/src/Spellchecker/PHPPspell.php b/src/Spellchecker/PHPPspell.php index e72eb5f..4ea4ea3 100644 --- a/src/Spellchecker/PHPPspell.php +++ b/src/Spellchecker/PHPPspell.php @@ -6,7 +6,6 @@ use PhpSpellcheck\Exception\RuntimeException; use PhpSpellcheck\Misspelling; -use PhpSpellcheck\MisspellingInterface; use Webmozart\Assert\Assert; class PHPPspell implements SpellcheckerInterface @@ -37,7 +36,7 @@ class PHPPspell implements SpellcheckerInterface public function __construct( ?int $mode = null, int $numberOfCharactersLowerLimit = 0, - Aspell $aspell = null + ?Aspell $aspell = null ) { if (!\extension_loaded('pspell')) { throw new RuntimeException('Pspell extension must be loaded to use the PHPPspell spellchecker'); @@ -61,52 +60,26 @@ public function check( string $text, array $languages, array $context - ): iterable { - if (PHP_VERSION_ID < 80100) { - return $this->checkBefore81($text, $languages, $context); - } - - return $this->checkAfter81($text, $languages, $context); - } - - /** - * {@inheritdoc} - */ - public function getSupportedLanguages(): iterable - { - return $this->aspell->getSupportedLanguages(); - } - - /** - * @param array $context - * @param array $languages - * - * @return iterable - */ - private function checkBefore81( - string $text, - array $languages, - array $context ): iterable { Assert::count($languages, 1, 'PHPPspell spellchecker doesn\'t support multi-language check'); $chosenLanguage = current($languages); - $pspellConfig = \Safe\pspell_config_create(current($languages)); - \Safe\pspell_config_mode($pspellConfig, $this->mode); - \Safe\pspell_config_ignore($pspellConfig, $this->numberOfCharactersLowerLimit); - $dictionary = \Safe\pspell_new_config($pspellConfig); + $pspellConfig = pspell_config_create($chosenLanguage); + pspell_config_mode($pspellConfig, $this->mode); + pspell_config_ignore($pspellConfig, $this->numberOfCharactersLowerLimit); + $dictionary = \PhpSpellcheck\pspell_new_config($pspellConfig); $lines = explode(PHP_EOL, $text); /** @var string $line */ foreach ($lines as $lineNumber => $line) { - $words = explode(' ', \Safe\preg_replace("/(?!['’-])(\p{P}|\+|--)/u", '', $line)); + $words = explode(' ', \PhpSpellcheck\preg_replace("/(?!['’-])(\p{P}|\+|--)/u", '', $line)); foreach ($words as $word) { if (!pspell_check($dictionary, $word)) { $suggestions = pspell_suggest($dictionary, $word); Assert::isArray( $suggestions, - \Safe\sprintf('pspell_suggest method failed with language "%s" and word "%s"', $chosenLanguage, $word) + \sprintf('pspell_suggest method failed with language "%s" and word "%s"', $chosenLanguage, $word) ); yield new Misspelling($word, 0, $lineNumber + 1, $suggestions, $context); @@ -116,40 +89,10 @@ private function checkBefore81( } /** - * @param array $context - * @param array $languages - * - * @return iterable + * {@inheritdoc} */ - private function checkAfter81( - string $text, - array $languages, - array $context - ): iterable { - Assert::count($languages, 1, 'PHPPspell spellchecker doesn\'t support multi-language check'); - - $chosenLanguage = current($languages); - $pspellConfig = pspell_config_create($chosenLanguage); - pspell_config_mode($pspellConfig, $this->mode); - pspell_config_ignore($pspellConfig, $this->numberOfCharactersLowerLimit); - $dictionary = pspell_new_config($pspellConfig); - - $lines = explode(PHP_EOL, $text); - - /** @var string $line */ - foreach ($lines as $lineNumber => $line) { - $words = explode(' ', \Safe\preg_replace("/(?!['’-])(\p{P}|\+|--)/u", '', $line)); - foreach ($words as $word) { - if (!pspell_check($dictionary, $word)) { - $suggestions = pspell_suggest($dictionary, $word); - Assert::isArray( - $suggestions, - \Safe\sprintf('pspell_suggest method failed with language "%s" and word "%s"', $chosenLanguage, $word) - ); - - yield new Misspelling($word, 0, $lineNumber + 1, $suggestions, $context); - } - } - } + public function getSupportedLanguages(): iterable + { + return $this->aspell->getSupportedLanguages(); } } diff --git a/src/TextProcessor/MarkdownRemover.php b/src/TextProcessor/MarkdownRemover.php index 73e13ca..0d850e5 100644 --- a/src/TextProcessor/MarkdownRemover.php +++ b/src/TextProcessor/MarkdownRemover.php @@ -17,53 +17,53 @@ class MarkdownRemover implements TextProcessorInterface public function process(TextInterface $text): TextInterface { // Horizontal rules (stripListHeaders conflict with this rule, which is why it has been moved to the top) - $output = \Safe\preg_replace('/^(-\s*?|\*\s*?|_\s*?){3,}(\s*)$/m', PHP_EOL . '$2', $text->getContent()); + $output = \PhpSpellcheck\preg_replace('/^(-\s*?|\*\s*?|_\s*?){3,}(\s*)$/m', PHP_EOL . '$2', $text->getContent()); // Github Flavored Markdown // Header - $output = \Safe\preg_replace('/\n={2,}/', '\n', $output); + $output = \PhpSpellcheck\preg_replace('/\n={2,}/', '\n', $output); /** * Fenced codeblocks. * *@TODO parse programming language comments from codeblock instead of removing whole block */ - $output = \Safe\preg_replace('/~{3}.*\n/', '', $output); + $output = \PhpSpellcheck\preg_replace('/~{3}.*\n/', '', $output); // Strikethrough - $output = \Safe\preg_replace('/~~/', '', $output); + $output = \PhpSpellcheck\preg_replace('/~~/', '', $output); // Common Markdown // Remove HTML tags - $output = \Safe\preg_replace('/<[^>]*>/', '', $output); + $output = \PhpSpellcheck\preg_replace('/<[^>]*>/', '', $output); // Remove setext-style headers - $output = \Safe\preg_replace('/^[=\-]{2,}\s*$/', '', $output); + $output = \PhpSpellcheck\preg_replace('/^[=\-]{2,}\s*$/', '', $output); // Remove footnotes? - $output = \Safe\preg_replace('/\[\^.+?\](\: .*?$)?/', '', $output); - $output = \Safe\preg_replace('/\s{0,2}\[.*?\]: .*?$/', '', $output); + $output = \PhpSpellcheck\preg_replace('/\[\^.+?\](\: .*?$)?/', '', $output); + $output = \PhpSpellcheck\preg_replace('/\s{0,2}\[.*?\]: .*?$/', '', $output); // Remove images - $output = \Safe\preg_replace('/\!\[(.*?)\][\[\(].*?[\]\)]/', '$1', $output); + $output = \PhpSpellcheck\preg_replace('/\!\[(.*?)\][\[\(].*?[\]\)]/', '$1', $output); // Remove inline links - $output = \Safe\preg_replace('/\[(.*?)\][\[\(].*?[\]\)]/', '$1', $output); + $output = \PhpSpellcheck\preg_replace('/\[(.*?)\][\[\(].*?[\]\)]/', '$1', $output); // Remove blockquotes - $output = \Safe\preg_replace('/^\s{0,3}>\s?/', '', $output); + $output = \PhpSpellcheck\preg_replace('/^\s{0,3}>\s?/', '', $output); // Remove reference-style links? - $output = \Safe\preg_replace('/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/', '', $output); + $output = \PhpSpellcheck\preg_replace('/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/', '', $output); /** * Remove atx-style headers. * *@TODO find a way to merge the two regex below * remove ## Heading ## */ - $output = \Safe\preg_replace('/^#{1,6}\s+(.*)(\s+#{1,6})$/m', '$1', $output); + $output = \PhpSpellcheck\preg_replace('/^#{1,6}\s+(.*)(\s+#{1,6})$/m', '$1', $output); // remove ## Heading - $output = \Safe\preg_replace('/^#{1,6}\s+(.*)$/m', '$1', $output); + $output = \PhpSpellcheck\preg_replace('/^#{1,6}\s+(.*)$/m', '$1', $output); // Remove emphasis (repeat the line to remove double emphasis) - $output = \Safe\preg_replace('/([\*_]{1,3})(\S.*?\S{0,1})\1/', '$2', $output); - $output = \Safe\preg_replace('/([\*_]{1,3})(\S.*?\S{0,1})\1/', '$2', $output); + $output = \PhpSpellcheck\preg_replace('/([\*_]{1,3})(\S.*?\S{0,1})\1/', '$2', $output); + $output = \PhpSpellcheck\preg_replace('/([\*_]{1,3})(\S.*?\S{0,1})\1/', '$2', $output); // Remove list items - $output = \Safe\preg_replace('/^([^\S\r\n]*)\*\s/m', '$1', $output); + $output = \PhpSpellcheck\preg_replace('/^([^\S\r\n]*)\*\s/m', '$1', $output); // Remove code blocks - $output = \Safe\preg_replace('/^`{3,}(.*)*$/m', '', $output); + $output = \PhpSpellcheck\preg_replace('/^`{3,}(.*)*$/m', '', $output); // Remove inline code - $output = \Safe\preg_replace('/`(.+?)`/', '$1', $output); + $output = \PhpSpellcheck\preg_replace('/`(.+?)`/', '$1', $output); return $text->replaceContent($output); } diff --git a/src/Utils/CommandLine.php b/src/Utils/CommandLine.php index ca1109b..375ea67 100644 --- a/src/Utils/CommandLine.php +++ b/src/Utils/CommandLine.php @@ -24,7 +24,7 @@ public function __construct($command) $this->commandArgs = [$command]; } else { throw new InvalidArgumentException( - \Safe\sprintf( + \sprintf( 'Command should be an "array" or a "string", "%s" given', \is_object($command) ? \get_class($command) : \gettype($command) ) @@ -81,10 +81,10 @@ private function escapeArgument(string $argument): string if (false !== strpos($argument, "\0")) { $argument = str_replace("\0", '?', $argument); } - if (\Safe\preg_match('/[\/()%!^"<>&|\s]/', $argument) !== 0) { + if (\PhpSpellcheck\preg_match('/[\/()%!^"<>&|\s]/', $argument) !== 0) { return $argument; } - $argument = \Safe\preg_replace('/(\\\\+)$/', '$1$1', $argument); + $argument = \PhpSpellcheck\preg_replace('/(\\\\+)$/', '$1$1', $argument); return '"' . str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument) . '"'; } diff --git a/src/Utils/IspellParser.php b/src/Utils/IspellParser.php index 304155e..bdd56e5 100644 --- a/src/Utils/IspellParser.php +++ b/src/Utils/IspellParser.php @@ -73,6 +73,6 @@ public static function parseMisspellingsFromOutput(string $output, array $contex */ public static function adaptInputForTerseModeProcessing(string $input): string { - return \Safe\preg_replace('/^/m', '^', $input); + return \PhpSpellcheck\preg_replace('/^/m', '^', $input); } } diff --git a/src/Utils/LineAndOffset.php b/src/Utils/LineAndOffset.php index 984cd96..c8dc763 100644 --- a/src/Utils/LineAndOffset.php +++ b/src/Utils/LineAndOffset.php @@ -21,17 +21,17 @@ class LineAndOffset public static function findFromFirstCharacterOffset(string $text, int $offsetFromFirstCharacter): array { // positive offset - Assert::greaterThanEq($offsetFromFirstCharacter, 0, \Safe\sprintf('Offset must be a positive integer, "%s" given', $offsetFromFirstCharacter)); + Assert::greaterThanEq($offsetFromFirstCharacter, 0, \sprintf('Offset must be a positive integer, "%s" given', $offsetFromFirstCharacter)); $textLength = mb_strlen($text); if ($textLength < $offsetFromFirstCharacter) { throw new InvalidArgumentException( - \Safe\sprintf('Offset given "%d" is higher than the string length "%d"', $offsetFromFirstCharacter, $textLength) + \sprintf('Offset given "%d" is higher than the string length "%d"', $offsetFromFirstCharacter, $textLength) ); } $textBeforeOffset = mb_substr($text, 0, $offsetFromFirstCharacter); - $line = ((int) \Safe\preg_match_all('/\R/u', $textBeforeOffset, $matches)) + 1; + $line = (\PhpSpellcheck\preg_match_all('/\R/u', $textBeforeOffset, $matches)) + 1; $offsetOfPreviousLinebreak = mb_strrpos($textBeforeOffset, PHP_EOL, 0); $offset = $offsetFromFirstCharacter - ($offsetOfPreviousLinebreak !== false ? $offsetOfPreviousLinebreak + 1 : 0); diff --git a/src/Utils/ProcessRunner.php b/src/Utils/ProcessRunner.php index 9161367..b220e51 100644 --- a/src/Utils/ProcessRunner.php +++ b/src/Utils/ProcessRunner.php @@ -14,7 +14,7 @@ class ProcessRunner * @param float|int|null $timeout The timeout in seconds * @param array $env */ - public static function run(Process $process, $timeout = null, callable $callback = null, array $env = []): Process + public static function run(Process $process, float|int|null $timeout = null, ?callable $callback = null, array $env = []): Process { $process->setTimeout($timeout); diff --git a/src/Utils/php-functions.php b/src/Utils/php-functions.php new file mode 100644 index 0000000..d538f7a --- /dev/null +++ b/src/Utils/php-functions.php @@ -0,0 +1,191 @@ +> + * : (TFlags is 2 + * ? list> + * : (TFlags is 256|257 + * ? array> + * : (TFlags is 258 + * ? list> + * : (TFlags is 512|513 + * ? array> + * : (TFlags is 514 + * ? list> + * : (TFlags is 770 + * ? list> + * : (TFlags is 0 ? array> : array) + * ) + * ) + * ) + * ) + * ) + * ) + * ) $matches + */ +function preg_match_all(string $pattern, string $subject, &$matches = [], int $flags = 1, int $offset = 0): int +{ + error_clear_last(); + $safeResult = \preg_match_all($pattern, $subject, $matches, $flags, $offset); + if ($safeResult === false) { + throw PcreException::createFromPhpError(); + } + + return $safeResult; +} + +/** + * @param string|string[] $pattern + * @param array|string $replacement + * @param array|string $subject + * + * @param-out 0|positive-int $count + * + * @return ($subject is array ? list : string) + */ +function preg_replace(array|string $pattern, array|string $replacement, array|string $subject, int $limit = -1, ?int &$count = null): array|string +{ + error_clear_last(); + $result = \preg_replace($pattern, $replacement, $subject, $limit, $count); + if (preg_last_error() !== PREG_NO_ERROR || $result === null) { + throw PcreException::createFromPhpError(); + } + + return $result; +} + +/** + * @template TFlags as int-mask<0, 256, 512> + * + * @param mixed $matches + * @param TFlags $flags + * + * @param-out ( + * TFlags is 256 + * ? array + * : (TFlags is 512 + * ? array + * : (TFlags is 768 + * ? array + * : array + * ) + * ) + * ) $matches + * + * @return 0|1 + */ +function preg_match(string $pattern, string $subject, &$matches = [], int $flags = 0, int $offset = 0) +{ + error_clear_last(); + $safeResult = \preg_match($pattern, $subject, $matches, $flags, $offset); + if ($safeResult === false) { + throw PcreException::createFromPhpError(); + } + + return $safeResult; +} + +/** + * @param ?resource $context + * @param int<0, max>|null $length + */ +function file_get_contents(string $filename, bool $use_include_path = false, $context = null, int $offset = 0, ?int $length = null): string +{ + error_clear_last(); + if ($length !== null) { + $safeResult = \file_get_contents($filename, $use_include_path, $context, $offset, $length); + } elseif ($offset !== 0) { + $safeResult = \file_get_contents($filename, $use_include_path, $context, $offset); + } elseif ($context !== null) { + $safeResult = \file_get_contents($filename, $use_include_path, $context); + } else { + $safeResult = \file_get_contents($filename, $use_include_path); + } + if ($safeResult === false) { + throw FilesystemException::createFromPhpError(); + } + + return $safeResult; +} + +/** + * @param ?resource $context + */ +function file_put_contents(string $filename, mixed $data, int $flags = 0, $context = null): int +{ + error_clear_last(); + if ($context !== null) { + $safeResult = \file_put_contents($filename, $data, $flags, $context); + } else { + $safeResult = \file_put_contents($filename, $data, $flags); + } + if ($safeResult === false) { + throw FilesystemException::createFromPhpError(); + } + + return $safeResult; +} + +function realpath(string $path): string +{ + error_clear_last(); + $safeResult = \realpath($path); + if ($safeResult === false) { + throw FilesystemException::createFromPhpError(); + } + + return $safeResult; +} + +/** + * @param int<1, max> $flags + */ +function json_encode(mixed $value, int $flags = 0, int $depth = 512): string +{ + error_clear_last(); + $safeResult = \json_encode($value, $flags, $depth); + if ($safeResult === false) { + throw JsonException::createFromPhpError(); + } + + return $safeResult; +} + +function json_decode(string $json, bool $assoc = false, int $depth = 512, int $flags = 0): mixed +{ + $data = \json_decode($json, $assoc, $depth, $flags); + if (JSON_ERROR_NONE !== json_last_error()) { + throw JsonException::createFromPhpError(); + } + + return $data; +} + +function pspell_new_config(Config $config): Dictionary +{ + error_clear_last(); + $result = \pspell_new_config($config); + if ($result === false) { + throw PspellException::createFromPhpError(); + } + + return $result; +} diff --git a/tests/MisspellingTest.php b/tests/MisspellingTest.php index b76567e..6bc7fe4 100644 --- a/tests/MisspellingTest.php +++ b/tests/MisspellingTest.php @@ -24,7 +24,7 @@ public function testCanDeterminateUniqueIdentity(Misspelling $misspelling): void $this->assertFalse($misspelling->canDeterminateUniqueIdentity()); } - public function nonDeterminableUniqueIdentityMisspellings(): array + public static function nonDeterminableUniqueIdentityMisspellings(): array { return [ [new Misspelling('mispelled')], diff --git a/tests/Source/FileTest.php b/tests/Source/FileTest.php index 04b93c2..db58420 100644 --- a/tests/Source/FileTest.php +++ b/tests/Source/FileTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); +use PhpSpellcheck\Exception\FilesystemException; use PhpSpellcheck\Source\File; use PHPUnit\Framework\TestCase; -use Safe\Exceptions\FilesystemException; use function PhpSpellcheck\t; diff --git a/tests/Spellchecker/AspellTest.php b/tests/Spellchecker/AspellTest.php index 38d9269..4df8404 100644 --- a/tests/Spellchecker/AspellTest.php +++ b/tests/Spellchecker/AspellTest.php @@ -55,7 +55,7 @@ public function testGetSupportedLanguagesFromRealBinaries(): void public function getFakeDicts(): array { - return explode(PHP_EOL, \Safe\file_get_contents(__DIR__ . '/../Fixtures/Aspell/dicts.txt')); + return explode(PHP_EOL, \PhpSpellcheck\file_get_contents(__DIR__ . '/../Fixtures/Aspell/dicts.txt')); } public function assertWorkingSupportedLanguages(string $binaries): void diff --git a/tests/Spellchecker/HunspellTest.php b/tests/Spellchecker/HunspellTest.php index 2e719b1..a356d73 100644 --- a/tests/Spellchecker/HunspellTest.php +++ b/tests/Spellchecker/HunspellTest.php @@ -65,7 +65,7 @@ public function testGetSupportedLanguagesFromRealBinaries(): void public function getFakeDicts(): array { - return explode(PHP_EOL, \Safe\file_get_contents(__DIR__ . '/../Fixtures/Hunspell/dicts.txt')); + return explode(PHP_EOL, \PhpSpellcheck\file_get_contents(__DIR__ . '/../Fixtures/Hunspell/dicts.txt')); } /** diff --git a/tests/Spellchecker/IspellTest.php b/tests/Spellchecker/IspellTest.php index 33d2e5b..bb416c5 100644 --- a/tests/Spellchecker/IspellTest.php +++ b/tests/Spellchecker/IspellTest.php @@ -55,10 +55,10 @@ public function getTextInput(): string public function getFakeDicts(): array { - return explode(PHP_EOL, \Safe\file_get_contents(__DIR__ . '/../Fixtures/Ispell/dicts.txt')); + return explode(PHP_EOL, \PhpSpellcheck\file_get_contents(__DIR__ . '/../Fixtures/Ispell/dicts.txt')); } - public function assertWorkingSupportedLanguages(string $binaries, string $shellEntryPoint = null): void + public function assertWorkingSupportedLanguages(string $binaries, ?string $shellEntryPoint = null): void { $ispell = new Ispell( new CommandLine($binaries), diff --git a/tests/bootstrap.php b/tests/bootstrap.php index a5c26bd..a579b48 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -10,12 +10,12 @@ ]; foreach ($dependencies as $dependency) { - echo sprintf('Waiting "%s" dependency'.PHP_EOL, $dependency); + echo \sprintf('Waiting "%s" dependency'.PHP_EOL, $dependency); for (; ;) { $url = parse_url(getenv($dependency)); if ($socket = @fsockopen($url['host'], $url['port'])) { - echo sprintf('"%s" dependency is up'.PHP_EOL, $dependency).PHP_EOL; + echo \sprintf('"%s" dependency is up'.PHP_EOL, $dependency).PHP_EOL; fclose($socket); break; diff --git a/tools/php-cs-fixer/composer.json b/tools/php-cs-fixer/composer.json index 927b1cc..c8a7807 100644 --- a/tools/php-cs-fixer/composer.json +++ b/tools/php-cs-fixer/composer.json @@ -1,6 +1,6 @@ { "require": { - "php": "^8.1", - "friendsofphp/php-cs-fixer": "^v3.54.0" + "php": "^8.2", + "friendsofphp/php-cs-fixer": "^v3.67.0" } }