Skip to content

Commit d35262c

Browse files
authored
Fix error messages to distinguish arguments from flags (#858)
1 parent ca998f9 commit d35262c

File tree

4 files changed

+69
-23
lines changed

4 files changed

+69
-23
lines changed

packages/effect/src/unstable/cli/CliError.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ export class MissingArgument extends Schema.ErrorClass(`${TypeId}/MissingArgumen
272272
}
273273

274274
/**
275-
* Error thrown when an option value is invalid.
275+
* Error thrown when an option or argument value is invalid.
276276
*
277277
* @example
278278
* ```ts
@@ -282,21 +282,23 @@ export class MissingArgument extends Schema.ErrorClass(`${TypeId}/MissingArgumen
282282
* const invalidValueError = new CliError.InvalidValue({
283283
* option: "port",
284284
* value: "abc123",
285-
* expected: "integer between 1 and 65535"
285+
* expected: "integer between 1 and 65535",
286+
* kind: "flag"
286287
* })
287288
*
288289
* console.log(invalidValueError.message)
289290
* // "Invalid value for flag --port: "abc123". Expected: integer between 1 and 65535"
290291
*
291-
* // In value validation
292-
* const validatePortValue = (value: string) =>
293-
* Effect.gen(function*() {
294-
* const port = Number(value)
295-
* if (isNaN(port) || port < 1 || port > 65535) {
296-
* return yield* Effect.fail(invalidValueError)
297-
* }
298-
* return port
299-
* })
292+
* // For positional arguments
293+
* const invalidArgError = new CliError.InvalidValue({
294+
* option: "count",
295+
* value: "abc",
296+
* expected: "integer",
297+
* kind: "argument"
298+
* })
299+
*
300+
* console.log(invalidArgError.message)
301+
* // "Invalid value for argument <count>: "abc". Expected: integer"
300302
* ```
301303
*
302304
* @since 4.0.0
@@ -306,7 +308,8 @@ export class InvalidValue extends Schema.ErrorClass(`${TypeId}/InvalidValue`)({
306308
_tag: Schema.tag("InvalidValue"),
307309
option: Schema.String,
308310
value: Schema.String,
309-
expected: Schema.String
311+
expected: Schema.String,
312+
kind: Schema.Union([Schema.Literal("flag"), Schema.Literal("argument")])
310313
}) {
311314
/**
312315
* @since 4.0.0
@@ -317,6 +320,9 @@ export class InvalidValue extends Schema.ErrorClass(`${TypeId}/InvalidValue`)({
317320
* @since 4.0.0
318321
*/
319322
override get message() {
323+
if (this.kind === "argument") {
324+
return `Invalid value for argument <${this.option}>: "${this.value}". Expected: ${this.expected}`
325+
}
320326
return `Invalid value for flag --${this.option}: "${this.value}". Expected: ${this.expected}`
321327
}
322328
}

packages/effect/src/unstable/cli/CliOutput.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,8 @@ export const layer = (formatter: Formatter): Layer.Layer<never> => Layer.succeed
276276
* const error = new CliError.InvalidValue({
277277
* option: "foo",
278278
* value: "bar",
279-
* expected: "baz"
279+
* expected: "baz",
280+
* kind: "flag"
280281
* })
281282
* const errorText = formatter.formatError(error)
282283
* console.log(errorText)

packages/effect/src/unstable/cli/Param.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -952,7 +952,8 @@ export const map: {
952952
* new CliError.InvalidValue({
953953
* option: "email",
954954
* value: email,
955-
* expected: "valid email format"
955+
* expected: "valid email format",
956+
* kind: "flag"
956957
* })
957958
* )
958959
* )
@@ -1037,7 +1038,8 @@ export const mapTryCatch: {
10371038
new CliError.InvalidValue({
10381039
option: single.name,
10391040
value: String(a),
1040-
expected: error
1041+
expected: error,
1042+
kind: single.kind
10411043
})
10421044
),
10431045
Effect.map((b) => [leftover, b] as const)
@@ -1371,7 +1373,8 @@ export const filterMap: {
13711373
return yield* new CliError.InvalidValue({
13721374
option: single.name,
13731375
value: String(a),
1374-
expected: onNone(a)
1376+
expected: onNone(a),
1377+
kind: single.kind
13751378
})
13761379
})
13771380
))
@@ -1486,7 +1489,8 @@ export const withSchema: {
14861489
return new CliError.InvalidValue({
14871490
option: single.name,
14881491
value: String(value),
1489-
expected: `Schema validation failed: ${error.message}`
1492+
expected: `Schema validation failed: ${error.message}`,
1493+
kind: single.kind
14901494
})
14911495
}))
14921496
})
@@ -1597,7 +1601,8 @@ const parsePositional: <A>(
15971601
new CliError.InvalidValue({
15981602
option: name,
15991603
value: arg,
1600-
expected: error
1604+
expected: error,
1605+
kind: "argument"
16011606
})
16021607
)
16031608

