Skip to content

ArgParser: reject default attributes on non-Choice fields at generation time - #572

Merged
Smaug123 merged 1 commit into
mainfrom
reject-default-on-non-choice
Jul 18, 2026
Merged

ArgParser: reject default attributes on non-Choice fields at generation time#572
Smaug123 merged 1 commit into
mainfrom
reject-default-on-non-choice

Conversation

@Smaug123

Copy link
Copy Markdown
Owner

Fixes #571.

Problem

[<ArgumentDefaultFunction>] and [<ArgumentDefaultEnvironmentVariable>] are only ever read inside the ChoiceType branch of createParseFunction. On any non-Choice field the generator accepted them, generated code without complaint, and silently dropped the default — the field stayed required at parse time. This is a fail-fast gap: an attribute that cannot be honoured should be rejected at generation time.

Defaults are intentionally surfaced through Choice<'a, 'a> so a successful parse can distinguish a user-supplied value (Choice1Of2) from a parser-filled default (Choice2Of2). A bare field cannot express "was this defaulted?", so honouring the default is impossible — but the generator didn't say so.

Fix

A syntactic guard in toParseSpec's per-field fold, before the record/union/positional dispatch. If a field carries a default attribute:

  • positional → rejected (positional args are collected, not defaulted);
  • non-positional, type is Choice<_,_> → accepted, unchanged (an unequal Choice<int,string> still hits the pre-existing "prove types equal" error);
  • non-positional, any other type (scalar, flag DU, record, union) → rejected with a message pointing at the Choice<'a,'a> convention.

The now-unreachable positional default-rejection arm inside the positional getChoice callback is removed (the guard front-runs it).

Tests

Five new tests in TestArgParserRejection.fs, written first and observed failing before the guard:

  1. the issue's exact repro (bare flag DU + ArgumentDefaultFunction);
  2. sibling attribute on a bare scalar (ArgumentDefaultEnvironmentVariable);
  3. default on a record-typed field;
  4. default on a positional field;
  5. positive control — a Choice<int,int> field with a default still generates.

Verification

  • New rejection fixture: 38/38 pass (4 were failing before the guard).
  • ConsumePlugin builds clean — every real Choice-typed default field still generates.
  • Broader arg-parser consumer suite: 222/222 pass.

🤖 Generated with Claude Code

…on time

`[<ArgumentDefaultFunction>]` and `[<ArgumentDefaultEnvironmentVariable>]`
were only ever read inside the Choice-parsing path, so on any non-Choice
field the generator accepted them and silently dropped the default, leaving
the field required (issue #571). Defaults are intentionally surfaced through
`Choice<'a, 'a>` so a successful parse can report whether a value was
user-supplied (Choice1Of2) or defaulted (Choice2Of2); a bare field cannot
express this, so honouring the default would be impossible and dropping it
silently is a fail-fast gap.

Add a syntactic guard in `toParseSpec` that rejects a default attribute on
any field whose type is not `Choice<'a, 'a>`, and on any positional field,
with messages pointing at the Choice convention. The now-unreachable
positional default-rejection arm inside the positional `getChoice` callback
is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Smaug123
Smaug123 enabled auto-merge (squash) July 18, 2026 19:03
@Smaug123
Smaug123 merged commit 673f766 into main Jul 18, 2026
19 checks passed
@Smaug123
Smaug123 deleted the reject-default-on-non-choice branch July 18, 2026 19:04
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.

ArgParser: reject [<ArgumentDefaultFunction>] on non-Choice fields at generation time instead of silently ignoring it

1 participant