mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
Introduce internal CLI to simplify development tasks (#6032)
Start with a command that generates a template for a new SwiftSyntax rule. This tool could remain separate or be merged into the official binary under a `dev` sub-command at a later point in time.
This commit is contained in:
@@ -7,6 +7,8 @@ steps:
|
||||
- make bazel_test
|
||||
- label: "SwiftPM"
|
||||
commands:
|
||||
- echo "+++ Generate Template Rule"
|
||||
- swift run swiftlint-dev rule-template FooBarBaz --rewriter --id foo_bar --config --kind metrics --severity error
|
||||
- echo "+++ Test"
|
||||
- make spm_test
|
||||
env:
|
||||
|
||||
+5
-1
@@ -8,7 +8,11 @@
|
||||
|
||||
### Experimental
|
||||
|
||||
* None.
|
||||
* Introduce `swiftlint-dev` command line tool that's intended to help to develop SwiftLint by encapsulating repetitive
|
||||
tasks. It can already be used to generate templates for new rules including optional configurations and tests. Run
|
||||
`swift run swiftlint-dev rule-template -h` to get an overview of the command's usage and the available customization
|
||||
options.
|
||||
[SimplyDanny](https://github.com/SimplyDanny)
|
||||
|
||||
### Enhancements
|
||||
|
||||
|
||||
+4
-1
@@ -82,7 +82,10 @@ open a pull request and watch the CI results carefully. They include all the nec
|
||||
|
||||
## Rules
|
||||
|
||||
New rules should be added in the `Source/SwiftLintBuiltInRules/Rules` directory.
|
||||
New rules should be added in the `Source/SwiftLintBuiltInRules/Rules` directory. We recommend to use the `swiftlint-dev`
|
||||
command line tool to generate scaffolds for new rules and their configurations. After having the repository cloned, run
|
||||
`swift run swiftlint-dev rule-template <RuleName>` to create the new rule at the correct location. Refer to the command's
|
||||
help `-h/--help` for customization options. Run `make sourcery` afterwards to register the new rule and its tests.
|
||||
|
||||
Prefer implementing new rules with the help of SwiftSyntax. Look for the `@SwiftSyntaxRule` attribute for examples and
|
||||
use the same on your own rule.
|
||||
|
||||
@@ -51,6 +51,14 @@ let package = Package(
|
||||
],
|
||||
swiftSettings: swiftFeatures + strictConcurrency
|
||||
),
|
||||
.executableTarget(
|
||||
name: "swiftlint-dev",
|
||||
dependencies: [
|
||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||
"SwiftLintCore",
|
||||
],
|
||||
swiftSettings: swiftFeatures + strictConcurrency
|
||||
),
|
||||
.target(
|
||||
name: "SwiftLintFramework",
|
||||
dependencies: [
|
||||
|
||||
@@ -119,10 +119,12 @@ public extension String {
|
||||
return String(dropFirst(prefix.count))
|
||||
}
|
||||
|
||||
func indent(by spaces: Int) -> String {
|
||||
components(separatedBy: "\n")
|
||||
.map { String(repeating: " ", count: spaces) + $0 }
|
||||
.joined(separator: "\n")
|
||||
func indent(by spaces: Int, skipFirst: Bool = false, skipEmptyLines: Bool = true) -> String {
|
||||
let lines = components(separatedBy: "\n")
|
||||
if skipFirst, let firstLine = lines.first {
|
||||
return firstLine + "\n" + lines.dropFirst().indent(by: spaces, skipEmptyLines: skipEmptyLines)
|
||||
}
|
||||
return lines.indent(by: spaces, skipEmptyLines: skipEmptyLines)
|
||||
}
|
||||
|
||||
func linesPrefixed(with prefix: Self) -> Self {
|
||||
@@ -142,3 +144,15 @@ public extension String {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private extension Sequence where Element == String {
|
||||
func indent(by spaces: Int, skipEmptyLines: Bool = true) -> String {
|
||||
map { line in
|
||||
if skipEmptyLines, line.isEmpty {
|
||||
return line
|
||||
}
|
||||
return String(repeating: " ", count: spaces) + line
|
||||
}
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// All the possible rule kinds (categories).
|
||||
public enum RuleKind: String, Codable, Sendable {
|
||||
public enum RuleKind: String, CaseIterable, Codable, Sendable {
|
||||
/// Describes rules that validate Swift source conventions.
|
||||
case lint
|
||||
/// Describes rules that validate common practices in the Swift community.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// The magnitude of a `StyleViolation`.
|
||||
@AcceptableByConfigurationElement
|
||||
public enum ViolationSeverity: String, Comparable, Codable, Sendable, InlinableOptionType {
|
||||
public enum ViolationSeverity: String, Comparable, CaseIterable, Codable, Sendable, InlinableOptionType {
|
||||
/// Non-fatal. If using SwiftLint as an Xcode build phase, Xcode will mark the build as having succeeded.
|
||||
case warning
|
||||
/// Fatal. If using SwiftLint as an Xcode build phase, Xcode will mark the build as having failed.
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import SwiftLintCore
|
||||
|
||||
extension SwiftLintDev {
|
||||
struct RuleTemplate: AsyncParsableCommand {
|
||||
// swiftlint:disable:next force_try
|
||||
private static let camelCaseRegex = try! NSRegularExpression(pattern: "(?<!^)(?=[A-Z])")
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "rule-template",
|
||||
abstract: "Generate a template for a new SwiftLint rule.",
|
||||
discussion: """
|
||||
This command generates a template for a SwiftLint rule. It creates a new file with the
|
||||
specified rule name and populates it with a basic structure. Optional flags allow you to
|
||||
customize the rule's properties.
|
||||
"""
|
||||
)
|
||||
|
||||
@Argument(help: "The name of the rule in PascalCase.")
|
||||
var name: String
|
||||
@Option(name: .long, help: "The rule's identifier. Defaults to the rule name in snake_case.")
|
||||
var id: String?
|
||||
@Option(name: .long, help: "Type of the rule.")
|
||||
var kind: RuleKind = .lint
|
||||
@Option(name: .long, help: "The rule's default severity.")
|
||||
var severity: ViolationSeverity = .warning
|
||||
@Flag(name: .long, help: "Indicates whether the rule shall be enabled by default.")
|
||||
var `default` = false
|
||||
@Flag(name: .long, help: "Indicates whether the rule is correctable.")
|
||||
var correctable = false
|
||||
@Flag(name: .long, help: "Indicates whether the rule has a custom rewriter.")
|
||||
var rewriter = false
|
||||
@Flag(name: .long, help: "Indicates whether the rule has a custom configuration.")
|
||||
var config = false
|
||||
@Flag(name: .long, help: "Indicates whether this rule has a dedicated test. This is usually not necessary.")
|
||||
var test = false
|
||||
@Flag(name: .long, help: "Indicates whether to overwrite existing files. Use with caution!")
|
||||
var overwrite = false
|
||||
|
||||
func run() async throws {
|
||||
let ruleDirectory = "Source/SwiftLintBuiltInRules/Rules"
|
||||
let ruleLocation = "\(ruleDirectory)/\(kind.rawValue.capitalized)"
|
||||
guard FileManager.default.fileExists(atPath: ruleLocation) else {
|
||||
throw ValidationError("Command must be run from the root of the SwiftLint repository.")
|
||||
}
|
||||
print("Creating template(s) for new rule \"\(ruleName)\" identified by '\(ruleId)' ...")
|
||||
let rulePath = "\(ruleLocation)/\(name)Rule.swift"
|
||||
guard overwrite || !FileManager.default.fileExists(atPath: rulePath) else {
|
||||
throw ValidationError("Rule file already exists at \(rulePath).")
|
||||
}
|
||||
try ruleTemplate.write(toFile: rulePath, atomically: true, encoding: .utf8)
|
||||
print("Rule file created at \(rulePath).")
|
||||
guard config else {
|
||||
return
|
||||
}
|
||||
let configPath = "\(ruleDirectory)/RuleConfigurations/\(name)Configuration.swift"
|
||||
guard overwrite || !FileManager.default.fileExists(atPath: configPath) else {
|
||||
throw ValidationError("Configuration file already exists at \(configPath).")
|
||||
}
|
||||
try configTemplate.write(toFile: configPath, atomically: true, encoding: .utf8)
|
||||
print("Configuration file created at \(configPath).")
|
||||
guard test else {
|
||||
return
|
||||
}
|
||||
let testDirectory = "Tests/BuiltInRulesTests"
|
||||
let testPath = "\(testDirectory)/\(name)RuleTests.swift"
|
||||
guard FileManager.default.fileExists(atPath: testDirectory) else {
|
||||
throw ValidationError("Command must be run from the root of the SwiftLint repository.")
|
||||
}
|
||||
guard overwrite || !FileManager.default.fileExists(atPath: testPath) else {
|
||||
throw ValidationError("Test file already exists at \(testPath).")
|
||||
}
|
||||
try testTemplate.write(toFile: testPath, atomically: true, encoding: .utf8)
|
||||
print("Test file created at \(testPath).")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension SwiftLintDev.RuleTemplate {
|
||||
var ruleId: String {
|
||||
id ?? Self.camelCaseRegex.stringByReplacingMatches(
|
||||
in: name,
|
||||
range: NSRange(location: 0, length: name.utf16.count),
|
||||
withTemplate: "_$0"
|
||||
).lowercased()
|
||||
}
|
||||
|
||||
var ruleName: String {
|
||||
Self.camelCaseRegex.stringByReplacingMatches(
|
||||
in: name,
|
||||
range: NSRange(location: 0, length: name.utf16.count),
|
||||
withTemplate: " $0"
|
||||
)
|
||||
}
|
||||
|
||||
var ruleTemplate: String {
|
||||
var attributeArguments = [String]()
|
||||
if rewriter {
|
||||
attributeArguments.append("explicitRewriter: true")
|
||||
}
|
||||
if correctable, !rewriter {
|
||||
attributeArguments.append("correctable: true")
|
||||
}
|
||||
attributeArguments.append("optIn: \(`default`)")
|
||||
var ruleDescriptionArguments = [
|
||||
"identifier: \"\(ruleId)\"",
|
||||
"name: \"\(ruleName)\"",
|
||||
"description: \"\"",
|
||||
"kind: .\(kind.rawValue)",
|
||||
"""
|
||||
nonTriggeringExamples: [
|
||||
Example(""),
|
||||
]
|
||||
""",
|
||||
"""
|
||||
triggeringExamples: [
|
||||
Example(""),
|
||||
]
|
||||
""",
|
||||
]
|
||||
if correctable || rewriter {
|
||||
ruleDescriptionArguments.append("""
|
||||
corrections: [
|
||||
Example(""):
|
||||
Example(""),
|
||||
]
|
||||
""")
|
||||
}
|
||||
let configDecl = "var configuration = " + (
|
||||
config
|
||||
? "\(name)Configuration()"
|
||||
: "SeverityConfiguration<Self>(.\(severity))"
|
||||
)
|
||||
var extensionContent = [
|
||||
"""
|
||||
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
|
||||
}
|
||||
""",
|
||||
]
|
||||
if rewriter {
|
||||
extensionContent.append("""
|
||||
|
||||
final class Rewriter: ViolationsSyntaxRewriter<ConfigurationType> {
|
||||
}
|
||||
""")
|
||||
}
|
||||
return """
|
||||
import SwiftLintCore
|
||||
import SwiftSyntax
|
||||
|
||||
@SwiftSyntaxRule(\(attributeArguments.joined(separator: ", ")))
|
||||
struct \(name)Rule: Rule {
|
||||
\(configDecl)
|
||||
|
||||
static let description = RuleDescription(
|
||||
\(ruleDescriptionArguments.joined(separator: ",\n").indent(by: 8, skipFirst: true))
|
||||
)
|
||||
}
|
||||
|
||||
private extension \(name)Rule {
|
||||
\(extensionContent.joined(separator: "\n").indent(by: 4, skipFirst: true))
|
||||
}
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
var configTemplate: String {
|
||||
"""
|
||||
import SwiftLintCore
|
||||
|
||||
@AutoConfigParser
|
||||
struct \(name)Configuration: SeverityBasedRuleConfiguration {
|
||||
typealias Parent = \(name)Rule
|
||||
|
||||
@ConfigurationElement(key: "severity")
|
||||
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.error)
|
||||
}
|
||||
|
||||
"""
|
||||
}
|
||||
|
||||
var testTemplate: String {
|
||||
"""
|
||||
@testable import SwiftLintBuiltInRules
|
||||
import TestHelpers
|
||||
|
||||
final class \(name)RuleTests: SwiftLintTestCase {
|
||||
func test() {
|
||||
verifyRule(\(name)Rule.description, ruleConfiguration: [])
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
extension RuleKind: ExpressibleByArgument {
|
||||
// Automatic conformance.
|
||||
}
|
||||
|
||||
extension ViolationSeverity: ExpressibleByArgument {
|
||||
// Automatic conformance.
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import ArgumentParser
|
||||
|
||||
@main
|
||||
struct SwiftLintDev: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "swiftlint-dev",
|
||||
abstract: "A tool to help develop SwiftLint.",
|
||||
subcommands: [
|
||||
RuleTemplate.self,
|
||||
]
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user