From b84bf055f09002e939f1a36a00ad78742cf8f6f4 Mon Sep 17 00:00:00 2001 From: Pavel Yaskevich Date: Mon, 24 Feb 2025 10:45:11 -0800 Subject: [PATCH] [TypeChecker] Implement `ExtensibleEnums` feature exhaustivily handling If an enum comes from a different module that has `ExtensibleEnums` feature enabled, unless it requires either `@unknown default:` or `@frozen` because it is allowed to introduce new cases in the future versions of the module. --- lib/AST/Decl.cpp | 18 +++- lib/Sema/TypeCheckSwitchStmt.cpp | 10 +- test/ModuleInterface/extensible_enums.swift | 110 ++++++++++++++++++++ 3 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 test/ModuleInterface/extensible_enums.swift diff --git a/lib/AST/Decl.cpp b/lib/AST/Decl.cpp index b00976a2961..40d78a7c9f0 100644 --- a/lib/AST/Decl.cpp +++ b/lib/AST/Decl.cpp @@ -6696,8 +6696,22 @@ bool EnumDecl::hasOnlyCasesWithoutAssociatedValues() const { } bool EnumDecl::treatAsExhaustiveForDiags(const DeclContext *useDC) const { - return isFormallyExhaustive(useDC) || - (useDC && getModuleContext()->inSamePackage(useDC->getParentModule())); + if (useDC) { + auto *enumModule = getModuleContext(); + if (enumModule->inSamePackage(useDC->getParentModule())) + return true; + + // If the module where enum is declared supports extensible enumerations + // and this enum is not explicitly marked as "@frozen", cross-module + // access cannot be exhaustive and requires `@unknown default:`. + if (enumModule->supportsExtensibleEnums() && + !getAttrs().hasAttribute()) { + if (useDC != enumModule->getDeclContext()) + return false; + } + } + + return isFormallyExhaustive(useDC); } bool EnumDecl::isFormallyExhaustive(const DeclContext *useDC) const { diff --git a/lib/Sema/TypeCheckSwitchStmt.cpp b/lib/Sema/TypeCheckSwitchStmt.cpp index 2cc8bf5a03d..5e5faf5d0b3 100644 --- a/lib/Sema/TypeCheckSwitchStmt.cpp +++ b/lib/Sema/TypeCheckSwitchStmt.cpp @@ -1154,13 +1154,19 @@ namespace { assert(defaultReason == RequiresDefault::No); Type subjectType = Switch->getSubjectExpr()->getType(); bool shouldIncludeFutureVersionComment = false; + bool shouldDowngradeToWarning = true; if (auto *theEnum = subjectType->getEnumOrBoundGenericEnum()) { + auto *enumModule = theEnum->getParentModule(); shouldIncludeFutureVersionComment = - theEnum->getParentModule()->isSystemModule(); + enumModule->isSystemModule() || + enumModule->supportsExtensibleEnums(); + // Since the module enabled `ExtensibleEnums` feature they + // opted-in all of their clients into exhaustivity errors. + shouldDowngradeToWarning = !enumModule->supportsExtensibleEnums(); } DE.diagnose(startLoc, diag::non_exhaustive_switch_unknown_only, subjectType, shouldIncludeFutureVersionComment) - .warnUntilSwiftVersion(6); + .warnUntilSwiftVersionIf(shouldDowngradeToWarning, 6); mainDiagType = std::nullopt; } break; diff --git a/test/ModuleInterface/extensible_enums.swift b/test/ModuleInterface/extensible_enums.swift new file mode 100644 index 00000000000..8f3d985a32a --- /dev/null +++ b/test/ModuleInterface/extensible_enums.swift @@ -0,0 +1,110 @@ +// RUN: %empty-directory(%t) +// RUN: %empty-directory(%t/src) +// RUN: split-file %s %t/src + +/// Build the library +// RUN: %target-swift-frontend -emit-module %t/src/Lib.swift \ +// RUN: -module-name Lib \ +// RUN: -emit-module-path %t/Lib.swiftmodule \ +// RUN: -enable-experimental-feature ExtensibleEnums + +// Check that the errors are produced when using enums from module with `ExtensibleEnums` feature enabled. +// RUN: %target-swift-frontend -typecheck %t/src/TestChecking.swift \ +// RUN: -swift-version 5 -module-name Client -I %t \ +// RUN: -verify + +// Test to make sure that if the library and client are in the same package enums are checked exhaustively + +/// Build the library +// RUN: %target-swift-frontend -emit-module %t/src/Lib.swift \ +// RUN: -module-name Lib \ +// RUN: -package-name Test \ +// RUN: -emit-module-path %t/Lib.swiftmodule \ +// RUN: -enable-experimental-feature ExtensibleEnums + +// Different module but the same package +// RUN: %target-swift-frontend -typecheck %t/src/TestSamePackage.swift \ +// RUN: -swift-version 5 -module-name Client -I %t \ +// RUN: -package-name Test \ +// RUN: -verify + +// REQUIRES: swift_feature_ExtensibleEnums + +//--- Lib.swift + +public enum E { + case a +} + +@frozen +public enum F { + case a + case b +} + +func test_same_module(e: E, f: F) { + switch e { // Ok + case .a: break + } + + switch f { // Ok + case .a: break + case .b: break + } +} + +//--- TestChecking.swift +import Lib + +func test(e: E, f: F) { + // `E` is not marked as `@frozen` which means it gets new semantics + + switch e { + // expected-error@-1 {{switch covers known cases, but 'E' may have additional unknown values, possibly added in future versions}} + // expected-note@-2 {{handle unknown values using "@unknown default"}} + case .a: break + } + + switch e { // Ok (no warnings) + case .a: break + @unknown default: break + } + + // `F` is marked as `@frozen` which means regular rules apply even with `ExtensibleEnums` feature enabled. + + switch f { // Ok (no errors because `F` is `@frozen`) + case .a: break + case .b: break + } + + switch f { // expected-error {{switch must be exhaustive}} expected-note {{dd missing case: '.b'}} + case .a: break + } + + switch f { // expected-warning {{switch must be exhaustive}} expected-note {{dd missing case: '.b'}} + case .a: break + @unknown default: break + } +} + +//--- TestSamePackage.swift +import Lib + +func test_no_default(e: E, f: F) { + switch e { // Ok + case .a: break + } + + switch e { // expected-warning {{switch must be exhaustive}} expected-note {{dd missing case: '.a'}} + @unknown default: break + } + + switch f { // expected-error {{switch must be exhaustive}} expected-note {{dd missing case: '.b'}} + case .a: break + } + + switch f { // expected-warning {{switch must be exhaustive}} expected-note {{dd missing case: '.b'}} + case .a: break + @unknown default: break + } +}