Skip to content

Conversation

@christerswahn
Copy link
Collaborator

@christerswahn christerswahn commented Jun 12, 2025

When Configuration is used without BetterCommand/Runner its default behavior should be to throw an informative exception on usage errors, rather then expecting the caller to explicitly check for errors and always provide custom error handling. (This also mirrors the behavior of args package ArgParser.) The old behavior is still available but now opt-in.

The thrown UsageException contains the error messages produced by the options resolution, and the usage help text. This exception behavior is consistent with the BetterCommand/Runner.

This is a breaking change for direct uses of Configuration, but not for uses of BetterCommand/Runner.

Depends on PR #47

Summary by CodeRabbit

  • New Features

    • Added a new configuration resolution method that does not throw exceptions, allowing for more flexible error handling.
    • Introduced a utility for improved formatting of configuration error messages.
  • Bug Fixes

    • Improved error reporting for invalid option definitions and configuration errors.
  • Refactor

    • Renamed error classes and updated error handling to use more descriptive types.
    • Adjusted regular expression matching for duration parsing to improve accuracy.
    • Modified configuration resolution logic to separate error throwing from resolution.
  • Tests

    • Updated and expanded test coverage to reflect new error handling behaviors and exception types.

@coderabbitai
Copy link

coderabbitai bot commented Jun 12, 2025

📝 Walkthrough

Walkthrough

This change replaces usage of Configuration.resolve with Configuration.resolveNoExcept throughout the codebase, altering error handling during configuration resolution to avoid throwing exceptions. It introduces a new Configuration.resolve factory that throws on errors, renames InvalidOptionConfigurationError to OptionDefinitionError, and centralizes error formatting utilities.

Changes

File(s) Change Summary
lib/src/config/configuration.dart Added Configuration.resolve factory (throws on errors), renamed resolve to resolveNoExcept, added throwExceptionOnErrors, updated error handling.
lib/src/config/exceptions.dart, lib/src/config/option_groups.dart,
lib/src/config/options.dart Renamed InvalidOptionConfigurationError to OptionDefinitionError and updated usages throughout.
lib/src/config/output_formatting.dart Added formatConfigError function and _isPunctuation helper for error message formatting.
lib/src/better_command_runner/better_command.dart,
lib/src/better_command_runner/better_command_runner.dart,
example/config_file_example.dart Modified resolveConfiguration to use Configuration.resolveNoExcept instead of resolve.
lib/src/config/config_parser.dart Removed error-checking and exception-throwing logic in ConfigParser.parse.
lib/src/config/option_types.dart Updated regex in DurationParser.parse to prioritize two-character unit suffixes.
test/config/config_source_test.dart,
test/config/configuration_test.dart,
test/configuration_type_test.dart,
test/config/file_options_test.dart Replaced Configuration.resolve with Configuration.resolveNoExcept in all tests; updated expected error types.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CommandRunner
    participant Configuration

    User->>CommandRunner: Run command (with args)
    CommandRunner->>Configuration: resolveNoExcept(options, argResults, env, configBroker)
    Configuration-->>CommandRunner: Returns Configuration (with errors if any)
    CommandRunner-->>User: Continues execution (handles errors as needed)
Loading
sequenceDiagram
    participant User
    participant Configuration

    User->>Configuration: resolve(options, argResults, env, configBroker)
    Configuration->>Configuration: resolveNoExcept(...)
    Configuration->>Configuration: throwExceptionOnErrors()
    alt Errors exist
        Configuration-->>User: Throws UsageException
    else No errors
        Configuration-->>User: Returns Configuration
    end
Loading

Possibly related PRs

Suggested reviewers

  • Isakdl

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.
    • Explain this complex logic.
    • 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 explain this code block.
    • @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 explain its main purpose.
    • @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.

Support

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

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 generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this 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.

@christerswahn christerswahn marked this pull request as ready for review June 12, 2025 17:40
@christerswahn christerswahn requested a review from SandPod June 12, 2025 17:41
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: 3

