Skip to content

Conversation

techouse
Copy link
Owner

@techouse techouse commented Aug 26, 2025

This pull request updates the test suite for the QsNet library, primarily focusing on improving exception handling and expanding coverage for edge cases in decoding and encoding. The most significant changes involve replacing IndexOutOfRangeException with InvalidOperationException for various error scenarios, adding new tests for comma-split and dot behavior in keys, and increasing coverage for Unicode and numeric entity decoding. Additionally, some minor code cleanups and improvements were made.

Exception Handling Improvements

  • Updated multiple tests in QsNet.Tests/DecodeTests.cs and QsNet.Tests/EncodeTests.cs to expect InvalidOperationException instead of IndexOutOfRangeException when decoding or encoding fails due to limits, invalid options, or circular references. This makes error reporting more consistent and accurate. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14] [15] [16]

Expanded Test Coverage for Decoding Features

  • Added new tests for comma-split behavior, including scenarios where list limits are exceeded, concatenation is allowed, and nested lists are formed. Also added tests for dot-in-key decoding options, clarifying how plain and encoded dots are handled. [1] [2] [3] [4]
  • Improved error message assertions for dot-in-key decoding to check for specific option values.

Unicode and Numeric Entity Decoding Enhancements

  • Enhanced tests in QsNet.Tests/UtilsTests.cs to verify decoding of hexadecimal numeric entities, astral Unicode characters, and mixed decimal/hex entities. Added tests for edge cases such as empty hex digits, invalid hex entities, and surrogate pairs. [1] [2] [3]

Minor Codebase Improvements

  • Added missing using System.Globalization; in QsNet/Internal/Decoder.cs and changed Latin1Encoding to a readonly field for efficiency. [1] [2]
  • Minor formatting and cleanup in test files, such as adding blank lines and organizing test regions. [1] [2]

These changes make the test suite more robust, clarify error handling, and ensure better coverage for complex decoding scenarios.

Summary by CodeRabbit

  • Bug Fixes

    • Error handling standardized: depth/limit, dot-in-keys and cyclic-reference errors now raise InvalidOperationException with clearer edge-case behavior.
  • New Features

    • Decoder now recognizes hexadecimal numeric entities (e.g., A → A) including surrogate-pair decoding.
  • Improvements

    • Culture-invariant numeric/index formatting and conditional trailing delimiter to avoid stray ampersands.
  • Compatibility

    • Improved handling for NETSTANDARD2_0-specific key/array detection.
  • Packaging

    • Expanded NuGet metadata, source-linking and CI/deterministic build settings.
  • Tests

    • Updated and extended test coverage for errors, comma-list boundaries, hex decoding, encoding boundaries and renamed expectations.

@techouse techouse self-assigned this Aug 26, 2025
@techouse techouse added the enhancement New feature or request label Aug 26, 2025
Copy link

coderabbitai bot commented Aug 26, 2025

Walkthrough

Standardized error semantics to InvalidOperationException across encode/decode paths; switched culture-sensitive formatting/parsing to CultureInfo.InvariantCulture; tightened DecodeOptions key-decoder contract and changed DefaultDecode to return string?; improved numeric-entity (hex) decoding and surrogate handling; tests updated and expanded accordingly.

Changes

