Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ decode: add throwOnLimitExceeded option #26

Merged
merged 5 commits into from
Nov 23, 2024
Merged

✨ decode: add throwOnLimitExceeded option #26

merged 5 commits into from
Nov 23, 2024

Conversation

techouse
Copy link
Owner

@techouse techouse commented Nov 22, 2024

Description

This pull request introduces significant enhancements to the qs_dart package, focusing on improving the handling of list and parameter limits, refining type handling, and adding new utility methods. The most important changes include the introduction of limit checks and error handling in DecodeOptions, updates to list and parameter parsing, and new utility methods in extensions.

Handling of list and parameter limits:

  • lib/src/extensions/decode.dart: Added _parseListValue method to handle list parsing with limit checks and error handling. Updated _parseQueryStringValues and related methods to include limit checks for parameters and lists. [1] [2] [3] [4]

Refining type handling:

New utility methods:

Enhancements to DecodeOptions:

Updates to tests:

Type of change

Please delete options that are not relevant.

  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

  • Added new tests

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

--

Related to ljharb/qs#517

@techouse techouse added the enhancement New feature or request label Nov 22, 2024
@techouse techouse self-assigned this Nov 22, 2024
Copy link

coderabbitai bot commented Nov 22, 2024

Walkthrough

The changes in this pull request involve significant enhancements to the parsing and filtering functionalities across various extensions and classes. Key modifications include renaming and expanding the _parseArrayValue method to _parseListValue, introducing a new throwOnLimitExceeded option in the DecodeOptions class, and replacing type-specific filtering methods with more generic alternatives in the IterableExtension and ListExtension. Additionally, the queryParametersQs method in the Uri class has been updated to support advanced parsing features, including list indices and depth control.

Changes

File Change Summary
lib/src/extensions/decode.dart Renamed _parseArrayValue to _parseListValue, added currentListLength parameter, enhanced error handling for list limits. Updated _parseQueryStringValues for integer handling of limit. Modified _parseObject to track list length.
lib/src/extensions/extensions.dart Removed whereNotUndefined() methods, added whereNotType<Q>() in IterableExtension. Removed whereNotUndefined() from ListExtension, added slice([int start = 0, int? end]) for list manipulation.
lib/src/models/decode_options.dart Added throwOnLimitExceeded boolean property to DecodeOptions, updated constructor and copyWith method to include this new property.
lib/src/qs.dart Added import for IterableExtension from package:collection. No structural changes to the QS class.
lib/src/utils.dart Updated utility methods to filter out Undefined values using whereNotType<Undefined>(), affecting methods like merge, _compactQueue, and others.
test/unit/decode_test.dart Added tests for parameterLimit, listLimit, and throwOnLimitExceeded options in the decode function, validating behaviour for exceeding limits and error handling.
test/unit/extensions/extensions_test.dart Updated tests to replace whereNotUndefined with whereNotType<Undefined> in Iterable and List tests. Added tests for the new slice method in ListExtension.
test/unit/uri_extension_test.dart Enhanced queryParametersQs method to support list indices, comma handling, dot notation, depth control, parameter limits, and custom decoders for character sets. Improved error handling for malformed URIs.

Poem

🐇 In the meadow, changes bloom,
Parsing lists with ample room.
Errors caught with gentle grace,
New methods join the lively race.
With limits set, we hop along,
In code's embrace, we all belong! 🌼


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ 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.
    • Generate unit testing code for this file.
    • 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. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • 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 src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

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

Documentation and Community

  • 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

