Skip to content

[args] Add IntDescriptor, EnumDescriptor, and CliEnum (2/3) - #1016

Draft
kevmoo wants to merge 2 commits into
args-typedfrom
args-typed-2-int-enum
Draft

kevmoo wants to merge 2 commits into
args-typedfrom
args-typed-2-int-enum

Conversation

@kevmoo

@kevmoo kevmoo commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Adds integer and enum option descriptors with parse-time integer validation and CliEnum allowed-value metadata, stacked on #1012 and withholding the public package:args/typed.dart export until #1017.

…m (2/3)

- Add optional parse-time value validator hook on `Option` (`validatorOf` /
  `validateOptionValue` in `lib/src/option.dart` and `Parser._validateAllowed`
  in `lib/src/parser.dart`).
- Add `IntOptionDescriptor` and `DefaultedIntOptionDescriptor` with
  parse-time integer validation and read-time `FormatException` fallback.
- Add `CliEnum`, `EnumOptionDescriptor`, and `DefaultedEnumOptionDescriptor`
  with automatic `allowed` and `allowedHelp` derivation.
Comment thread pkgs/args/lib/src/typed/cli_enum.dart Outdated
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// An interface for enums that provide custom CLI names and help text.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How? Why? What does this mean?

Are the enum values related? Are they the valid values of a single property (that would make sense),
or are they independent unrelated properties for some command line?

(It's the former. Not clear from this doc.)

}

/// A private base class providing shared integer parsing and registration
/// infrastructure for [IntOptionDescriptor] and [DefaultedIntOptionDescriptor].

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Document classes by documenting an instance.

Don't start with "A private base class". That's documenting the declaration, not its meaning, and it says all that right below: base class _... (aka private base class).

}
}

/// A descriptor for single-value integer options.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

See below. I'm guessing:

/// Descriptor for a single-value integer option.

}
}

/// A descriptor for single-value integer options that have a non-null default

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Singular descriptor for (single-value) plural options that have a singular defaul value.
I'm confused!

Is an instance of this class a descriptor for multiple options?
Document classes by documenting a single instance.
I'm guessing:

/// Descriptor for a single-value integer option with a non-`null` default value.

Does this mean that IntOptionDescriptor has a null default value?
Or is it that it has a nullable/optional default value, so you can use it whether you know that there is a the default value or not.

Would it make sense to:
a. Remove super.defaultsTo from IntOptionDescriptor?
b. Combine these two into class IntOptionDescriptor<Default extends int?>, required Default super.defaultsTo constructor parameter, and Default getValue(...)? It makes the defaultsTo required even when it's nullable, because we can't abstract over optionality. I think it may be worth it.

}
}

/// A private base class providing shared enum parsing and registration

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Document instance.

OptionType get _optionType => OptionType.single;

@override
Object? get _rawDefaultsTo =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Doesn't feel very "raw" when it processes the actual defaultsTo value.
Maybe _stringDefaultsTo?

defaultsTo != null ? _formatName(defaultsTo! as T) : null;

@override
List<String> get allowed => values.map(_formatName).toList();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You know I love literals:

  => [for (final v in values) _formatName(v)];

Otherwise, maybe do .toList(growable: false) to save a little memory.

Also, is this called more than once? Then it may be worth caching the result.
(Or not, if it's short.)

}

final defaultString =
defaultsTo != null ? _formatName(defaultsTo! as T) : null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Promote!

return valueParser!(raw);
}
for (final val in values) {
if (_formatName(val) == raw || val.name == raw) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This treats val.name as a valid representation of the val even if it has a cliName.
If you have

enum WhatnotX(final String cliName, final String helpText) implements CliEnum {
  foo('xfoo', 'the xfoo'),
  bar('xbar', 'the xbar'),
}

then _parse('foo') will return the WhatnotX.foo value, even if it's not the correct name.

Maybe do:

  if (raw == _firmatName(val) ?? val.name) { ... }

Then you only compare to one name, and only use val.name as default if _formatName gives you nothing.

(But then, why does _formatName not return val.name at the end? That would ensure consistency.)

Comment thread pkgs/args/test/typed_test.dart Outdated
final defaultPri = priorityOpt.getValue(defaultResults);
expect(defaultPri, equals(TestPriority.p1));

final customResults = parser.parse(['-p', 'P0']);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Check that parser.parse(['-p', 'p0']) does not parse to TestPriority.p0!

- Shorten integer and enum descriptor names (`IntDescriptor`, `DefaultedIntDescriptor`, `EnumDescriptor`, `DefaultedEnumDescriptor`) and omit `defaultsTo` from nullable constructors.
- Enforce mapped CLI names in `_EnumDescriptorBase._parse`, include raw input as `source` in `FormatException`, privatize `_nameMapper`, and drop `valueParser` and `CliEnum.allowedHelp`.
- Apply instance-centric Dartdoc phrasing and Dart 3 pattern promotion across `cli_enum.dart` and `option_descriptor.dart`.
@kevmoo kevmoo changed the title [args] Add IntOptionDescriptor, EnumOptionDescriptor, and CliEnum (2/3) [args] Add IntDescriptor, EnumDescriptor, and CliEnum (2/3) Nov 4, 2026
@kevmoo

kevmoo commented Nov 4, 2026

Copy link
Copy Markdown
Member Author

Updated #1016 to address all inline review threads:

  • Fixed _EnumDescriptorBase._parse to match only _formatName(val) == raw (rejecting raw val.name when overridden) and passed raw as source to FormatException.
  • Removed EnumDescriptor.valueParser, CliEnum.allowedHelp, OptionScope, and SafeArgResults; made _nameMapper private; renamed _rawDefaultsTo to _parserDefaultsTo; and decoupled typed.dart from command_runner.dart.
  • Kept paired nullable/Defaulted*Descriptor const classes (indexing subclasses on OptionDescriptor), removed defaultsTo from nullable constructors, typed final T defaultsTo on OptionDescriptor<T>, and updated Dartdoc and variance-safe CliEnum checks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants