Skip to content

✨ qs.js 6.15.3 parity#58

Merged
techouse merged 2 commits into
mainfrom
chore/qs-6.15.3
Jul 4, 2026
Merged

✨ qs.js 6.15.3 parity#58
techouse merged 2 commits into
mainfrom
chore/qs-6.15.3

Conversation

@techouse

@techouse techouse commented Jul 4, 2026

Copy link
Copy Markdown
Owner

This pull request implements stricter and more consistent enforcement of list limits during query string decoding and merging, aligning behavior with Node qs 6.15.3. It also improves error handling for oversized list values and updates internal logic for list merging and compaction. Additionally, regression tests and documentation updates are included.

List limit enforcement and error handling:

  • List limits are now enforced across all duplicate-key combinations and during list merges, matching Node qs 6.15.3 cumulative enforcement. Oversized lists raise ValueError if raise_on_limit_exceeded is enabled, otherwise they are converted to OverflowDict. (src/qs_codec/utils/utils.py, src/qs_codec/decode.py) [1] [2] [3] [4] [5] [6] [7]
  • Flat comma-separated values are now rejected before allocation and decoding if they would exceed the configured list limit and raise_on_limit_exceeded is set, preventing unnecessary processing. (src/qs_codec/decode.py)

Decoding and merging logic updates:

  • The parser no longer disables list parsing based on the top-level token count; instead, parse_lists is always honored, and list_limit is enforced during list construction and merging. (src/qs_codec/decode.py) [1] [2] [3]
  • List merging and compaction logic has been refactored to retain or remove sparse holes as appropriate and to enforce list limits at all stages, including during deep merges and when combining mappings with sequences. (src/qs_codec/utils/utils.py) [1] [2] [3] [4] [5] [6] [7] [8]

Testing and documentation:

  • Regression coverage for unbalanced bracket keys and cyclic compaction has been added to match Node qs 6.15.3. Documentation and changelog have been updated to reflect these changes. (CHANGELOG.md)

Summary by CodeRabbit

  • Bug Fixes
    • Enforced list_limit consistently across duplicate keys and mixed list merges, including cumulative growth scenarios.
    • When comma-splitting is used, oversized comma values now fail fast once the limit is exceeded.
    • Fixed merge/combine edge cases for over-limit and overflowed values, including correct handling of certain bracket-form collisions.
    • Improved lenient interpretation of malformed/unbalanced bracket syntax in supported cases.
  • Tests
    • Added and expanded unit coverage for list-limit, overflow behavior, and bracket parsing edge cases.

@techouse techouse self-assigned this Jul 4, 2026
@techouse techouse added the enhancement New feature or request label Jul 4, 2026
@techouse techouse requested a review from Copilot July 4, 2026 08:18
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@techouse, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 18c2915a-b4b1-4e13-ac8d-a207e382523b

📥 Commits

Reviewing files that changed from the base of the PR and between 8fc86ec and 088d918.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • README.rst
  • docs/README.rst
  • docs/index.rst
  • skills/qs-codec/SKILL.md
  • src/qs_codec/decode.py
  • src/qs_codec/models/decode_options.py
  • src/qs_codec/utils/utils.py
  • tests/unit/decode_test.py
  • tests/unit/utils_test.py
📝 Walkthrough

Walkthrough

List-limit handling is updated across decoding, merging, and combining. Decode now honors parse_lists directly and applies stricter comma-split enforcement, while utils centralizes list-limit enforcement and routes merge/combine results through it. Tests and changelog entries were expanded.

Changes

List limit enforcement

