SwiftLint/Source/SwiftLintFramework/Rules/Lint/LowerACLThanParentRule.swift

244 lines
9.7 KiB
Swift

import SwiftSyntax
struct LowerACLThanParentRule: OptInRule, ConfigurationProviderRule, SwiftSyntaxCorrectableRule {
var configuration = SeverityConfiguration(.warning)
init() {}
static let description = RuleDescription(
identifier: "lower_acl_than_parent",
name: "Lower ACL than Parent",
description: "Ensure declarations have a lower access control level than their enclosing parent",
kind: .lint,
nonTriggeringExamples: [
Example("public struct Foo { public func bar() {} }"),
Example("internal struct Foo { func bar() {} }"),
Example("struct Foo { func bar() {} }"),
Example("struct Foo { internal func bar() {} }"),
Example("open class Foo { public func bar() {} }"),
Example("open class Foo { open func bar() {} }"),
Example("fileprivate struct Foo { private func bar() {} }"),
Example("private struct Foo { private func bar(id: String) }"),
Example("extension Foo { public func bar() {} }"),
Example("private struct Foo { fileprivate func bar() {} }"),
Example("private func foo(id: String) {}"),
Example("private class Foo { func bar() {} }"),
Example("public extension Foo { struct Bar { public func baz() {} }}"),
Example("public extension Foo { struct Bar { internal func baz() {} }}"),
Example("internal extension Foo { struct Bar { internal func baz() {} }}"),
Example("extension Foo { struct Bar { internal func baz() {} }}")
],
triggeringExamples: [
Example("struct Foo { ↓public func bar() {} }"),
Example("enum Foo { ↓public func bar() {} }"),
Example("public class Foo { ↓open func bar() }"),
Example("class Foo { ↓public private(set) var bar: String? }"),
Example("private struct Foo { ↓public func bar() {} }"),
Example("private class Foo { ↓public func bar() {} }"),
Example("private actor Foo { ↓public func bar() {} }"),
Example("fileprivate struct Foo { ↓public func bar() {} }"),
Example("class Foo { ↓public func bar() {} }"),
Example("actor Foo { ↓public func bar() {} }"),
Example("private struct Foo { ↓internal func bar() {} }"),
Example("fileprivate struct Foo { ↓internal func bar() {} }"),
Example("extension Foo { struct Bar { ↓public func baz() {} }}"),
Example("internal extension Foo { struct Bar { ↓public func baz() {} }}"),
Example("private extension Foo { struct Bar { ↓public func baz() {} }}"),
Example("fileprivate extension Foo { struct Bar { ↓public func baz() {} }}"),
Example("private extension Foo { struct Bar { ↓internal func baz() {} }}"),
Example("fileprivate extension Foo { struct Bar { ↓internal func baz() {} }}"),
Example("public extension Foo { struct Bar { struct Baz { ↓public func qux() {} }}}"),
Example("final class Foo { ↓public func bar() {} }")
],
corrections: [
Example("struct Foo { ↓public func bar() {} }"):
Example("struct Foo { func bar() {} }"),
Example("enum Foo { ↓public func bar() {} }"):
Example("enum Foo { func bar() {} }"),
Example("public class Foo { ↓open func bar() }"):
Example("public class Foo { func bar() }"),
Example("class Foo { ↓public private(set) var bar: String? }"):
Example("class Foo { private(set) var bar: String? }"),
Example("private struct Foo { ↓public func bar() {} }"):
Example("private struct Foo { func bar() {} }"),
Example("private class Foo { ↓public func bar() {} }"):
Example("private class Foo { func bar() {} }"),
Example("private actor Foo { ↓public func bar() {} }"):
Example("private actor Foo { func bar() {} }"),
Example("class Foo { ↓public func bar() {} }"):
Example("class Foo { func bar() {} }"),
Example("actor Foo { ↓public func bar() {} }"):
Example("actor Foo { func bar() {} }")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
Rewriter(
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
private extension LowerACLThanParentRule {
private final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: DeclModifierSyntax) {
if node.isHigherACLThanParent {
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
private(set) var correctionPositions: [AbsolutePosition] = []
let locationConverter: SourceLocationConverter
let disabledRegions: [SourceRange]
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
override func visit(_ node: DeclModifierSyntax) -> DeclModifierSyntax {
guard
node.isHigherACLThanParent,
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
else {
return super.visit(node)
}
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
let newNode = node.withName(
.contextualKeyword("", leadingTrivia: node.leadingTrivia ?? .zero)
)
return super.visit(newNode)
}
}
}
private extension DeclModifierSyntax {
var isHigherACLThanParent: Bool {
guard let nearestNominalParent = parent?.nearestNominalParent() else {
return false
}
switch name.tokenKind {
case .internalKeyword
where nearestNominalParent.modifiers.isPrivate ||
nearestNominalParent.modifiers.isFileprivate:
return true
case .internalKeyword
where !nearestNominalParent.modifiers.containsACLModifier:
guard let nominalExtension = nearestNominalParent.nearestNominalExtensionDeclParent() else {
return false
}
return nominalExtension.modifiers.isPrivate ||
nominalExtension.modifiers.isFileprivate
case .publicKeyword
where nearestNominalParent.modifiers.isPrivate ||
nearestNominalParent.modifiers.isFileprivate ||
nearestNominalParent.modifiers.isInternal:
return true
case .publicKeyword
where !nearestNominalParent.modifiers.containsACLModifier:
guard let nominalExtension = nearestNominalParent.nearestNominalExtensionDeclParent() else {
return true
}
return !nominalExtension.modifiers.isPublic
case .contextualKeyword("open") where !nearestNominalParent.modifiers.isOpen:
return true
default:
return false
}
}
}
private extension SyntaxProtocol {
func nearestNominalParent() -> Syntax? {
guard let parent else {
return nil
}
return parent.isNominalTypeDecl ? parent : parent.nearestNominalParent()
}
func nearestNominalExtensionDeclParent() -> Syntax? {
guard let parent, !parent.isNominalTypeDecl else {
return nil
}
return parent.isExtensionDecl ? parent : parent.nearestNominalExtensionDeclParent()
}
}
private extension Syntax {
var isNominalTypeDecl: Bool {
self.is(StructDeclSyntax.self) ||
self.is(ClassDeclSyntax.self) ||
self.is(ActorDeclSyntax.self) ||
self.is(EnumDeclSyntax.self)
}
var isExtensionDecl: Bool {
self.is(ExtensionDeclSyntax.self)
}
var modifiers: ModifierListSyntax? {
if let node = self.as(StructDeclSyntax.self) {
return node.modifiers
} else if let node = self.as(ClassDeclSyntax.self) {
return node.modifiers
} else if let node = self.as(ActorDeclSyntax.self) {
return node.modifiers
} else if let node = self.as(EnumDeclSyntax.self) {
return node.modifiers
} else if let node = self.as(ExtensionDeclSyntax.self) {
return node.modifiers
} else {
return nil
}
}
}
private extension ModifierListSyntax? {
var isFileprivate: Bool {
self?.contains(where: { $0.name.tokenKind == .fileprivateKeyword }) == true
}
var isPrivate: Bool {
self?.contains(where: { $0.name.tokenKind == .privateKeyword }) == true
}
var isInternal: Bool {
self?.contains(where: { $0.name.tokenKind == .internalKeyword }) == true
}
var isPublic: Bool {
self?.contains(where: { $0.name.tokenKind == .publicKeyword }) == true
}
var isOpen: Bool {
self?.contains(where: { $0.name.tokenKind == .contextualKeyword("open") }) == true
}
var containsACLModifier: Bool {
guard self?.isEmpty == false else {
return false
}
let aclTokens: [TokenKind] = [
.fileprivateKeyword,
.privateKeyword,
.internalKeyword,
.publicKeyword,
.contextualKeyword("open")
]
return self?.contains(where: {
aclTokens.contains($0.name.tokenKind)
}) == true
}
}