Skip to content

Commit 478c2df

Browse files
authored
Improve failure message for backdeployed AsyncParsableCommand (#547)
When an executable with asynchronous commands is backdeployed, the compiler chooses the synchronous `main()` unless a minimum availability target is provided for the root command type. This changes the error message provided when the incorrect `main()` function is called to direct the tool's author to a correct solution.
1 parent 4ad606b commit 478c2df

File tree

1 file changed

+59
-1
lines changed

1 file changed

+59
-1
lines changed

Sources/ArgumentParser/Parsable Types/ParsableCommand.swift

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,13 @@ extension ParsableCommand {
127127

128128
#if DEBUG
129129
if #available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *) {
130-
checkAsyncHierarchy(self, root: "\(self)")
130+
if let asyncCommand = firstAsyncSubcommand(self) {
131+
if Self() is AsyncParsableCommand {
132+
failAsyncPlatform(rootCommand: self)
133+
} else {
134+
failAsyncHierarchy(rootCommand: self, subCommand: asyncCommand)
135+
}
136+
}
131137
}
132138
#endif
133139

@@ -194,5 +200,57 @@ extension ParsableCommand {
194200
""".wrapped(to: 70))
195201
}
196202
}
203+
204+
@available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
205+
internal static func firstAsyncSubcommand(_ command: ParsableCommand.Type) -> AsyncParsableCommand.Type? {
206+
for sub in command.configuration.subcommands {
207+
if let asyncCommand = sub as? AsyncParsableCommand.Type,
208+
sub.configuration.subcommands.isEmpty
209+
{
210+
return asyncCommand
211+
}
212+
213+
if let asyncCommand = firstAsyncSubcommand(sub) {
214+
return asyncCommand
215+
}
216+
}
217+
218+
return nil
219+
}
197220
#endif
198221
}
222+
223+
// MARK: Async Configuration Errors
224+
225+
func failAsyncHierarchy(
226+
rootCommand: ParsableCommand.Type, subCommand: ParsableCommand.Type
227+
) -> Never {
228+
fatalError("""
229+
230+
--------------------------------------------------------------------
231+
Asynchronous subcommand of a synchronous root.
232+
233+
The asynchronous command `\(subCommand)` is declared as a subcommand of the synchronous root command `\(rootCommand)`.
234+
235+
With this configuration, your asynchronous `run()` method will not be called. To fix this issue, change `\(rootCommand)`'s `ParsableCommand` conformance to `AsyncParsableCommand`.
236+
--------------------------------------------------------------------
237+
238+
""".wrapped(to: 70))
239+
}
240+
241+
func failAsyncPlatform(rootCommand: ParsableCommand.Type) -> Never {
242+
fatalError("""
243+
244+
--------------------------------------------------------------------
245+
Asynchronous root command needs availability annotation.
246+
247+
The asynchronous root command `\(rootCommand)` needs an availability annotation in order to be executed asynchronously. To fix this issue, add the following availability attribute to your `\(rootCommand)` declaration or set the minimum platform in your "Package.swift" file.
248+
249+
""".wrapped(to: 70)
250+
+ """
251+
252+
@available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
253+
--------------------------------------------------------------------
254+
255+
""")
256+
}

0 commit comments

Comments
 (0)