Skip to content

⚡ optimize decoder#51

Merged
techouse merged 3 commits intomainfrom
chore/KT/optimize-decoder
Mar 4, 2026
Merged

⚡ optimize decoder#51
techouse merged 3 commits intomainfrom
chore/KT/optimize-decoder

Conversation

@techouse
Copy link
Owner

@techouse techouse commented Mar 3, 2026

This PR ports key decode performance ideas from qs-kotlin PRs #77/#78 into qs.dart, focused on the decode hot path.

Main goal: remove the large decode bottleneck on flat query workloads (the C1/C2/C3 benchmark shapes), while preserving existing semantics and test parity.

What Changed

1. Flat-query fast path in QS.decode

  • Added structured key detection ([, ., %2E/%2e with allowDots) and early return for fully flat queries.
  • If no structured syntax is present, decode now returns Utils.compact(tempObj) directly, skipping key parsing + merge.

File:

  • lib/src/qs.dart

2. Mixed flat/structured bypass with structured-root protection

  • Added structuredRoots computation.
  • For string decode:
    • Non-structured keys that do not collide with any structured root are assigned directly.
    • Structured/colliding keys still go through _parseKeys + Utils.merge.

This keeps merge behavior for cases like a=1&a[b]=2.

File:

  • lib/src/qs.dart

3. Tokenization optimization in _parseQueryStringValues

  • Replaced broad split + take + toList flow with capped non-empty collectors.
  • Supports both string delimiters and regex delimiters.
  • Preserves parameter-limit behavior (limit + 1 probe in strict mode).
  • Added explicit guard for empty string delimiter (ArgumentError).
  • Skips empty decoded keys in tokenization (keeps existing empty-key behavior expectations).

File:

  • lib/src/extensions/decode.dart

4. Comma value split optimization

  • Replaced eager split(',') with manual capped splitting.
  • Strict mode uses remaining + 1 to detect overflow without over-allocating.
  • Non-strict mode uses remaining cap directly.
  • Preserves Dart listLimit semantics (including negative listLimit behavior currently covered by tests).

File:

  • lib/src/extensions/decode.dart

5. Decode hot-path micro-optimizations

  • Replaced hot slice usage with substring where indices are known.
  • Added fast no-op return in Utils.decode when token has no + and no %.

Files:

  • lib/src/extensions/decode.dart
  • lib/src/utils.dart

6. Benchmark harness added

  • Added a dedicated decode performance snapshot script aligned to C1/C2/C3.

File:

  • tool/decode_perf_snapshot.dart

7. Targeted decode tests added

Added coverage for:

  • Flat query behavior with non-structured keys
  • Flat query + comma + duplicates
  • Flat query + charset sentinel
  • Mixed flat/structured merge behavior
  • Empty segment handling (a=b&&c=d, trailing delimiter)
  • Empty delimiter guard
  • Parameter-limit counting with non-empty segments

File:

  • test/unit/decode_test.dart

Benchmark Results

Measured on the same machine before vs after this PR using equivalent C1/C2/C3 shapes.

Case Before (ms/op) After (ms/op) Speedup
C1 (count=100, comma=false, utf8=false, len=8) 0.349 0.028 12.5x
C2 (count=1000, comma=false, utf8=false, len=40) 21.320 0.563 37.9x
C3 (count=1000, comma=true, utf8=true, len=40) 21.093 0.590 35.8x

Validation

Ran successfully:

  • make analyze
  • make check_format
  • make test
  • make sure
  • dart run tool/decode_perf_snapshot.dart

Compatibility / API Impact

  • No public API changes.
  • No new options.
  • Decode behavior is preserved for existing test coverage, with explicit validation for flat and mixed-key paths.

@techouse techouse self-assigned this Mar 3, 2026
@techouse techouse added the enhancement New feature or request label Mar 3, 2026
@techouse techouse requested a review from Copilot March 3, 2026 08:59
@techouse
Copy link
Owner Author

techouse commented Mar 3, 2026

@codex review

@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e6d2e565-2ca8-4b97-97d8-ca43031dfb9c

📥 Commits

Reviewing files that changed from the base of the PR and between d607d33 and 794724f.

📒 Files selected for processing (19)
  • lib/src/enums/format.dart
  • lib/src/enums/list_format.dart
  • lib/src/extensions/decode.dart
  • lib/src/extensions/encode.dart
  • lib/src/extensions/extensions.dart
  • lib/src/methods.dart
  • lib/src/models/decode_options.dart
  • lib/src/models/encode_config.dart
  • lib/src/models/encode_options.dart
  • lib/src/models/key_path_node.dart
  • lib/src/models/structured_key_scan.dart
  • lib/src/models/value_sentinel.dart
  • lib/src/qs.dart
  • lib/src/utils.dart
  • test/unit/decode_test.dart
  • test/unit/extensions/extensions_test.dart
  • test/unit/models/decode_options_test.dart
  • test/unit/uri_extension_test.dart
  • tool/decode_perf_snapshot.dart
💤 Files with no reviewable changes (1)
  • test/unit/extensions/extensions_test.dart
🚧 Files skipped from review as they are similar to previous changes (5)
  • test/unit/models/decode_options_test.dart
  • test/unit/uri_extension_test.dart
  • lib/src/models/structured_key_scan.dart
  • lib/src/qs.dart
  • lib/src/models/encode_options.dart

Walkthrough

Adds structured-key detection and selective merging for string inputs; extracts comma/list‑splitting helpers; threads list‑growth/list‑limit semantics through parsing; replaces slice with substring; replaces internal encode sentinel with ValueSentinel; expands tests and adds a decode benchmark script.

Changes

Cohort / File(s) Summary
Decode helpers & parsing
lib/src/extensions/decode.dart
Introduces private helpers for comma/list splitting and non-empty-part collection; refactors _parseListValue and _parseObject to propagate isListGrowthPath and enforce list‑limit via remaining capacity; replaces slice usage with substring; adds structured-key scanning utilities.
Main decode flow & selective merge
lib/src/qs.dart
Removes export of decode_kind.dart; adds decodeFromString flag; computes StructuredKeyScan for string inputs and fast‑returns compacted map when no structured syntax; skips merging flat keys not in structured roots; forwards decodeFromString into parsing.
Structured-key model & scanning
lib/src/models/structured_key_scan.dart, lib/src/models/value_sentinel.dart
Adds StructuredKeyScan value type and new internal ValueSentinel class for sentinel payloads used during traversal and decoding.
Encode sentinel & encode path
lib/src/extensions/encode.dart
Removes private _ValueSentinel, replaces usages with ValueSentinel, updates constructions, type checks and helper signatures to the public sentinel type.
Utilities, API refinements & small edits
lib/src/utils.dart, lib/src/enums/*, lib/src/models/*, lib/src/methods.dart
Adds null/presence guards and fast paths in utils decode; wide-spread final parameter annotations across many public/internal signatures; minor formatter/list-format parameter refinements; no intended behavioural changes beyond guards.
Extensions cleanup
lib/src/extensions/extensions.dart
Removes public ListExtension.slice and StringExtension.slice; updates Iterable.whereNotType to use final lambda parameter.
Tests
test/unit/decode_test.dart, test/unit/models/decode_options_test.dart, test/unit/uri_extension_test.dart, test/unit/extensions/extensions_test.dart
Adds extensive decode tests for structured vs flat keys, custom Pattern delimiters, comma/list handling, list limits, depth and edge cases; removes tests for removed slice extensions; adds imports for decode_kind.dart.
Benchmark & tooling
tool/decode_perf_snapshot.dart
Adds benchmark script main() to snapshot QS.decode performance across predefined cases.
Minor signature/format changes
various lib/src/models/*, lib/src/extensions/*
Many copyWith, encoder/decoder and helper parameters annotated final (signature cosmetic changes); small formatting and parameter immutability tweaks across the codebase.

Sequence Diagram

sequenceDiagram
    participant Client as Input (query string)
    participant QS as QS.decode()
    participant Scan as StructuredKeyScan
    participant Parser as _parseKeys/_parseObject
    participant ListParser as _parseListValue
    participant Utils as Utils.decode()
    participant Result as Decoded map

    Client->>QS: decode(input, options)
    alt input is String
        QS->>Parser: parse into tempObj
        QS->>Scan: scanStructuredKeys(tempObj, options)
        Scan-->>QS: StructuredKeyScan (hasAnyStructuredSyntax, roots, keys)
        alt hasAnyStructuredSyntax == false
            QS->>Result: return compact(tempObj)
        else
            QS->>Parser: _parseKeys(decodeFromString=true, scan)
            Parser->>ListParser: parse list segments (pass isListGrowthPath)
            ListParser->>Utils: decode parts (uses '+'/'%' guards)
            ListParser-->>Parser: parts/lists (enforce remaining capacity/listLimit)
            Parser->>QS: merge respecting structuredRoots and list-growth
            QS->>Result: return final decoded map
        end
    else
        QS->>Parser: _parseKeys(decodeFromString=false)
        Parser->>Result: return merged object
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Poem

🐰 I hopped through commas, brackets and dots,

Helpers stitched seams in tidy little plots,
Roots found burrows where nested keys hide,
Lists grew politely with limits by my side,
I nibbled a benchmark and danced with pride.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title ':zap: optimize decoder' is vague and generic, using only an emoji and 'optimize decoder' without specifying the key changes or scope. Consider a more descriptive title such as 'Add flat-query fast path and decode hot-path optimisations' to better convey the main changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive and detailed, covering all major changes, benchmarks, validation steps, and API impact. However, the description template was not followed—required checkbox sections were omitted.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/KT/optimize-decoder

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.

@codacy-production
Copy link

codacy-production bot commented Mar 3, 2026

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
-0.28% (target: -1.00%) 96.28%
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (729f970) 1322 1296 98.03%
Head commit (d607d33) 1422 (+100) 1390 (+94) 97.75% (-0.28%)

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 (#51) 188 181 96.28%

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 Mar 3, 2026

Codecov Report

❌ Patch coverage is 96.23656% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.71%. Comparing base (729f970) to head (794724f).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
lib/src/models/structured_key_scan.dart 28.57% 5 Missing ⚠️
lib/src/extensions/decode.dart 98.18% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #51      +/-   ##
==========================================
- Coverage   98.03%   97.71%   -0.32%     
==========================================
  Files          18       20       +2     
  Lines        1322     1402      +80     
==========================================
+ Hits         1296     1370      +74     
- Misses         26       32       +6     

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

Copy link

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

Ports decode hot-path optimizations into qs_dart to improve performance on flat and mixed flat/structured query workloads while preserving Node qs semantics.

Changes:

  • Adds flat-query and mixed flat/structured fast paths in QS.decode to bypass key parsing/merge when safe.
  • Optimizes query tokenization and comma-list splitting to reduce allocations and enforce limits efficiently.
  • Adds targeted unit tests and a tool/ benchmark snapshot script for decode performance regression tracking.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tool/decode_perf_snapshot.dart Adds a standalone decode performance snapshot harness aligned to C1/C2/C3 benchmark shapes.
test/unit/decode_test.dart Adds tests covering flat fast path, mixed merge behavior, delimiter edge cases, and parameter-limit counting.
lib/src/utils.dart Adds a no-op fast path to Utils.decode when no decoding work is needed (+/% absent).
lib/src/qs.dart Introduces structured-syntax detection, structured-root protection, and fast paths in QS.decode.
lib/src/extensions/decode.dart Refactors tokenization/comma splitting into capped collectors and replaces hot slice usage with substring.
Comments suppressed due to low confidence (1)

lib/src/extensions/decode.dart:236

  • parts is built via _collectNonEmptyParts(...), which already filters out empty segments, so this if (part.isEmpty) continue; branch should be unreachable. Removing it would simplify the loop and avoid implying that empty parts can still occur here.
      final String part = parts[i];
      if (part.isEmpty) continue;
      final int bracketEqualsPos = part.indexOf(']=');

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. 👍

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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

🧹 Nitpick comments (1)
lib/src/qs.dart (1)

37-103: Consider moving structured-key helper logic into extensions/decode.dart.

_hasStructuredSyntax, _leadingStructuredRoot, and _computeStructuredRoots are decode-only concerns; relocating them would keep QS focused on orchestration.

Based on learnings: Implementation should delegate heavy lifting to private part files (extensions/decode.dart & extensions/encode.dart) rather than monolithic functions in src/qs.dart.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/qs.dart` around lines 37 - 103, Move the decode-only helper functions
_hasStructuredSyntax, _leadingStructuredRoot, and _computeStructuredRoots out of
src/qs.dart into the private extensions/decode.dart part file; implement them
there (keeping their signatures and using DecodeOptions and
_$Decode._splitKeyIntoSegments) and export or make them available to the main QS
code via the library/part directive so callers in src/qs.dart call the relocated
helpers instead of the originals; update imports/part declarations and any
references (e.g., usages that iterate tempObj or call _leadingStructuredRoot) so
compilation and behavior remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/src/extensions/decode.dart`:
- Around line 53-75: The strict listLimit check can be bypassed when an existing
scalar value is converted into a list because currentListLength is only computed
for List instances; update the logic that reads currentListLength (used where
options.throwOnLimitExceeded is checked and where _splitCommaValue is called) to
treat a non-list existing value as length 1 (i.e., if the existing entry is not
a List but is non-null, count it as one element) before comparing against
options.listLimit and before computing remaining; apply the same fix in both
places referenced (the scalar→list transition path around _splitCommaValue and
the incremental growth guard where currentListLength is checked) and keep using
_listLimitExceededMessage when throwing.

---

Nitpick comments:
In `@lib/src/qs.dart`:
- Around line 37-103: Move the decode-only helper functions
_hasStructuredSyntax, _leadingStructuredRoot, and _computeStructuredRoots out of
src/qs.dart into the private extensions/decode.dart part file; implement them
there (keeping their signatures and using DecodeOptions and
_$Decode._splitKeyIntoSegments) and export or make them available to the main QS
code via the library/part directive so callers in src/qs.dart call the relocated
helpers instead of the originals; update imports/part declarations and any
references (e.g., usages that iterate tempObj or call _leadingStructuredRoot) so
compilation and behavior remain unchanged.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 729f970 and d53a45f.

📒 Files selected for processing (5)
  • lib/src/extensions/decode.dart
  • lib/src/qs.dart
  • lib/src/utils.dart
  • test/unit/decode_test.dart
  • tool/decode_perf_snapshot.dart

@techouse techouse requested a review from Copilot March 3, 2026 19:05
Copy link

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/src/extensions/decode.dart`:
- Around line 321-329: The code misdetects list-growth when checking
part.contains('[]=') (it inspects the full "key=value" token); change the check
to inspect the parsed key only (e.g. use key.contains('[]') or
key.endsWith('[]') as appropriate) when computing listGrowthFromKey so
expressions like "a=x[]=y" are not treated as list growth; update the
listGrowthFromKey assignment that references part to use the parsed key variable
instead while leaving existingKey, combiningDuplicates, and currentListLength
logic unchanged.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fc2dadb and abbb3b5.

📒 Files selected for processing (3)
  • lib/src/extensions/decode.dart
  • lib/src/qs.dart
  • test/unit/decode_test.dart

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.

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)

430-435: ⚠️ Potential issue | 🟡 Minor

Update the _parseObject contract comment to match new strict [] growth checks.

_parseObject now forwards list-growth context into _parseListValue (Line 434), which can throw on throwOnLimitExceeded. The nearby method docs still state that this path is not enforced, so the comment is now misleading.

📝 Suggested doc-only fix
-  /// - A negative `listLimit` disables numeric‑index parsing (bracketed numbers become map keys).
-  ///   Empty‑bracket pushes (`[]`) still create lists here; this method does not enforce
-  ///   `throwOnLimitExceeded` for that path. Comma‑split growth (if any) has already been
-  ///   handled by `_parseListValue`.
+  /// - A negative `listLimit` disables numeric‑index parsing (bracketed numbers become map keys).
+  ///   Empty‑bracket pushes (`[]`) still create lists here.
+  ///   When `throwOnLimitExceeded` is true, list-growth checks are enforced via `_parseListValue`
+  ///   for list-growth paths.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/extensions/decode.dart` around lines 430 - 435, Update the
comment/contract for _parseObject to reflect that it now forwards list-growth
context into _parseListValue (via the chain '[]' check) and therefore enforces
strict list growth checks which can trigger throwOnLimitExceeded; explicitly
document that callers should expect _parseListValue to potentially throw when
options.parseLists and throwOnLimitExceeded are enabled, and note which
parameters (chain and options) convey this behavior so reviewers can locate the
relevant code paths in _parseObject and _parseListValue.
🧹 Nitpick comments (1)
tool/decode_perf_snapshot.dart (1)

108-109: Prefer decoupling benchmark output from direct print calls.

This harness currently emits console output directly. Consider returning structured rows (or injecting a reporter callback) so measurement logic stays side-effect-light and reusable.

As per coding guidelines, "Avoid printing and side-effects outside encode/decode operations".

Also applies to: 148-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tool/decode_perf_snapshot.dart` around lines 108 - 109, The direct print
calls emitting benchmark text (e.g., the string literals 'qs.dart decode perf
snapshot (median of 7 samples)' and 'Decode (public API):' and the prints around
lines 148-154) should be removed and the harness refactored to return structured
measurement rows (e.g., a list/map of results) or accept an injected reporter
callback parameter; update the function that runs the decode benchmark in
decode_perf_snapshot.dart to collect results into a data structure and either
return it or call the provided reporter with each row, ensuring no direct
console printing occurs inside the measurement logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@lib/src/extensions/decode.dart`:
- Around line 430-435: Update the comment/contract for _parseObject to reflect
that it now forwards list-growth context into _parseListValue (via the chain
'[]' check) and therefore enforces strict list growth checks which can trigger
throwOnLimitExceeded; explicitly document that callers should expect
_parseListValue to potentially throw when options.parseLists and
throwOnLimitExceeded are enabled, and note which parameters (chain and options)
convey this behavior so reviewers can locate the relevant code paths in
_parseObject and _parseListValue.

---

Nitpick comments:
In `@tool/decode_perf_snapshot.dart`:
- Around line 108-109: The direct print calls emitting benchmark text (e.g., the
string literals 'qs.dart decode perf snapshot (median of 7 samples)' and 'Decode
(public API):' and the prints around lines 148-154) should be removed and the
harness refactored to return structured measurement rows (e.g., a list/map of
results) or accept an injected reporter callback parameter; update the function
that runs the decode benchmark in decode_perf_snapshot.dart to collect results
into a data structure and either return it or call the provided reporter with
each row, ensuring no direct console printing occurs inside the measurement
logic.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between abbb3b5 and d2085a5.

📒 Files selected for processing (5)
  • lib/src/extensions/decode.dart
  • lib/src/qs.dart
  • lib/src/utils.dart
  • test/unit/decode_test.dart
  • tool/decode_perf_snapshot.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/src/utils.dart

Copy link

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

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

Comments suppressed due to low confidence (1)

lib/src/extensions/decode.dart:435

  • _parseObject only treats a list-growth path as chain.length >= 2 && chain.last == '[]'. When QS.decode is called with a pre-parsed Map<String, dynamic> (so valuesParsed == false), a root key of just '[]' will not be considered a growth path and therefore will not enforce throwOnLimitExceeded/listLimit (notably listLimit < 0 should throw on any list growth per DecodeOptions docs). This makes strict-limit behavior differ between String input and Map input.

Consider treating chain.last == '[]' && options.parseLists as a growth path even when chain.length == 1 (and keep currentListLength as 0 for the root case), so strict list-limit enforcement is consistent across input types.

    // Lazily parse comma-lists once per leaf unless the caller already did.
    dynamic leaf = valuesParsed
        ? val
        : _parseListValue(
            val,
            options,
            currentListLength,
            chain.length >= 2 && chain.last == '[]' && options.parseLists,
          );

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.

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

305-306: Remove redundant empty-part guard in the hot loop.

Line 305 is now defensive-only, because _collectNonEmptyParts already filters empty tokens before iteration. Dropping this branch simplifies the hot path slightly.

♻️ Proposed micro-refactor
-      if (part.isEmpty) continue;
       final int bracketEqualsPos = part.indexOf(']=');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/extensions/decode.dart` around lines 305 - 306, Remove the redundant
empty-part guard inside the hot loop: delete the `if (part.isEmpty) continue;`
check (the variable `part` and subsequent `final int bracketEqualsPos =
part.indexOf(']=');` are in the same loop) because `_collectNonEmptyParts`
already filters out empty tokens; ensure no other code relies on that guard and
run tests to confirm behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/src/extensions/decode.dart`:
- Around line 305-306: Remove the redundant empty-part guard inside the hot
loop: delete the `if (part.isEmpty) continue;` check (the variable `part` and
subsequent `final int bracketEqualsPos = part.indexOf(']=');` are in the same
loop) because `_collectNonEmptyParts` already filters out empty tokens; ensure
no other code relies on that guard and run tests to confirm behavior unchanged.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d2085a5 and efbac46.

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

@techouse techouse requested a review from Copilot March 3, 2026 22:13
@techouse
Copy link
Owner Author

techouse commented Mar 3, 2026

@coderabbitai resume

@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

✅ Actions performed

Reviews resumed.

Copy link

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

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

Comments suppressed due to low confidence (2)

lib/src/extensions/decode.dart:189

  • _collectNonEmptyParts still calls input.split(...) for RegExp and generic Pattern delimiters. In Dart, String.split eagerly allocates the full list of parts, so maxParts (used for parameterLimit truncation/overflow probing) doesn’t prevent work/allocation for large inputs with non-String delimiters. Consider implementing a capped splitter for RegExp/Pattern as well (iterate matches and stop at maxParts), so the optimization and DoS protection apply consistently across delimiter types.
  static List<String> _collectNonEmptyParts(
    String input,
    Pattern delimiter, {
    int? maxParts,
  }) {
    return switch (delimiter) {
      String d => _collectNonEmptyStringParts(input, d, maxParts: maxParts),
      RegExp r => _collectNonEmptyIterableParts(
          input.split(r),
          maxParts: maxParts,
        ),
      _ => _collectNonEmptyIterableParts(
          input.split(delimiter),
          maxParts: maxParts,
        ),
    };

lib/src/qs.dart:83

  • In QS.decode (string path), _hasStructuredSyntax is computed in an initial scan of tempObj.keys, and then recomputed again for each entry during the merge loop (plus _computeStructuredRoots does its own scanning). For structured queries this adds extra passes over the same keys; consider folding the initial scan into the per-entry logic (e.g., track hasAnyStructuredSyntax while building structuredRoots and/or while iterating entries) to avoid redundant key scans.
    if (decodeFromString && (tempObj?.isNotEmpty ?? false)) {
      bool hasAnyStructuredSyntax = false;
      for (final String key in tempObj!.keys) {
        if (_hasStructuredSyntax(key, options.allowDots)) {
          hasAnyStructuredSyntax = true;
          break;
        }
      }
      if (!hasAnyStructuredSyntax) {
        return Utils.compact(tempObj);
      }

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/src/extensions/decode.dart`:
- Around line 326-327: The detection of list-growth via "[]=" is currently done
unconditionally in the local variable listGrowthFromKey (uses
part.contains('[]=') || combiningDuplicates) which can trigger a RangeError
before honoring DecodeOptions.parseLists; change this logic to only consider
part.contains('[]=') when DecodeOptions.parseLists is true (but still allow
combiningDuplicates to force list growth), i.e. gate the '[]=' check behind the
parseLists option in the decode routine where listGrowthFromKey is computed; add
a regression test in test/unit/decode_test.dart that decodes "a[]=1" with
DecodeOptions(parseLists: false, throwOnLimitExceeded: true) to assert no
premature RangeError and correct behavior.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between efbac46 and b5ee29d.

📒 Files selected for processing (2)
  • lib/src/extensions/decode.dart
  • test/unit/decode_test.dart

Copy link

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

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

lib/src/qs.dart:76

  • The structured-key scan / flat fast-path doesn’t consider options.depth <= 0. When depth is 0 (or negative), _splitKeyIntoSegments will treat all keys as flat (no bracket/dot structure), so the structuredKeyScan work and the subsequent _parseKeys+merge loop are unnecessary. You can skip _scanStructuredKeys entirely (and return Utils.compact(tempObj) directly) when options.depth <= 0 to keep the flat-query optimization effective for that configuration.
    final bool decodeFromString = input is String;
    final _StructuredKeyScan structuredKeyScan =
        decodeFromString && (tempObj?.isNotEmpty ?? false)
            ? _scanStructuredKeys(tempObj!, options)
            : const _StructuredKeyScan.empty();

    if (decodeFromString && !structuredKeyScan.hasAnyStructuredSyntax) {
      return Utils.compact(tempObj!);
    }

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.

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

3226-3228: Consider simplifying the type cast.

The cast to Iterable<dynamic> followed by toList() could be simplified if the result is already a List. However, this approach is safe and handles potential edge cases where the implementation might return different iterable types.

💡 Optional simplification
-      final Iterable<dynamic> iterable = result['a'] as Iterable;
-      expect(iterable.toList(), equals(['1', '2']));
+      expect(result['a'], equals(['1', '2']));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/decode_test.dart` around lines 3226 - 3228, The test unnecessarily
casts result['a'] to Iterable<dynamic> and then calls toList(); instead, cast
directly to List<String> (or List<dynamic> if types vary) to simplify and make
intent explicit—replace the line using the local variable iterable (from
result['a'] as Iterable) with a direct typedList assignment from result['a']
(e.g., final list = result['a'] as List<String>) and assert on list, keeping the
expect(...) equals(['1','2']) assertion unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/unit/decode_test.dart`:
- Around line 3226-3228: The test unnecessarily casts result['a'] to
Iterable<dynamic> and then calls toList(); instead, cast directly to
List<String> (or List<dynamic> if types vary) to simplify and make intent
explicit—replace the line using the local variable iterable (from result['a'] as
Iterable) with a direct typedList assignment from result['a'] (e.g., final list
= result['a'] as List<String>) and assert on list, keeping the expect(...)
equals(['1','2']) assertion unchanged.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e859707 and 141d6a9.

📒 Files selected for processing (2)
  • lib/src/extensions/decode.dart
  • test/unit/decode_test.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/src/extensions/decode.dart

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/src/extensions/decode.dart`:
- Around line 267-286: The list-growth check currently gates bracket-detected
arrays behind options.parseLists, which lets inputs like a[]=1 bypass strict
list limits; update the computation of listGrowthFromKey in decode.dart (the
variable used before calling _parseListValue) to remove the options.parseLists
condition so it becomes combiningDuplicates || rawKey.endsWith('[]'), ensuring
bracket notation always signals potential list growth and that _parseListValue's
strict-limit checks (and RangeError behavior) run regardless of
options.parseLists; keep existing uses of Duplicates.combine, currentListLength,
and the call into _parseListValue unchanged.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 141d6a9 and d8188bf.

📒 Files selected for processing (5)
  • lib/src/extensions/decode.dart
  • lib/src/extensions/encode.dart
  • lib/src/models/structured_key_scan.dart
  • lib/src/models/value_sentinel.dart
  • lib/src/qs.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/src/qs.dart

@techouse
Copy link
Owner Author

techouse commented Mar 4, 2026

@coderabbitai resume

@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

✅ Actions performed

Reviews resumed.

@codacy-production
Copy link

codacy-production bot commented Mar 4, 2026

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
-0.32% (target: -1.00%) 96.24%
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (729f970) 1322 1296 98.03%
Head commit (794724f) 1402 (+80) 1370 (+74) 97.72% (-0.32%)

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 (#51) 186 179 96.24%

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

Copy link

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

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

Copy link

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

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

@techouse techouse merged commit 3d6fba7 into main Mar 4, 2026
16 of 18 checks passed
@techouse techouse deleted the chore/KT/optimize-decoder branch March 4, 2026 19:49
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