You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This PR adds diagnostics (emitted at macro expansion time) when an
effectful expression is passed to `#expect()` or `#require()`. For
example:
```swift
#expect(try foo()) // ⚠️ Expression 'try foo()' will not be expanded on failure;
// move the throwing part out of the call to '#expect(_:_:)'
```
(A fix-it here is not possible for the same reasons we need to diagnose
in the first place, explained momentarily.)
Expressions containing `try` or `await` are affected; the diagnostic can
be suppressed by adding an explicit cast to the expression's type (`as
Bool` or `as T?`.)
### Why can't we break down these expressions?
The `try` and `await` keywords in Swift are allowed to be used anywhere
in an expression or a containing expression and cover _all_
throwing/asynchronous subexpressions. For example, the following is
valid even though only `foo()` strictly needs the `await` keyword:
```swift
func foo() async -> Int { ... }
#expect(await quux(1, 2, 3, foo() + bar()) > 10)
```
Because swift-testing can only see the syntax tree (that is, the
characters the developer typed into a Swift source file) and not the
types or effects of expressions, when presented with the `#expect()`
expression above, it has no way to know that the only part of the
expression that needs to be awaited is `foo()`.
Expression expansion works by breaking down an expression into known
subexpression patterns. For example, `x.y(z: 123)` represents a member
function call and useful subexpressions include `x`, and the argument
`z: 123`:
```swift
__checkFunctionCall(
x, // the base expression
calling: { $0.y(z: $1) }, // a closure that invokes the .y(z:) member function
123 // the argument, labelled 'z', to the member function
)
```
These subexpressions can then be presented as their source code _and_
runtime values if an expectation fails, allowing developers to quickly
see that e.g. `x` was misspecified or `123` should have been `456`.
But if some subexpression is effectful, there's no way for swift-testing
to break down the whole expression into syntactically and contextually
correct subexpressions because there's no way to know where the effects
need to be reapplied. Given the similar expression `await x.y(z: 123)`,
where does `await` need to go when calling `__checkFunctionCall()`?
```swift
await __checkFunctionCall(
x, // should this be `await x`?
calling: { $0.y(z: $1) }, // `{ await $0.y(z: $1) }` perhaps?
123 // well, at least this is an integer literal...
)
```
If the `await` is placed in the wrong location, an error occurs after
macro expansion. If swift-testing is paranoid and adds `await` to
_every_ subexpression (literals aside), warnings occur. Diagnostics
occur no matter what we do unless _every_ subexpression just so happened
to be effectful.
### What about that `__requiringAwait` trick used during expansion of
`@Test`?
(See
[here](https://github.com/apple/swift-testing/blob/932cb01aa2da55fb410b8fede294efa8d5545f62/Sources/Testing/Test%2BMacro.swift#L464)
and
[here](https://github.com/apple/swift-testing/blob/932cb01aa2da55fb410b8fede294efa8d5545f62/Sources/TestingMacros/TestDeclarationMacro.swift#L306).)
This is a tempting approach, but it comes with a serious caveat: it
would introduce additional suspension points to code that might only
need a single one. The result would be code that behaves differently in
a call to `#expect()` than when invoked directly, which would be a
serious defect in swift-testing.
#### What about just doing that for `try`?
I had _almost_ gotten this working, but ran into the problem that macros
behave differently from functions in a way that would make an expansion
syntactically incorrect:
```swift
#expect(try foo()) // 🛑 Call can throw but is not marked with 'try'
```
In effect, adding the expansion here would require that the developer
always write `try #expect(try foo())` (i.e. `try` twice) which is
inconsistent with how the developer would write a function call that
takes the result of `foo()` in a non-obvious way.
### Okay, so where does that leave us?
This PR adds the diagnostics I mentioned above, remember?
I also took the time to adjust the other diagnostics we emit to more
closely match Swift/LLVM [house
style](https://github.com/apple/swift/blob/main/docs/Diagnostics.md). So
the diff is more extensive than was necessary _just_ for the new
diagnostics, but the result is a more consistent developer experience.
Resolves rdar://124976452.
### Checklist:
- [x] Code and documentation should follow the style of the [Style
Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md).
- [x] If public symbols are renamed or modified, DocC references should
be updated.
staticfunc asExclamationMarkIsEvaluatedEarly(_ expr:AsExprSyntax, in macro:someFreestandingMacroExpansionSyntax)->Self{
51
52
returnSelf(
52
53
syntax:Syntax(expr.asKeyword),
53
-
message:"The expression \(expr.trimmed) will be evaluated before #\(macro.macroName.textWithoutBackticks)(_:_:) is invoked; use as? instead of as! to silence this warning",
54
+
message:"Expression '\(expr.trimmed)' will be evaluated before \(_macroName(macro)) is invoked; use 'as?' instead of 'as!' to silence this warning",
55
+
severity:.warning
56
+
)
57
+
}
58
+
59
+
/// Create a diagnostic message stating that an effectful (`try` or `await`)
60
+
/// expression cannot be parsed and should be broken apart.
61
+
///
62
+
/// - Parameters:
63
+
/// - expr: The expression being diagnosed.
64
+
/// - macro: The macro expression.
65
+
///
66
+
/// - Returns: A diagnostic message.
67
+
staticfunc effectfulExpressionNotParsed(_ expr:ExprSyntax, in macro:someFreestandingMacroExpansionSyntax)->Self{
staticfunc availabilityAttributeNotSupported(_ availabilityAttribute:AttributeSyntax, on decl:someSyntaxProtocol, whenUsing attribute:AttributeSyntax)->Self{
156
205
Self(
157
206
syntax:Syntax(availabilityAttribute),
158
-
message:"The @\(attribute.attributeNameText) attribute cannot be applied to this \(_kindString(for: decl)) because it has been marked \(availabilityAttribute.trimmed).",
207
+
message:"Attribute \(_macroName(attribute))cannot be applied to this \(_kindString(for: decl)) because it has been marked '\(availabilityAttribute.trimmed)'",
message:"The @\(attribute.attributeNameText) attribute cannot specify arguments when used with \(functionDecl.completeName) because it does not take any.",
264
+
message:"Attribute \(_macroName(attribute))cannot specify arguments when used with '\(functionDecl.completeName)' because it does not take any",
210
265
severity:.error
211
266
)
212
267
case1:
213
268
returnSelf(
214
269
syntax:Syntax(functionDecl),
215
-
message:"The @\(attribute.attributeNameText) attribute must specify an argument when used with \(functionDecl.completeName).",
270
+
message:"Attribute \(_macroName(attribute))must specify an argument when used with '\(functionDecl.completeName)'",
216
271
severity:.error
217
272
)
218
273
default:
219
274
returnSelf(
220
275
syntax:Syntax(functionDecl),
221
-
message:"The @\(attribute.attributeNameText) attribute must specify \(expectedArgumentCount) arguments when used with \(functionDecl.completeName).",
276
+
message:"Attribute \(_macroName(attribute))must specify \(expectedArgumentCount) arguments when used with '\(functionDecl.completeName)'",
staticfunc tagExprNotSupported(_ tagExpr:someSyntaxProtocol, in attribute:AttributeSyntax)->Self{
303
358
Self(
304
359
syntax:Syntax(tagExpr),
305
-
message:"The tag \(tagExpr.trimmed) cannot be used with the @\(attribute.attributeNameText) attribute. Pass a member of Tag or a string literal instead.",
360
+
message:"Tag '\(tagExpr.trimmed)' cannot be used with attribute \(_macroName(attribute)); pass a member of 'Tag' or a string literal instead",
Copy file name to clipboardExpand all lines: Tests/TestingMacrosTests/ConditionMacroTests.swift
+43Lines changed: 43 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -331,6 +331,49 @@ struct ConditionMacroTests {
331
331
#expect(diagnostics.isEmpty)
332
332
}
333
333
334
+
@Test("#expect(try/await) produces a diagnostic",
335
+
arguments:[
336
+
"#expect(try foo())":["Expression 'try foo()' will not be expanded on failure; move the throwing part out of the call to '#expect(_:_:)'"],
337
+
"#expect(await foo())":["Expression 'await foo()' will not be expanded on failure; move the asynchronous part out of the call to '#expect(_:_:)'"],
338
+
"#expect(try await foo())":["Expression 'try await foo()' will not be expanded on failure; move the throwing/asynchronous part out of the call to '#expect(_:_:)'"],
339
+
"#expect(try await foo(try bar(await quux())))":[
340
+
"Expression 'try await foo(try bar(await quux()))' will not be expanded on failure; move the throwing/asynchronous part out of the call to '#expect(_:_:)'",
341
+
"Expression 'try bar(await quux())' will not be expanded on failure; move the throwing part out of the call to '#expect(_:_:)'",
342
+
"Expression 'await quux()' will not be expanded on failure; move the asynchronous part out of the call to '#expect(_:_:)'",
343
+
],
344
+
345
+
// Diagnoses because the diagnostic for `await` is suppressed due to the
346
+
// `as T` cast, but the parentheses limit the effect of the suppression.
347
+
"#expect(try (await foo() as T))":["Expression 'try (await foo() as T)' will not be expanded on failure; move the throwing part out of the call to '#expect(_:_:)'"],
0 commit comments