diff --git a/CHANGELOG.md b/CHANGELOG.md index af4d275c8..0a9634743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -138,6 +138,7 @@ - `private_action` - `private_over_fileprivate` - `private_outlet` + - `private_subject` - `private_unit_test` - `prohibited_interface_builder` - `protocol_property_accessors_order` diff --git a/Source/SwiftLintFramework/Rules/Lint/PrivateSubjectRule.swift b/Source/SwiftLintFramework/Rules/Lint/PrivateSubjectRule.swift index 0bb9cfc62..476839efc 100644 --- a/Source/SwiftLintFramework/Rules/Lint/PrivateSubjectRule.swift +++ b/Source/SwiftLintFramework/Rules/Lint/PrivateSubjectRule.swift @@ -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 = ["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` - /// * `let subject: PassthroughSubject = .init()` - /// * `let subject: CurrentValueSubject` - /// * `let subject: CurrentValueSubject = .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()` - /// * `let subject = CurrentValueSubject("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 = ["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` + // * `let subject: PassthroughSubject = .init()` + // * `let subject: CurrentValueSubject` + // * `let subject: CurrentValueSubject = .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()` + // * `let subject = CurrentValueSubject("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) + } + } + } } } diff --git a/Source/SwiftLintFramework/Rules/Lint/PrivateSubjectRuleExamples.swift b/Source/SwiftLintFramework/Rules/Lint/PrivateSubjectRuleExamples.swift index 9cbd8da51..671e602ae 100644 --- a/Source/SwiftLintFramework/Rules/Lint/PrivateSubjectRuleExamples.swift +++ b/Source/SwiftLintFramework/Rules/Lint/PrivateSubjectRuleExamples.swift @@ -188,7 +188,7 @@ internal struct PrivateSubjectRuleExamples { Example( #""" final class Foobar { - let goodSubject: CurrentValueSubject = .ini("toto") + let goodSubject: CurrentValueSubject = .init("toto") } """# ),