Files

162 lines
7.0 KiB
Swift

import SwiftSyntax
/// A SwiftLint Rule backed by SwiftSyntax that does not use SourceKit requests.
public protocol SwiftSyntaxRule: SourceKitFreeRule {
/// Produce a `ViolationsSyntaxVisitor` for the given file.
///
/// - parameter file: The file for which to produce the visitor.
///
/// - returns: A `ViolationsSyntaxVisitor` for the given file.
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor<ConfigurationType>
/// Produce a violation for the given file and absolute position.
///
/// - parameter file: The file for which to produce the violation.
/// - parameter violation: A violation in the file.
///
/// - returns: A violation for the given file and absolute position.
func makeViolation(file: SwiftLintFile, violation: ReasonedRuleViolation) -> StyleViolation
/// Gives a chance for the rule to do some pre-processing on the syntax tree.
/// One typical example is using `SwiftOperators` to "fold" the tree, resolving operators precedence.
/// This can also be used to skip validation in a given file.
/// By default, it just returns the file's `syntaxTree`.
///
/// - parameter file: The file to run pre-processing on.
///
/// - returns: The tree that will be used to check for violations. If `nil`, this rule will return no violations.
func preprocess(file: SwiftLintFile) -> SourceFileSyntax?
}
public extension SwiftSyntaxRule where ConfigurationType: SeverityBasedRuleConfiguration {
func makeViolation(file: SwiftLintFile, violation: ReasonedRuleViolation) -> StyleViolation {
StyleViolation(
ruleDescription: Self.description,
severity: violation.severity ?? configuration.severity,
location: Location(file: file, position: violation.position),
reason: violation.reason
)
}
}
public extension SwiftSyntaxRule {
@inlinable
func validate(file: SwiftLintFile) -> [StyleViolation] {
guard let syntaxTree = preprocess(file: file) else {
return []
}
let violations = makeVisitor(file: file)
.walk(tree: syntaxTree, handler: \.violations)
assert(
violations.allSatisfy { $0.correction == nil || self is any SwiftSyntaxCorrectableRule },
"\(Self.self) produced corrections without being correctable."
)
return violations
.sorted()
.map { makeViolation(file: file, violation: $0) }
}
func makeViolation(file: SwiftLintFile, violation: ReasonedRuleViolation) -> StyleViolation {
guard let severity = violation.severity else {
// This error will only be thrown in tests. It cannot come up at runtime.
queuedFatalError("""
A severity must be provided. Either define it in the violation or make the rule configuration \
conform to `SeverityBasedRuleConfiguration` to take the default.
""")
}
return StyleViolation(
ruleDescription: Self.description,
severity: severity,
location: Location(file: file, position: violation.position),
reason: violation.reason
)
}
func preprocess(file: SwiftLintFile) -> SourceFileSyntax? {
file.syntaxTree
}
}
/// A violation produced by `ViolationsSyntaxVisitor`s.
public struct ReasonedRuleViolation: Comparable, Hashable {
/// The correction of a violation that is basically the violation's range in the source code and a
/// replacement for this range that would fix the violation.
public struct ViolationCorrection: Hashable {
/// Start position of the violation range.
let start: AbsolutePosition
/// End position of the violation range.
let end: AbsolutePosition
/// Replacement for the violating range.
let replacement: String
/// Create a ``ViolationCorrection``.
/// - Parameters:
/// - start: Start position of the violation range.
/// - end: End position of the violation range.
/// - replacement: Replacement for the violating range.
public init(start: AbsolutePosition, end: AbsolutePosition, replacement: String) {
self.start = start
self.end = end
self.replacement = replacement
}
}
/// The violation's position.
public let position: AbsolutePosition
/// A specific reason for the violation.
public let reason: String?
/// The violation's severity.
public let severity: ViolationSeverity?
/// An optional correction of the violation to be used in rewriting (see ``SwiftSyntaxCorrectableRule``). Can be
/// left unset when creating a violation, especially if the rule is not correctable or provides a custom rewriter.
public let correction: ViolationCorrection?
/// Creates a `ReasonedRuleViolation`.
///
/// - Parameters:
/// - position: The violations position in the analyzed source file.
/// - reason: The reason for the violation if different from the rule's description.
/// - severity: The severity of the violation if different from the rule's default configured severity.
/// - correction: An optional correction of the violation to be used in rewriting.
public init(position: AbsolutePosition,
reason: String? = nil,
severity: ViolationSeverity? = nil,
correction: ViolationCorrection? = nil) {
self.position = position
self.reason = reason
self.severity = severity
self.correction = correction
}
public static func < (lhs: Self, rhs: Self) -> Bool {
lhs.position < rhs.position
}
}
/// Extension for arrays of `ReasonedRuleViolation`s that provides the automatic conversion of
/// `AbsolutePosition`s into `ReasonedRuleViolation`s (without a specific reason).
public extension Array where Element == ReasonedRuleViolation {
/// Add a violation at the specified position using the default description and severity.
///
/// - parameter position: The position for the violation to append.
mutating func append(_ position: AbsolutePosition) {
append(ReasonedRuleViolation(position: position))
}
/// Add a violation and the correction at the specified position using the default description and severity.
///
/// - parameter position: The position for the violation to append.
/// - parameter correction: An optional correction to be applied when running with `--fix`.
mutating func append(at position: AbsolutePosition, correction: ReasonedRuleViolation.ViolationCorrection? = nil) {
append(ReasonedRuleViolation(position: position, correction: correction))
}
/// Add violations for the specified positions using the default description and severity.
///
/// - parameter positions: The positions for the violations to append.
mutating func append(contentsOf positions: [AbsolutePosition]) {
append(contentsOf: positions.map { ReasonedRuleViolation(position: $0) })
}
}