Skip to content

Commit 94cf562

Browse files
committed
[Sema] Catch invalid if/switch exprs in more places
Move out-of-place SingleValueStmtExpr checking into `performSyntacticExprDiagnostics`, to ensure we catch all expressions. Previously we did the walk as a part of Decl-based MiscDiagnostics, but it turns out that can miss expressions in property initializers, subscript default arguments, and custom attrs. This does mean that we'll now no longer diagnose out-of-place if/switch exprs if the expression didn't type-check, but that's consistent with the rest of MiscDiagnostics, and I don't think it will be a major issue in practice. We probably ought to consider moving this checking into PreCheckExpr, but that would require first separating out SequenceExpr folding, which has other consequences, and so I'm leaving as future work for now.
1 parent c04281c commit 94cf562

File tree

13 files changed

+358
-70
lines changed

13 files changed

+358
-70
lines changed

lib/Sema/MiscDiagnostics.cpp

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3848,7 +3848,20 @@ class SingleValueStmtUsageChecker final : public ASTWalker {
38483848
llvm::DenseSet<SingleValueStmtExpr *> ValidSingleValueStmtExprs;
38493849

38503850
public:
3851-
SingleValueStmtUsageChecker(ASTContext &ctx) : Ctx(ctx), Diags(ctx.Diags) {}
3851+
SingleValueStmtUsageChecker(
3852+
ASTContext &ctx, ASTNode root,
3853+
llvm::Optional<ContextualTypePurpose> contextualPurpose)
3854+
: Ctx(ctx), Diags(ctx.Diags) {
3855+
assert(!root.is<Expr *>() || contextualPurpose &&
3856+
"Must provide contextual purpose for expr");
3857+
3858+
// If we have a contextual purpose, this is for an expression. Check if it's
3859+
// an expression in a valid position.
3860+
if (contextualPurpose) {
3861+
markAnyValidTopLevelSingleValueStmt(root.get<Expr *>(),
3862+
*contextualPurpose);
3863+
}
3864+
}
38523865

38533866
private:
38543867
/// Mark a given expression as a valid position for a SingleValueStmtExpr.
@@ -3860,8 +3873,23 @@ class SingleValueStmtUsageChecker final : public ASTWalker {
38603873
ValidSingleValueStmtExprs.insert(SVE);
38613874
}
38623875

3876+
/// Mark a valid top-level expression with a given contextual purpose.
3877+
void markAnyValidTopLevelSingleValueStmt(Expr *E, ContextualTypePurpose ctp) {
3878+
// Allowed in returns, throws, and bindings.
3879+
switch (ctp) {
3880+
case CTP_ReturnStmt:
3881+
case CTP_ReturnSingleExpr:
3882+
case CTP_ThrowStmt:
3883+
case CTP_Initialization:
3884+
markValidSingleValueStmt(E);
3885+
break;
3886+
default:
3887+
break;
3888+
}
3889+
}
3890+
38633891
MacroWalking getMacroWalkingBehavior() const override {
3864-
return MacroWalking::Expansion;
3892+
return MacroWalking::ArgumentsAndExpansion;
38653893
}
38663894

38673895
AssignExpr *findAssignment(Expr *E) const {
@@ -3987,28 +4015,33 @@ class SingleValueStmtUsageChecker final : public ASTWalker {
39874015
if (auto *PBD = dyn_cast<PatternBindingDecl>(D)) {
39884016
for (auto idx : range(PBD->getNumPatternEntries()))
39894017
markValidSingleValueStmt(PBD->getInit(idx));
4018+
4019+
return Action::Continue();
39904020
}
3991-
// Valid as a single expression body of a function. This is needed in
3992-
// addition to ReturnStmt checking, as we will remove the return if the
3993-
// expression is inferred to be Never.
3994-
if (auto *AFD = dyn_cast<AbstractFunctionDecl>(D)) {
3995-
if (AFD->hasSingleExpressionBody())
3996-
markValidSingleValueStmt(AFD->getSingleExpressionBody());
3997-
}
3998-
return Action::Continue();
4021+
// We don't want to walk into any other decl, we will visit them as part of
4022+
// typeCheckDecl.
4023+
return Action::SkipChildren();
39994024
}
40004025
};
40014026
} // end anonymous namespace
40024027

