diff --git a/include/swift/AST/AvailabilityDomain.h b/include/swift/AST/AvailabilityDomain.h index 86001ffb4b39b..7d5ecac7df03c 100644 --- a/include/swift/AST/AvailabilityDomain.h +++ b/include/swift/AST/AvailabilityDomain.h @@ -289,6 +289,12 @@ class AvailabilityDomain final { return getRemappedDomain(ctx, unused); } + /// Returns true for a domain that is permanently always available, and + /// therefore availability constraints in the domain are effectively the same + /// as constraints in the `*` domain. This is used to diagnose unnecessary + /// `@available` attributes and `if #available` statements. + bool isPermanentlyAlwaysEnabled() const; + bool operator==(const AvailabilityDomain &other) const { return storage.getOpaqueValue() == other.storage.getOpaqueValue(); } @@ -330,7 +336,7 @@ struct StableAvailabilityDomainComparator { /// Represents an availability domain that has been defined in a module. class CustomAvailabilityDomain : public llvm::FoldingSetNode { public: - enum class Kind { + enum class Kind : uint8_t { /// A domain that is known to be enabled at compile time. Enabled, /// A domain that is known to be enabled at compile time and is also assumed @@ -344,10 +350,20 @@ class CustomAvailabilityDomain : public llvm::FoldingSetNode { private: Identifier name; - Kind kind; ModuleDecl *mod; ValueDecl *decl; FuncDecl *predicateFunc; + Kind kind; + + struct { + /// Whether the "isPermanentlyEnabled" bit has been computed yet. + unsigned isPermanentlyEnabledComputed : 1; + /// Whether the domain is permanently enabled, which makes constraints in + /// the domain equivalent to those in the `*` domain. + unsigned isPermanentlyEnabled : 1; + } flags = {}; + + friend class IsCustomAvailabilityDomainPermanentlyEnabled; CustomAvailabilityDomain(Identifier name, Kind kind, ModuleDecl *mod, ValueDecl *decl, FuncDecl *predicateFunc); @@ -375,6 +391,11 @@ class CustomAvailabilityDomain : public llvm::FoldingSetNode { void Profile(llvm::FoldingSetNodeID &ID) const { Profile(ID, name, mod); } }; +inline void simple_display(llvm::raw_ostream &os, + const CustomAvailabilityDomain *domain) { + os << domain->getName(); +} + /// Represents either a resolved availability domain or an identifier written /// in source that has not yet been resolved to a domain. class AvailabilityDomainOrIdentifier { diff --git a/include/swift/AST/DiagnosticGroups.def b/include/swift/AST/DiagnosticGroups.def index 172e60fa750ea..1db991e120c75 100644 --- a/include/swift/AST/DiagnosticGroups.def +++ b/include/swift/AST/DiagnosticGroups.def @@ -41,6 +41,7 @@ GROUP(no_group, "") GROUP(ActorIsolatedCall, "actor-isolated-call") +GROUP(AlwaysAvailableDomain, "always-available-domain") GROUP(AvailabilityUnrecognizedName, "availability-unrecognized-name") GROUP(ClangDeclarationImport, "clang-declaration-import") GROUP(ConformanceIsolation, "conformance-isolation") diff --git a/include/swift/AST/DiagnosticsSema.def b/include/swift/AST/DiagnosticsSema.def index 5171e4514860e..2a969d3868917 100644 --- a/include/swift/AST/DiagnosticsSema.def +++ b/include/swift/AST/DiagnosticsSema.def @@ -7006,6 +7006,15 @@ ERROR(attr_availability_domain_not_usable_from_inline, none, "'@usableFromInline' or public", (AvailabilityDomain, DeclAttribute, const Decl *)) +GROUPED_WARNING(attr_availability_has_no_effect_domain_always_available, + AlwaysAvailableDomain, none, + "'%0' has no effect because '%1' is always available", + (DeclAttribute, AvailabilityDomain)) +GROUPED_WARNING(attr_availability_domain_always_available, + AlwaysAvailableDomain, none, + "'%0' is always available, use '*' instead", + (AvailabilityDomain)) + ERROR(availability_decl_unavailable, none, "%0 is unavailable%select{ in %2|}1%select{|: %3}3", (const ValueDecl *, bool, AvailabilityDomain, StringRef)) @@ -7127,6 +7136,12 @@ WARNING(availability_query_useless_enclosing_scope, none, NOTE(availability_query_useless_enclosing_scope_here, none, "enclosing scope here", ()) +GROUPED_WARNING(availability_query_useless_always_true, + AlwaysAvailableDomain, none, + "unnecessary check for '%0'; " + "this condition will always be %select{false|true}1", + (AvailabilityDomain, unsigned)) + ERROR(availability_decl_no_potential, none, "%kindonly0 cannot be marked potentially unavailable with '@available'", (const Decl *)) diff --git a/include/swift/AST/TypeCheckRequests.h b/include/swift/AST/TypeCheckRequests.h index 00c3fcdfc3325..e5e3cdd70475c 100644 --- a/include/swift/AST/TypeCheckRequests.h +++ b/include/swift/AST/TypeCheckRequests.h @@ -5505,7 +5505,6 @@ class AvailabilityDomainForDeclRequest private: friend SimpleRequest; - // Evaluation. std::optional evaluate(Evaluator &evaluator, ValueDecl *decl) const; @@ -5515,6 +5514,30 @@ class AvailabilityDomainForDeclRequest void cacheResult(std::optional domain) const; }; +class IsCustomAvailabilityDomainPermanentlyEnabled + : public SimpleRequest { +public: + using SimpleRequest::SimpleRequest; + +private: + friend SimpleRequest; + + bool evaluate(Evaluator &evaluator, + const CustomAvailabilityDomain *customDomain) const; + +public: + bool isCached() const { return true; } + std::optional getCachedResult() const; + void cacheResult(bool isPermanentlyEnabled) const; + + SourceLoc getNearestLoc() const { + auto *domain = std::get<0>(getStorage()); + return extractNearestSourceLoc(domain->getDecl()); + } +}; + #define SWIFT_TYPEID_ZONE TypeChecker #define SWIFT_TYPEID_HEADER "swift/AST/TypeCheckerTypeIDZone.def" #include "swift/Basic/DefineTypeIDZone.h" diff --git a/include/swift/AST/TypeCheckerTypeIDZone.def b/include/swift/AST/TypeCheckerTypeIDZone.def index dbb8e382756a0..ff32da6deb13f 100644 --- a/include/swift/AST/TypeCheckerTypeIDZone.def +++ b/include/swift/AST/TypeCheckerTypeIDZone.def @@ -660,3 +660,7 @@ SWIFT_REQUEST(TypeChecker, ModuleHasTypeCheckerPerformanceHacksEnabledRequest, SWIFT_REQUEST(TypeChecker, AvailabilityDomainForDeclRequest, std::optional(ValueDecl *), Cached | SplitCached, NoLocationInfo) + +SWIFT_REQUEST(TypeChecker, IsCustomAvailabilityDomainPermanentlyEnabled, + bool(const CustomAvailabilityDomain *), + SeparatelyCached, NoLocationInfo) diff --git a/lib/AST/AvailabilityDomain.cpp b/lib/AST/AvailabilityDomain.cpp index 774b69b1242d0..40f9135862e25 100644 --- a/lib/AST/AvailabilityDomain.cpp +++ b/lib/AST/AvailabilityDomain.cpp @@ -357,6 +357,45 @@ AvailabilityDomain::getRemappedDomain(const ASTContext &ctx, return *this; } +bool IsCustomAvailabilityDomainPermanentlyEnabled::evaluate( + Evaluator &evaluator, const CustomAvailabilityDomain *customDomain) const { + switch (customDomain->getKind()) { + case CustomAvailabilityDomain::Kind::Enabled: + case CustomAvailabilityDomain::Kind::Disabled: + case CustomAvailabilityDomain::Kind::Dynamic: + return false; + + case CustomAvailabilityDomain::Kind::AlwaysEnabled: + break; + } + + auto *domainDecl = customDomain->getDecl(); + if (!domainDecl) + return false; + + if (auto deprecatedAttr = domainDecl->getDeprecatedAttr()) { + if (deprecatedAttr->getDomain().isUniversal()) + return true; + } + + if (auto unavailableAttr = domainDecl->getUnavailableAttr()) { + if (unavailableAttr->getDomain().isUniversal()) + return true; + } + + return false; +} + +bool AvailabilityDomain::isPermanentlyAlwaysEnabled() const { + if (auto *customDomain = getCustomDomain()) { + if (auto *domainDecl = customDomain->getDecl()) + return evaluateOrDefault( + domainDecl->getASTContext().evaluator, + IsCustomAvailabilityDomainPermanentlyEnabled{customDomain}, false); + } + return false; +} + void AvailabilityDomain::print(llvm::raw_ostream &os) const { os << getNameForAttributePrinting(); } @@ -408,8 +447,8 @@ CustomAvailabilityDomain::CustomAvailabilityDomain(Identifier name, Kind kind, ModuleDecl *mod, ValueDecl *decl, FuncDecl *predicateFunc) - : name(name), kind(kind), mod(mod), decl(decl), - predicateFunc(predicateFunc) { + : name(name), mod(mod), decl(decl), predicateFunc(predicateFunc), + kind(kind) { ASSERT(!name.empty()); ASSERT(mod); if (predicateFunc) diff --git a/lib/AST/TypeCheckRequests.cpp b/lib/AST/TypeCheckRequests.cpp index a69bef26b326f..d84abcc283f11 100644 --- a/lib/AST/TypeCheckRequests.cpp +++ b/lib/AST/TypeCheckRequests.cpp @@ -2867,3 +2867,23 @@ void AvailabilityDomainForDeclRequest::cacheResult( decl->getASTContext().evaluator.cacheNonEmptyOutput(*this, std::move(domain)); } + +//----------------------------------------------------------------------------// +// IsCustomAvailabilityDomainPermanentlyEnabled computation. +//----------------------------------------------------------------------------// +std::optional +IsCustomAvailabilityDomainPermanentlyEnabled::getCachedResult() const { + auto *domain = std::get<0>(getStorage()); + + if (domain->flags.isPermanentlyEnabledComputed) + return domain->flags.isPermanentlyEnabled; + return std::nullopt; +} + +void IsCustomAvailabilityDomainPermanentlyEnabled::cacheResult( + bool isPermanentlyEnabled) const { + auto *domain = const_cast(std::get<0>(getStorage())); + + domain->flags.isPermanentlyEnabledComputed = true; + domain->flags.isPermanentlyEnabled = isPermanentlyEnabled; +} diff --git a/lib/Sema/TypeCheckAccess.cpp b/lib/Sema/TypeCheckAccess.cpp index 4a844cf7b2e0f..c1bc81761a64f 100644 --- a/lib/Sema/TypeCheckAccess.cpp +++ b/lib/Sema/TypeCheckAccess.cpp @@ -2274,12 +2274,45 @@ class DeclAvailabilityChecker : public DeclVisitor { void checkAvailabilityDomains(const Decl *D) { D = D->getAbstractSyntaxDeclForAttributes(); + auto &ctx = D->getASTContext(); auto where = Where.withReason(ExportabilityReason::AvailableAttribute); for (auto attr : D->getSemanticAvailableAttrs()) { - if (auto *domainDecl = attr.getDomain().getDecl()) { + auto domain = attr.getDomain(); + if (auto *domainDecl = domain.getDecl()) { diagnoseDeclAvailability(domainDecl, attr.getParsedAttr()->getDomainLoc(), nullptr, where, std::nullopt); + + if (!attr.isVersionSpecific()) { + // Check whether the availability domain is permanently enabled. If it + // is, suggest modifying or removing the attribute. + if (domain.isPermanentlyAlwaysEnabled()) { + auto parsedAttr = attr.getParsedAttr(); + switch (parsedAttr->getKind()) { + case AvailableAttr::Kind::Default: + // This introduction constraint has no effect since it will never + // restrict use of the declaration. Provide a fix-it remove the + // attribute. + diagnoseAndRemoveAttr( + D, attr.getParsedAttr(), + diag::attr_availability_has_no_effect_domain_always_available, + parsedAttr, domain); + break; + case AvailableAttr::Kind::Deprecated: + case AvailableAttr::Kind::NoAsync: + case AvailableAttr::Kind::Unavailable: + // Any other kind of constraint always constrains use of the + // declaration, so provide a fix-it to specify `*` instead of the + // custom domain. + ctx.Diags + .diagnose(parsedAttr->getDomainLoc(), + diag::attr_availability_domain_always_available, + domain) + .fixItReplace(parsedAttr->getDomainLoc(), "*"); + break; + } + } + } } } } diff --git a/lib/Sema/TypeCheckStmt.cpp b/lib/Sema/TypeCheckStmt.cpp index 08477dc6a42fa..2e99295f2be8e 100644 --- a/lib/Sema/TypeCheckStmt.cpp +++ b/lib/Sema/TypeCheckStmt.cpp @@ -810,6 +810,37 @@ ConcreteDeclRef TypeChecker::getReferencedDeclForHasSymbolCondition(Expr *E) { return ConcreteDeclRef(); } +static bool typeCheckAvailableStmtConditionElement(StmtConditionElement &elt, + bool &isFalseable, + DeclContext *dc) { + auto info = elt.getAvailability(); + if (info->isInvalid()) { + isFalseable = true; + return false; + } + + auto &diags = dc->getASTContext().Diags; + bool isConditionAlwaysTrue = false; + + if (auto query = info->getAvailabilityQuery()) { + auto domain = query->getDomain(); + if (query->isConstant() && domain.isPermanentlyAlwaysEnabled()) { + isConditionAlwaysTrue = *query->getConstantResult(); + + diags + .diagnose(elt.getStartLoc(), + diag::availability_query_useless_always_true, domain, + isConditionAlwaysTrue) + .highlight(elt.getSourceRange()); + } + } + + if (!isConditionAlwaysTrue) + isFalseable = true; + + return false; +} + static bool typeCheckHasSymbolStmtConditionElement(StmtConditionElement &elt, DeclContext *dc) { auto Info = elt.getHasSymbolInfo(); @@ -895,8 +926,7 @@ bool TypeChecker::typeCheckStmtConditionElement(StmtConditionElement &elt, DeclContext *dc) { switch (elt.getKind()) { case StmtConditionElement::CK_Availability: - isFalsable = true; - return false; + return typeCheckAvailableStmtConditionElement(elt, isFalsable, dc); case StmtConditionElement::CK_HasSymbol: isFalsable = true; return typeCheckHasSymbolStmtConditionElement(elt, dc); @@ -924,8 +954,9 @@ static bool typeCheckConditionForStatement(LabeledConditionalStmt *stmt, TypeChecker::typeCheckStmtConditionElement(elt, hadAnyFalsable, dc); } - // If the binding is not refutable, and there *is* an else, reject it as - // unreachable. + // If none of the statement's conditions can be false, diagnose. + // FIXME: Also diagnose if none of the statements conditions can be true. + // FIXME: Offer a fix-it to remove the unreachable code. if (!hadAnyFalsable && !hadError) { auto &diags = dc->getASTContext().Diags; Diag<> msg = diag::invalid_diagnostic; diff --git a/test/ClangImporter/access-level-import-availability-custom-domains.swift b/test/ClangImporter/availability_custom_domains_access_availability.swift similarity index 81% rename from test/ClangImporter/access-level-import-availability-custom-domains.swift rename to test/ClangImporter/availability_custom_domains_access_availability.swift index 26d39dbc58da5..00b7d28210636 100644 --- a/test/ClangImporter/access-level-import-availability-custom-domains.swift +++ b/test/ClangImporter/availability_custom_domains_access_availability.swift @@ -302,6 +302,9 @@ extension PublicGenericStruct: PublicProtocolWithAssociatedType { if #available(Baltic) { } if #available(BayBridge) { } if #available(Salt) { } // expected-error {{availability domain 'Salt' cannot be used in an '@inlinable' function because 'Lakes' was imported for SPI only}} + if #available(Aral) { } // expected-warning {{availability domain 'Aral' is deprecated}} + // expected-warning@-1 {{unnecessary check for 'Aral'; this condition will always be true}} + // expected-warning@-2 {{'if' condition is always true}} @available(Colorado) // expected-error {{availability domain 'Colorado' is internal and cannot be referenced from an '@inlinable' function}} @available(Grand) // expected-error {{availability domain 'Grand' is internal and cannot be referenced from an '@inlinable' function}} @@ -309,6 +312,8 @@ extension PublicGenericStruct: PublicProtocolWithAssociatedType { @available(Baltic) @available(BayBridge) @available(Salt) // expected-error {{availability domain 'Salt' cannot be used in an '@inlinable' function because 'Lakes' was imported for SPI only}} + @available(Aral) // expected-warning {{availability domain 'Aral' is deprecated}} + // expected-warning@-1 {{'@available' has no effect because 'Aral' is always available}}{{3-20=}} func nestedFunc() { } } @@ -319,6 +324,9 @@ public func nonInlinablePublicFunc() { if #available(Baltic) { } if #available(BayBridge) { } if #available(Salt) { } + if #available(Aral) { } // expected-warning {{availability domain 'Aral' is deprecated}} + // expected-warning@-1 {{unnecessary check for 'Aral'; this condition will always be true}} + // expected-warning@-2 {{'if' condition is always true}} @available(Colorado) @available(Grand) // expected-warning {{availability domain 'Grand' is deprecated: Use Colorado instead}} @@ -326,5 +334,111 @@ public func nonInlinablePublicFunc() { @available(Baltic) @available(BayBridge) @available(Salt) + @available(Aral) // expected-warning {{availability domain 'Aral' is deprecated}} + // expected-warning@-1 {{'@available' has no effect because 'Aral' is always available}}{{3-20=}} func nestedFunc() { } } + +@available(Aral) // expected-warning {{availability domain 'Aral' is deprecated}} +// expected-warning@-1 {{'@available' has no effect because 'Aral' is always available}}{{1-18=}} +func testPermanentlyAvailable() { } + +@available(Aral, deprecated) +// expected-warning@-1 {{'Aral' is always available, use '*' instead}}{{12-16=*}} +func testPermanentlyDeprecated() { } + +@available(Aral, unavailable) // expected-warning {{availability domain 'Aral' is deprecated}} +// expected-warning@-1 {{'Aral' is always available, use '*' instead}}{{12-16=*}} +func testPermanentlyUnavailable() { } + +@available(*, deprecated) +extension PublicGenericStruct { + @available(Aral) + // expected-warning@-1 {{'@available' has no effect because 'Aral' is always available}}{{3-19=}} + func testPermanentlyAvailableInDeprecatedExtension() { } + + @available(Aral, deprecated) + // expected-warning@-1 {{'Aral' is always available, use '*' instead}}{{14-18=*}} + func testPermanentlyDeprecatedInDeprecatedExtension() { } + + @available(Aral, unavailable) + // expected-warning@-1 {{'Aral' is always available, use '*' instead}}{{14-18=*}} + func testPermanentlyUnavailableInDeprecatedExtension() { } +} + +@available(*, unavailable) +extension PublicGenericStruct { + @available(Aral) // expected-warning {{availability domain 'Aral' is deprecated}} + // expected-warning@-1 {{'@available' has no effect because 'Aral' is always available}}{{3-20=}} + func testPermanentlyAvailableInUnavailableExtension() { } + + @available(Aral, deprecated) + // expected-warning@-1 {{'Aral' is always available, use '*' instead}}{{14-18=*}} + func testPermanentlyDeprecatedInUnavailableExtension() { } + + @available(Aral, unavailable) // expected-warning {{availability domain 'Aral' is deprecated}} + // expected-warning@-1 {{'Aral' is always available, use '*' instead}}{{14-18=*}} + func testPermanentlyUnavailableInUnavailableExtension() { } +} + +func testUnnecessaryIfAvailableStmt() { + if #available(Aral) { + // expected-warning@-1 {{availability domain 'Aral' is deprecated}} + // expected-warning@-2 {{unnecessary check for 'Aral'; this condition will always be true}}{{none}} + // expected-warning@-3 {{'if' condition is always true}}{{none}} + } +} + +func testUnnecessaryIfAvailableExpr() { + _ = if #available(Aral) { true } else { false } + // expected-warning@-1 {{availability domain 'Aral' is deprecated}} + // FIXME: [availability] Missing "always true" diagnostic + // https://github.com/swiftlang/swift/issues/84453 +} + +func testUnnecessaryIfAvailableCompoundStmt() { + if #available(Colorado), #available(Aral) { + // expected-warning@-1 {{availability domain 'Aral' is deprecated}} + // expected-warning@-2 {{unnecessary check for 'Aral'; this condition will always be true}}{{none}} + } +} + +func testUnnecessaryIfUnavailableStmt() { + if #unavailable(Aral) { + // expected-warning@-1 {{availability domain 'Aral' is deprecated}} + // expected-warning@-2 {{unnecessary check for 'Aral'; this condition will always be false}}{{none}} + } +} + +func testUnnecessaryIfUnavailableCompoundStmt() { + if #unavailable(Colorado), #unavailable(Aral) { + // expected-warning@-1 {{availability domain 'Aral' is deprecated}} + // expected-warning@-2 {{unnecessary check for 'Aral'; this condition will always be false}}{{none}} + } +} + +func testUnnecessaryGuardAvailableStmt() { + guard #available(Aral) else { + // expected-warning@-1 {{availability domain 'Aral' is deprecated}} + // expected-warning@-2 {{unnecessary check for 'Aral'; this condition will always be true}}{{none}} + // expected-warning@-3 {{'guard' condition is always true}}{{none}} + return + } +} + +func testUnnecessaryGuardUnavailableStmt() { + guard #unavailable(Aral) else { + // expected-warning@-1 {{availability domain 'Aral' is deprecated}} + // expected-warning@-2 {{unnecessary check for 'Aral'; this condition will always be false}}{{none}} + return + } +} + +func testUnnecessaryWhileAvailableStmt() { + while #available(Aral) { + // expected-warning@-1 {{availability domain 'Aral' is deprecated}} + // expected-warning@-2 {{unnecessary check for 'Aral'; this condition will always be true}}{{none}} + // expected-warning@-3 {{'while' condition is always true}}{{none}} + break + } +} diff --git a/test/Inputs/custom-modules/availability-domains/Seas.h b/test/Inputs/custom-modules/availability-domains/Seas.h index d67cb296ed25b..4a1a4e3b1fdb9 100644 --- a/test/Inputs/custom-modules/availability-domains/Seas.h +++ b/test/Inputs/custom-modules/availability-domains/Seas.h @@ -6,6 +6,8 @@ CLANG_ENABLED_AVAILABILITY_DOMAIN(Baltic); CLANG_ALWAYS_ENABLED_AVAILABILITY_DOMAIN(Bering); CLANG_DISABLED_AVAILABILITY_DOMAIN(Mediterranean); CLANG_DYNAMIC_AVAILABILITY_DOMAIN(Aegean, aegean_pred); +__attribute__((deprecated)) +CLANG_ALWAYS_ENABLED_AVAILABILITY_DOMAIN(Aral); #define AVAIL 0 #define UNAVAIL 1 diff --git a/userdocs/diagnostics/always-available-domain.md b/userdocs/diagnostics/always-available-domain.md new file mode 100644 index 0000000000000..03ee851a517b3 --- /dev/null +++ b/userdocs/diagnostics/always-available-domain.md @@ -0,0 +1,16 @@ +# Always enabled availability domains (AlwaysAvailableDomain) + +Warnings that identify `@available` attributes and `if #available` statements that reference custom availability domains which have been permanently enabled. + +## Overview + +Custom availability domains (which are an experimental feature gated by `-enable-experimental-feature CustomAvailability`) may be declared as "permanently enabled" which indicates that the domain is known available at compile time and can never become unavailable in a different configuration. This state is useful for representing feature flags for features which have become permanently enabled. + +Restricting a region of code to only being available in a permanently enabled availability domain will result in diagnostics that suggest the removal of the restriction. For example: + +``` +@available(MyFeature) // warning: '@available' has no effect because 'PermanentlyEnabled' is always available +struct MyFeatureView: View { + // ... +} +```