codacy-production bot commented Nov 22, 2024

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
-0.37% (target: -1.00%) 83.33%
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (a46a104) 653 638 97.70%
Head commit (23ca2fc) 674 (+21) 656 (+18) 97.33% (-0.37%)

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 (#26) 48 40 83.33%

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

Codacy stopped sending the deprecated coverage status on June 5th, 2024. Learn more

Copy link

codecov bot commented Nov 22, 2024

Codecov Report

Attention: Patch coverage is 83.33333% with 8 lines in your changes missing coverage. Please review.

Project coverage is 97.32%. Comparing base (a46a104) to head (23ca2fc).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/src/extensions/decode.dart 79.31% 6 Missing ⚠️
lib/src/utils.dart 85.71% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #26      +/-   ##
==========================================
- Coverage   97.70%   97.32%   -0.38%     
==========================================
  Files          14       14              
  Lines         653      674      +21     
==========================================
+ Hits          638      656      +18     
- Misses         15       18       +3     

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


🚨 Try these New Features:

@techouse techouse changed the title ✨ decode: add throwOnLimitExceeded option qs#517 ✨ decode: add throwOnLimitExceeded option Nov 23, 2024
@techouse techouse changed the title ✨ decode: add throwOnLimitExceeded option ✨ decode: add throwOnLimitExceeded option Nov 23, 2024
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: 4

🧹 Outside diff range and nitpick comments (13)
lib/src/extensions/extensions.dart (2)

9-17: Consider improving readability of the index calculations.

The implementation is functionally correct, but the nested ternary operations could be clearer.

Consider this more readable alternative:

-  List<T> slice([int start = 0, int? end]) => sublist(
-        (start < 0 ? length + start : start).clamp(0, length),
-        (end == null ? length : (end < 0 ? length + end : end))
-            .clamp(0, length),
-      );
+  List<T> slice([int start = 0, int? end]) {
+    final adjustedStart = start < 0 ? length + start : start;
+    final boundedStart = adjustedStart.clamp(0, length);
+    
+    final rawEnd = end ?? length;
+    final adjustedEnd = rawEnd < 0 ? length + rawEnd : rawEnd;
+    final boundedEnd = adjustedEnd.clamp(0, length);
+    
+    return sublist(boundedStart, boundedEnd);
+  }

9-13: Add usage examples to the documentation.

Whilst the MDN reference is helpful, inline examples would make the usage clearer.

Consider adding examples like:

   /// Modeled after JavaScript's `Array.prototype.slice()` method.
   /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
+  /// 
+  /// Examples:
+  /// ```dart
+  /// final fruits = ['apple', 'banana', 'orange'];
+  /// fruits.slice(1, 2);    // ['banana']
+  /// fruits.slice(-2);      // ['banana', 'orange']
+  /// fruits.slice(1, -1);   // ['banana']
+  /// ```
test/unit/extensions/extensions_test.dart (1)

23-37: Well-structured test cases for slice functionality

The test cases comprehensively cover various slicing scenarios including positive indices, negative indices, and ranges. Consider adding these edge cases:

  • Empty list
  • Single-element list
  • Out-of-bounds indices
  • start > end scenarios

Here's a suggested addition:

    test('slice', () {
      const List<String> animals = [
        'ant',
        'bison',
        'camel',
        'duck',
        'elephant',
      ];
      expect(animals.slice(2), ['camel', 'duck', 'elephant']);
      expect(animals.slice(2, 4), ['camel', 'duck']);
      expect(animals.slice(1, 5), ['bison', 'camel', 'duck', 'elephant']);
      expect(animals.slice(-2), ['duck', 'elephant']);
      expect(animals.slice(2, -1), ['camel', 'duck']);
      expect(animals.slice(), ['ant', 'bison', 'camel', 'duck', 'elephant']);
+     // Edge cases
+     expect(<String>[].slice(), []);
+     expect(['single'].slice(0), ['single']);
+     expect(animals.slice(10), []);
+     expect(animals.slice(3, 2), []);
    });
lib/src/qs.dart (1)

Line range hint 1-150: Well-structured implementation of the new feature

The implementation of the throwOnLimitExceeded option follows good architectural principles by:

  1. Keeping the main QS class interface unchanged
  2. Encapsulating the new functionality in the appropriate part files
  3. Using the DecodeOptions class for configuration

This approach maintains clean separation of concerns and makes the feature easily discoverable through the options pattern.

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

114-116: Consider enhancing the documentation with examples.

Whilst the documentation explains the purpose clearly, it would be more helpful to include:

  • Example scenarios when this option should be enabled
  • Code examples showing the behaviour with and without this option
  • Related properties that work in conjunction with this option (e.g., listLimit and parameterLimit)

Line range hint 129-186: Critical: Update supporting methods to include the new property.

The following methods need to be updated to handle the new throwOnLimitExceeded property:

  1. copyWith: Add parameter to support cloning with modified value
  2. props: Add property to ensure proper value comparison
  3. toString: Add property to ensure complete string representation

Apply these changes:

  DecodeOptions copyWith({
    bool? allowDots,
    bool? allowEmptyLists,
    // ... other parameters ...
    bool? strictDepth,
    Decoder? decoder,
+   bool? throwOnLimitExceeded,
  }) =>
      DecodeOptions(
        allowDots: allowDots ?? this.allowDots,
        // ... other assignments ...
        strictDepth: strictDepth ?? this.strictDepth,
        decoder: decoder ?? _decoder,
+       throwOnLimitExceeded: throwOnLimitExceeded ?? this.throwOnLimitExceeded,
      );

  @override
  String toString() => 'DecodeOptions(\n'
      // ... other properties ...
      '  strictDepth: $strictDepth,\n'
-     '  strictNullHandling: $strictNullHandling\n'
+     '  strictNullHandling: $strictNullHandling,\n'
+     '  throwOnLimitExceeded: $throwOnLimitExceeded\n'
      ')';

  @override
  List<Object?> get props => [
        // ... other properties ...
        strictDepth,
        strictNullHandling,
        _decoder,
+       throwOnLimitExceeded,
      ];
lib/src/extensions/decode.dart (1)

143-145: Add unit tests to cover list length calculation in _parseObject

The logic on lines 143-145 calculates currentListLength for lists with indexed keys. This code is not currently covered by tests, which may lead to unnoticed bugs in list limit enforcement.

Would you like assistance in generating unit tests to cover this scenario?

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 143-145: lib/src/extensions/decode.dart#L143-L145
Added lines #L143 - L145 were not covered by tests

test/unit/uri_extension_test.dart (5)

Line range hint 442-455: Suggestion: Add tests for throwOnLimitExceeded when list limits are exceeded

Currently, the test cases verify the behaviour when listLimit is exceeded without throwing an error. To ensure the new error-handling logic functions correctly, please add tests where throwOnLimitExceeded is set to true and the listLimit is exceeded, to confirm that an exception is thrown as expected.


Line range hint 867-881: Suggestion: Add tests for throwOnLimitExceeded with parameter limits

The existing tests check that parameters exceeding parameterLimit are ignored. To fully validate the new error-handling behaviour, consider adding tests where throwOnLimitExceeded is set to true and parameterLimit is exceeded, ensuring that an appropriate exception is raised.


Line range hint 1300-1324: Consider handling unsupported charsets gracefully

An assertion failure occurs when an unknown charset is provided. It might be beneficial to handle unsupported charsets by throwing a more descriptive exception or by defaulting to a standard charset, improving the robustness of the code.


Line range hint 1570-1585: Avoid potential null pointer exceptions in custom decoders

In the test where a custom decoder returns null, ensure that subsequent code can handle null values without throwing exceptions. This prevents potential runtime errors when dealing with unexpected or malformed input.


Line range hint 1700-1715: Enhance test coverage for numeric entity interpretation

Currently, tests verify numeric entity interpretation in iso-8859-1 charset when interpretNumericEntities is enabled. To ensure comprehensive coverage, add tests for cases where interpretNumericEntities is disabled, and when using different charsets like utf-8, to confirm behaviour remains consistent.

test/unit/decode_test.dart (1)

1791-1799: Consider documenting silent truncation behaviour

When throwOnLimitExceeded is not specified, and the parameter limit is exceeded, the decoder silently truncates excess parameters. It would be beneficial to clearly document this behaviour to prevent confusion for users who might expect an error or warning.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between a46a104 and e8ac65d.

📒 Files selected for processing (8)
  • lib/src/extensions/decode.dart (4 hunks)
  • lib/src/extensions/extensions.dart (1 hunks)
  • lib/src/models/decode_options.dart (2 hunks)
  • lib/src/qs.dart (1 hunks)
  • lib/src/utils.dart (5 hunks)
  • test/unit/decode_test.dart (3 hunks)
  • test/unit/extensions/extensions_test.dart (2 hunks)
  • test/unit/uri_extension_test.dart (2 hunks)
🧰 Additional context used
🪛 GitHub Check: codecov/patch
lib/src/extensions/decode.dart

[warning] 143-145: lib/src/extensions/decode.dart#L143-L145
Added lines #L143 - L145 were not covered by tests

lib/src/utils.dart

[warning] 123-123: lib/src/utils.dart#L123
Added line #L123 was not covered by tests


[warning] 127-127: lib/src/utils.dart#L127
Added line #L127 was not covered by tests


[warning] 389-389: lib/src/utils.dart#L389
Added line #L389 was not covered by tests


[warning] 391-391: lib/src/utils.dart#L391
Added line #L391 was not covered by tests

🔇 Additional comments (17)
lib/src/extensions/extensions.dart (1)

4-5: LGTM! Note the breaking change.

The replacement of whereNotUndefined() with a more generic whereNotType<Q>() is a good improvement that provides more flexibility. However, this is a breaking change that will require updates in consuming code.

Let's verify the usage of the old method to ensure all occurrences are updated:

✅ Verification successful

The change is properly reflected in tests

The test file shows that the tests have already been updated to use the new whereNotType<Undefined>() method instead of the old whereNotUndefined(). The tests demonstrate that the functionality remains the same whilst being more generic. No further action is required.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining usage of whereNotUndefined
rg "whereNotUndefined" --type dart

Length of output: 188


Script:

#!/bin/bash
# Let's check the test file content to understand the context
cat test/unit/extensions/extensions_test.dart

Length of output: 1713

test/unit/extensions/extensions_test.dart (2)

9-9: Excellent improvement to type safety!

The change from whereNotUndefined to whereNotType<Undefined> makes the method more generic and type-safe whilst maintaining the same functionality.


18-18: LGTM - Consistent with IterableExtension changes

The addition of .toList() properly maintains the return type whilst adopting the more generic whereNotType<Undefined> approach.

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

29-29: LGTM! Constructor parameter addition is well-implemented.

The new parameter follows the existing pattern and maintains backward compatibility with its default value of false.

lib/src/utils.dart (3)

45-52: LGTM: Robust implementation of undefined value handling

The implementation correctly maintains collection type (Set/List) whilst filtering undefined values, with proper key ordering via SplayTreeMap.


76-80: LGTM: Improved collection filtering

Good use of whereNotType<Undefined>() for a more type-safe and cleaner implementation.


104-104: Add test coverage for list construction logic

These lines handling list construction with undefined filtering lack test coverage.

Let's verify the current test coverage:

Would you like me to help generate comprehensive test cases for these scenarios?

Also applies to: 123-127

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

20-26: List limit enforcement in _parseListValue is correctly implemented

The condition currentListLength >= options.listLimit appropriately throws a RangeError when the list limit is exceeded, ensuring that lists do not grow beyond the specified limit.

test/unit/decode_test.dart (9)

415-415: No issues found in expected result

The test case correctly specifies the expected result when decoding mixed simple and explicit lists with a specified listLimit.


821-821: Verification of sparse list compacting

The test accurately checks the decoder's behaviour when dealing with sparse lists and a specified listLimit.


1773-1779: Good coverage of parameter limit within bounds

The test correctly confirms that no error is thrown when the number of parameters is within the specified limit and throwOnLimitExceeded is true.


1781-1789: Proper error handling when parameter limit is exceeded

The test appropriately asserts that a RangeError is thrown when the parameter limit is exceeded and throwOnLimitExceeded is true. This ensures robust error handling in the decoder.


1801-1809: Explicit test for silent truncation without error

The test verifies that when throwOnLimitExceeded is false, the decoder silently truncates parameters exceeding the limit. This behaviour should align with user expectations and be well-documented.


1811-1819: Confirmation of unlimited parameters with parameterLimit set to Infinity

The test correctly confirms that setting parameterLimit to double.infinity allows the decoder to handle an unlimited number of parameters as expected.


1823-1833: Testing list decoding within limits

The test appropriately checks that the decoder functions correctly when the list size is within the specified listLimit and throwOnLimitExceeded is true.


1835-1843: Correct error handling when list limit is exceeded

The test effectively asserts that a RangeError is thrown when the list limit is exceeded with throwOnLimitExceeded set to true, ensuring proper error handling in the decoder.


1845-1855: Verify the conversion of lists to maps when limits are exceeded

When the list exceeds listLimit and throwOnLimitExceeded is not set or false, the decoder converts the list into a map. Ensure that this behaviour is compatible with the rest of the codebase and does not introduce issues where lists are expected.

Run the following script to check for code that may be affected by this change:

✅ Verification successful

Let me gather more context about these usages and the options being passed.


Let me check one more thing to understand how this behaviour is documented and if there are any related tests.


List-to-map conversion behaviour is well-documented and tested

The conversion of lists to maps when exceeding listLimit is an intentional and well-tested behaviour in the codebase. The evidence shows:

  • The default listLimit of 20 is consistently used across the codebase
  • There are comprehensive test cases covering this behaviour, including:
    • Conversion of lists to maps when indices exceed the limit
    • Proper handling of sparse lists
    • Consistent behaviour across both direct QS.decode and queryParametersQs usage

The two main usages in the library code (in uri.dart and methods.dart) both use the default DecodeOptions, making this behaviour consistent throughout the codebase.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for usages of QS.decode without throwOnLimitExceeded to identify potential impacts.

# Find all QS.decode calls without throwOnLimitExceeded parameter
rg 'QS\.decode\([^\)]*\)' --glob 'lib/**/*.dart' --vimgrep --no-filename | grep -v 'throwOnLimitExceeded'

Length of output: 226


Script:

#!/bin/bash
# Get more context around the QS.decode usages
rg 'QS\.decode' -B 3 -A 3 --glob 'lib/**/*.dart'

# Also check for any documentation about listLimit
rg 'listLimit' --glob 'lib/**/*.dart'

Length of output: 2797


Script:

#!/bin/bash
# Check for documentation and tests related to listLimit behaviour
rg -B 3 -A 3 'listLimit.*20' --glob '**/*.{dart,md}'

# Also check for any tests that verify list to map conversion
rg -B 3 -A 3 'converts.*list.*to.*map' --glob 'test/**/*.dart'

Length of output: 11843

lib/src/qs.dart Show resolved Hide resolved
lib/src/utils.dart Outdated Show resolved Hide resolved
lib/src/extensions/decode.dart Show resolved Hide resolved
lib/src/extensions/decode.dart Outdated Show resolved Hide resolved
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: 2

🧹 Outside diff range and nitpick comments (11)
lib/src/extensions/extensions.dart (3)

4-5: Documentation could be enhanced with examples

The generic implementation is elegant and more flexible than the previous type-specific approach. Consider enhancing the documentation with usage examples to make it more developer-friendly.

Add examples to the documentation:

  /// Returns a new [Iterable] without elements of type [Q].
+ ///
+ /// Example:
+ /// ```dart
+ /// final items = ['a', 1, 'b', 2];
+ /// final onlyStrings = items.whereNotType<int>(); // ['a', 'b']
+ /// ```
  Iterable<T> whereNotType<Q>() => where((T el) => el is! Q);

13-17: Consider caching the length calculation

The implementation correctly handles negative indices and bounds checking. However, we could optimise it by caching the length calculation.

Consider this more efficient implementation:

- List<T> slice([int start = 0, int? end]) => sublist(
-       (start < 0 ? length + start : start).clamp(0, length),
-       (end == null ? length : (end < 0 ? length + end : end))
-           .clamp(0, length),
-     );
+ List<T> slice([int start = 0, int? end]) {
+   final len = length;
+   final actualStart = (start < 0 ? len + start : start).clamp(0, len);
+   final actualEnd = (end == null ? len : (end < 0 ? len + end : end))
+       .clamp(0, len);
+   return sublist(actualStart, actualEnd);
+ }

9-12: Documentation could benefit from examples

The documentation clearly states the purpose and includes a helpful MDN reference. However, it would be more helpful with examples showing both positive and negative indices.

Add examples to the documentation:

  /// Extracts a section of a list and returns a new list.
  ///
  /// Modeled after JavaScript's `Array.prototype.slice()` method.
  /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
+ ///
+ /// Example:
+ /// ```dart
+ /// final list = [1, 2, 3, 4, 5];
+ /// list.slice(1, 3);   // [2, 3]
+ /// list.slice(-2);     // [4, 5]
+ /// list.slice(2, -1);  // [3, 4]
+ /// ```
test/unit/extensions/extensions_test.dart (1)

23-37: Comprehensive slice test coverage with room for enhancement!

The test cases effectively cover various slicing scenarios including positive indices, negative indices, and different range combinations. The use of a clear, meaningful example with animal names makes the tests easy to understand.

Consider adding these additional test cases to improve coverage:

  • Slicing an empty list
  • Verifying the original list remains unchanged after slicing
 test('slice', () {
   const List<String> animals = [
     'ant',
     'bison',
     'camel',
     'duck',
     'elephant',
   ];
   expect(animals.slice(2), ['camel', 'duck', 'elephant']);
   expect(animals.slice(2, 4), ['camel', 'duck']);
   expect(animals.slice(1, 5), ['bison', 'camel', 'duck', 'elephant']);
   expect(animals.slice(-2), ['duck', 'elephant']);
   expect(animals.slice(2, -1), ['camel', 'duck']);
   expect(animals.slice(), ['ant', 'bison', 'camel', 'duck', 'elephant']);
+  // Test empty list
+  expect(<String>[].slice(0), isEmpty);
+  // Test immutability
+  final result = animals.slice(1, 3);
+  expect(animals, ['ant', 'bison', 'camel', 'duck', 'elephant']);
 });
lib/src/models/decode_options.dart (4)

114-116: Enhance documentation clarity

The documentation should specify which limits this option affects (listLimit and parameterLimit) to provide better context for developers.

-  /// Set to `true` to throw an error when the limit is exceeded.
+  /// Set to `true` to throw an error when either [listLimit] or [parameterLimit] is exceeded.
+  /// This provides a way to handle limit violations more strictly instead of silently
+  /// truncating the data.
   final bool throwOnLimitExceeded;

Line range hint 127-166: Add missing throwOnLimitExceeded to copyWith method

The throwOnLimitExceeded parameter is missing from the copyWith method, which means this property cannot be modified when creating a copy of the options.

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

Line range hint 168-189: Add missing throwOnLimitExceeded to toString method

The throwOnLimitExceeded property is missing from the string representation, which could affect debugging and logging.

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

Line range hint 191-209: Add missing throwOnLimitExceeded to props list

The throwOnLimitExceeded property is missing from the props list, which means it won't be considered during equality comparisons. This could lead to bugs where two instances with different throwOnLimitExceeded values are incorrectly considered equal.

   @override
   List<Object?> get props => [
         allowDots,
         allowEmptyLists,
         listLimit,
         charset,
         charsetSentinel,
         comma,
         decodeDotInKeys,
         delimiter,
         depth,
         duplicates,
         ignoreQueryPrefix,
         interpretNumericEntities,
         parameterLimit,
         parseLists,
         strictDepth,
         strictNullHandling,
+        throwOnLimitExceeded,
         _decoder,
       ];
lib/src/utils.dart (1)

45-52: Consider simplifying the nested conditional structure

The current implementation, whilst correct, could benefit from being restructured for improved readability.

Consider this more straightforward approach:

-          target = target_.values.any((el) => el is Undefined)
-              ? SplayTreeMap.from({
-                  for (final MapEntry<int, dynamic> entry in target_.entries)
-                    if (entry.value is! Undefined) entry.key: entry.value,
-                })
-              : target is Set
-                  ? target_.values.toSet()
-                  : target_.values.toList();
+          final values = target_.values.whereNotType<Undefined>();
+          target = target is Set ? values.toSet() : values.toList();
test/unit/decode_test.dart (2)

1787-1787: Use specific exception matcher for clarity

For better readability, consider using the throwsRangeError matcher instead of throwsA(isA<RangeError>()). This simplifies the test and makes its intent clearer.

Apply this diff to update the test:

-            throwsA(isA<RangeError>()),
+            throwsRangeError,

1841-1841: Use specific exception matcher for clarity

For better readability, consider using the throwsRangeError matcher instead of throwsA(isA<RangeError>()). This simplifies the test and makes its intent clearer.

Apply this diff to update the test:

-            throwsA(isA<RangeError>()),
+            throwsRangeError,
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between a46a104 and e8ac65d.

📒 Files selected for processing (8)
  • lib/src/extensions/decode.dart (4 hunks)
  • lib/src/extensions/extensions.dart (1 hunks)
  • lib/src/models/decode_options.dart (2 hunks)
  • lib/src/qs.dart (1 hunks)
  • lib/src/utils.dart (5 hunks)
  • test/unit/decode_test.dart (3 hunks)
  • test/unit/extensions/extensions_test.dart (2 hunks)
  • test/unit/uri_extension_test.dart (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • lib/src/qs.dart
🧰 Additional context used
🪛 GitHub Check: codecov/patch
lib/src/extensions/decode.dart

[warning] 143-145: lib/src/extensions/decode.dart#L143-L145
Added lines #L143 - L145 were not covered by tests

lib/src/utils.dart

[warning] 123-123: lib/src/utils.dart#L123
Added line #L123 was not covered by tests


[warning] 127-127: lib/src/utils.dart#L127
Added line #L127 was not covered by tests


[warning] 389-389: lib/src/utils.dart#L389
Added line #L389 was not covered by tests


[warning] 391-391: lib/src/utils.dart#L391
Added line #L391 was not covered by tests

🔇 Additional comments (7)
test/unit/extensions/extensions_test.dart (2)

9-9: Well-structured generic type filtering implementation!

The change to whereNotType<Undefined> makes the filtering more generic and type-safe whilst maintaining the same functionality. This approach aligns well with Dart's type system and promotes code reuse.


18-18: Consistent implementation with IterableExtension!

The change maintains consistency with the IterableExtension implementation whilst properly preserving the List type through the toList() call.

lib/src/utils.dart (2)

76-77: LGTM! Clean implementation of generic type filtering

The change to use whereNotType<Undefined>() is a good improvement, making the code more consistent with Dart's generic type system.

Also applies to: 79-80


104-104: Add test coverage for list combination logic

These lines handle important edge cases in list combination, but they currently lack test coverage. Please add tests to verify the behaviour when combining lists with Undefined values.

Let's verify the existing test coverage:

Would you like me to help generate comprehensive test cases for these scenarios?

Also applies to: 123-123, 127-127

test/unit/uri_extension_test.dart (2)

442-445: Verify the expected output for list parsing with listLimit

In this test case, given the input 'a[1]=b&a=c' and listLimit: 20, the expected output is {'a': {1: 'b', 2: 'c'}}. Please confirm if this is correct, considering that a=c might be intended to increment the list, resulting in {'a': {1: 'b', 1: 'c'}}, or if a should be parsed as a list rather than a map.


867-870: Clarify handling of sparse arrays in test case

The test expects {'a': {2: '2', 10: '1'}} for the input 'a[10]=1&a[2]=2' with listLimit: 20. Please verify if the parsing behaviour should result in a being a sparse array or an object. If listLimit allows lists up to 20 elements, should a be an array with indices at positions 2 and 10, or remain as an object with those keys?

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

143-145: ⚠️ Potential issue

Undefined method 'firstWhereIndexedOrNull'

The method firstWhereIndexedOrNull used here may not be defined or imported in the current context. Please ensure that this method is implemented or properly imported, as it is essential for the list parsing logic.

Run the following script to verify if firstWhereIndexedOrNull is defined in the codebase:

✅ Verification successful

The method firstWhereIndexedOrNull is provided by the collection package

The code is actually correct. The firstWhereIndexedOrNull method is provided by the collection package, which is listed as a dependency in the project's pubspec.yaml. This is a standard Dart package that provides additional collection methods, including firstWhereIndexedOrNull. The method is available for use in the codebase through this dependency.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for the definition of 'firstWhereIndexedOrNull' in the codebase.

# Expectation: Find the method definition or an import statement.

rg 'firstWhereIndexedOrNull[ \t]*\('

Length of output: 149


Script:

#!/bin/bash
# Let's check if there's an extension or import that provides firstWhereIndexedOrNull
rg -l "firstWhereIndexedOrNull" lib/src/extensions/decode.dart -C 10

# Also search for any potential extension methods or imports
rg "extension.*on List" 

Length of output: 200


Script:

#!/bin/bash
# Let's check the ListExtension implementation to see if it contains firstWhereIndexedOrNull
cat lib/src/extensions/extensions.dart

# Also check the imports in decode.dart
rg "^import" lib/src/extensions/decode.dart

Length of output: 1282


Script:

#!/bin/bash
# Let's check if firstWhereIndexedOrNull is provided by a package
# First, let's locate the pubspec.yaml
fd "pubspec.yaml"

# Then read its contents to check dependencies
cat pubspec.yaml

Length of output: 677

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 143-145: lib/src/extensions/decode.dart#L143-L145
Added lines #L143 - L145 were not covered by tests

lib/src/utils.dart Outdated Show resolved Hide resolved
lib/src/extensions/decode.dart Show resolved Hide resolved
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

🧹 Outside diff range and nitpick comments (3)
lib/src/extensions/decode.dart (2)

11-36: Consider improving error message consistency and reusability

The implementation looks solid, but the error messages are duplicated. Consider extracting the message construction to a separate method for better maintainability.

Here's a suggested improvement:

+ static String _buildListLimitErrorMessage(int limit) =>
+   'List limit exceeded. Only $limit element${limit == 1 ? '' : 's'} allowed in a list.';

  static dynamic _parseListValue(
    dynamic val,
    DecodeOptions options,
    int currentListLength,
  ) {
    if (val is String && val.isNotEmpty && options.comma && val.contains(',')) {
      final List<String> splitVal = val.split(',');
      if (options.throwOnLimitExceeded && splitVal.length > options.listLimit) {
-       throw RangeError(
-         'List limit exceeded. '
-         'Only ${options.listLimit} element${options.listLimit == 1 ? '' : 's'} allowed in a list.',
-       );
+       throw RangeError(_buildListLimitErrorMessage(options.listLimit));
      }
      return splitVal;
    }

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

    return val;
  }
🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 19-19: lib/src/extensions/decode.dart#L19
Added line #L19 was not covered by tests


[warning] 21-21: lib/src/extensions/decode.dart#L21
Added line #L21 was not covered by tests


49-67: Consider consolidating limit validation logic

The implementation correctly handles parameter limits, but the validation and error handling logic could be more centralised.

Consider extracting the limit validation into a separate method:

+ static void _validateLimit(int? limit, String type) {
+   if (limit != null && limit <= 0) {
+     throw ArgumentError('$type limit must be a positive integer.');
+   }
+ }

  static Map<String, dynamic> _parseQueryStringValues(
    String str, [
    DecodeOptions options = const DecodeOptions(),
  ]) {
    // ...
    final int? limit = options.parameterLimit == double.infinity
        ? null
        : options.parameterLimit.toInt();

-   if (limit != null && limit <= 0) {
-     throw ArgumentError('Parameter limit must be a positive integer.');
-   }
+   _validateLimit(limit, 'Parameter');
🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 54-54: lib/src/extensions/decode.dart#L54
Added line #L54 was not covered by tests

test/unit/decode_test.dart (1)

1783-1831: Consider adding test coverage for negative parameter limits

The parameter limit test group provides good coverage of standard cases and edge cases. However, it would be beneficial to add test cases for negative parameter limits to ensure robust error handling.

Consider adding these test cases:

+    test('handles negative parameter limit correctly', () {
+      expect(
+        () => QS.decode(
+          'a=1&b=2',
+          const DecodeOptions(parameterLimit: -1, throwOnLimitExceeded: true),
+        ),
+        throwsA(isA<RangeError>()),
+      );
+    });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between e8ac65d and 7215cd1.

📒 Files selected for processing (2)
  • lib/src/extensions/decode.dart (4 hunks)
  • test/unit/decode_test.dart (4 hunks)
🧰 Additional context used
🪛 GitHub Check: codecov/patch
lib/src/extensions/decode.dart

[warning] 19-19: lib/src/extensions/decode.dart#L19
Added line #L19 was not covered by tests


[warning] 21-21: lib/src/extensions/decode.dart#L21
Added line #L21 was not covered by tests


[warning] 54-54: lib/src/extensions/decode.dart#L54
Added line #L54 was not covered by tests


[warning] 154-156: lib/src/extensions/decode.dart#L154-L156
Added lines #L154 - L156 were not covered by tests

🔇 Additional comments (2)
lib/src/extensions/decode.dart (1)

148-160: 🛠️ Refactor suggestion

Add documentation and improve test coverage

The currentListLength calculation logic is complex and lacks proper documentation. Additionally, the static analysis indicates missing test coverage for this section.

Let's verify the test coverage:

Consider adding documentation and tests:

+ /// Calculates the current length of a list at a specific position in the chain.
+ /// 
+ /// If the chain ends with '[]' and contains a valid parent key index,
+ /// returns the length of the list at that index. Otherwise, returns 0.
+ /// 
+ /// @param chain The chain of keys being processed
+ /// @param val The current value being processed
+ /// @returns The calculated list length
  late final int currentListLength;

  if (chain.isNotEmpty && chain.last == '[]') {
    final int? parentKey = int.tryParse(chain.slice(0, -1).join(''));

    currentListLength = parentKey != null &&
            val is List &&
            val.firstWhereIndexedOrNull((int i, _) => i == parentKey) != null
        ? val.elementAt(parentKey).length
        : 0;
  } else {
    currentListLength = 0;
  }
✅ Verification successful

Test coverage exists through comprehensive query string parsing tests

The currentListLength calculation is indirectly tested through numerous test cases that exercise array/list handling in query string parsing. The test files show extensive coverage of:

  • Empty list handling: foo[]&bar=baz
  • Nested list operations: a[b][]=c&a[b][]=d
  • List index handling: a[0][]=b
  • Mixed list and object operations: foo[bad]=baz&foo[]=bar

The implementation is well-tested through these integration tests, though they could be made more explicit by adding specific test cases for the currentListLength calculation.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check test coverage for the currentListLength calculation logic
rg -l "currentListLength.*=.*parentKey" test/

Length of output: 47


Script:

#!/bin/bash
# Let's check for any test files related to decode.dart
fd "decode.*test\.dart$" test/

# Then search for any test cases involving array/list operations
rg "\[\]|List<|elementAt|firstWhereIndexedOrNull" test/ -A 5 -B 5

Length of output: 100512


Script:

#!/bin/bash
# Let's check for test coverage of the currentListLength calculation logic
rg -A 5 -B 5 "currentListLength.*=.*parentKey" lib/

Length of output: 784

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 154-156: lib/src/extensions/decode.dart#L154-L156
Added lines #L154 - L156 were not covered by tests

test/unit/decode_test.dart (1)

415-415: LGTM: Test cases correctly validate list limit behaviour

The modifications to the expected outputs properly verify that sparse lists exceeding the limit are converted to maps, which is the expected behaviour.

Also applies to: 832-832

test/unit/decode_test.dart Outdated Show resolved Hide resolved
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

🧹 Outside diff range and nitpick comments (2)
lib/src/utils.dart (1)

45-52: Consider refactoring for better readability

The nested ternary operators make the code harder to follow. Consider extracting the logic into a separate method or using if-else blocks for better clarity.

-          target = target_.values.any((el) => el is Undefined)
-              ? SplayTreeMap.from({
-                  for (final MapEntry<int, dynamic> entry in target_.entries)
-                    if (entry.value is! Undefined) entry.key: entry.value,
-                })
-              : target is Set
-                  ? target_.values.toSet()
-                  : target_.values.toList();
+          final values = target_.values;
+          if (values.any((el) => el is Undefined)) {
+            target = SplayTreeMap.from({
+              for (final entry in target_.entries)
+                if (entry.value is! Undefined) entry.key: entry.value,
+            });
+          } else {
+            target = target is Set ? values.toSet() : values.toList();
+          }
test/unit/decode_test.dart (1)

1782-1899: LGTM! Comprehensive test coverage for parameter and list limits.

The new test groups thoroughly cover the parameterLimit, listLimit, and throwOnLimitExceeded options. The tests effectively verify:

  • Success and error cases
  • Silent truncation behaviour
  • Edge cases (zero, negative, and infinite limits)
  • Nested list scenarios

Consider adding a test case to verify the interaction between parameterLimit and listLimit when both are exceeded simultaneously:

test('handles both parameter and list limits being exceeded', () {
  expect(
    () => QS.decode(
      'a[]=1&a[]=2&a[]=3&b=4&c=5&d=6',
      const DecodeOptions(
        parameterLimit: 2,
        listLimit: 2,
        throwOnLimitExceeded: true,
      ),
    ),
    throwsA(isA<RangeError>()),
  );
});
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 7215cd1 and 23ca2fc.

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

76-80: LGTM! Clean implementation of generic type filtering

The replacement of whereNotUndefined() with whereNotType<Undefined>() is well-implemented and maintains proper collection type handling.


104-104: LGTM! Consistent implementation of type filtering

The changes consistently replace whereNotUndefined() with whereNotType<Undefined>() across all instances, improving code consistency.

Also applies to: 123-123, 127-127

test/unit/decode_test.dart (2)

415-415: LGTM! Fixed assertion to use integer keys.

The change correctly updates the expected output to use integer keys instead of string keys when transforming lists to maps.


468-478: LGTM! Good test coverage for nested lists.

The test case effectively verifies that nested lists maintain their structure when created with a parent key. This is a valuable addition to the test suite.

@techouse techouse merged commit 90f545f into main Nov 23, 2024
12 of 14 checks passed
@techouse techouse deleted the feat/qs-517 branch November 23, 2024 11:53
techouse added a commit that referenced this pull request Nov 23, 2024
## 1.3.0

- [FEAT] add `DecodeOptions.throwOnLimitExceeded` option ([#26](#26))
- [CHORE] remove dead code in `Utils`
- [CHORE] add more tests
- [CHORE] update dependencies
@techouse techouse mentioned this pull request Nov 23, 2024
techouse added a commit that referenced this pull request Nov 23, 2024
* 🔖 release v1.3.0

## 1.3.0

- [FEAT] add `DecodeOptions.throwOnLimitExceeded` option ([#26](#26))
- [CHORE] remove dead code in `Utils`
- [CHORE] add more tests
- [CHORE] update dependencies
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