4028+
void swift::diagnoseOutOfPlaceExprs(
4029+
ASTContext &ctx, ASTNode root,
4030+
llvm::Optional<ContextualTypePurpose> contextualPurpose) {
4031+
// TODO: We ought to consider moving this into pre-checking such that we can
4032+
// still diagnose on invalid code, and don't have to traverse over implicit
4033+
// exprs. We need to first separate out SequenceExpr folding though.
4034+
SingleValueStmtUsageChecker sveChecker(ctx, root, contextualPurpose);
4035+
root.walk(sveChecker);
4036+
}
4037+
40034038
/// Apply the warnings managed by VarDeclUsageChecker to the top level
40044039
/// code declarations that haven't been checked yet.
40054040
void swift::
40064041
performTopLevelDeclDiagnostics(TopLevelCodeDecl *TLCD) {
40074042
auto &ctx = TLCD->getDeclContext()->getASTContext();
40084043
VarDeclUsageChecker checker(TLCD, ctx.Diags);
40094044
TLCD->walk(checker);
4010-
SingleValueStmtUsageChecker sveChecker(ctx);
4011-
TLCD->walk(sveChecker);
40124045
}
40134046

40144047
/// Perform diagnostics for func/init/deinit declarations.
@@ -4024,10 +4057,6 @@ void swift::performAbstractFuncDeclDiagnostics(AbstractFunctionDecl *AFD) {
40244057
auto &ctx = AFD->getDeclContext()->getASTContext();
40254058
VarDeclUsageChecker checker(AFD, ctx.Diags);
40264059
AFD->walk(checker);
4027-
4028-
// Do a similar walk to check for out of place SingleValueStmtExprs.
4029-
SingleValueStmtUsageChecker sveChecker(ctx);
4030-
AFD->walk(sveChecker);
40314060
}
40324061

40334062
auto *body = AFD->getBody();
@@ -5849,10 +5878,10 @@ diagnoseDictionaryLiteralDuplicateKeyEntries(const Expr *E,
58495878
//===----------------------------------------------------------------------===//
58505879

58515880
/// Emit diagnostics for syntactic restrictions on a given expression.
5852-
void swift::performSyntacticExprDiagnostics(const Expr *E,
5853-
const DeclContext *DC,
5854-
bool isExprStmt,
5855-
bool disableExprAvailabilityChecking) {
5881+
void swift::performSyntacticExprDiagnostics(
5882+
const Expr *E, const DeclContext *DC,
5883+
llvm::Optional<ContextualTypePurpose> contextualPurpose, bool isExprStmt,
5884+
bool disableExprAvailabilityChecking, bool disableOutOfPlaceExprChecking) {
58565885
auto &ctx = DC->getASTContext();
58575886
TypeChecker::diagnoseSelfAssignment(E);
58585887
diagSyntacticUseRestrictions(E, DC, isExprStmt);
@@ -5871,6 +5900,8 @@ void swift::performSyntacticExprDiagnostics(const Expr *E,
58715900
diagnoseConstantArgumentRequirement(E, DC);
58725901
diagUnqualifiedAccessToMethodNamedSelf(E, DC);
58735902
diagnoseDictionaryLiteralDuplicateKeyEntries(E, DC);
5903+
if (!disableOutOfPlaceExprChecking)
5904+
diagnoseOutOfPlaceExprs(ctx, const_cast<Expr *>(E), contextualPurpose);
58745905
}
58755906

58765907
void swift::performStmtDiagnostics(const Stmt *S, DeclContext *DC) {

lib/Sema/MiscDiagnostics.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ namespace swift {
2828
class ApplyExpr;
2929
class CallExpr;
3030
class ClosureExpr;
31+
enum ContextualTypePurpose : uint8_t;
3132
class DeclContext;
3233
class Decl;
3334
class Expr;
@@ -37,10 +38,22 @@ namespace swift {
3738
class ValueDecl;
3839
class ForEachStmt;
3940

41+
/// Diagnose any expressions that appear in an unsupported position. If visiting
42+
/// an expression directly, its \p contextualPurpose should be provided to
43+
/// evaluate its position.
44+
void diagnoseOutOfPlaceExprs(
45+
ASTContext &ctx, ASTNode root,
46+
llvm::Optional<ContextualTypePurpose> contextualPurpose);
47+
4048
/// Emit diagnostics for syntactic restrictions on a given expression.
49+
///
50+
/// Note: \p contextualPurpose must be non-nil, unless
51+
/// \p disableOutOfPlaceExprChecking is set to \c true.
4152
void performSyntacticExprDiagnostics(
4253
const Expr *E, const DeclContext *DC,
43-
bool isExprStmt, bool disableExprAvailabilityChecking = false);
54+
llvm::Optional<ContextualTypePurpose> contextualPurpose,
55+
bool isExprStmt, bool disableExprAvailabilityChecking = false,
56+
bool disableOutOfPlaceExprChecking = false);
4457

4558
/// Emit diagnostics for a given statement.
4659
void performStmtDiagnostics(const Stmt *S, DeclContext *DC);

lib/Sema/TypeCheckConstraints.cpp

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,11 @@ class FunctionSyntacticDiagnosticWalker : public ASTWalker {
307307
}
308308

309309
PreWalkResult<Expr *> walkToExprPre(Expr *expr) override {
310-
performSyntacticExprDiagnostics(expr, dcStack.back(), /*isExprStmt=*/false);
310+
// We skip out-of-place expr checking here since we've already performed it.
311+
performSyntacticExprDiagnostics(expr, dcStack.back(), /*ctp*/ llvm::None,
312+
/*isExprStmt=*/false,
313+
/*disableAvailabilityChecking*/ false,
314+
/*disableOutOfPlaceExprChecking*/ true);
311315

312316
if (auto closure = dyn_cast<ClosureExpr>(expr)) {
313317
if (closure->isSeparatelyTypeChecked()) {
@@ -354,8 +358,9 @@ void constraints::performSyntacticDiagnosticsForTarget(
354358
switch (target.kind) {
355359
case SyntacticElementTarget::Kind::expression: {
356360
// First emit diagnostics for the main expression.
357-
performSyntacticExprDiagnostics(target.getAsExpr(), dc,
358-
isExprStmt, disableExprAvailabilityChecking);
361+
performSyntacticExprDiagnostics(
362+
target.getAsExpr(), dc, target.getExprContextualTypePurpose(),
363+
isExprStmt, disableExprAvailabilityChecking);
359364
return;
360365
}
361366

@@ -364,17 +369,25 @@ void constraints::performSyntacticDiagnosticsForTarget(
364369

365370
// First emit diagnostics for the main expression.
366371
performSyntacticExprDiagnostics(stmt->getTypeCheckedSequence(), dc,
367-
isExprStmt,
372+
CTP_ForEachSequence, isExprStmt,
368373
disableExprAvailabilityChecking);
369374

370375
if (auto *whereExpr = stmt->getWhere())
371-
performSyntacticExprDiagnostics(whereExpr, dc, /*isExprStmt*/ false);
376+
performSyntacticExprDiagnostics(whereExpr, dc, CTP_Condition,
377+
/*isExprStmt*/ false);
372378
return;
373379
}
374380

375381
case SyntacticElementTarget::Kind::function: {
382+
// Check for out of place expressions. This needs to be done on the entire
383+
// function body rather than on individual expressions since we need the
384+
// context of the parent nodes.
385+
auto *body = target.getFunctionBody();
386+
diagnoseOutOfPlaceExprs(dc->getASTContext(), body,
387+
/*contextualPurpose*/ llvm::None);
388+
376389
FunctionSyntacticDiagnosticWalker walker(dc);
377-
target.getFunctionBody()->walk(walker);
390+
body->walk(walker);
378391
return;
379392
}
380393
case SyntacticElementTarget::Kind::closure:

test/Constraints/closures.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,11 +1152,9 @@ struct R_76250381<Result, Failure: Error> {
11521152
// rdar://77022842 - crash due to a missing argument to a ternary operator
11531153
func rdar77022842(argA: Bool? = nil, argB: Bool? = nil) {
11541154
if let a = argA ?? false, if let b = argB ?? {
1155-
// expected-error@-1 {{'if' may only be used as expression in return, throw, or as the source of an assignment}}
1156-
// expected-error@-2 {{initializer for conditional binding must have Optional type, not 'Bool'}}
1157-
// expected-error@-3 {{cannot convert value of type '() -> ()' to expected argument type 'Bool?'}}
1158-
// expected-error@-4 {{cannot convert value of type 'Void' to expected condition type 'Bool'}}
1159-
// expected-error@-5 {{'if' must have an unconditional 'else' to be used as expression}}
1155+
// expected-error@-1 {{initializer for conditional binding must have Optional type, not 'Bool'}}
1156+
// expected-error@-2 {{cannot convert value of type '() -> ()' to expected argument type 'Bool?'}}
1157+
// expected-error@-3 {{cannot convert value of type 'Void' to expected condition type 'Bool'}}
11601158
} // expected-error {{expected '{' after 'if' condition}}
11611159
}
11621160

test/Constraints/if_expr.swift

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,6 @@ func testReturnMismatch() {
387387
let _ = if .random() {
388388
return 1 // expected-error {{unexpected non-void return value in void function}}
389389
// expected-note@-1 {{did you mean to add a return type?}}
390-
// expected-error@-2 {{cannot 'return' in 'if' when used as expression}}
391390
} else {
392391
0
393392
}
@@ -651,9 +650,46 @@ func builderWithBinding() -> Either<String, Int> {
651650
}
652651
}
653652

653+
@Builder
654+
func builderWithInvalidBinding() -> Either<String, Int> {
655+
let str = (if .random() { "a" } else { "b" })
656+
// expected-error@-1 {{'if' may only be used as expression in return, throw, or as the source of an assignment}}
657+
if .random() {
658+
str
659+
} else {
660+
1
661+
}
662+
}
663+
664+
func takesBuilder(@Builder _ fn: () -> Either<String, Int>) {}
665+
666+
func builderClosureWithBinding() {
667+
takesBuilder {
668+
// Make sure the binding gets type-checked as an if expression, but the
669+
// other if block gets type-checked as a stmt.
670+
let str = if .random() { "a" } else { "b" }
671+
if .random() {
672+
str
673+
} else {
674+
1
675+
}
676+
}
677+
}
678+
679+
func builderClosureWithInvalidBinding() {
680+
takesBuilder {
681+
let str = (if .random() { "a" } else { "b" })
682+
// expected-error@-1 {{'if' may only be used as expression in return, throw, or as the source of an assignment}}
683+
if .random() {
684+
str
685+
} else {
686+
1
687+
}
688+
}
689+
}
690+
654691
func builderInClosure() {
655-
func build(@Builder _ fn: () -> Either<String, Int>) {}
656-
build {
692+
takesBuilder {
657693
if .random() {
658694
""
659695
} else {

test/Constraints/switch_expr.swift

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,7 @@ func builderNotPostfix() -> (Either<String, Int>, Bool) {
754754

755755
@Builder
756756
func builderWithBinding() -> Either<String, Int> {
757-
// Make sure the binding gets type-checked as an if expression, but the
757+
// Make sure the binding gets type-checked as a switch expression, but the
758758
// other if block gets type-checked as a stmt.
759759
let str = switch Bool.random() {
760760
case true: "a"
@@ -767,9 +767,49 @@ func builderWithBinding() -> Either<String, Int> {
767767
}
768768
}
769769

770+
771+
@Builder
772+
func builderWithInvalidBinding() -> Either<String, Int> {
773+
let str = (switch Bool.random() { default: "a" })
774+
// expected-error@-1 {{'switch' may only be used as expression in return, throw, or as the source of an assignment}}
775+
if .random() {
776+
str
777+
} else {
778+
1
779+
}
780+
}
781+
782+
func takesBuilder(@Builder _ fn: () -> Either<String, Int>) {}
783+
784+
func builderClosureWithBinding() {
785+
takesBuilder {
786+
// Make sure the binding gets type-checked as a switch expression, but the
787+
// other if block gets type-checked as a stmt.
788+
let str = switch Bool.random() { case true: "a" case false: "b" }
789+
switch Bool.random() {
790+
case true:
791+
str
792+
case false:
793+
1
794+
}
795+
}
796+
}
797+
798+
func builderClosureWithInvalidBinding() {
799+
takesBuilder {
800+
let str = (switch Bool.random() { case true: "a" case false: "b" })
801+
// expected-error@-1 {{'switch' may only be used as expression in return, throw, or as the source of an assignment}}
802+
switch Bool.random() {
803+
case true:
804+
str
805+
case false:
806+
1
807+
}
808+
}
809+
}
810+
770811
func builderInClosure() {
771-
func build(@Builder _ fn: () -> Either<String, Int>) {}
772-
build {
812+
takesBuilder {
773813
switch Bool.random() {
774814
case true:
775815
""

test/Macros/Inputs/syntax_macro_definitions.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1970,6 +1970,20 @@ public struct NestedMagicLiteralMacro: ExpressionMacro {
19701970
}
19711971
}
19721972

1973+
public struct InvalidIfExprMacro: MemberMacro {
1974+
public static func expansion(
1975+
of node: AttributeSyntax,
1976+
providingMembersOf decl: some DeclGroupSyntax,
1977+
in context: some MacroExpansionContext
1978+
) throws -> [DeclSyntax] {
1979+
return ["""
1980+
func bar() {
1981+
let _ = (if .random() { 0 } else { 1 })
1982+
}
1983+
"""]
1984+
}
1985+
}
1986+
19731987
public struct InitWithProjectedValueWrapperMacro: PeerMacro {
19741988
public static func expansion(
19751989
of node: AttributeSyntax,

test/Macros/if_expr.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %host-build-swift -swift-version 5 -emit-library -o %t/%target-library-name(MacroDefinition) -module-name=MacroDefinition %S/Inputs/syntax_macro_definitions.swift -g -no-toolchain-stdlib-rpath
3+
4+
// RUN: not %target-swift-frontend -typecheck %s -swift-version 5 -load-plugin-library %t/%target-library-name(MacroDefinition) -serialize-diagnostics-path %t/diags.dia
5+
6+
// RUN: c-index-test -read-diagnostics %t/diags.dia 2>&1 | %FileCheck -check-prefix DIAGS %s
7+
8+
// REQUIRES: swift_swift_parser
9+
10+
@attached(member, names: named(bar))
11+
macro TestMacro<T>(_ x: T) = #externalMacro(module: "MacroDefinition", type: "InvalidIfExprMacro")
12+
13+
// Make sure we diagnose both the use in the custom attribute and in the expansion.
14+
// DIAGS-DAG: if_expr.swift:[[@LINE+1]]:12: error: 'if' may only be used as expression in return, throw, or as the source of an assignment
15+
@TestMacro(if .random() { 0 } else { 0 })
16+
struct S {}
17+
18+
// DIAGS-DAG: @__swiftmacro_7if_expr1S9TestMacrofMm_.swift:2:12: error: 'if' may only be used as expression in return, throw, or as the source of an assignment

0 commit comments

Comments
 (0)