Skip to content

🐛 fix encode dot in keys#37

Merged
techouse merged 25 commits intomainfrom
fix/encode-dot-in-keys
Aug 23, 2025
Merged

🐛 fix encode dot in keys#37
techouse merged 25 commits intomainfrom
fix/encode-dot-in-keys

Conversation

@techouse
Copy link
Owner

@techouse techouse commented Aug 23, 2025

This pull request refines the query-string decoding logic to better match the reference qs library semantics and clarifies option behaviors, particularly around dot notation and bracketed keys. The changes improve how keys are split and normalized, enforce stricter option validation, and update documentation for clarity. The most important changes are grouped below.

Decoder logic and key splitting improvements

  • Refactored the key splitting algorithm in _splitKeyIntoSegments to exactly match qs semantics: unterminated brackets now cause the whole key to be treated as a single segment, and trailing or excess segments are handled more robustly. Introduced _dotToBracketTopLevel to convert top-level dots to bracket segments, preserving degenerate cases and ensuring percent-decoded dots are handled correctly.
  • Updated comments and logic in _parseObject and related areas to clarify that keys are decoded before bracket/dot splitting, and that numeric indices are only honored for bracketed segments. [1] [2]

Option validation and documentation

  • Enforced stricter validation in DecodeOptions: setting decodeDotInKeys: true while forcing allowDots: false now throws at construction time, preventing invalid configurations. Documentation for these options was expanded to clarify their interaction and effects. [1] [2] [3]
  • Improved documentation and comments for DecodeOptions fields, especially around charset sentinel handling and dot notation, to clarify subtle behaviors and edge cases. [1] [2]

Decoder API modernization

  • Deprecated legacy decoder typedefs in favor of a unified Decoder type, and provided clear migration guidance in comments.

Parameter and list limit enforcement

  • Fixed list limit checks in the decoder to ensure correct enforcement and error throwing, both for comma-separated values and incremental list growth. [1] [2]

[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16] [17]

@techouse techouse self-assigned this Aug 23, 2025
@techouse techouse added the bug Something isn't working label Aug 23, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 23, 2025

Warning

Rate limit exceeded

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

⌛ 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 69445f7 and 398ef76.

📒 Files selected for processing (1)
  • lib/src/extensions/decode.dart (12 hunks)

Walkthrough

Replaces the single decoder with kind-aware key/value decoding (Decoder + deprecated LegacyDecoder), adds depth-aware top-level dot-to-bracket conversion, refactors key segmentation/bracket handling and parameter-splitting limits, removes SplitResult, and expands tests for dot/bracket and decoder semantics.

Changes

Cohort / File(s) Summary
Decoding flow & key parsing
lib/src/extensions/decode.dart
Introduces // ignore_for_file: deprecated_member_use_from_same_package; replaces RegExp-based dot handling with _dotToBracketTopLevel; refactors _splitKeyIntoSegments, bracket/unterminated handling, _parseObject and _parseQueryStringValues to use DecodeOptions.decodeKey/decodeValue; adjusts depth/strictDepth behaviour and parameter-limit splitting; removes SplitResult typedef.
DecodeOptions API & decoder selection
lib/src/models/decode_options.dart
Adds typed, kind-aware Decoder and deprecated LegacyDecoder; constructor accepts decoder and legacyDecoder (legacy deprecated); adds decodeKey/decodeValue helpers and deprecated decoder(...) wrapper; strengthens validations (parameterLimit positive, decodeDotInKeys implies allowDots); updates copyWith, Equatable props and removes older multi-shape typedefs.
Public surface/import cleanup
lib/src/qs.dart
Removes internal import of decode_kind.dart while still re-exporting it; no outward API change.
Unit tests: decoding and dot/bracket semantics
test/unit/decode_test.dart, test/unit/models/decode_options_test.dart, test/unit/uri_extension_test.dart, test/unit/example.dart
Adds top-of-file ignore; adapts tests to new 3-arg decoder signature and legacy 2-arg path; expands tests for allowDots/decodeDotInKeys, percent-decoded dots, charset permutations, leading/trailing dots, nested lists and strictDepth behaviour; relaxes some exception-type assertions; refactors long-string tests and removes obsolete helpers.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant QS as QS.decode
  participant DO as DecodeOptions
  participant DEC as Decoder
  participant LDEC as LegacyDecoder
  participant Utils as Utils.decode

  Client->>QS: decode(query, options)
  QS->>DO: decodeKey(rawKey, charset)
  alt Decoder present
    DO->>DEC: call(rawKey, charset, kind=KEY)
    DEC-->>DO: decodedKey
  else if LegacyDecoder present
    DO->>LDEC: call(rawKey, charset)
    LDEC-->>DO: decodedKey
  else
    DO->>Utils: decode(rawKey, charset)
    Utils-->>DO: decodedKey
  end

  QS->>QS: _dotToBracketTopLevel(decodedKey)
  QS->>QS: _splitKeyIntoSegments(...)
  QS->>DO: decodeValue(rawValue, charset)
  alt Decoder present
    DO->>DEC: call(rawValue, charset, kind=VALUE)
    DEC-->>DO: decodedValue
  else if LegacyDecoder present
    DO->>LDEC: call(rawValue, charset)
    LDEC-->>DO: decodedValue
  else
    DO->>Utils: decode(rawValue, charset)
    Utils-->>DO: decodedValue
  end

  QS-->>Client: parsed object (with depth/list rules)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A rabbit nudged the dots apart,
Bracketed keys learnt to play their part,
Old paths kept for those who stay,
New decoders tell key from value’s way,
Tests hopped in and stitched the art. 🐇✨

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/encode-dot-in-keys

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@codacy-production
Copy link