Cohort / File(s) Summary
Tests: expectations, names & coverage
QsNet.Tests/DecodeOptionsTests.cs, QsNet.Tests/DecodeTests.cs, QsNet.Tests/EncodeTests.cs, QsNet.Tests/UtilsTests.cs, QsNet.Tests/Fixtures/DummyEnum.cs
Tests updated to expect InvalidOperationException (depth/limit/dot/cycle cases), adjusted one ArgumentException expectation; several test method renames; added comma-split/list-limit boundary tests, hex numeric-entity and surrogate/UTF‑8 boundary tests; minor formatting-only fixture change; added using System.Linq.
Decoder: parsing, depth & limits
QsNet/Internal/Decoder.cs
Replaced IndexOutOfRange/Argument exceptions with InvalidOperationException for depth/limit/dot errors; SplitKeyIntoSegments now handles overflow remainders, unterminated brackets, strictDepth semantics and remainder-wrapping; NETSTANDARD2_0-aware IndexOf/Contains switch; culture-invariant integer parsing; Latin1Encoding changed to private static readonly.
Encoder: cycle & dot detection
QsNet/Internal/Encoder.cs
Cyclic/self-referential detection now throws InvalidOperationException("Cyclic object value"); NETSTANDARD2_0 conditional IndexOf('.') vs Contains('.') retained for dot detection.
Utils: invariant culture, hex & surrogates
QsNet/Internal/Utils.cs
Index-to-key formatting uses CultureInfo.InvariantCulture; hex formatting/parsing and numeric-entity parsing use invariant culture; InterpretNumericEntities now supports hex (&#x...;) and decimal, parses digits robustly, decodes surrogate pairs properly and falls back for lone surrogates; hex emitters use invariant formatting.
Options: DecodeOptions adjustments
QsNet/Models/DecodeOptions.cs
Decoder delegate XML docs clarify key-decoders must return string? or null (otherwise InvalidOperationException); DecodeDotInKeys validation now throws InvalidOperationException (message updated); DefaultDecode signature changed from object?string?.
Core: Qs surface & formatting
QsNet/Qs.cs
Added System.Globalization; documented exceptions updated to InvalidOperationException; index keys use ToString(CultureInfo.InvariantCulture); sentinel trailing ampersand appended conditionally.
Project: packaging & build metadata
QsNet/QsNet.csproj
Expanded packaging metadata, SourceLink, symbol packaging, deterministic/CI flags and analyzers — build/package changes only.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Caller
    participant Decoder
    participant Utils

    Caller->>Decoder: Decode(input, options)
    alt DecodeDotInKeys=true && decoding keys && AllowDots=false
        Decoder-->>Caller: throw InvalidOperationException("Invalid DecodeOptions: DecodeDotInKeys=true requires AllowDots=true when decoding keys.")
    else depth/limit exceeded (param/list/depth)
        Decoder-->>Caller: throw InvalidOperationException("...limit exceeded...")
    else
        Decoder->>Utils: Unescape / InterpretNumericEntities (decimal & hex, surrogates)
        Utils-->>Decoder: decoded values
        Decoder-->>Caller: parsed object
    end
Loading
sequenceDiagram
    autonumber
    participant Caller
    participant Encoder

    Caller->>Encoder: Encode(object)
    alt cyclic/self-referential detected
        Encoder-->>Caller: throw InvalidOperationException("Cyclic object value")
    else
        Encoder-->>Caller: encoded string
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I nibble tangles in percent-escaped dots,
I hop through hex and keep surrogates got,
One exception now speaks clear and neat,
Keys moulded invariant, tests take their seat,
A rabbit cheers—no more indexing knots! 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent 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 ade83a2 and 5c6804a.

📒 Files selected for processing (1)
  • QsNet.Tests/DecodeTests.cs (17 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • QsNet.Tests/DecodeTests.cs
⏰ 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). (4)
  • GitHub Check: Coverage (merged)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Codacy Security Scan
  • GitHub Check: Analyze (csharp)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/lint

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.

Copy link

codecov bot commented Aug 26, 2025

Codecov Report

❌ Patch coverage is 89.61039% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.22%. Comparing base (5ea801d) to head (5c6804a).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
QsNet/Internal/Utils.cs 85.41% 7 Missing ⚠️
QsNet/Internal/Decoder.cs 95.23% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #11      +/-   ##
==========================================
- Coverage   90.40%   90.22%   -0.18%     
==========================================
  Files          15       15              
  Lines        1323     1351      +28     
  Branches      374      381       +7     
==========================================
+ Hits         1196     1219      +23     
- Misses        127      132       +5     
Flag Coverage Δ
unittests 90.22% <89.61%> (-0.18%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ 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

@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 (2)
QsNet/Qs.cs (1)

29-30: Docs still reference IndexOutOfRangeException — update to InvalidOperationException

The exception type documented for “limits exceeded” is now InvalidOperationException in the implementation. Update the XML docs to avoid misleading consumers.

 /// <exception cref="ArgumentException">If the input is not a string or Dictionary</exception>
-/// <exception cref="IndexOutOfRangeException">If limits are exceeded and ThrowOnLimitExceeded is true</exception>
+/// <exception cref="InvalidOperationException">If limits are exceeded and ThrowOnLimitExceeded is true</exception>
@@
-/// <exception cref="IndexOutOfRangeException">Thrown when index is out of bounds</exception>
+/// <exception cref="InvalidOperationException">Thrown when options/limits are violated during encoding</exception>

Also applies to: 125-126

QsNet/Internal/Decoder.cs (1)

73-75: Decoder XML docs still reference IndexOutOfRangeException — update to InvalidOperationException

Update comments to reflect the current behavior and keep documentation accurate.

-/// <exception cref="IndexOutOfRangeException">If the parameter limit is exceeded and ThrowOnLimitExceeded is true.</exception>
+/// <exception cref="InvalidOperationException">If the parameter limit is exceeded and ThrowOnLimitExceeded is true.</exception>
@@
-/// <exception cref="IndexOutOfRangeException">If the depth exceeds maxDepth and strictDepth is true.</exception>
+/// <exception cref="InvalidOperationException">If the depth exceeds maxDepth and strictDepth is true.</exception>

Also applies to: 499-500

🧹 Nitpick comments (11)
QsNet/Internal/Encoder.cs (1)

373-377: Prefer ordinal-based checks and Replace overload to avoid culture-sensitive comparisons.

Small robustness tweak: for non-NETSTANDARD2_0, use StringComparison.Ordinal in Contains, and also prefer the Replace overload with StringComparison.Ordinal (guarded with the same conditional).

Within the current block:

-            if (allowDots && encodeDotInKeys && keyStr.Contains('.'))
+            if (allowDots && encodeDotInKeys && keyStr.Contains('.', StringComparison.Ordinal))

Additionally, right below (outside the changed lines), consider applying ordinal Replace on supported TFMs:

-                encodedKey = keyStr.Replace(".", "%2E");
+#if NETSTANDARD2_0
+                encodedKey = keyStr.Replace(".", "%2E");
+#else
+                encodedKey = keyStr.Replace(".", "%2E", StringComparison.Ordinal);
+#endif
QsNet/QsNet.csproj (1)

10-15: Packaging/diagnostics upgrades look solid; optionally fail CI on analyzer warnings.

Great additions: SourceLink, deterministic builds, analyzers, and richer tags. To keep quality high, consider treating warnings as errors in CI to catch analyzer regressions early.

+  <PropertyGroup>
+    <!-- Make analyzer and build warnings fail CI builds -->
+    <TreatWarningsAsErrors Condition="'$(CI)' == 'true'">true</TreatWarningsAsErrors>
+  </PropertyGroup>

Also applies to: 22-36, 38-40, 47-49

QsNet/Internal/Utils.cs (1)

790-797: Fast-path guard is good; consider supporting hex entities (&#xNNNN;).

Optional enhancement: InterpretNumericEntities currently handles only decimal entities (e.g., ©). Many sources also use hex (e.g., ©). If in scope, supporting hex would increase robustness.

Example sketch (not exhaustive; ensure bounds checks and overflow handling):

-        if (!str.Contains("&#", StringComparison.Ordinal))
+        if (!str.Contains("&#", StringComparison.Ordinal))
             return str;
...
-                if (j < n && char.IsDigit(str[j]))
+                if (j < n && (char.IsDigit(str[j]) || (str[j] is 'x' or 'X' && j + 1 < n)))
                 {
-                    var code = 0;
+                    var code = 0;
                     var startDigits = j;
-                    while (j < n && char.IsDigit(str[j]))
+                    var hex = false;
+                    if (str[j] is 'x' or 'X') { hex = true; j++; startDigits = j; }
+                    while (j < n && (hex ? Uri.IsHexDigit(str[j]) : char.IsDigit(str[j])))
                     {
-                        code = code * 10 + (str[j] - '0');
+                        code = hex
+                            ? (code << 4) + Convert.ToInt32(str[j].ToString(), 16)
+                            : code * 10 + (str[j] - '0');
                         j++;
                     }

If you’d like, I can send a fuller, allocation-free version using integer math only.

QsNet.Tests/DecodeTests.cs (2)

2279-2308: StrictDepth tests now expect InvalidOperationException — consistent with decoder changes

All four assertions changed to Throw() are in line with the decoder’s strict-depth overflow behavior. Consider asserting an indicative message (even a wildcard) to catch regressions in error source vs. other InvalidOperationException paths.


4182-4185: Dot-in-keys option conflict: double-check message casing to match assertion

The tests now expect InvalidOperationException, which matches DecodeOptions.Decode’s new validation. One caveat: the .WithMessage("decodeDotInKeysallowDots*") matcher in Line 4353 uses lowercase property names. If FluentAssertions’ WithMessage is case-sensitive in your version, the current thrown message from DecodeOptions uses “DecodeDotInKeys” / “AllowDots” (PascalCase) and may not match. Either:

  • Update the thrown message to use lowercase property names, or
  • Relax the assertion (e.g., use case-insensitive verification or a broader wildcard).

Proposed change (lowercase the property identifiers in the message):

-throw new InvalidOperationException(
-    "Invalid DecodeOptions: DecodeDotInKeys=true requires AllowDots=true when decoding keys."
-);
+throw new InvalidOperationException(
+    "Invalid DecodeOptions: decodeDotInKeys=true requires allowDots=true when decoding keys."
+);

Also applies to: 4350-4354

QsNet/Qs.cs (2)

256-263: Micro-optimization: chained Append over interpolation

Chaining Append for the charset sentinel avoids a transient string. Low risk; fine as-is.


256-267: Optional: avoid trailing ampersand when only sentinel is emitted

If CharsetSentinel is enabled and there are no parts (joined is empty), the output ends with “&” (this was already the case before the change). Consider omitting the trailing “&” for a slightly cleaner result:

- if (opts.CharsetSentinel)
+ if (opts.CharsetSentinel)
  {
    if (opts.Charset.WebName.Equals("iso-8859-1", StringComparison.OrdinalIgnoreCase))
-     sb.Append(Sentinel.Iso.GetEncoded()).Append('&');
+     sb.Append(Sentinel.Iso.GetEncoded()).Append(joined.Length > 0 ? "&" : "");
    else if (opts.Charset.WebName.Equals("utf-8", StringComparison.OrdinalIgnoreCase))
-     sb.Append(Sentinel.Charset.GetEncoded()).Append('&');
+     sb.Append(Sentinel.Charset.GetEncoded()).Append(joined.Length > 0 ? "&" : "");
  }
QsNet/Models/DecodeOptions.cs (2)

199-202: Conflict validation now throws InvalidOperationException — good, but align message casing with tests

The exception type aligns with the standardized approach. To avoid fragile assertions around casing, consider using lowercase option names in the message (or change the test to be case-insensitive). Suggested tweak:

- "Invalid DecodeOptions: DecodeDotInKeys=true requires AllowDots=true when decoding keys."
+ "Invalid DecodeOptions: decodeDotInKeys=true requires allowDots=true when decoding keys."

210-224: Key decoder contract clarification — nice

Explicitly requiring string or null for keys and throwing InvalidOperationException otherwise tightens guarantees. Consider adding a short remark in the XML summary on Decoder/KindAwareDecoder delegates noting the key-return contract to help custom decoder implementers.

I can add a brief XML comment update for the delegates if you want.

QsNet/Internal/Decoder.cs (2)

300-335: Synthetic double-bracket remainder trimming: add guardrails/tests

The post-unwrapping trimming (opens > closes and ends with ‘]’) correctly targets cases like "[[b[c]]" so the inner literal becomes "[b[c". Two suggestions:

  • Extract this into a small helper with a unit test matrix (e.g., "[[c][d]]", "[[b[c]]", "[][x]]", balanced inputs) for maintainability and to prevent regressions.
  • Add comments explaining why opens > closes implies the trailing ‘]’ is synthetic.

I can contribute targeted tests under Split Key Segmentation Remainder Tests if helpful.


657-686: Enumerator join helper: small micro-optimization optional

JoinAsCommaSeparatedStrings could pre-size the StringBuilder with a small constant (e.g., 32) to reduce allocations for tiny lists; not critical.

📜 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 5ea801d and 62222da.

📒 Files selected for processing (9)
  • QsNet.Tests/DecodeOptionsTests.cs (1 hunks)
  • QsNet.Tests/DecodeTests.cs (12 hunks)
  • QsNet.Tests/EncodeTests.cs (3 hunks)
  • QsNet/Internal/Decoder.cs (9 hunks)
  • QsNet/Internal/Encoder.cs (2 hunks)
  • QsNet/Internal/Utils.cs (8 hunks)
  • QsNet/Models/DecodeOptions.cs (2 hunks)
  • QsNet/Qs.cs (3 hunks)
  • QsNet/QsNet.csproj (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
QsNet/Qs.cs (1)
QsNet/Enums/Sentinel.cs (2)
  • GetEncoded (50-58)
  • ToString (65-68)
QsNet.Tests/EncodeTests.cs (2)
QsNet/Qs.cs (3)
  • Dictionary (30-117)
  • Qs (20-280)
  • Encode (126-279)
QsNet/Internal/Encoder.cs (1)
  • Encode (42-428)
⏰ 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). (3)
  • GitHub Check: Coverage (merged)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Codacy Security Scan
🔇 Additional comments (19)
QsNet/Internal/Encoder.cs (1)

89-90: No further doc updates required for cycle exception

I verified that the only cycle check in QsNet/Internal/Encoder.cs now throws an InvalidOperationException("Cyclic object value") and there are no XML docs or public‐API comments still referencing IndexOutOfRangeException for cyclic object values. All remaining ArgumentException and IndexOutOfRangeException entries in the codebase correspond to other, distinct error conditions (invalid input types, limit overflows, etc.) and should remain unchanged.

QsNet/Internal/Utils.cs (5)

191-193: Invariant culture for index keys into maps: LGTM.

Using i.ToString(CultureInfo.InvariantCulture) ensures locale-agnostic keys and matches the PR goal.


236-236: Invariant culture for list-to-dictionary merge keys: LGTM.

Consistent with the rest of the invariant-culture changes.


299-302: Hex formatting now invariant: LGTM.

ToString("X2"/"X4", InvariantCulture) avoids locale surprises in Escape output.


330-357: Invariant hex parsing in Unescape across TFMs: LGTM.

int.TryParse(..., NumberStyles.HexNumber, CultureInfo.InvariantCulture, ...) is the right fix; also good job using Span on modern TFMs.


410-413: Invariant hex parsing when converting %uXXXX to numeric entities: LGTM.

This keeps ISO-8859-1 sentinel mode stable across locales.

QsNet.Tests/DecodeOptionsTests.cs (1)

124-126: Test updated to expect InvalidOperationException: LGTM.

The assertion aligns with the standardized error type and still verifies the option names in the message.

QsNet.Tests/EncodeTests.cs (1)

1794-1794: Updated expectations to InvalidOperationException for circular/self-referential inputs: LGTM.

These assertions now match Encoder’s behavior and the PR objective.

Also applies to: 1800-1800, 3408-3408, 3419-3419

QsNet.Tests/DecodeTests.cs (2)

273-277: Exception type migration to InvalidOperationException looks correct

Using InvalidOperationException here aligns with the PR’s objective of standardizing limit-related failures. Message remains stable, which is good for consumers relying on it.


2407-2409: Parameter/List limit tests updated to InvalidOperationException — good coverage

The migration to InvalidOperationException for parameter limit exceedance, list limit exceedance, negative list limit, and nested list overflow test cases stays consistent with the core changes.

Also applies to: 2485-2487, 2531-2534, 2541-2543

QsNet/Qs.cs (2)

6-6: Culture-invariant index formatting import — good

Adding System.Globalization sets up for consistent ToString(CultureInfo.InvariantCulture) usage.


270-277: InvariantCulture for index keys — correct and necessary

Using invariant ToString for auto-indexed enumerable encoding prevents locale-dependent digits.

QsNet/Models/DecodeOptions.cs (1)

238-241: DefaultDecode now returns string? — compatible and clearer

Returning string? is more precise and still upcasts to object? at call sites. No external API break since it’s private.

QsNet/Internal/Decoder.cs (6)

49-56: List limit violations now throw InvalidOperationException — consistent and message-stable

Exception type and message align with the tests and PR goals.

Also applies to: 59-63


107-111: Parameter limit overflow standardized to InvalidOperationException

Matches the test updates and improves semantic clarity over IndexOutOfRangeException.


191-196: Explicit “[]=“ detection uses ordinal comparisons per target — good

The NETSTANDARD2_0 IndexOf and modern Contains(StringComparison.Ordinal) keep behavior stable across TFMs.


347-351: Culture-invariant numeric check for bracketed indices — correct

Comparing idx.ToString(CultureInfo.InvariantCulture) to the decoded token avoids locale pitfalls.


429-488: Dot-to-bracket conversion is careful with degenerate cases — good

Top-level '.' handling respects:

  • double dots → literal first dot
  • trailing dot → literal dot
  • “.[” → skips the dot
    This matches the new tests under “Top-level dot guardrails & depth remainder”.

578-599: Remainder handling: strictDepth overflow vs unterminated groups separated — correct

  • Throws for well‑formed overflow when strictDepth is true.
  • Doesn’t throw for unterminated segments; wraps the remainder once.
  • Special-cases keys starting with '[' to return the original literal.

This matches the updated tests and provides robust parsing.

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 (2)
QsNet/Internal/Decoder.cs (2)

49-57: Fix cumulative list-limit check for comma-split values (can exceed ListLimit).

When Comma is enabled and value contains commas, the code checks splitVal.Length against ListLimit but ignores currentListLength. This allows exceeding the limit when appending comma-split values to an existing list.

Apply this diff to enforce the cumulative limit:

-            var splitVal = str.Split(',');
-            if (options.ThrowOnLimitExceeded && splitVal.Length > options.ListLimit)
+            var splitVal = str.Split(',');
+            if (options.ThrowOnLimitExceeded && currentListLength + splitVal.Length > options.ListLimit)
                 throw new InvalidOperationException(
                     $"List limit exceeded. Only {options.ListLimit} element{(options.ListLimit == 1 ? "" : "s")} allowed in a list."
                 );
             return splitVal.ToList<object?>();

Consider adding tests:

  • existing list length = N, incoming comma-split length = M, N + M == limit → allowed
  • N + M == limit + 1 and ThrowOnLimitExceeded = true → throws InvalidOperationException

Also applies to: 59-65


227-233: Fix CA1859 build failure: change ParseObject parameter to List.

The pipeline fails with CA1859 (treating analyzer warnings as errors). Since SplitKeyIntoSegments already returns List and ParseObject is private, switching the parameter type is safe and resolves the warning.

Apply this diff:

-    private static object? ParseObject(
-        IReadOnlyList<string> chain,
+    private static object? ParseObject(
+        List<string> chain,
         object? value,
         DecodeOptions options,
         bool valuesParsed
     )
@@
-        return ParseObject(segments, value, options, valuesParsed);
+        return ParseObject(segments, value, options, valuesParsed);

Alternative: if you intentionally prefer IReadOnlyList for API intent, you can suppress CA1859 locally with a pragma or [SuppressMessage] attribute, but changing the type is simplest here.

Also applies to: 417-418

🧹 Nitpick comments (7)
QsNet/QsNet.csproj (1)

22-37: Packaging/diagnostics hardening LGTM; consider adding a package icon

The new SourceLink, symbols, determinism, analyzers, and CI-guarded Werror settings are on point. Optional: add a package icon so the NuGet gallery shows branding.

Example (requires adding an icon file at repo root or project folder):

   <PropertyGroup>
+    <PackageIcon>icon.png</PackageIcon>
   </PropertyGroup>
   <ItemGroup>
+    <None Include="..\icon.png" Pack="true" PackagePath="\" />
   </ItemGroup>
QsNet.Tests/UtilsTests.cs (1)

1305-1337: Broaden hex-entity coverage (upper ‘X’, max range, empty digits)

The new tests validate lowercase hex and surrogate-pair decoding. To harden coverage, add:

  • Uppercase ‘X’ prefix: A -> A
  • Max valid hex: � -> U+10FFFF
  • Out-of-range hex remains unchanged: �
  • Empty digits after x/X: &#x; and &#X; remain unchanged

Suggested additional tests (place near the existing hex tests):

[Fact]
public void InterpretNumericEntities_DecodesSingleHexEntity_UppercaseX()
{
    Utils.InterpretNumericEntities("&#X41;").Should().Be("A");
}

[Fact]
public void InterpretNumericEntities_AcceptsMaxValidHexAndRejectsBeyond()
{
    // U+10FFFF is valid
    Utils.InterpretNumericEntities("&#x10FFFF;").Should().Be(char.ConvertFromUtf32(0x10FFFF));
    // One above max should remain unchanged
    Utils.InterpretNumericEntities("&#x110000;").Should().Be("&#x110000;");
}

[Fact]
public void InterpretNumericEntities_EmptyHexDigitsRemainUnchanged()
{
    Utils.InterpretNumericEntities("&#x;").Should().Be("&#x;");
    Utils.InterpretNumericEntities("&#X;").Should().Be("&#X;");
}
QsNet/Internal/Utils.cs (3)

411-415: Encode (latin-1 path): invariant parse of %uXXXX — good; consider avoiding obsolete Escape

Current approach is correct functionally. Optional: avoid the obsolete Escape call by computing the code points directly and replacing %uXXXX with numeric entities in one pass to reduce allocations and remove the Obsolete API dependency.


810-842: InterpretNumericEntities: avoid per-digit allocations and guard against overflow

The decoder now supports hex entities — great. Two improvements:

  • Convert the entire digits span once (via int.TryParse on a ReadOnlySpan) instead of Convert.ToInt32 on each digit to cut allocations.
  • Guard against excessively long sequences that can overflow int during accumulation (malformed input like &#xFFFFFFFFF;). Parsing the span with TryParse naturally handles this; otherwise, bail out and leave the input unchanged.

Proposed refactor:

-                var code = 0;
-                var startDigits = j;
-                var hex = false;
-                if (str[j] is 'x' or 'X') { hex = true; j++; startDigits = j; }
-                while (j < n && (hex ? Uri.IsHexDigit(str[j]) : char.IsDigit(str[j])))
-                {
-                    code = hex
-                           ? (code << 4) + Convert.ToInt32(str[j].ToString(), 16)
-                           : code * 10 + (str[j] - '0');
-                    j++;
-                }
-
-                if (j < n && str[j] == ';' && j > startDigits)
+                var startDigits = j;
+                var hex = false;
+                if (str[j] is 'x' or 'X') { hex = true; j++; startDigits = j; }
+                while (j < n && (hex ? Uri.IsHexDigit(str[j]) : char.IsDigit(str[j]))) j++;
+                if (j < n && str[j] == ';' && j > startDigits)
                 {
-                    switch (code)
+                    ReadOnlySpan<char> digits = str.AsSpan(startDigits, j - startDigits);
+                    if (!int.TryParse(digits,
+                                      hex ? System.Globalization.NumberStyles.HexNumber : System.Globalization.NumberStyles.None,
+                                      System.Globalization.CultureInfo.InvariantCulture,
+                                      out var code))
+                    {
+                        // leave as-is
+                        sb.Append(ch);
+                        i++;
+                        continue;
+                    }
+                    switch (code)
                     {
                         case <= 0xFFFF:
                             sb.Append((char)code);
                             break;
                         case <= 0x10FFFF:
                             sb.Append(char.ConvertFromUtf32(code));
                             break;
                         default:
                             sb.Append('&');
                             i++;
                             continue;
                     }

468-476: Surrogate pair emission: validate low surrogate before 4-byte path

Today, any char in the surrogate range takes the 4-byte path without checking for a valid low surrogate, which can emit invalid UTF-8 for malformed input. Optional guard:

-                // 4 bytes (surrogate pair)
-                var nextC = i + 1 < segment.Length ? segment[i + 1] : 0;
-                var codePoint = 0x10000 + (((c & 0x3FF) << 10) | (nextC & 0x3FF));
+                // 4 bytes (surrogate pair) – only if valid pair; otherwise treat as 3-byte fallback
+                if (i + 1 >= segment.Length || !char.IsSurrogatePair(segment[i], segment[i + 1]))
+                {
+                    // Fallback: percent-encode the single surrogate code unit to remain lossless
+                    buffer.Append(HexTable.Table[0xE0 | (c >> 12)]);
+                    buffer.Append(HexTable.Table[0x80 | ((c >> 6) & 0x3F)]);
+                    buffer.Append(HexTable.Table[0x80 | (c & 0x3F)]);
+                    i++;
+                    continue;
+                }
+                var nextC = segment[i + 1];
+                var codePoint = char.ConvertToUtf32((char)c, nextC);

This mirrors the JS behavior of losslessly encoding lone surrogates rather than producing an invalid sequence.

QsNet/Internal/Decoder.cs (2)

350-355: Make numeric parsing culture-invariant, too.

Formatting uses CultureInfo.InvariantCulture; mirror that in parsing to avoid locale surprises with signs/whitespace.

Apply this diff:

-                var isPureNumeric =
-                    int.TryParse(decodedRoot, out var idx) && !string.IsNullOrEmpty(decodedRoot);
+                var isPureNumeric =
+                    !string.IsNullOrEmpty(decodedRoot)
+                    && int.TryParse(decodedRoot, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var idx);

21-27: Minor: cache Latin1Encoding for NETSTANDARD2_0.

Encoding.GetEncoding(28591) may be slightly costlier if accessed frequently. Consider caching in a static readonly field under NETSTANDARD2_0.

Example:

-    private static Encoding Latin1Encoding =>
-#if NETSTANDARD2_0
-        Encoding.GetEncoding(28591);
-#else
-        Encoding.Latin1;
-#endif
+    private static readonly Encoding Latin1Encoding =
+#if NETSTANDARD2_0
+        Encoding.GetEncoding(28591);
+#else
+        Encoding.Latin1;
+#endif
📜 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 62222da and 58b4300.

📒 Files selected for processing (7)
  • QsNet.Tests/UtilsTests.cs (2 hunks)
  • QsNet/Internal/Decoder.cs (11 hunks)
  • QsNet/Internal/Encoder.cs (2 hunks)
  • QsNet/Internal/Utils.cs (9 hunks)
  • QsNet/Models/DecodeOptions.cs (4 hunks)
  • QsNet/Qs.cs (5 hunks)
  • QsNet/QsNet.csproj (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • QsNet/Internal/Encoder.cs
🧰 Additional context used
🧬 Code graph analysis (2)
QsNet.Tests/UtilsTests.cs (1)
QsNet/Internal/Utils.cs (2)
  • Utils (20-1180)
  • InterpretNumericEntities (788-851)
QsNet/Qs.cs (1)
QsNet/Enums/Sentinel.cs (2)
  • GetEncoded (50-58)
  • ToString (65-68)
🪛 GitHub Actions: Test
QsNet/Internal/Decoder.cs

[error] 228-228: dotnet build --configuration Release --no-restore failed: CA1859: Change type of parameter 'chain' from 'System.Collections.Generic.IReadOnlyList' to 'System.Collections.Generic.List' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859).

⏰ 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). (2)
  • GitHub Check: Codacy Security Scan
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (23)
QsNet/QsNet.csproj (3)

10-15: Richer, multi-line PackageTags look good

The expanded, categorized tags will help discoverability on NuGet. No issues spotted.


39-41: net8-only ImplicitUsings is a sensible tradeoff

Leaving ImplicitUsings off for netstandard2.0 avoids noise. Looks good.


48-50: SourceLink reference is correct and scoped

Microsoft.SourceLink.GitHub v8.0.0 with PrivateAssets=All is appropriate for these targets.

QsNet.Tests/UtilsTests.cs (1)

1288-1290: Updated expectation for hex entities is correct

Acknowledging that hex numeric entities are now supported makes this test consistent with the new decoder behavior.

QsNet/Qs.cs (4)

29-29: Doc: standardized to InvalidOperationException — good

The Decode XML docs now reflect the standardized exception type for limit violations. Matches PR objective.


125-125: Doc: Encode also standardized to InvalidOperationException

Consistent with the encoder/decoder changes; good alignment.


260-263: Avoid trailing ‘&’ after charset sentinel

Conditionally appending “&” only when there are subsequent parts prevents dangling delimiters. Nice polish.


276-276: InvariantCulture for index keys is the right call

Using ToString(CultureInfo.InvariantCulture) for numeric indices ensures locale independence across platforms.

QsNet/Models/DecodeOptions.cs (4)

15-19: Key-decoder contract clarified

Remarks make the “key must be string or null” rule explicit, which prevents subtle downstream type issues. Good documentation tightening.


31-34: Kind-aware decoder key contract clarified

Same commendation here; explicit contract reduces ambiguity for custom decoders.


206-208: Switch to InvalidOperationException on option conflict is appropriate

Throwing InvalidOperationException for incompatible options (DecodeDotInKeys vs AllowDots) matches the PR’s exception standardization.


244-247: DefaultDecode returns string? — consistent and safer

Returning string? from the default path aligns with Utils.Decode and simplifies key-path enforcement. No issues.

QsNet/Internal/Utils.cs (4)

299-302: Hex formatting should be invariant — good

Using ToString("X2"/"X4", CultureInfo.InvariantCulture) removes locale variance from Escape’s output.


331-358: Unescape: invariant hex parsing — good

Passing CultureInfo.InvariantCulture to int.TryParse for both %uXXXX and %XX paths is correct and cross-locale safe.


191-193: InvariantCulture for index-stringification in merges

Converting indices with InvariantCulture both when merging into maps and when list→map promotion occurs keeps dictionary key shape stable across locales.

Also applies to: 236-236


792-799: Fast-path ‘&#’ presence checks are correct per target framework

Ordinal IndexOf/Contains split by TFMs is appropriate for netstandard2.0 vs newer frameworks. Good.

QsNet/Internal/Decoder.cs (7)

108-111: Standardized exception type for parameter overflow — LGTM.

Switching to InvalidOperationException on parameter overflow aligns with the PR’s error semantics. Message is clear and pluralization is handled.


300-305: Synthetic-wrapper trimming logic looks sound; please add edge-case tests.

The opens>closes heuristic to trim a leaked trailing ']' from the inner slice (after removing the outer synthetic wrapper) is reasonable. To guard against regressions, add cases such as:

  • original: "a[[b[c" (unterminated remainder wrapped once) → segment becomes "[b[c"
  • original: "a[[b[c]]d" with strictDepth=false → verify resulting segments
  • deeply nested balanced groups followed by unterminated: "a[b[c]d[e" → behavior per comments

I can generate targeted unit tests for SplitKeyIntoSegments if useful. Do you want me to open a follow-up PR with those?

Also applies to: 307-338


433-439: Efficient dot short-circuit for NETSTANDARD2_0 — LGTM.

Using IndexOf('.') < 0 avoids allocations and keeps behavior consistent across TFMs.


586-589: Depth overflow semantics switched to InvalidOperationException — LGTM.

Exception type now matches the PR objective and the message is actionable.

Also applies to: 615-617


191-195: Validate behavior when "[]=" appears alongside comma-split values.

Wrapping non-string IEnumerable into a one-element list preserves identity, but combined with ParseListValue (which may return List<object?> for comma splits) this can yield nested lists for inputs like a[]=1,2. If that’s the desired semantics, fine; otherwise, flatten once when "[]=" is present.

Would you like me to draft tests for:

  • "a[]=1&a[]=2" (no comma)
  • "a[]=1,2" with Comma=true
  • "a[]=1,2&a[]=3" with Comma=true
    …to assert expected nesting/flattening?

6-6: Import for CultureInfo — LGTM.

Necessary for invariant numeric formatting introduced below.


73-75: Doc comment updated for new exception type — LGTM.

Documentation now reflects InvalidOperationException when ThrowOnLimitExceeded is true.

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 (2)
QsNet/Internal/Utils.cs (1)

468-485: Prevent surrogate pairs from splitting across segment boundaries in Utils.Encode

The current implementation in QsNet/Internal/Utils.cs (around lines 426–550) uses Substring to process the input in fixed‐size chunks (defined by SegmentLimit = 1024). It then checks surrogate pairs only within each chunk:

  • If a high surrogate appears at the end of a chunk (i + 1 >= segment.Length), it’s treated as “lone” and encoded as a 3-byte sequence, even if its matching low surrogate is the first character of the next chunk.
  • This causes valid astral code points (e.g. U+1F600) landing at SegmentLimit-1/SegmentLimit to be mis-encoded.

Locations needing fixes

  • File: QsNet/Internal/Utils.cs
  • Encoding loop starting at the declaration of var segment = nonNullStr.Substring(j, ...) through the outer while (j < nonNullStr.Length).

Recommended refactor
Replace the chunk-based loops with a single pass over the full string, using the overload char.IsSurrogatePair(string, index) to detect pairs across any boundary:

@@ -424,30 +424,18 @@
-        var buffer = new StringBuilder();
-        var j = 0;
-
-        while (j < nonNullStr.Length)
-        {
-            var segment = 
-                nonNullStr.Length >= SegmentLimit
-                    ? nonNullStr.Substring(j, Math.Min(SegmentLimit, nonNullStr.Length - j))
-                    : nonNullStr;
-
-            var i = 0;
-            while (i < segment.Length)
-            {
-                var c = (int)segment[i];
-
-                // 4 bytes (surrogate pair) – only if valid pair; otherwise treat as 3-byte fallback
-                if (i + 1 >= segment.Length || !char.IsSurrogatePair(segment[i], segment[i + 1]))
-                {
-                    // encode lone surrogate as 3-byte sequence
-
-                    i++;
-                    continue;
-                }
-                var nextC = segment[i + 1];
-                var codePoint = char.ConvertToUtf32((char)c, nextC);
-                // emit 4-byte sequence
-
-                i += 2;
-            }
-
-            j += SegmentLimit;
-        }
+        // Single-pass encoding without chunking
+        var buffer = new StringBuilder();
+        for (int i = 0; i < nonNullStr.Length;)
+        {
+            if (char.IsSurrogatePair(nonNullStr, i))
+            {
+                int codePoint = char.ConvertToUtf32(nonNullStr[i], nonNullStr[i + 1]);
+                // append 4-byte UTF-8 percent-encoded sequence
+                buffer.Append(HexTable.Table[0xF0 | (codePoint >> 18)]);
+                buffer.Append(HexTable.Table[0x80 | ((codePoint >> 12) & 0x3F)]);
+                buffer.Append(HexTable.Table[0x80 | ((codePoint >> 6) & 0x3F)]);
+                buffer.Append(HexTable.Table[0x80 | (codePoint & 0x3F)]);
+                i += 2;
+            }
+            else
+            {
+                int c = nonNullStr[i];
+                // handle ASCII, 2-byte, 3-byte, and lone surrogates (WTF-8 fallback)
+
+                i++;
+            }
+        }

Add a regression test to catch this boundary case:

// Ensure astral characters at SegmentLimit-1/SegmentLimit encode as 4-byte sequences
var s = new string('a', SegmentLimit - 1) + "\U0001F600" + "b";
var encoded = Utils.Encode(s, Encoding.UTF8, Format.Rfc3986);
Assert.Contains("%F0%9F%98%80", encoded);

This change restores correct 4-byte UTF-8 emission for all valid surrogate pairs and preserves the 3-byte fallback for isolated surrogates.

QsNet/Internal/Decoder.cs (1)

243-261: Bug risk: incorrect parent index detection when chain ends with "[]"

parentKeyStr is built by concatenating all prior segments, e.g. "a[0]" for chain ["a", "[0]", "[]"]. int.TryParse on "a[0]" will always fail, so currentListLength stays 0 and nested list limit checks may undercount.

Parse the immediate parent segment only (chain[^2]), strip brackets, and TryParse that.

-            string parentKeyStr;
-            if (chain.Count > 1)
-            {
-                var sbTmp = new StringBuilder();
-                for (var t = 0; t < chain.Count - 1; t++) sbTmp.Append(chain[t]);
-                parentKeyStr = sbTmp.ToString();
-            }
-            else
-            {
-                parentKeyStr = string.Empty;
-            }
-
-            if (
-                int.TryParse(parentKeyStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parentKey)
-                && value is IList<object?> list
-                && parentKey < list.Count
-            )
-                currentListLength = (list[parentKey] as IList<object?>)?.Count ?? 0;
+            // Look only at the immediate parent segment, e.g. "[0]" in ["a", "[0]", "[]"]
+            if (chain.Count > 1)
+            {
+#if NETSTANDARD2_0
+                var parentSeg = chain[chain.Count - 2];
+#else
+                var parentSeg = chain[^2];
+#endif
+                if (parentSeg.Length >= 2 && parentSeg[0] == '[' && parentSeg[parentSeg.Length - 1] == ']')
+                {
+#if NETSTANDARD2_0
+                    var idxStr = parentSeg.Substring(1, parentSeg.Length - 2);
+#else
+                    var idxStr = parentSeg[1..^1];
+#endif
+                    if (int.TryParse(idxStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parentIndex)
+                        && value is IList<object?> incomingList
+                        && parentIndex >= 0
+                        && parentIndex < incomingList.Count)
+                    {
+                        currentListLength = (incomingList[parentIndex] as IList<object?>)?.Count ?? 0;
+                    }
+                }
+            }

Please confirm this matches the intended nested list behavior and limits; I can push a follow-up PR if needed.

🧹 Nitpick comments (9)
QsNet/Internal/Utils.cs (1)

819-873: Robust, culture-invariant numeric entity parsing; a couple of edge-case considerations.

What’s great:

  • Zero-allocation scanning with spans on newer TFMs.
  • Culture-invariant TryParse across hex/decimal.
  • Graceful failure path that preserves input on overflow/invalid digits.
  • Proper handling of BMP vs non-BMP code points.

Optional refinements:

  • Consider treating surrogate-range code values (0xD800–0xDFFF) similar to your WTF‑8 stance: either preserve input unchanged (strict) or keep current behavior (append lone surrogate). Document the choice for consumers.
  • If you want HTML5 compliance, only the “&#x...;” hex form is standard (your code correctly supports that). If support for non-standard forms is desired later, gate them behind an option.

No changes strictly required; just be explicit in docs/tests about intended behavior for surrogate-range numeric entities.

If you want, I can add focused tests for:

  • Overflowing entities like &#9999999999999;
  • Surrogate-range entities like � and mixed-case hex like 😀.

Also applies to: 833-848, 849-856

QsNet/Internal/Decoder.cs (4)

52-63: List-limit check now accounts for both existing and incoming comma-split items (N + M) — correct

Using currentListLength + splitVal.Length ensures you enforce the limit across merges. Minor: consider guarding the non-comma path with the same message constant to avoid future string drift.

-            if (options.ThrowOnLimitExceeded && currentListLength >= options.ListLimit)
-            throw new InvalidOperationException(
-                $"List limit exceeded. Only {options.ListLimit} element{(options.ListLimit == 1 ? "" : "s")} allowed in a list."
-            );
+            if (options.ThrowOnLimitExceeded && currentListLength >= options.ListLimit)
+                throw new InvalidOperationException(
+                    $"List limit exceeded. Only {options.ListLimit} element{(options.ListLimit == 1 ? "" : "s")} allowed in a list."
+                );

191-197: Behavior of "[]=" with Comma: confirm intended nesting for single occurrence

When the part contains "[]=", value is wrapped as a single element if it’s IEnumerable (and not string):

value = value is IEnumerable and not string ? new List<object?> { value } : value;

With Comma=true, a single occurrence like a[]=1,2,3 will make value a List, which then gets wrapped into List<object?> { List }, yielding a nested list for a single bracketed occurrence. The new tests cover multiple parts (which should indeed produce a list of lists), but not the single-occurence case.

Is the nested result for a single a[]=1,2,3 intentional? If you intend a flat list for a single bracketed comma value, consider flattening when no previous value exists for that key.

Example tweak (flatten only when first occurrence):

-            if (part.Contains("[]=", StringComparison.Ordinal))
-                value = value is IEnumerable and not string ? new List<object?> { value } : value;
+            if (part.Contains("[]=", StringComparison.Ordinal))
+            {
+                // If this is the first occurrence for this key, keep comma-split values flat.
+                var isFirstForKey = !obj.ContainsKey(key);
+                if (!isFirstForKey && value is IEnumerable and not string)
+                    value = new List<object?> { value };
+            }

Would you like me to add a targeted test for a single a[]=1,2,3 to lock this down?


350-356: Micro: avoid TryParse on empty strings first

You call int.TryParse before checking for empty. Swap the order to skip parsing on empty and reduce allocations/branches slightly.

-                var isPureNumeric =
-                    int.TryParse(decodedRoot, NumberStyles.Integer, CultureInfo.InvariantCulture, out var idx) &&
-                    !string.IsNullOrEmpty(decodedRoot);
+                var isPureNumeric =
+                    !string.IsNullOrEmpty(decodedRoot) &&
+                    int.TryParse(decodedRoot, NumberStyles.Integer, CultureInfo.InvariantCulture, out var idx);

504-621: Depth handling and error semantics look right; messages could be centralized

  • InvalidOperationException for strictDepth overflow is consistent.
  • Unterminated groups do not trigger strictDepth exceptions and yield a wrapped remainder — matches tests.

Optional: extract the repeated error message into a const to avoid drift.

+    private const string StrictDepthExceededMsg = "Input depth exceeded depth option of {0} and strictDepth is true";
...
-                throw new InvalidOperationException(
-                    $"Input depth exceeded depth option of {maxDepth} and strictDepth is true"
-                );
+                throw new InvalidOperationException(string.Format(StrictDepthExceededMsg, maxDepth));
...
-        if (strictDepth)
-            throw new InvalidOperationException(
-                $"Input depth exceeded depth option of {maxDepth} and strictDepth is true"
-            );
+        if (strictDepth)
+            throw new InvalidOperationException(string.Format(StrictDepthExceededMsg, maxDepth));
QsNet.Tests/DecodeTests.cs (4)

2408-2410: ParameterLimit exceedance should assert the message for stronger contract

Currently you assert only the exception type. Consider also asserting the message "Parameter limit exceeded. Only {limit} parameter(s) allowed." to lock the behavior, as you already do for list-limit.

-        Action act = () => Qs.Decode("a=1&b=2&c=3&d=4&e=5&f=6", options);
-        act.Should().Throw<InvalidOperationException>();
+        Action act = () => Qs.Decode("a=1&b=2&c=3&d=4&e=5&f=6", options);
+        act.Should()
+           .Throw<InvalidOperationException>()
+           .WithMessage("Parameter limit exceeded. Only 3 parameter*allowed.");

(Use a wildcard for pluralization to avoid brittleness.)


2486-2488: ListLimit exceedance test: type-only assertion is fine; consider asserting message too

Mirroring the comma-list test would make failures easier to diagnose.

-        Action act = () => Qs.Decode("a[]=1&a[]=2&a[]=3&a[]=4", options);
-        act.Should().Throw<InvalidOperationException>();
+        Action act = () => Qs.Decode("a[]=1&a[]=2&a[]=3&a[]=4", options);
+        act.Should().Throw<InvalidOperationException>()
+           .WithMessage("List limit exceeded. Only 3 elements allowed in a list.");

4686-4724: New tests for comma split vs existing list length: great boundary coverage

  • N + M == limit allowed.
  • N + M > limit throws when ThrowOnLimitExceeded is true.

Consider adding an additional test where ThrowOnLimitExceeded = false to ensure silent truncation or well-defined behavior when combining parts pushes over the limit.

+    [Fact]
+    public void Decode_CommaSplit_TruncatesWhenSumExceedsLimit_AndThrowOff()
+    {
+        var opts = new DecodeOptions { Comma = true, ListLimit = 3, ThrowOnLimitExceeded = false, Duplicates = Duplicates.Combine };
+        var result = Qs.Decode("a=1,2&a=3,4,5", opts);
+        // Define expected behavior explicitly: either first 3, or last 3, or exactly how ParseListValue truncates upstream.
+        // Assert accordingly once decided.
+    }

Would you like me to pin the exact truncation semantics and add an assertion?


1714-1766: Array-of-arrays with comma-split: confirms desired nesting semantics

This suite validates that each []= occurrence can carry a flat list and that duplicates produce a list of lists. Consider adding the single-occurrence case a[]=1,2,3 to explicitly document expected behavior (see Decoder comment).

+    [Fact]
+    public void Decode_BracketSingle_CommaSplit_DefinesSingleOccurrenceBehavior()
+    {
+        var opts = new DecodeOptions { Comma = true };
+        var res = Qs.Decode("a=1,2,3", opts);      // control
+        var res2 = Qs.Decode("a[]=1,2,3", opts);   // bracketed
+        // Decide and assert: flat ["1","2","3"] or nested [["1","2","3"]]
+    }
📜 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 5ab597c and 3ac1490.

📒 Files selected for processing (4)
  • QsNet.Tests/DecodeTests.cs (14 hunks)
  • QsNet.Tests/UtilsTests.cs (2 hunks)
  • QsNet/Internal/Decoder.cs (14 hunks)
  • QsNet/Internal/Utils.cs (10 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • QsNet.Tests/UtilsTests.cs
🧰 Additional context used
🧬 Code graph analysis (2)
QsNet/Internal/Utils.cs (1)
QsNet/Constants/HexTable.cs (1)
  • HexTable (6-41)
QsNet.Tests/DecodeTests.cs (4)
QsNet/Models/DecodeOptions.cs (4)
  • DecodeOptions (40-321)
  • DecodeOptions (49-63)
  • DecodeOptions (274-320)
  • Decode (203-214)
QsNet/Internal/Decoder.cs (2)
  • Dictionary (75-217)
  • List (505-622)
QsNet/Internal/Utils.cs (9)
  • Dictionary (535-676)
  • Dictionary (889-895)
  • Dictionary (902-911)
  • Dictionary (918-930)
  • Dictionary (983-986)
  • Dictionary (994-1060)
  • Dictionary (1089-1210)
  • Decode (499-527)
  • List (684-712)
QsNet/Qs.cs (2)
  • Dictionary (30-117)
  • Qs (20-280)
⏰ 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). (2)
  • GitHub Check: Coverage (merged)
  • GitHub Check: Codacy Security Scan
🔇 Additional comments (15)
QsNet/Internal/Utils.cs (5)

191-193: Good move: use culture-invariant index keys when materializing list indices into maps.

Switching to i.ToString(CultureInfo.InvariantCulture) ensures ASCII digits regardless of the current UI culture. This removes subtle bugs in locales with non-Latin native digits and stabilizes serialized shapes. No further action needed.

Also applies to: 235-237


299-302: Hex formatting now culture-invariant — aligned with intent.

Using ToString("X2", CultureInfo.InvariantCulture)/ToString("X4", CultureInfo.InvariantCulture) guarantees consistent percent-encoding for ASCII and Unicode escapes across cultures. Looks correct.


331-341: Hex parsing in Unescape standardized to InvariantCulture.

The TryParse calls for both %uXXXX and %XX are now culture-invariant and branch correctly for NETSTANDARD2_0 vs newer. This removes locale surprises and avoids allocation in newer TFMs via spans. Good change.

Also applies to: 354-358


411-415: Invariant hex parse in ISO-8859-1 branch of Encode is consistent.

Parsing match.Value’s hex digits with InvariantCulture keeps behavior deterministic across cultures. Implementation looks solid.


801-807: Efficient pre-check for numeric entities with TFM-appropriate APIs.

Using IndexOf for NETSTANDARD2_0 and Contains with StringComparison.Ordinal otherwise is the right tradeoff to avoid unnecessary work. Looks good.

QsNet/Internal/Decoder.cs (5)

21-33: Good: consolidate Latin‑1 handling and runtime check

Switching Latin1Encoding to a private static readonly field and using a helper IsLatin1 for platform-conditional checks is clean and avoids repeated calls. This also keeps NETSTANDARD2_0 parity.


74-111: Parameter limit: exception standardization and truncation logic look consistent

  • Throws ArgumentException for non-positive limit (input validation).
  • Throws InvalidOperationException when ThrowOnLimitExceeded is true and the count exceeds the limit; otherwise silently truncates.

All aligned with the PR objective and tests.


300-338: Bracket-wrapper leak fix: trimming synthetic trailing ']' is a solid edge-case recovery

The explanation and guard (opens > closes and inner ends with ']') are careful and avoid breaking balanced groups. Nice resilience improvement for unterminated inputs.


432-493: DotToBracketTopLevel: guardrails for degenerate cases are well thought-out

  • Skipping ".[" avoids creating empty segments.
  • Preserving a literal '.' for trailing/double dots and ignoring trailing '.' later in SplitKeyIntoSegments gives predictable results.
  • NETSTANDARD2_0 vs newer Contains/IndexOf split is correct.

No changes requested.


632-655: Efficient ordinal, case-insensitive replace for NETSTANDARD2_0: nice

The streaming approach with deferred StringBuilder allocation is efficient and avoids regex overhead on NS2.0.

QsNet.Tests/DecodeTests.cs (5)

276-278: Exception type update for comma list-limit exceedance matches production change

Swapping IndexOutOfRangeException to InvalidOperationException is consistent with Decoder.ParseListValue.


2280-2310: StrictDepth tests now expect InvalidOperationException — aligned with SplitKeyIntoSegments

All four scenarios cover objects, lists, mixed, and differing types. Good boundary coverage.


2533-2544: Negative ListLimit with ThrowOnLimitExceeded: good guardrail

Asserting InvalidOperationException for negative limits exercises the non-comma path too.


2686-2700: Invalid options combination test is on-point (DecodeDotInKeys requires AllowDots)

The message wildcard check already accounts for wording variations; thanks for making it resilient.


4470-4617: Top-level dot guardrails and remainder handling tests provide strong coverage

  • Leading dot, double dot, trailing dot behavior.
  • Depth interaction with dot-to-bracket conversion.
  • Unterminated bracket under strictDepth.

These directly exercise the complex paths in DotToBracketTopLevel and SplitKeyIntoSegments. Nicely done.

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 (7)
QsNet/QsNet.csproj (1)

39-41: Minor: consider aligning tooling features across TFMs

You enable ImplicitUsings only for net8.0. If you want identical compile experiences across TFMs, consider enabling it conditionally when supported (keeping it disabled on NETSTANDARD2_0), or explicitly documenting the difference.

 <PropertyGroup Condition="'$(TargetFramework)' == 'net8.0'">
-    <ImplicitUsings>enable</ImplicitUsings>
+    <ImplicitUsings>enable</ImplicitUsings>
 </PropertyGroup>
+<!-- Optionally: for other TFMs where supported in future -->
+<!-- <PropertyGroup Condition="'$(TargetFramework)' == 'netX.Y'"> -->
+<!--   <ImplicitUsings>enable</ImplicitUsings> -->
+<!-- </PropertyGroup> -->

Also applies to: 48-50

QsNet/Internal/Decoder.cs (2)

235-263: Current-length discovery for nested [] relies on parent index and incoming list shape

The parent-index probe is careful and culture-invariant. One limitation: if value isn’t an IList<object?> at this stage (e.g., custom enumerable), currentListLength remains 0 and the sum-check won’t reflect existing nested counts. If that scenario exists in inputs, you might want a defensive fallback (e.g., treat non-list enumerables as 1 item when Comma=false). Otherwise, this is acceptable.


435-496: Dot-to-bracket conversion: good guardrails for degenerate cases

Handling of leading/trailing/double dots and “.[” cases at top-level is precise. Given the complex semantics, consider adding a short XML remark with examples (you already have inline comments) so IDE hovers capture it too.

QsNet/Internal/Utils.cs (1)

808-898: InterpretNumericEntities: robust scanning with invariant parsing and bounds checks

Efficient early-outs, Span/Substring branches per TFM, and overflow handling are all solid. Minor nit: in the “> 0x10FFFF” path you append only ‘&’ and advance by one; this faithfully preserves input as “&#…;” on subsequent iterations, but a short code comment explaining the intent would help future readers.

-                        default:
-                            sb.Append('&');
-                            i++;
-                            continue;
+                        default:
+                            // Out-of-range codepoint: preserve input verbatim by emitting '&'
+                            // and advancing one char; the subsequent loop iterations will append
+                            // the remaining "#digits;" characters unchanged.
+                            sb.Append('&');
+                            i++;
+                            continue;
QsNet.Tests/DecodeTests.cs (3)

4152-4177: Test name contradicts expected behavior

Method name claims “EncodedDotDoesNotSplit”, but the expectations for both "a.b" and "a%2Eb" assert split into nested objects. Rename to reflect behavior.

-[Fact]
-public void EncodedDot_TopLevel_AllowDotsTrue_DecodeDotInKeysTrue_PlainDotSplits_EncodedDotDoesNotSplit()
+[Fact]
+public void EncodedDot_TopLevel_AllowDotsTrue_DecodeDotInKeysTrue_PlainAndEncodedDotSplit()

4180-4197: Another misnamed test: encoded dot “remains percent sequence” but expected split

Given the expectation, rename to indicate that encoded dot also splits when AllowDots=true (independent of DecodeDotInKeys for top-level).

-[Fact]
-public void EncodedDot_TopLevel_AllowDotsTrue_DecodeDotInKeysFalse_EncodedDotRemainsPercentSequence()
+[Fact]
+public void EncodedDot_TopLevel_AllowDotsTrue_DecodeDotInKeysFalse_EncodedDotAlsoSplits()

4228-4245: Bracketed encoded dot: expected output assumes decoding to '.' even when DecodeDotInKeys=false

Per current implementation, percent-decoding of keys happens before structural splitting; bracket inner “%2E” will be decoded to '.' by key decoding. The test name “RemainsPercentSequence” is misleading; either rename or adjust expectation if you intend different semantics.

-[Fact]
-public void EncodedDot_BracketSegment_RemainsPercentSequence_WhenDecodeDotInKeysFalse()
+[Fact]
+public void EncodedDot_BracketSegment_DecodesToDot_WhenDecodeDotInKeysFalse()
📜 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 3ac1490 and 0facbea.

📒 Files selected for processing (8)
  • QsNet.Tests/DecodeTests.cs (15 hunks)
  • QsNet.Tests/Fixtures/DummyEnum.cs (1 hunks)
  • QsNet.Tests/UtilsTests.cs (3 hunks)
  • QsNet/Internal/Decoder.cs (14 hunks)
  • QsNet/Internal/Utils.cs (11 hunks)
  • QsNet/Models/DecodeOptions.cs (4 hunks)
  • QsNet/Qs.cs (5 hunks)
  • QsNet/QsNet.csproj (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • QsNet.Tests/Fixtures/DummyEnum.cs
🚧 Files skipped from review as they are similar to previous changes (3)
  • QsNet.Tests/UtilsTests.cs
  • QsNet/Qs.cs
  • QsNet/Models/DecodeOptions.cs
🧰 Additional context used
🧬 Code graph analysis (2)
QsNet.Tests/DecodeTests.cs (4)
QsNet/Models/DecodeOptions.cs (4)
  • DecodeOptions (40-321)
  • DecodeOptions (49-63)
  • DecodeOptions (274-320)
  • Decode (203-214)
QsNet/Qs.cs (2)
  • Qs (20-280)
  • Dictionary (30-117)
QsNet/Internal/Utils.cs (9)
  • Decode (510-538)
  • Dictionary (546-687)
  • Dictionary (905-911)
  • Dictionary (918-927)
  • Dictionary (934-946)
  • Dictionary (999-1002)
  • Dictionary (1010-1076)
  • Dictionary (1105-1226)
  • List (695-723)
QsNet/Internal/Decoder.cs (2)
  • Dictionary (75-217)
  • List (508-625)
QsNet/Internal/Utils.cs (1)
QsNet/Constants/HexTable.cs (1)
  • HexTable (6-41)
⏰ 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). (3)
  • GitHub Check: Coverage (merged)
  • GitHub Check: Codacy Security Scan
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (13)
QsNet/QsNet.csproj (1)

10-15: Packaging metadata hardening looks solid

Expanded tags, SourceLink, deterministic build, analyzers, and CI-conditional flags are all good moves for package quality/reproducibility. No issues spotted.

Also applies to: 22-37

QsNet/Internal/Decoder.cs (4)

21-26: Static Latin1Encoding cache is correct and avoids repeated lookups

Caching Encoding.Latin1 / code page 28591 is a good micro-optimization and simplifies equality checks via IsLatin1().


49-57: Comma-split limit now accounts for existing length + incoming split (correct)

The sum-based check fixes the common footgun where multiple comma-splits could bypass a per-part limit. The exception type standardization to InvalidOperationException matches the PR goals.


59-65: Edge case: ListLimit == 0 with ThrowOnLimitExceeded = true throws immediately

This condition makes any list addition invalid when ListLimit is 0 and ThrowOnLimitExceeded is true (currentListLength >= 0). If that’s intended, consider adding/adjusting a test that explicitly documents the behavior for (ListLimit=0, ThrowOnLimitExceeded=true) vs (ThrowOnLimitExceeded=false).

Would you like me to add a focused test to lock this in?


507-625: Depth/strictDepth overflow semantics are clearer and consistent

  • Throws InvalidOperationException when strictDepth is true and overflow occurs.
  • Remainder-wrapping logic for overflow/unterminated groups is well explained and should improve downstream reconstruction.

Nice improvement.

QsNet/Internal/Utils.cs (3)

191-196: Invariant-culture index keys in merges are the right call

Using i.ToString(CultureInfo.InvariantCulture) removes locale sensitivity from objectified list keys. Matches the PR’s culture-invariant objective.

Also applies to: 235-236


299-302: Hex formatting/parsing switched to invariant culture (correct)

ToString("X2"/"X4", InvariantCulture) and int.TryParse(..., NumberStyles.HexNumber, InvariantCulture, ...) ensure stable behavior across locales.

Also applies to: 411-415, 354-358


427-499: UTF-8 encoding: surrogate-pair boundary handling and lone-surrogate fallback

  • Segment boundary avoids splitting valid pairs.
  • Valid pairs encode as 4-byte UTF-8.
  • Lone surrogates emit 3-byte “code unit” encoding to remain lossless.

This mirrors web behavior and avoids exceptions on malformed input. Good trade-off.

QsNet.Tests/DecodeTests.cs (5)

4-4: LINQ import is justified

Used later to project list contents in new comma-limit tests.


272-278: Exception type updated to InvalidOperationException is consistent with decoder changes

Message assertion is precise; good.


2280-2309: All limit/overflow tests now expect InvalidOperationException (consistent)

Aligns with the standardized error semantics in the implementation.

Also applies to: 2408-2410, 2486-2488, 2531-2535, 2540-2544


4368-4374: Good: invalid option combo throws InvalidOperationException

AllowDots=false with DecodeDotInKeys=true is invalid; test now asserts the right exception type and message pattern.


4705-4745: Comma-limit combined across duplicates: solid boundary tests

  • Equal-to-limit aggregate allowed.
  • Exceeding with ThrowOnLimitExceeded=true throws.
    These lock in the new sum-check behavior.

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)
QsNet.Tests/DecodeTests.cs (1)

4221-4226: Fix message assertion pattern to match actual exception text
The thrown exception message is:

Invalid DecodeOptions: DecodeDotInKeys=true requires AllowDots=true when decoding keys.

The test’s .WithMessage pattern currently uses lowercase and won’t match. Update it to use the correct casing and/or include the exact values.

• File: QsNet.Tests/DecodeTests.cs (lines 4221–4226)

-act.Should().Throw<InvalidOperationException>()
-    .WithMessage("*decodeDotInKeys*allowDots*");
+act.Should().Throw<InvalidOperationException>()
+    .WithMessage("*DecodeDotInKeys*AllowDots*");

Optional: tighten the pattern to assert the numeric values as well:

+    .WithMessage("*DecodeDotInKeys=true*AllowDots=true*");
♻️ Duplicate comments (1)
QsNet.Tests/DecodeTests.cs (1)

4131-4149: Resolved earlier placeholder: comma-split no-truncation with ThrowOn=false now has concrete expectations.

This addresses the prior review about tests without assertions by validating full concatenation behavior under Duplicates.Combine. Nice.

🧹 Nitpick comments (4)
QsNet/QsNet.csproj (3)

10-15: NuGet tags look solid; consider a couple more discoverability terms.

Current tags are comprehensive. You may optionally add: "query-parameters" and "http-utility" to improve search hits on NuGet. No action required if you prefer the current set.


35-36: Release notes link is helpful; consider adding PackageIcon.

NuGet Gallery now favors embedded icons. If you have a project icon, add a PackageIcon and include the asset in the package.

Example change (adjust the icon path if/when you add it):

   <PackageReadmeFile>README.md</PackageReadmeFile>
+  <PackageIcon>icon.png</PackageIcon>

And include the file for packing:

   <ItemGroup>
     <None Include="..\README.md" Pack="true" PackagePath="\"/>
     <None Include="..\LICENSE" Pack="true" PackagePath="\"/>
+    <!-- Adjust the path to where your icon actually lives -->
+    <None Include="icon.png" Pack="true" PackagePath="\"/>
     <InternalsVisibleTo Include="QsNet.Tests"/>
   </ItemGroup>

52-54: Pinned SourceLink reference is appropriate.

Microsoft.SourceLink.GitHub 8.0.0 with PrivateAssets=All is correct. Consider periodically bumping to the latest patch to pick up fixes.

QsNet.Tests/DecodeTests.cs (1)

4131-4170: Minor test ergonomics: factor small helpers to reduce repetition.

Several tests project list values via Select(x => x?.ToString()). Consider a small helper to DRY this and improve failure messages.

For example, add:

private static IEnumerable<string?> AsStrings(object? list) =>
    Assert.IsAssignableFrom<IEnumerable>(list).Cast<object?>().Select(x => x?.ToString());

Then assertions reduce to:

AsStrings(dict["a"]).Should().Equal("1","2","3","4","5");

Also applies to: 4726-4766

📜 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 0facbea and ade83a2.

📒 Files selected for processing (2)
  • QsNet.Tests/DecodeTests.cs (17 hunks)
  • QsNet/QsNet.csproj (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
QsNet.Tests/DecodeTests.cs (3)
QsNet/Models/DecodeOptions.cs (4)
  • DecodeOptions (40-321)
  • DecodeOptions (49-63)
  • DecodeOptions (274-320)
  • Decode (203-214)
QsNet/Qs.cs (2)
  • Qs (20-280)
  • Dictionary (30-117)
QsNet/Internal/Decoder.cs (2)
  • Dictionary (75-217)
  • List (508-625)
⏰ 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). (3)
  • GitHub Check: Coverage (merged)
  • GitHub Check: Codacy Security Scan
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (17)
QsNet/QsNet.csproj (4)

22-26: Good SourceLink packaging setup.

Using PublishRepositoryUrl + EmbedUntrackedSources aligns with SourceLink best practices. This will improve package debugging experience.


27-29: Analyzer and package validation: nice hardening.

EnableNETAnalyzers with AnalysisLevel=latest-recommended and EnablePackageValidation will catch regressions early. LGTM.


31-33: Deterministic + CI-only WarningsAsErrors is a good balance.

This keeps local dev flexible while enforcing cleanliness in CI. Approved.


39-41: net8.0-only ImplicitUsings is correct.

Scoping ImplicitUsings to net8.0 avoids surprises on netstandard2.0. Approved.

QsNet.Tests/DecodeTests.cs (13)

4-4: Added System.Linq is necessary for new list projections in assertions.

Required for Select() usage later. Good catch.


272-278: Correct exception type for list limit with comma-splitting.

Switching to InvalidOperationException aligns with library changes for limit violations. Message assertion looks precise.


2281-2309: StrictDepth overflow assertions updated to InvalidOperationException — consistent with decoder changes.

These now reflect SplitKeyIntoSegments throwing InvalidOperationException on well-formed overflows under StrictDepth=true.


2408-2410: Parameter limit exceed now expects InvalidOperationException — matches ParseQueryStringValues semantics when ThrowOn is true.

Looks correct given the updated enforcement path.


2486-2488: List limit exceed (flat list) -> InvalidOperationException is correct.

Matches standardized exception policy.


2534-2535: Negative ListLimit with ThrowOnLimitExceeded=true throwing InvalidOperationException is consistent with “invalid options in operation” semantics.

Approved.


2542-2544: Nested list limit exceed also asserts InvalidOperationException — consistent.

Good coverage of nested case.


4152-4169: Behavior clarified: bracketed single occurrence with comma-split yields a nested list.

This precisely documents the intended shape difference between “a=1,2,3” and “a[]=1,2,3”. Approved.


4174-4198: Top-level plain/encoded dot splitting with AllowDots=true, DecodeDotInKeys=true — solid tests.

These ensure percent-decoded dots behave equivalently to literal dots at the top level.


4201-4218: Encoded dot splits even when DecodeDotInKeys=false (with AllowDots=true).

This codifies that DecodeDotInKeys controls key-decoding semantics, while top-level dot splitting remains governed by AllowDots. Approved.


4249-4266: Bracket-segment percent-decoding to '.' regardless of DecodeDotInKeys — good clarification.

This matches the “inside brackets still percent-decodes” contract.


4686-4691: StrictDepth well-formed overflow unit test matches SplitKeyIntoSegments behavior.

Asserting InvalidOperationException here is correct.


4726-4766: Comma limit aggregation tests add valuable boundary coverage.

  • Sum equals limit → allowed.
  • Sum exceeds limit with ThrowOn → throws.

These confirm the combine-and-check semantics across duplicate parts. Nicely scoped.

@techouse techouse merged commit 08dc697 into main Aug 26, 2025
12 of 15 checks passed
@techouse techouse deleted the chore/lint branch August 26, 2025 21:51
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.

1 participant