@@ -1633,7 +1638,8 @@ const parseFlag: <A>(
16331638
new CliError.InvalidValue({
16341639
option: name,
16351640
value: arg,
1636-
expected: error
1641+
expected: error,
1642+
kind: "flag"
16371643
})
16381644
)
16391645

@@ -1675,7 +1681,8 @@ const parsePositionalVariadic: <Kind extends ParamKind, A>(
16751681
return yield* new CliError.InvalidValue({
16761682
option: single.name,
16771683
value: `${count} values`,
1678-
expected: `at least ${minValue} value${minValue === 1 ? "" : "s"}`
1684+
expected: `at least ${minValue} value${minValue === 1 ? "" : "s"}`,
1685+
kind: single.kind
16791686
})
16801687
}
16811688

@@ -1709,15 +1716,17 @@ const parseOptionVariadic: <Kind extends ParamKind, A>(
17091716
: new CliError.InvalidValue({
17101717
option: single.name,
17111718
value: `${count} occurrences`,
1712-
expected: `at least ${options.min} value${options.min === 1 ? "" : "s"}`
1719+
expected: `at least ${options.min} value${options.min === 1 ? "" : "s"}`,
1720+
kind: single.kind
17131721
})
17141722
}
17151723

17161724
if (Predicate.isNotUndefined(options?.max) && count > options.max) {
17171725
return yield* new CliError.InvalidValue({
17181726
option: single.name,
17191727
value: `${count} occurrences`,
1720-
expected: `at most ${options.max} value${options.max === 1 ? "" : "s"}`
1728+
expected: `at most ${options.max} value${options.max === 1 ? "" : "s"}`,
1729+
kind: single.kind
17211730
})
17221731
}
17231732

packages/effect/test/unstable/cli/Arguments.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ describe("Command arguments", () => {
148148
expect(errorText).toMatchInlineSnapshot(`
149149
"
150150
ERROR
151-
Invalid value for flag --count: "not-a-number". Expected: Failed to parse integer: Expected an integer, got NaN"
151+
Invalid value for argument <count>: "not-a-number". Expected: Failed to parse integer: Expected an integer, got NaN"
152152
`)
153153
}).pipe(Effect.provide(TestLayer)))
154154

@@ -355,4 +355,34 @@ describe("Command arguments", () => {
355355
const [_leftover, value] = result
356356
assert.isTrue(Option.isNone(value), "Should be Option.none()")
357357
}).pipe(Effect.provide(TestLayer)))
358+
359+
it.effect("should show correct error message for invalid argument (not 'flag')", () =>
360+
Effect.gen(function*() {
361+
// When a positional argument has an invalid value, the error should say "argument"
362+
// not "flag" (which would be confusing)
363+
const intArg = Argument.integer("count")
364+
365+
const result = yield* Effect.exit(
366+
intArg.parse({ flags: {}, arguments: ["not-a-number"] })
367+
)
368+
369+
assert.isTrue(result._tag === "Failure", "Should fail with invalid integer")
370+
if (result._tag === "Failure") {
371+
// Extract the error from the cause
372+
const cause = result.cause
373+
// The cause structure is { _id: 'Cause', failures: [...] } but we need to access it properly
374+
const errorMessage = String(cause)
375+
376+
// Error message should NOT say "flag"
377+
assert.isFalse(
378+
errorMessage.includes("flag --"),
379+
`Error message should not say 'flag --' for positional arguments: ${errorMessage}`
380+
)
381+
// Error message SHOULD say "argument"
382+
assert.isTrue(
383+
errorMessage.includes("argument"),
384+
`Error message should say 'argument': ${errorMessage}`
385+
)
386+
}
387+
}).pipe(Effect.provide(TestLayer)))
358388
})

0 commit comments

Comments
 (0)