Skip to content

Commit

Permalink
Rewrite type_name rule with SwiftSyntax (realm#4428)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelofabri authored Oct 20, 2022
1 parent 59ad84f commit 469a3aa
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 42 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
- `trailing_comma`
- `trailing_semicolon`
- `type_body_length`
- `type_name`
- `unneeded_break_in_switch`
- `unneeded_parentheses_in_closure_argument`
- `unowned_variable_capture`
Expand Down
160 changes: 118 additions & 42 deletions Source/SwiftLintFramework/Rules/Idiomatic/TypeNameRule.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation
import SourceKittenFramework
import SwiftSyntax

public struct TypeNameRule: ASTRule, ConfigurationProviderRule {
public struct TypeNameRule: SwiftSyntaxRule, ConfigurationProviderRule {
public var configuration = NameConfiguration(minLengthWarning: 3,
minLengthError: 0,
maxLengthWarning: 40,
Expand All @@ -20,59 +20,135 @@ public struct TypeNameRule: ASTRule, ConfigurationProviderRule {
triggeringExamples: TypeNameRuleExamples.triggeringExamples
)

private let typeKinds = SwiftDeclarationKind.typeKinds
public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(configuration: configuration)
}
}

public func validate(file: SwiftLintFile, kind: SwiftDeclarationKind,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
guard typeKinds.contains(kind),
let name = dictionary.name,
let offset = dictionary.nameOffset else {
return []
private extension TypeNameRule {
final class Visitor: ViolationsSyntaxVisitor {
private let configuration: NameConfiguration

init(configuration: NameConfiguration) {
self.configuration = configuration
super.init(viewMode: .sourceAccurate)
}

return validate(name: name, dictionary: dictionary, file: file, offset: offset)
}
override func visitPost(_ node: StructDeclSyntax) {
if let violation = violation(identifier: node.identifier, modifiers: node.modifiers,
inheritedTypes: node.inheritanceClause?.inheritedTypeCollection) {
violations.append(violation)
}
}

override func visitPost(_ node: ClassDeclSyntax) {
if let violation = violation(identifier: node.identifier, modifiers: node.modifiers,
inheritedTypes: node.inheritanceClause?.inheritedTypeCollection) {
violations.append(violation)
}
}

private func validate(name: String, dictionary: SourceKittenDictionary = SourceKittenDictionary([:]),
file: SwiftLintFile, offset: ByteCount) -> [StyleViolation] {
guard !configuration.excluded.contains(name) else {
return []
override func visitPost(_ node: TypealiasDeclSyntax) {
if let violation = violation(identifier: node.identifier, modifiers: node.modifiers, inheritedTypes: nil) {
violations.append(violation)
}
}

let name = name
.nameStrippingLeadingUnderscoreIfPrivate(dictionary)
.nameStrippingTrailingSwiftUIPreviewProvider(dictionary)
let allowedSymbols = configuration.allowedSymbols.union(.alphanumerics)
if !allowedSymbols.isSuperset(of: CharacterSet(charactersIn: name)) {
return [StyleViolation(ruleDescription: Self.description,
severity: .error,
location: Location(file: file, byteOffset: offset),
reason: "Type name should only contain alphanumeric characters: '\(name)'")]
} else if configuration.validatesStartWithLowercase &&
name.first?.isLowercase == true {
return [StyleViolation(ruleDescription: Self.description,
severity: .error,
location: Location(file: file, byteOffset: offset),
reason: "Type name should start with an uppercase character: '\(name)'")]
} else if let severity = severity(forLength: name.count) {
return [StyleViolation(ruleDescription: Self.description,
severity: severity,
location: Location(file: file, byteOffset: offset),
reason: "Type name should be between \(configuration.minLengthThreshold) and " +
"\(configuration.maxLengthThreshold) characters long: '\(name)'")]
override func visitPost(_ node: AssociatedtypeDeclSyntax) {
if let violation = violation(identifier: node.identifier, modifiers: node.modifiers,
inheritedTypes: node.inheritanceClause?.inheritedTypeCollection) {
violations.append(violation)
}
}

return []
override func visitPost(_ node: EnumDeclSyntax) {
if let violation = violation(identifier: node.identifier, modifiers: node.modifiers,
inheritedTypes: node.inheritanceClause?.inheritedTypeCollection) {
violations.append(violation)
}
}

override func visitPost(_ node: ActorDeclSyntax) {
if let violation = violation(identifier: node.identifier, modifiers: node.modifiers,
inheritedTypes: node.inheritanceClause?.inheritedTypeCollection) {
violations.append(violation)
}
}

private func violation(identifier: TokenSyntax,
modifiers: ModifierListSyntax?,
inheritedTypes: InheritedTypeListSyntax?) -> ReasonedRuleViolation? {
let originalName = identifier.text
guard !configuration.excluded.contains(originalName) else {
return nil
}

let name = originalName
.strippingLeadingUnderscoreIfPrivate(modifiers: modifiers)
.strippingTrailingSwiftUIPreviewProvider(inheritedTypes: inheritedTypes)
let allowedSymbols = configuration.allowedSymbols.union(.alphanumerics)

if !allowedSymbols.isSuperset(of: CharacterSet(charactersIn: name)) {
return ReasonedRuleViolation(
position: identifier.positionAfterSkippingLeadingTrivia,
reason: "Type name should only contain alphanumeric characters: '\(name)'",
severity: .error
)
} else if configuration.validatesStartWithLowercase &&
name.first?.isLowercase == true {
return ReasonedRuleViolation(
position: identifier.positionAfterSkippingLeadingTrivia,
reason: "Type name should start with an uppercase character: '\(name)'",
severity: .error
)
} else if let severity = configuration.severity(forLength: name.count) {
return ReasonedRuleViolation(
position: identifier.positionAfterSkippingLeadingTrivia,
reason: "Type name should be between \(configuration.minLengthThreshold) and " +
"\(configuration.maxLengthThreshold) characters long: '\(name)'",
severity: severity
)
}

return nil
}
}
}

private extension String {
func nameStrippingTrailingSwiftUIPreviewProvider(_ dictionary: SourceKittenDictionary) -> String {
guard dictionary.inheritedTypes.contains("PreviewProvider"),
hasSuffix("_Previews"),
let lastPreviewsIndex = lastIndex(of: "_Previews")
else { return self }
func strippingTrailingSwiftUIPreviewProvider(inheritedTypes: InheritedTypeListSyntax?) -> String {
guard let inheritedTypes = inheritedTypes,
hasSuffix("_Previews"),
let lastPreviewsIndex = lastIndex(of: "_Previews"),
inheritedTypes.typeNames.contains("PreviewProvider") else {
return self
}

return substring(from: 0, length: lastPreviewsIndex)
}

func strippingLeadingUnderscoreIfPrivate(modifiers: ModifierListSyntax?) -> String {
if first == "_", modifiers.isPrivateOrFileprivate {
return String(self[index(after: startIndex)...])
}
return self
}
}

private extension InheritedTypeListSyntax {
var typeNames: Set<String> {
Set(compactMap { $0.typeName.as(SimpleTypeIdentifierSyntax.self) }.map(\.name.text))
}
}

private extension ModifierListSyntax? {
var isPrivateOrFileprivate: Bool {
guard let modifiers = self else {
return false
}

return modifiers.contains { elem in
elem.name.tokenKind == .privateKeyword || elem.name.tokenKind == .fileprivateKeyword
}
}
}

0 comments on commit 469a3aa

Please sign in to comment.