Skip to content

Commit

Permalink
Update file_name rule to allow fully-qualified names of nested types (
Browse files Browse the repository at this point in the history
  • Loading branch information
fraioli authored Nov 26, 2024
1 parent 75e5e05 commit b9d33e4
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 34 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@
[Martin Redington](https://github.com/mildm8nnered)
[#5711](https://github.com/realm/SwiftLint/issues/5711)

* Fixes `file_name` rule to match fully-qualified names of nested types.
Additionally adds a `require_fully_qualified_names` boolean option to enforce
that file names match nested types only using their fully-qualified name.
[fraioli](https://github.com/fraioli)
[#5840](https://github.com/realm/SwiftLint/issues/5840)

## 0.57.0: Squeaky Clean Cycle

#### Breaking
Expand Down
95 changes: 80 additions & 15 deletions Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ struct FileNameRule: OptInRule, SourceKitFreeRule {
}

// Process nested type separator
let allDeclaredTypeNames = TypeNameCollectingVisitor(viewMode: .sourceAccurate)
let allDeclaredTypeNames = TypeNameCollectingVisitor(
requireFullyQualifiedNames: configuration.requireFullyQualifiedNames
)
.walk(tree: file.syntaxTree, handler: \.names)
.map {
$0.replacingOccurrences(of: ".", with: configuration.nestedTypeSeparator)
Expand All @@ -56,33 +58,96 @@ 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 = Stack<String>()

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

override func visitPost(_ node: ClassDeclSyntax) {
names.insert(node.name.text)
/// 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)
}

/// Calls `visit(name:)` using the name of the provided node
private func visit(node: some NamedDeclSyntax) -> SyntaxVisitorContinueKind {
visit(name: node.name.trimmedDescription)
}

/// Visits a node with the provided name, storing that name as an ancestor type name to prepend to
/// any children to form their fully-qualified names
private func visit(name: String) -> SyntaxVisitorContinueKind {
let fullyQualifiedName = (ancestorNames + [name]).joined(separator: ".")
names.insert(fullyQualifiedName)

// If the options don't require only fully-qualified names, then we will allow this node's
// name to be used by itself
if !requireFullyQualifiedNames {
names.insert(name)
}

ancestorNames.push(name)
return .visitChildren
}

override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
visit(node: node)
}

override func visitPost(_: ClassDeclSyntax) {
ancestorNames.pop()
}

override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
visit(node: node)
}

override func visitPost(_: ActorDeclSyntax) {
ancestorNames.pop()
}

override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
visit(node: node)
}

override func visitPost(_: StructDeclSyntax) {
ancestorNames.pop()
}

override func visit(_ node: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind {
visit(node: node)
}

override func visitPost(_: TypeAliasDeclSyntax) {
ancestorNames.pop()
}

override func visitPost(_ node: ActorDeclSyntax) {
names.insert(node.name.text)
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
visit(node: node)
}

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

override func visitPost(_ node: TypeAliasDeclSyntax) {
names.insert(node.name.text)
override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
visit(node: node)
}

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

override func visitPost(_ node: ProtocolDeclSyntax) {
names.insert(node.name.text)
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
visit(name: node.extendedType.trimmedDescription)
}

override func visitPost(_ node: ExtensionDeclSyntax) {
names.insert(node.extendedType.trimmedDescription)
override func visitPost(_: ExtensionDeclSyntax) {
ancestorNames.pop()
}
}
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: "require_fully_qualified_names")
private(set) var requireFullyQualifiedNames = false
}
1 change: 1 addition & 0 deletions Tests/IntegrationTests/default_rule_configurations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ file_name:
prefix_pattern: ""
suffix_pattern: "\+.*"
nested_type_separator: "."
require_fully_qualified_names: false
file_name_no_space:
severity: warning
excluded: []
Expand Down
61 changes: 42 additions & 19 deletions Tests/SwiftLintFrameworkTests/FileNameRuleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,33 @@ private let fixturesDirectory = "\(TestResources.path)/FileNameRuleFixtures"

final class FileNameRuleTests: SwiftLintTestCase {
private func validate(fileName: String,
excludedOverride: [String]? = nil,
excluded: [String]? = nil,
prefixPattern: String? = nil,
suffixPattern: String? = nil,
nestedTypeSeparator: String? = nil) throws -> [StyleViolation] {
nestedTypeSeparator: String? = nil,
requireFullyQualifiedNames: Bool = false) throws -> [StyleViolation] {
let file = SwiftLintFile(path: fixturesDirectory.stringByAppendingPathComponent(fileName))!
let rule: FileNameRule
if let excluded = excludedOverride {
rule = try FileNameRule(configuration: ["excluded": excluded])
} else if let prefixPattern, let suffixPattern {
rule = try FileNameRule(configuration: ["prefix_pattern": prefixPattern, "suffix_pattern": suffixPattern])
} else if let prefixPattern {
rule = try FileNameRule(configuration: ["prefix_pattern": prefixPattern])
} else if let suffixPattern {
rule = try FileNameRule(configuration: ["suffix_pattern": suffixPattern])
} else if let nestedTypeSeparator {
rule = try FileNameRule(configuration: ["nested_type_separator": nestedTypeSeparator])
} else {
rule = FileNameRule()

var configuration = [String: Any]()

if let excluded {
configuration["excluded"] = excluded
}
if let prefixPattern {
configuration["prefix_pattern"] = prefixPattern
}
if let suffixPattern {
configuration["suffix_pattern"] = suffixPattern
}
if let nestedTypeSeparator {
configuration["nested_type_separator"] = nestedTypeSeparator
}
if requireFullyQualifiedNames {
configuration["require_fully_qualified_names"] = requireFullyQualifiedNames
}

let rule = try FileNameRule(configuration: configuration)

return rule.validate(file: file)
}

Expand Down Expand Up @@ -52,26 +59,42 @@ final class FileNameRuleTests: SwiftLintTestCase {
XCTAssert(try validate(fileName: "Notification.Name+Extension.swift").isEmpty)
}

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

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

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

func testNestedTypeNotFullyQualifiedDoesTriggerWithOverride() {
XCTAssert(try validate(fileName: "MyType.swift", requireFullyQualifiedNames: true).isNotEmpty)
}

func testNestedTypeSeparatorDoesntTrigger() {
XCTAssert(try validate(fileName: "NotificationName+Extension.swift", nestedTypeSeparator: "").isEmpty)
XCTAssert(try validate(fileName: "Notification__Name+Extension.swift", nestedTypeSeparator: "__").isEmpty)
}

func testWrongNestedTypeSeparatorDoesTrigger() {
XCTAssert(try !validate(fileName: "Notification__Name+Extension.swift", nestedTypeSeparator: ".").isEmpty)
XCTAssert(try !validate(fileName: "NotificationName+Extension.swift", nestedTypeSeparator: "__").isEmpty)
XCTAssert(try validate(fileName: "Notification__Name+Extension.swift", nestedTypeSeparator: ".").isNotEmpty)
XCTAssert(try validate(fileName: "NotificationName+Extension.swift", nestedTypeSeparator: "__").isNotEmpty)
}

func testMisspelledNameDoesTrigger() {
XCTAssertEqual(try validate(fileName: "MyStructf.swift").count, 1)
}

func testMisspelledNameDoesntTriggerWithOverride() {
XCTAssert(try validate(fileName: "MyStructf.swift", excludedOverride: ["MyStructf.swift"]).isEmpty)
XCTAssert(try validate(fileName: "MyStructf.swift", excluded: ["MyStructf.swift"]).isEmpty)
}

func testMainDoesTriggerWithoutOverride() {
XCTAssertEqual(try validate(fileName: "main.swift", excludedOverride: []).count, 1)
XCTAssertEqual(try validate(fileName: "main.swift", excluded: []).count, 1)
}

func testCustomSuffixPattern() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
extension Multiple {
enum Levels {
class Deeply {
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 b9d33e4

Please sign in to comment.