CI Code Quality Coverage Version: 0.11.0 License: MIT
PayPal: Donate GitHub: Become a sponsor Patreon: Become a patron

安装入门指南配置Xcode 构建脚本捐赠问题正则表达式速查表许可证

AnyLint

使用 Swift 和正则表达式来 Lint 任何语言的任何项目。内置支持匹配和非匹配示例验证以及自动更正替换。替代 SwiftLint 自定义规则,并且也适用于其他语言!🎉

安装

通过 Homebrew

首次安装 AnyLint,请运行以下命令

brew tap FlineDev/AnyLint https://github.com/FlineDev/AnyLint.git
brew install anylint

更新到最新版本,请运行此命令

brew upgrade anylint

通过 Mint

安装 AnyLint 或更新到最新版本,请运行此命令

mint install FlineDev/AnyLint

入门指南

要在项目中初始化 AnyLint,请运行

anylint --init blank

这将创建 Swift 脚本文件 lint.swift,其内容如下所示

#!/opt/local/bin/swift-sh
import AnyLint // @FlineDev

Lint.logSummaryAndExit(arguments: CommandLine.arguments) {
    // MARK: - Variables
    let readmeFile: Regex = #"README\.md"#

    // MARK: - Checks
    // MARK: Readme
    try Lint.checkFilePaths(
        checkInfo: "Readme: Each project should have a README.md file explaining the project.",
        regex: readmeFile,
        matchingExamples: ["README.md"],
        nonMatchingExamples: ["README.markdown", "Readme.md", "ReadMe.md"],
        violateIfNoMatchesFound: true
    )

    // MARK: ReadmeTypoLicense
    try Lint.checkFileContents(
        checkInfo: "ReadmeTypoLicense: Misspelled word 'license'.",
        regex: #"([\s#]L|l)isence([\s\.,:;])"#,
        matchingExamples: [" license:", "## Lisence\n"],
        nonMatchingExamples: [" license:", "## License\n"],
        includeFilters: [readmeFile],
        autoCorrectReplacement: "$1icense$2",
        autoCorrectExamples: [
            ["before": " lisence:", "after": " license:"],
            ["before": "## Lisence\n", "after": "## License\n"],
        ]
    )
}

需要注意的是,前三行是 AnyLint 正常工作所必需的

所有其他代码都可以调整,实际上您可以在其中配置 lint 检查(blank 模板默认提供了一些示例)。请注意,前两行声明该文件为 Swift 脚本,使用 swift-sh。因此,您可以运行任何 Swift 代码,甚至导入 Swift 包(请参阅 swift-sh 文档),如果需要的话。第三行确保在完成块中运行代码过程中发现的所有违规行为都会被正确报告,并在最后以适当的退出代码退出脚本。

有了这个配置文件,您现在可以运行 anylint 来运行您的 lint 检查。默认情况下,如果任何检查失败,整个命令都会失败并报告违规原因。要了解有关如何配置您自己的检查的更多信息,请参阅下面的配置部分。

如果您想创建和运行多个配置文件,或者想要为默认配置文件使用不同的名称或位置,您可以传递 --path 选项,该选项也可以多次使用,如下所示

在给定位置初始化配置文件

anylint --init blank --path Sources/lint.swift --path Tests/lint.swift

运行两个配置文件的 lint 检查

anylint --path Sources/lint.swift --path Tests/lint.swift

您还可以将几个标志传递给 anylint

  1. -s / --strict:对警告也失败。(默认情况下,该命令仅对错误失败。)
  2. -x / --xcode:以一种格式打印警告和错误,以便直接在 Xcode 的左侧边栏中报告。
  3. -l / --validate:仅运行 matchingExamplesnonMatchingExamplesautoCorrectExamples 的验证。
  4. -u / --unvalidated:运行检查而不验证其正确性。仅在验证运行成功后,为了加快后续运行而使用。
  5. -m / --measure:打印执行每个检查所花费的时间,以进行性能优化。
  6. -v / --version:打印当前工具版本。(不运行任何 lint 检查。)
  7. -d / --debug:记录有关 AnyLint 正在执行的操作的更多详细信息,以进行调试。

配置

AnyLint 提供三种不同类型的 lint 检查

  1. checkFileContents:将文本文件的内容与给定的正则表达式进行匹配。
  2. checkFilePaths:将当前目录的文件路径与给定的正则表达式进行匹配。
  3. customCheck:允许编写自定义 Swift 代码来执行其他类型的检查。

可以在此项目本身的 lint.swift 文件中找到几个 lint 检查的示例。

基本类型