Layer / File(s) Summary
Decode-time list parsing and comma-limit enforcement
src/qs_codec/decode.py
Removes the parse_lists_effective override in decode(), updates _parse_array_value() to raise on comma-limit overflow without full splitting, and enables comma-limit enforcement for non-[]= tokens.
Centralized list-limit helper
src/qs_codec/utils/utils.py
Adds _enforce_list_limit and updates list-merge frame state and docstrings to support the new enforcement flow.
Utils.merge list handling rewrite
src/qs_codec/utils/utils.py
Reworks sequence/mapping merge paths and list iteration to preserve Undefined handling, index-based reconstruction, and limit enforcement.
Utils.combine list-limit enforcement
src/qs_codec/utils/utils.py
Routes combined results through _enforce_list_limit and raises ValueError when configured.
Test coverage and changelog
tests/unit/decode_test.py, tests/unit/utils_test.py, CHANGELOG.md
Adds and updates tests for malformed brackets, cumulative list growth, overflow/list merges, combine limits, compact cycles, mixed bypass parity, and records the fixes in the changelog.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • techouse/qs_codec#11: Both PRs change decode.py to enforce list_limit with raise_on_limit_exceeded.
  • techouse/qs_codec#37: Both PRs adjust Utils.combine and Utils.merge list-limit / OverflowDict behavior.
  • techouse/qs_codec#53: Both PRs touch the same decode comma/bracket and merge overflow code paths.

Suggested labels: bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the change summary and context, but it omits the required issue reference, type of change, testing details, and checklist. Add a Fixes #(issue) line, choose the type of change, describe how you tested it, and complete the checklist items.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the PR's main goal: qs.js parity with Node 6.15.3.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/qs-6.15.3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

codacy-production Bot commented Jul 4, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics -4 complexity · 0 duplication

Metric Results
Complexity -4
Duplication 0

View in Codacy

🟢 Coverage 100.00% diff coverage · +0.00% coverage variation

