Skip to content

Commit

Permalink
Update file_name rule to match fully-qualified names of nested types
Browse files Browse the repository at this point in the history
This PR is aimed to address Issue #5840. It does the following:

1. Allows the `file_name` rule to match nested types when using fully-qualified names, meaning naming the following file `Nested.MyType.swift` is no longer a violation:

```
// Nested.MyType.swift

enum Nested {
    struct MyType {
    }
}
```

2. Introduces a new option `require_fully_qualified` to have the `file_name` rule enforce using fully-qualified names, meaning naming the above file `MyType.swift` instead of `Nested.MyType.swift` would become a violation where it wasn't before (naming the file `Nested.swift` would still not be a violation).
  • Loading branch information
Nick Fraioli committed Oct 28, 2024
1 parent 01f5ecd commit 9399bc9
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 9 deletions.
81 changes: 73 additions & 8 deletions Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ struct FileNameRule: OptInRule, SourceKitFreeRule {
}

// Process nested type separator
let allDeclaredTypeNames = TypeNameCollectingVisitor(viewMode: .sourceAccurate)
let allDeclaredTypeNames = TypeNameCollectingVisitor(requireFullyQualifiedNames: configuration.fullyQualified)
.walk(tree: file.syntaxTree, handler: \.names)
.map {
$0.replacingOccurrences(of: ".", with: configuration.nestedTypeSeparator)
Expand All @@ -56,33 +56,98 @@ struct FileNameRule: OptInRule, SourceKitFreeRule {
}

private class TypeNameCollectingVisitor: SyntaxVisitor {
// All of a visited node's ancestor type names if that node is nested, starting with the furthest
// ancestor and ending with the direct parent
private var ancestorNames: [String] = []

// All of the type names found in the file
private(set) var names: Set<String> = []

// If true, nested types are only allowed in the file name when used by their fully-qualified name
// (e.g. `My.Nested.Type` and not just `Type`)
private let requireFullyQualifiedNames: Bool

init(requireFullyQualifiedNames: Bool) {
self.requireFullyQualifiedNames = requireFullyQualifiedNames
super.init(viewMode: .sourceAccurate)
}

private func addVisitedNodeName(_ name: String) {
let fullyQualifiedName = (ancestorNames + [name]).joined(separator: ".")
names.insert(fullyQualifiedName)

if !requireFullyQualifiedNames {
names.insert(name)
}
}

override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
ancestorNames.append(node.name.text)
return .visitChildren
}

override func visitPost(_ node: ClassDeclSyntax) {
names.insert(node.name.text)
ancestorNames.removeLast()
addVisitedNodeName(node.name.text)
}

override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
ancestorNames.append(node.name.text)
return .visitChildren
}

override func visitPost(_ node: ActorDeclSyntax) {
names.insert(node.name.text)
ancestorNames.removeLast()
addVisitedNodeName(node.name.text)
}

override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
ancestorNames.append(node.name.text)
return .visitChildren
}

override func visitPost(_ node: StructDeclSyntax) {
names.insert(node.name.text)
ancestorNames.removeLast()
addVisitedNodeName(node.name.text)
}

override func visit(_ node: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind {
ancestorNames.append(node.name.text)
return .visitChildren
}

override func visitPost(_ node: TypeAliasDeclSyntax) {
names.insert(node.name.text)
ancestorNames.removeLast()
addVisitedNodeName(node.name.text)
}

override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
ancestorNames.append(node.name.text)
return .visitChildren
}

override func visitPost(_ node: EnumDeclSyntax) {
names.insert(node.name.text)
ancestorNames.removeLast()
addVisitedNodeName(node.name.text)
}

override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
ancestorNames.append(node.name.text)
return .visitChildren
}

override func visitPost(_ node: ProtocolDeclSyntax) {
names.insert(node.name.text)
ancestorNames.removeLast()
addVisitedNodeName(node.name.text)
}

override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
ancestorNames.append(node.extendedType.trimmedDescription)
return .visitChildren
}

override func visitPost(_ node: ExtensionDeclSyntax) {
names.insert(node.extendedType.trimmedDescription)
ancestorNames.removeLast()
addVisitedNodeName(node.extendedType.trimmedDescription)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ struct FileNameConfiguration: SeverityBasedRuleConfiguration {
private(set) var suffixPattern = "\\+.*"
@ConfigurationElement(key: "nested_type_separator")
private(set) var nestedTypeSeparator = "."
@ConfigurationElement(key: "fully_qualified")
private(set) var fullyQualified = false
}
21 changes: 20 additions & 1 deletion Tests/SwiftLintFrameworkTests/FileNameRuleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ final class FileNameRuleTests: SwiftLintTestCase {
excludedOverride: [String]? = nil,
prefixPattern: String? = nil,
suffixPattern: String? = nil,
nestedTypeSeparator: String? = nil) throws -> [StyleViolation] {
nestedTypeSeparator: String? = nil,
fullyQualified: Bool? = nil) throws -> [StyleViolation] {
let file = SwiftLintFile(path: fixturesDirectory.stringByAppendingPathComponent(fileName))!
let rule: FileNameRule
if let excluded = excludedOverride {
Expand All @@ -21,6 +22,8 @@ final class FileNameRuleTests: SwiftLintTestCase {
rule = try FileNameRule(configuration: ["suffix_pattern": suffixPattern])
} else if let nestedTypeSeparator {
rule = try FileNameRule(configuration: ["nested_type_separator": nestedTypeSeparator])
} else if let fullyQualified {
rule = try FileNameRule(configuration: ["fully_qualified": fullyQualified])
} else {
rule = FileNameRule()
}
Expand Down Expand Up @@ -52,6 +55,22 @@ final class FileNameRuleTests: SwiftLintTestCase {
XCTAssert(try validate(fileName: "Notification.Name+Extension.swift").isEmpty)
}

func testNestedTypeDoesntTrigger() {
XCTAssert(try validate(fileName: "Nested.MyType.swift").isEmpty)
}

func testMultipleLevelsDeepNestedTypeDoesntTrigger() {
XCTAssert(try validate(fileName: "Multiple.Levels.Deep.Nested.MyType.swift").isEmpty)
}

func testNestedTypeNotFullyQualifiedDoesntTrigger() {
XCTAssert(try validate(fileName: "MyType.swift").isEmpty)
}

func testNestedTypeNotFullyQualifiedDoesTriggerWithOverride() {
XCTAssert(try !validate(fileName: "MyType.swift", fullyQualified: true).isEmpty)
}

func testNestedTypeSeparatorDoesntTrigger() {
XCTAssert(try validate(fileName: "NotificationName+Extension.swift", nestedTypeSeparator: "").isEmpty)
XCTAssert(try validate(fileName: "Notification__Name+Extension.swift", nestedTypeSeparator: "__").isEmpty)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
extension Multiple {
enum Levels {
class Deep {
struct Nested {
actor MyType {}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
enum Nested {
struct MyType {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
enum Nested {
struct MyType {
}
}

0 comments on commit 9399bc9

Please sign in to comment.