Validate command models eagerly - #3860
Conversation
|
@claude review |
There was a problem hiding this comment.
Code review
Reviewed the eager command-model validation change (CommandOptionsGenerator.cs, CommandModelProvider.cs, CommandArgumentBuilder.cs, PropertyCommandLinePart.cs, CliAttributeTests.cs).
Design read: This is a solid architectural improvement. Moving the "grouped option must use space separator" / "value-pair option must use space separator" / "optional-value option must be CliOptionValue" checks out of the renderer (CommandArgumentBuilder, where they only fired if a value happened to be set) and into CommandModelProvider.ValidateModel/ValidateProperty (run once per type via the ConditionalWeakTable cache) is a good fail-fast pattern — it catches misconfigured option classes even when the offending property is never populated at runtime, and it consolidates what used to be three separate scattered throw sites into one declarative switch. The nullable IsSupportedPropertyType tri-state (true = validated supported, false = validated unsupported, null = unknown/not validated) is a nice touch: it lets hand-built PropertyCommandLineParts (used in a few existing unit tests and the OtherGeneratorsCanContributeAotSafeRuntimeMetadata third-party-metadata test) bypass the new eager check instead of breaking them, while still failing closed for anything that actually went through CommandModelProvider/the source generator.
I traced the IsSupportedFlagType / IsSupportedOptionalValueType / IsLegacyGeneratedOption logic through both the Roslyn-symbol version (compile-time, CommandOptionsGenerator.cs) and the System.Type version (runtime fallback, CommandModelProvider.cs), and spot-checked the shipped generated option classes that rely on the legacy string/IEnumerable<string> optional-value allowance (Helm/Podman/Pulumi/Jq *.Generated.cs, all tagged [GeneratedCode("ModularPipelines.OptionsGenerator", ...)]) — they all pass the new validation. No correctness issues found.
One maintainability suggestion (non-blocking): the shape rules in CommandOptionsGenerator.IsSupportedOptionalValueType/IsLegacyGeneratedOption and CommandModelProvider.IsSupportedOptionalValueType/IsLegacyGeneratedOption are two independent implementations of the same business rule (one over ITypeSymbol, one over Type), and this PR itself had to update both in lockstep. Test coverage for the new "optional-value type" and "legacy generated option" rules currently only exercises the generated (compile-time) path (Parser_Rejects_Handwritten_Legacy_Optional_String_Value, Parser_Rejects_Unrelated_Generated_Legacy_Optional_String_Value) — unlike the flag-type rule, which has both a generated-path test and a reflection-fallback test (Reflection_CommandModel_Rejects_Unsupported_Flag_Type via ReflectionInvalidFlagOptions<T>, forcing the reflection fallback with a generic type). Adding an equivalent reflection-path test for the optional-value/legacy rules would give the same drift protection the flag-type rule already has, so a future edit to one implementation without the other gets caught by CI instead of by manual review.
Nothing else stood out — the removed inline separator/format checks in CommandArgumentBuilder are fully subsumed by the new eager validation, and the updated tests correctly assert the new "no value needs to be set to trigger the error" behavior described in the PR's breaking-change note.
|
CI diagnosis: Ubuntu/Windows/macOS fail on the same current-main compile break in |
Validate static CLI property shapes while caching command metadata so invalid models fail before rendering values.\n\nBREAKING CHANGE: Invalid CLI attribute and property type combinations now fail when command metadata is first requested.
|
The shared RunReportTests regression is fixed on current main by merged PR #3844. Refreshed this branch onto current main for clean CI and review. |
d4fdb6c to
b3309e6
Compare
|
@codex review |
|
@claude review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b3309e6d28
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var positions = new Dictionary<(CommandLinePhase Phase, int Position), string>(); | ||
| foreach (var argument in parts.OfType<ArgumentPart>()) | ||
| { | ||
| var key = (argument.Phase, argument.Attribute.Position); |
There was a problem hiding this comment.
Preserve existing multi-operand positional options
When a command model has more than one [CliArgument] in the same phase without an explicit position, every attribute defaults to position 0, so this new key makes GetCommandModel() throw before any values are rendered. The repo already has shipped options in that shape, e.g. AptGetInstallOptions.CommandName/Package at src/ModularPipelines/Options/Linux/AptGet/AptGetInstallOptions.cs:28-32 and NpmTeamAddOptions.Scope/User/Otpcode at src/ModularPipelines.Node/Models/NpmTeamAddOptions.cs:9-11; those commands used to render multiple positional operands in declaration order but now fail even when the caller supplies valid arguments. Either assign distinct positions in the existing models/generator or allow the legacy duplicate-position pattern to continue rendering.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 7835bb9. Parameterless [CliArgument] metadata now records its position as implicit, so shipped multi-operand models retain stable declaration-order rendering; explicitly duplicated positions still fail. Added direct AptGetInstallOptions coverage plus legacy/default and explicit-duplicate regressions. CLI tests 66/66; core Release build 0 warnings/errors.
Code review (re-review after rebase onto
|
|
Refreshed onto current main after the Ubuntu failure was isolated to the unrelated Node test host (exit 2; core suite completed). Validation on 9514ed4: CliAttributeTests 63/63 and ModularPipelines.slnx Release build 0 warnings/errors. A fresh detached worktree was used because the existing local branch diverged from the force-updated remote; that existing worktree was preserved untouched. |
Code reviewRe-checked this PR: the head is now the merge commit I independently traced through the eager-validation path again (
No correctness or security issues found. Design read: moving the grouped-option/value-pair separator checks and the optional-value type check out of the renderer and into Standing suggestion (non-blocking, unaddressed since the last review): Since the source generator can't reference the runtime assembly (and vice versa), true code sharing isn't possible today, but the duplication surface can still be shrunk: express the rule as data instead of two parallel algorithms — e.g. a shared, linked source file ( Short of that refactor, the test gap is easy to close now: the flag-type rule has both a generated-path test and a reflection-fallback test ( CI is still running on the current head ( |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9514ed4ff7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var positions = new Dictionary<(CommandLinePhase Phase, int Position), string>(); | ||
| foreach (var argument in parts.OfType<ArgumentPart>()) | ||
| { | ||
| var key = (argument.Phase, argument.Attribute.Position); |
There was a problem hiding this comment.
Include option scope in positional validation
Separate from the existing same-phase multi-operand case, this also rejects valid global/command combinations: CommandLineBuilder.Build partitions IsGlobalOption parts and command-specific parts and renders them on opposite sides of the subcommand, so their positional indexes are independent. With this key, a [CliGlobalOptions] base that has a position-0 operand and a derived subcommand with its own position-0 operand now fails before rendering even though those arguments would not compete in the same argument list; include the global/command scope in the key or validate after partitioning.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 7835bb9. Duplicate-position validation is now keyed by (IsGlobalOption, Phase, Position), matching the renderer partition, with a regression proving identical explicit positions in global and command scopes are accepted. CLI tests 66/66; core Release build 0 warnings/errors.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7835bb9278
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var positions = new Dictionary<(bool IsGlobalOption, CommandLinePhase Phase, int Position), string>(); | ||
| foreach (var argument in parts.OfType<ArgumentPart>().Where(static argument => argument.HasExplicitPosition)) | ||
| { | ||
| var key = (argument.IsGlobalOption, argument.Phase, argument.Attribute.Position); |
There was a problem hiding this comment.
Do not split terminal positions by global scope
When a [CliGlobalOptions] base and the derived command both declare an explicit [CliArgument(0, Phase = CommandLinePhase.Terminal)], this key treats them as independent because IsGlobalOption differs, so GetCommandModel() accepts the duplicate. I checked CommandLineBuilder.Build: terminal parts are pulled out before the global/command partition and terminal arguments are rendered together from terminalCommandModel, so these operands compete in the same positional list and can be emitted in declaration order instead of being rejected like other explicit duplicates. Scope the key by global/command only for non-terminal phases, or validate after the same partitioning the renderer uses.
Useful? React with 👍 / 👎.
Code reviewRe-reviewed after the latest push ( What this commit does: adds
I also double-checked that explicit-position duplicates are still caught: Verified locally:
No bugs or CLAUDE.md violations found in this push. Still open (non-blocking, carried over from the prior review): |
Summary
CliValuePairproperty shapes when command metadata is cachedBreaking change
Invalid CLI attribute/property combinations now fail when command metadata is first requested, even when the corresponding option value is null or unset.
Validation
CliAttributeTests: 63 passedModularPipelines.slnxRelease build: 0 warnings, 0 errorsgit diff --check: passedThe standalone source-generator test project exceeded the required local 2 GB agent guard before execution. The normal unit-test project also has a pre-existing stale
RunReportTestsinitializer onmain; it was temporarily adapted only for local execution and fully reverted before this PR.Closes #3779