当前位置:网站首页>How to generate code using the Swift Package plugin

How to generate code using the Swift Package plugin

2022-08-10 16:41:00 Yisuyun

如何使用Swift Package插件生成代码

这篇文章主要介绍“如何使用Swift Package插件生成代码”,在日常操作中,相信很多人在如何使用Swift PackageThe existence plug-in code generation doubt,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”如何使用Swift Package插件生成代码”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

前言

不久前,I am working in the development of a new service,该服务由 Swift Package 组成,该 Package 公开了一个类似于Decodable协议,For the rest of the application we use.事实上,This agreement is fromDecodableInherited itself,看起来像这样:

Fetchable.swit

protocol Fetchable: Decodable, Equatable {}

新的 package To meetFetchableThe type of to try from the remote or cacheJSONData block decoding them.

Because the service is very important to the proper operation of the application,作为这项工作的一部分,We want to make sure that's always there fail safe( fail-safe).因此,We let the application comes with a spareJSON文件,If the remote and cache data decoding failure,Will use this file,来保证程序的正常运行.

无论如何,We need to meetFetchableNew type correctly decoded from backup data.然而,有一个问题,Sometimes it's hard to find spareJSONFile or the model itself if there is any mistake,Because the decoding error will occur at run time,And only on a visit to some screen/Occurs when function.

In order to let us to send to us the code more confidence,We added some unit tests,Try to according to our attached spareJSONDecoding inFetchableAgreement of each model.These will make us inCIHave an early indication,Backup data or model errors exist in the,If all tests pass,我们将确定,Once we have released a new service,It is always fail safe function.

Let us write the manual test,But we soon realized that this solution is an extension of,Because as more and more inFetchableThe type of agreement is added,We introduced a number of code copy,And someone might be eventually forget write the tests for a specific function.

We considered the process automation,But due to the nature of our code base,我们遇到了一些问题,The code base highly modular,混合了Xcode项目和Swift Package.Some architectural decision also means that we must collect a lot of symbol information,To get the right type of generating test.

What makes me focus on the it again?

After I forgot it for a period of time,Xcode 14Announcements are allowed inXcode项目中使用 Swift Package 插件,And some of the architectural changes make extraction type information much easier,This let me have an incentive to research the problem again.

请注意,XcodeProject build tools plugin has not been released in accordance with the instructions inXcode 14 Beta 2中提供,但将在Xcode 14In a future version of offer.

图片取自 Xcode Beta 2 The release of the instructions

在过去的几周里,I have been studying how to use the software package plug-in generated unit test,在这篇文章中,I will explain to which direction to try and what it covers.

实施细节

I started a task,Creating a build tool plug-ins,与 Xcode 14 The introduction of different command plug-ins,The plug-in can run arbitrary and depends on the user input,作为SwiftPart of the package build process is running.

I know I need to create an executable file,因为 Build Tool Plug-in relied on to perform the operation.This script will completely with Swift 编写,因为这是我最熟悉的语言,And undertake the following responsibilities:

  • Scans the target directory and extract all.swift文件.Target will be recursive scanning,In order to ensure that won't miss subdirectory.

  • 使用sourcekit,或者更具体地说,SourceKitten,扫描这些.swiftFile and collect the type information.This will allow to extract conforms to theFetchableAll types of agreement,So that you can for them to write the test.

  • After get these types,生成一个带有XCTestCase的.swift文件,Containing each type of unit testing.

Let's write some code

与所有 Swift Package 一样,Introduction to the simplest method is to run on the command lineswift package init.

This creates two goals,一个是包含FetchableProtocol defines the type of and comply with the definition of the implementation code,Another is the application of plug-in generated unit test for such type of test target.

Package.swit

// swift-tools-version: 5.6// The swift-tools-version declares the minimum version of Swift required to build this package.import PackageDescriptionlet package = Package(    name: "CodeGenSample",    platforms: [.macOS(.v10_11)],    products: [        .library(            name: "CodeGenSample",            targets: ["CodeGenSample"]),    ],    dependencies: [    ],    targets: [        .target(            name: "CodeGenSample",            dependencies: []        ),        .testTarget(            name: "CodeGenSampleTests",            dependencies: ["CodeGenSample"]        )     ])

编写可执行文件

如前所述,All build tools plugin executable file is required to perform all necessary operations.

In order to help the development of the command line,Will use a few dependencies.第一个是SourceKitten——特别是其SourceKitten框架库,这是一个Swift包装器,用于帮助使用Swift代码编写sourcekit请求,The second is the rapid parameter parser,This is apple provide package,You can easily create command line tools,And faster、A safer way of analysis in the execution of the command line parameters.

