💻
ExistOrLive' Gitbook
  • README
  • ReadMe
  • 开发语言学习
    • Python
      • 0.概述
      • 1.基本数据类型
      • 2.变量
      • 3.函数
      • 4.运算符
      • 5.字符串
      • 6.列表
      • 7.元组
      • 8.集合
      • 9.字典
      • 10.流程控制
      • 12.函数
      • 13.模块
      • 14.输入输出格式化
      • 15.面向对象
    • Swift
      • 1.基础部分
        • 常量和变量以及基本类型_1
        • 常量和变量以及基本类型_2
      • 2.基本运算符
        • 基本运算符
      • 3.字符串和字符
        • 字符串
        • 字符串操作
      • 4.集合类型
        • 概述
        • Array
        • Set
        • Dictionary
      • 6.函数
        • 函数
        • 函数返回值
        • 函数类型
      • 7.闭包
        • 闭包
        • 闭包表达式
        • 尾随闭包
        • 捕获变量或常量
        • 闭包是引用类型
        • 逃逸闭包
        • 自动闭包
      • 8.枚举
        • 枚举
        • 枚举与switch语句
      • 9.类和结构体
        • 类和结构体
        • 属性
          • 属性
          • 属性观察器
          • 属性包装器
          • 全局变量和局部变量
          • 类属性
        • 方法
    • Shell
      • Shell变量和基本类型
      • Shell函数
      • Shell基本运算符
      • Shell脚本的参数
      • Shell流程控制
      • Shell输入输出
      • Shell文件包含
  • iOS
    • UI
      • UIKit
        • UIView
          • UIView
          • drawRect
        • UITableView
          • UITableView
          • UITableView的加载过程
  • 学习开发工具
    • 静态分析
      • OCLint
      • infer
      • SwiftLint
    • iOS构建工具
      • fastlane
        • fastlane
        • fastlane的安装
        • fastlane一键打包
        • fastlane证书管理
    • Cocoapods
      • 安装pod库-podfile
      • 创建pod库-podspec
  • 开源库
    • Tool
      • Swiftlint源码学习
      • 利用Swiftlint自定义规则实现自动化code review
由 GitBook 提供支持
在本页
  • 1. 项目结构
  • 1.1 Package.swift
  • 1.2 代码结构
  • 2. SwiftLint的lint流程
  • 3. Rule
  • 3.1 SourceKitFreeRule
  • 3.2 ASTRule
  • 3.3 AnalyzerRule
  • 4. SourceKit相关的工具
  • 参考文档

这有帮助吗?

在GitHub上编辑
  1. 开源库
  2. Tool

Swiftlint源码学习

上一页Tool下一页利用Swiftlint自定义规则实现自动化code review

最后更新于7个月前

这有帮助吗?

#swiftlint #开源库

是统一Swift代码风格的工具,本身就是使用Swift语言编写。Swiftlint 使用 来组织源代码,可以直接用 Xcode打开。

1. 项目结构

1.1 Package.swift

产物

  • swiftlint 可执行文件

  • SwiftLintFramework 库

依赖

关于Swift Package Manager 请参考

1.2 代码结构

- swiftlint
   - main 
   - Commands                  // 命令的定义 
   - Extensions
   - Helpers 
      - LintOrAnalyzeCommand   // 对主流程的封装(查找swift文件,读取规则的配置,调用rule的检查方法)

- SwiftLintFramework
   - Rules                     // rule的定义
   - Documentation
   - Reporters                
   - Protocols
   - Models
   - Helpers
   - Extensions

2. SwiftLint的lint流程

// 1. 命令的解析 
// 2. 查询出所有的Swift文件,及文件对应的Swiftlint规格及配置
// 3. 利用规则扫描swift文件
// 4. 输出表格 

swiflint lint  
  1. 命令的解析

// ParsableCommand.run()
Lint.Run()
struct LintOrAnalyzeOptions {
    let mode: LintOrAnalyzeMode
    let paths: [String]
    let useSTDIN: Bool
    let configurationFiles: [String]
    let strict: Bool
    let lenient: Bool
    let forceExclude: Bool
    let useExcludingByPrefix: Bool
    let useScriptInputFiles: Bool
    let benchmark: Bool
    let reporter: String?
    let quiet: Bool
    let cachePath: String?
    let ignoreCache: Bool
    let enableAllRules: Bool
    let autocorrect: Bool
    let format: Bool
    let compilerLogPath: String?
    let compileCommands: String?
    let inProcessSourcekit: Bool
} 
  1. 查询出所有的Swift文件,及文件对应的Swiftlint配置及规则, 返回 [CollectedLinter]

