mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
Rewrite redundant_objc_attribute with SwiftSyntax (#4441)
This commit is contained in:
@@ -132,6 +132,7 @@
|
||||
- `reduce_boolean`
|
||||
- `redundant_discardable_let`
|
||||
- `redundant_nil_coalescing`
|
||||
- `redundant_objc_attribute`
|
||||
- `redundant_string_enum_value`
|
||||
- `required_deinit`
|
||||
- `self_in_property_initialization`
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import Foundation
|
||||
import SourceKittenFramework
|
||||
import SwiftSyntax
|
||||
|
||||
private let kindsImplyingObjc: Set<SwiftDeclarationAttributeKind> =
|
||||
[.ibaction, .iboutlet, .ibinspectable, .gkinspectable, .ibdesignable, .nsManaged]
|
||||
private let attributeNamesImplyingObjc: Set<String> = [
|
||||
"IBAction", "IBOutlet", "IBInspectable", "GKInspectable", "IBDesignable", "NSManaged"
|
||||
]
|
||||
|
||||
public struct RedundantObjcAttributeRule: SubstitutionCorrectableRule, ConfigurationProviderRule {
|
||||
public struct RedundantObjcAttributeRule: SwiftSyntaxRule, SubstitutionCorrectableRule, ConfigurationProviderRule {
|
||||
public var configuration = SeverityConfiguration(.warning)
|
||||
|
||||
public init() {}
|
||||
@@ -16,84 +17,97 @@ public struct RedundantObjcAttributeRule: SubstitutionCorrectableRule, Configura
|
||||
kind: .idiomatic,
|
||||
nonTriggeringExamples: RedundantObjcAttributeRuleExamples.nonTriggeringExamples,
|
||||
triggeringExamples: RedundantObjcAttributeRuleExamples.triggeringExamples,
|
||||
corrections: RedundantObjcAttributeRuleExamples.corrections)
|
||||
corrections: RedundantObjcAttributeRuleExamples.corrections
|
||||
)
|
||||
|
||||
public func validate(file: SwiftLintFile) -> [StyleViolation] {
|
||||
return violationRanges(in: file).map {
|
||||
StyleViolation(ruleDescription: Self.description,
|
||||
severity: configuration.severity,
|
||||
location: Location(file: file, characterOffset: $0.location))
|
||||
public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||
final class Visitor: ViolationsSyntaxVisitor {
|
||||
override func visitPost(_ node: AttributeListSyntax) {
|
||||
if let objcAttribute = node.violatingObjCAttribute {
|
||||
violations.append(objcAttribute.positionAfterSkippingLeadingTrivia)
|
||||
}
|
||||
}
|
||||
}
|
||||
return Visitor(viewMode: .sourceAccurate)
|
||||
}
|
||||
|
||||
public func violationRanges(in file: SwiftLintFile) -> [NSRange] {
|
||||
return file.structureDictionary.traverseWithParentDepthFirst { parent, subDict in
|
||||
guard let kind = subDict.declarationKind else { return nil }
|
||||
return violationRanges(file: file, kind: kind, dictionary: subDict, parentStructure: parent)
|
||||
}
|
||||
}
|
||||
|
||||
private func violationRanges(file: SwiftLintFile,
|
||||
kind: SwiftDeclarationKind,
|
||||
dictionary: SourceKittenDictionary,
|
||||
parentStructure: SourceKittenDictionary?) -> [NSRange] {
|
||||
let objcAttribute = dictionary.swiftAttributes
|
||||
.first(where: { $0.attribute == SwiftDeclarationAttributeKind.objc.rawValue })
|
||||
guard let objcByteRange = objcAttribute?.byteRange,
|
||||
let range = file.stringView.byteRangeToNSRange(objcByteRange),
|
||||
!dictionary.isObjcAndIBDesignableDeclaredExtension
|
||||
else {
|
||||
return []
|
||||
}
|
||||
|
||||
let isInObjcVisibleScope = { () -> Bool in
|
||||
guard let parentStructure = parentStructure,
|
||||
let kind = dictionary.declarationKind,
|
||||
let parentKind = parentStructure.declarationKind else {
|
||||
return false
|
||||
makeVisitor(file: file)
|
||||
.walk(tree: file.syntaxTree, handler: \.violations)
|
||||
.compactMap { violation in
|
||||
let end = AbsolutePosition(utf8Offset: violation.position.utf8Offset + "@objc".count)
|
||||
return file.stringView.NSRange(start: violation.position, end: end)
|
||||
}
|
||||
|
||||
let isInObjCExtension = [.extensionClass, .extension].contains(parentKind) &&
|
||||
parentStructure.enclosedSwiftAttributes.contains(.objc)
|
||||
|
||||
let isPrivate = dictionary.accessibility?.isPrivate ?? false
|
||||
let isInObjcMembers = parentStructure.enclosedSwiftAttributes.contains(.objcMembers) && !isPrivate
|
||||
|
||||
guard isInObjCExtension || isInObjcMembers else {
|
||||
return false
|
||||
}
|
||||
|
||||
return !SwiftDeclarationKind.typeKinds.contains(kind)
|
||||
}
|
||||
|
||||
let isUsedWithObjcAttribute = !Set(dictionary.enclosedSwiftAttributes).isDisjoint(with: kindsImplyingObjc)
|
||||
|
||||
if isUsedWithObjcAttribute || isInObjcVisibleScope() {
|
||||
return [range]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private extension SourceKittenDictionary {
|
||||
var isObjcAndIBDesignableDeclaredExtension: Bool {
|
||||
guard let declaration = declarationKind else {
|
||||
private extension AttributeListSyntax {
|
||||
var hasObjCMembers: Bool {
|
||||
contains { $0.as(AttributeSyntax.self)?.attributeName.tokenKind == .identifier("objcMembers") }
|
||||
}
|
||||
|
||||
var objCAttribute: AttributeSyntax? {
|
||||
lazy
|
||||
.compactMap { $0.as(AttributeSyntax.self) }
|
||||
.first { attribute in
|
||||
attribute.attributeName.tokenKind == .contextualKeyword("objc") &&
|
||||
attribute.argument == nil
|
||||
}
|
||||
}
|
||||
|
||||
var hasAttributeImplyingObjC: Bool {
|
||||
contains { element in
|
||||
guard case let .identifier(attributeName) = element.as(AttributeSyntax.self)?.attributeName.tokenKind else {
|
||||
return false
|
||||
}
|
||||
|
||||
return attributeNamesImplyingObjc.contains(attributeName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Syntax {
|
||||
var isFunctionOrStoredProperty: Bool {
|
||||
if self.is(FunctionDeclSyntax.self) {
|
||||
return true
|
||||
} else if let variableDecl = self.as(VariableDeclSyntax.self),
|
||||
variableDecl.bindings.allSatisfy({ $0.accessor == nil }) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return [.extensionClass, .extension].contains(declaration)
|
||||
&& Set(enclosedSwiftAttributes).isSuperset(of: [.ibdesignable, .objc])
|
||||
}
|
||||
}
|
||||
|
||||
private extension AttributeListSyntax {
|
||||
var violatingObjCAttribute: AttributeSyntax? {
|
||||
guard let objcAttribute = objCAttribute else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if hasAttributeImplyingObjC, parent?.is(ExtensionDeclSyntax.self) != true {
|
||||
return objcAttribute
|
||||
} else if parent?.isFunctionOrStoredProperty == true,
|
||||
let parentClassDecl = parent?.parent?.parent?.parent?.parent?.as(ClassDeclSyntax.self),
|
||||
parentClassDecl.attributes?.hasObjCMembers == true {
|
||||
return objcAttribute
|
||||
} else if let parentExtensionDecl = parent?.parent?.parent?.parent?.parent?.as(ExtensionDeclSyntax.self),
|
||||
parentExtensionDecl.attributes?.objCAttribute != nil {
|
||||
return objcAttribute
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension RedundantObjcAttributeRule {
|
||||
func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? {
|
||||
func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? {
|
||||
var whitespaceAndNewlineOffset = 0
|
||||
let nsCharSet = CharacterSet.whitespacesAndNewlines.bridge()
|
||||
let nsContent = file.contents.bridge()
|
||||
while nsCharSet
|
||||
.characterIsMember(nsContent.character(at: violationRange.upperBound + whitespaceAndNewlineOffset)) {
|
||||
whitespaceAndNewlineOffset += 1
|
||||
whitespaceAndNewlineOffset += 1
|
||||
}
|
||||
|
||||
let withTrailingWhitespaceAndNewlineRange = NSRange(location: violationRange.location,
|
||||
|
||||
@@ -69,6 +69,12 @@ struct RedundantObjcAttributeRuleExamples {
|
||||
class Foo {
|
||||
@objc class Bar {}
|
||||
}
|
||||
"""),
|
||||
Example("""
|
||||
extension BlockEditorSettings {
|
||||
@objc(addElementsObject:)
|
||||
@NSManaged public func addToElements(_ value: BlockEditorSettingElement)
|
||||
}
|
||||
""")
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user