Rewrite private_subject rule with SwiftSyntax (#4495)

This commit is contained in:
Marcelo Fabri
2022-10-30 18:41:57 -07:00
committed by GitHub
parent 5144cccb57
commit 697eaa73dd
3 changed files with 44 additions and 77 deletions
+1
View File
@@ -138,6 +138,7 @@
- `private_action`
- `private_over_fileprivate`
- `private_outlet`
- `private_subject`
- `private_unit_test`
- `prohibited_interface_builder`
- `protocol_property_accessors_order`
@@ -1,6 +1,6 @@
import SourceKittenFramework
import SwiftSyntax
public struct PrivateSubjectRule: ASTRule, OptInRule, ConfigurationProviderRule {
public struct PrivateSubjectRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
// MARK: - Properties
public var configuration = SeverityConfiguration(.warning)
@@ -14,85 +14,51 @@ public struct PrivateSubjectRule: ASTRule, OptInRule, ConfigurationProviderRule
triggeringExamples: PrivateSubjectRuleExamples.triggeringExamples
)
private let subjectTypes: Set<String> = ["PassthroughSubject", "CurrentValueSubject"]
// MARK: - Life cycle
public init() {}
// MARK: - Public
public func validate(file: SwiftLintFile,
kind: SwiftDeclarationKind,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
guard
kind == .varInstance,
dictionary.accessibility?.isPrivate == false
else {
return []
}
let declarationViolation = declarationViolationOffset(
dictionary: dictionary
)
let defaultValueViolation = defaultValueViolationOffset(
file: file,
dictionary: dictionary
)
let violations = [declarationViolation, defaultValueViolation]
.compactMap { $0 }
.map {
StyleViolation(ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, byteOffset: $0))
}
return violations
}
// MARK: - Private
/// Looks for violations matching the format:
///
/// * `let subject: PassthroughSubject<Bool, Never>`
/// * `let subject: PassthroughSubject<Bool, Never> = .init()`
/// * `let subject: CurrentValueSubject<Bool, Never>`
/// * `let subject: CurrentValueSubject<String, Never> = .ini("toto")`
///
/// - Returns: The violation offset.
private func declarationViolationOffset(dictionary: SourceKittenDictionary) -> ByteCount? {
guard
let typeName = dictionary.typeName,
subjectTypes.contains(where: typeName.hasPrefix) == true
else {
return nil
}
return dictionary.nameOffset
}
/// Looks for violations matching the format:
///
/// * `let subject = PassthroughSubject<Bool, Never>()`
/// * `let subject = CurrentValueSubject<String, Never>("toto")`
///
/// - Returns: The violation offset.
private func defaultValueViolationOffset(file: SwiftLintFile,
dictionary: SourceKittenDictionary) -> ByteCount? {
guard
let offset = dictionary.offset,
let length = dictionary.length,
case let byteRange = ByteRange(location: offset, length: length),
let range = file.stringView.byteRangeToNSRange(byteRange),
case let subjects = subjectTypes.joined(separator: "|"),
case let pattern = "(\(subjects))<(.+)>\\((.*)\\)",
file.match(pattern: pattern, range: range).isEmpty == false
else {
return nil
}
return dictionary.nameOffset
public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension PrivateSubjectRule {
final class Visitor: ViolationsSyntaxVisitor {
private let subjectTypes: Set<String> = ["PassthroughSubject", "CurrentValueSubject"]
override func visitPost(_ node: VariableDeclSyntax) {
guard !node.modifiers.isPrivateOrFileprivate,
!node.modifiers.containsStaticOrClass else {
return
}
for binding in node.bindings {
// Looks for violations matching the format:
//
// * `let subject: PassthroughSubject<Bool, Never>`
// * `let subject: PassthroughSubject<Bool, Never> = .init()`
// * `let subject: CurrentValueSubject<Bool, Never>`
// * `let subject: CurrentValueSubject<String, Never> = .init("toto")`
if let type = binding.typeAnnotation?.type.as(SimpleTypeIdentifierSyntax.self),
subjectTypes.contains(type.name.text) {
violations.append(binding.pattern.positionAfterSkippingLeadingTrivia)
continue
}
// Looks for violations matching the format:
//
// * `let subject = PassthroughSubject<Bool, Never>()`
// * `let subject = CurrentValueSubject<String, Never>("toto")`
if let functionCall = binding.initializer?.value.as(FunctionCallExprSyntax.self),
let specializeExpr = functionCall.calledExpression.as(SpecializeExprSyntax.self),
let identifierExpr = specializeExpr.expression.as(IdentifierExprSyntax.self),
subjectTypes.contains(identifierExpr.identifier.text) {
violations.append(binding.pattern.positionAfterSkippingLeadingTrivia)
}
}
}
}
}
@@ -188,7 +188,7 @@ internal struct PrivateSubjectRuleExamples {
Example(
#"""
final class Foobar {
let goodSubject: CurrentValueSubject<String, Never> = .ini("toto")
let goodSubject: CurrentValueSubject<String, Never> = .init("toto")
}
"""#
),