Skip to content

Commit 9399bc9

Browse files
author
Nick Fraioli
committed
Update file_name rule to match fully-qualified names of nested types
This PR is aimed to address Issue realm#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).
1 parent 01f5ecd commit 9399bc9

File tree

6 files changed

+111
-9
lines changed

6 files changed

+111
-9
lines changed

Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ struct FileNameRule: OptInRule, SourceKitFreeRule {
3535
}
3636

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

5858
private class TypeNameCollectingVisitor: SyntaxVisitor {
59+
// All of a visited node's ancestor type names if that node is nested, starting with the furthest
60+
// ancestor and ending with the direct parent
61+
private var ancestorNames: [String] = []
62+
63+
// All of the type names found in the file
5964
private(set) var names: Set<String> = []
6065

66+
// If true, nested types are only allowed in the file name when used by their fully-qualified name
67+
// (e.g. `My.Nested.Type` and not just `Type`)
68+
private let requireFullyQualifiedNames: Bool
69+
70+
init(requireFullyQualifiedNames: Bool) {
71+
self.requireFullyQualifiedNames = requireFullyQualifiedNames
72+
super.init(viewMode: .sourceAccurate)
73+
}
74+
75+
private func addVisitedNodeName(_ name: String) {
76+
let fullyQualifiedName = (ancestorNames + [name]).joined(separator: ".")
77+
names.insert(fullyQualifiedName)
78+
79+
if !requireFullyQualifiedNames {
80+
names.insert(name)
81+
}
82+
}
83+
84+
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
85+
ancestorNames.append(node.name.text)
86+
return .visitChildren
87+
}
88+
6189
override func visitPost(_ node: ClassDeclSyntax) {
62-
names.insert(node.name.text)
90+
ancestorNames.removeLast()
91+
addVisitedNodeName(node.name.text)
92+
}
93+
94+
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
95+
ancestorNames.append(node.name.text)
96+
return .visitChildren
6397
}
6498

6599
override func visitPost(_ node: ActorDeclSyntax) {
66-
names.insert(node.name.text)
100+
ancestorNames.removeLast()
101+
addVisitedNodeName(node.name.text)
102+
}
103+
104+
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
105+
ancestorNames.append(node.name.text)
106+
return .visitChildren
67107
}
68108

69109
override func visitPost(_ node: StructDeclSyntax) {
70-
names.insert(node.name.text)
110+
ancestorNames.removeLast()
111+
addVisitedNodeName(node.name.text)
112+
}
113+
114+
override func visit(_ node: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind {
115+
ancestorNames.append(node.name.text)
116+
return .visitChildren
71117
}
72118

73119
override func visitPost(_ node: TypeAliasDeclSyntax) {
74-
names.insert(node.name.text)
120+
ancestorNames.removeLast()
121+
addVisitedNodeName(node.name.text)
122+
}
123+
124+
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
125+
ancestorNames.append(node.name.text)
126+
return .visitChildren
75127
}
76128

77129
override func visitPost(_ node: EnumDeclSyntax) {
78-
names.insert(node.name.text)
130+
ancestorNames.removeLast()
131+
addVisitedNodeName(node.name.text)
132+
}
133+
134+
override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
135+
ancestorNames.append(node.name.text)
136+
return .visitChildren
79137
}
80138

81139
override func visitPost(_ node: ProtocolDeclSyntax) {
82-
names.insert(node.name.text)
140+
ancestorNames.removeLast()
141+
addVisitedNodeName(node.name.text)
142+
}
143+
144+
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
145+
ancestorNames.append(node.extendedType.trimmedDescription)
146+
return .visitChildren
83147
}
84148

85149
override func visitPost(_ node: ExtensionDeclSyntax) {
86-
names.insert(node.extendedType.trimmedDescription)
150+
ancestorNames.removeLast()
151+
addVisitedNodeName(node.extendedType.trimmedDescription)
87152
}
88153
}

Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@ struct FileNameConfiguration: SeverityBasedRuleConfiguration {
1414
private(set) var suffixPattern = "\\+.*"
1515
@ConfigurationElement(key: "nested_type_separator")
1616
private(set) var nestedTypeSeparator = "."
17+
@ConfigurationElement(key: "fully_qualified")
18+
private(set) var fullyQualified = false
1719
}

Tests/SwiftLintFrameworkTests/FileNameRuleTests.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ final class FileNameRuleTests: SwiftLintTestCase {
88
excludedOverride: [String]? = nil,
99
prefixPattern: String? = nil,
1010
suffixPattern: String? = nil,
11-
nestedTypeSeparator: String? = nil) throws -> [StyleViolation] {
11+
nestedTypeSeparator: String? = nil,
12+
fullyQualified: Bool? = nil) throws -> [StyleViolation] {
1213
let file = SwiftLintFile(path: fixturesDirectory.stringByAppendingPathComponent(fileName))!
1314
let rule: FileNameRule
1415
if let excluded = excludedOverride {
@@ -21,6 +22,8 @@ final class FileNameRuleTests: SwiftLintTestCase {
2122
rule = try FileNameRule(configuration: ["suffix_pattern": suffixPattern])
2223
} else if let nestedTypeSeparator {
2324
rule = try FileNameRule(configuration: ["nested_type_separator": nestedTypeSeparator])
25+
} else if let fullyQualified {
26+
rule = try FileNameRule(configuration: ["fully_qualified": fullyQualified])
2427
} else {
2528
rule = FileNameRule()
2629
}
@@ -52,6 +55,22 @@ final class FileNameRuleTests: SwiftLintTestCase {
5255
XCTAssert(try validate(fileName: "Notification.Name+Extension.swift").isEmpty)
5356
}
5457

58+
func testNestedTypeDoesntTrigger() {
59+
XCTAssert(try validate(fileName: "Nested.MyType.swift").isEmpty)
60+
}
61+
62+
func testMultipleLevelsDeepNestedTypeDoesntTrigger() {
63+
XCTAssert(try validate(fileName: "Multiple.Levels.Deep.Nested.MyType.swift").isEmpty)
64+
}
65+
66+
func testNestedTypeNotFullyQualifiedDoesntTrigger() {
67+
XCTAssert(try validate(fileName: "MyType.swift").isEmpty)
68+
}
69+
70+
func testNestedTypeNotFullyQualifiedDoesTriggerWithOverride() {
71+
XCTAssert(try !validate(fileName: "MyType.swift", fullyQualified: true).isEmpty)
72+
}
73+
5574
func testNestedTypeSeparatorDoesntTrigger() {
5675
XCTAssert(try validate(fileName: "NotificationName+Extension.swift", nestedTypeSeparator: "").isEmpty)
5776
XCTAssert(try validate(fileName: "Notification__Name+Extension.swift", nestedTypeSeparator: "__").isEmpty)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
extension Multiple {
2+
enum Levels {
3+
class Deep {
4+
struct Nested {
5+
actor MyType {}
6+
}
7+
}
8+
}
9+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
enum Nested {
2+
struct MyType {}
3+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
enum Nested {
2+
struct MyType {
3+
}
4+
}

0 commit comments

Comments
 (0)