LintOrAnalyzeCommand.run(_ options: LintOrAnalyzeOptions)
public struct CollectedLinter {
    /// The file to lint with this linter.
    public let file: SwiftLintFile
    private let rules: [Rule]
    private let cache: LinterCache?
    private let configuration: Configuration
    private let compilerArguments: [String]
}
  1. 利用规则扫描swift文件, 返回 [StyleViolation]

Rules.validate(file: SwiftLintFile) -> [StyleViolation]
public struct StyleViolation: CustomStringConvertible, Equatable, Codable {
    /// The identifier of the rule that generated this violation.
    public let ruleIdentifier: String

    /// The description of the rule that generated this violation.
    public let ruleDescription: String

    /// The name of the rule that generated this violation.
    public let ruleName: String

    /// The severity of this violation.
    public private(set) var severity: ViolationSeverity

    /// The location of this violation.
    public private(set) var location: Location

    /// The justification for this violation.
    public let reason: String

    /// A printable description for this violation.
    public var description: String {
        return XcodeReporter.generateForSingleViolation(self)
    }
  1. 按照指定格式输出表格

Reporter.report(violations: [StyleViolation], realtimeCondition: Bool)

3. Rule

/// An executable value that can identify issues (violations) in Swift source code.
public protocol Rule {

    static var description: RuleDescription { get }
    
    var configurationDescription: String { get }
    
    init()
    
    init(configuration: Any) throws

    func validate(file: SwiftLintFile, compilerArguments: [String]) -> [StyleViolation]

    func validate(file: SwiftLintFile) -> [StyleViolation]

    func isEqualTo(_ rule: Rule) -> Bool

    func collectInfo(for file: SwiftLintFile, into storage: RuleStorage, compilerArguments: [String])

    /// Executes the rule on a file after collecting file info for all files and returns any violations to the rule's
    /// expectations.
    ///
    /// - note: This function is called by the linter and is always implemented in extensions.
    ///
    /// - parameter file:              The file for which to execute the rule.
    /// - parameter storage:           The storage object containing all collected info.
    /// - parameter compilerArguments: The compiler arguments needed to compile this file.
    ///
    /// - returns: All style violations to the rule's expectations.
    func validate(file: SwiftLintFile, using storage: RuleStorage, compilerArguments: [String]) -> [StyleViolation]
}

3.1 SourceKitFreeRule

不需要AST抽象语法树,直接通过文本查询或正则匹配实现的Rule

public struct LeadingWhitespaceRule: CorrectableRule, ConfigurationProviderRule, SourceKitFreeRule {

    public func validate(file: SwiftLintFile) -> [StyleViolation] {
        let countOfLeadingWhitespace = file.contents.countOfLeadingCharacters(in: .whitespacesAndNewlines)
        if countOfLeadingWhitespace == 0 {
            return []
        }

        let reason = "File shouldn't start with whitespace: " +
                     "currently starts with \(countOfLeadingWhitespace) whitespace characters"

        return [StyleViolation(ruleDescription: Self.description,
                               severity: configuration.severity,
                               location: Location(file: file.path, line: 1),
                               reason: reason)]
    }
} 