🔭 Outside diff range comments (1)
example/config_file_example.dart (1)

63-68: 🛠️ Refactor suggestion

Consider surfacing configuration errors before continuing execution

resolveNoExcept will happily return a Configuration containing errors.
Because runWithConfig does not call cfg.throwExceptionOnErrors() (or otherwise inspect cfg.errors), the command may continue with invalid input and only fail later or – worse – behave incorrectly.

   Configuration<TimeSeriesOption> resolveConfiguration(ArgResults? argResults) {
-    return Configuration.resolveNoExcept(
+    final cfg = Configuration.resolveNoExcept(
       options: options,
       argResults: argResults,
       env: envVariables,
       configBroker: FileConfigBroker(),
     );
+    cfg.throwExceptionOnErrors(); // will print usage + exit with UsageException
+    return cfg;
   }

This keeps the non-throwing resolution (so tests can still probe errors) while ensuring the CLI exits early in production runs.

🧹 Nitpick comments (11)
lib/src/config/multi_config_source.dart (1)

3-4: Imports updated correctly; consider lazy regex compilation

Imports look right.
Minor nit: the constructor regex() recompiles every pattern on each call site. If large or hot, consider caching compiled RegExp outside or accepting RegExp keys directly to avoid repeated compilation.

lib/src/config/config_parser.dart (1)

6-8: option_types.dart appears to be an unused import

The file compiles fine without it – all referenced symbols come from options.dart. An unused import will trigger an unused_import linter warning.

-import 'option_types.dart';

Remove or comment-out unless something later in the PR requires it.

lib/src/config/output_formatting.dart (1)

1-11: formatConfigError is not Unicode-safe and ignores leading whitespace

  1. error[0] returns the first UTF-16 code unit, not the first user-perceived character.
  2. Leading whitespace (' invalid path') will be upper-cased as whitespace, leaving the first letter lowercase.
  3. The same single-code-unit issue exists when checking the last “character”.

A safer, still lightweight implementation:

String formatConfigError(final String error) {
-  if (error.isEmpty) return error;
-  final suffix = _isPunctuation(error.substring(error.length - 1)) ? '' : '.';
-  return '${error[0].toUpperCase()}${error.substring(1)}$suffix';
+  final trimmed = error.trimLeft();
+  if (trimmed.isEmpty) return error;
+
+  final firstRune = String.fromCharCode(trimmed.runes.first).toUpperCase();
+  final rest = trimmed.substring(trimmed.runes.first.length);
+  final suffix =
+      _isPunctuation(String.fromCharCode(trimmed.runes.last)) ? '' : '.';
+
+  return '${' ' * (error.length - trimmed.length)}$firstRune$rest$suffix';
 }

This keeps semantics while handling multi-byte runes and preserving original indentation.

test/config/file_options_test.dart (1)

17-20: Tests validate the non-throwing path only – add coverage for the throwing variant

Replacing Configuration.resolve with resolveNoExcept is correct for these cases, but we now lack regression tests that resolve() still throws a UsageException with the same message set. Consider duplicating a representative test group and calling the throwing variant to keep both behaviours locked down.

test/config/config_source_test.dart (1)

245-248: Minor: test name still mentions “creating the configuration” but now asserts on resolveNoExcept throwing

Nothing functionally wrong, but renaming the test to include “(non-throwing API should still fail fast on invalid option configuration)” would make the intent clearer to future readers.

lib/src/better_command_runner/better_command.dart (2)

7-8: Prefer a relative import to avoid leaking the src namespace

Using a package: import into src/ makes the symbol part of the public API surface (users can import it).
Since both libraries live inside the same package, a relative path keeps the helper private:

-import 'package:cli_tools/src/config/output_formatting.dart';
+import '../config/output_formatting.dart';

124-129: Duplication of error-handling logic – consider delegating to the throwing factory

Configuration.resolve() now already throws a UsageException that contains the formatted usage text.
By calling resolveNoExcept() and repeating the “collect → format → throw” pattern here, we keep two independent code paths in sync.

-    return Configuration.resolveNoExcept(
+    // Let the config layer deal with the exception formatting.
+    return Configuration.resolve(
       options: options,
       argResults: argResults,
       env: envVariables,
     );

This removes ~10 lines in run() and centralises responsibility in the config layer.
If keeping custom formatting is intentional, consider extracting the duplicated block in this file and in better_command_runner.dart into a shared helper to avoid the drift risk.

lib/src/better_command_runner/better_command_runner.dart (2)

7-8: Same src import privacy concern as in better_command.dart

For the same reason, a relative import is preferable:

-import 'package:cli_tools/src/config/output_formatting.dart';
+import '../config/output_formatting.dart';

312-318: Repeated manual error aggregation – can lean on Configuration.resolve()

As in the command class, the runner still performs manual aggregation after switching to resolveNoExcept().
Unless the CLI absolutely needs its own bullet-list formatting, letting Configuration.resolve() throw directly would simplify the code path and guarantee consistency.

See previous suggestion for a minimal change.

test/config/configuration_test.dart (1)

125-145: Avoid brittle exact-match on ArgParser usage text.

ArgParser formatting may change between Dart versions, causing spurious test failures.
Prefer a substring/regex check, e.g.

expect(options.usage, contains('--project'));
expect(options.usage, contains('(defaults to "defaultValueFunction")'));
lib/src/config/configuration.dart (1)

287-293: Positional-arg resolution order can mis-consume arguments.

_options.sorted((a,b)=>(a.pos??-1)...) puts non-positional options before positional ones (-1 < 0).
If the list mixes both kinds, positional arguments may be consumed too late, leading to “unexpected positional argument” errors.

Sort nulls last to guarantee ascending position order:

-    final orderedOpts = _options.sorted(
-        (a, b) => (a.option.argPos ?? -1).compareTo(b.option.argPos ?? -1));
+    final orderedOpts = _options.sorted(
+        (a, b) => (a.option.argPos ?? 1<<20).compareTo(b.option.argPos ?? 1<<20));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6081606 and f405167.

📒 Files selected for processing (19)
  • example/config_file_example.dart (1 hunks)
  • lib/src/better_command_runner/better_command.dart (2 hunks)
  • lib/src/better_command_runner/better_command_runner.dart (2 hunks)
  • lib/src/config/config.dart (1 hunks)
  • lib/src/config/config_parser.dart (1 hunks)
  • lib/src/config/config_source_provider.dart (1 hunks)
  • lib/src/config/configuration.dart (6 hunks)
  • lib/src/config/configuration_broker.dart (1 hunks)
  • lib/src/config/exceptions.dart (1 hunks)
  • lib/src/config/file_system_options.dart (1 hunks)
  • lib/src/config/multi_config_source.dart (1 hunks)
  • lib/src/config/option_groups.dart (1 hunks)
  • lib/src/config/option_types.dart (1 hunks)
  • lib/src/config/options.dart (1 hunks)
  • lib/src/config/output_formatting.dart (1 hunks)
  • test/config/config_source_test.dart (7 hunks)
  • test/config/configuration_test.dart (73 hunks)
  • test/config/configuration_type_test.dart (60 hunks)
  • test/config/file_options_test.dart (18 hunks)
🔇 Additional comments (7)
lib/src/config/configuration_broker.dart (1)

4-12: Public-facing API addition looks clean

The interface is minimal yet expressive, docs are concise, and the generic bound communicates intent. No issues spotted.

lib/src/config/config_source_provider.dart (1)

4-4: Import aligns dependencies after refactor

Adding options.dart is required for OptionDefinition – good catch.
No other changes needed.

lib/src/config/file_system_options.dart (1)

5-5: Correct dependency adjustment

Switching to options.dart keeps the file compiling after the split-out. Looks fine.

lib/src/config/option_groups.dart (1)

1-3: Updated imports match the new module layout

InvalidOptionConfigurationError and OptionGroup now live in the new files, so the replacements are correct. 👍

lib/src/config/config.dart (1)

6-12: Exports look good

New modules are re-exported cleanly and no duplicate symbols are exposed.
LGTM.

test/config/configuration_type_test.dart (1)

20-25: Tests updated appropriately

Switching to resolveNoExcept() keeps the assertions intact and exercises the new non-throwing path – good coverage.

lib/src/config/options.dart (1)

520-522: Verify minimum SDK version for bool.parse API.

bool.parse(String, {bool caseSensitive = true}) was added in Dart 3.
If the package’s lower SDK bound is <3.0 this will break builds.
Confirm pubspec.yaml and bump sdk constraint if necessary or drop the named parameter.

Copy link
Contributor

@SandPod SandPod left a comment

Choose a reason for hiding this comment

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

👍

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
lib/src/config/configuration.dart (3)

69-80: Minor doc-block drift.

The macro comment still describes the behaviour but doesn’t explicitly mention that the new factory resolve now throws by default.

-/// {@template Configuration.resolveNoExcept}
+/// {@template Configuration.resolveNoExcept}
 /// Creates a configuration with option values resolved from the provided context.
@@
 /// {@endtemplate}

Consider adding a short “See also: [Configuration.resolve] for the throwing variant.”


115-140: Factory duplicates parameter list – consider DRYing.

The factory currently mirrors the full parameter list of resolveNoExcept, which is easy to let drift. One neat pattern is to forward the Map of named params:

-factory Configuration.resolve({
-  required final Iterable<O> options,
-  final ArgResults? argResults,
-  ...
-}) {
-  final config = Configuration.resolveNoExcept(
-    options: options,
-    argResults: argResults,
-    ...
-  );
+factory Configuration.resolve({
+  required Iterable<O> options,
+  ArgResults? argResults,
+  Iterable<String>? args,
+  Map<String, String>? env,
+  ConfigurationBroker? configBroker,
+  Map<O, Object?>? presetValues,
+  bool ignoreUnexpectedPositionalArgs = false,
+}) {
+  final config = Configuration.resolveNoExcept(
+    options: options,
+    argResults: argResults,
+    args: args,
+    env: env,
+    configBroker: configBroker,
+    presetValues: presetValues,
+    ignoreUnexpectedPositionalArgs: ignoreUnexpectedPositionalArgs,
+  );
   config.throwExceptionOnErrors();
   return config;
 }

Optional, but reduces maintenance surface.


142-152: Clear error list after throwing to avoid double-throw.

If a caller catches UsageException and inspects the same Configuration instance later, another call to throwExceptionOnErrors() will re-throw the same exception.

-throw UsageException(buffer.toString(), usage);
+final message = buffer.toString();
+_errors.clear();
+throw UsageException(message, usage);

Not strictly required, but prevents surprises in multi-stage workflows.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e0a301 and 7a0abbe.

📒 Files selected for processing (1)
  • lib/src/config/configuration.dart (5 hunks)
🔇 Additional comments (3)
lib/src/config/configuration.dart (3)

9-9: Import looks correct.

The additional import is necessary for formatConfigError and doesn’t introduce any side effects.


55-58: Good move to resolveNoExcept.

Routing fromValues through the non-throwing constructor matches the new error-handling model.


268-268: Exception type may not fit the scenario.

OptionDefinitionError sounds like a definition-time error, yet this branch is hit when an option’s value is requested before its resolution. That’s arguably a parse-state issue rather than a definition issue. Double-check that the new name still conveys the intended semantics.

@christerswahn christerswahn merged commit ab576b9 into main Jun 13, 2025
9 checks passed
@christerswahn christerswahn deleted the throw-usage branch June 13, 2025 08:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants