Skip to content

Commit e846570

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 157e488 commit e846570

File tree

13 files changed

+363
-70
lines changed

13 files changed

+363
-70
lines changed

lib/Sema/MiscDiagnostics.cpp

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

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

38553868
private:
38563869
/// Mark a given expression as a valid position for a SingleValueStmtExpr.
@@ -3862,8 +3875,23 @@ class SingleValueStmtUsageChecker final : public ASTWalker {
38623875
ValidSingleValueStmtExprs.insert(SVE);
38633876
}
38643877

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

38693897
AssignExpr *findAssignment(Expr *E) const {
@@ -3989,28 +4017,33 @@ class SingleValueStmtUsageChecker final : public ASTWalker {
39894017
if (auto *PBD = dyn_cast<PatternBindingDecl>(D)) {
39904018
for (auto idx : range(PBD->getNumPatternEntries()))
39914019
markValidSingleValueStmt(PBD->getInit(idx));
4020+
4021+
return Action::Continue();
39924022
}
3993-
// Valid as a single expression body of a function. This is needed in
3994-
// addition to ReturnStmt checking, as we will remove the return if the
3995-
// expression is inferred to be Never.
3996-
if (auto *AFD = dyn_cast<AbstractFunctionDecl>(D)) {
3997-
if (AFD->hasSingleExpressionBody())
3998-
markValidSingleValueStmt(AFD->getSingleExpressionBody());
3999-
}
4000-
return Action::Continue();
4023+
// We don't want to walk into any other decl, we will visit them as part of
4024+
// typeCheckDecl.
4025+
return Action::SkipChildren();
40014026
}
40024027
};
40034028
} // end anonymous namespace
40044029

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

40164049
/// Perform diagnostics for func/init/deinit declarations.
@@ -4026,10 +4059,6 @@ void swift::performAbstractFuncDeclDiagnostics(AbstractFunctionDecl *AFD) {
40264059
auto &ctx = AFD->getDeclContext()->getASTContext();
40274060
VarDeclUsageChecker checker(AFD, ctx.Diags);
40284061
AFD->walk(checker);
4029-
4030-
// Do a similar walk to check for out of place SingleValueStmtExprs.
4031-
SingleValueStmtUsageChecker sveChecker(ctx);
4032-
AFD->walk(sveChecker);
40334062
}
40344063

40354064
auto *body = AFD->getBody();
@@ -5864,10 +5893,10 @@ diagnoseDictionaryLiteralDuplicateKeyEntries(const Expr *E,
58645893
//===----------------------------------------------------------------------===//
58655894

58665895
/// Emit diagnostics for syntactic restrictions on a given expression.
5867-
void swift::performSyntacticExprDiagnostics(const Expr *E,
5868-
const DeclContext *DC,
5869-
bool isExprStmt,
5870-
bool disableExprAvailabilityChecking) {
5896+
void swift::performSyntacticExprDiagnostics(
5897+
const Expr *E, const DeclContext *DC,
5898+
llvm::Optional<ContextualTypePurpose> contextualPurpose, bool isExprStmt,
5899+
bool disableExprAvailabilityChecking, bool disableOutOfPlaceExprChecking) {
58715900
auto &ctx = DC->getASTContext();
58725901
TypeChecker::diagnoseSelfAssignment(E);
58735902
diagSyntacticUseRestrictions(E, DC, isExprStmt);
@@ -5886,6 +5915,8 @@ void swift::performSyntacticExprDiagnostics(const Expr *E,
58865915
diagnoseConstantArgumentRequirement(E, DC);
58875916
diagUnqualifiedAccessToMethodNamedSelf(E, DC);
58885917
diagnoseDictionaryLiteralDuplicateKeyEntries(E, DC);
5918+
if (!disableOutOfPlaceExprChecking)
5919+
diagnoseOutOfPlaceExprs(ctx, const_cast<Expr *>(E), contextualPurpose);
58895920
}
58905921

58915922
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
@@ -299,7 +299,11 @@ class FunctionSyntacticDiagnosticWalker : public ASTWalker {
299299
}
300300

301301
PreWalkResult<Expr *> walkToExprPre(Expr *expr) override {
302-
performSyntacticExprDiagnostics(expr, dcStack.back(), /*isExprStmt=*/false);
302+
// We skip out-of-place expr checking here since we've already performed it.
303+
performSyntacticExprDiagnostics(expr, dcStack.back(), /*ctp*/ llvm::None,
304+
/*isExprStmt=*/false,
305+
/*disableAvailabilityChecking*/ false,
306+
/*disableOutOfPlaceExprChecking*/ true);
303307

304308
if (auto closure = dyn_cast<ClosureExpr>(expr)) {
305309
if (closure->isSeparatelyTypeChecked()) {
@@ -346,8 +350,9 @@ void constraints::performSyntacticDiagnosticsForTarget(
346350
switch (target.kind) {
347351
case SyntacticElementTarget::Kind::expression: {
348352
// First emit diagnostics for the main expression.
349-
performSyntacticExprDiagnostics(target.getAsExpr(), dc,
350-
isExprStmt, disableExprAvailabilityChecking);
353+
performSyntacticExprDiagnostics(
354+
target.getAsExpr(), dc, target.getExprContextualTypePurpose(),
355+
isExprStmt, disableExprAvailabilityChecking);
351356
return;
352357
}
353358

@@ -356,17 +361,25 @@ void constraints::performSyntacticDiagnosticsForTarget(
356361

357362
// First emit diagnostics for the main expression.
358363
performSyntacticExprDiagnostics(stmt->getTypeCheckedSequence(), dc,
359-
isExprStmt,
364+
CTP_ForEachSequence, isExprStmt,
360365
disableExprAvailabilityChecking);
361366

362367
if (auto *whereExpr = stmt->getWhere())
363-
performSyntacticExprDiagnostics(whereExpr, dc, /*isExprStmt*/ false);
368+
performSyntacticExprDiagnostics(whereExpr, dc, CTP_Condition,
369+
/*isExprStmt*/ false);
364370
return;
365371
}
366372

367373
case SyntacticElementTarget::Kind::function: {
374+
// Check for out of place expressions. This needs to be done on the entire
375+
// function body rather than on individual expressions since we need the
376+
// context of the parent nodes.
377+
auto *body = target.getFunctionBody();
378+
diagnoseOutOfPlaceExprs(dc->getASTContext(), body,
379+
/*contextualPurpose*/ llvm::None);
380+
368381
FunctionSyntacticDiagnosticWalker walker(dc);
369-
target.getFunctionBody()->walk(walker);
382+
body->walk(walker);
370383
return;
371384
}
372385
case SyntacticElementTarget::Kind::closure:

test/Constraints/closures.swift

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

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
@@ -1998,6 +1998,20 @@ public struct NestedMagicLiteralMacro: ExpressionMacro {
19981998
}
19991999
}
20002000

2001+
public struct InvalidIfExprMacro: MemberMacro {
2002+
public static func expansion(
2003+
of node: AttributeSyntax,
2004+
providingMembersOf decl: some DeclGroupSyntax,
2005+
in context: some MacroExpansionContext
2006+
) throws -> [DeclSyntax] {
2007+
return ["""
2008+
func bar() {
2009+
let _ = (if .random() { 0 } else { 1 })
2010+
}
2011+
"""]
2012+
}
2013+
}
2014+
20012015
public struct InitWithProjectedValueWrapperMacro: PeerMacro {
20022016
public static func expansion(
20032017
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)