Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR adds SemVer-aware comparison to the evaluator: a new Evaluator.versionCompare method with SemVer parsing and comparison (core, pre-release and build metadata handling), registers five new binary operators (semver_eq, semver_gt, semver_gte, semver_lt, semver_lte) in the jsonexpr operators map, and implements those operator classes. Extensive Jest tests for versionCompare and each operator are included, alongside a small CI workflow update and test helper changes to mock versionCompare. Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (7)
src/__tests__/jsonexpr/operators/semver_lt.test.js (2)
34-39: Consider asserting thatversionCompareis called for null inputs.The test verifies that
nullis returned for null inputs, but doesn't assert on theversionComparecall. Based on the operator implementation,versionCompare(null, null)should still be called (and returnnull). Adding this assertion would strengthen the test.🧪 Proposed enhancement
it("should return null for null inputs", () => { expect(operator.evaluate(evaluator, [null, null])).toBe(null); + expect(evaluator.versionCompare).toHaveBeenCalledWith(null, null); evaluator.evaluate.mockClear(); evaluator.versionCompare.mockClear(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/jsonexpr/operators/semver_lt.test.js` around lines 34 - 39, Add an assertion in the "should return null for null inputs" test to verify that evaluator.versionCompare was invoked with the null inputs when operator.evaluate(evaluator, [null, null]) is called; specifically assert evaluator.versionCompare was called once (or called with (null, null)) after the evaluate call and before clearing mocks (use the existing mock on evaluator.versionCompare and evaluator.evaluate to perform the check).
7-9: Consider usingbeforeEachfor mock clearing.The shared
evaluatorwith manualmockClear()at the end of each test is fragile—if a test fails before clearing, subsequent tests may be affected. Using Jest'sbeforeEachhook would be more robust and reduce repetition.♻️ Proposed refactor
describe("evaluate", () => { const evaluator = mockEvaluator(); + beforeEach(() => { + evaluator.evaluate.mockClear(); + evaluator.versionCompare.mockClear(); + }); + it("should return true when left version is less", () => { expect(operator.evaluate(evaluator, ["1.0.0", "2.0.0"])).toBe(true); expect(evaluator.versionCompare).toHaveBeenCalledWith("1.0.0", "2.0.0"); - - evaluator.evaluate.mockClear(); - evaluator.versionCompare.mockClear(); }); // ... similarly for other tests🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/jsonexpr/operators/semver_lt.test.js` around lines 7 - 9, The tests create a shared evaluator via const evaluator = mockEvaluator() and call evaluator.mockClear() manually at each test end; replace this pattern with a Jest beforeEach that either reassigns evaluator = mockEvaluator() or calls evaluator.mockClear() before each test so mocks are reset even if a test fails — update references to the evaluator created by mockEvaluator() in the describe("evaluate", ...) block and remove per-test manual mockClear() calls to avoid fragile cleanup.src/__tests__/jsonexpr/operators/semver_gt.test.js (2)
34-39: AddversionComparecall assertion for null inputs.Similar to the suggestion for
semver_lt.test.js, consider asserting thatversionCompareis called with(null, null)to fully document the expected behaviour.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/jsonexpr/operators/semver_gt.test.js` around lines 34 - 39, The test for null inputs should also assert that evaluator.versionCompare was invoked with (null, null); update the "should return null for null inputs" case to add expect(evaluator.versionCompare).toHaveBeenCalledWith(null, null) (placed after the operator.evaluate assertion and before mockClear calls) so the test documents that operator.evaluate triggered versionCompare with those arguments.
7-9: Same refactoring opportunity assemver_lt.test.js.Consider using
beforeEachfor mock clearing to improve test isolation and reduce repetition. The same pattern applies to other semver operator test files.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/jsonexpr/operators/semver_gt.test.js` around lines 7 - 9, Tests in describe("evaluate") repeat mock setup and lack per-test isolation; change the const evaluator = mockEvaluator() pattern to initialize/reset the mock in a beforeEach block: declare evaluator in the describe scope (use let evaluator) and call evaluator = mockEvaluator() (or jest.clearAllMocks() plus re-init) inside beforeEach so every test gets a fresh mock; apply the same change to semver_gt.test.js and other semver operator test files following the semver_lt.test.js refactor.src/__tests__/jsonexpr/operators/evaluator.js (1)
21-27: Inconsistent indentation inversionComparemock body.The body of
versionCompare(lines 22-26) lacks the leading tab indentation used elsewhere in this object (e.g.,compareon lines 29+). This breaks the consistent formatting style and may trigger linter warnings.🔧 Proposed fix
versionCompare: jest.fn((lhs, rhs) => { - const lhsStr = typeof lhs === "string" ? lhs : null; - const rhsStr = typeof rhs === "string" ? rhs : null; - if (lhsStr === null || rhsStr === null) return null; - return lhsStr === rhsStr ? 0 : lhsStr > rhsStr ? 1 : -1; -}), + const lhsStr = typeof lhs === "string" ? lhs : null; + const rhsStr = typeof rhs === "string" ? rhs : null; + if (lhsStr === null || rhsStr === null) return null; + return lhsStr === rhsStr ? 0 : lhsStr > rhsStr ? 1 : -1; + }), - compare: jest.fn((lhs, rhs) => { + compare: jest.fn((lhs, rhs) => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/jsonexpr/operators/evaluator.js` around lines 21 - 27, The mock implementation for versionCompare has inconsistent indentation compared to other operator mocks; update the body of the jest.fn for versionCompare so its inner lines (the const declarations, null checks and return) use the same leading tab indentation as the surrounding entries (e.g., match the indentation style used by compare) to satisfy the linter and maintain consistent formatting for the versionCompare jest.fn mock.src/__tests__/jsonexpr/operators/semver_lte.test.js (2)
7-39: Consider usingbeforeEach/afterEachfor mock clearing.The manual
mockClear()calls at the end of each test are repetitive. Using Jest'safterEachhook would be cleaner and ensure mocks are always cleared, even if a test fails mid-execution.♻️ Suggested refactor
describe("evaluate", () => { const evaluator = mockEvaluator(); + afterEach(() => { + evaluator.evaluate.mockClear(); + evaluator.versionCompare.mockClear(); + }); + it("should return true when left version is less", () => { expect(operator.evaluate(evaluator, ["1.0.0", "2.0.0"])).toBe(true); expect(evaluator.versionCompare).toHaveBeenCalledWith("1.0.0", "2.0.0"); - - evaluator.evaluate.mockClear(); - evaluator.versionCompare.mockClear(); }); // ... apply same pattern to other tests🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/jsonexpr/operators/semver_lte.test.js` around lines 7 - 39, Replace the repetitive manual evaluator.evaluate.mockClear() and evaluator.versionCompare.mockClear() calls in each it block with a single afterEach hook that clears those mocks; locate the test suite around describe("evaluate", ...) and add an afterEach(() => { evaluator.evaluate.mockClear(); evaluator.versionCompare.mockClear(); }) (or use jest.clearAllMocks() if preferred) and remove the per-test mockClear() lines so mocks are always cleared even on test failures.
34-39: Consider verifyingversionComparecall behaviour for null inputs.The other semver operator tests (e.g.,
semver_eq.test.js) verify whetherversionComparewas or wasn't called when inputs are null. This test lacks that assertion, making it inconsistent with the test suite.♻️ Suggested improvement
it("should return null for null inputs", () => { expect(operator.evaluate(evaluator, [null, null])).toBe(null); + expect(evaluator.versionCompare).not.toHaveBeenCalled(); evaluator.evaluate.mockClear(); evaluator.versionCompare.mockClear(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/jsonexpr/operators/semver_lte.test.js` around lines 34 - 39, The test semver_lte.test.js's "should return null for null inputs" currently checks the return value but doesn't assert whether evaluator.versionCompare was invoked; update the test for operator.evaluate(evaluator, [null, null]) to also assert that evaluator.versionCompare was not called (e.g., expect(evaluator.versionCompare).not.toHaveBeenCalled()), and keep the existing mockClear() calls for evaluator.evaluate and evaluator.versionCompare to match the other semver operator tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/__tests__/jsonexpr/operators/evaluator.js`:
- Around line 21-27: The mock implementation for versionCompare has inconsistent
indentation compared to other operator mocks; update the body of the jest.fn for
versionCompare so its inner lines (the const declarations, null checks and
return) use the same leading tab indentation as the surrounding entries (e.g.,
match the indentation style used by compare) to satisfy the linter and maintain
consistent formatting for the versionCompare jest.fn mock.
In `@src/__tests__/jsonexpr/operators/semver_gt.test.js`:
- Around line 34-39: The test for null inputs should also assert that
evaluator.versionCompare was invoked with (null, null); update the "should
return null for null inputs" case to add
expect(evaluator.versionCompare).toHaveBeenCalledWith(null, null) (placed after
the operator.evaluate assertion and before mockClear calls) so the test
documents that operator.evaluate triggered versionCompare with those arguments.
- Around line 7-9: Tests in describe("evaluate") repeat mock setup and lack
per-test isolation; change the const evaluator = mockEvaluator() pattern to
initialize/reset the mock in a beforeEach block: declare evaluator in the
describe scope (use let evaluator) and call evaluator = mockEvaluator() (or
jest.clearAllMocks() plus re-init) inside beforeEach so every test gets a fresh
mock; apply the same change to semver_gt.test.js and other semver operator test
files following the semver_lt.test.js refactor.
In `@src/__tests__/jsonexpr/operators/semver_lt.test.js`:
- Around line 34-39: Add an assertion in the "should return null for null
inputs" test to verify that evaluator.versionCompare was invoked with the null
inputs when operator.evaluate(evaluator, [null, null]) is called; specifically
assert evaluator.versionCompare was called once (or called with (null, null))
after the evaluate call and before clearing mocks (use the existing mock on
evaluator.versionCompare and evaluator.evaluate to perform the check).
- Around line 7-9: The tests create a shared evaluator via const evaluator =
mockEvaluator() and call evaluator.mockClear() manually at each test end;
replace this pattern with a Jest beforeEach that either reassigns evaluator =
mockEvaluator() or calls evaluator.mockClear() before each test so mocks are
reset even if a test fails — update references to the evaluator created by
mockEvaluator() in the describe("evaluate", ...) block and remove per-test
manual mockClear() calls to avoid fragile cleanup.
In `@src/__tests__/jsonexpr/operators/semver_lte.test.js`:
- Around line 7-39: Replace the repetitive manual evaluator.evaluate.mockClear()
and evaluator.versionCompare.mockClear() calls in each it block with a single
afterEach hook that clears those mocks; locate the test suite around
describe("evaluate", ...) and add an afterEach(() => {
evaluator.evaluate.mockClear(); evaluator.versionCompare.mockClear(); }) (or use
jest.clearAllMocks() if preferred) and remove the per-test mockClear() lines so
mocks are always cleared even on test failures.
- Around line 34-39: The test semver_lte.test.js's "should return null for null
inputs" currently checks the return value but doesn't assert whether
evaluator.versionCompare was invoked; update the test for
operator.evaluate(evaluator, [null, null]) to also assert that
evaluator.versionCompare was not called (e.g.,
expect(evaluator.versionCompare).not.toHaveBeenCalled()), and keep the existing
mockClear() calls for evaluator.evaluate and evaluator.versionCompare to match
the other semver operator tests.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: eb6230c3-3c5f-4fb8-b977-7362a31b498b
📒 Files selected for processing (15)
cross-sdk-tests/test_scenarios_complete.jsonsrc/__tests__/jsonexpr/evaluator.test.jssrc/__tests__/jsonexpr/operators/evaluator.jssrc/__tests__/jsonexpr/operators/semver_eq.test.jssrc/__tests__/jsonexpr/operators/semver_gt.test.jssrc/__tests__/jsonexpr/operators/semver_gte.test.jssrc/__tests__/jsonexpr/operators/semver_lt.test.jssrc/__tests__/jsonexpr/operators/semver_lte.test.jssrc/jsonexpr/evaluator.tssrc/jsonexpr/jsonexpr.tssrc/jsonexpr/operators/semver_eq.tssrc/jsonexpr/operators/semver_gt.tssrc/jsonexpr/operators/semver_gte.tssrc/jsonexpr/operators/semver_lt.tssrc/jsonexpr/operators/semver_lte.ts
- Return null for empty string inputs in versionCompare - Extract parseSemver/compareIdentifiers to module-level functions - Add explicit return type annotation to versionCompare - Add tests for empty string, undefined, pre-release+build metadata - Add rhs null test for semver_eq operator - Fix mock evaluator indentation - Add basic semver_gt scenarios to cross-SDK tests
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/__tests__/jsonexpr/operators/evaluator.js (1)
21-26: Avoid lexicographic fallback in the SemVer mock.On Line [25],
lhsStr > rhsStris lexicographic, so values like"1.10.0"and"1.2.0"are ordered incorrectly for semver. In this shared helper, that can hide semver-ordering issues in operator tests. Prefer a neutral default for non-equality and let each test explicitly stub ordering.Proposed change
versionCompare: jest.fn((lhs, rhs) => { const lhsStr = typeof lhs === "string" ? lhs : null; const rhsStr = typeof rhs === "string" ? rhs : null; if (lhsStr === null || rhsStr === null) return null; - return lhsStr === rhsStr ? 0 : lhsStr > rhsStr ? 1 : -1; + return lhsStr === rhsStr ? 0 : null; }),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/__tests__/jsonexpr/operators/evaluator.js` around lines 21 - 26, The mock for versionCompare currently falls back to lexicographic comparison (lhsStr > rhsStr), which misorders semver like "1.10.0" vs "1.2.0"; update the jest.fn mock for versionCompare so it only returns 0 when lhs and rhs are equal strings and returns null for non-equal string inputs (i.e., remove the lexicographic comparison branch), so tests must explicitly stub ordering when needed; locate the versionCompare mock in the evaluator tests and change its implementation accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/jsonexpr/evaluator.ts`:
- Around line 4-20: The parseSemver function must reject inputs that normalize
to an empty core: after stripping leading "v"/"V" and trimming "+build"
metadata, if core === "" or the resulting parts array has no non-empty segments,
return null instead of {parts, preRelease}; also ensure the version-comparison
logic that currently compares parsed results (the block that uses parseSemver's
output) checks for a null parseSemver return and returns null immediately when
either side is null rather than proceeding to compare empty/malformed parts.
- Around line 23-33: The current numeric detection and comparison in
src/jsonexpr/evaluator.ts (variables aNum, bNum, aIsNum, bIsNum using parseInt)
loses precision for integers beyond Number.MAX_SAFE_INTEGER; replace
parseInt-based logic with a regex numeric check (e.g., /^\d+$/) to set aIsNum
and bIsNum, then when both are numeric compare by length (longer = larger) and,
if lengths equal, by lexicographic string comparison to preserve arbitrary
precision; keep the existing numeric-vs-non-numeric precedence (if aIsNum &&
!bIsNum return -1, if bIsNum && !aIsNum return 1) and fall back to the existing
string equality/lexicographic branch for non-numeric identifiers.
---
Nitpick comments:
In `@src/__tests__/jsonexpr/operators/evaluator.js`:
- Around line 21-26: The mock for versionCompare currently falls back to
lexicographic comparison (lhsStr > rhsStr), which misorders semver like "1.10.0"
vs "1.2.0"; update the jest.fn mock for versionCompare so it only returns 0 when
lhs and rhs are equal strings and returns null for non-equal string inputs
(i.e., remove the lexicographic comparison branch), so tests must explicitly
stub ordering when needed; locate the versionCompare mock in the evaluator tests
and change its implementation accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 63034ccf-5a80-4d24-a29f-05a1d128d767
📒 Files selected for processing (5)
cross-sdk-tests/test_scenarios_complete.jsonsrc/__tests__/jsonexpr/evaluator.test.jssrc/__tests__/jsonexpr/operators/evaluator.jssrc/__tests__/jsonexpr/operators/semver_eq.test.jssrc/jsonexpr/evaluator.ts
✅ Files skipped from review due to trivial changes (2)
- cross-sdk-tests/test_scenarios_complete.json
- src/tests/jsonexpr/evaluator.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
- src/tests/jsonexpr/operators/semver_eq.test.js
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. Reviewed: semver parsing logic, operator delegation, null propagation, test coverage, git history context, and past PR comments. 5 independent review passes found no issues scoring above the confidence threshold. 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
- Reject inputs with empty core after stripping (e.g., "v", "+build") - Use regex + string-length comparison for numeric identifiers to avoid large integer precision loss with parseInt - Use nullish coalescing (??) instead of || for missing version parts - Fix mock versionCompare to use numeric segment comparison - Switch all operator tests from manual mockClear to afterEach - Add versionCompare not-called assertions to all null tests - Split null tests into separate lhs/rhs cases for gt/gte/lt/lte - Add tests for empty-core inputs
Scenarios moved to the dedicated cross-sdk-tests repository.
calthejuggler
left a comment
There was a problem hiding this comment.
LGTM 🙂 Just one question about what we do with invalid strings
src/jsonexpr/evaluator.ts
Outdated
| if (a.length !== b.length) { | ||
| return a.length > b.length ? 1 : -1; | ||
| } | ||
| return a === b ? 0 : a > b ? 1 : -1; |
There was a problem hiding this comment.
Won't this still be a string comparison? 🤔 "12" > "7" === false
There was a problem hiding this comment.
Oh, I figured it out 😅 They have different lengths, so "12".length > "7".length === true
There was a problem hiding this comment.
Correct — for same-length numeric strings, lexicographic comparison gives the right answer (e.g., "12" > "07" is true). Different lengths are handled by the length check first ("12".length > "7".length). This is how we avoid parseInt precision loss for very large numbers.
| } | ||
| if (aIsNum) return -1; | ||
| if (bIsNum) return 1; | ||
| return a === b ? 0 : a > b ? 1 : -1; |
There was a problem hiding this comment.
Same here 🤔 Also, if neither aIsNum or bIsNum, then do we fall back to straight string comparison?
There was a problem hiding this comment.
I'm thinking strings like 1.02.30. Not sure what the best way to go is if they are invalid, but we should be explicit in what we choose 🙂
There was a problem hiding this comment.
Good catch. Leading zeros like 1.02.30 were being rejected by the strict semver regex and falling through to string comparison, causing incorrect results (e.g., versionCompare("1.02.0", "1.2.0") was returning 1 instead of 0).
Fixed in fd43a80 — now using /^\d+$/ with leading zero stripping so 02 is treated as 2. This makes us lenient with non-strict semver inputs while preserving correct numeric ordering.
For the non-numeric fallback: yes, if neither side is numeric (e.g., pre-release identifiers like "alpha" vs "beta"), we fall back to straight string comparison, which is correct per the semver spec for alphanumeric pre-release identifiers.
Leading zeros like "1.02.30" are now treated as their numeric value (02 == 2). Uses permissive regex /^\d+$/ with leading zero stripping instead of strict semver regex that rejected them.
The 5 new semver operator modules + evaluator versionCompare method add ~5.6 KiB of source which translates to ~14 KiB in the minified bundle due to per-module webpack overhead. No bloat or unnecessary polyfills — the increase is proportional to the code added.
Enable helpers: true in @babel/plugin-transform-runtime so Babel helpers (_classCallCheck, _createClass, _slicedToArray, etc.) are imported from a shared runtime instead of being inlined into every module. Reduces minified browser bundle from 132 KiB to 97.7 KiB. Also reverts the maxAssetSize bump since the bundle is now well under the 128 KiB limit.
Summary
versionCompare()method toEvaluatorwith full semver parsing (v prefix stripping, build metadata ignored, pre-release handling, numeric comparison, missing parts treated as 0, leading zeros tolerated)semver_eq,semver_gt,semver_gte,semver_lt,semver_lte— each delegates toversionComparefollowing the existingBinaryOperatorpattern"v","+build") by returningnullparseIntprecision loss beyondNumber.MAX_SAFE_INTEGERopenedBundle Size
Enabling
helpers: truein@babel/plugin-transform-runtimededuplicates Babel helpers (_classCallCheck,_createClass,_slicedToArray, etc.) that were previously inlined into every module. This reduces the minified browser bundle from 118 KiB (main) to 97.7 KiB — a 17% reduction — even after adding 5 new operator modules and theversionComparemethod.absmartly.min.jsBackward Compatibility
Old SDKs receiving expressions with unknown semver operators will not crash. The evaluator returns
nullfor unrecognized operators, which converts tofalse— users simply won't match the targeting rule. Verified this behavior across all SDKs (JS, Java, Python, Go, Ruby, PHP, Swift, .NET, Dart, C++, Rust, Scala, Elixir).Test Plan
versionCompare(equal versions, major/minor/patch, numeric ordering, missing parts, pre-release, v prefix, null inputs, coercion, build metadata, pre-release+build, empty strings, undefined, empty core, leading zeros)afterEachcleanup and null path assertionscross-sdk-testsrepo (9 scenarios ingenerate_scenarios.py)