mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
394 lines
16 KiB
Swift
394 lines
16 KiB
Swift
import SwiftSyntax
|
|
|
|
@SwiftSyntaxRule(correctable: true, optIn: true)
|
|
struct PreferSelfInStaticReferencesRule: Rule {
|
|
var configuration = SeverityConfiguration<Self>(.warning)
|
|
|
|
static let description = RuleDescription(
|
|
identifier: "prefer_self_in_static_references",
|
|
name: "Prefer Self in Static References",
|
|
description: "Use `Self` to refer to the surrounding type name",
|
|
kind: .style,
|
|
nonTriggeringExamples: PreferSelfInStaticReferencesRuleExamples.nonTriggeringExamples,
|
|
triggeringExamples: PreferSelfInStaticReferencesRuleExamples.triggeringExamples,
|
|
corrections: PreferSelfInStaticReferencesRuleExamples.corrections
|
|
)
|
|
}
|
|
|
|
private extension PreferSelfInStaticReferencesRule {
|
|
private enum ExtendedType {
|
|
/// A type named by a plain identifier, e.g. `extension Foo`.
|
|
case identifier(String)
|
|
/// A member type, e.g. `extension Foo.Bar`, stored as its component path.
|
|
case memberType([String])
|
|
}
|
|
|
|
private enum ParentDeclBehavior {
|
|
case likeClass(ExtendedType)
|
|
case likeStruct(String)
|
|
case skipReferences
|
|
|
|
/// The single identifier matched for token-based violations and for the
|
|
/// nested-type shadowing check. `nil` for member-type extensions, which
|
|
/// are matched structurally instead.
|
|
var parentName: String? {
|
|
switch self {
|
|
case let .likeClass(.identifier(name)): return name
|
|
case .likeClass(.memberType): return nil
|
|
case let .likeStruct(name): return name
|
|
case .skipReferences: return nil
|
|
}
|
|
}
|
|
|
|
/// The component path of a member-type extension, e.g. `["Foo", "Bar"]`,
|
|
/// or `nil` for any other scope.
|
|
var memberTypeComponents: [String]? {
|
|
if case let .likeClass(.memberType(components)) = self {
|
|
return components
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private enum VariableDeclBehavior {
|
|
case handleReferences
|
|
case skipReferences
|
|
}
|
|
|
|
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
|
|
private var parentDeclScopes = Stack<ParentDeclBehavior>()
|
|
private var variableDeclScopes = Stack<VariableDeclBehavior>()
|
|
|
|
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
pushParentDeclScope(.likeClass(.identifier(node.name.text)), memberBlock: node.memberBlock)
|
|
return .skipChildren
|
|
}
|
|
|
|
override func visitPost(_: ActorDeclSyntax) {
|
|
parentDeclScopes.pop()
|
|
}
|
|
|
|
override func visit(_: AttributeSyntax) -> SyntaxVisitorContinueKind {
|
|
if case .skipReferences = variableDeclScopes.peek() {
|
|
return .skipChildren
|
|
}
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
pushParentDeclScope(.likeClass(.identifier(node.name.text)), memberBlock: node.memberBlock)
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visitPost(_: ClassDeclSyntax) {
|
|
parentDeclScopes.pop()
|
|
}
|
|
|
|
override func visit(_: CodeBlockItemListSyntax) -> SyntaxVisitorContinueKind {
|
|
variableDeclScopes.push(.handleReferences)
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visitPost(_: CodeBlockItemListSyntax) {
|
|
variableDeclScopes.pop()
|
|
}
|
|
|
|
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
pushParentDeclScope(.likeStruct(node.name.text), memberBlock: node.memberBlock)
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visitPost(_: EnumDeclSyntax) {
|
|
parentDeclScopes.pop()
|
|
}
|
|
|
|
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
// Treat the extended type as a class. Without knowing the exact
|
|
// declaration kind, this is the most conservative scope that still
|
|
// surfaces violations on member-access expressions inside methods,
|
|
// computed properties and closure bodies. Cases the class scope
|
|
// already skips (e.g. stored-property initializers, function
|
|
// signatures) remain skipped to avoid false positives.
|
|
if let type = extendedType(of: node.extendedType) {
|
|
pushParentDeclScope(.likeClass(type), memberBlock: node.memberBlock)
|
|
} else {
|
|
parentDeclScopes.push(.skipReferences)
|
|
}
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visitPost(_: ExtensionDeclSyntax) {
|
|
parentDeclScopes.pop()
|
|
}
|
|
|
|
private func extendedType(of type: TypeSyntax) -> ExtendedType? {
|
|
if let identifier = type.as(IdentifierTypeSyntax.self) {
|
|
return .identifier(identifier.name.text)
|
|
}
|
|
if let memberType = type.as(MemberTypeSyntax.self), let tokens = memberTypeChain(memberType) {
|
|
return .memberType(tokens.map(\.text))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
override func visit(_ node: MemberAccessExprSyntax) -> SyntaxVisitorContinueKind {
|
|
if case .likeClass = parentDeclScopes.peek() {
|
|
if node.declName.baseName.tokenKind == .keyword(.self) {
|
|
return .skipChildren
|
|
}
|
|
}
|
|
// In a member-type extension (e.g. `extension Foo.Bar`), match the
|
|
// extended-type access chain structurally; a single token can't
|
|
// disambiguate `Outer.Inner.x` from `Other.Inner.x`. Skip when the
|
|
// path is the callee of a call or carries explicit generic
|
|
// arguments (`Foo.Bar()`, `Foo.Bar<Int>()`), since `Self` doesn't
|
|
// substitute for those.
|
|
if let components = parentDeclScopes.peek()?.memberTypeComponents,
|
|
node.parent?.is(FunctionCallExprSyntax.self) != true,
|
|
node.parent?.is(GenericSpecializationExprSyntax.self) != true,
|
|
let tokens = memberAccessChain(node),
|
|
tokens.map(\.text) == components {
|
|
addMemberTypeViolation(spanning: tokens)
|
|
return .skipChildren
|
|
}
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visitPost(_ node: DeclReferenceExprSyntax) {
|
|
guard let parent = node.parent, !parent.is(GenericSpecializationExprSyntax.self),
|
|
node.keyPathInParent != \MemberAccessExprSyntax.declName else {
|
|
return
|
|
}
|
|
if parent.is(FunctionCallExprSyntax.self), case .likeClass = parentDeclScopes.peek() {
|
|
return
|
|
}
|
|
addViolation(on: node.baseName)
|
|
}
|
|
|
|
override func visit(_: InitializerClauseSyntax) -> SyntaxVisitorContinueKind {
|
|
if case .skipReferences = variableDeclScopes.peek() {
|
|
return .skipChildren
|
|
}
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visit(_: MemberBlockSyntax) -> SyntaxVisitorContinueKind {
|
|
if case .likeClass = parentDeclScopes.peek() {
|
|
variableDeclScopes.push(.skipReferences)
|
|
} else {
|
|
variableDeclScopes.push(.handleReferences)
|
|
}
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visitPost(_: MemberBlockSyntax) {
|
|
variableDeclScopes.pop()
|
|
}
|
|
|
|
override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind {
|
|
if case .likeClass = parentDeclScopes.peek(), case .identifier("selector") = node.macroName.tokenKind {
|
|
return .visitChildren
|
|
}
|
|
return .skipChildren
|
|
}
|
|
|
|
override func visit(_: FunctionParameterClauseSyntax) -> SyntaxVisitorContinueKind {
|
|
if case .likeStruct = parentDeclScopes.peek() {
|
|
return .visitChildren
|
|
}
|
|
return .skipChildren
|
|
}
|
|
|
|
override func visit(_: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
parentDeclScopes.push(.skipReferences)
|
|
return .skipChildren
|
|
}
|
|
|
|
override func visitPost(_: ProtocolDeclSyntax) {
|
|
parentDeclScopes.pop()
|
|
}
|
|
|
|
override func visit(_: ReturnClauseSyntax) -> SyntaxVisitorContinueKind {
|
|
if case .likeStruct = parentDeclScopes.peek() {
|
|
return .visitChildren
|
|
}
|
|
return .skipChildren
|
|
}
|
|
|
|
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
pushParentDeclScope(.likeStruct(node.name.text), memberBlock: node.memberBlock)
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visitPost(_: StructDeclSyntax) {
|
|
parentDeclScopes.pop()
|
|
}
|
|
|
|
override func visit(_: GenericArgumentListSyntax) -> SyntaxVisitorContinueKind {
|
|
if case .likeClass = parentDeclScopes.peek() {
|
|
return .skipChildren
|
|
}
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visit(_: GenericParameterListSyntax) -> SyntaxVisitorContinueKind {
|
|
if case .likeClass = parentDeclScopes.peek() {
|
|
return .skipChildren
|
|
}
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visit(_: GenericRequirementListSyntax) -> SyntaxVisitorContinueKind {
|
|
if case .likeClass = parentDeclScopes.peek() {
|
|
return .skipChildren
|
|
}
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visitPost(_ node: IdentifierTypeSyntax) {
|
|
guard let parent = node.parent else {
|
|
return
|
|
}
|
|
// Don't flag identifiers that belong to the extension declaration
|
|
// header (the extended type itself); the new class-like scope
|
|
// pushed for the extension would otherwise rewrite it to `Self`.
|
|
if parent.is(ExtensionDeclSyntax.self) {
|
|
return
|
|
}
|
|
if node.genericArguments == nil {
|
|
// Type is specialized.
|
|
addViolation(on: node.name)
|
|
}
|
|
}
|
|
|
|
override func visitPost(_ node: MemberTypeSyntax) {
|
|
guard let components = parentDeclScopes.peek()?.memberTypeComponents else {
|
|
return
|
|
}
|
|
// Don't flag the extended type in the extension header itself.
|
|
if node.parent?.is(ExtensionDeclSyntax.self) == true {
|
|
return
|
|
}
|
|
// Mirror the single-identifier rule: a type appearing as a generic
|
|
// argument is left alone, since `Self` may not be available in that
|
|
// position (e.g. an extension's own inheritance clause).
|
|
if node.parent?.is(GenericArgumentSyntax.self) == true {
|
|
return
|
|
}
|
|
if let tokens = memberTypeChain(node), tokens.map(\.text) == components {
|
|
addMemberTypeViolation(spanning: tokens)
|
|
}
|
|
}
|
|
|
|
override func visit(_: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind {
|
|
if case .likeClass = parentDeclScopes.peek() {
|
|
return .skipChildren
|
|
}
|
|
return .visitChildren
|
|
}
|
|
|
|
override func visit(_ node: TypeAnnotationSyntax) -> SyntaxVisitorContinueKind {
|
|
guard case .likeStruct = parentDeclScopes.peek() else {
|
|
return .skipChildren
|
|
}
|
|
if let varDecl = node.parent?.parent?.parent?.as(VariableDeclSyntax.self) {
|
|
if varDecl.parent?.is(CodeBlockItemSyntax.self) == true // Local variable declaration
|
|
|| varDecl.bindings.onlyElement?.accessorBlock != nil // Computed property
|
|
|| !node.type.is(IdentifierTypeSyntax.self) { // Complex or collection type
|
|
return .visitChildren
|
|
}
|
|
}
|
|
return .skipChildren
|
|
}
|
|
|
|
private func addViolation(on node: TokenSyntax) {
|
|
if let parentName = parentDeclScopes.peek()?.parentName,
|
|
node.tokenKind == .identifier(parentName) {
|
|
violations.append(
|
|
at: node.positionAfterSkippingLeadingTrivia,
|
|
correction: .init(
|
|
start: node.positionAfterSkippingLeadingTrivia,
|
|
end: node.endPositionBeforeTrailingTrivia,
|
|
replacement: "Self"
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private func addMemberTypeViolation(spanning tokens: [TokenSyntax]) {
|
|
guard let first = tokens.first, let last = tokens.last else {
|
|
return
|
|
}
|
|
let start = first.positionAfterSkippingLeadingTrivia
|
|
violations.append(
|
|
at: start,
|
|
correction: .init(
|
|
start: start,
|
|
end: last.endPositionBeforeTrailingTrivia,
|
|
replacement: "Self"
|
|
)
|
|
)
|
|
}
|
|
|
|
/// Flattens an expression member-access chain (e.g. `Foo.Bar`) into its
|
|
/// component tokens, or `nil` if the chain is not a plain type path.
|
|
private func memberAccessChain(_ node: MemberAccessExprSyntax) -> [TokenSyntax]? {
|
|
var tokens = [node.declName.baseName]
|
|
var base = node.base
|
|
while let member = base?.as(MemberAccessExprSyntax.self) {
|
|
tokens.insert(member.declName.baseName, at: 0)
|
|
base = member.base
|
|
}
|
|
guard let reference = base?.as(DeclReferenceExprSyntax.self) else {
|
|
return nil
|
|
}
|
|
tokens.insert(reference.baseName, at: 0)
|
|
return tokens
|
|
}
|
|
|
|
/// Flattens a member type (e.g. `Foo.Bar`) into its component tokens, or
|
|
/// `nil` if the type is not a plain member path. Member types carrying
|
|
/// generic arguments are excluded, since `Self` cannot stand in for them.
|
|
private func memberTypeChain(_ node: MemberTypeSyntax) -> [TokenSyntax]? {
|
|
guard node.genericArgumentClause == nil else {
|
|
return nil
|
|
}
|
|
var tokens = [node.name]
|
|
var base = node.baseType
|
|
while let member = base.as(MemberTypeSyntax.self) {
|
|
guard member.genericArgumentClause == nil else {
|
|
return nil
|
|
}
|
|
tokens.insert(member.name, at: 0)
|
|
base = member.baseType
|
|
}
|
|
guard let identifier = base.as(IdentifierTypeSyntax.self),
|
|
identifier.genericArgumentClause == nil else {
|
|
return nil
|
|
}
|
|
tokens.insert(identifier.name, at: 0)
|
|
return tokens
|
|
}
|
|
|
|
private func pushParentDeclScope(_ behavior: ParentDeclBehavior, memberBlock: MemberBlockSyntax) {
|
|
let hasShadowingNestedType =
|
|
if let name = behavior.parentName {
|
|
containsSameNamedNestedType(named: name, in: memberBlock)
|
|
} else {
|
|
false
|
|
}
|
|
parentDeclScopes.push(hasShadowingNestedType ? .skipReferences : behavior)
|
|
}
|
|
|
|
private func containsSameNamedNestedType(named name: String, in memberBlock: MemberBlockSyntax) -> Bool {
|
|
memberBlock.members.contains { member in
|
|
if member.decl.isProtocol((any DeclGroupSyntax).self) || member.decl.is(TypeAliasDeclSyntax.self) {
|
|
return member.decl.asProtocol((any NamedDeclSyntax).self)?.name.text == name
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|