与使用的方法无关,您应该了解 AnyLint 包中指定的一些类型。

正则表达式 (Regex)

上面提到的 lint 检查方法中的许多参数都是 Regex 类型。Regex 可以通过以下几种方式初始化

  1. 使用字符串
let regex = Regex(#"(foo|bar)[0-9]+"#) // => /(foo|bar)[0-9]+/
let regexWithOptions = Regex(#"(foo|bar)[0-9]+"#, options: [.ignoreCase, .dotMatchesLineSeparators, .anchorsMatchLines]) // => /(foo|bar)[0-9]+/im
  1. 使用字符串字面量
let regex: Regex = #"(foo|bar)[0-9]+"#  // => /(foo|bar)[0-9]+/
let regexWithOptions: Regex = #"(foo|bar)[0-9]+\im"#  // => /(foo|bar)[0-9]+/im
  1. 使用字典字面量:(用于命名捕获组
let regex: Regex = ["key": #"foo|bar"#, "num": "[0-9]+"] // => /(?<key>foo|bar)(?<num>[0-9]+)/
let regexWithOptions: Regex = ["key": #"foo|bar"#, "num": "[0-9]+", #"\"#: "im"] // => /(?<key>foo|bar)(?<num>[0-9]+)/im

请注意,我们建议对所有正则表达式使用原始字符串#"foo"# 而不是 "foo"),以消除双重转义反斜杠(例如,\\s 变为 \s)。这还允许您先在像 Rubular 这样的在线正则表达式编辑器中测试正则表达式,然后从它们复制和粘贴,而无需任何额外的转义(除了 {},用 \{\} 替换)。

正则表达式选项

在字面量中指定正则表达式选项是通过 \ 分隔符完成的,如上面的示例所示。可用的选项有

  1. i 表示 .ignoreCase:任何指定的字符都将匹配大写和小写变体。
  2. m 表示 .dotMatchesLineSeparators:正则表达式中所有出现的 . 也将匹配换行符(默认情况下不匹配)。

.anchorsMatchLines 选项始终在使用字面量时激活,因为我们强烈建议这样做。它确保可以使用 ^ 来匹配行的开头,并使用 $ 来匹配行的结尾。默认情况下,它们将匹配整个字符串的开头和结尾。如果这实际上是您想要的,您仍然可以使用 \A\z 来实现。这使得默认的字面量正则表达式行为更符合像 Rubular 这样的网站。

检查信息 (CheckInfo)

CheckInfo 包含有关 lint 检查的基本信息。它由以下组成

  1. id:lint 检查的标识符。例如:EmptyTodo
  2. hint:解释违规原因或修复步骤的提示。
  3. severity:违规的严重程度。errorwarninginfo 之一。默认值:error

虽然有一个可用的初始化器,但我们建议使用字符串字面量代替,如下所示

// accepted structure: <id>(@<severity>): <hint>
let checkInfo: CheckInfo = "ReadmePath: The README file should be named exactly `README.md`."
let checkInfoCustomSeverity: CheckInfo = "ReadmePath@warning: The README file should be named exactly `README.md`."

自动更正 (AutoCorrection)

AutoCorrection 包含一个示例 beforeafter 字符串,以验证给定的自动更正规则是否行为正确。

它可以通过两种方式初始化,可以使用默认的初始化器

let example: AutoCorrection = AutoCorrection(before: "Lisence", after: "License")

或者使用字典字面量

let example: AutoCorrection = ["before": "Lisence", "after": "License"]

检查文件内容

AnyLint 具有丰富的功能,可以使用正则表达式检查文件的内容。该设计遵循“使简单的事情简单,使困难的事情成为可能”的方法。因此,让我们通过一个简单和一个复杂的示例来解释 checkFileContents 方法。

在其最简单的形式中,该方法只需要一个 checkInfo 和一个 regex

// MARK: EmptyTodo
try Lint.checkFileContents(
    checkInfo: "EmptyTodo: TODO comments should not be empty.",
    regex: #"// TODO: *\n"#
)

但是我们强烈建议始终提供

  1. matchingExamples:期望与给定字符串匹配以进行 regex 验证的字符串数组。
  2. nonMatchingExamples:不与给定字符串匹配以进行 regex 验证的字符串数组。
  3. includeFilters:要包含到要检查的文件路径中的 Regex 对象数组。

前两个将在每次运行 AnyLint 时用于检查提供的 regex 是否实际按预期工作。如果任何 matchingExamples 不匹配,或者如果任何 nonMatchingExamples确实匹配,则整个 AnyLint 命令将提前失败。这是一个内置的验证步骤,有助于防止许多问题并提高您对 lint 检查的信心。

建议使用第三个,因为它提高了 linter 的性能。只会检查路径与至少一个提供的正则表达式匹配的文件。如果未提供,则将为每个检查递归读取当前目录中的所有文件,这效率低下。

这是一个推荐的最小示例

// MARK: - Variables
let swiftSourceFiles: Regex = #"Sources/.*\.swift"#
let swiftTestFiles: Regex = #"Tests/.*\.swift"#

// MARK: - Checks
// MARK: empty_todo
try Lint.checkFileContents(
    checkInfo: "EmptyTodo: TODO comments should not be empty.",
    regex: #"// TODO: *\n"#,
    matchingExamples: ["// TODO:\n"],
    nonMatchingExamples: ["// TODO: not yet implemented\n"],
    includeFilters: [swiftSourceFiles, swiftTestFiles]
)

如果需要,您可以选择性地设置另外 5 个参数

  1. excludeFilters:要从要检查的文件路径中排除的 Regex 对象数组。
  2. violationLocation:指定应报告违规标记违规的位置。可以是 fullMatchcaptureGroup(index:)lowerupper 端。
  3. autoCorrectReplacement:可以引用 regex 中任何捕获组的替换字符串。
  4. autoCorrectExamples:具有 beforeafter 的示例结构,用于自动更正验证。
  5. repeatIfAutoCorrected:如果在上次运行中至少应用了一个自动更正,则重复检查。默认为 false

excludeFilters 可以替代 includeFilters 使用,也可以与它们一起使用。如果一起使用,排除将优先于包含。

如果提供了 autoCorrectReplacement,AnyLint 将自动将 regex 的匹配项替换为给定的替换字符串。支持捕获组,包括编号样式(([a-z]+)(\d+) => $1$2)和命名组样式((?<alpha>[a-z])(?<num>\d+) => $alpha$num)。如果提供,我们强烈建议也提供 autoCorrectExamples 进行验证。与 matchingExamples / nonMatchingExamples 类似,如果其中一个示例没有从 before 字符串更正到预期的 after 字符串,则整个命令将提前失败。

注意: 使用 autoCorrectReplacement 参数时,请务必仔细检查您的正则表达式是否匹配了太多的内容。此外,我们强烈建议您定期提交更改以进行备份。

这是一个使用所有参数的完整示例

// MARK: - Variables
let swiftSourceFiles: Regex = #"Sources/.*\.swift"#
let swiftTestFiles: Regex = #"Tests/.*\.swift"#

// MARK: - Checks
// MARK: empty_method_body
try Lint.checkFileContents(
    checkInfo: "EmptyMethodBody: Don't use whitespaces for the body of empty methods.",
    regex: [
      "declaration": #"func [^\(\s]+\([^{]*\)"#,
      "spacing": #"\s*"#,
      "body": #"\{\s+\}"#
    ],
    violationLocation: .init(range: .fullMatch, bound: .upper),
    matchingExamples: [
        "func foo2bar()  { }",
        "func foo2bar(x: Int, y: Int)  { }",
        "func foo2bar(\n    x: Int,\n    y: Int\n) {\n    \n}",
    ],
    nonMatchingExamples: [
      "func foo2bar() {}",
      "func foo2bar(x: Int, y: Int) {}"
    ],
    includeFilters: [swiftSourceFiles],
    excludeFilters: [swiftTestFiles],
    autoCorrectReplacement: "$declaration {}",
    autoCorrectExamples: [
        ["before": "func foo2bar()  { }", "after": "func foo2bar() {}"],
        ["before": "func foo2bar(x: Int, y: Int)  { }", "after": "func foo2bar(x: Int, y: Int) {}"],
        ["before": "func foo2bar()\n{\n    \n}", "after": "func foo2bar() {}"],
    ]
)

请注意,当 autoCorrectReplacement 生成的替换字符串与 regex 的匹配字符串完全匹配时,将不会报告违规行为。这使我们能够提供更通用的 regex 模式,这些模式也与正确的字符串匹配,而实际上不会报告正确的字符串的违规行为。例如,使用正则表达式 if\s*\(([^)]+)\)\s*\{ 检查 if 语句后面的大括号周围的空格将报告以下所有示例的违规行为

if(x == 5) { /* some code */ }
if (x == 5){ /* some code */ }
if(x == 5){ /* some code */ }
if (x == 5) { /* some code */ }

问题是最后一个示例实际上是我们期望的格式,不应违反。通过提供 if ($1) {autoCorrectReplacement,我们可以解决该问题,因为替换将等于匹配的字符串,因此不会报告最后一个示例的违规行为,并且所有其他示例都会自动更正 – 正是我们想要的。🎉

(另一种方法是将检查分为两个单独的检查,一个用于检查前缀,一个用于检查后缀空格 - 不像这样美观,因为它很快就会使我们的 lint.swift 配置文件膨胀。)

跳过文件内容检查

虽然可以使用配置文件中的 includeFiltersexcludeFilters 参数来跳过对指定文件的检查,但有时需要进行例外处理并在文件本身中指定。例如,当有一个检查在 99% 的时间有效时,这会派上用场,但在 1% 的情况下,该检查可能会报告误报

对于这种情况,有2 种方法可以在文件本身中跳过检查

  1. AnyLint.skipHere: <CheckInfo.ID>:将跳过同一行和下一行上指定的检查。
var x: Int = 5 // AnyLint.skipHere: MinVarNameLength

// or

// AnyLint.skipHere: MinVarNameLength
var x: Int = 5
  1. AnyLint.skipInFile: <All 或 CheckInfo.ID>:将跳过整个文件中的 All 或指定检查。
// AnyLint.skipInFile: MinVarNameLength

var x: Int = 5
var y: Int = 5

// AnyLint.skipInFile: All

var x: Int = 5
var y: Int = 5

也可以在一行中同时跳过多个检查,如下所示

// AnyLint.skipHere: MinVarNameLength, LineLength, ColonWhitespaces

检查文件路径

checkFilePaths 方法具有与 checkFileContents 方法相同的所有参数,因此请阅读上面的章节以了解有关它们的更多信息。只有一项区别和一个附加参数。

  1. autoCorrectReplacement: 在这里,它将使用路径替换安全地移动文件。
  2. violateIfNoMatchesFound: 如果 true,当没有找到匹配项时,将报告违规。默认值:false

由于此方法是关于文件路径而不是文件内容,因此 autoCorrectReplacement 实际上也会修复路径,这对应于将文件从 before 状态移动到 after 状态。 请注意,在此处移动/重命名文件是安全地完成的,这意味着如果结果路径上已存在文件,则命令将失败。

默认情况下,如果给定的 regex 匹配到文件,checkFilePaths 将失败。但是,如果要检查文件的存在性,则可以将 violateIfNoMatchesFound 设置为 true,那么如果它没有匹配到任何文件,该方法将失败。

自定义检查

AnyLint 允许您进行任何类型的 lint 检查(因此得名),因为它为您提供了 Swift 编程语言及其软件包 生态系统 的全部能力。需要使用 customCheck 方法才能从此灵活性中受益。实际上,它是三种方法中最简单的,仅包含两个参数。

  1. checkInfo: 提供有关 lint 检查的一些常规信息。
  2. customClosure: 您的自定义逻辑,它生成一个 Violation 对象数组。

请注意,Violation 类型仅保存有关文件、匹配的字符串、文件中位置和应用的自动更正的一些附加信息,并且所有这些字段都是可选的。它是由 AnyLint 报告器使用的一个简单结构,用于更详细的输出,没有附加逻辑。 唯一必需的字段是导致违规的 CheckInfo 对象。

如果要在自定义代码中使用正则表达式,您可以了解有关如何使用 Regex 对象匹配字符串的更多信息,请参阅 HandySwift 文档(该项目,该类取自该项目)或阅读代码文档注释

使用 customCheck 时,您可能还想包含一些 Swift 包,以便更轻松地处理文件运行 shell 命令。您可以通过在文件顶部添加它们来实现,如下所示

#!/opt/local/bin/swift-sh
import AnyLint // @FlineDev
import ShellOut // @JohnSundell

Lint.logSummaryAndExit(arguments: CommandLine.arguments) {
    // MARK: - Variables
    let projectName: String = "AnyLint"

    // MARK: - Checks
    // MARK: LinuxMainUpToDate
    try Lint.customCheck(checkInfo: "LinuxMainUpToDate: The tests in Tests/LinuxMain.swift should be up-to-date.") { checkInfo in
        var violations: [Violation] = []

        let linuxMainFilePath = "Tests/LinuxMain.swift"
        let linuxMainContentsBeforeRegeneration = try! String(contentsOfFile: linuxMainFilePath)

        let sourceryDirPath = ".sourcery"
        try! shellOut(to: "sourcery", arguments: ["--sources", "Tests/\(projectName)Tests", "--templates", "\(sourceryDirPath)/LinuxMain.stencil", "--output", sourceryDirPath])

        let generatedLinuxMainFilePath = "\(sourceryDirPath)/LinuxMain.generated.swift"
        let linuxMainContentsAfterRegeneration = try! String(contentsOfFile: generatedLinuxMainFilePath)

        // move generated file to LinuxMain path to update its contents
        try! shellOut(to: "mv", arguments: [generatedLinuxMainFilePath, linuxMainFilePath])

        if linuxMainContentsBeforeRegeneration != linuxMainContentsAfterRegeneration {
            violations.append(
                Violation(
                    checkInfo: checkInfo,
                    filePath: linuxMainFilePath,
                    appliedAutoCorrection: AutoCorrection(
                        before: linuxMainContentsBeforeRegeneration,
                        after: linuxMainContentsAfterRegeneration
                    )
                )
            )
        }

        return violations
    }
}

Xcode 构建脚本

如果您在 Xcode 项目中使用 AnyLint,您可以配置一个构建脚本,以便在每次构建时运行它。 为此,请选择您的目标,选择“Build Phases”选项卡,然后单击该窗格左上角的 + 按钮。 选择“New Run Script Phase”并将以下内容复制到新运行脚本阶段的“Shell: /bin/sh”下方的文本框中

export PATH="$PATH:/opt/homebrew/bin"

if which anylint > /dev/null; then
    anylint -x
else
    echo "warning: AnyLint not installed, see from https://github.com/FlineDev/AnyLint"
fi

接下来,通过拖放操作,确保 AnyLint 脚本在 Compiling Sources 步骤之前运行,例如紧接在 Dependencies 之后。 您可能还想将其重命名为类似 AnyLint 的名称。

注意:在非 macOS 平台的 targets 中使用构建脚本时,存在一个 已知错误

正则表达式速查表

请参考 rubular.com 上的正则表达式快速参考,这些参考也适用于 Swift。

在 Swift 中,与 Ruby 中的正则表达式(rubular.com 基于 Ruby)存在一些差异——复制正则表达式时请注意。

  1. 在 Ruby 中,正斜杠 (/) 必须转义 (\/),这在 Swift 中是不必要的。
  2. 在 Swift 中,花括号 ({ & }) 必须转义 (\{ & \}),这在 Ruby 中是不必要的。

以下是一些您可能想要使用或了解更多信息的 高级正则表达式功能

  1. 反向引用可以在正则表达式中使用,以匹配先前的捕获组。

    例如,您可以确保 PR 编号和链接在 PR: [#100](https://github.com/FlineDev/AnyLint/pull/100) 中匹配,方法是使用捕获组 ((\d+)) 和反向引用 (\1),例如: \[#(\d+)\]\(https://[^)]+/pull/\1\)

    了解更多

  2. 否定 & 肯定先行断言 & 后行断言允许您指定一些有限制的模式,这些模式将从匹配范围中排除。它们使用 (?=PATTERN)(肯定先行断言)、(?!PATTERN)(否定先行断言)、(?<=PATTERN)(肯定后行断言)或 (?<!PATTERN)(否定后行断言)指定。

    例如,您可以使用正则表达式 - (?!None\.).* 来匹配 CHANGELOG.md 文件中的任何条目,但名为 None. 的空条目除外。

    了解更多

  3. 特别是,您可以使用后行断言来确保跨多行的正则表达式的报告行仅报告开发人员需要进行更改的确切行,而不是前一行。 这是可行的,因为后行断言匹配的模式不被视为匹配范围的一部分。

    例如,考虑一个正则表达式,如果在左花括号后有一个空行,则违反规则,如下所示:{\n\s*\n\s*\S。 这将匹配 func do() {\n\n return 5} 的行,但您实际上希望它从空的换行符开始匹配,如下所示: (?<={\n)\s*\n\s*\S

    另请参见 #3

捐赠

AnyLint 是由 Cihat Gündüz 在业余时间开发的。 如果您想感谢我并支持该项目的开发,请PayPal 上进行小额捐款。 如果您也喜欢我的其他 开源贡献文章,请考虑通过GitHub 上成为赞助者Patreon 上成为支持者来激励我。

非常感谢您的任何捐款,它真的很有帮助! 💯

贡献

欢迎贡献。 随意在 GitHub 上提出包含您想法的问题,或者自己实现一个想法并发布拉取请求。 如果您想贡献代码,请尝试在您的 提交消息 中遵循相同的语法和语义(请参阅 此处 的理由)。 此外,请确保在 CHANGELOG.md 文件中添加一个条目,以解释您的更改。

许可证

该库在 MIT 许可证下发布。 有关详细信息,请参见 LICENSE。