Skip to content

✨ enhance decoding options and tests for strict merge behavior, bracket notation, and list limits#55

Merged
techouse merged 5 commits into
mainfrom
chore/more-upstream
May 22, 2026
Merged

✨ enhance decoding options and tests for strict merge behavior, bracket notation, and list limits#55
techouse merged 5 commits into
mainfrom
chore/more-upstream

Conversation

@techouse
Copy link
Copy Markdown
Owner

This pull request introduces significant improvements to the query string decoding logic to better match the behavior of Node's qs library, particularly regarding list limits, bracket notation, duplicate handling, and object/scalar merge conflicts. The main change is the addition of the DecodeOptions.strictMerge option, which defaults to modern qs behavior for merging objects and scalars. The PR also fixes several edge cases to ensure parity with recent versions of qs, and updates the documentation accordingly.

Decode behavior improvements:

  • Added DecodeOptions.strictMerge (default: true) to control how object/scalar merge conflicts are handled, matching modern Node qs behavior by wrapping conflicts in a list, with an option to restore legacy merging. [1] [2] Fb87a85fL247R247, [3]
  • Updated handling of bracket notation so that duplicate bracketed keys are always combined into arrays, regardless of the selected duplicate strategy, matching Node qs 6.15.0.
  • Improved list limit handling for indexed notation and comma-separated lists to match Node qs 6.14.2, including correct overflow and unlimited parsing behavior. [1] [2] [3] [4] [5]
  • Fixed merge logic to properly handle legacy scalar-key merge mode when strictMerge is false, including protection against unsafe object keys. (Fb87a85fL247R247, lib/src/utils.dartR918-R953)

Documentation updates:

  • Updated README.md and CHANGELOG.md to document the new strictMerge option, improved duplicate handling, and clarified list limit semantics. [1] [2] [3] [4]

Other codebase improvements:

  • Refactored and clarified internal utility methods for merging and overflow handling, and improved comments for maintainability. [1] [2] [3]

These changes ensure that the Dart implementation closely tracks the behavior of Node's qs library, improving compatibility and predictability for users porting code or expecting parity.

@techouse techouse requested a review from Copilot May 22, 2026 16:46
@techouse techouse self-assigned this May 22, 2026
@techouse techouse added the enhancement New feature or request label May 22, 2026
@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 22, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 duplication

Metric Results
Duplication 0

View in Codacy

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

