From 7a78798478d99ef3ca2e88a94ad63650ecc7fa48 Mon Sep 17 00:00:00 2001 From: Artem Chikin Date: Thu, 10 Jul 2025 11:19:18 -0700 Subject: [PATCH] [APINotes] Add support for capturing all possible versioned APINotes without applying them Swift-versioned API notes get applied at PCM constrution time relying on '-fapinotes-swift-version=X' argument to pick the appropriate version. This change adds a new APINotes application mode with '-fswift-version-independent-apinotes' which causes *all* versioned API notes to get recorded into the PCM wrapped in 'SwiftVersionedAttr' instances. The expectation in this mode is that the Swift client will perform the required transformations as per the API notes on the client side, when loading the PCM, instead of them getting applied on the producer side. This will allow the same PCM to be usable by Swift clients building with different language versions. In addition to versioned-wrapping the various existing API notes annotations which are carried in declaration attributes, this change adds a new attribute for two annotations which were previously applied directly to the declaration at the PCM producer side: 1) Type and 2) Nullability annotations with 'SwiftTypeAttr' and 'SwiftNullabilityAttr', respectively. The logic to apply these two annotations to a declaration is refactored into API. --- .../include/clang/APINotes/APINotesManager.h | 9 + clang/include/clang/Basic/Attr.td | 20 ++ clang/include/clang/Basic/LangOptions.def | 1 + clang/include/clang/Driver/Options.td | 6 + clang/include/clang/Sema/Sema.h | 13 +- clang/lib/APINotes/APINotesManager.cpp | 3 +- clang/lib/Driver/ToolChains/Clang.cpp | 4 + clang/lib/Sema/SemaAPINotes.cpp | 213 +++++++++++------- .../APINotes/versioned-version-independent.m | 36 +++ 9 files changed, 226 insertions(+), 79 deletions(-) create mode 100644 clang/test/APINotes/versioned-version-independent.m diff --git a/clang/include/clang/APINotes/APINotesManager.h b/clang/include/clang/APINotes/APINotesManager.h index 98592438e90ea..772fa5faa0f87 100644 --- a/clang/include/clang/APINotes/APINotesManager.h +++ b/clang/include/clang/APINotes/APINotesManager.h @@ -50,6 +50,13 @@ class APINotesManager { /// source file from which an entity was declared. bool ImplicitAPINotes; + /// Whether to apply all APINotes as optionally-applied versioned + /// entities. This means that when building a Clang module, + /// we capture every note on a given decl wrapped in a SwiftVersionedAttr + /// (with an empty version field for unversioned notes), and have the + /// client apply the relevant version's notes. + bool VersionIndependentSwift; + /// The Swift version to use when interpreting versioned API notes. llvm::VersionTuple SwiftVersion; @@ -167,6 +174,8 @@ class APINotesManager { /// Find the API notes readers that correspond to the given source location. llvm::SmallVector findAPINotes(SourceLocation Loc); + + bool captureVersionIndependentSwift() { return VersionIndependentSwift; } }; } // end namespace api_notes diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td index 35a18aecef7d3..c1f00a4e4f7e6 100644 --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -2987,6 +2987,26 @@ def Regparm : TypeAttr { let ASTNode = 0; } +def SwiftType : Attr { + // This attribute has no spellings as it is only ever created implicitly + // from API notes. + let Spellings = []; + let Args = [StringArgument<"TypeString">]; + let SemaHandler = 0; + let Documentation = [InternalOnly]; +} + +def SwiftNullability : Attr { + // This attribute has no spellings as it is only ever created implicitly + // from API notes. + let Spellings = []; + let Args = [EnumArgument<"Kind", "Kind", /*is_string=*/false, + ["non_null", "nullable", "unspecified", "nullable_result"], + ["NonNull", "Nullable", "Unspecified", "NullableResult"]>]; + let SemaHandler = 0; + let Documentation = [InternalOnly]; +} + def SwiftAsyncName : InheritableAttr { let Spellings = [GNU<"swift_async_name">]; let Args = [StringArgument<"Name">]; diff --git a/clang/include/clang/Basic/LangOptions.def b/clang/include/clang/Basic/LangOptions.def index e27370f6e0f4e..9538059d4b145 100644 --- a/clang/include/clang/Basic/LangOptions.def +++ b/clang/include/clang/Basic/LangOptions.def @@ -438,6 +438,7 @@ LANGOPT(RetainCommentsFromSystemHeaders, 1, 0, "retain documentation comments fr LANGOPT(APINotes, 1, 0, "use external API notes") LANGOPT(APINotesModules, 1, 0, "use module-based external API notes") LANGOPT(NeededByPCHOrCompilationUsesPCH, 1, 0, "compilation involves pch") +LANGOPT(SwiftVersionIndependentAPINotes, 1, 0, "use external API notes capturing all versions") LANGOPT(SanitizeAddressFieldPadding, 2, 0, "controls how aggressive is ASan " "field padding (0: none, 1:least " diff --git a/clang/include/clang/Driver/Options.td b/clang/include/clang/Driver/Options.td index 907742522a3df..1d42daef7f7c0 100644 --- a/clang/include/clang/Driver/Options.td +++ b/clang/include/clang/Driver/Options.td @@ -1934,6 +1934,12 @@ defm apinotes_modules : BoolOption<"f", "apinotes-modules", NegFlag, BothFlags<[], [ClangOption, CC1Option], " module-based external API notes support">>, Group; +defm swift_version_independent_apinotes : BoolOption<"f", "swift-version-independent-apinotes", + LangOpts<"SwiftVersionIndependentAPINotes">, DefaultFalse, + PosFlag, + NegFlag, + BothFlags<[], [ClangOption, CC1Option], " version-independent external API notes support">>, + Group; def fapinotes_swift_version : Joined<["-"], "fapinotes-swift-version=">, Group, Visibility<[ClangOption, CC1Option]>, MetaVarName<"">, diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 79610dda0b4f6..1540ba1ae6f33 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1416,7 +1416,6 @@ class Sema final : public SemaBase { // ------------------------------------------------------------------------- // // - /// \name C++ Access Control /// Implementations are in SemaAccess.cpp ///@{ @@ -15543,7 +15542,17 @@ class Sema final : public SemaBase { /// /// Triggered by declaration-attribute processing. void ProcessAPINotes(Decl *D); - + /// Apply the 'Nullability:' annotation to the specified declaration + void ApplyNullability(Decl *D, NullabilityKind Nullability); + /// Apply the 'Type:' annotation to the specified declaration + void ApplyAPINotesType(Decl *D, StringRef TypeString); + + /// Whether APINotes should be gathered for all applicable Swift language + /// versions, without being applied. Leaving clients of the current module + /// to select and apply the correct version. + bool captureSwiftVersionIndependentAPINotes() { + return APINotes.captureVersionIndependentSwift(); + } ///@} // diff --git a/clang/lib/APINotes/APINotesManager.cpp b/clang/lib/APINotes/APINotesManager.cpp index 039d09fa7cf57..51d322205d9f3 100644 --- a/clang/lib/APINotes/APINotesManager.cpp +++ b/clang/lib/APINotes/APINotesManager.cpp @@ -51,7 +51,8 @@ class PrettyStackTraceDoubleString : public llvm::PrettyStackTraceEntry { } // namespace APINotesManager::APINotesManager(SourceManager &SM, const LangOptions &LangOpts) - : SM(SM), ImplicitAPINotes(LangOpts.APINotes) {} + : SM(SM), ImplicitAPINotes(LangOpts.APINotes), + VersionIndependentSwift(LangOpts.SwiftVersionIndependentAPINotes) {} APINotesManager::~APINotesManager() { // Free the API notes readers. diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp index e85955d92dc24..c1466883a54af 100644 --- a/clang/lib/Driver/ToolChains/Clang.cpp +++ b/clang/lib/Driver/ToolChains/Clang.cpp @@ -7214,6 +7214,10 @@ void Clang::ConstructJob(Compilation &C, const JobAction &Job, Args.AddLastArg(CmdArgs, options::OPT_fapinotes_swift_version); } + if (Args.hasFlag(options::OPT_fswift_version_independent_apinotes, + options::OPT_fno_swift_version_independent_apinotes, false)) + CmdArgs.push_back("-fswift-version-independent-apinotes"); + // -fblocks=0 is default. if (Args.hasFlag(options::OPT_fblocks, options::OPT_fno_blocks, TC.IsBlocksDefault()) || diff --git a/clang/lib/Sema/SemaAPINotes.cpp b/clang/lib/Sema/SemaAPINotes.cpp index 15e77045800d5..dffc2a31ef62a 100644 --- a/clang/lib/Sema/SemaAPINotes.cpp +++ b/clang/lib/Sema/SemaAPINotes.cpp @@ -53,63 +53,58 @@ static bool isIndirectPointerType(QualType Type) { Pointee->isMemberPointerType(); } -/// Apply nullability to the given declaration. -static void applyNullability(Sema &S, Decl *D, NullabilityKind Nullability, - VersionedInfoMetadata Metadata) { - if (!Metadata.IsActive) - return; - - auto GetModified = - [&](Decl *D, QualType QT, - NullabilityKind Nullability) -> std::optional { - QualType Original = QT; - S.CheckImplicitNullabilityTypeSpecifier(QT, Nullability, D->getLocation(), - isa(D), - /*OverrideExisting=*/true); - return (QT.getTypePtr() != Original.getTypePtr()) ? std::optional(QT) - : std::nullopt; - }; +static void applyAPINotesType(Sema &S, Decl *decl, StringRef typeString, + VersionedInfoMetadata metadata) { + if (typeString.empty()) - if (auto Function = dyn_cast(D)) { - if (auto Modified = - GetModified(D, Function->getReturnType(), Nullability)) { - const FunctionType *FnType = Function->getType()->castAs(); - if (const FunctionProtoType *proto = dyn_cast(FnType)) - Function->setType(S.Context.getFunctionType( - *Modified, proto->getParamTypes(), proto->getExtProtoInfo())); - else - Function->setType( - S.Context.getFunctionNoProtoType(*Modified, FnType->getExtInfo())); - } - } else if (auto Method = dyn_cast(D)) { - if (auto Modified = GetModified(D, Method->getReturnType(), Nullability)) { - Method->setReturnType(*Modified); + return; - // Make it a context-sensitive keyword if we can. - if (!isIndirectPointerType(*Modified)) - Method->setObjCDeclQualifier(Decl::ObjCDeclQualifier( - Method->getObjCDeclQualifier() | Decl::OBJC_TQ_CSNullability)); - } - } else if (auto Value = dyn_cast(D)) { - if (auto Modified = GetModified(D, Value->getType(), Nullability)) { - Value->setType(*Modified); + // Version-independent APINotes add "type" annotations + // with a versioned attribute for the client to select and apply. + if (S.captureSwiftVersionIndependentAPINotes()) { + auto *typeAttr = SwiftTypeAttr::CreateImplicit(S.Context, typeString); + auto *versioned = SwiftVersionedAdditionAttr::CreateImplicit( + S.Context, metadata.Version, typeAttr, metadata.IsReplacement); + decl->addAttr(versioned); + } else { + if (!metadata.IsActive) + return; + S.ApplyAPINotesType(decl, typeString); + } +} - // Make it a context-sensitive keyword if we can. - if (auto Parm = dyn_cast(D)) { - if (Parm->isObjCMethodParameter() && !isIndirectPointerType(*Modified)) - Parm->setObjCDeclQualifier(Decl::ObjCDeclQualifier( - Parm->getObjCDeclQualifier() | Decl::OBJC_TQ_CSNullability)); - } +/// Apply nullability to the given declaration. +static void applyNullability(Sema &S, Decl *decl, NullabilityKind nullability, + VersionedInfoMetadata metadata) { + // Version-independent APINotes add "nullability" annotations + // with a versioned attribute for the client to select and apply. + if (S.captureSwiftVersionIndependentAPINotes()) { + SwiftNullabilityAttr::Kind attrNullabilityKind; + switch (nullability) { + case NullabilityKind::NonNull: + attrNullabilityKind = SwiftNullabilityAttr::Kind::NonNull; + break; + case NullabilityKind::Nullable: + attrNullabilityKind = SwiftNullabilityAttr::Kind::Nullable; + break; + case NullabilityKind::Unspecified: + attrNullabilityKind = SwiftNullabilityAttr::Kind::Unspecified; + break; + case NullabilityKind::NullableResult: + attrNullabilityKind = SwiftNullabilityAttr::Kind::NullableResult; + break; } - } else if (auto Property = dyn_cast(D)) { - if (auto Modified = GetModified(D, Property->getType(), Nullability)) { - Property->setType(*Modified, Property->getTypeSourceInfo()); + auto *nullabilityAttr = + SwiftNullabilityAttr::CreateImplicit(S.Context, attrNullabilityKind); + auto *versioned = SwiftVersionedAdditionAttr::CreateImplicit( + S.Context, metadata.Version, nullabilityAttr, metadata.IsReplacement); + decl->addAttr(versioned); + return; + } else { + if (!metadata.IsActive) + return; - // Make it a property attribute if we can. - if (!isIndirectPointerType(*Modified)) - Property->setPropertyAttributes( - ObjCPropertyAttribute::kind_null_resettable); - } + S.ApplyNullability(decl, nullability); } } @@ -363,42 +358,99 @@ static bool checkAPINotesReplacementType(Sema &S, SourceLocation Loc, return false; } -/// Process API notes for a variable or property. -static void ProcessAPINotes(Sema &S, Decl *D, - const api_notes::VariableInfo &Info, - VersionedInfoMetadata Metadata) { - // Type override. - if (Metadata.IsActive && !Info.getType().empty() && - S.ParseTypeFromStringCallback) { - auto ParsedType = S.ParseTypeFromStringCallback( - Info.getType(), "", D->getLocation()); +void Sema::ApplyAPINotesType(Decl *D, StringRef TypeString) { + if (!TypeString.empty() && ParseTypeFromStringCallback) { + auto ParsedType = ParseTypeFromStringCallback(TypeString, "", + D->getLocation()); if (ParsedType.isUsable()) { QualType Type = Sema::GetTypeFromParser(ParsedType.get()); - auto TypeInfo = - S.Context.getTrivialTypeSourceInfo(Type, D->getLocation()); - + auto TypeInfo = Context.getTrivialTypeSourceInfo(Type, D->getLocation()); if (auto Var = dyn_cast(D)) { // Make adjustments to parameter types. if (isa(Var)) { - Type = S.ObjC().AdjustParameterTypeForObjCAutoRefCount( + Type = ObjC().AdjustParameterTypeForObjCAutoRefCount( Type, D->getLocation(), TypeInfo); - Type = S.Context.getAdjustedParameterType(Type); + Type = Context.getAdjustedParameterType(Type); } - if (!checkAPINotesReplacementType(S, Var->getLocation(), Var->getType(), - Type)) { + if (!checkAPINotesReplacementType(*this, Var->getLocation(), + Var->getType(), Type)) { Var->setType(Type); Var->setTypeSourceInfo(TypeInfo); } - } else if (auto Property = dyn_cast(D)) { - if (!checkAPINotesReplacementType(S, Property->getLocation(), - Property->getType(), Type)) - Property->setType(Type, TypeInfo); - - } else + } else if (auto property = dyn_cast(D)) { + if (!checkAPINotesReplacementType(*this, property->getLocation(), + property->getType(), Type)) { + property->setType(Type, TypeInfo); + } + } else { llvm_unreachable("API notes allowed a type on an unknown declaration"); + } + } + } +} + +void Sema::ApplyNullability(Decl *D, NullabilityKind Nullability) { + auto GetModified = + [&](class Decl *D, QualType QT, + NullabilityKind Nullability) -> std::optional { + QualType Original = QT; + CheckImplicitNullabilityTypeSpecifier(QT, Nullability, D->getLocation(), + isa(D), + /*OverrideExisting=*/true); + return (QT.getTypePtr() != Original.getTypePtr()) ? std::optional(QT) + : std::nullopt; + }; + + if (auto Function = dyn_cast(D)) { + if (auto Modified = + GetModified(D, Function->getReturnType(), Nullability)) { + const FunctionType *FnType = Function->getType()->castAs(); + if (const FunctionProtoType *proto = dyn_cast(FnType)) + Function->setType(Context.getFunctionType( + *Modified, proto->getParamTypes(), proto->getExtProtoInfo())); + else + Function->setType( + Context.getFunctionNoProtoType(*Modified, FnType->getExtInfo())); + } + } else if (auto Method = dyn_cast(D)) { + if (auto Modified = GetModified(D, Method->getReturnType(), Nullability)) { + Method->setReturnType(*Modified); + + // Make it a context-sensitive keyword if we can. + if (!isIndirectPointerType(*Modified)) + Method->setObjCDeclQualifier(Decl::ObjCDeclQualifier( + Method->getObjCDeclQualifier() | Decl::OBJC_TQ_CSNullability)); + } + } else if (auto Value = dyn_cast(D)) { + if (auto Modified = GetModified(D, Value->getType(), Nullability)) { + Value->setType(*Modified); + + // Make it a context-sensitive keyword if we can. + if (auto Parm = dyn_cast(D)) { + if (Parm->isObjCMethodParameter() && !isIndirectPointerType(*Modified)) + Parm->setObjCDeclQualifier(Decl::ObjCDeclQualifier( + Parm->getObjCDeclQualifier() | Decl::OBJC_TQ_CSNullability)); + } + } + } else if (auto Property = dyn_cast(D)) { + if (auto Modified = GetModified(D, Property->getType(), Nullability)) { + Property->setType(*Modified, Property->getTypeSourceInfo()); + + // Make it a property attribute if we can. + if (!isIndirectPointerType(*Modified)) + Property->setPropertyAttributes( + ObjCPropertyAttribute::kind_null_resettable); } } +} + +/// Process API notes for a variable or property. +static void ProcessAPINotes(Sema &S, Decl *D, + const api_notes::VariableInfo &Info, + VersionedInfoMetadata Metadata) { + // Type override. + applyAPINotesType(S, D, Info.getType(), Metadata); // Nullability. if (auto Nullability = Info.getNullability()) @@ -892,7 +944,8 @@ static void ProcessVersionedAPINotes( Sema &S, SpecificDecl *D, const api_notes::APINotesReader::VersionedInfo Info) { - maybeAttachUnversionedSwiftName(S, D, Info); + if (!S.captureSwiftVersionIndependentAPINotes()) + maybeAttachUnversionedSwiftName(S, D, Info); unsigned Selected = Info.getSelected().value_or(Info.size()); @@ -902,10 +955,18 @@ static void ProcessVersionedAPINotes( std::tie(Version, InfoSlice) = Info[i]; auto Active = (i == Selected) ? IsActive_t::Active : IsActive_t::Inactive; auto Replacement = IsSubstitution_t::Original; - if (Active == IsActive_t::Inactive && Version.empty()) { + + // When collection all APINotes as version-independent, + // capture all as inactive and defer to the client select the + // right one. + if (S.captureSwiftVersionIndependentAPINotes()) { + Active = IsActive_t::Inactive; + Replacement = IsSubstitution_t::Original; + } else if (Active == IsActive_t::Inactive && Version.empty()) { Replacement = IsSubstitution_t::Replacement; Version = Info[Selected].first; } + ProcessAPINotes(S, D, InfoSlice, VersionedInfoMetadata(Version, Active, Replacement)); } diff --git a/clang/test/APINotes/versioned-version-independent.m b/clang/test/APINotes/versioned-version-independent.m new file mode 100644 index 0000000000000..da8b34a1d9ba3 --- /dev/null +++ b/clang/test/APINotes/versioned-version-independent.m @@ -0,0 +1,36 @@ +// RUN: rm -rf %t && mkdir -p %t + +// Build and check the module file in version-independent mode. +// RUN: %clang_cc1 -fswift-version-independent-apinotes -fmodules -fblocks -fimplicit-module-maps -fmodules-cache-path=%t/ModulesCache/Versioned -fdisable-module-hash -fapinotes-modules -fsyntax-only -I %S/Inputs/Headers -F %S/Inputs/Frameworks %s +// RUN: %clang_cc1 -fswift-version-independent-apinotes -fmodules -fblocks -fimplicit-module-maps -fmodules-cache-path=%t/ModulesCache/Versioned -fdisable-module-hash -fapinotes-modules -I %S/Inputs/Headers -F %S/Inputs/Frameworks %s -ast-dump -ast-dump-filter 'DUMP' &> %t/VersionedKit_AST_Dump.txt +// RUN: cat %t/VersionedKit_AST_Dump.txt | FileCheck -check-prefix=CHECK-VERSIONED-DUMP %s + +#import + +// CHECK-VERSIONED-DUMP-LABEL: Dumping moveToPointDUMP +// CHECK-VERSIONED-DUMP: SwiftNameAttr {{.+}} "moveTo(x:y:)" +// CHECK-VERSIONED-DUMP-NEXT: SwiftVersionedAdditionAttr {{.+}} Implicit 3.0 +// CHECK-VERSIONED-DUMP-NEXT: SwiftNameAttr {{.+}} <> "moveTo(a:b:)" + +// CHECK-VERSIONED-DUMP-LABEL: Dumping unversionedRenameDUMP +// CHECK-VERSIONED-DUMP: SwiftNameAttr {{.+}} "unversionedRename_HEADER()" +// CHECK-VERSIONED-DUMP-NEXT: SwiftVersionedAdditionAttr {{.+}} Implicit 0 +// CHECK-VERSIONED-DUMP-NEXT: SwiftNameAttr {{.+}} "unversionedRename_NOTES()" + +// CHECK-VERSIONED-DUMP-LABEL: Dumping TestGenericDUMP +// CHECK-VERSIONED-DUMP: SwiftVersionedAdditionAttr {{.+}} Implicit 3.0 +// CHECK-VERSIONED-DUMP-NEXT: SwiftImportAsNonGenericAttr {{.+}} <> + +// CHECK-VERSIONED-DUMP: Swift3RenamedOnlyDUMP +// CHECK-VERSIONED-DUMP: SwiftVersionedAdditionAttr {{.+}} Implicit 3.0 +// CHECK-VERSIONED-DUMP-NEXT: SwiftNameAttr {{.+}} "SpecialSwift3Name" + +// CHECK-VERSIONED-DUMP: Swift3RenamedAlsoDUMP +// CHECK-VERSIONED-DUMP: SwiftNameAttr {{.+}} "Swift4Name" +// CHECK-VERSIONED-DUMP-NEXT: SwiftVersionedAdditionAttr {{.+}} Implicit 3.0 +// CHECK-VERSIONED-DUMP-NEXT: SwiftNameAttr {{.+}} "SpecialSwift3Also" + +// CHECK-VERSIONED-DUMP: Swift4RenamedDUMP +// CHECK-VERSIONED-DUMP: SwiftVersionedAdditionAttr {{.+}} Implicit 4 +// CHECK-VERSIONED-DUMP-NEXT: SwiftNameAttr {{.+}} "SpecialSwift4Name" +