codacy-production bot commented Aug 23, 2025

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
+0.02% (target: -1.00%) 96.34%
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (489b4ee) 835 792 94.85%
Head commit (398ef76) 878 (+43) 833 (+41) 94.87% (+0.02%)

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 (#37) 82 79 96.34%

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%

See your quality gate settings    Change summary preferences

@codecov
Copy link

codecov bot commented Aug 23, 2025

Codecov Report

❌ Patch coverage is 96.34146% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.87%. Comparing base (489b4ee) to head (398ef76).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/src/models/decode_options.dart 89.47% 2 Missing ⚠️
lib/src/extensions/decode.dart 98.41% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #37      +/-   ##
==========================================
+ Coverage   94.85%   94.87%   +0.02%     
==========================================
  Files          14       14              
  Lines         835      878      +43     
==========================================
+ Hits          792      833      +41     
- Misses         43       45       +2     

☔ 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.

@techouse techouse marked this pull request as ready for review August 23, 2025 12:45
Copy link

@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.

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 (2)
test/unit/decode_test.dart (1)

2182-2193: “Dynamic fallback” test does not actually miss named parameters

The helper _Loose1.call currently accepts {Encoding? charset, DecodeKind? kind}, which matches the library’s typedefs, so the dynamic fallback isn’t exercised as the comment claims. If the goal is to force the untyped “(value) only” path, change the named parameter names so they don’t match, or remove them entirely.

-class _Loose1 {
+class _Loose1 {
   final List<String?> sink;
 
   _Loose1(this.sink);
 
-  dynamic call(String? v, {Encoding? charset, DecodeKind? kind}) {
+  // Intentionally mismatched named parameters to exercise the dynamic fallback.
+  // Do not rename to `charset`/`kind`.
+  dynamic call(String? v, {Encoding? cs, Object? kd}) {
     sink.add(v);
     return v == null ? null : 'X$v';
   }
 }

Also applies to: 2564-2568

lib/src/extensions/decode.dart (1)

43-53: List limit enforcement misses combined length when splitting comma values

When val is a comma-separated string, the check only compares splitVal.length to listLimit. It should also account for currentListLength so appending "d,e" to an existing list near the limit throws correctly when throwOnLimitExceeded is true.

-    if (val is String && val.isNotEmpty && options.comma && val.contains(',')) {
-      final List<String> splitVal = val.split(',');
-      if (options.throwOnLimitExceeded && splitVal.length > options.listLimit) {
+    if (val is String && val.isNotEmpty && options.comma && val.contains(',')) {
+      final List<String> splitVal = val.split(',');
+      if (options.throwOnLimitExceeded &&
+          (currentListLength + splitVal.length) > options.listLimit) {
         throw RangeError(
           'List limit exceeded. '
           'Only ${options.listLimit} element${options.listLimit == 1 ? '' : 's'} allowed in a list.',
         );
       }
       return splitVal;
     }
 
     // Guard incremental growth of an existing list as we parse additional items.
-    if (options.throwOnLimitExceeded &&
-        currentListLength >= options.listLimit) {
+    if (options.throwOnLimitExceeded && currentListLength >= options.listLimit) {
       throw RangeError(
         'List limit exceeded. '
         'Only ${options.listLimit} element${options.listLimit == 1 ? '' : 's'} allowed in a list.',
       );
     }

Follow-up: add a test where an existing list has listLimit - 1 items and a comma-value with 2 more items is appended (should throw).

Also applies to: 55-62, 150-159

🧹 Nitpick comments (14)
test/unit/example.dart (1)

900-916: Decoder signature update looks correct; minor clean-up opportunity

The Shift-JIS decoder matches the new (String? str, {Encoding? charset, DecodeKind? kind}) shape and returns null safely. You can simplify the loop to avoid str! assertions and repeated firstMatch re-allocations.

-      Match? parts;
-      while ((parts = reg.firstMatch(str!)) != null && parts != null) {
-        result.add(int.parse(parts.group(1)!, radix: 16));
-        str = str.substring(parts.end);
-      }
+      for (Match? m = reg.firstMatch(str); m != null; m = reg.firstMatch(str)) {
+        result.add(int.parse(m.group(1)!, radix: 16));
+        str = str.substring(m.end);
+      }
test/unit/models/decode_options_test.dart (1)

169-221: Key decoding semantics: add “leading dot” coverage

The tests thoroughly cover encoded dot handling in and out of brackets. Consider adding a leading-dot case to lock in the “preserve literal leading dot” behaviour the PR description promises.

For example:

expect(const DecodeOptions(allowDots: true).decodeKey('.a'), equals('.a'));
expect(const DecodeOptions(allowDots: true).decodeKey('..a'), equals('..a'));

Want me to push a test for this?

test/unit/uri_extension_test.dart (1)

1309-1332: Custom decoder including kind: good, but consider negative-path test

The custom Shift-JIS decoder now supports the kind parameter. To strengthen coverage, consider asserting that the decoder is invoked with DecodeKind.key on the key side for at least one pair in this test file (similar to what’s done elsewhere), to ensure kind flows here too.

test/unit/decode_test.dart (1)

2195-2553: C# parity suite is strong; consider adding “leading dot” and “double dot” cases

You covered trailing dots, bracket-before-dot, and encoded-dot flows. To fully capture the PR’s stated behaviour (“.a”, “a..b”), add:

  • Leading dot preserved: QS.decode('.a=x', allowDots: true) → {'.a': 'x'}.
  • Double dots: first dot literal, second splits: QS.decode('a..b=x', allowDots: true) → {'a.': {'b': 'x'}}.
lib/src/extensions/decode.dart (3)

255-261: Doc/implementation drift: extra %2E handling after decodeKey

Comments state “keys have already been percent‑decoded earlier by decodeKey”, yet this block still replaces %2E/%2e. If decodeKey guarantees percent-decoding (as the tests imply), this is dead code; if not, it creates a split responsibility.

  • Either remove the %2E replacement here and centralise percent-decoding in DecodeOptions.decodeKey, or
  • Update the comment to reflect that decodeKey may return raw tokens for custom decoders, and we normalise encoded dots inside bracket segments here.

If you choose the first option:

-        final String decodedRoot = options.decodeDotInKeys
-            ? cleanRoot.replaceAll('%2E', '.').replaceAll('%2e', '.')
-            : cleanRoot;
+        final String decodedRoot = cleanRoot;

379-397: Remainder-wrapping behaviour is correct but deserves an inline comment

Using '[${key.substring(open)}]' yields a double-bracket token (e.g., [[g][h]]), which subsequently becomes a single literal segment "[g][h]" after trimming in _parseObject. This matches the tests; a one-line comment would prevent future “fixes”.

-      segments.add('[${key.substring(open)}]');
+      // Wrap the remaining bracket groups as a single literal segment.
+      // Example: key="a[b][c][d]", depth=2 → segment="[[c][d]]" which becomes "[c][d]" later.
+      segments.add('[${key.substring(open)}]');

91-107: Parameter limit logic is sound; slight simplification possible

You can compute overflow first and short-circuit without materialising limit + 1 entries. Purely optional.

-    if (limit != null && limit > 0) {
-      final int takeCount = options.throwOnLimitExceeded ? limit + 1 : limit;
-      final int count =
-          allParts.length < takeCount ? allParts.length : takeCount;
-      parts = allParts.sublist(0, count);
-      if (options.throwOnLimitExceeded && allParts.length > limit) {
+    if (limit != null && limit > 0) {
+      if (options.throwOnLimitExceeded && allParts.length > limit) {
         throw RangeError(
           'Parameter limit exceeded. Only $limit parameter${limit == 1 ? '' : 's'} allowed.',
         );
-      }
+      }
+      parts = allParts.sublist(0, allParts.length < limit ? allParts.length : limit);
     } else {
       parts = allParts;
     }
lib/src/models/decode_options.dart (7)

19-21: Docs say “will throw”, but implementation uses asserts (won’t run in release builds)

As written, this will not throw in release modes; it’s enforced via asserts only. Please reword to avoid promising a runtime exception.

Apply this doc tweak:

-///   set `allowDots: false` — which is an invalid combination and will throw at construction time.
+///   set `allowDots: false` — which is an invalid combination and is checked
+///   via asserts at construction time (debug/profile builds).

76-88: Validation via asserts is coherent, but consider tightening edge cases

  • Good: charset whitelist, allowDots/decodeDotInKeys coupling, positive parameterLimit.
  • Potential follow‑ups:
    • Depth/listLimit invariants aren’t validated; consider asserting they’re positive to catch misconfig early.
    • parameterLimit is a num; if only integers are meaningful downstream, consider int to prevent fractional inputs.

Example tightening (non‑breaking; asserts only):

         assert(
           charset == utf8 || charset == latin1,
           'Invalid charset',
         ),
         assert(
           !(decodeDotInKeys ?? false) || allowDots != false,
           'decodeDotInKeys requires allowDots to be true',
         ),
         assert(
           parameterLimit > 0,
           'Parameter limit must be positive',
-        );
+        ),
+        assert(depth > 0, 'Depth must be positive'),
+        assert(listLimit > 0, 'List limit must be positive');

If you want hard runtime failures, we’ll need a non‑const constructor or a factory that performs runtime checks — happy to sketch that if desired.


129-136: Re‑word to avoid promising a throw in release builds

Same note as earlier: this invalid combination is guarded by asserts, not guaranteed throws.

Doc tweak:

-/// Passing `decodeDotInKeys: true` while forcing `allowDots: false` is an
-/// invalid combination and will throw *at construction time*.
+/// Passing `decodeDotInKeys: true` while forcing `allowDots: false` is an
+/// invalid combination and is rejected via asserts at construction time
+/// (debug/profile builds).

184-204: Unify charset fallback in legacy path

decode() falls back to this.charset only for Utils.decode; the legacy decoder currently receives a potentially null charset. For symmetry and back‑compat, pass the effective charset to the legacy decoder too.

Apply:

-    if (_legacyDecoder != null) {
-      return _legacyDecoder!(value, charset: charset);
-    }
+    if (_legacyDecoder != null) {
+      return _legacyDecoder!(value, charset: charset ?? this.charset);
+    }

228-236: Deprecated decoder(...) shim is helpful for migration

Clear deprecation message pointing to the new decode() signature. Consider adding an @internal TODO to remove in the next major.


255-257: Expose throwOnLimitExceeded in copyWith; preserve decoders

Decoders are carried over correctly. However, copyWith can’t toggle throwOnLimitExceeded today. Suggest adding it for parity with the constructor and props.

Proposed change:

   DecodeOptions copyWith({
     bool? allowDots,
     bool? allowEmptyLists,
     int? listLimit,
     Encoding? charset,
     bool? charsetSentinel,
     bool? comma,
     bool? decodeDotInKeys,
     Pattern? delimiter,
     int? depth,
     Duplicates? duplicates,
     bool? ignoreQueryPrefix,
     bool? interpretNumericEntities,
     num? parameterLimit,
     bool? parseLists,
     bool? strictNullHandling,
     bool? strictDepth,
+    bool? throwOnLimitExceeded,
     Decoder? decoder,
     LegacyDecoder? legacyDecoder,
   }) =>
       DecodeOptions(
         allowDots: allowDots ?? this.allowDots,
         allowEmptyLists: allowEmptyLists ?? this.allowEmptyLists,
         listLimit: listLimit ?? this.listLimit,
         charset: charset ?? this.charset,
         charsetSentinel: charsetSentinel ?? this.charsetSentinel,
         comma: comma ?? this.comma,
         decodeDotInKeys: decodeDotInKeys ?? this.decodeDotInKeys,
         delimiter: delimiter ?? this.delimiter,
         depth: depth ?? this.depth,
         duplicates: duplicates ?? this.duplicates,
         ignoreQueryPrefix: ignoreQueryPrefix ?? this.ignoreQueryPrefix,
         interpretNumericEntities:
             interpretNumericEntities ?? this.interpretNumericEntities,
         parameterLimit: parameterLimit ?? this.parameterLimit,
         parseLists: parseLists ?? this.parseLists,
         strictNullHandling: strictNullHandling ?? this.strictNullHandling,
         strictDepth: strictDepth ?? this.strictDepth,
+        throwOnLimitExceeded:
+            throwOnLimitExceeded ?? this.throwOnLimitExceeded,
         decoder: decoder ?? _decoder,
         legacyDecoder: legacyDecoder ?? _legacyDecoder,
       );

Also applies to: 276-278


280-299: Include throwOnLimitExceeded in toString for diagnostics

It’s in props and constructor but omitted from toString, which hinders debugging.

Suggested addition:

   String toString() => 'DecodeOptions(\n'
       '  allowDots: $allowDots,\n'
       '  allowEmptyLists: $allowEmptyLists,\n'
       '  listLimit: $listLimit,\n'
       '  charset: $charset,\n'
       '  charsetSentinel: $charsetSentinel,\n'
       '  comma: $comma,\n'
       '  decodeDotInKeys: $decodeDotInKeys,\n'
       '  delimiter: $delimiter,\n'
       '  depth: $depth,\n'
       '  duplicates: $duplicates,\n'
       '  ignoreQueryPrefix: $ignoreQueryPrefix,\n'
       '  interpretNumericEntities: $interpretNumericEntities,\n'
       '  parameterLimit: $parameterLimit,\n'
       '  parseLists: $parseLists,\n'
       '  strictDepth: $strictDepth,\n'
-      '  strictNullHandling: $strictNullHandling\n'
+      '  strictNullHandling: $strictNullHandling,\n'
+      '  throwOnLimitExceeded: $throwOnLimitExceeded\n'
       ')';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 489b4ee and 61e8b05.

📒 Files selected for processing (7)
  • lib/src/extensions/decode.dart (11 hunks)
  • lib/src/models/decode_options.dart (10 hunks)
  • lib/src/qs.dart (0 hunks)
  • test/unit/decode_test.dart (8 hunks)
  • test/unit/example.dart (1 hunks)
  • test/unit/models/decode_options_test.dart (2 hunks)
  • test/unit/uri_extension_test.dart (4 hunks)
💤 Files with no reviewable changes (1)
  • lib/src/qs.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (14)
test/unit/models/decode_options_test.dart (1)

144-167: Constructor/copyWith guardrails are well-covered

Good assertions for inconsistent allowDots/decodeDotInKeys combinations at construction and via copyWith. The anyOf matcher keeps the suite resilient across platforms/assertion modes.

test/unit/uri_extension_test.dart (2)

1160-1177: Dual decoder paths exercised correctly (legacy + new)

Great separation of legacyDecoder (2-arg) and decoder (3-arg with kind). This meaningfully asserts backward compatibility while validating the new API.


1463-1471: Null-returning decoder with latin1 + interpretNumericEntities

This is a good edge-case test confirming null-propagation and numeric entity interpretation precedence. No action needed.

test/unit/decode_test.dart (2)

206-224: Comma + listLimit throwing path: nice targeted failure message

Clear assertion on the thrown message; it will catch regressions in the limit check.


1270-1281: Number-aware decoder (3-arg) path looks good

The 3-arg decoder with kind parameter behaves correctly and is used in both 0 and 1 numeric cases. No changes needed.

lib/src/models/decode_options.dart (9)

1-1: File‑level ignore is justified

The file defines and consumes a deprecated public typedef for back‑compat. The scoped ignore is reasonable here.


43-49: Deprecation path for LegacyDecoder looks good

Clear migration guidance and a dedicated typedef minimise friction for adopters.


92-95: allowDots defaulting clarified

Good clarification that decodeDotInKeys implies allowDots unless explicitly set false (and that such a combination is invalid).


111-114: Charset sentinel “first occurrence” semantics documented

The explicit “first occurrence” rule and omission from the result map reduce ambiguity. Nice.


179-183: Private decoder fields are well factored

Keeping the primary and legacy decoders separate simplifies the selection logic and equatability.


206-216: decodeKey() coercion to String via toString() is acceptable

This keeps the API forgiving even if a custom decoder returns a non‑String. Documented behaviour is implicit; all good.


217-227: decodeValue() helper is straightforward

Thin wrapper keeps call sites tidy and encourages kind‑aware decoders.


320-321: Equatable props extended with _legacyDecoder — good

Ensures equality reflects decoder identity changes across both paths.


54-57: No matches found — manual verification required

I ran the provided ripgrep checks and did not locate any occurrences of:

  • The instance method .decoder(...)
  • The LegacyDecoder type or legacyDecoder: parameter
  • Any instantiation of DecodeOptions(

However, absence of output is not evidence of absence. Please manually confirm that no callers—particularly in test suites, example/ directories, or downstream packages—still rely on the removed multi-shape decoder(...) typedef or the legacyDecoder parameter. Ensuring these call sites are updated will guarantee the change remains safe and fully back-compatible.

Copy link

@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.

Actionable comments posted: 0

♻️ Duplicate comments (1)
lib/src/extensions/decode.dart (1)

401-464: Top-level dot splitter preserves degenerate cases and fixes the leading-dot issue

Depth-aware conversion, leading-dot preservation, duplicate/trailing dot handling, and “.[” degeneracy are all handled cleanly. This addresses the prior concern about leading '.' being preserved as a literal.

🧹 Nitpick comments (5)
lib/src/extensions/decode.dart (4)

45-61: Guard comma-split lists against cumulative overflow, not only per-token length

When comma == true, the limit check only considers splitVal.length. This can be bypassed when the same key appears multiple times with comma-separated values, cumulatively exceeding listLimit. Use the already-available currentListLength to enforce the projected size.

Apply:

-      if (options.throwOnLimitExceeded && splitVal.length > options.listLimit) {
+      if (options.throwOnLimitExceeded &&
+          currentListLength + splitVal.length > options.listLimit) {
         throw RangeError(
           'List limit exceeded. '
           'Only ${options.listLimit} element${options.listLimit == 1 ? '' : 's'} allowed in a list.',
         );
       }

94-104: Avoid allocating extra items when throwing on parameter overflow

Minor: when throwOnLimitExceeded is true, you can detect allParts.length > limit and throw before slicing, avoiding the limit + 1 sublist. This is micro-optimisation and simplifies reasoning.

Suggested change:

-      final int takeCount = options.throwOnLimitExceeded ? limit + 1 : limit;
-      final int count =
-          allParts.length < takeCount ? allParts.length : takeCount;
-      parts = allParts.sublist(0, count);
-      if (options.throwOnLimitExceeded && allParts.length > limit) {
+      if (options.throwOnLimitExceeded && allParts.length > limit) {
         throw RangeError(
           'Parameter limit exceeded. Only $limit parameter${limit == 1 ? '' : 's'} allowed.',
         );
       }
+      parts = allParts.sublist(0, limit);

252-261: Redundant %2E replacement likely unnecessary post-decoding

By this stage, keys have already been percent-decoded via decodeKey, so cleanRoot should not contain %2E/%2e. Keeping the replacement is harmless (supports custom decoders that skip %2E), but consider documenting this rationale inline to avoid confusion for future maintainers.


379-396: Strict-depth branch: add an explicit test for “text after depth + more groups”

You already cover many depth cases. Add a test where the key has trailing text and additional groups beyond maxDepth (e.g., a[b][c]tail[d] at depth 2 with strictDepth: true) to validate this throw path.

I can draft an additional unit test if useful.

test/unit/decode_test.dart (1)

30-92: Anchoring tests to specific line numbers is brittle

The comment “targets lines 154–156 in decode.dart” will drift quickly. Consider replacing the line reference with a brief behavioural description only.

-  // This test targets lines 154-156 in decode.dart
+  // This test exercises nested list handling inside _parseObject.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 61e8b05 and e1a29c9.

📒 Files selected for processing (2)
  • lib/src/extensions/decode.dart (11 hunks)
  • test/unit/decode_test.dart (8 hunks)
🔇 Additional comments (12)
lib/src/extensions/decode.dart (5)

142-159: Key/value decoding order and null-to-empty coercion look correct

Decoding keys with decodeKey(...)??: '' and values with decodeValue after _parseListValue aligns with the documented semantics and keeps kind-awareness intact.


262-281: Index handling matches qs semantics and tests

The bracketed-numeric test (root != decodedRoot gating) correctly disambiguates bare numeric keys, and index <= listLimit enforces the upper bound as expected.


318-399: Depth, unterminated groups, and trailing text handling are solid

  • depth == 0 returns a single segment (qs parity).
  • Unterminated [ returns the whole key as a literal segment.
  • Trailing text after the last balanced group becomes one final bracket segment (ignores lone trailing .).
  • For extra groups beyond maxDepth, throwing under strictDepth is correct; otherwise collapsing remainder into a single bracket segment preserves content (e.g., [[g][h]]).

470-511: Fast-path normalisation of %5B/%5D is clear and efficient

Single-pass rewrite for bracket tokens (upper/lower case) improves downstream splitting without over-decoding. Good balance of correctness and performance.


139-159: slice extension confirmed – no action required

The slice method is provided by the StringExtension in lib/src/extensions/extensions.dart (extension declared on line 69, String slice(int start, [int? end])), covering all uses in decode.dart.

test/unit/decode_test.dart (7)

16-28: Parameter-limit test now tolerates platform/throw variations

Accepting ArgumentError | StateError | AssertionError is pragmatic and should reduce false negatives across environments.


206-224: Good coverage for comma + list-limit throw path

The assertion message matches the implementation (“Only 3 elements allowed in a list.”). This aligns with the refactor in _parseListValue.


2195-2261: C# parity: top-level encoded dot and bracket-segment behaviours are well exercised

These cases comprehensively verify:

  • top-level %2E/%2e splitting with allowDots=true (regardless of decodeDotInKeys),
  • bracket %2E/%2e mapping to '.' across charsets,
  • the invalid combo allowDots=false + decodeDotInKeys=true.

2462-2470: Lower-case %2e top-level split is explicitly tested

Nice catch to ensure case-insensitive behaviour remains intact for encoded dots.


2496-2516: Leading dot and double-dot degeneracy tests validate the new splitter

The “leading dot preserved” and “double dot” expectations match the refined _dotToBracketTopLevel. Great edge coverage.


2042-2052: Legacy 2‑arg decoder compatibility test is concise and effective

Ensures legacyDecoder integration still functions without impacting the new 3‑arg path.


2586-2590: Dynamic callable fallback coverage is valuable

This test ensures the dynamic ladder in DecodeOptions.decoder remains robust when named parameters don’t match.

Copy link

@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.

Actionable comments posted: 0

🧹 Nitpick comments (4)
test/unit/decode_test.dart (2)

30-92: Make this test resilient to code refactors; remove brittle file/line references.

The comment “targets lines 154-156 in decode.dart” will rot quickly. Also, the note “Decode it with the existing result as the input” is misleading: the code decodes queryString4 independently. Consider tightening comments to describe behaviour, not implementation line numbers.

Apply this diff to clarify:

-  // This test targets lines 154-156 in decode.dart
-  // We need to create a scenario where val is a List and parentKey exists in the list
+  // Exercise the _parseObject branch that handles nested lists and parent indices.
+  // Create scenarios where `val` is a List and verify index-based insertion/compaction.
@@
-  // Decode it with the existing result as the input
+  // Decode it separately; ensure compaction yields only the provided element

1502-1518: Percent-decoder in this test ignores non-encoded bytes; make it robust.

The loop only consumes “%XX” fragments and drops any intervening plain text. For safety and clarity, parse sequentially so both encoded and literal bytes survive. This improves future-proofing if the fixture ever contains mixed content.

Apply this diff:

-String? decode(String? str, {Encoding? charset, DecodeKind? kind}) {
-  if (str == null) {
-    return null;
-  }
-
-  final RegExp reg = RegExp(r'%([0-9A-F]{2})', caseSensitive: false);
-  final List<int> result = [];
-  Match? parts;
-  while ((parts = reg.firstMatch(str!)) != null && parts != null) {
-    result.add(int.parse(parts.group(1)!, radix: 16));
-    str = str.substring(parts.end);
-  }
-  return ShiftJIS().decode(
-    Uint8List.fromList(result),
-  );
-}
+String? decode(String? s, {Encoding? charset, DecodeKind? kind}) {
+  if (s == null) return null;
+  final bytes = <int>[];
+  for (int i = 0; i < s.length; ) {
+    final c = s.codeUnitAt(i);
+    if (c == 0x25 /* '%' */ && i + 2 < s.length) {
+      final h1 = s.codeUnitAt(i + 1), h2 = s.codeUnitAt(i + 2);
+      int d(int u) => (u >= 0x30 && u <= 0x39) ? u - 0x30
+          : (u | 0x20) >= 0x61 && (u | 0x20) <= 0x66 ? (u | 0x20) - 0x61 + 10
+          : -1;
+      final hi = d(h1), lo = d(h2);
+      if (hi >= 0 && lo >= 0) {
+        bytes.add((hi << 4) | lo);
+        i += 3;
+        continue;
+      }
+    }
+    bytes.add(c);
+    i++;
+  }
+  return ShiftJIS().decode(Uint8List.fromList(bytes));
+}
lib/src/extensions/decode.dart (2)

101-105: Avoid building the full allParts list when a small positive parameterLimit is set.

cleanStr.split(delimiter) materialises all parts, then slices; for very large strings this doubles memory. Consider a limited split that stops after limit parts when throwOnLimitExceeded is false, or counts to limit+1 when true.

Example approach:

  • For string delimiter: manual scan and push until limit (or limit+1 to detect overflow).
  • For RegExp delimiter: iterate allMatches lazily and stop at limit.

Keeps behaviour identical while reducing peak allocations on long inputs.


201-204: Doc/implementation mismatch around percent-decoded dots.

Comment states keys arrive percent‑decoded before this phase, so %2E/%2e shouldn’t be present. That makes the %2E handling below redundant and potentially misleading. Align the code or the comment.

Apply this diff to simplify and match the documented pipeline:

-        final String decodedRoot = options.decodeDotInKeys
-            ? cleanRoot.replaceAll('%2E', '.').replaceAll('%2e', '.')
-            : cleanRoot;
+        // `decodeKey` already percent-decodes; `%2E` should not appear here.
+        final String decodedRoot = cleanRoot;

If you prefer to keep a safeguard, add an assertion in debug mode rather than conditional replacement.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e1a29c9 and 8f47a6a.

📒 Files selected for processing (2)
  • lib/src/extensions/decode.dart (12 hunks)
  • test/unit/decode_test.dart (7 hunks)
🔇 Additional comments (12)
test/unit/decode_test.dart (4)

16-28: Good defence around invalid parameterLimit (multiple exception types accepted).

Covers ctor-time/runtime variants cleanly; aligns with stricter options validation.


206-224: List limit enforcement for comma-splitting is precise and user-facing error is clear.

Message matches library behaviour; good job asserting the exact string.


2042-2052: Decoder API migration tests (legacy 2‑arg and new 3‑arg) are crisp.

They verify backwards compatibility and that kind-aware decoding is now the default path. Nice.


2181-2561: Excellent dot-notation parity coverage (top-level, bracketed, case-insensitive, charset).

These tests lock in nuanced behaviour: leading/trailing dots, duplicate dots, encoded dots before/after bracket segments, invalid option combinations, and kind propagation. Strong guardrails for future refactors.

lib/src/extensions/decode.dart (8)

20-23: Good contract: avoid mutating caller containers.

Clear and important guarantee; matches how objects/lists are freshly allocated and merged.


46-62: List-limit checks are now correct for comma-splits and incremental growth.

  • Split-time: throws when current + new > limit.
  • Incremental: throws when current >= limit.

Matches tests and avoids off-by-one.


265-281: Index semantics correctly restrict list indices to bracketed numerics and <= listLimit.

Prevents accidental list creation from bare numeric keys; mirrors qs. Good attention to detail.


325-333: Depth=0 short-circuit is correct and cheap.

Returning the whole key ensures parity with qs; avoids unnecessary scanning.


346-399: Balanced-group splitter: robust handling of unterminated brackets, trailing text, and strictDepth.

  • Unterminated group returns the entire key as a literal (matches qs).
  • Trailing text is wrapped as one segment (except a lone '.').
  • Remainder beyond maxDepth throws when strictDepth.

Well-structured and readable.


404-468: Top-level dot splitter preserves degenerate cases correctly.

Leading dot literal, “a..b” partial literal, trailing dot ignored by splitter, and “.[” normalised — all covered. Matches the new tests.


469-517: Fast-path bracket normalisation is a nice win.

Single pass, case-insensitive %5B/%5D rewrite keeps downstream parsing simple and fast.


140-161: String.slice Extension Verified

The JS-style String.slice extension is implemented in lib/src/extensions/extensions.dart (lines 69–85), so all calls to part.slice(…) will compile successfully.
No further changes are required.

Copy link

@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.

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
test/unit/decode_test.dart (1)

1093-1132: Clarify negative listLimit semantics vs throwOnLimitExceeded

The existing tests show that:

  • With listLimit: -1 and throwOnLimitExceeded: false (the default), numeric bracket indices are always treated as map keys (i.e. “disable list growth/force map”) – see the “allows overriding list limit” tests at lines 1093–1132.
  • With listLimit: -1 and throwOnLimitExceeded: true, any list growth (e.g. empty-bracket pushes or comma-splits) immediately throws a RangeError – see the “handles negative list limit correctly” test at lines 2012–2019.

These semantics are intentional but aren’t currently documented, which can be surprising. I recommend updating the inline API docs to spell out that:

  • A negative listLimit disables numeric-index parsing (all numbered brackets become map keys).
  • However, when throwOnLimitExceeded is true, any list-growth operation—empty brackets (a[]), comma-separated values, or nested pushes—will throw if the (implicit) limit of –1 is exceeded.

Please update:

  • lib/src/models/decode_options.dart
    • In the doc comment for listLimit, note that negative values disable numeric-indexed lists.
    • In the doc for throwOnLimitExceeded, clarify that it applies to all list-growth paths, even when listLimit is negative.

  • lib/src/extensions/decode.dart
    • In the extension’s method docs, call out how throwOnLimitExceeded interacts with negative listLimit for empty-bracket and comma-split list growth.

This makes the contract crystal-clear and aligns the documentation with the verified test coverage.

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

30-32: Avoid coupling test names to private implementation details

The test name references a private implementation detail (_parseObject). Prefer behaviour-focused wording to keep tests resilient to internal refactors.

Apply this diff to rename the test:

-    test('Nested list handling in _parseObject method', () {
+    test('Nested list handling preserves nested lists and compaction behaviour', () {

216-221: Make error-message assertion less brittle

Asserting the exact error message string is fragile and can break on minor copy changes. Match a stable substring instead.

-          isA<RangeError>().having(
-            (e) => e.message,
-            'message',
-            'List limit exceeded. Only 3 elements allowed in a list.',
-          ),
+          isA<RangeError>().having(
+            (e) => e.message,
+            'message',
+            contains('List limit exceeded'),
+          ),

1297-1313: Remove duplicated test block

This block duplicates the preceding test case (same input, same expectation). It increases runtime and maintenance without adding coverage.

-      expect(
-        QS.decode('foo[]=1,2,3&foo[]=', const DecodeOptions(comma: true)),
-        equals({
-          'foo': [
-            ['1', '2', '3'],
-            ''
-          ]
-        }),
-      );

1053-1059: Reduce work in the “very long list” generator

Inside the loop, utf8.encode(str.toString()) re-encodes the growing buffer every iteration, which is quadratic in practice. This is fine functionally, but you can keep the test fast and deterministic by growing an ordinary String first, then wrapping it in a StringBuffer if needed.

Example alternative:

var s = 'a[]=a';
while (utf8.encode(s).length < 128 * 1024) {
  s = '$s&$s';
}
expect(() => QS.decode(s), returnsNormally);

1502-1526: Handle “+” as space in the custom Shift-JIS decoder

URL-encoded query semantics typically treat “+” as a space for x-www-form-urlencoded. Your custom decoder ignores it; harmless for this test input, but easy to make correct.

       for (int i = 0; i < s.length;) {
         final c = s.codeUnitAt(i);
         if (c == 0x25 /* '%' */ && i + 2 < s.length) {
           final h1 = s.codeUnitAt(i + 1), h2 = s.codeUnitAt(i + 2);
           int d(int u) => switch (u) {
                 >= 0x30 && <= 0x39 => u - 0x30,
                 >= 0x61 && <= 0x66 => u - 0x61 + 10,
                 >= 0x41 && <= 0x46 => u - 0x41 + 10,
                 _ => -1,
               };
           final hi = d(h1), lo = d(h2);
           if (hi >= 0 && lo >= 0) {
             bytes.add((hi << 4) | lo);
             i += 3;
             continue;
           }
         }
+        if (c == 0x2B /* '+' */) {
+          bytes.add(0x20); // space
+          i++;
+          continue;
+        }
         bytes.add(c);
         i++;
       }

2399-2408: Deduplicate overlapping C#‑parity dot‑handling tests

These four tests materially overlap with earlier cases in the same group (which already cover plain dot, encoded dot upper/lower, and bracket segments). Removing them keeps the suite lean without reducing coverage.

Remove the redundant cases:

-    test(
-        'top-level encoded dot splits when allowDots=true, decodeDotInKeys=true',
-        () {
-      const opt = DecodeOptions(allowDots: true, decodeDotInKeys: true);
-      expect(
-          QS.decode('a%2Eb=c', opt),
-          equals({
-            'a': {'b': 'c'}
-          }));
-    });
-    test(
-        'top-level encoded dot also splits when allowDots=true, decodeDotInKeys=false',
-        () {
-      const opt = DecodeOptions(allowDots: true, decodeDotInKeys: false);
-      expect(
-          QS.decode('a%2Eb=c', opt),
-          equals({
-            'a': {'b': 'c'}
-          }));
-    });
-    test(
-        'top-level lowercase encoded dot splits when allowDots=true (decodeDotInKeys=false)',
-        () {
-      const opt = DecodeOptions(allowDots: true, decodeDotInKeys: false);
-      expect(
-          QS.decode('a%2eb=c', opt),
-          equals({
-            'a': {'b': 'c'}
-          }));
-    });
-    test(
-        'bracket segment: encoded dot mapped to \'.\' (allowDots=true, decodeDotInKeys=true)',
-        () {
-      const opt = DecodeOptions(allowDots: true, decodeDotInKeys: true);
-      expect(
-          QS.decode('a[%2E]=x', opt),
-          equals({
-            'a': {'.': 'x'}
-          }));
-      expect(
-          QS.decode('a[%2e]=x', opt),
-          equals({
-            'a': {'.': 'x'}
-          }));
-    });

If you prefer to keep them, consider collapsing these into a parameterised helper to avoid repetition.

Also applies to: 2410-2419, 2456-2465, 2513-2527

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8f47a6a and 5e0466d.

📒 Files selected for processing (1)
  • test/unit/decode_test.dart (8 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test (chrome)
🔇 Additional comments (3)
test/unit/decode_test.dart (3)

2051-2054: Legacy 2‑arg decoder coverage looks good

Nice, this confirms DecodeOptions.legacyDecoder still routes both key and value through the legacy signature without involving DecodeKind.


2548-2569: Align decoder “key” expectations with documented order (percent‑decode before splitting)

The PR summary states keys are percent‑decoded before bracket/dot splitting. This test currently allows either the raw a%2Eb or the bracketised a[b]. If the intended contract is “decoder sees percent‑decoded key”, we should also accept a.b here.

Proposed adjustment:

-      expect(
-        calls.any((it) =>
-            it[1] == DecodeKind.key && (it[0] == 'a%2Eb' || it[0] == 'a[b]')),
-        isTrue,
-      );
+      expect(
+        calls.any((it) =>
+            it[1] == DecodeKind.key &&
+            (it[0] == 'a%2Eb' || it[0] == 'a[b]' || it[0] == 'a.b')),
+        isTrue,
+      );

If, instead, the decoder is intentionally invoked pre‑decode or post‑segmentation, the summary or doc comments may need a small tweak for clarity. I can update docs/tests accordingly once you confirm the desired sequence.


2230-2240: Invalid option combination is correctly enforced

Good to see allowDots: false + decodeDotInKeys: true throwing early and the test accepting multiple error types for cross‑platform consistency.

Copy link

@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.

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
lib/src/extensions/decode.dart (1)

268-275: Off‑by‑one in list index bound (allows listLimit+1 elements).

The check index <= options.listLimit lets index == listLimit pass, and you then allocate index + 1 slots — effectively permitting listLimit + 1 elements. Elsewhere (e.g., Line 57) the limit is enforced as a count (>= listLimit blocks further growth), so this path is inconsistent and can exceed the configured cap.

Recommend making the bound strict so the highest admissible index is listLimit - 1.

Apply this diff:

-        } else if (index != null &&
+        } else if (index != null &&
             index >= 0 &&
             root != decodedRoot &&
             index.toString() == decodedRoot &&
             options.parseLists &&
-            index <= options.listLimit) {
+            index < options.listLimit) {

Please add/adjust tests to cover:

  • listLimit: 1 with a[1]=x → should throw (strict) or be rejected (non‑throwing mode).
  • listLimit: 2 with a[2]=x → should be out of bounds.
♻️ Duplicate comments (1)
lib/src/extensions/decode.dart (1)

436-443: Leading . preservation in _dotToBracketTopLevel — LGTM and matches prior review.

The new guard keeps a leading dot literal (except the ".[..." degenerate), aligning with the documented “degenerate cases” behaviour and our previous review ask.

🧹 Nitpick comments (3)
lib/src/extensions/decode.dart (3)

46-47: Non‑throwing path can silently exceed listLimit for comma‑split values; clamp instead of ignoring.

When throwOnLimitExceeded is false, _parseListValue returns the full splitVal even if it pushes the list length past listLimit. Consider clamping to the remaining capacity to enforce limits consistently across throw/non‑throw paths.

Apply this diff:

-      return splitVal;
+      final int remaining = options.listLimit - currentListLength;
+      if (remaining <= 0) return const <String>[];
+      return splitVal.length <= remaining
+          ? splitVal
+          : splitVal.sublist(0, remaining);

This mirrors the intent described in the PR (“enforce limits correctly for comma‑separated values”) without hard throwing. Please ensure tests cover the non‑throwing path.

Also applies to: 55-63


101-104: Minor: favour take(limit) over manual sublist bounds logic.

Using take(limit).toList() is clearer and avoids computing the min yourself.

-      parts = allParts.sublist(
-        0,
-        allParts.length < limit ? allParts.length : limit,
-      );
+      parts = allParts.take(limit).toList();

201-204: Docs around encoded dots are internally inconsistent; align commentary with behaviour.

  • Line 201 suggests keys arrive “already decoded (top‑level encoded dots become literal '.')”.
  • Lines 254–265 then say percent‑encoded dots “may still be present here”, and conditionally map %2E. based on decodeDotInKeys.
  • Lines 414–416 state encoded dots are “not handled here because keys have already been decoded”.

These statements contradict. Given the code, the intended flow appears to be:

  • DecodeOptions.decodeKey may choose to leave %2E encoded when decodeDotInKeys is false (so top‑level dot splitting won’t accidentally trigger).
  • _dotToBracketTopLevel only splits literal . at depth 0.
  • Inside bracket segments, you optionally map %2E. here when decodeDotInKeys is true.

Recommend tightening the comments to reflect this exact flow.

Apply this doc tweak:

-  /// - Percent‑encoded dots are not handled here because keys have already been decoded by
-  ///   `DecodeOptions.decodeKey` (defaulting to percent‑decoding).
+  /// - Percent‑encoded dots may be left encoded by `DecodeOptions.decodeKey` when
+  ///   `decodeDotInKeys` is false; only literal `.` are considered for splitting here.

And at Line 201, consider:

-  /// - Keys arrive already decoded (top‑level encoded dots become literal `.` before we get here).
+  /// - Keys have been decoded per `DecodeOptions.decodeKey`; top‑level splitting applies to
+  ///   literal `.` only. Encoded dots can remain encoded depending on `decodeDotInKeys`.

Please also add/confirm tests covering:

  • allowDots: true, decodeDotInKeys: false where %2E at top level must not split.
  • allowDots: true, decodeDotInKeys: true where a top‑level %2E behaves like a literal dot.

Also applies to: 254-265, 414-416

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5e0466d and e6924e2.

📒 Files selected for processing (1)
  • lib/src/extensions/decode.dart (12 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Security Scan
🔇 Additional comments (3)
lib/src/extensions/decode.dart (3)

383-393: Edge case: trailing remainder "." is ignored — confirm multi‑dot tails are preserved as intended.

You special‑case remainder == '.' to drop it. For inputs like a[b]. this matches the comment. For a[b].. the remainder becomes ".." and will be wrapped as "[..]" (literal), which seems correct; just ensure tests lock this in so future refactors don’t regress double‑dot handling.

Suggested tests:

  • QS.decode('a.b.=x', allowDots: true){'a': {'b': 'x'}} (single trailing dot ignored).
  • QS.decode('a.b..=x', allowDots: true){'a': {'b.': 'x'}} (double dot: first is literal).

145-150: No changes needed: slice extension is in scope

The StringExtension.slice method is defined in lib/src/extensions/extensions.dart and lib/src/qs.dart imports this file, making slice available in all its part files, including decode.dart. Therefore, replacing slice with substring is unnecessary.

• Extension definition: lib/src/extensions/extensions.dart, lines 69–86
• Import in main library: lib/src/qs.dart imports package:qs_dart/src/extensions/extensions.dart

Likely an incorrect or invalid review comment.


300-312: ValuesParsed flag is correctly applied; no changes needed

All call sites to _parseKeys originate from QS.decode in lib/src/qs.dart, where it’s invoked as:

  • _$Decode._parseKeys(entry.key, entry.value, options, input is String) (lib/src/qs.dart lines 86–88).
    Since input is String here, valuesParsed is always true for raw query‐string inputs, preventing any double list parsing.

No other direct calls to _parseKeys or _parseObject exist in the codebase, so the valuesParsed guard fully covers all paths.

…ching, and long input parsing; fix percent-decoding to handle '+' as space
…th negative listLimit in decode logic comments
@techouse techouse merged commit 2260f84 into main Aug 23, 2025
14 checks passed
@techouse techouse deleted the fix/encode-dot-in-keys branch August 23, 2025 15:11
@coderabbitai coderabbitai bot mentioned this pull request Sep 26, 2025
@coderabbitai coderabbitai bot mentioned this pull request Mar 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant