Rewrite `explicit_acl` rule with SwiftSyntax

This commit is contained in:
Marcelo Fabri 2022-10-30 16:32:40 -07:00
parent 6c7e2107ae
commit f81e28c574
5 changed files with 126 additions and 106 deletions

View File

@ -96,6 +96,7 @@
- `empty_parentheses_with_trailing_closure`
- `empty_string`
- `enum_case_associated_values_count`
- `explicit_acl`
- `explicit_enum_raw_value`
- `explicit_init`
- `explicit_top_level_acl`

View File

@ -148,6 +148,20 @@ extension ModifierListSyntax? {
}
}
extension DeclModifierSyntax {
var isACLModifier: Bool {
let aclModifiers: Set<TokenKind> = [
.privateKeyword,
.fileprivateKeyword,
.internalKeyword,
.publicKeyword,
.contextualKeyword("open")
]
return detail == nil && aclModifiers.contains(name.tokenKind)
}
}
extension VariableDeclSyntax {
var isIBOutlet: Bool {
attributes?.contains { attr in

View File

@ -152,6 +152,14 @@ open class ViolationsSyntaxVisitor: SyntaxVisitor {
skippableDeclarations.contains { $0 == VariableDeclSyntax.self } ? .skipChildren : .visitChildren
}
override open func visit(_ node: SubscriptDeclSyntax) -> SyntaxVisitorContinueKind {
skippableDeclarations.contains { $0 == SubscriptDeclSyntax.self } ? .skipChildren : .visitChildren
}
override open func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
skippableDeclarations.contains { $0 == InitializerDeclSyntax.self } ? .skipChildren : .visitChildren
}
override open func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
skippableDeclarations.contains { $0 == ProtocolDeclSyntax.self } ? .skipChildren : .visitChildren
}
@ -170,7 +178,9 @@ extension Array where Element == DeclSyntaxProtocol.Type {
ExtensionDeclSyntax.self,
ProtocolDeclSyntax.self,
StructDeclSyntax.self,
VariableDeclSyntax.self
VariableDeclSyntax.self,
SubscriptDeclSyntax.self,
InitializerDeclSyntax.self
]
/// Useful for class-specific checks since extensions and protocols do not allow nested classes.

View File

@ -1,9 +1,6 @@
import Foundation
import SourceKittenFramework
import SwiftSyntax
private typealias SourceKittenElement = SourceKittenDictionary
public struct ExplicitACLRule: OptInRule, ConfigurationProviderRule {
public struct ExplicitACLRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
@ -77,7 +74,9 @@ public struct ExplicitACLRule: OptInRule, ConfigurationProviderRule {
Example("↓enum A {}\n"),
Example("final ↓class B {}\n"),
Example("internal struct C { ↓let d = 5 }\n"),
Example("internal struct C { ↓static let d = 5 }\n"),
Example("public struct C { ↓let d = 5 }\n"),
Example("public struct C { ↓init() }\n"),
Example("func a() {}\n"),
Example("internal let a = 0\n↓func b() {}\n"),
Example("""
@ -88,109 +87,119 @@ public struct ExplicitACLRule: OptInRule, ConfigurationProviderRule {
]
)
private func findAllExplicitInternalTokens(in file: SwiftLintFile) -> [ByteRange] {
let contents = file.stringView
return file.match(pattern: "internal", with: [.attributeBuiltin]).compactMap {
contents.NSRangeToByteRange(start: $0.location, length: $0.length)
}
public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private func offsetOfElements(from elements: [SourceKittenElement], in file: SwiftLintFile,
thatAreNotInRanges ranges: [ByteRange]) -> [ByteCount] {
return elements.compactMap { element in
guard let typeOffset = element.offset else {
return nil
}
guard let kind = element.declarationKind,
!SwiftDeclarationKind.extensionKinds.contains(kind) else {
return nil
}
// find the last "internal" token before the type
guard let previousInternalByteRange = lastInternalByteRange(before: typeOffset, in: ranges) else {
return typeOffset
}
// the "internal" token correspond to the type if there're only
// attributeBuiltin (`final` for example) tokens between them
let length = typeOffset - previousInternalByteRange.location
let range = ByteRange(location: previousInternalByteRange.location, length: length)
let internalDoesntBelongToType = Set(file.syntaxMap.kinds(inByteRange: range)) != [.attributeBuiltin]
return internalDoesntBelongToType ? typeOffset : nil
}
}
public func validate(file: SwiftLintFile) -> [StyleViolation] {
let implicitAndExplicitInternalElements = internalTypeElements(in: file.structureDictionary)
guard implicitAndExplicitInternalElements.isNotEmpty else {
return []
private extension ExplicitACLRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] {
[
FunctionDeclSyntax.self,
SubscriptDeclSyntax.self,
VariableDeclSyntax.self,
ProtocolDeclSyntax.self,
InitializerDeclSyntax.self,
]
}
let explicitInternalRanges = findAllExplicitInternalTokens(in: file)
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.classKeyword.positionAfterSkippingLeadingTrivia)
}
let violations = offsetOfElements(from: implicitAndExplicitInternalElements, in: file,
thatAreNotInRanges: explicitInternalRanges)
return violations.map {
StyleViolation(ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, byteOffset: $0))
return node.modifiers.isPrivateOrFileprivate ? .skipChildren : .visitChildren
}
}
private func lastInternalByteRange(before typeOffset: ByteCount, in ranges: [ByteRange]) -> ByteRange? {
let firstPartition = ranges.prefix(while: { typeOffset > $0.location })
return firstPartition.last
}
private func internalTypeElements(in parent: SourceKittenElement) -> [SourceKittenElement] {
return parent.substructure.flatMap { element -> [SourceKittenElement] in
guard let elementKind = element.declarationKind,
elementKind != .varLocal, elementKind != .varParameter else {
return []
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.structKeyword.positionAfterSkippingLeadingTrivia)
}
let isDeinit = elementKind == .functionMethodInstance && element.name == "deinit"
guard !isDeinit else {
return []
return node.modifiers.isPrivateOrFileprivate ? .skipChildren : .visitChildren
}
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.enumKeyword.positionAfterSkippingLeadingTrivia)
}
let isPrivate = element.accessibility?.isPrivate ?? false
let internalTypeElementsInSubstructure = elementKind.childsAreExemptFromACL || isPrivate ? [] :
internalTypeElements(in: element)
return node.modifiers.isPrivateOrFileprivate ? .skipChildren : .visitChildren
}
var isInExtension = false
if let kind = parent.declarationKind {
isInExtension = SwiftDeclarationKind.extensionKinds.contains(kind)
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.actorKeyword.positionAfterSkippingLeadingTrivia)
}
if element.accessibility == .internal || (element.accessibility == nil && isInExtension) {
return internalTypeElementsInSubstructure + [element]
return node.modifiers.isPrivateOrFileprivate ? .skipChildren : .visitChildren
}
override func visitPost(_ node: ProtocolDeclSyntax) {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.protocolKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: TypealiasDeclSyntax) {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.typealiasKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: FunctionDeclSyntax) {
if hasViolation(modifiers: node.modifiers) {
let position = node.modifiers.staticOrClassPosition ??
node.funcKeyword.positionAfterSkippingLeadingTrivia
violations.append(position)
}
}
override func visitPost(_ node: SubscriptDeclSyntax) {
if hasViolation(modifiers: node.modifiers) {
let position = node.modifiers.staticOrClassPosition ??
node.subscriptKeyword.positionAfterSkippingLeadingTrivia
violations.append(position)
}
}
override func visitPost(_ node: InitializerDeclSyntax) {
if hasViolation(modifiers: node.modifiers) {
violations.append(node.initKeyword.positionAfterSkippingLeadingTrivia)
}
}
override func visitPost(_ node: VariableDeclSyntax) {
if hasViolation(modifiers: node.modifiers) {
let position = node.modifiers.staticOrClassPosition ??
node.letOrVarKeyword.positionAfterSkippingLeadingTrivia
violations.append(position)
}
}
override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
.skipChildren
}
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
.skipChildren
}
private func hasViolation(modifiers: ModifierListSyntax?) -> Bool {
guard let modifiers = modifiers else {
return true
}
return internalTypeElementsInSubstructure
return !modifiers.contains(where: \.isACLModifier)
}
}
}
private extension SwiftDeclarationKind {
var childsAreExemptFromACL: Bool {
switch self {
case .associatedtype, .enumcase, .enumelement, .functionAccessorAddress,
.functionAccessorDidset, .functionAccessorGetter, .functionAccessorMutableaddress,
.functionAccessorSetter, .functionAccessorWillset, .genericTypeParam, .module,
.precedenceGroup, .varLocal, .varParameter, .varClass,
.varGlobal, .varInstance, .varStatic, .typealias, .functionAccessorModify, .functionAccessorRead,
.functionConstructor, .functionDestructor, .functionFree, .functionMethodClass,
.functionMethodInstance, .functionMethodStatic, .functionOperator, .functionOperatorInfix,
.functionOperatorPostfix, .functionOperatorPrefix, .functionSubscript, .protocol, .opaqueType:
return true
case .class, .enum, .extension, .extensionClass, .extensionEnum,
.extensionProtocol, .extensionStruct, .struct:
return false
}
}
}
private extension ModifierListSyntax? {
var staticOrClassPosition: AbsolutePosition? {
self?.first { modifier in
modifier.name.tokenKind == .staticKeyword || modifier.name.tokenKind == .classKeyword
}?.positionAfterSkippingLeadingTrivia
}
}

View File

@ -104,17 +104,3 @@ private extension ExplicitTopLevelACLRule {
}
}
}
private extension DeclModifierSyntax {
var isACLModifier: Bool {
let aclModifiers: Set<TokenKind> = [
.privateKeyword,
.fileprivateKeyword,
.internalKeyword,
.publicKeyword,
.contextualKeyword("open")
]
return detail == nil && aclModifiers.contains(name.tokenKind)
}
}