Files
SwiftLint/Source/SwiftLintFramework/Models/Command.swift
T
JP Simard 05ac3c9d75 Require macOS 12 & Swift 5.6 (#4037)
This will unblock using Swift Concurrency features and updating to the
latest versions of Swift Argument Parser.

This won't drop support for linting projects with an older toolchain /
Xcode selected, as long as SwiftLint was _built_ with 5.6+ and is
_running_ on macOS 12+. So the main breaking change for end users here
is requiring macOS 12 to run.

However, the upside to using Swift Concurrency features is worth the
breaking change in my opinion. Also being able to stay on recent Swift
Argument Parser releases.
2022-07-26 03:55:36 -04:00

150 lines
6.3 KiB
Swift

import Foundation
/// A SwiftLint-interpretable command to modify SwiftLint's behavior embedded as comments in source code.
public struct Command: Equatable {
/// The action (verb) that SwiftLint should perform when interpreting this command.
public enum Action: String {
/// The rule(s) associated with this command should be enabled by the SwiftLint engine.
case enable
/// The rule(s) associated with this command should be disabled by the SwiftLint engine.
case disable
/// - returns: The inverse action that can cancel out the current action, restoring the SwifttLint engine's
/// state prior to the current action.
internal func inverse() -> Action {
switch self {
case .enable: return .disable
case .disable: return .enable
}
}
}
/// The modifier for a command, used to modify its scope.
public enum Modifier: String {
/// The command should only apply to the line preceding its definition.
case previous
/// The command should only apply to the same line as its definition.
case this
/// The command should only apply to the line following its definition.
case next
}
/// Text after this delimiter is not considered part of the rule.
/// The purpose of this delimiter is to allow SwiftLint
/// commands to be documented in source code.
///
/// swiftlint:disable:next force_try - Explanation here
private static let commentDelimiter = " - "
internal let action: Action
internal let ruleIdentifiers: Set<RuleIdentifier>
internal let line: Int
internal let character: Int?
internal let modifier: Modifier?
/// Currently unused but parsed separate from rule identifiers
internal let trailingComment: String?
/// Creates a command based on the specified parameters.
///
/// - parameter action: This command's action.
/// - parameter ruleIdentifiers: The identifiers for the rules associated with this command.
/// - parameter line: The line in the source file where this command is defined.
/// - parameter character: The character offset within the line in the source file where this command is
/// defined.
/// - parameter modifier: This command's modifier, if any.
/// - parameter trailingComment: The comment following this command's `-` delimiter, if any.
public init(action: Action, ruleIdentifiers: Set<RuleIdentifier>, line: Int = 0,
character: Int? = nil, modifier: Modifier? = nil, trailingComment: String? = nil) {
self.action = action
self.ruleIdentifiers = ruleIdentifiers
self.line = line
self.character = character
self.modifier = modifier
self.trailingComment = trailingComment
}
/// Creates a command based on the specified parameters.
///
/// - parameter actionString: The string in the command's definition describing its action.
/// - parameter line: The line in the source file where this command is defined.
/// - parameter character: The character offset within the line in the source file where this command is
/// defined.
public init?(actionString: String, line: Int, character: Int) {
let scanner = Scanner(string: actionString)
_ = scanner.scanString("swiftlint:")
// (enable|disable)(:previous|:this|:next)
guard let actionAndModifierString = scanner.scanUpToString(" ") else {
return nil
}
let actionAndModifierScanner = Scanner(string: actionAndModifierString)
guard let actionString = actionAndModifierScanner.scanUpToString(":"),
let action = Action(rawValue: actionString)
else {
return nil
}
self.action = action
self.line = line
self.character = character
let rawRuleTexts = scanner.scanUpToString(Self.commentDelimiter) ?? ""
if scanner.isAtEnd {
trailingComment = nil
} else {
// Store any text after the comment delimiter as the trailingComment.
// The addition to currentIndex is to move past the delimiter
trailingComment = String(
scanner
.string[scanner.currentIndex...]
.dropFirst(Self.commentDelimiter.count)
)
}
let ruleTexts = rawRuleTexts.components(separatedBy: .whitespacesAndNewlines).filter {
let component = $0.trimmingCharacters(in: .whitespaces)
return component.isNotEmpty && component != "*/"
}
ruleIdentifiers = Set(ruleTexts.map(RuleIdentifier.init(_:)))
// Modifier
let hasModifier = actionAndModifierScanner.scanString(":") != nil
if hasModifier {
modifier = Modifier(
rawValue: String(
actionAndModifierScanner.string[actionAndModifierScanner.currentIndex...]
)
)
} else {
modifier = nil
}
}
/// Expands the current command into its fully descriptive form without any modifiers.
/// If the command doesn't have a modifier, it is returned as-is.
///
/// - returns: The expanded commands.
internal func expand() -> [Command] {
guard let modifier = modifier else {
return [self]
}
switch modifier {
case .previous:
return [
Command(action: action, ruleIdentifiers: ruleIdentifiers, line: line - 1),
Command(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line - 1,
character: Int.max)
]
case .this:
return [
Command(action: action, ruleIdentifiers: ruleIdentifiers, line: line),
Command(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line,
character: Int.max)
]
case .next:
return [
Command(action: action, ruleIdentifiers: ruleIdentifiers, line: line + 1),
Command(action: action.inverse(), ruleIdentifiers: ruleIdentifiers, line: line + 1, character: Int.max)
]
}
}
}