diff --git a/clang/include/clang/InstallAPI/FileList.h b/clang/include/clang/InstallAPI/FileList.h new file mode 100644 index 0000000000000..460af003b6a0a --- /dev/null +++ b/clang/include/clang/InstallAPI/FileList.h @@ -0,0 +1,42 @@ +//===- InstallAPI/FileList.h ------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +/// +/// The JSON file list parser is used to communicate input to InstallAPI. +/// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_INSTALLAPI_FILELIST_H +#define LLVM_CLANG_INSTALLAPI_FILELIST_H + +#include "clang/Basic/Diagnostic.h" +#include "clang/Basic/FileManager.h" +#include "clang/InstallAPI/HeaderFile.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/MemoryBuffer.h" + +namespace clang { +namespace installapi { + +class FileListReader { +public: + /// Decode JSON input and append header input into destination container. + /// Headers are loaded in the order they appear in the JSON input. + /// + /// \param InputBuffer JSON input data. + /// \param Destination Container to load headers into. + static llvm::Error + loadHeaders(std::unique_ptr InputBuffer, + HeaderSeq &Destination); + + FileListReader() = delete; +}; + +} // namespace installapi +} // namespace clang + +#endif // LLVM_CLANG_INSTALLAPI_FILELIST_H diff --git a/clang/include/clang/InstallAPI/HeaderFile.h b/clang/include/clang/InstallAPI/HeaderFile.h new file mode 100644 index 0000000000000..6ccd944f8b01b --- /dev/null +++ b/clang/include/clang/InstallAPI/HeaderFile.h @@ -0,0 +1,72 @@ +//===- InstallAPI/HeaderFile.h ----------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +/// +/// Representations of a library's headers for InstallAPI. +/// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_INSTALLAPI_HEADERFILE_H +#define LLVM_CLANG_INSTALLAPI_HEADERFILE_H + +#include "clang/Basic/LangStandard.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Regex.h" +#include +#include + +namespace clang::installapi { +enum class HeaderType { + /// Represents declarations accessible to all clients. + Public, + /// Represents declarations accessible to a disclosed set of clients. + Private, + /// Represents declarations only accessible as implementation details to the + /// input library. + Project, +}; + +class HeaderFile { + /// Full input path to header. + std::string FullPath; + /// Access level of header. + HeaderType Type; + /// Expected way header will be included by clients. + std::string IncludeName; + /// Supported language mode for header. + std::optional Language; + +public: + HeaderFile(StringRef FullPath, HeaderType Type, + StringRef IncludeName = StringRef(), + std::optional Language = std::nullopt) + : FullPath(FullPath), Type(Type), IncludeName(IncludeName), + Language(Language) {} + + static llvm::Regex getFrameworkIncludeRule(); + + bool operator==(const HeaderFile &Other) const { + return std::tie(Type, FullPath, IncludeName, Language) == + std::tie(Other.Type, Other.FullPath, Other.IncludeName, + Other.Language); + } +}; + +/// Assemble expected way header will be included by clients. +/// As in what maps inside the brackets of `#include ` +/// For example, +/// "/System/Library/Frameworks/Foo.framework/Headers/Foo.h" returns +/// "Foo/Foo.h" +/// +/// \param FullPath Path to the header file which includes the library +/// structure. +std::optional createIncludeHeaderName(const StringRef FullPath); +using HeaderSeq = std::vector; + +} // namespace clang::installapi + +#endif // LLVM_CLANG_INSTALLAPI_HEADERFILE_H diff --git a/clang/lib/ExtractAPI/CMakeLists.txt b/clang/lib/ExtractAPI/CMakeLists.txt index 2b6a5b7273f55..f508ffc862cb5 100644 --- a/clang/lib/ExtractAPI/CMakeLists.txt +++ b/clang/lib/ExtractAPI/CMakeLists.txt @@ -16,5 +16,6 @@ add_clang_library(clangExtractAPI clangBasic clangFrontend clangIndex + clangInstallAPI clangLex ) diff --git a/clang/lib/ExtractAPI/ExtractAPIConsumer.cpp b/clang/lib/ExtractAPI/ExtractAPIConsumer.cpp index fd62d841197d9..275f49be22e15 100644 --- a/clang/lib/ExtractAPI/ExtractAPIConsumer.cpp +++ b/clang/lib/ExtractAPI/ExtractAPIConsumer.cpp @@ -30,6 +30,7 @@ #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/FrontendOptions.h" #include "clang/Frontend/MultiplexConsumer.h" +#include "clang/InstallAPI/HeaderFile.h" #include "clang/Lex/MacroInfo.h" #include "clang/Lex/PPCallbacks.h" #include "clang/Lex/Preprocessor.h" @@ -61,9 +62,6 @@ std::optional getRelativeIncludeName(const CompilerInstance &CI, "CompilerInstance does not have a FileNamager!"); using namespace llvm::sys; - // Matches framework include patterns - const llvm::Regex Rule("/(.+)\\.framework/(.+)?Headers/(.+)"); - const auto &FS = CI.getVirtualFileSystem(); SmallString<128> FilePath(File.begin(), File.end()); @@ -147,7 +145,8 @@ std::optional getRelativeIncludeName(const CompilerInstance &CI, // include name `` if (Entry.IsFramework) { SmallVector Matches; - Rule.match(File, &Matches); + clang::installapi::HeaderFile::getFrameworkIncludeRule().match( + File, &Matches); // Returned matches are always in stable order. if (Matches.size() != 4) return std::nullopt; diff --git a/clang/lib/InstallAPI/CMakeLists.txt b/clang/lib/InstallAPI/CMakeLists.txt index 1476b737c5e61..6c9cb4b559f67 100644 --- a/clang/lib/InstallAPI/CMakeLists.txt +++ b/clang/lib/InstallAPI/CMakeLists.txt @@ -5,6 +5,8 @@ set(LLVM_LINK_COMPONENTS add_clang_library(clangInstallAPI Context.cpp + FileList.cpp + HeaderFile.cpp LINK_LIBS clangAST diff --git a/clang/lib/InstallAPI/FileList.cpp b/clang/lib/InstallAPI/FileList.cpp new file mode 100644 index 0000000000000..baa524db5d7f8 --- /dev/null +++ b/clang/lib/InstallAPI/FileList.cpp @@ -0,0 +1,184 @@ +//===- FileList.cpp ---------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/InstallAPI/FileList.h" +#include "clang/Basic/DiagnosticFrontend.h" +#include "clang/InstallAPI/FileList.h" +#include "llvm/ADT/StringSwitch.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/JSON.h" +#include "llvm/TextAPI/TextAPIError.h" +#include + +// clang-format off +/* +InstallAPI JSON Input Format specification. + +{ + "headers" : [ # Required: Key must exist. + { # Optional: May contain 0 or more header inputs. + "path" : "/usr/include/mach-o/dlfn.h", # Required: Path should point to destination + # location where applicable. + "type" : "public", # Required: Maps to HeaderType for header. + "language": "c++" # Optional: Language mode for header. + } + ], + "version" : "3" # Required: Version 3 supports language mode + & project header input. +} +*/ +// clang-format on + +using namespace llvm; +using namespace llvm::json; +using namespace llvm::MachO; +using namespace clang::installapi; + +namespace { +class Implementation { +private: + Expected parseString(const Object *Obj, StringRef Key, + StringRef Error); + Expected parsePath(const Object *Obj); + Expected parseType(const Object *Obj); + std::optional parseLanguage(const Object *Obj); + Error parseHeaders(Array &Headers); + +public: + std::unique_ptr InputBuffer; + unsigned Version; + HeaderSeq HeaderList; + + Error parse(StringRef Input); +}; + +Expected +Implementation::parseString(const Object *Obj, StringRef Key, StringRef Error) { + auto Str = Obj->getString(Key); + if (!Str) + return make_error(Error, inconvertibleErrorCode()); + return *Str; +} + +Expected Implementation::parseType(const Object *Obj) { + auto TypeStr = + parseString(Obj, "type", "required field 'type' not specified"); + if (!TypeStr) + return TypeStr.takeError(); + + if (*TypeStr == "public") + return HeaderType::Public; + else if (*TypeStr == "private") + return HeaderType::Private; + else if (*TypeStr == "project" && Version >= 2) + return HeaderType::Project; + + return make_error(TextAPIErrorCode::InvalidInputFormat, + "unsupported header type"); +} + +Expected Implementation::parsePath(const Object *Obj) { + auto Path = parseString(Obj, "path", "required field 'path' not specified"); + if (!Path) + return Path.takeError(); + + return *Path; +} + +std::optional +Implementation::parseLanguage(const Object *Obj) { + auto Language = Obj->getString("language"); + if (!Language) + return std::nullopt; + + return StringSwitch(*Language) + .Case("c", clang::Language::C) + .Case("c++", clang::Language::CXX) + .Case("objective-c", clang::Language::ObjC) + .Case("objective-c++", clang::Language::ObjCXX) + .Default(clang::Language::Unknown); +} + +Error Implementation::parseHeaders(Array &Headers) { + for (const auto &H : Headers) { + auto *Obj = H.getAsObject(); + if (!Obj) + return make_error("expect a JSON object", + inconvertibleErrorCode()); + auto Type = parseType(Obj); + if (!Type) + return Type.takeError(); + auto Path = parsePath(Obj); + if (!Path) + return Path.takeError(); + auto Language = parseLanguage(Obj); + + StringRef PathStr = *Path; + if (*Type == HeaderType::Project) { + HeaderList.emplace_back( + HeaderFile{PathStr, *Type, /*IncludeName=*/"", Language}); + continue; + } + auto IncludeName = createIncludeHeaderName(PathStr); + HeaderList.emplace_back(PathStr, *Type, + IncludeName.has_value() ? IncludeName.value() : "", + Language); + } + + return Error::success(); +} + +Error Implementation::parse(StringRef Input) { + auto Val = json::parse(Input); + if (!Val) + return Val.takeError(); + + auto *Root = Val->getAsObject(); + if (!Root) + return make_error("not a JSON object", + inconvertibleErrorCode()); + + auto VersionStr = Root->getString("version"); + if (!VersionStr) + return make_error(TextAPIErrorCode::InvalidInputFormat, + "required field 'version' not specified"); + if (VersionStr->getAsInteger(10, Version)) + return make_error(TextAPIErrorCode::InvalidInputFormat, + "invalid version number"); + + if (Version < 1 || Version > 3) + return make_error(TextAPIErrorCode::InvalidInputFormat, + "unsupported version"); + + // Not specifying any header files should be atypical, but valid. + auto Headers = Root->getArray("headers"); + if (!Headers) + return Error::success(); + + Error Err = parseHeaders(*Headers); + if (Err) + return Err; + + return Error::success(); +} +} // namespace + +llvm::Error +FileListReader::loadHeaders(std::unique_ptr InputBuffer, + HeaderSeq &Destination) { + Implementation Impl; + Impl.InputBuffer = std::move(InputBuffer); + + if (llvm::Error Err = Impl.parse(Impl.InputBuffer->getBuffer())) + return Err; + + Destination.reserve(Destination.size() + Impl.HeaderList.size()); + llvm::move(Impl.HeaderList, std::back_inserter(Destination)); + + return Error::success(); +} diff --git a/clang/lib/InstallAPI/HeaderFile.cpp b/clang/lib/InstallAPI/HeaderFile.cpp new file mode 100644 index 0000000000000..c2d8372741ee0 --- /dev/null +++ b/clang/lib/InstallAPI/HeaderFile.cpp @@ -0,0 +1,37 @@ +//===- HeaderFile.cpp ------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/InstallAPI/HeaderFile.h" + +using namespace llvm; +namespace clang::installapi { + +llvm::Regex HeaderFile::getFrameworkIncludeRule() { + return llvm::Regex("/(.+)\\.framework/(.+)?Headers/(.+)"); +} + +std::optional createIncludeHeaderName(const StringRef FullPath) { + // Headers in usr(/local)*/include. + std::string Pattern = "/include/"; + auto PathPrefix = FullPath.find(Pattern); + if (PathPrefix != StringRef::npos) { + PathPrefix += Pattern.size(); + return FullPath.drop_front(PathPrefix).str(); + } + + // Framework Headers. + SmallVector Matches; + HeaderFile::getFrameworkIncludeRule().match(FullPath, &Matches); + // Returned matches are always in stable order. + if (Matches.size() != 4) + return std::nullopt; + + return Matches[1].drop_front(Matches[1].rfind('/') + 1).str() + "/" + + Matches[3].str(); +} +} // namespace clang::installapi diff --git a/clang/unittests/CMakeLists.txt b/clang/unittests/CMakeLists.txt index f4e4f585bdd80..37ca3107b5477 100644 --- a/clang/unittests/CMakeLists.txt +++ b/clang/unittests/CMakeLists.txt @@ -51,5 +51,6 @@ endif() add_subdirectory(DirectoryWatcher) add_subdirectory(Rename) add_subdirectory(Index) +add_subdirectory(InstallAPI) add_subdirectory(Serialization) add_subdirectory(Support) diff --git a/clang/unittests/InstallAPI/CMakeLists.txt b/clang/unittests/InstallAPI/CMakeLists.txt new file mode 100644 index 0000000000000..4255001ff51f1 --- /dev/null +++ b/clang/unittests/InstallAPI/CMakeLists.txt @@ -0,0 +1,11 @@ +add_clang_unittest(InstallAPITests + HeaderFileTest.cpp + FileListTest.cpp + ) + +clang_target_link_libraries(InstallAPITests + PRIVATE + clangInstallAPI + ) + +target_link_libraries(InstallAPITests PRIVATE LLVMTestingSupport) diff --git a/clang/unittests/InstallAPI/FileListTest.cpp b/clang/unittests/InstallAPI/FileListTest.cpp new file mode 100644 index 0000000000000..822e3b202bc9d --- /dev/null +++ b/clang/unittests/InstallAPI/FileListTest.cpp @@ -0,0 +1,146 @@ +//===- unittests/InstallAPI/FileList.cpp - File List Tests ---------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/InstallAPI/FileList.h" +#include "clang/InstallAPI/HeaderFile.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Testing/Support/Error.h" +#include "gtest/gtest.h" + +using namespace llvm; +using namespace clang::installapi; + +namespace FileListTests { + +static inline void testValidFileList(std::string Input, HeaderSeq &Expected) { + auto InputBuf = MemoryBuffer::getMemBuffer(Input); + HeaderSeq Headers; + llvm::Error Err = FileListReader::loadHeaders(std::move(InputBuf), Headers); + ASSERT_THAT_ERROR(std::move(Err), Succeeded()); + + EXPECT_EQ(Expected.size(), Headers.size()); + EXPECT_EQ(Headers, Expected); +} + +TEST(FileList, Version3) { + static const char Input[] = R"({ + "version" : "3", + "headers" : [ + { + "type" : "public", + "path" : "/tmp/dst/usr/include/foo.h", + "language" : "objective-c" + }, + { + "type" : "private", + "path" : "/tmp/dst/usr/local/include/bar.h", + "language" : "objective-c++" + }, + { + "type" : "project", + "path" : "/tmp/src/baz.h" + } + ] + })"; + + HeaderSeq Expected = { + {"/tmp/dst/usr/include/foo.h", HeaderType::Public, "foo.h", + clang::Language::ObjC}, + {"/tmp/dst/usr/local/include/bar.h", HeaderType::Private, "bar.h", + clang::Language::ObjCXX}, + {"/tmp/src/baz.h", HeaderType::Project, "", std::nullopt}}; + + testValidFileList(Input, Expected); +} + +TEST(FileList, Version1) { + static const char Input[] = R"({ + "version" : "1", + "headers" : [ + { + "type" : "public", + "path" : "/usr/include/foo.h" + }, + { + "type" : "private", + "path" : "/usr/local/include/bar.h" + } + ] + })"; + + HeaderSeq Expected = { + {"/usr/include/foo.h", HeaderType::Public, "foo.h", std::nullopt}, + {"/usr/local/include/bar.h", HeaderType::Private, "bar.h", std::nullopt}, + }; + + testValidFileList(Input, Expected); +} + +TEST(FileList, Version2) { + static const auto Input = R"({ + "version" : "2", + "headers" : [ + { + "type" : "public", + "path" : "/usr/include/foo.h" + }, + { + "type" : "project", + "path" : "src/bar.h" + } + ] + })"; + HeaderSeq Expected = { + {"/usr/include/foo.h", HeaderType::Public, "foo.h", std::nullopt}, + {"src/bar.h", HeaderType::Project, "", std::nullopt}, + }; + + testValidFileList(Input, Expected); +} + +TEST(FileList, MissingVersion) { + static const char Input[] = R"({ + "headers" : [ + { + "type" : "public", + "path" : "/usr/include/foo.h" + }, + { + "type" : "private", + "path" : "/usr/local/include/bar.h" + } + ] + })"; + auto InputBuf = MemoryBuffer::getMemBuffer(Input); + HeaderSeq Headers; + llvm::Error Err = FileListReader::loadHeaders(std::move(InputBuf), Headers); + EXPECT_THAT_ERROR( + std::move(Err), + FailedWithMessage( + "invalid input format: required field 'version' not specified\n")); +} + +TEST(FileList, InvalidTypes) { + static const char Input[] = R"({ + "version" : "1", + "headers" : [ + { + "type" : "project", + "path" : "/usr/include/foo.h" + } + ] + })"; + auto InputBuf = MemoryBuffer::getMemBuffer(Input); + HeaderSeq Headers; + llvm::Error Err = FileListReader::loadHeaders(std::move(InputBuf), Headers); + EXPECT_THAT_ERROR( + std::move(Err), + FailedWithMessage("invalid input format: unsupported header type\n")); +} +} // namespace FileListTests diff --git a/clang/unittests/InstallAPI/HeaderFileTest.cpp b/clang/unittests/InstallAPI/HeaderFileTest.cpp new file mode 100644 index 0000000000000..985220c72937d --- /dev/null +++ b/clang/unittests/InstallAPI/HeaderFileTest.cpp @@ -0,0 +1,92 @@ +//===- unittests/InstallAPI/HeaderFile.cpp - HeaderFile Test --------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/InstallAPI/HeaderFile.h" +#include "gtest/gtest.h" + +using namespace llvm; +using namespace clang::installapi; + +namespace HeaderFileTests { + +TEST(HeaderFile, FrameworkIncludes) { + const char *Path = "/System/Library/Frameworks/Foo.framework/Headers/Foo.h"; + std::optional IncludeName = createIncludeHeaderName(Path); + EXPECT_TRUE(IncludeName.has_value()); + EXPECT_STREQ(IncludeName.value().c_str(), "Foo/Foo.h"); + + Path = "/System/Library/Frameworks/Foo.framework/Frameworks/Bar.framework/" + "Headers/SimpleBar.h"; + IncludeName = createIncludeHeaderName(Path); + EXPECT_TRUE(IncludeName.has_value()); + EXPECT_STREQ(IncludeName.value().c_str(), "Bar/SimpleBar.h"); + + Path = "/tmp/Foo.framework/Versions/A/Headers/SimpleFoo.h"; + IncludeName = createIncludeHeaderName(Path); + EXPECT_TRUE(IncludeName.has_value()); + EXPECT_STREQ(IncludeName.value().c_str(), "Foo/SimpleFoo.h"); + + Path = "/System/Library/PrivateFrameworks/Foo.framework/Headers/Foo.h"; + IncludeName = createIncludeHeaderName(Path); + EXPECT_TRUE(IncludeName.has_value()); + EXPECT_STREQ(IncludeName.value().c_str(), "Foo/Foo.h"); + + Path = "/AppleInternal/Developer/Library/Frameworks/" + "HelloFramework.framework/Headers/HelloFramework.h"; + IncludeName = createIncludeHeaderName(Path); + EXPECT_TRUE(IncludeName.has_value()); + EXPECT_STREQ(IncludeName.value().c_str(), "HelloFramework/HelloFramework.h"); + + Path = "/tmp/BuildProducts/Foo.framework/Versions/A/" + "PrivateHeaders/Foo+Private.h"; + IncludeName = createIncludeHeaderName(Path); + EXPECT_TRUE(IncludeName.has_value()); + EXPECT_STREQ(IncludeName.value().c_str(), "Foo/Foo+Private.h"); + + Path = "/Applications/Xcode.app/Contents/Developer/SDKS/MacOS.sdk/System/" + "Library/Frameworks/Foo.framework/PrivateHeaders/Foo_Private.h"; + IncludeName = createIncludeHeaderName(Path); + EXPECT_TRUE(IncludeName.has_value()); + EXPECT_STREQ(IncludeName.value().c_str(), "Foo/Foo_Private.h"); + + Path = + "/System/Library/PrivateFrameworks/Foo.framework/PrivateHeaders/Foo.hpp"; + IncludeName = createIncludeHeaderName(Path); + EXPECT_TRUE(IncludeName.has_value()); + EXPECT_STREQ(IncludeName.value().c_str(), "Foo/Foo.hpp"); + + Path = "/Applications/Xcode.app/Contents/Developer/SDKS/MacOS.sdk/System/" + "Library/Frameworks/Foo.framework/Headers/BarDir/Bar.h"; + IncludeName = createIncludeHeaderName(Path); + EXPECT_TRUE(IncludeName.has_value()); + EXPECT_STREQ(IncludeName.value().c_str(), "Foo/BarDir/Bar.h"); +} + +TEST(HeaderFile, DylibIncludes) { + const char *Path = "/usr/include/foo.h"; + std::optional IncludeName = createIncludeHeaderName(Path); + EXPECT_TRUE(IncludeName.has_value()); + EXPECT_STREQ(IncludeName.value().c_str(), "foo.h"); + + Path = "/tmp/BuildProducts/usr/include/a/A.h"; + IncludeName = createIncludeHeaderName(Path); + EXPECT_TRUE(IncludeName.has_value()); + EXPECT_STREQ(IncludeName.value().c_str(), "a/A.h"); + + Path = "/Applications/Xcode.app/Contents/Developer/SDKS/MacOS.sdk/" + "usr/include/simd/types.h"; + IncludeName = createIncludeHeaderName(Path); + EXPECT_TRUE(IncludeName.has_value()); + EXPECT_STREQ(IncludeName.value().c_str(), "simd/types.h"); + + Path = "/usr/local/include/hidden/A.h"; + IncludeName = createIncludeHeaderName(Path); + EXPECT_TRUE(IncludeName.has_value()); + EXPECT_STREQ(IncludeName.value().c_str(), "hidden/A.h"); +} +} // namespace HeaderFileTests