mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
257 lines
10 KiB
Swift
257 lines
10 KiB
Swift
import Foundation
|
|
import SwiftSyntax
|
|
|
|
/// An identifier declaration.
|
|
public enum IdentifierDeclaration: Hashable {
|
|
/// Parameter declaration with a name token.
|
|
case parameter(name: TokenSyntax)
|
|
/// Local variable declaration with a name token.
|
|
case localVariable(name: TokenSyntax)
|
|
/// A member variable declaration with a name token.
|
|
case memberVariable(name: TokenSyntax)
|
|
/// A variable that is implicitly added by the compiler (e.g. `error` in `catch` clauses).
|
|
case implicitVariable(name: String)
|
|
/// A variable hidden from scope because its name is a wildcard `_`.
|
|
case wildcard
|
|
/// Special case that marks a type boundary at which name lookup stops.
|
|
case lookupBoundary
|
|
|
|
/// The name of the declared identifier (e.g. in `let a = 1` this is `a`).
|
|
fileprivate var name: String {
|
|
switch self {
|
|
case let .parameter(name): name.text
|
|
case let .localVariable(name): name.text
|
|
case let .memberVariable(name): name.text
|
|
case let .implicitVariable(name): name
|
|
case .wildcard: "_"
|
|
case .lookupBoundary: ""
|
|
}
|
|
}
|
|
|
|
/// Check whether self declares a variable given by name.
|
|
///
|
|
/// - Parameters:
|
|
/// - id: Name of the variable.
|
|
/// - disregardBackticks: If `true`, normalize all names before comparison by removing all backticks. This is the
|
|
/// default since backticks only disambiguate, but don't contribute to name resolution.
|
|
public func declares(id: String, disregardBackticks: Bool = true) -> Bool {
|
|
if self == .wildcard || id == "_" {
|
|
// Insignificant names cannot refer to each other.
|
|
return false
|
|
}
|
|
if disregardBackticks {
|
|
let backticks = CharacterSet(charactersIn: "`")
|
|
return id.trimmingCharacters(in: backticks) == name.trimmingCharacters(in: backticks)
|
|
}
|
|
return id == name
|
|
}
|
|
}
|
|
|
|
/// A specialized `ViolationsSyntaxVisitor` that tracks declared identifiers per scope while traversing the AST.
|
|
open class DeclaredIdentifiersTrackingVisitor<Configuration: RuleConfiguration>:
|
|
ViolationsSyntaxVisitor<Configuration> {
|
|
/// A type that remembers the declared identifiers (in order) up to the current position in the code.
|
|
public typealias Scope = Stack<[IdentifierDeclaration]>
|
|
|
|
/// Whether to include class/struct/actor/enum member declarations in the scope. If `false`, only function-local
|
|
/// scopes are tracked.
|
|
public let includeMembers: Bool
|
|
|
|
/// The hierarchical stack of identifiers declared up to the current position in the code.
|
|
public var scope: Scope
|
|
|
|
/// Initializer.
|
|
///
|
|
/// - Parameters:
|
|
/// - configuration: Configuration of a rule.
|
|
/// - file: File from which the syntax tree stems from.
|
|
/// - includeMembers: Whether to include class/struct/actor/enum member declarations in the scope.
|
|
/// - scope: A (potentially already pre-filled) scope to collect identifiers into.
|
|
@inlinable
|
|
public init(configuration: Configuration,
|
|
file: SwiftLintFile,
|
|
includeMembers: Bool = false,
|
|
scope: Scope = Scope()) {
|
|
self.includeMembers = includeMembers
|
|
self.scope = scope
|
|
super.init(configuration: configuration, file: file)
|
|
}
|
|
|
|
/// Indicate whether a given identifier is in scope.
|
|
///
|
|
/// - Parameters:
|
|
/// - identifier: An identifier.
|
|
/// - Returns: `true` if the identifier was declared previously.
|
|
public func hasSeenDeclaration(for identifier: String) -> Bool {
|
|
scope.contains { $0.contains { $0.declares(id: identifier) } }
|
|
}
|
|
|
|
override open func visit(_ node: CodeBlockItemListSyntax) -> SyntaxVisitorContinueKind {
|
|
scope.openChildScope()
|
|
guard let parent = node.parent, !parent.is(SourceFileSyntax.self), let grandParent = parent.parent else {
|
|
return .visitChildren
|
|
}
|
|
if let ifStmt = grandParent.as(IfExprSyntax.self), parent.keyPathInParent != \IfExprSyntax.elseBody {
|
|
collectIdentifiers(from: ifStmt.conditions)
|
|
} else if let whileStmt = grandParent.as(WhileStmtSyntax.self) {
|
|
collectIdentifiers(from: whileStmt.conditions)
|
|
} else if let pattern = grandParent.as(ForStmtSyntax.self)?.pattern {
|
|
collectIdentifiers(from: pattern)
|
|
} else if let parameters = grandParent.as(FunctionDeclSyntax.self)?.signature.parameterClause.parameters {
|
|
collectIdentifiers(from: parameters)
|
|
} else if let parameters = grandParent.as(InitializerDeclSyntax.self)?.signature.parameterClause.parameters {
|
|
collectIdentifiers(from: parameters)
|
|
} else if let parameters = grandParent.as(SubscriptDeclSyntax.self)?.parameterClause.parameters {
|
|
collectIdentifiers(from: parameters)
|
|
} else if let closureParameters = parent.as(ClosureExprSyntax.self)?.signature?.parameterClause {
|
|
collectIdentifiers(from: closureParameters)
|
|
} else if let switchCase = parent.as(SwitchCaseSyntax.self)?.label.as(SwitchCaseLabelSyntax.self) {
|
|
collectIdentifiers(from: switchCase)
|
|
} else if let catchClause = grandParent.as(CatchClauseSyntax.self) {
|
|
collectIdentifiers(from: catchClause)
|
|
}
|
|
return .visitChildren
|
|
}
|
|
|
|
override open func visitPost(_: CodeBlockItemListSyntax) {
|
|
scope.pop()
|
|
}
|
|
|
|
override open func visitPost(_ node: VariableDeclSyntax) {
|
|
if node.parent?.is(MemberBlockItemSyntax.self) != true {
|
|
for binding in node.bindings {
|
|
collectIdentifiers(from: binding.pattern)
|
|
}
|
|
}
|
|
}
|
|
|
|
override open func visitPost(_ node: GuardStmtSyntax) {
|
|
collectIdentifiers(from: node.conditions)
|
|
}
|
|
|
|
// MARK: Type declaration boundaries
|
|
|
|
override open func visit(_ node: MemberBlockSyntax) -> SyntaxVisitorContinueKind {
|
|
if node.belongsToTypeDefinableInFunction {
|
|
scope.push([.lookupBoundary])
|
|
}
|
|
if includeMembers {
|
|
scope.openChildScope()
|
|
for binding in node.members.compactMap({ $0.decl.as(VariableDeclSyntax.self) }).flatMap(\.bindings) {
|
|
if let id = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier {
|
|
scope.addToCurrentScope(.memberVariable(name: id))
|
|
}
|
|
}
|
|
}
|
|
return .visitChildren
|
|
}
|
|
|
|
override open func visitPost(_ node: MemberBlockSyntax) {
|
|
if node.belongsToTypeDefinableInFunction {
|
|
scope.pop()
|
|
}
|
|
if includeMembers {
|
|
scope.pop()
|
|
}
|
|
}
|
|
|
|
override open func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
if node.parent?.is(MemberBlockItemSyntax.self) != true {
|
|
scope.addToCurrentScope(.localVariable(name: node.name))
|
|
}
|
|
return .visitChildren
|
|
}
|
|
|
|
// MARK: Private methods
|
|
|
|
private func collectIdentifiers(from parameters: FunctionParameterListSyntax) {
|
|
for param in parameters {
|
|
let name = param.secondName ?? param.firstName
|
|
scope.addToCurrentScope(.parameter(name: name))
|
|
}
|
|
}
|
|
|
|
private func collectIdentifiers(from closureParameters: ClosureSignatureSyntax.ParameterClause) {
|
|
switch closureParameters {
|
|
case let .parameterClause(parameters):
|
|
for param in parameters.parameters {
|
|
let name = param.secondName ?? param.firstName
|
|
scope.addToCurrentScope(.parameter(name: name))
|
|
}
|
|
case let .simpleInput(parameters):
|
|
for param in parameters {
|
|
let name = param.name
|
|
scope.addToCurrentScope(.parameter(name: name))
|
|
}
|
|
}
|
|
}
|
|
|
|
private func collectIdentifiers(from switchCase: SwitchCaseLabelSyntax) {
|
|
switchCase.caseItems
|
|
.map { item -> PatternSyntax in
|
|
item.pattern.as(ValueBindingPatternSyntax.self)?.pattern ?? item.pattern
|
|
}
|
|
.compactMap { pattern -> FunctionCallExprSyntax? in
|
|
pattern.as(ExpressionPatternSyntax.self)?.expression.asFunctionCall
|
|
}
|
|
.map(\.arguments)
|
|
.flatMap(\.self)
|
|
.compactMap { labeledExpr -> PatternExprSyntax? in
|
|
labeledExpr.expression.as(PatternExprSyntax.self)
|
|
}
|
|
.map { patternExpr -> any PatternSyntaxProtocol in
|
|
patternExpr.pattern.as(ValueBindingPatternSyntax.self)?.pattern ?? patternExpr.pattern
|
|
}
|
|
.forEach {
|
|
collectIdentifiers(from: PatternSyntax(fromProtocol: $0))
|
|
}
|
|
}
|
|
|
|
private func collectIdentifiers(from catchClause: CatchClauseSyntax) {
|
|
let items = catchClause.catchItems
|
|
if items.isEmpty {
|
|
// A catch clause without explicit catch items has an implicit `error` variable in scope.
|
|
scope.addToCurrentScope(.implicitVariable(name: "error"))
|
|
} else {
|
|
items
|
|
.compactMap { $0.pattern?.as(ValueBindingPatternSyntax.self)?.pattern }
|
|
.forEach(collectIdentifiers(from:))
|
|
}
|
|
}
|
|
|
|
private func collectIdentifiers(from conditions: ConditionElementListSyntax) {
|
|
conditions
|
|
.compactMap { $0.condition.as(OptionalBindingConditionSyntax.self)?.pattern }
|
|
.forEach { collectIdentifiers(from: $0) }
|
|
}
|
|
|
|
private func collectIdentifiers(from pattern: PatternSyntax) {
|
|
if let id = pattern.as(IdentifierPatternSyntax.self)?.identifier {
|
|
scope.addToCurrentScope(.localVariable(name: id))
|
|
} else if let tuple = pattern.as(TuplePatternSyntax.self) {
|
|
for element in tuple.elements {
|
|
collectIdentifiers(from: element.pattern)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension DeclaredIdentifiersTrackingVisitor.Scope {
|
|
mutating func addToCurrentScope(_ decl: IdentifierDeclaration) {
|
|
modifyLast { $0.append(decl.name == "_" ? .wildcard : decl) }
|
|
}
|
|
|
|
mutating func openChildScope() {
|
|
push([])
|
|
}
|
|
}
|
|
|
|
private extension MemberBlockSyntax {
|
|
var belongsToTypeDefinableInFunction: Bool {
|
|
if let parent {
|
|
return [.actorDecl, .classDecl, .enumDecl, .structDecl].contains(parent.kind)
|
|
}
|
|
return false
|
|
}
|
|
}
|