mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
162 lines
5.2 KiB
Swift
162 lines
5.2 KiB
Swift
import SwiftSyntax
|
|
|
|
struct FileNameRule: OptInRule, SourceKitFreeRule {
|
|
var configuration = FileNameConfiguration()
|
|
|
|
static let description = RuleDescription(
|
|
identifier: "file_name",
|
|
name: "File Name",
|
|
description: "File name should match a type or extension declared in the file (if any)",
|
|
kind: .idiomatic
|
|
)
|
|
|
|
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
|
guard let filePath = file.path,
|
|
!configuration.shouldExclude(filePath: filePath) else {
|
|
return []
|
|
}
|
|
|
|
let prefixRegex = regex("\\A(?:\(configuration.prefixPattern))")
|
|
let suffixRegex = regex("(?:\(configuration.suffixPattern))\\z")
|
|
|
|
let fileName = filePath.bridge().lastPathComponent
|
|
var typeInFileName = fileName.bridge().deletingPathExtension
|
|
|
|
// Process prefix
|
|
if let match = prefixRegex.firstMatch(in: typeInFileName, options: [], range: typeInFileName.fullNSRange),
|
|
let range = typeInFileName.nsrangeToIndexRange(match.range) {
|
|
typeInFileName.removeSubrange(range)
|
|
}
|
|
|
|
// Process suffix
|
|
if let match = suffixRegex.firstMatch(in: typeInFileName, options: [], range: typeInFileName.fullNSRange),
|
|
let range = typeInFileName.nsrangeToIndexRange(match.range) {
|
|
typeInFileName.removeSubrange(range)
|
|
}
|
|
|
|
// Process nested type separator
|
|
let allDeclaredTypeNames = TypeNameCollectingVisitor(
|
|
requireFullyQualifiedNames: configuration.requireFullyQualifiedNames
|
|
)
|
|
.walk(tree: file.syntaxTree, handler: \.names)
|
|
.map {
|
|
$0.replacingOccurrences(of: ".", with: configuration.nestedTypeSeparator)
|
|
}
|
|
|
|
guard allDeclaredTypeNames.isNotEmpty, !allDeclaredTypeNames.contains(typeInFileName) else {
|
|
return []
|
|
}
|
|
|
|
return [
|
|
StyleViolation(
|
|
ruleDescription: Self.description,
|
|
severity: configuration.severity,
|
|
location: Location(file: filePath, line: 1)
|
|
),
|
|
]
|
|
}
|
|
}
|
|
|
|
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> = []
|
|
|
|
/// 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 visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
visit(node: node)
|
|
}
|
|
|
|
override func visitPost(_: EnumDeclSyntax) {
|
|
ancestorNames.pop()
|
|
}
|
|
|
|
override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
visit(node: node)
|
|
}
|
|
|
|
override func visitPost(_: ProtocolDeclSyntax) {
|
|
ancestorNames.pop()
|
|
}
|
|
|
|
override func visit(_ node: MacroDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
visit(node: node)
|
|
}
|
|
|
|
override func visitPost(_: MacroDeclSyntax) {
|
|
ancestorNames.pop()
|
|
}
|
|
|
|
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
visit(name: node.extendedType.trimmedDescription)
|
|
}
|
|
|
|
override func visitPost(_: ExtensionDeclSyntax) {
|
|
ancestorNames.pop()
|
|
}
|
|
}
|