Files
SwiftLint/Source/SwiftLintFramework/Rules/Idiomatic/ExtensionAccessModifierRule.swift
T
Zev Eisenberg fcf848608e Add Inline test failure messages (#3040)
* Add Example wrapper in order to display test failures inline when running in Xcode.
* Stop using Swift 5.1-only features so we can compile on Xcode 10.2.
* Wrap strings in Example.
* Add Changelog entry.
* Wrap all examples in Example struct.
* Better and more complete capturing of line numbers.
* Fix broken test.
* Better test traceability.
* Address or disable linting warnings.
* Add documentation comments.
* Disable linter for a few cases.
* Limit mutability and add copy-and-mutate utility functions.
* Limit scope of mutability.
2020-02-02 10:35:37 +02:00

162 lines
5.9 KiB
Swift

import Foundation
import SourceKittenFramework
public struct ExtensionAccessModifierRule: ASTRule, ConfigurationProviderRule, OptInRule, AutomaticTestableRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public 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("""
public extension Foo {
var bar: Int { return 1 }
var baz: Int { return 1 }
}
"""),
Example("""
extension Foo {
private bar: Int { return 1 }
private baz: Int { return 1 }
}
"""),
Example("""
extension Foo {
open bar: Int { return 1 }
open baz: Int { return 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() {}
}
""")
]
)
public func validate(file: SwiftLintFile, kind: SwiftDeclarationKind,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
guard kind == .extension, let offset = dictionary.offset,
dictionary.inheritedTypes.isEmpty
else {
return []
}
let declarations = dictionary.substructure
.compactMap { entry -> (acl: AccessControlLevel, offset: ByteCount)? in
guard entry.declarationKind != nil,
let acl = entry.accessibility,
let offset = entry.offset
else {
return nil
}
return (acl: acl, offset: offset)
}
let declarationsACLs = declarations.map { $0.acl }.unique
let allowedACLs: Set<AccessControlLevel> = [.internal, .private, .open]
guard declarationsACLs.count == 1, !allowedACLs.contains(declarationsACLs[0]) else {
return []
}
let syntaxTokens = file.syntaxMap.tokens
let parts = syntaxTokens.partitioned { offset <= $0.offset }
if let aclToken = parts.first.last, file.isACL(token: aclToken) {
return declarationsViolations(file: file, acl: declarationsACLs[0],
declarationOffsets: declarations.map { $0.offset },
dictionary: dictionary)
}
return [
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, byteOffset: offset))
]
}
private func declarationsViolations(file: SwiftLintFile, acl: AccessControlLevel,
declarationOffsets: [ByteCount],
dictionary: SourceKittenDictionary) -> [StyleViolation] {
guard let byteRange = dictionary.byteRange,
case let contents = file.stringView,
let range = contents.byteRangeToNSRange(byteRange) else {
return []
}
// find all ACL tokens
let allACLRanges = file.match(pattern: acl.description, with: [.attributeBuiltin], range: range).compactMap {
contents.NSRangeToByteRange(start: $0.location, length: $0.length)
}
let violationOffsets = declarationOffsets.filter { typeOffset in
// find the last ACL token before the type
guard let previousInternalByteRange = lastACLByteRange(before: typeOffset, in: allACLRanges) else {
// didn't find a candidate token, so the ACL is implict (not a violation)
return false
}
// the ACL 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 internalBelongsToType = Set(file.syntaxMap.kinds(inByteRange: range)) == [.attributeBuiltin]
return internalBelongsToType
}
return violationOffsets.map {
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, byteOffset: $0))
}
}
private func lastACLByteRange(before typeOffset: ByteCount, in ranges: [ByteRange]) -> ByteRange? {
let firstPartition = ranges.partitioned(by: { $0.location > typeOffset }).first
return firstPartition.last
}
}