AST: Type-check @available attributes before synthesizing CaseIterable.

An `AvailableAttr` written in source with an unrecognized availability domain
is now only marked invalid after type-checking the attribute. This resulted in a
regression where `CaseIterable` synthesis was blocked incorrectly under the
following very narrow circumstances:

1. Every `@available` attribute on the elements of the enum is invalid.
2. The module is being emitted and lazy type-checking is enabled.
3. The enum is public and the only top-level declaration in the file.

Type-checking the attribute was delayed just enough that it would not be
considered invalid by the type the `CaseIterable` conformance was being
synthesized, resulting in a spurious error.

There were zero tests exercising `CaseIterable` synthesis for enums with
elements that have availability requirements, so I added some.

Resolves rdar://144897917.
This commit is contained in:
Allan Shortlidge
2025-02-16 11:42:01 -08:00
parent 5eca0fd132
commit b6ee0796c8
7 changed files with 137 additions and 32 deletions

View File

@@ -6665,11 +6665,9 @@ bool EnumDecl::hasOnlyCasesWithoutAssociatedValues() const {
bool hasAssociatedValues = false;
for (auto elt : getAllElements()) {
for (auto Attr : elt->getAttrs()) {
if (auto AvAttr = dyn_cast<AvailableAttr>(Attr)) {
if (!AvAttr->isInvalid())
hasAnyUnavailableValues = true;
}
for (auto Attr : elt->getSemanticAvailableAttrs()) {
// FIXME: [availability] Deprecation doesn't make an element unavailable
hasAnyUnavailableValues = true;
}
if (!elt->isAvailableDuringLowering())

View File

@@ -0,0 +1,9 @@
// RUN: %target-swift-frontend -emit-silgen %s -parse-as-library -enable-library-evolution -module-name Test -experimental-lazy-typecheck | %FileCheck %s
public enum E: CaseIterable {
case a
@available(deprecated, renamed: "a") // Intentionally invalid
case b
}
// CHECK: sil_witness_table E: CaseIterable module Test

View File

@@ -1,7 +1,6 @@
// Note that for the test to be effective, each of these enums must only have
// its Equatable or Hashable conformance referenced /once/ in the primary file.
enum FromOtherFile : String {
// expected-note@-1 {{type declared here}}
case A = "a"
}
enum AlsoFromOtherFile : Int {
@@ -29,7 +28,3 @@ extension ImpliedMain: ImplierMain {}
enum ImpliedOther: ImplierOther {
case a(Int)
}
enum CaseIterableAcrossFiles {
case A
}

View File

@@ -2,7 +2,7 @@
var hasher = Hasher()
enum Foo: CaseIterable {
enum Foo {
case A, B
}
@@ -10,12 +10,10 @@ func foo() {
if Foo.A == .B { }
var _: Int = Foo.A.hashValue
Foo.A.hash(into: &hasher)
_ = Foo.allCases
Foo.A == Foo.B // expected-warning {{result of operator '==' is unused}}
}
enum Generic<T>: CaseIterable {
enum Generic<T> {
case A, B
static func method() -> Int {
@@ -29,7 +27,6 @@ func generic() {
if Generic<Foo>.A == .B { }
var _: Int = Generic<Foo>.A.hashValue
Generic<Foo>.A.hash(into: &hasher)
_ = Generic<Foo>.allCases
}
func localEnum() -> Bool {
@@ -98,7 +95,7 @@ func useEnumBeforeDeclaration() {
if .A == overloadFromOtherFile() {}
}
// Complex enums are not automatically Equatable, Hashable, or CaseIterable.
// Complex enums are not automatically Equatable, or Hashable.
enum Complex {
case A(Int)
case B
@@ -204,13 +201,6 @@ enum Instrument {
extension Instrument : Equatable {}
extension Instrument : CaseIterable {}
enum UnusedGeneric<T> {
case a, b, c
}
extension UnusedGeneric : CaseIterable {}
// Explicit conformance should work too
public enum Medicine {
case Antibiotic
@@ -229,13 +219,6 @@ enum Complex2 {
case B
}
extension Complex2 : Hashable {}
extension Complex2 : CaseIterable {} // expected-error {{type 'Complex2' does not conform to protocol 'CaseIterable'}} expected-note {{add stubs for conformance}}
extension FromOtherFile: CaseIterable {} // expected-error {{extension outside of file declaring enum 'FromOtherFile' prevents automatic synthesis of 'allCases' for protocol 'CaseIterable'}} expected-note {{add stubs for conformance}}
extension CaseIterableAcrossFiles: CaseIterable {
public static var allCases: [CaseIterableAcrossFiles] {
return [ .A ]
}
}
// No explicit conformance and it cannot be derived.
enum NotExplicitlyHashableAndCannotDerive {
@@ -243,7 +226,6 @@ enum NotExplicitlyHashableAndCannotDerive {
// expected-note@-1 {{associated value type 'NotHashable' does not conform to protocol 'Equatable', preventing synthesized conformance of 'NotExplicitlyHashableAndCannotDerive' to 'Equatable'}}
}
extension NotExplicitlyHashableAndCannotDerive : Hashable {} // expected-error 2 {{does not conform}} expected-note {{add stubs for conformance}}
extension NotExplicitlyHashableAndCannotDerive : CaseIterable {} // expected-error {{does not conform}} expected-note {{add stubs for conformance}}
// Verify that conformance (albeit manually implemented) can still be added to
// a type in a different file.
@@ -255,7 +237,6 @@ extension OtherFileNonconforming: Hashable {
}
// ...but synthesis in a type defined in another file doesn't work yet.
extension YetOtherFileNonconforming: Equatable {} // expected-error {{extension outside of file declaring enum 'YetOtherFileNonconforming' prevents automatic synthesis of '==' for protocol 'Equatable'}} expected-note {{add stubs for conformance}}
extension YetOtherFileNonconforming: CaseIterable {} // expected-error {{does not conform}} expected-note {{add stubs for conformance}}
// Verify that an indirect enum doesn't emit any errors as long as its "leaves"
// are conformant.

View File

@@ -0,0 +1,4 @@
enum FromOtherFile { // expected-unsupported-note {{type declared here}}
case a
case b
}

View File

@@ -0,0 +1,45 @@
// RUN: %target-swift-frontend -typecheck -verify -primary-file %s %S/../Inputs/case_iterable_other.swift
enum Simple: CaseIterable {
case a, b
static func staticMethod() -> Int {
return Self.allCases.count
}
}
enum Generic<T>: CaseIterable {
case a, b
static func staticMethod() -> Int {
return Self.allCases.count
}
}
enum InExtension {
case a, b
}
extension InExtension: CaseIterable {}
enum UnavailableCase: CaseIterable {
case a
@available(*, unavailable)
case b
public static var allCases: [UnavailableCase] {
return [.a]
}
}
extension FromOtherFile: CaseIterable {
public static var allCases: [FromOtherFile] {
return [.a, .b]
}
}
enum InvalidAvailableAttribute: CaseIterable {
case a
@available(deprecated, renamed: "a") // expected-warning {{unknown platform 'deprecated' for attribute 'available'}}
case b
}

View File

@@ -0,0 +1,73 @@
// RUN: %target-swift-frontend -typecheck -verify -primary-file %s %S/../Inputs/case_iterable_other.swift -verify-additional-prefix unsupported- -verify-ignore-unknown
extension FromOtherFile: CaseIterable {} // expected-error {{extension outside of file declaring enum 'FromOtherFile' prevents automatic synthesis of 'allCases' for protocol 'CaseIterable'}} expected-note {{add stubs for conformance}}
enum NotCaseIterableAssociatedValues: CaseIterable { // expected-error {{type 'NotCaseIterableAssociatedValues' does not conform to protocol 'CaseIterable'}}
// expected-note@-1 {{add stubs for conformance}}
case a(Int)
case b
}
// FIXME: [availability] Deprecation should not block this conformance synthesis
enum NotCaseIterableUniversallyDeprecatedCase: CaseIterable { // expected-error {{type 'NotCaseIterableUniversallyDeprecatedCase' does not conform to protocol 'CaseIterable'}}
// expected-note@-1 {{add stubs for conformance}}
case a
@available(*, deprecated)
case b
}
enum NotCaseIterableUniversallyUnavailableCase: CaseIterable { // expected-error {{type 'NotCaseIterableUniversallyUnavailableCase' does not conform to protocol 'CaseIterable'}}
// expected-note@-1 {{add stubs for conformance}}
case a
@available(*, unavailable)
case b
}
enum NotCaseIterableSwiftIntroducedLaterCase: CaseIterable { // expected-error {{type 'NotCaseIterableSwiftIntroducedLaterCase' does not conform to protocol 'CaseIterable'}}
// expected-note@-1 {{add stubs for conformance}}
case a
@available(swift, introduced: 99)
case b
}
enum NotCaseIterableSwiftIntroducedEarlierCase: CaseIterable { // expected-error {{type 'NotCaseIterableSwiftIntroducedEarlierCase' does not conform to protocol 'CaseIterable'}}
// expected-note@-1 {{add stubs for conformance}}
case a
@available(swift, introduced: 4)
case b
}
enum NotCaseIterableSwiftObsoletedLaterCase: CaseIterable { // expected-error {{type 'NotCaseIterableSwiftObsoletedLaterCase' does not conform to protocol 'CaseIterable'}}
// expected-note@-1 {{add stubs for conformance}}
case a
@available(swift, obsoleted: 99)
case b
}
enum NotCaseIterableSwiftObsoletedEarlierCase: CaseIterable { // expected-error {{type 'NotCaseIterableSwiftObsoletedEarlierCase' does not conform to protocol 'CaseIterable'}}
// expected-note@-1 {{add stubs for conformance}}
case a
@available(swift, obsoleted: 4)
case b
}
enum NotCaseIterableMacOSUnavailableCase: CaseIterable { // expected-error {{type 'NotCaseIterableMacOSUnavailableCase' does not conform to protocol 'CaseIterable'}}
// expected-note@-1 {{add stubs for conformance}}
case a
@available(macOS, unavailable)
case b
}
enum NotCaseIterableMacOSPotentiallyUnavailableCase: CaseIterable { // expected-error {{type 'NotCaseIterableMacOSPotentiallyUnavailableCase' does not conform to protocol 'CaseIterable'}}
// expected-note@-1 {{add stubs for conformance}}
case a
@available(macOS, introduced: 99)
case b
}
enum NotCaseIterableMacOSObsoletedCase: CaseIterable { // expected-error {{type 'NotCaseIterableMacOSObsoletedCase' does not conform to protocol 'CaseIterable'}}
// expected-note@-1 {{add stubs for conformance}}
case a
@available(macOS, obsoleted: 10.9)
case b
}