SwiftLint/Source/SwiftLintFramework/Rules/Lint/QuickDiscouragedCallRule.swift

136 lines
4.8 KiB
Swift

import SourceKittenFramework
public struct QuickDiscouragedCallRule: ConfigurationProviderRule, AutomaticTestableRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "quick_discouraged_call",
name: "Quick Discouraged Call",
description: "Discouraged call inside 'describe' and/or 'context' block.",
kind: .lint,
isOptIn: true,
nonTriggeringExamples: QuickDiscouragedCallRuleExamples.nonTriggeringExamples,
triggeringExamples: QuickDiscouragedCallRuleExamples.triggeringExamples
)
public func validate(file: File) -> [StyleViolation] {
let testClasses = file.structure.dictionary.substructure.filter {
return $0.inheritedTypes.contains("QuickSpec") &&
$0.kind.flatMap(SwiftDeclarationKind.init) == .class
}
let specDeclarations = testClasses.flatMap { classDict in
return classDict.substructure.filter {
return $0.name == "spec()" && $0.enclosedVarParameters.isEmpty &&
$0.kind.flatMap(SwiftDeclarationKind.init) == .functionMethodInstance &&
$0.enclosedSwiftAttributes.contains(.override)
}
}
return specDeclarations.flatMap {
validate(file: file, dictionary: $0)
}
}
private func validate(file: File, dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
return dictionary.substructure.flatMap { subDict -> [StyleViolation] in
var violations = validate(file: file, dictionary: subDict)
if let kindString = subDict.kind,
let kind = SwiftExpressionKind(rawValue: kindString) {
violations += validate(file: file, kind: kind, dictionary: subDict)
}
return violations
}
}
private func validate(file: File,
kind: SwiftExpressionKind,
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
// is it a call to a restricted method?
guard
kind == .call,
let name = dictionary.name,
let kindName = QuickCallKind(rawValue: name),
QuickCallKind.restrictiveKinds.contains(kindName)
else { return [] }
return violationOffsets(in: dictionary.enclosedArguments).map {
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, byteOffset: $0),
reason: "Discouraged call inside a '\(name)' block.")
}
}
private func violationOffsets(in substructure: [[String: SourceKitRepresentable]]) -> [Int] {
return substructure.flatMap { dictionary -> [Int] in
let substructure = dictionary.substructure.flatMap { dict -> [[String: SourceKitRepresentable]] in
if dict.kind.flatMap(SwiftExpressionKind.init) == .closure {
return dict.substructure
} else {
return [dict]
}
}
return substructure.flatMap(toViolationOffsets)
}
}
private func toViolationOffsets(dictionary: [String: SourceKitRepresentable]) -> [Int] {
guard
let kind = dictionary.kind,
let offset = dictionary.offset
else { return [] }
if SwiftExpressionKind(rawValue: kind) == .call,
let name = dictionary.name, QuickCallKind(rawValue: name) == nil {
return [offset]
}
guard SwiftExpressionKind(rawValue: kind) != .call else { return [] }
return dictionary.substructure.compactMap(toViolationOffset)
}
private func toViolationOffset(dictionary: [String: SourceKitRepresentable]) -> Int? {
guard
let name = dictionary.name,
let offset = dictionary.offset,
let kind = dictionary.kind,
SwiftExpressionKind(rawValue: kind) == .call,
QuickCallKind(rawValue: name) == nil
else { return nil }
return offset
}
}
private enum QuickCallKind: String {
case describe
case context
case sharedExamples
case itBehavesLike
case beforeEach
case beforeSuite
case afterEach
case afterSuite
case it // swiftlint:disable:this identifier_name
case pending
case xdescribe
case xcontext
case xit
case xitBehavesLike
case fdescribe
case fcontext
case fit
case fitBehavesLike
static let restrictiveKinds: Set<QuickCallKind> = [
.describe, .fdescribe, .xdescribe, .context, .fcontext, .xcontext, .sharedExamples
]
}