SwiftLint/Source/SwiftLintFramework/Models/Command.swift

172 lines
6.9 KiB
Swift

import Foundation
import SourceKittenFramework
#if os(Linux)
private extension Scanner {
func scanString(string: String) -> String? {
return scanString(string)
}
}
#else
private extension Scanner {
func scanUpToString(_ string: String) -> String? {
var result: NSString?
let success = scanUpTo(string, into: &result)
if success {
return result?.bridge()
}
return nil
}
func scanString(string: String) -> String? {
var result: NSString?
let success = scanString(string, into: &result)
if success {
return result?.bridge()
}
return nil
}
}
#endif
/// 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 `-` delimeter, 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(string: "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(Command.commentDelimiter) ?? ""
if scanner.isAtEnd {
trailingComment = nil
} else {
// Store any text after the comment delimiter as the trailingComment.
// The addition to scanLocation is to move past the delimiter
let startOfCommentPastDelimiter = scanner.scanLocation + Command.commentDelimiter.count
trailingComment = scanner.string.bridge().substring(from: startOfCommentPastDelimiter)
}
let ruleTexts = rawRuleTexts.components(separatedBy: .whitespaces).filter {
let component = $0.trimmingCharacters(in: .whitespaces)
return !component.isEmpty && component != "*/"
}
ruleIdentifiers = Set(ruleTexts.map(RuleIdentifier.init(_:)))
// Modifier
let hasModifier = actionAndModifierScanner.scanString(string: ":") != nil
if hasModifier {
let modifierString = actionAndModifierScanner.string.bridge()
.substring(from: actionAndModifierScanner.scanLocation)
modifier = Modifier(rawValue: modifierString)
} 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.
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)
]
}
}
}