 // 检查文件开头的空格数量 
    public func countOfLeadingCharacters(in characterSet: CharacterSet) -> Int {
        let characterSet = characterSet.bridge()
        var count = 0
        for char in utf16 {
            if !characterSet.characterIsMember(char) {
                break
            }
            count += 1
        }
        return count
    }

3.2 ASTRule

需要抽象语法树配合的Rule, 抽象语法树通过sourcekit来获取

import Foundation

let array = [1,2,3]

let array2 = array.map { return $0 }

通过 sourcekitten structure --file main.swift获取AST

{
  "key.diagnostic_stage" : "source.diagnostic.stage.swift.parse",
  "key.length" : 74,
  "key.offset" : 0,
  "key.substructure" : [
    {
      "key.accessibility" : "source.lang.swift.accessibility.internal",
      "key.kind" : "source.lang.swift.decl.var.global",
      "key.length" : 19,
      "key.name" : "array",
      "key.namelength" : 5,
      "key.nameoffset" : 23,
      "key.offset" : 19
    },
    {
      "key.bodylength" : 5,
      "key.bodyoffset" : 32,
      "key.elements" : [
        {
          "key.kind" : "source.lang.swift.structure.elem.expr",
          "key.length" : 1,
          "key.offset" : 32
        },
        {
          "key.kind" : "source.lang.swift.structure.elem.expr",
          "key.length" : 1,
          "key.offset" : 34
        },
        {
          "key.kind" : "source.lang.swift.structure.elem.expr",
          "key.length" : 1,
          "key.offset" : 36
        }
      ],
      "key.kind" : "source.lang.swift.expr.array",
      "key.length" : 7,
      "key.namelength" : 0,
      "key.nameoffset" : 0,
      "key.offset" : 31
    },
    {
      "key.accessibility" : "source.lang.swift.accessibility.internal",
      "key.kind" : "source.lang.swift.decl.var.global",
      "key.length" : 29,
      "key.name" : "array2",
      "key.namelength" : 6,
      "key.nameoffset" : 44,
      "key.offset" : 40
    },
    {
      "key.bodylength" : 4,
      "key.bodyoffset" : 64,
      "key.kind" : "source.lang.swift.expr.call",
      "key.length" : 16,
      "key.name" : "array.map",
      "key.namelength" : 9,
      "key.nameoffset" : 53,
      "key.offset" : 53,
      "key.substructure" : [
        {
          "key.bodylength" : 4,
          "key.bodyoffset" : 64,
          "key.kind" : "source.lang.swift.expr.closure",
          "key.length" : 6,
          "key.namelength" : 0,
          "key.nameoffset" : 0,
          "key.offset" : 63,
          "key.substructure" : [
            {
              "key.bodylength" : 4,
              "key.bodyoffset" : 64,
              "key.kind" : "source.lang.swift.stmt.brace",
              "key.length" : 6,
              "key.namelength" : 0,
              "key.nameoffset" : 0,
              "key.offset" : 63
            }
          ]
        }
      ]
    }
}

通过 sourcekitten syntax --file main.swift获取Token list

[
  {
    "length" : 6,
    "offset" : 0,
    "type" : "source.lang.swift.syntaxtype.keyword"
  },
  {
    "length" : 10,
    "offset" : 7,
    "type" : "source.lang.swift.syntaxtype.identifier"
  },
  {
    "length" : 3,
    "offset" : 19,
    "type" : "source.lang.swift.syntaxtype.keyword"
  },
  {
    "length" : 5,
    "offset" : 23,
    "type" : "source.lang.swift.syntaxtype.identifier"
  },
  {
    "length" : 1,
    "offset" : 32,
    "type" : "source.lang.swift.syntaxtype.number"
  },
  {
    "length" : 1,
    "offset" : 34,
    "type" : "source.lang.swift.syntaxtype.number"
  },
  {
    "length" : 1,
    "offset" : 36,
    "type" : "source.lang.swift.syntaxtype.number"
  },
  {
    "length" : 3,
    "offset" : 40,
    "type" : "source.lang.swift.syntaxtype.keyword"
  },
  {
    "length" : 6,
    "offset" : 44,
    "type" : "source.lang.swift.syntaxtype.identifier"
  },
  {
    "length" : 5,
    "offset" : 53,
    "type" : "source.lang.swift.syntaxtype.identifier"
  },
  {
    "length" : 3,
    "offset" : 59,
    "type" : "source.lang.swift.syntaxtype.identifier"
  },
  {
    "length" : 6,
    "offset" : 65,
    "type" : "source.lang.swift.syntaxtype.keyword"
  },
  {
    "length" : 2,
    "offset" : 72,
    "type" : "source.lang.swift.syntaxtype.identifier"
  }
]

   // 结合抽象语法树进行分析 
  public func validate(file: SwiftLintFile, kind: SwiftExpressionKind,
                         dictionary: SourceKittenDictionary) -> [StyleViolation] {

        guard kind == .call, let name = dictionary.name, name.hasSuffix(".map"),
            let bodyOffset = dictionary.bodyOffset,
            let bodyLength = dictionary.bodyLength,
            let bodyRange = dictionary.bodyByteRange,
            let nameOffset = dictionary.nameOffset,
            let nameLength = dictionary.nameLength,
            let offset = dictionary.offset else {
                return []
        }

        let tokens = file.syntaxMap.tokens(inByteRange: bodyRange).filter { token in
            guard let kind = token.kind else {
                return false
            }

            return !SyntaxKind.commentKinds.contains(kind)
        }

        guard let firstToken = tokens.first,
            case let nameEndPosition = nameOffset + nameLength,
            isClosureParameter(firstToken: firstToken, nameEndPosition: nameEndPosition, file: file),
            isShortParameterStyleViolation(file: file, tokens: tokens) ||
            isParameterStyleViolation(file: file, dictionary: dictionary, tokens: tokens),
            let lastToken = tokens.last,
            case let bodyEndPosition = bodyOffset + bodyLength,
            !containsTrailingContent(lastToken: lastToken, bodyEndPosition: bodyEndPosition, file: file),
            !containsLeadingContent(tokens: tokens, bodyStartPosition: bodyOffset, file: file) else {
                return []
        }

        return [
            StyleViolation(ruleDescription: Self.description,
                           severity: configuration.severity,
                           location: Location(file: file, byteOffset: offset))
        ]
    }

3.3 AnalyzerRule

需要抽象语法树和编译日志配合的Rule, 抽象语法树通过sourcekit来获取

import UIKit
import WebKit

class TestObject: NSObject {
    var a = 11
}
[
  {
    "length" : 6,
    "offset" : 0,
    "type" : "source.lang.swift.syntaxtype.keyword"
  },
  {
    "length" : 5,
    "offset" : 7,
    "type" : "source.lang.swift.syntaxtype.identifier"
  },
  {
    "length" : 6,
    "offset" : 13,
    "type" : "source.lang.swift.syntaxtype.keyword"
  },
  {
    "length" : 6,
    "offset" : 20,
    "type" : "source.lang.swift.syntaxtype.identifier"
  },
  {
    "length" : 5,
    "offset" : 28,
    "type" : "source.lang.swift.syntaxtype.keyword"
  },
  {
    "length" : 10,
    "offset" : 34,
    "type" : "source.lang.swift.syntaxtype.identifier"
  },
  {
    "length" : 8,
    "offset" : 46,
    "type" : "source.lang.swift.syntaxtype.typeidentifier"
  },
  {
    "length" : 3,
    "offset" : 61,
    "type" : "source.lang.swift.syntaxtype.keyword"
  },
  {
    "length" : 1,
    "offset" : 65,
    "type" : "source.lang.swift.syntaxtype.identifier"
  },
  {
    "length" : 2,
    "offset" : 69,
    "type" : "source.lang.swift.syntaxtype.number"
  }
]
    func getImportsAndUSRFragments(compilerArguments: [String]) -> (imports: Set<String>, usrFragments: Set<String>) {
        var imports = Set<String>()
        var usrFragments = Set<String>()
        var nextIsModuleImport = false
        for token in syntaxMap.tokens {
            guard let tokenKind = token.kind else {
                continue
            }
            if tokenKind == .keyword, contents(for: token) == "import" {
                nextIsModuleImport = true
                continue
            }
            // 过滤掉没有模块信息的token 
            if SyntaxKind.kindsWithoutModuleInfo.contains(tokenKind) {
                continue
            }
            // 获取当前token对应的抽象语法树 
            let cursorInfoRequest = Request.cursorInfo(file: path!, offset: token.offset,
                                                       arguments: compilerArguments)
            guard let cursorInfo = (try? cursorInfoRequest.sendIfNotDisabled()).map(SourceKittenDictionary.init) else {
                queuedPrintError("Could not get cursor info")
                continue
            }
            if nextIsModuleImport {
                 // 通过语法树并获取当前文件的所有import 
                if let importedModule = cursorInfo.moduleName,
                    cursorInfo.kind == "source.lang.swift.ref.module" {
                    imports.insert(importedModule)
                    nextIsModuleImport = false
                    continue
                } else {
                    nextIsModuleImport = false
                }
            }
            // 获取使用的import 
            appendUsedImports(cursorInfo: cursorInfo, usrFragments: &usrFragments)
        }

        return (imports: imports, usrFragments: usrFragments)
    }


  func appendUsedImports(cursorInfo: SourceKittenDictionary, usrFragments: inout Set<String>) {
        if let rootModuleName = cursorInfo.moduleName?.split(separator: ".").first.map(String.init) {
            usrFragments.insert(rootModuleName)
            if rootModuleName == moduleToLog, let filePath = path, let usr = cursorInfo.value["key.usr"] as? String {
                queuedPrintError(
                    "[SWIFTLINT_LOG_MODULE_USAGE] \(rootModuleName) referenced by USR '\(usr)' in file '\(filePath)'"
                )
            }
        }
    }

4. SourceKit相关的工具

参考文档

swift命令行的脚手架工具,用于使用swift快速创建命令行工具

允许 Swift 工具解析、检查、生成和转换 Swift 源代码。

用于和SourceKit进行交互的工具

解析yaml格式的工具

生成文本格式表格的工具

以 为例:

以 为例:

以 为例:

SourceKit 是一套工具集,使得大多数 Swift 源代码层面的操作特性得以支持,例如Xcode中的源代码解析、语法高亮、排版、自动补全、跨语言头文件生成等等。 是与 SourceKit 后台进程进行交互的工具 ,基于 还有很多开源工具

: 为Swift/OC生成文档

: 生成模版代码

: 生成Mock代码的框架

: 为任何文本编辑器提供swift代码补全

: 从源码注释中生成Markdown文档的命令行工具

: Swift Mock 框架

: 无需运行代码或单元测试发现xib或storyboard相关的问题

swift-argument-parser
SwiftSyntax
SourceKitten
Yams
SwiftyTextTable
Using the Package Manager
Package Manager
PackageDescription
LeadingWhitespaceRule
ArrayInitRule
UnusedImportRule
SourceKitten
SourceKitten
Jazzy
Sourcery
SwiftyMocky
SourceKittenDaemon
SourceDocs
Cuckoo
IBAnalyzer
Swiftlint Github
Using the Package Manager
SourceKit
Swiftlint
Swift_Package_Manager