142 lines
5.7 KiB
Swift
142 lines
5.7 KiB
Swift
import Foundation
|
|
import SourceKittenFramework
|
|
|
|
/// A rule configuration used for defining custom rules in yaml.
|
|
public struct RegexConfiguration: SeverityBasedRuleConfiguration, Hashable, CacheDescriptionProvider {
|
|
/// The identifier for this custom rule.
|
|
public let identifier: String
|
|
/// The name for this custom rule.
|
|
public var name: String?
|
|
/// The message to be presented when producing violations.
|
|
public var message = "Regex matched"
|
|
/// The regular expression to apply to trigger violations for this custom rule.
|
|
public var regex: NSRegularExpression!
|
|
/// Regular expressions to include when matching the file path.
|
|
public var included: [NSRegularExpression] = []
|
|
/// Regular expressions to exclude when matching the file path.
|
|
public var excluded: [NSRegularExpression] = []
|
|
/// The syntax kinds to exclude from matches. If the regex matched syntax kinds from this list, it would
|
|
/// be ignored and not count as a rule violation.
|
|
public var excludedMatchKinds = Set<SyntaxKind>()
|
|
public var severityConfiguration = SeverityConfiguration(.warning)
|
|
/// The index of the regex capture group to match.
|
|
public var captureGroup: Int = 0
|
|
|
|
public var consoleDescription: String {
|
|
return "\(severity.rawValue): \(regex.pattern)"
|
|
}
|
|
|
|
public var cacheDescription: String {
|
|
let jsonObject: [String] = [
|
|
identifier,
|
|
name ?? "",
|
|
message,
|
|
regex.pattern,
|
|
included.map(\.pattern).joined(separator: ","),
|
|
excluded.map(\.pattern).joined(separator: ","),
|
|
SyntaxKind.allKinds.subtracting(excludedMatchKinds)
|
|
.map({ $0.rawValue }).sorted(by: <).joined(separator: ","),
|
|
severityConfiguration.consoleDescription
|
|
]
|
|
if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject),
|
|
let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
return jsonString
|
|
}
|
|
queuedFatalError("Could not serialize regex configuration for cache")
|
|
}
|
|
|
|
/// The `RuleDescription` for the custom rule defined here.
|
|
public var description: RuleDescription {
|
|
return RuleDescription(identifier: identifier, name: name ?? identifier,
|
|
description: "", kind: .style)
|
|
}
|
|
|
|
/// Create a `RegexConfiguration` with the specified identifier, with other properties to be set later.
|
|
///
|
|
/// - parameter identifier: The rule identifier to use.
|
|
public init(identifier: String) {
|
|
self.identifier = identifier
|
|
}
|
|
|
|
public mutating func apply(configuration: Any) throws {
|
|
guard let configurationDict = configuration as? [String: Any],
|
|
let regexString = configurationDict["regex"] as? String else {
|
|
throw Issue.unknownConfiguration
|
|
}
|
|
|
|
regex = try .cached(pattern: regexString)
|
|
|
|
if let includedString = configurationDict["included"] as? String {
|
|
included = [try .cached(pattern: includedString)]
|
|
} else if let includedArray = configurationDict["included"] as? [String] {
|
|
included = try includedArray.map { pattern in
|
|
try .cached(pattern: pattern)
|
|
}
|
|
}
|
|
|
|
if let excludedString = configurationDict["excluded"] as? String {
|
|
excluded = [try .cached(pattern: excludedString)]
|
|
} else if let excludedArray = configurationDict["excluded"] as? [String] {
|
|
excluded = try excludedArray.map { pattern in
|
|
try .cached(pattern: pattern)
|
|
}
|
|
}
|
|
|
|
if let name = configurationDict["name"] as? String {
|
|
self.name = name
|
|
}
|
|
if let message = configurationDict["message"] as? String {
|
|
self.message = message
|
|
}
|
|
if let severityString = configurationDict["severity"] as? String {
|
|
try severityConfiguration.apply(configuration: severityString)
|
|
}
|
|
if let captureGroup = configurationDict["capture_group"] as? Int {
|
|
guard (0 ... regex.numberOfCaptureGroups).contains(captureGroup) else {
|
|
throw Issue.unknownConfiguration
|
|
}
|
|
self.captureGroup = captureGroup
|
|
}
|
|
|
|
self.excludedMatchKinds = try self.excludedMatchKinds(from: configurationDict)
|
|
}
|
|
|
|
public func hash(into hasher: inout Hasher) {
|
|
hasher.combine(identifier)
|
|
}
|
|
|
|
func shouldValidate(filePath: String) -> Bool {
|
|
let pathRange = filePath.fullNSRange
|
|
let isIncluded = included.isEmpty || included.contains { regex in
|
|
regex.firstMatch(in: filePath, range: pathRange) != nil
|
|
}
|
|
|
|
guard isIncluded else {
|
|
return false
|
|
}
|
|
|
|
return excluded.allSatisfy { regex in
|
|
regex.firstMatch(in: filePath, range: pathRange) == nil
|
|
}
|
|
}
|
|
|
|
private func excludedMatchKinds(from configurationDict: [String: Any]) throws -> Set<SyntaxKind> {
|
|
let matchKinds = [String].array(of: configurationDict["match_kinds"])
|
|
let excludedMatchKinds = [String].array(of: configurationDict["excluded_match_kinds"])
|
|
|
|
switch (matchKinds, excludedMatchKinds) {
|
|
case (.some(let matchKinds), nil):
|
|
let includedKinds = Set(try matchKinds.map({ try SyntaxKind(shortName: $0) }))
|
|
return SyntaxKind.allKinds.subtracting(includedKinds)
|
|
case (nil, .some(let excludedMatchKinds)):
|
|
return Set(try excludedMatchKinds.map({ try SyntaxKind(shortName: $0) }))
|
|
case (nil, nil):
|
|
return .init()
|
|
case (.some, .some):
|
|
throw Issue.genericWarning(
|
|
"The configuration keys 'match_kinds' and 'excluded_match_kinds' cannot appear at the same time."
|
|
)
|
|
}
|
|
}
|
|
}
|