mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
Rewrite private_subject rule with SwiftSyntax (#4495)
This commit is contained in:
@@ -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")
|
||||
}
|
||||
"""#
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user