Metric Results
Coverage variation +0.06% 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 (05b1506) 1391 1359 97.70%
Head commit (26c5945) 1430 (+39) 1398 (+39) 97.76% (+0.06%)

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 (#55) 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
Copy link
Copy Markdown

codecov Bot commented May 22, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.76%. Comparing base (05b1506) to head (26c5945).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #55      +/-   ##
==========================================
+ Coverage   97.69%   97.76%   +0.06%     
==========================================
  Files          20       20              
  Lines        1391     1430      +39     
==========================================
+ Hits         1359     1398      +39     
  Misses         32       32              

☔ View full report in Codecov by Sentry.
📢 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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

Review Change Stack

Warning

Rate limit exceeded

@techouse has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 41 minutes and 43 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e1440a3b-7fc3-4d15-b870-17228259d64b

📥 Commits

Reviewing files that changed from the base of the PR and between cdd26c9 and 26c5945.

📒 Files selected for processing (1)
  • test/unit/utils_test.dart

Walkthrough

Adds DecodeOptions.strictMerge and wires it through options and tests; refactors comma-list splitting, bracket-notation duplicate combining, and strict list-index boundary enforcement; updates Utils.merge overflow/legacy-scalar handling and adjusts tests and docs to match new decode/merge behaviour.

Changes

QueryString Decoder strictMerge Option and List-Limit Parity

Layer / File(s) Summary
DecodeOptions strictMerge field and wiring
lib/src/models/decode_options.dart
Add public strictMerge field (default true), wire through constructor with default, copyWith override, include in toString() and props for equality.
Utils.merge strictMerge and overflow handling
lib/src/utils.dart
Replace null-only short-circuit with _isFalsyMergeSource; add strictMerge branch to handle scalar/object conflicts (wrap result vs legacy scalar-key merge), add _legacyScalarMergeKey and _isProtectedObjectKey, and derive/consider overflowMax from both source and target. Simplify combine overflow gating.
Decoder comma-list and list-limit enforcement
lib/src/extensions/decode.dart
Move [] wrapping earlier, remove maxParts limiting from initial comma split, add _promoteCommaListIfNeeded post-processing, ensure bracket-notation duplicates always combine, and change numeric-index acceptance to index < listLimit with explicit overflow marking or throwing.
Documentation: CHANGELOG and README examples
CHANGELOG.md, README.md
Document strictMerge semantics and default conflict wrapping, correct listLimit boundary example (a[0]=b with listLimit: 0 yields map), and show bracket-notation duplicate handling examples.
Unit tests: models, decode, utils, uri_extension
test/unit/models/decode_options_test.dart, test/unit/decode_test.dart, test/unit/utils_test.dart, test/unit/uri_extension_test.dart
Update tests to assert default strict wrapping order and legacy strictMerge: false behaviour, adjust list-index boundary expectations (index at limit → map), expand bracket-notation duplicate tests, add comma-list promotion/overflow tests, and confirm parameterLimit: double.infinity can succeed with throwOnLimitExceeded: true.

Sequence Diagram(s)

sequenceDiagram
  participant Parser as Decoder
  participant Comma as _parseListValue
  participant Promote as _promoteCommaListIfNeeded
  participant Merge as Utils.merge
  participant Mark as Utils.markOverflow
  Parser->>Comma: Parse comma value
  Comma->>Promote: Split and return values
  Promote->>Merge: Combine iterable with listLimit check
  Merge->>Mark: Mark overflow if limit exceeded
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • techouse/qs#26: Related changes around list/parameter limit enforcement and throwOnLimitExceeded.
  • techouse/qs#27: Related work on throwOnLimitExceeded and unlimited parameter handling.
  • techouse/qs#46: Overlaps with overflow-aware listLimit enforcement and combine/merge logic.

Suggested labels

test, documentation

Suggested reviewers

  • Copilot

"I'm a rabbit in the query tree,
I hop through brackets, numbers and key,
I wrap a conflict, keep order tight,
Promote comma lists, mark overflow right,
Hooray — decoding hops into the light!"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and accurately describes the main enhancements in the changeset: adding strictMerge decoding options, improving bracket notation handling, and adjusting list limits.
Description check ✅ Passed The description is comprehensive and well-structured, covering all key changes, improvements, and documentation updates, though it does not follow the provided template structure with sections like 'Fixes', 'Type of change', and 'Checklist'.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/more-upstream

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 and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

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 the Dart qs decoder to more closely match modern Node qs behavior, focusing on strict object/scalar merge conflicts, bracket-notation duplicate handling, and list-limit edge cases. It also expands unit coverage and updates end-user docs/changelog to reflect the new semantics.

Changes:

  • Added DecodeOptions.strictMerge (default true) and updated merge behavior for object/scalar conflicts, with a legacy mode (strictMerge: false) that restores scalar-key merging.
  • Adjusted decoding semantics for bracket notation duplicates and refined list-limit behavior (indexed brackets and comma-splitting overflow/promotion).
  • Updated and expanded unit tests, plus README/CHANGELOG documentation for the new behaviors.

Reviewed changes

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

Show a summary per file
File Description
lib/src/models/decode_options.dart Adds strictMerge option, wires it through copyWith, toString, and equality.
lib/src/utils.dart Updates core merge/combine/overflow utilities to support new strict-merge and list-limit semantics.
lib/src/extensions/decode.dart Adjusts decode pipeline for bracket-duplicate combining and post-decode comma-list promotion/limit handling.
test/unit/decode_test.dart Adds/updates decoding tests for strict-merge conflicts, bracket duplicates, list-limit boundaries, and strict overflow behavior.
test/unit/utils_test.dart Updates merge/overflow expectations and adds targeted tests for strict vs legacy merge behavior.
test/unit/models/decode_options_test.dart Verifies strictMerge is preserved/overridden via copyWith and appears in toString.
test/unit/uri_extension_test.dart Updates URI extension expectations to match revised list-limit/indexed bracket parsing semantics.
README.md Documents bracket-duplicate combining behavior and introduces strictMerge behavior and examples.
CHANGELOG.md Notes the new option and parity fixes (list limits, bracket duplicates, unlimited parameter parsing).
Comments suppressed due to low confidence (1)

lib/src/utils.dart:117

  • Utils.merge now treats several non-null scalar values ('', 0, false, NaN) as a no-op via _isFalsyMergeSource, returning the target unchanged. This causes data loss for decode merge conflicts like a[b]=c&a= under the default strictMerge: true (the empty string is dropped instead of being wrapped in a conflict list). Consider limiting the early-return check to only null/Undefined, and handling the “ignore empty/missing scalars” behavior only inside the strictMerge: false legacy branch (e.g., by using _legacyScalarMergeKey there).
      if (frame.phase == MergePhase.start) {
        final dynamic currentTarget = frame.target;
        final dynamic currentSource = frame.source;

        if (_isFalsyMergeSource(currentSource)) {
          stack.removeLast();
          frame.onResult(currentTarget);
          continue;
        }

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/unit/decode_test.dart (1)

2487-2592: ⚡ Quick win

Add a regression case for parseLists: false with a[] under strict list limits.

These new boundary tests are solid, but please also pin the learned guardrail: a[] should stay literal when parseLists is disabled and should not trigger list-limit strict throws.

Suggested test addition
+    test('parseLists=false keeps [] literal and bypasses strict list-limit growth checks', () {
+      expect(
+        QS.decode(
+          'a[]=1',
+          const DecodeOptions(
+            parseLists: false,
+            listLimit: -1,
+            throwOnLimitExceeded: true,
+          ),
+        ),
+        equals({
+          'a': {'0': '1'}
+        }),
+      );
+    });

Based on learnings: In lib/src/extensions/decode.dart, list growth gating for array-like keys must use combiningDuplicates || (options.parseLists && rawKey.endsWith('[]')), and this should be explicitly covered by tests.

🤖 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 `@test/unit/decode_test.dart` around lines 2487 - 2592, Update the list-growth
gating logic in lib/src/extensions/decode.dart so that array-like keys only
count as list growth when either combiningDuplicates is true OR
options.parseLists is true and the raw key ends with '[]' (i.e., change the
condition to combiningDuplicates || (options.parseLists &&
rawKey.endsWith('[]'))); adjust the code path that checks/increments list size
(the block that inspects rawKey, combiningDuplicates, and options.parseLists)
and add a unit test asserting that with parseLists: false an input like 'a[]='
does not trigger list-limit strict throws (it remains literal), ensuring strict
listLimit behavior only applies when parseLists is enabled.
🤖 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.

Nitpick comments:
In `@test/unit/decode_test.dart`:
- Around line 2487-2592: Update the list-growth gating logic in
lib/src/extensions/decode.dart so that array-like keys only count as list growth
when either combiningDuplicates is true OR options.parseLists is true and the
raw key ends with '[]' (i.e., change the condition to combiningDuplicates ||
(options.parseLists && rawKey.endsWith('[]'))); adjust the code path that
checks/increments list size (the block that inspects rawKey,
combiningDuplicates, and options.parseLists) and add a unit test asserting that
with parseLists: false an input like 'a[]=' does not trigger list-limit strict
throws (it remains literal), ensuring strict listLimit behavior only applies
when parseLists is enabled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5d7d6f1b-0e22-4ffc-bd1b-0b9399acd453

📥 Commits

Reviewing files that changed from the base of the PR and between 05b1506 and d5b2417.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • README.md
  • lib/src/extensions/decode.dart
  • lib/src/models/decode_options.dart
  • lib/src/utils.dart
  • test/unit/decode_test.dart
  • test/unit/models/decode_options_test.dart
  • test/unit/uri_extension_test.dart
  • test/unit/utils_test.dart

@techouse techouse merged commit 3db8a81 into main May 22, 2026
18 checks passed
@techouse techouse deleted the chore/more-upstream branch May 22, 2026 19:41
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