Skip to content

Commit

Permalink
Fix explicit_type_interface when used in statements or in capture g…
Browse files Browse the repository at this point in the history
…roups.
  • Loading branch information
Daniel Metzing authored and dirtydanee committed Aug 13, 2018
1 parent 5257b61 commit 15e8cab
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 23 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@

#### Bug Fixes

* Fix `explicit_type_interface` when used in statements.
[Daniel Metzing](https://github.com/dirtydanee)
[#2154](https://github.com/realm/SwiftLint/issues/2154)

* Fix an issue with `control_statement` where commas in clauses prevented the
rule from applying.
[Allen Wu](https://github.com/allewun)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
import Foundation
import SourceKittenFramework

public struct ExplicitTypeInterfaceRule: ASTRule, OptInRule, ConfigurationProviderRule {
private typealias DeclarationKindWithMetadata = (declarationKind: SwiftDeclarationKind,
dictionary: [String: SourceKitRepresentable],
parentDictionary: [String: SourceKitRepresentable])

private typealias ParentDeclarationKindWithElementKind = (parentDeclaration: String, element: String)

public struct ExplicitTypeInterfaceRule: OptInRule, ConfigurationProviderRule {

public var configuration = ExplicitTypeInterfaceConfiguration()

public init() {}

fileprivate static let captureGroupPattern =
"\\{" + // The { character
"\\s*" + // Zero or more whitespace character(s)
"\\[" + // The [ characterassociatedEnum
"(" + // Start if the first capturing group
"\\s*" + // Zero or more whitespace character(s)
"\\w+" + // At least one word character
"\\s+" + // At least one whitespace character
"\\w+" + // At least one world character
",*" + // Zero or more , character
")" + // End of the first capturing group
"+" + // At least occurance of the first capturing group
"\\]" // The ] character

public static let description = RuleDescription(
identifier: "explicit_type_interface",
name: "Explicit Type Interface",
Expand All @@ -27,39 +48,130 @@ public struct ExplicitTypeInterfaceRule: ASTRule, OptInRule, ConfigurationProvid
]
)

public func validate(file: File, kind: SwiftDeclarationKind,
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
public func validate(file: File) -> [StyleViolation] {

let captureGroupByteRanges = file.captureGroupByteRanges
let elements = file.dictionaries(of: configuration.allowedKinds)

return elements.compactMap { element -> StyleViolation? in
guard !element.dictionary.containsType,
let offset = element.dictionary.offset,
(!configuration.allowRedundancy || !element.dictionary.isInitCall(file: file)),
!element.parentDictionary.contains([.forEach, .guard]),
!element.parentDictionary.caseStatementPatternRanges.contains(offset),
!element.parentDictionary.caseExpressionRanges.contains(offset),
!captureGroupByteRanges.contains(offset) else {
return nil
}

guard configuration.allowedKinds.contains(kind),
!containsType(dictionary: dictionary),
(!configuration.allowRedundancy || !assigneeIsInitCall(file: file, dictionary: dictionary)),
let offset = dictionary.offset else {
return []
return StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severityConfiguration.severity,
location: Location(file: file, byteOffset: offset))
}
}
}

return [
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severityConfiguration.severity,
location: Location(file: file, byteOffset: offset))
]
private extension File {

var captureGroupByteRanges: [NSRange] {
return match(pattern: ExplicitTypeInterfaceRule.captureGroupPattern,
excludingSyntaxKinds: SyntaxKind.commentKinds)
.compactMap { contents.bridge().NSRangeToByteRange(start: $0.location, length: $0.length) }
}

func dictionaries(of declarationKinds: Set<SwiftDeclarationKind>) -> [DeclarationKindWithMetadata] {
var declarations = [DeclarationKindWithMetadata]()
func search(in dictionary: [String: SourceKitRepresentable], parent: [String: SourceKitRepresentable]) {
if let kind = dictionary.kind,
let declarationKind = SwiftDeclarationKind(rawValue: kind),
declarationKinds.contains(declarationKind) {
declarations.append((declarationKind,
dictionary,
parent))
}

dictionary.substructure.forEach { search(in: $0, parent: dictionary) }
}

search(in: structure.dictionary, parent: [:])
return declarations
}
}

private func containsType(dictionary: [String: SourceKitRepresentable]) -> Bool {
return dictionary.typeName != nil
private extension Dictionary where Key == String, Value == SourceKitRepresentable {
var containsType: Bool {
return typeName != nil
}

private func assigneeIsInitCall(file: File, dictionary: [String: SourceKitRepresentable]) -> Bool {
var caseStatementPatternRanges: [NSRange] {
return ranges(with: StatementKind.case.rawValue, for: "source.lang.swift.structure.elem.pattern")
}

var caseExpressionRanges: [NSRange] {
return ranges(with: "source.lang.swift.expr.tuple", for: "source.lang.swift.structure.elem.expr")
}

func isInitCall(file: File) -> Bool {
guard
let nameOffset = dictionary.nameOffset,
let nameLength = dictionary.nameLength,
let afterNameRange = file.contents.bridge().byteRangeToNSRange(start: nameOffset + nameLength, length: 0)
else {
return false
let nameOffset = nameOffset,
let nameLength = nameLength,
case let contents = file.contents.bridge(),
let afterNameRange = contents.byteRangeToNSRange(start: nameOffset + nameLength, length: 0)
else {
return false
}

let contentAfterName = file.contents.bridge().substring(from: afterNameRange.location)
let contentAfterName = contents.substring(from: afterNameRange.location)
let initCallRegex = regex("^\\s*=\\s*\\p{Lu}[^\\(\\s<]*(?:<[^\\>]*>)?\\(")

return initCallRegex.firstMatch(in: contentAfterName, options: [], range: contentAfterName.fullNSRange) != nil
}

func contains(_ statements: Set<StatementKind>) -> Bool {
return StatementKind(optionalRawValue: kind).isKind(of: statements)
}

func ranges(with parentKind: String, for elementKind: String) -> [NSRange] {
guard parentKind == kind else {
return []
}

return elements.filter { elementKind == $0.kind }
.compactMap { NSRange(location: $0.offset, length: $0.length) }
}
}

private extension Optional where Wrapped == StatementKind {
func isKind(of statements: Set<StatementKind>) -> Bool {
guard let stmt = self else {
return false
}
return statements.contains(stmt)
}
}

private extension StatementKind {
init?(optionalRawValue: String?) {
guard let rawValue = optionalRawValue,
let stmt = StatementKind(rawValue: rawValue) else {
return nil
}
self = stmt
}
}

private extension NSRange {
init?(location: Int?, length: Int?) {
guard let location = location, let length = length else {
return nil
}

self = NSRange(location: location, length: length)
}
}

private extension Collection where Element == NSRange {
func contains(_ index: Int) -> Bool {
return first(where: { $0.contains(index) }) != nil
}
}
6 changes: 5 additions & 1 deletion Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,11 @@ extension ExplicitTypeInterfaceRuleTests {
("testExplicitTypeInterface", testExplicitTypeInterface),
("testExcludeLocalVars", testExcludeLocalVars),
("testExcludeClassVars", testExcludeClassVars),
("testAllowRedundancy", testAllowRedundancy)
("testAllowRedundancy", testAllowRedundancy),
("testEmbededInStatements", testEmbededInStatements),
("testCaptureGroup", testCaptureGroup),
("testFastEnumerationDeclaration", testFastEnumerationDeclaration),
("testSwitchCaseDeclarations", testSwitchCaseDeclarations)
]
}

Expand Down
118 changes: 118 additions & 0 deletions Tests/SwiftLintFrameworkTests/ExplicitTypeInterfaceRuleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,128 @@ class ExplicitTypeInterfaceRuleTests: XCTestCase {
"class Foo {\n ↓static var myStaticVar = 0\n}\n",
"class Foo {\n ↓class var myClassVar = 0\n}\n"
]

let description = ExplicitTypeInterfaceRule.description
.with(triggeringExamples: triggeringExamples)
.with(nonTriggeringExamples: nonTriggeringExamples)

verifyRule(description, ruleConfiguration: ["allow_redundancy": true])
}

func testEmbededInStatements() {
let nonTriggeringExamples = [
"func foo() {\n" +
" var bar: String?\n" +
" guard let strongBar = bar else {\n" +
" return\n" +
" }\n" +
"}",
"struct SomeError: Error {}\n" +
"var error: Error?\n" +
"switch error {\n" +
"case let error as SomeError:\n" +
" break\n" +
"default:\n" +
" break\n" +
"}"
]
let triggeringExamples = ExplicitTypeInterfaceRule.description.triggeringExamples
let description = ExplicitTypeInterfaceRule.description
.with(triggeringExamples: triggeringExamples)
.with(nonTriggeringExamples: nonTriggeringExamples)

verifyRule(description)
}

func testCaptureGroup() {
let nonTriggeringExamples = [
"var k: Int = 0\n" +
"_ = { [weak k] in\n" +
" print(k)\n" +
" }",
"var k: Int = 0\n" +
"_ = { [unowned k] in\n" +
" print(k)\n" +
" }",
"class Foo {\n" +
" func bar() {\n" +
" var k: Int = 0\n" +
" _ = { [weak self, weak k] in\n" +
" guard let strongSelf = self else { return }\n" +
" }\n" +
" }\n" +
"}"
]
let triggeringExamples = ExplicitTypeInterfaceRule.description.triggeringExamples
let description = ExplicitTypeInterfaceRule.description
.with(triggeringExamples: triggeringExamples)
.with(nonTriggeringExamples: nonTriggeringExamples)

verifyRule(description)
}

func testFastEnumerationDeclaration() {
let nonTriggeringExaples = [
"func foo() {\n" +
" let elements: [Int] = [1, 2]\n" +
" for element in elements {}\n" +
"}",
"func foo() {\n" +
" let elements: [Int] = [1, 2]\n" +
" for (index, element) in elements.enumerated() {}\n" +
"}\n"
]

let triggeringExamples = ExplicitTypeInterfaceRule.description.triggeringExamples
let description = ExplicitTypeInterfaceRule.description
.with(triggeringExamples: triggeringExamples)
.with(nonTriggeringExamples: nonTriggeringExaples)
verifyRule(description)
}

func testSwitchCaseDeclarations() {

let nonTriggeringExamples = [
"enum Foo {\n" +
" case failure(Any)\n" +
" case success(Any)\n" +
"}\n" +
"func bar {\n" +
" let foo: Foo = Foo.success(1)\n" +
" switch foo {\n" +
" case .failure(let error): let bar: Int = 1\n" +
" case .success(let result): let bar: Int = 2\n" +
" }\n" +
"}",
"func foo() {\n" +
" switch foo {\n" +
" case var (x, y): break\n" +
" }\n" +
"}"
]

let triggeringExamples = [
"enum Foo {\n" +
" case failure(Any)\n" +
" case success(Any, Any)\n" +
"}\n" +
"func bar {\n" +
" let foo: Foo = Foo.success(1)\n" +
" switch foo {\n" +
" case .failure(let foo): ↓let fooBar = 1\n" +
" case let .success(foo, bar): ↓let fooBar = 1\n" +
" }\n" +
"}",
"func foo() {\n" +
" switch foo {\n" +
" case var (x, y): ↓let fooBar = 1\n" +
" }\n" +
"}"
]

let description = ExplicitTypeInterfaceRule.description
.with(triggeringExamples: triggeringExamples)
.with(nonTriggeringExamples: nonTriggeringExamples)
verifyRule(description)
}
}

0 comments on commit 15e8cab

Please sign in to comment.