diff --git a/CHANGELOG.md b/CHANGELOG.md index 76dedbb..803d771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - feat[!!!]: skip KeyNamingConventionValidator by default (opt-in to reduce false positives) - feat: provide a standalone PHAR distribution for CI and non-Composer projects +- perf: minor render and file-collection optimizations +- perf: dedupe render keys via hash set instead of in_array +- perf: memoize placeholder extraction per value +- perf: memoize segment convention detection +- perf: return bool from validator validate() instead of building issue arrays +- perf: reuse parser-read file content in encoding validation +- perf: scan each directory once and bucket files by parser +- perf: evict parser cache per file set to bound memory +- perf: cache validator severity and drop per-comparison reflection +- perf: memoize extractKeys() results in parsers +- perf: cache prepared XLIFF schema source per version +- perf: resolve XLIFF content via a memoized id map +- fix: reject DOCTYPE and disable network access in XLIFF parser to harden against XXE and entity-expansion attacks +- fix: skip translation files exceeding a size limit to prevent memory exhaustion +- fix: strip control characters and escape markup in CLI output to prevent terminal injection +- fix: harden system path safety check with boundary matching +- fix: do not follow symlinks during recursive file scan +- fix: reject invalid validator classes in --only/--skip options instead of silently ignoring them +- fix: exclude PHP config files from auto-detection to avoid accidental code execution +- fix: parse PHP translation files via AST instead of executing them to prevent remote code execution ## [1.5.0] - 2026-06-29 diff --git a/PERFORMANCE.md b/PERFORMANCE.md new file mode 100644 index 0000000..6b81d79 --- /dev/null +++ b/PERFORMANCE.md @@ -0,0 +1,89 @@ +# Performance Baseline + +Tracks wall-clock and memory numbers for `ValidationOrchestrationService::executeValidation()` +so the impact of the open `perf/*` PRs can be measured before/after they land. + +## Methodology + +- **Fixture set**: synthetic, generated (not part of the test suite) — 25 domains × 2 languages + (`en`/`de`) YAML files, 500 keys each (10 sections × 10 groups × 5 keys), 3 levels deep. + 50 files, ~24,925 keys total. A small percentage of entries carry placeholders, HTML tags, + missing translations, or empty values so every validator has real work to do (not a zero-issue + run). +- **What's measured**: `ValidationOrchestrationService::executeValidation()` called directly + (bypassing the Composer command layer) with all validators from + `ValidatorRegistry::getAvailableValidators()` and default `TranslationValidatorConfig`, + against the fixture directory, non-recursive, auto-detected file grouping. +- **Runs**: 5 back-to-back invocations in the same PHP process; `gc_collect_cycles()` before each + run. Reported: min / median / mean / max wall time (ms) per run, and peak memory + (`memory_get_peak_usage(true)`) as a **process-level** high-water mark across all 5 runs — + `gc_collect_cycles()` does not reset it, so it is not an independent per-run figure. +- **Environment**: PHP 8.5.0 (CLI, NTS), Apple M4 Pro, 14 cores, 24 GB RAM, macOS. +- Scripts used are not part of the repository (kept in a scratch location); the fixture generator + and benchmark runner can be recreated on request if this needs to become a repeatable CI check. + +## Baseline: `main` @ `c570fe0` (2026-07-28) + +| Metric | Value | +|---|---| +| Files checked | 50 | +| Keys checked | 24,925 | +| Min | 742.34 ms | +| Median | 758.25 ms | +| Mean | 784.89 ms | +| Max | 897.95 ms | +| Peak memory | 32.00 MB | + +Raw durations (ms): `742.34, 748.93, 758.25, 776.97, 897.95` + +## Performance PRs reflected below + +All merged into `main` via squash, one after another, 2026-07-28: + +- #137 fix: parse PHP translation files via AST instead of executing them +- #138 fix: exclude PHP config files from auto-detection +- #139 fix: reject invalid validator classes in --only/--skip options +- #140 fix: do not follow symlinks during recursive file scan +- #141 fix: harden system path safety check with boundary matching +- #142 fix: strip control characters and escape markup in CLI output +- #143 fix: skip translation files exceeding a size limit +- #144 fix: reject DOCTYPE and disable network in XLIFF parser +- #145 perf: resolve XLIFF content via a memoized id map +- #146 perf: cache prepared XLIFF schema source per version +- #147 perf: memoize extractKeys() results in parsers +- #148 perf: cache validator severity and drop per-comparison reflection +- #149 perf: evict parser cache per file set to bound memory +- #150 perf: scan each directory once and bucket files by parser +- #151 perf: reuse parser-read file content in encoding validation +- #152 perf: return bool from validator validate() instead of building issue arrays +- #153 perf: memoize segment convention detection +- #154 perf: memoize placeholder extraction per value +- #155 perf: dedupe render keys via hash set instead of in_array +- #156 perf: minor render and file-collection optimizations +- #157 feat: add standalone PHAR distribution +- #158 feat: disable KeyNamingConventionValidator by default + +## After: `main` @ `f8910bc` (2026-07-28) + +Same fixture set, same methodology (5 runs), run immediately after the merge sequence above. + +| Metric | Before (`c570fe0`) | After (`f8910bc`) | Δ | +|---|---|---|---| +| Files checked | 50 | 50 | — | +| Keys checked | 24,925 | 24,925 | — | +| Min | 742.34 ms | 565.73 ms | **-23.8%** | +| Median | 758.25 ms | 566.42 ms | **-25.3%** | +| Mean | 784.89 ms | 573.74 ms | **-26.9%** | +| Max | 897.95 ms | 601.49 ms | **-33.0%** | +| Peak memory | 32.00 MB | 28.00 MB | **-12.5%** | + +Raw durations (ms): `565.73, 565.96, 566.42, 569.10, 601.49` + +The combined effect of the 22 merged PRs (8 security/correctness fixes, 12 performance +optimizations, 2 features) is a validation run that is roughly a **quarter faster** and uses +**~12.5% less peak memory** on the same workload, with no change in what was validated (same file +and key counts). Note that #158 (disabling `KeyNamingConventionValidator` by default) is a +behavioral default change, not a code path optimization, and is not exercised by this benchmark +since it calls `ValidatorRegistry::getAvailableValidators()` directly rather than the +config-resolved default set — the perf gain above is attributable to the `perf/*` and `fix/*` +changes to the parsing/collection/rendering hot paths. diff --git a/docs/configuration/schema.md b/docs/configuration/schema.md index ef8aded..be62412 100644 --- a/docs/configuration/schema.md +++ b/docs/configuration/schema.md @@ -17,7 +17,7 @@ paths: ## validators - **Type**: `array` -- **Default**: `[]` (uses all available validators) +- **Default**: `[]` (uses all available validators, still subject to the `skip` default below) - **Description**: Array of validator class names to use for validation ```yaml @@ -56,8 +56,8 @@ only: ## skip - **Type**: `array` -- **Default**: `['DuplicateValuesValidator']` -- **Description**: Array of validator class names to skip +- **Default**: `['DuplicateValuesValidator', 'KeyNamingConventionValidator']` +- **Description**: Array of validator class names to skip. Set explicitly to `[]` to enable every available validator, including the two skipped by default. ```yaml skip: diff --git a/docs/index.md b/docs/index.md index 80e9770..6b623b9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -82,6 +82,6 @@ composer validate-translations ./translations | [EncodingValidator](/reference/validators#encodingvalidator) | WARNING | Validates UTF-8 encoding and Unicode issues | | [KeyCountValidator](/reference/validators#keycountvalidator) | WARNING | Warns when files exceed key threshold | | [KeyDepthValidator](/reference/validators#keydepthvalidator) | WARNING | Warns about excessive nesting depth | -| [KeyNamingConventionValidator](/reference/validators#keynamingconventionvalidator) | WARNING | Enforces key naming patterns | +| [KeyNamingConventionValidator](/reference/validators#keynamingconventionvalidator) | WARNING | Enforces key naming patterns (opt-in) | See the [Validators Reference](/reference/validators) for detailed documentation. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 69d0dd0..a61d498 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -34,6 +34,10 @@ The command is also available under the alias `vt`. Either a path to translation files must be provided as a command argument or within the configuration file. If no path is provided, the validator will abort. ::: +::: warning +If `--only` or `--skip` is given a value that resolves to no valid validator classes (e.g. a typo in the FQCN), the command fails with an error rather than silently falling back to running all validators. +::: + ## Examples ### Basic Validation @@ -123,10 +127,10 @@ composer validate-translations ./translations --dry-run | Code | Description | |------|-------------| -| `0` | Validation passed (no errors) | -| `1` | Validation failed (errors found) | +| `0` | Validation passed (no errors, or only warnings without `--strict`) | +| `1` | Validation failed (errors found, or warnings found with `--strict`) | -In `--dry-run` mode, the exit code is always `0` regardless of validation results. +In `--dry-run` mode, errors no longer produce a non-zero exit code. This does **not** apply to `--strict`: warnings still produce exit code `1` under `--strict` even when combined with `--dry-run`. ## Output Formats @@ -134,7 +138,7 @@ In `--dry-run` mode, the exit code is always `0` regardless of validation result Human-readable output with colored indicators: -``` +```text translations/messages.en.yaml MismatchValidator @@ -145,33 +149,51 @@ translations/messages.en.yaml ### JSON Format -Machine-readable JSON output: +Machine-readable JSON output. Issues are keyed by file path, then by validator short name: ```json { - "success": false, - "files": [ - { - "path": "translations/messages.en.yaml", - "issues": [ - { - "validator": "MismatchValidator", - "type": "error", - "message": "the translation key `delete` is missing" - } - ] + "status": 1, + "message": "Language validation failed with errors.", + "issues": { + "translations/messages.en.yaml": { + "MismatchValidator": { + "type": "Error", + "issues": [ + { + "message": "the translation key `delete` is missing but present in other files", + "details": { + "key": "delete" + } + } + ] + } } - ] + }, + "statistics": { + "execution_time": 0.012, + "execution_time_formatted": "12ms", + "files_checked": 1, + "keys_checked": 4, + "validators_run": 10, + "parsers_cached": 1 + } } ``` +`status` is the same process exit code described under [Exit Codes](#exit-codes) above (`0` success, `1` failure). `details` varies per validator and mirrors the data available to `--verbose` CLI output. + ### GitHub Format -Outputs GitHub Actions workflow commands for annotations: +Outputs GitHub Actions workflow commands for annotations, one per issue, followed by a summary annotation and a statistics notice: +```text +::error file=translations/messages.en.yaml::the translation key `delete` is missing but present in other files +::error::Language validation failed with errors. +::notice::Validation completed in 12ms - Files: 1, Keys: 4, Validators: 10 ``` -::error file=translations/messages.en.yaml::the translation key `delete` is missing -``` + +Annotations additionally include `line=`/`col=` when a validator reports a source position (e.g. `XliffSchemaValidator`), and `title=` when available. ## See Also diff --git a/docs/reference/file-formats.md b/docs/reference/file-formats.md index 6838a93..76bc85d 100644 --- a/docs/reference/file-formats.md +++ b/docs/reference/file-formats.md @@ -31,7 +31,8 @@ The Composer Translation Validator supports multiple translation file formats co - XML-based industry standard format - Supports translation metadata and states -- Schema validation available (XLIFF 1.2) +- Schema validation available (XLIFF 1.2 and 2.0) +- Target-language consistency check between filename locale and declared `target-language`/`trgLang` - Nested keys via ID dots: `user.greeting` ## YAML (YAML Ain't Markup Language) diff --git a/docs/reference/programmatic-usage.md b/docs/reference/programmatic-usage.md index 9cdca36..84e6388 100644 --- a/docs/reference/programmatic-usage.md +++ b/docs/reference/programmatic-usage.md @@ -55,7 +55,8 @@ $result = $service->executeValidation( ## Working with Results -The `executeValidation()` method returns a `ValidationResult` object: +The `executeValidation()` method returns a `ValidationResult` object, or `null` if no matching +files were found: ```php if ($result === null) { @@ -63,11 +64,19 @@ if ($result === null) { return; } -if ($result->hasErrors()) { - foreach ($result->getIssues() as $issue) { - echo $issue->getFilePath() . ': ' . $issue->getMessage() . PHP_EOL; +if ($result->hasIssues()) { + foreach ($result->getValidatorsWithIssues() as $validator) { + foreach ($validator->getIssues() as $issue) { + echo $issue->getFile() . ': ' . $validator->formatIssueMessage($issue) . PHP_EOL; + } } } + +// The exit code Composer/the CLI would use, respecting --dry-run and --strict +$exitCode = $result->getOverallResult()->resolveErrorToCommandExitCode( + dryRun: $config->getDryRun(), + strict: $config->getStrict(), +); ``` ## Use Cases diff --git a/docs/reference/validators.md b/docs/reference/validators.md index 9673173..7b2751b 100644 --- a/docs/reference/validators.md +++ b/docs/reference/validators.md @@ -325,7 +325,7 @@ navigation: large-file.en.yaml KeyCountValidator - - Warning File contains 339 translation keys, exceeds threshold of 300 + - Warning File contains 339 translation keys, which exceeds the threshold of 300 keys [WARNING] Language validation completed with warnings. ``` @@ -343,10 +343,10 @@ validator-settings: ```bash # Run the validator using pre-created fixtures (from project root) -composer validate-translations Fixtures/examples/key-count --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyCountValidator" -v +composer -d tests validate-translations Fixtures/examples/key-count --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyCountValidator" -v # Test with custom configuration (threshold: 100) -composer validate-translations Fixtures/examples/key-count --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyCountValidator" -v --config Fixtures/examples/key-count/translation-validator.yaml +composer -d tests validate-translations Fixtures/examples/key-count --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyCountValidator" -v --config Fixtures/examples/key-count/translation-validator.yaml ``` @@ -417,10 +417,10 @@ validator-settings: ```bash # Run the validator using pre-created fixtures (from project root) -composer validate-translations -d tests Fixtures/examples/key-depth --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyDepthValidator" -v +composer -d tests validate-translations Fixtures/examples/key-depth --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyDepthValidator" -v # Test with custom configuration (threshold: 5) -composer validate-translations -d tests Fixtures/examples/key-depth --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyDepthValidator" -v --config Fixtures/examples/key-depth/translation-validator.yaml +composer -d tests validate-translations Fixtures/examples/key-depth --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyDepthValidator" -v --config Fixtures/examples/key-depth/translation-validator.yaml ``` @@ -493,9 +493,9 @@ User.Address: "Address" # Mixed styles mixed.en.yaml KeyNamingConventionValidator - - Warning key naming violation: `userEmail` does not follow snake_case (suggestion: `user_email`) - - Warning key naming violation: `user-phone` does not follow snake_case (suggestion: `user_phone`) - - Warning key naming violation: `User.Address` does not follow snake_case (suggestion: `user.address`) + - Warning key naming convention violation: `userEmail` does not follow the configured snake_case convention. Suggested: `user_email` + - Warning key naming convention violation: `user-phone` does not follow the configured snake_case convention. Suggested: `user_phone` + - Warning key naming convention violation: `User.Address` does not follow the configured snake_case convention. Suggested: `user.address` [WARNING] Language validation completed with warnings. ``` @@ -630,9 +630,9 @@ email: "Gesendet an {email_address}" # Missing double braces notifications.de.yaml PlaceholderConsistencyValidator - - Warning placeholder inconsistency in key `welcome` - missing: {username}, extra: {benutzername} - - Warning placeholder inconsistency in key `order` - missing: {amount}, extra: {sum} - - Warning placeholder inconsistency in key `email` - extra: {{ email_address }} + - Warning placeholder inconsistency in translation key `welcome` - missing: {username}, extra: {benutzername} + - Warning placeholder inconsistency in translation key `order` - missing: {amount}, extra: {sum} + - Warning placeholder inconsistency in translation key `email` - extra: {{ email_address }} +-----------------+----------------------------------+--------------------------------+ | Translation Key | notifications.de.yaml | notifications.en.yaml |