Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExtensionAccessModifierRule.swift
2024-12-26 20:40:29 +01:00

254 lines
7.6 KiB
Swift

import SwiftLintCore
import SwiftSyntax
@SwiftSyntaxRule(optIn: true)
struct ExtensionAccessModifierRule: Rule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "extension_access_modifier",
name: "Extension Access Modifier",
description: "Prefer to use extension access modifiers",
kind: .idiomatic,
nonTriggeringExamples: [
Example("""
extension Foo: SomeProtocol {
public var bar: Int { return 1 }
}
"""),
Example("""
extension Foo {
private var bar: Int { return 1 }
public var baz: Int { return 1 }
}
"""),
Example("""
extension Foo {
private var bar: Int { return 1 }
public func baz() {}
}
"""),
Example("""
extension Foo {
var bar: Int { return 1 }
var baz: Int { return 1 }
}
"""),
Example("""
extension Foo {
var bar: Int { return 1 }
internal var baz: Int { return 1 }
}
"""),
Example("""
internal extension Foo {
var bar: Int { return 1 }
var baz: Int { return 1 }
}
"""),
Example("""
public extension Foo {
var bar: Int { return 1 }
var baz: Int { return 1 }
}
"""),
Example("""
public extension Foo {
var bar: Int { return 1 }
internal var baz: Int { return 1 }
}
"""),
Example("""
extension Foo {
private var bar: Int { return 1 }
private var baz: Int { return 1 }
}
"""),
Example("""
extension Foo {
open var bar: Int { return 1 }
open var baz: Int { return 1 }
}
"""),
Example("""
extension Foo {
func setup() {}
public func update() {}
}
"""),
Example("""
private extension Foo {
private var bar: Int { return 1 }
var baz: Int { return 1 }
}
"""),
Example("""
extension Foo {
internal private(set) var bar: Int {
get { Foo.shared.bar }
set { Foo.shared.bar = newValue }
}
}
"""),
Example("""
extension Foo {
private(set) internal var bar: Int {
get { Foo.shared.bar }
set { Foo.shared.bar = newValue }
}
}
"""),
Example("""
public extension Foo {
private(set) var value: Int { 1 }
}
"""),
],
triggeringExamples: [
Example("""
↓extension Foo {
public var bar: Int { return 1 }
public var baz: Int { return 1 }
}
"""),
Example("""
↓extension Foo {
public var bar: Int { return 1 }
public func baz() {}
}
"""),
Example("""
public extension Foo {
↓public func bar() {}
↓public func baz() {}
}
"""),
Example("""
↓extension Foo {
public var bar: Int {
let value = 1
return value
}
public var baz: Int { return 1 }
}
"""),
Example("""
↓extension Array where Element: Equatable {
public var unique: [Element] {
var uniqueValues = [Element]()
for item in self where !uniqueValues.contains(item) {
uniqueValues.append(item)
}
return uniqueValues
}
}
"""),
Example("""
↓extension Foo {
#if DEBUG
public var bar: Int {
let value = 1
return value
}
#endif
public var baz: Int { return 1 }
}
"""),
Example("""
public extension Foo {
↓private func bar() {}
↓private func baz() {}
}
"""),
Example("""
↓extension Foo {
private(set) public var value: Int { 1 }
}
"""),
]
)
}
private extension ExtensionAccessModifierRule {
private enum ACL: Hashable {
case implicit
case explicit(TokenKind)
static func from(tokenKind: TokenKind?) -> Self {
switch tokenKind {
case nil:
return .implicit
case let value?:
return .explicit(value)
}
}
static func isAllowed(_ acl: Self) -> Bool {
[
.explicit(.keyword(.internal)),
.explicit(.keyword(.private)),
.explicit(.keyword(.open)),
.implicit,
].contains(acl)
}
}
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all }
override func visitPost(_ node: ExtensionDeclSyntax) {
guard node.inheritanceClause == nil else {
return
}
var areAllACLsEqual = true
var aclTokens = [(position: AbsolutePosition, acl: ACL)]()
for decl in node.memberBlock.expandingIfConfigs() {
let modifiers = decl.asProtocol((any WithModifiersSyntax).self)?.modifiers
let aclToken = modifiers?.accessLevelModifier()?.name
let acl = ACL.from(tokenKind: aclToken?.tokenKind)
if areAllACLsEqual, acl != aclTokens.last?.acl, aclTokens.isNotEmpty {
areAllACLsEqual = false
}
aclTokens.append((decl.positionAfterSkippingLeadingTrivia, acl))
}
guard areAllACLsEqual, let lastACL = aclTokens.last else {
return
}
let isAllowedACL = ACL.isAllowed(lastACL.acl)
let extensionACL = ACL.from(tokenKind: node.modifiers.accessLevelModifier?.name.tokenKind)
if extensionACL != .implicit {
if !isAllowedACL || lastACL.acl != extensionACL, lastACL.acl != .implicit {
violations.append(contentsOf: aclTokens.map(\.position))
}
} else if !isAllowedACL {
violations.append(node.extensionKeyword.positionAfterSkippingLeadingTrivia)
}
}
}
}
private extension MemberBlockSyntax {
func expandingIfConfigs() -> [DeclSyntax] {
members.flatMap { member in
if let ifConfig = member.decl.as(IfConfigDeclSyntax.self) {
return ifConfig.clauses.flatMap { clause in
switch clause.elements {
case .decls(let decls):
return decls.map(\.decl)
default:
return []
}
}
}
return [member.decl]
}
}
}