在创建executableTargetAfter two dependencies and give it,Package.swift就是这个样子:

Package.swift

// swift-tools-version: 5.6// The swift-tools-version declares the minimum version of Swift required to build this package.import PackageDescriptionlet package = Package(    name: "CodeGenSample",    platforms: [.macOS(.v10_11)],    products: [        .library(            name: "CodeGenSample",            targets: ["CodeGenSample"]),    ],    dependencies: [        .package(url: "https://github.com/jpsim/SourceKitten.git", exact: "0.32.0"),        .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0")    ],    targets: [        .target(            name: "CodeGenSample",            dependencies: []        ),        .testTarget(            name: "CodeGenSampleTests",            dependencies: ["CodeGenSample"]        ),        .executableTarget(            name: "PluginExecutable",            dependencies: [                .product(name: "SourceKittenFramework", package: "SourceKitten"),                .product(name: "ArgumentParser", package: "swift-argument-parser")            ]        )     ])

The executable target need an entry point,因此,在PluginExecutableThe target of the source directory,必须创建一个名为PluginExecutable.swift的文件,Including all executable logic need to create a.

请注意,This file can follow one's inclinationsly named,I tend to be with me inPackage.swiftCreated in the target in the same way named it.

As shown in the following script to import the necessary dependencies,And creates an executable file the entry point to the(必须用@main装饰),And declared at execution time passed4个输入.

All logic and method calls are inrun函数中,This function is called an executable file run way.这是ArgumentParser语法的一部分,如果您想了解更多信息,Andy IbañezHave an article on the subject of great,Can be very helpful.

PluginExecutable.swift

