fix: length rules reject a valid length of 0 - #17
Conversation
…erage for Between, Max, and Min rules
There was a problem hiding this comment.
Pull request overview
Fixes an edge case in WPValidator’s length-based rules where a valid computed length of 0 was treated as falsy and incorrectly rejected. The change ensures only “unmeasurable” values (where getValueLength() returns false) short-circuit validation, while a real length/value of 0 proceeds through comparisons.
Changes:
- Update
BetweenRule,MinRule, andMaxRuleto guard on$length === falserather than truthiness. - Expand rule test coverage to include zero-length/value cases (
0,0.0,'',[]) and explicitly unmeasurable cases (true,null,stdClass). - Add boundary-negative assertions in existing min/max/between tests to validate rejection beyond configured limits.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/Rules/MinRule.php | Treat 0 as a valid measurable length/value; only false (unmeasurable) fails fast. |
| src/Rules/MaxRule.php | Same fix as MinRule for max comparisons (accepts 0). |
| src/Rules/BetweenRule.php | Same fix as Min/Max for range comparisons (accepts 0). |
| tests/Rules/MinTest.php | Adds coverage for zero-length acceptance, unmeasurable rejection, and boundary negatives. |
| tests/Rules/MaxTest.php | Adds coverage for zero-length acceptance, unmeasurable rejection, and boundary negatives. |
| tests/Rules/BetweenTest.php | Adds coverage for zero-length acceptance, unmeasurable rejection, and boundary negatives. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…tor Between, Max, and Min rules to utilize new method; add test-watch script and update README
…an up .gitattributes and composer.json
…ator into fix/between-rule
There was a problem hiding this comment.
🟡 Changes recommended
Reject non-numeric floats such as NAN and resolve the documented watcher-tooling and guidance inconsistencies.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
src/Helpers.php:40
- Because
getValueLength()accepts every float, this also receivesNAN. For amin-only call,NAN < $minis false and the open-ended max causes the final expression to return true, whereas the previous implementation rejected it (NAN >= $minis false). Reject non-numeric float values such asNANbefore applying the bounds so this fix only changes legitimate zero lengths.
if ($length === false) {
return false;
- Files reviewed: 8/9 changed files
- Comments generated: 2
- Review effort level: Lite
| composer test:unit # full suite (Pest, --testdox, excludes group 'db') | ||
| ./vendor/bin/pest tests/Rules/BetweenTest.php # single file | ||
| ./vendor/bin/pest --filter='between accepts a length of zero' # single test by name | ||
| composer compat # PHPCompatibility check against PHP 7.2 | ||
| composer rector # Rector dry-run (never auto-applies) |
| - `isEmpty()` treats `'0'`, `0`, `0.0`, and `false` as **non**-empty. | ||
| - `getValueLength()` returns `mb_strlen` for strings, `count` for arrays, the number itself for ints/floats, and `false` for anything unmeasurable (bool, null, object). `false` is distinct from length `0` — a length of 0 is valid. | ||
|
|
||
| `lengthWithin($value, $min, $max)` wraps both and is what `between`, `min`, `max`, and `size` should use (`null` for an open bound) rather than comparing lengths by hand. |
There was a problem hiding this comment.
🔵 Needs a closer look
Unresolved issues include a NAN regression, inaccurate SizeRule guidance, and missing advertised test-watch tooling.
Review details
Suppressed comments (3)
CLAUDE.md:69
- This guidance says
sizeshould uselengthWithin, butSizeRule::validate()still implements its own string/int/array comparisons and never calls this helper. That makes the documented invariant false and can mislead future rule changes; either migrateSizeRulewith compatibility tests or removesizefrom this sentence.
`lengthWithin($value, $min, $max)` wraps both and is what `between`, `min`, `max`, and `size` should use (`null` for an open bound) rather than comparing lengths by hand.
CLAUDE.md:14
- The PR description says this change adds
composer test:watch,bin/test-watch, and documentation for it, but this checkout has nobin/test-watchandcomposer.jsonstill defines onlytest:unit,compat, andrector. The advertised developer tooling is therefore not delivered; please add the missing files/script/docs or remove that scope from the description.
composer test:unit # full suite (Pest, --testdox, excludes group 'db')
src/Helpers.php:47
- Because the lower-bound check only rejects
$length < $min, aNANfloat (whichgetValueLength()accepts as a float) skips that check; with$max === null, the final expression then returnstrue. The previousMinRulecomparison ($length >= $min) rejectedNAN, so this is a regression. Use the positive lower-bound comparison or explicitly reject non-finite lengths.
if ($min !== null && $length < $min) {
return false;
}
return $max === null || $length <= $max;
- Files reviewed: 8/9 changed files
- Comments generated: 0 new
- Review effort level: Lite
Summary
The
min,max, andbetweenrules rejected any value whose length is0. An empty string, an empty array, and the numbers0/0.0all failed validation even when the configured bounds explicitly allowed a length of zero —min:0,max:5, andbetween:0,5would each reject''.The cause is a truthiness check on a length value:
getValueLength()returns an integer length, orfalsewhen the value has no measurable length at all (bool, null, object). Because0andfalseare both falsy, the guard could not tell "this value is legitimately empty" from "this value cannot be measured", and collapsed both into a failure.This PR distinguishes the two, and moves the distinction into the shared helper so it is made in exactly one place.
Behaviour change
Only values with a length of
0are affected. Verified before/after:min:0max:5between:0,5''false→truefalse→truefalse→true[]false→truefalse→truefalse→true0false→truefalse→truefalse→true0.0false→truefalse→truefalse→trueEverything else is untouched. Non-zero bounds behave identically to before (
min:3still accepts'abc'and rejects'ab'), and values with no measurable length —true,null,new stdClass()— still fail, as they should.Note this only changes whether an empty value passes a length rule. It does not make empty values pass validation overall:
requiredis still the rule that rejects them, which is the correct separation of concerns. A field declaredmin:0was always meant to accept an empty value.Changes
Fix — added
Helpers::lengthWithin()next togetValueLength()in the trait all three rules already use. It decodes thefalsesentinel once and applies the bounds, withnullmeaning "no bound on this side":MinRule,MaxRule, andBetweenRulenow each delegate to it in a single line rather than repeating the same guard three times. The original fix applied the corrected check separately in all three classes; consolidating it means a future length-based rule cannot reintroduce the bug by writingif ($length)again, and there is one definition of what a measurable length is.Tests — added coverage to
MinTest,MaxTest, andBetweenTestfor the three distinct cases the old code conflated:0is accepted, for strings, arrays, and numberstrue,null,stdClass) are still rejectedThe previously commented-out array assertion in
MaxTestis now enabled.Developer tooling — added
composer test:watch(bin/test-watch), which re-runs the suite when a PHP file undersrc/ortests/changes. It delegates to the existingtest:unitscript so there is a single definition of the test command, and uses plain polling so it needs no extra dependencies. Added/bin export-ignoreto.gitattributesso this dev-only script is not shipped in the distributed package, and documented the two commands briefly under Contributing.Testing
composer test:unit— 36 tests, 123 assertions, all passing.Before/after behaviour in the table above was verified by running both revisions of the rules directly against each value.
Compatibility
No API changes.
lengthWithin()is a newprotectedtrait method;getValueLength()keeps its existing signature and return contract, so any code extendingRuleor usingHelperscontinues to work unchanged. PHP 7.2+ compatible, consistent with the package's existing constraint.One caveat worth calling out for reviewers: if a project currently relies on
min:0orbetween:0,Nto reject empty values, that behaviour was a bug and this PR ends it. Such a field should userequiredinstead.