Metric Results
Coverage variation +0.00% coverage variation (-1.00%)
Diff coverage 100.00% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (f843c85) 1931 1931 100.00%
Head commit (088d918) 1929 (-2) 1929 (-2) 100.00% (+0.00%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#58) 62 62 100.00%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (f843c85) to head (088d918).

Additional details and impacted files
@@            Coverage Diff            @@
##              main       #58   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           22        22           
  Lines         1931      1929    -2     
=========================================
- Hits          1931      1929    -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates qs_codec’s decoder/merge internals to more closely match Node qs 6.15.3 around cumulative list-limit enforcement (including earlier rejection of oversized comma payloads), and adds regression tests for bracket-parsing edge cases and cycle-safe compaction.

Changes:

  • Introduces a shared list-limit enforcement helper and applies it across Utils.merge / Utils.combine, including raising behavior when raise_on_limit_exceeded=True.
  • Adjusts decode parsing behavior for comma limits (especially around bracket-array assignments) and removes a local parse_lists override to keep caller options consistent.
  • Expands regression coverage for list-limit overflow combinations, unbalanced bracket keys, and multi-step cycles during compaction; documents the change in the changelog.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/qs_codec/utils/utils.py Adds _enforce_list_limit and refactors merge/combine logic to enforce list limits consistently.
src/qs_codec/decode.py Removes local parse_lists override and tweaks comma-limit enforcement behavior.
tests/unit/utils_test.py Adds focused tests for list-limit growth behavior across merge/combine and cyclic compaction.
tests/unit/decode_test.py Adds regression tests for unbalanced bracket keys and list-limit overflow scenarios.
CHANGELOG.md Documents the parity fixes and new regression coverage.
Comments suppressed due to low confidence (1)

src/qs_codec/utils/utils.py:200

  • When merging a scalar into an existing list/tuple target, Undefined sources should be ignored (not appended). Currently the scalar path appends Undefined, which can leak the sentinel into results and can also trigger list-limit enforcement erroneously.
                        candidate = [*current_target, current_source]
                        enforced = _enforce_list_limit(candidate, frame.options)
                        if isinstance(enforced, OverflowDict):
                            stack.pop()
                            last_result = enforced
                            continue

                        mutable_target = list(current_target) if isinstance(current_target, tuple) else current_target
                        mutable_target.append(current_source)

Comment thread src/qs_codec/utils/utils.py
Comment thread src/qs_codec/decode.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/qs_codec/decode.py (1)

63-66: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale docstring: the "disable list parsing on token count" note no longer reflects the code.

The invocation-local parse_lists_effective switch was removed and _parse_keys is now always called with parse_lists=opts.parse_lists. The Notes bullet still claims the parser "temporarily disables list parsing for this invocation" when top-level tokens exceed list_limit, which is now inaccurate and misleading for callers.

As per coding guidelines ("Keep docstrings descriptive and parity-focused").

📝 Proposed docstring fix
     Notes
     -----
     - Empty/falsey ``value`` returns an empty dict.
-    - When the *number of top-level tokens* exceeds ``list_limit`` and ``parse_lists`` is enabled, the parser temporarily **disables list parsing** for this invocation to avoid quadratic work. This mirrors the behavior of other ports and keeps large flat query strings efficient.
+    - List parsing follows ``options.parse_lists`` for the whole invocation; ``list_limit`` is
+      enforced incrementally during comma-splitting and merge/combine rather than by disabling
+      list parsing based on token count.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/qs_codec/decode.py` around lines 63 - 66, The Notes docstring in
decode.py is stale and still describes the removed invocation-local list-parsing
disable behavior. Update the Notes text near the decode/parse logic to match the
current flow where _parse_keys is always called with opts.parse_lists, and
remove or rewrite the bullet about temporarily disabling list parsing when
top-level token count exceeds list_limit so it reflects the actual behavior.

Source: Coding guidelines

🧹 Nitpick comments (2)
src/qs_codec/utils/utils.py (1)

350-359: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

src/qs_codec/utils/utils.py:355 — Avoid mutating the caller list here frame.target[:] = enforced rewrites the input list in place; the merged list is already available as enforced, so return that instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/qs_codec/utils/utils.py` around lines 350 - 359, The list iteration
completion path in the stack unwinding logic is mutating the caller’s list in
place via frame.target[:] = enforced; update the list handling in the phase ==
"list_iter" branch to avoid rewriting the original input and instead use the
already computed merged result from _enforce_list_limit, assigning last_result
to that value directly. Keep the change localized around the
list_iter/frame.target handling in utils.py.

Source: Coding guidelines

tests/unit/utils_test.py (1)

611-621: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider asserting no caller-input mutation, like the sibling test does.

test_merge_raises_when_list_growth_exceeds_limit (Lines 582-589) explicitly asserts target == original_target after a raise. This test reuses source across two Utils.merge() calls (one non-raising, one raising) without any equivalent check that target/source remain unmutated, so a caller-input mutation regression here would go undetected.

♻️ Suggested addition
     def test_merge_enforces_limit_after_nested_list_merge(self) -> None:
         target = [{"a": 1}]
         source = [{"b": 2}, {"c": 3}]
+        original_source = [dict(item) for item in source]
 
         result = Utils.merge(target, source, DecodeOptions(list_limit=1))
 
         assert result == {"0": {"a": 1, "b": 2}, "1": {"c": 3}}
         assert isinstance(result, OverflowDict)
+        assert source == original_source
 
         with pytest.raises(ValueError, match="List limit exceeded"):
             Utils.merge([{"a": 1}], source, DecodeOptions(list_limit=1, raise_on_limit_exceeded=True))
+
+        assert source == original_source

As per coding guidelines, **/*.py: "Do not mutate caller inputs—copy/normalize before traversal", the test suite should verify this contract wherever list-limit merges are exercised, consistent with the pattern already used in the adjacent test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/utils_test.py` around lines 611 - 621, Add an assertion in
test_merge_enforces_limit_after_nested_list_merge that the caller inputs are
unchanged after Utils.merge runs, mirroring the adjacent test’s mutation check.
Use the existing target/source variables around Utils.merge and verify they
still match their original contents after both the non-raising and
raise_on_limit_exceeded calls, so any future caller-input mutation in
Utils.merge/merge_enforces_limit_after_nested_list_merge is caught.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/decode_test.py`:
- Around line 2023-2028: The `decode_test.py` expectation for the `pytest.param`
covering `0=y&[]=x` is incorrect; update the asserted result to match `qs`
6.15.3 behavior where `decode` keeps the flat key and empty-bracket root
separate. Locate the failing case by the
`flat-zero-combines-leading-bracket-root` test id and change the expected value
from a merged list under `0` to separate entries for `0` and the empty key.

---

Outside diff comments:
In `@src/qs_codec/decode.py`:
- Around line 63-66: The Notes docstring in decode.py is stale and still
describes the removed invocation-local list-parsing disable behavior. Update the
Notes text near the decode/parse logic to match the current flow where
_parse_keys is always called with opts.parse_lists, and remove or rewrite the
bullet about temporarily disabling list parsing when top-level token count
exceeds list_limit so it reflects the actual behavior.

---

Nitpick comments:
In `@src/qs_codec/utils/utils.py`:
- Around line 350-359: The list iteration completion path in the stack unwinding
logic is mutating the caller’s list in place via frame.target[:] = enforced;
update the list handling in the phase == "list_iter" branch to avoid rewriting
the original input and instead use the already computed merged result from
_enforce_list_limit, assigning last_result to that value directly. Keep the
change localized around the list_iter/frame.target handling in utils.py.

In `@tests/unit/utils_test.py`:
- Around line 611-621: Add an assertion in
test_merge_enforces_limit_after_nested_list_merge that the caller inputs are
unchanged after Utils.merge runs, mirroring the adjacent test’s mutation check.
Use the existing target/source variables around Utils.merge and verify they
still match their original contents after both the non-raising and
raise_on_limit_exceeded calls, so any future caller-input mutation in
Utils.merge/merge_enforces_limit_after_nested_list_merge is caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f5ca638a-f3ab-4ff6-8f85-af14f8b2469b

📥 Commits

Reviewing files that changed from the base of the PR and between f843c85 and 30529a6.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • src/qs_codec/decode.py
  • src/qs_codec/utils/utils.py
  • tests/unit/decode_test.py
  • tests/unit/utils_test.py

Comment thread tests/unit/decode_test.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/qs_codec/utils/utils.py (1)

355-358: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid writing the merged list back into target
frame.target[:] = enforced mutates the caller’s list on successful list merges. Return the merged copy directly instead so Utils.merge stays side-effect free.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/qs_codec/utils/utils.py` around lines 355 - 358, The list-merge path in
Utils.merge is mutating the caller’s list via frame.target, which breaks
side-effect-free behavior. Update the branch around _enforce_list_limit so it
does not assign back into frame.target after a successful merge. Instead, keep
the merged list in the local result flow by returning the enforced list directly
and using last_result to hold that merged copy, while preserving the existing
behavior for frame.list_merged and list handling.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/qs_codec/utils/utils.py`:
- Around line 355-358: The list-merge path in Utils.merge is mutating the
caller’s list via frame.target, which breaks side-effect-free behavior. Update
the branch around _enforce_list_limit so it does not assign back into
frame.target after a successful merge. Instead, keep the merged list in the
local result flow by returning the enforced list directly and using last_result
to hold that merged copy, while preserving the existing behavior for
frame.list_merged and list handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: df772d45-42e7-408c-bd3b-b38c4c234726

📥 Commits

Reviewing files that changed from the base of the PR and between 30529a6 and 8fc86ec.

📒 Files selected for processing (2)
  • src/qs_codec/decode.py
  • src/qs_codec/utils/utils.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/qs_codec/decode.py

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/qs_codec/utils/utils.py:202

  • When merging a scalar into a list/tuple target, merge appends current_source unconditionally. If current_source is the internal Undefined sentinel, this will insert a sparse-hole placeholder as an actual element and also consume list_limit capacity, which is inconsistent with other merge branches that treat Undefined as “skip / no-op”.

Consider short-circuiting when current_source is Undefined so list targets are left unchanged and the limit check does not count the skipped sentinel.

                        mutable_target = list(current_target) if isinstance(current_target, tuple) else current_target
                        mutable_target.append(current_source)

techouse added 2 commits July 4, 2026 11:23
Enforce cumulative limits across duplicate and mixed list merges. Reject oversized flat comma values before splitting or decoding, and add upstream parity regressions.
Describe cumulative enforcement, numeric-key overflow, ValueError semantics, and bracketed comma groups across the changelog, public docs, option docstrings, and qs-codec skill.
@techouse techouse merged commit b95b6ac into main Jul 4, 2026
26 checks passed
@techouse techouse deleted the chore/qs-6.15.3 branch July 4, 2026 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants