Rewrite `quick_discouraged_call` with SwiftSyntax

This commit is contained in:
Marcelo Fabri 2022-10-30 21:00:46 -07:00
parent 20bfe264f5
commit e400174591
6 changed files with 99 additions and 110 deletions

View File

@ -144,6 +144,7 @@
- `prohibited_interface_builder`
- `prohibited_super_call`
- `protocol_property_accessors_order`
- `quick_discouraged_call`
- `quick_discouraged_focused_test`
- `quick_discouraged_pending_test`
- `raw_value_for_camel_cased_codable_enum`

View File

@ -188,6 +188,12 @@ extension FunctionDeclSyntax {
return SuperCallVisitor(expectedFunctionName: identifier.text)
.walk(tree: body, handler: \.superCallsCount)
}
var isQuickSpecFunction: Bool {
return identifier.tokenKind == .identifier("spec") &&
signature.input.parameterList.isEmpty &&
modifiers.containsOverride
}
}
extension AccessorBlockSyntax {
@ -220,6 +226,16 @@ extension Trivia {
}
}
extension ClassDeclSyntax {
var containsInheritance: Bool {
guard let inheritanceList = inheritanceClause?.inheritedTypeCollection else {
return false
}
return inheritanceList.isNotEmpty
}
}
extension IntegerLiteralExprSyntax {
var isZero: Bool {
guard case let .integerLiteral(number) = digits.tokenKind else {

View File

@ -1,6 +1,6 @@
import SourceKittenFramework
import SwiftSyntax
public struct QuickDiscouragedCallRule: OptInRule, ConfigurationProviderRule {
public struct QuickDiscouragedCallRule: OptInRule, SwiftSyntaxRule, ConfigurationProviderRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
@ -14,92 +14,78 @@ public struct QuickDiscouragedCallRule: OptInRule, ConfigurationProviderRule {
triggeringExamples: QuickDiscouragedCallRuleExamples.triggeringExamples
)
public func validate(file: SwiftLintFile) -> [StyleViolation] {
let dict = file.structureDictionary
let testClasses = dict.substructure.filter {
return $0.inheritedTypes.isNotEmpty &&
$0.declarationKind == .class
public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension QuickDiscouragedCallRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] {
.all
}
let specDeclarations = testClasses.flatMap { classDict in
return classDict.substructure.filter {
return $0.name == "spec()" && $0.enclosedVarParameters.isEmpty &&
$0.declarationKind == .functionMethodInstance &&
$0.enclosedSwiftAttributes.contains(.override)
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
guard let identifierExpr = node.calledExpression.as(IdentifierExprSyntax.self),
case let name = identifierExpr.identifier.text,
let kind = QuickCallKind(rawValue: name),
QuickCallKind.restrictiveKinds.contains(kind) else {
return .skipChildren
}
let functionViolations = FunctionCallVisitor(nameToReport: name)
.walk(tree: node, handler: \.violations)
violations.append(contentsOf: functionViolations.unique)
return .skipChildren
}
return specDeclarations.flatMap {
validate(file: file, dictionary: $0)
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
node.containsInheritance ? .visitChildren : .skipChildren
}
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
node.isQuickSpecFunction ? .visitChildren : .skipChildren
}
}
private func validate(file: SwiftLintFile, dictionary: SourceKittenDictionary) -> [StyleViolation] {
return dictionary.traverseDepthFirst { subDict in
guard let kind = subDict.expressionKind else { return nil }
return validate(file: file, kind: kind, dictionary: subDict)
private class FunctionCallVisitor: ViolationsSyntaxVisitor {
private let nameToReport: String
override var skippableDeclarations: [DeclSyntaxProtocol.Type] {
.allExcept(VariableDeclSyntax.self)
}
}
private func validate(file: SwiftLintFile,
kind: SwiftExpressionKind,
dictionary: SourceKittenDictionary) -> [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: Self.description,
severity: configuration.severity,
location: Location(file: file, byteOffset: $0),
reason: "Discouraged call inside a '\(name)' block.")
init(nameToReport: String) {
self.nameToReport = nameToReport
super.init(viewMode: .sourceAccurate)
}
}
private func violationOffsets(in substructure: [SourceKittenDictionary]) -> [ByteCount] {
return substructure.flatMap { dictionary -> [ByteCount] in
let substructure = dictionary.substructure.flatMap { dict -> [SourceKittenDictionary] in
if dict.expressionKind == .closure {
return dict.substructure
} else {
return [dict]
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
let hasViolation: Bool
if let identifierExpr = node.calledExpression.as(IdentifierExprSyntax.self) {
let name = identifierExpr.identifier.text
if let kind = QuickCallKind(rawValue: name), QuickCallKind.restrictiveKinds.contains(kind) {
return .visitChildren
}
hasViolation = QuickCallKind(rawValue: name) == nil
} else {
hasViolation = true
}
return substructure.flatMap(toViolationOffsets)
if hasViolation {
violations.append(
ReasonedRuleViolation(
position: node.positionAfterSkippingLeadingTrivia,
reason: "Discouraged call inside a '\(nameToReport)' block."
)
)
}
return .skipChildren
}
}
private func toViolationOffsets(dictionary: SourceKittenDictionary) -> [ByteCount] {
guard
dictionary.kind != nil,
let offset = dictionary.offset
else { return [] }
if dictionary.expressionKind == .call,
let name = dictionary.name, QuickCallKind(rawValue: name) == nil {
return [offset]
}
guard dictionary.expressionKind != .call else { return [] }
return dictionary.substructure.compactMap(toViolationOffset)
}
private func toViolationOffset(dictionary: SourceKittenDictionary) -> ByteCount? {
guard
let name = dictionary.name,
let offset = dictionary.offset,
dictionary.expressionKind == .call,
QuickCallKind(rawValue: name) == nil
else { return nil }
return offset
}
}
private enum QuickCallKind: String {
@ -107,6 +93,7 @@ private enum QuickCallKind: String {
case context
case sharedExamples
case itBehavesLike
case aroundEach
case beforeEach
case beforeSuite
case afterEach

View File

@ -139,7 +139,12 @@ internal struct QuickDiscouragedCallRuleExamples {
xitBehavesLike("foo")
}
}
""")
"""),
Example("""
class Foo: Bar {
let a = something()
}
""", excludeFromDocumentation: true)
]
static let triggeringExamples: [Example] = [
@ -169,6 +174,22 @@ internal struct QuickDiscouragedCallRuleExamples {
}
"""),
Example("""
class TotoTests: QuickSpec {
override func spec() {
describe("foo") {
context("bar") {
let foo = Foo()
foo.bar()
it("does something") {
let bar = Bar()
bar.toto()
}
}
}
}
}
"""),
Example("""
class TotoTests: QuickSpec {
override func spec() {
describe("foo") {

View File

@ -36,29 +36,11 @@ private extension QuickDiscouragedFocusedTestRule {
}
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
node.isSpecFunction ? .visitChildren : .skipChildren
node.isQuickSpecFunction ? .visitChildren : .skipChildren
}
}
}
private extension ClassDeclSyntax {
var containsInheritance: Bool {
guard let inheritanceList = inheritanceClause?.inheritedTypeCollection else {
return false
}
return inheritanceList.isNotEmpty
}
}
private extension FunctionDeclSyntax {
var isSpecFunction: Bool {
return identifier.tokenKind == .identifier("spec") &&
signature.input.parameterList.isEmpty &&
modifiers.containsOverride
}
}
private enum QuickFocusedCallKind: String {
case fdescribe
case fcontext

View File

@ -36,29 +36,11 @@ private extension QuickDiscouragedPendingTestRule {
}
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
node.isSpecFunction ? .visitChildren : .skipChildren
node.isQuickSpecFunction ? .visitChildren : .skipChildren
}
}
}
private extension ClassDeclSyntax {
var containsInheritance: Bool {
guard let inheritanceList = inheritanceClause?.inheritedTypeCollection else {
return false
}
return inheritanceList.isNotEmpty
}
}
private extension FunctionDeclSyntax {
var isSpecFunction: Bool {
return identifier.tokenKind == .identifier("spec") &&
signature.input.parameterList.isEmpty &&
modifiers.containsOverride
}
}
private enum QuickPendingCallKind: String {
case pending
case xdescribe