mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
293 lines
12 KiB
Swift
293 lines
12 KiB
Swift
import Foundation
|
|
import SourceKittenFramework
|
|
|
|
/// An executable value that can identify issues (violations) in Swift source code.
|
|
public protocol Rule: Sendable {
|
|
/// The type of the configuration used to configure this rule.
|
|
associatedtype ConfigurationType: RuleConfiguration
|
|
|
|
/// A verbose description of many of this rule's properties.
|
|
static var description: RuleDescription { get }
|
|
|
|
/// This rule's configuration.
|
|
var configuration: ConfigurationType { get set }
|
|
|
|
/// Whether this rule should be used on empty files. Defaults to `false`.
|
|
var shouldLintEmptyFiles: Bool { get }
|
|
|
|
/// A default initializer for rules. All rules need to be trivially initializable.
|
|
init()
|
|
|
|
/// Creates a rule by applying its configuration.
|
|
///
|
|
/// - parameter configuration: The untyped configuration value to apply.
|
|
///
|
|
/// - throws: Throws if the configuration didn't match the expected format.
|
|
init(configuration: Any) throws
|
|
|
|
/// Create a description of how this rule has been configured to run.
|
|
///
|
|
/// - parameter exclusiveOptions: A set of options that should be excluded from the description.
|
|
///
|
|
/// - returns: A description of the rule's configuration.
|
|
func createConfigurationDescription(exclusiveOptions: Set<String>) -> RuleConfigurationDescription
|
|
|
|
/// Executes the rule on a file and returns any violations to the rule's expectations.
|
|
///
|
|
/// - parameter file: The file for which to execute the rule.
|
|
/// - parameter compilerArguments: The compiler arguments needed to compile this file.
|
|
///
|
|
/// - returns: All style violations to the rule's expectations.
|
|
func validate(file: SwiftLintFile, compilerArguments: [String]) -> [StyleViolation]
|
|
|
|
/// Executes the rule on a file and returns any violations to the rule's expectations.
|
|
///
|
|
/// - parameter file: The file for which to execute the rule.
|
|
///
|
|
/// - returns: All style violations to the rule's expectations.
|
|
func validate(file: SwiftLintFile) -> [StyleViolation]
|
|
|
|
/// Whether or not the specified rule is equivalent to the current rule.
|
|
///
|
|
/// - parameter rule: The `rule` value to compare against.
|
|
///
|
|
/// - returns: Whether or not the specified rule is equivalent to the current rule.
|
|
func isEqualTo(_ rule: some Rule) -> Bool
|
|
|
|
/// Collects information for the specified file in a storage object, to be analyzed by a `CollectedLinter`.
|
|
///
|
|
/// - note: This function is called by the linter and is always implemented in extensions.
|
|
///
|
|
/// - parameter file: The file for which to collect info.
|
|
/// - parameter storage: The storage object where collected info should be saved.
|
|
/// - parameter compilerArguments: The compiler arguments needed to compile this file.
|
|
func collectInfo(for file: SwiftLintFile, into storage: RuleStorage, compilerArguments: [String])
|
|
|
|
/// Executes the rule on a file after collecting file info for all files and returns any violations to the rule's
|
|
/// expectations.
|
|
///
|
|
/// - note: This function is called by the linter and is always implemented in extensions.
|
|
///
|
|
/// - parameter file: The file for which to execute the rule.
|
|
/// - parameter storage: The storage object containing all collected info.
|
|
/// - parameter compilerArguments: The compiler arguments needed to compile this file.
|
|
///
|
|
/// - returns: All style violations to the rule's expectations.
|
|
func validate(file: SwiftLintFile, using storage: RuleStorage, compilerArguments: [String]) -> [StyleViolation]
|
|
|
|
/// Checks if a style violation can be disabled by a command specifying a rule ID. Only the rule can claim that for
|
|
/// sure since it knows all the possible identifiers.
|
|
///
|
|
/// - Parameters:
|
|
/// - violation: A style violation.
|
|
/// - ruleID: The name of a rule as used in a disable command.
|
|
///
|
|
/// - Returns: A boolean value indicating whether the violation can be disabled by the given ID.
|
|
func canBeDisabled(violation: StyleViolation, by ruleID: RuleIdentifier) -> Bool
|
|
|
|
/// Checks if a the rule is enabled in a given region. A specific rule ID can be provided in case a rule supports
|
|
/// more than one identifier.
|
|
///
|
|
/// - Parameters:
|
|
/// - region: The region to check.
|
|
/// - ruleID: Rule identifier deviating from the default rule's name.
|
|
///
|
|
/// - Returns: A boolean value indicating whether the rule is enabled in the given region.
|
|
func isEnabled(in region: Region, for ruleID: String) -> Bool
|
|
|
|
/// Prints a warning to the console once about the rule being disabled due to SourceKit being unavailable. The
|
|
/// default implementation does nothing. Rules that depend on SourceKit should override this appropriately.
|
|
func notifyRuleDisabledOnce()
|
|
}
|
|
|
|
public extension Rule {
|
|
var shouldLintEmptyFiles: Bool {
|
|
false
|
|
}
|
|
|
|
init(configuration: Any) throws {
|
|
self.init()
|
|
try self.configuration.apply(configuration: configuration)
|
|
}
|
|
|
|
func validate(file: SwiftLintFile, using _: RuleStorage, compilerArguments: [String]) -> [StyleViolation] {
|
|
validate(file: file, compilerArguments: compilerArguments)
|
|
}
|
|
|
|
func validate(file: SwiftLintFile, compilerArguments _: [String]) -> [StyleViolation] {
|
|
validate(file: file)
|
|
}
|
|
|
|
func isEqualTo(_ rule: some Rule) -> Bool {
|
|
if let rule = rule as? Self {
|
|
return configuration == rule.configuration
|
|
}
|
|
return false
|
|
}
|
|
|
|
func collectInfo(for _: SwiftLintFile, into _: RuleStorage, compilerArguments _: [String]) {
|
|
// no-op: only CollectingRules mutate their storage
|
|
}
|
|
|
|
/// The cache description which will be used to determine if a previous
|
|
/// cached value is still valid given the new cache value.
|
|
var cacheDescription: String {
|
|
(self as? any CacheDescriptionProvider)?.cacheDescription ?? createConfigurationDescription().oneLiner()
|
|
}
|
|
|
|
func createConfigurationDescription(exclusiveOptions: Set<String> = []) -> RuleConfigurationDescription {
|
|
RuleConfigurationDescription.from(configuration: configuration, exclusiveOptions: exclusiveOptions)
|
|
}
|
|
|
|
func canBeDisabled(violation: StyleViolation, by ruleID: RuleIdentifier) -> Bool {
|
|
switch ruleID {
|
|
case .all:
|
|
true
|
|
case let .single(identifier: id):
|
|
Self.description.allIdentifiers.contains(id)
|
|
&& Self.description.allIdentifiers.contains(violation.ruleIdentifier)
|
|
}
|
|
}
|
|
|
|
func isEnabled(in region: Region, for ruleID: String) -> Bool {
|
|
!Self.description.allIdentifiers.contains(ruleID) || region.isRuleEnabled(self)
|
|
}
|
|
|
|
func notifyRuleDisabledOnce() {
|
|
// Intentionally empty by default.
|
|
}
|
|
}
|
|
|
|
public extension Rule {
|
|
/// The rule's unique identifier which is the same as `Rule.description.identifier`.
|
|
static var identifier: String { description.identifier }
|
|
}
|
|
|
|
/// A rule that is not enabled by default. Rules conforming to this need to be explicitly enabled by users.
|
|
public protocol OptInRule: Rule {}
|
|
|
|
/// A rule that can correct violations.
|
|
public protocol CorrectableRule: Rule {
|
|
/// Attempts to correct the violations to this rule in the specified file.
|
|
///
|
|
/// - parameter file: The file for which to correct violations.
|
|
/// - parameter compilerArguments: The compiler arguments needed to compile this file.
|
|
///
|
|
/// - returns: Number of corrections that were applied.
|
|
func correct(file: SwiftLintFile, compilerArguments: [String]) -> Int
|
|
|
|
/// Attempts to correct the violations to this rule in the specified file.
|
|
///
|
|
/// - parameter file: The file for which to correct violations.
|
|
///
|
|
/// - returns: Number of corrections that were applied.
|
|
func correct(file: SwiftLintFile) -> Int
|
|
|
|
/// Attempts to correct the violations to this rule in the specified file after collecting file info for all files
|
|
/// and returns all corrections that were applied.
|
|
///
|
|
/// - note: This function is called by the linter and is always implemented in extensions.
|
|
///
|
|
/// - parameter file: The file for which to execute the rule.
|
|
/// - parameter storage: The storage object containing all collected info.
|
|
/// - parameter compilerArguments: The compiler arguments needed to compile this file.
|
|
///
|
|
/// - returns: All corrections that were applied.
|
|
func correct(file: SwiftLintFile, using storage: RuleStorage, compilerArguments: [String]) -> Int
|
|
}
|
|
|
|
public extension CorrectableRule {
|
|
func correct(file: SwiftLintFile, compilerArguments _: [String]) -> Int {
|
|
correct(file: file)
|
|
}
|
|
func correct(file: SwiftLintFile, using _: RuleStorage, compilerArguments: [String]) -> Int {
|
|
correct(file: file, compilerArguments: compilerArguments)
|
|
}
|
|
}
|
|
|
|
/// A correctable rule that can apply its corrections by replacing the content of ranges in the offending file with
|
|
/// updated content.
|
|
public protocol SubstitutionCorrectableRule: CorrectableRule {
|
|
/// Returns the NSString-based `NSRange`s to be replaced in the specified file.
|
|
///
|
|
/// - parameter file: The file in which to find ranges of violations for this rule.
|
|
///
|
|
/// - returns: The NSString-based `NSRange`s to be replaced in the specified file.
|
|
func violationRanges(in file: SwiftLintFile) -> [NSRange]
|
|
|
|
/// Returns the substitution to apply for the given range.
|
|
///
|
|
/// - parameter violationRange: The NSString-based `NSRange` of the violation that should be replaced.
|
|
/// - parameter file: The file in which the violation should be replaced.
|
|
///
|
|
/// - returns: The range of the correction and its contents, if one could be computed.
|
|
func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)?
|
|
}
|
|
|
|
public extension SubstitutionCorrectableRule {
|
|
func correct(file: SwiftLintFile) -> Int {
|
|
let violatingRanges = file.ruleEnabled(violatingRanges: violationRanges(in: file), for: self)
|
|
guard violatingRanges.isNotEmpty else {
|
|
return 0
|
|
}
|
|
var numberOfCorrections = 0
|
|
var contents = file.contents
|
|
for range in violatingRanges.sorted(by: { $0.location > $1.location }) {
|
|
let contentsNSString = contents.bridge()
|
|
if let (rangeToRemove, substitution) = substitution(for: range, in: file) {
|
|
contents = contentsNSString.replacingCharacters(in: rangeToRemove, with: substitution)
|
|
numberOfCorrections += 1
|
|
}
|
|
}
|
|
|
|
file.write(contents)
|
|
return numberOfCorrections
|
|
}
|
|
}
|
|
|
|
/// A rule that does not need SourceKit to operate and can still operate even after SourceKit has crashed.
|
|
public protocol SourceKitFreeRule: Rule {}
|
|
|
|
/// A rule that may or may not require SourceKit depending on its configuration.
|
|
public protocol ConditionallySourceKitFree: Rule {
|
|
/// Whether this rule is currently configured in a way that doesn't require SourceKit.
|
|
var isEffectivelySourceKitFree: Bool { get }
|
|
}
|
|
|
|
public extension Rule {
|
|
/// Whether this rule requires SourceKit to operate.
|
|
/// Returns false if the rule conforms to SourceKitFreeRule or if it conforms to
|
|
/// ConditionallySourceKitFree and is currently configured to not require SourceKit.
|
|
var requiresSourceKit: Bool {
|
|
// Check if rule conforms to SourceKitFreeRule
|
|
if self is any SourceKitFreeRule {
|
|
return false
|
|
}
|
|
|
|
// Check if rule is conditionally SourceKit-free and currently doesn't need SourceKit
|
|
if let conditionalRule = self as? any ConditionallySourceKitFree {
|
|
return !conditionalRule.isEffectivelySourceKitFree
|
|
}
|
|
|
|
// All other rules require SourceKit
|
|
return true
|
|
}
|
|
}
|
|
|
|
/// A rule that can operate on the post-typechecked AST using compiler arguments. Performs rules that are more like
|
|
/// static analysis than syntactic checks.
|
|
public protocol AnalyzerRule: OptInRule {}
|
|
|
|
public extension AnalyzerRule {
|
|
func validate(file _: SwiftLintFile) -> [StyleViolation] {
|
|
queuedFatalError("Must call `validate(file:compilerArguments:)` for AnalyzerRule")
|
|
}
|
|
}
|
|
|
|
/// :nodoc:
|
|
public extension AnalyzerRule where Self: CorrectableRule {
|
|
func correct(file _: SwiftLintFile) -> Int {
|
|
queuedFatalError("Must call `correct(file:compilerArguments:)` for AnalyzerRule")
|
|
}
|
|
}
|