import SourceKittenFrameworkimport ArgumentParserimport [email protected] PluginExecutable: ParsableCommand {    @Argument(help: "The protocol name to match")    var protocolName: String    @Argument(help: "The module's name")    var moduleName: String    @Option(help: "Directory containing the swift files")    var input: String    @Option(help: "The path where the generated files will be created")    var output: String    func run() throws {  // 1        let files = try deepSearch(URL(fileURLWithPath: input, isDirectory: true))        // 2        setenv("IN_PROCESS_SOURCEKIT", "YES", 1)        let structures = try files.map { try Structure(file: File(path: $0.path)!) }        // 3        var matchedTypes = [String]()        structures.forEach { walkTree(dictionary: $0.dictionary, acc: &matchedTypes) }        // 4        try createOutputFile(withContent: matchedTypes)    }    // ...}

Now let's focus on the aboverun方法,In order to understand what happens when the plug-in run the executable file:

  • 首先,Scans the target directory to find all of them.swift文件.This is a recursive complete,This directory will not miss.This directory path is passed as a parameter to the executable file.

  • For the last call found in each file,通过SourceKitten发出Structure请求,To find the fileSwiftThe type of code information.请注意,环境变量(IN_PROCESS_SOURCEKIT)也被设置为true.The process of the need to ensure that select the source suite version,So that it can comply with the sandbox rule of plug-ins.

XcodeAttached two versions of thesourcekit可执行文件,A version of the file in analytic process,另一个使用XPCTo the parsing process files outside the daemon sends a request.后者是macThe default version,为了能够将sourcekitUsed as part of the plugin process,Must be chosen in the process of version.This recently inSourceKittenAs environment variables to realize,Is running under the hood to usesourcekitOther executables key,例如SwiftLint.

  • Browse all the response of the last call,And scan type information to extract conforms to theFetchable协议的任何类型.

  • In the passed to the executable fileoutputSpecifies the location of the create an output file,Containing each type of unit testing.

请注意,There is no emphasis on the details of each call,But if you are interested in the implementation,包含所有代码的repo现在已经在Github上公开了!

Create the plug-in

与可执行文件一样,必须向Package.swift添加.plugin目标,And must create contains the plug-in implementation.swift文件(Plugins/SourceKitPlugin/SourceKitPlugin.swift).

Package.swift

// swift-tools-version: 5.6// The swift-tools-version declares the minimum version of Swift required to build this package.import PackageDescriptionlet package = Package(    name: "CodeGenSample",    platforms: [.macOS(.v10_11)],    products: [        .library(            name: "CodeGenSample",            targets: ["CodeGenSample"]),    ],    dependencies: [        .package(url: "https://github.com/jpsim/SourceKitten.git", exact: "0.32.0"),        .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0")    ],    targets: [        .target(            name: "CodeGenSample",            dependencies: []        ),        .testTarget(            name: "CodeGenSampleTests",            dependencies: [“CodeGenSample"],plugins: [“SourceKitPlugin”],        ),        .executableTarget(            name: "PluginExecutable",            dependencies: [                .product(name: "SourceKittenFramework", package: "SourceKitten"),                .product(name: "ArgumentParser", package: "swift-argument-parser")            ]        ),        .plugin(            name: "SourceKitPlugin",            capability: .buildTool(),            dependencies: [.target(name: "PluginExecutable")]        )     ])

The following code shows the initial implementation of plug-ins,其struct符合BuildToolPlugin的协议.The need to implement a return with a single array as the build commandscreateBuildCommands方法.

此插件使用buildCommand而不是preBuildCommand,Because it needs as part of the build process is running,Rather than run before it,So it has the opportunity to build and use it depends on the executable file.在这种情况下,支持使用buildCommand的另一点是,It will only be in the input file change run,But not every time we build targets run.

This command must offer to run an executable file name and path,This can be found in the context of the plugin:

SourceKitPlugin.swift

import [email protected] SourceKitPlugin: BuildToolPlugin {    func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {        return [            .buildCommand(                displayName: "Protocol Extraction!",                executable: try context.tool(named: "PluginExecutable").path,                arguments: [                    "FindThis",                      ,                    "--input",                      ,                    "--output",                                      ],                environment: ["IN_PROCESS_SOURCEKIT": "YES"],                outputFiles: [  ]            )        ]    }}

如上面的代码所示,There are some blank need to fill( ):

  • 提供outputPath,Used to generate unit test file.此文件可以在pluginWorkDirectory中生成,Also can be found in the context of the plugin.This directory provides read and write access and create any file will be part of the package build process.

  • Provide input path and the module name.This is the trickiest part of,These need to is the source of the target,Instead of plug-in is applied to the target——单元测试.谢天谢地,The goal of plug-in dependencies is accessible,We can get from this array the dependencies which we are interested in.The dependencies will be inside(target而不是product),It will provide its name into executable files and directories.

SourceKitPlugin.swift

import [email protected] SourceKitPlugin: BuildToolPlugin {    func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {        let outputPath = context.pluginWorkDirectory.appending(“GeneratedTests.swift”)        guard let dependencyTarget = target            .dependencies            .compactMap { dependency -> Target? in                switch dependency {                case .target(let target): return target                default: return nil                }            }            .filter { "\($0.name)Tests" == target.name  }            .first else {                Diagnostics.error("Could not get a dependency to scan!”)                return []        }        return [            .buildCommand(                displayName: "Protocol Extraction!",                executable: try context.tool(named: "PluginExecutable").path,                arguments: [                    "Fetchable",                  dependencyTarget.name,                    "--input",                    dependencyTarget.directory,                    "--output",                    outputPath                ],                environment: ["IN_PROCESS_SOURCEKIT": "YES"],                outputFiles: [outputPath]            )        ]    }}

Note the above optional sex way.If can't find in the test target dependencies 合适的 目标,则使用Diagnostics APITo forward the error back toXcode,And tell it to complete the build process.

让我们看下结果

Plug-in that is done!现在让我们在 Xcode 中运行它!为了测试这种方法,Will contain the following files are added to theCodeGenSample目标中:

CodeGenSample.swift

import Foundationprotocol Fetchable: Decodable, Equatable {}struct FeatureABlock: Fetchable {    let featureA: FeatureA    struct FeatureA: Fetchable {        let url: URL    }}enum Root {    struct RootBlock: Fetchable {        let url: URL        let areAllFeaturesEnabled: Bool    }}

请注意,The script will in structure for the first timeFetchableAgreement to stop.This means that any nested inFetchableThe type of agreement will be testing,Only external model.

Given this input and run the test on the main target,生成并运行XCTestCase,Contains meetFetchableAgreement of the two types of testing.

GeneratedTests.swift

import [email protected] import CodeGenSampleclass GeneratedTests: XCTestCase { func testFeatureABlock() {  assertCanParseFromDefaults(FeatureABlock.self) } func testRoot_RootBlock() {  assertCanParseFromDefaults(Root.RootBlock.self) }    private func assertCanParseFromDefaults<T: Fetchable>(_ type: T.Type) {        // Logic goes here...    }}

所有测试都通过了:sweat_smile::white_check_mark:而且,Although they are now not to do a lot of things,But can be extended to realize,In order to provide some sample data and aJSONDecoderInstance to each unit test for analytic.

到此,关于“如何使用Swift Package插件生成代码”的学习就结束了,希望能够解决大家的疑惑.理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

原网站

版权声明
本文为[Yisuyun]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/222/202208101617426588.html