Skip to content

Commit

Permalink
Add DuplicateImportsRule (realm#2004)
Browse files Browse the repository at this point in the history
  • Loading branch information
sammy-SC authored and sjavora committed Mar 9, 2019
1 parent 13a4e69 commit ded37c3
Show file tree
Hide file tree
Showing 8 changed files with 390 additions and 1 deletion.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

#### Enhancements

* None.
* Add `duplicate_imports` rule to prevent importing the same module twice.
[Samuel Susla](https://github.com/sammy-sc)
[#1881](https://github.com/realm/SwiftLint/issues/1881)

#### Bug Fixes

Expand Down
202 changes: 202 additions & 0 deletions Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
* [Discouraged Object Literal](#discouraged-object-literal)
* [Discouraged Optional Boolean](#discouraged-optional-boolean)
* [Discouraged Optional Collection](#discouraged-optional-collection)
* [Duplicate Imports](#duplicate-imports)
* [Dynamic Inline](#dynamic-inline)
* [Empty Count](#empty-count)
* [Empty Enum Arguments](#empty-enum-arguments)
Expand Down Expand Up @@ -4623,6 +4624,207 @@ enum Foo {



## Duplicate Imports

Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version
--- | --- | --- | --- | --- | ---
`duplicate_imports` | Enabled | No | idiomatic | No | 3.0.0

Imports should be unique.

### Examples

<details>
<summary>Non Triggering Examples</summary>

```swift
import A
import B
import C
```

```swift
import A.B
import A.C
```

```swift
#if DEBUG
@testable import KsApi
#else
import KsApi
#endif
```

```swift
import A // module
import B // module
```

</details>
<details>
<summary>Triggering Examples</summary>

```swift
import Foundation
import Dispatch
↓import Foundation
```

```swift
import Foundation
↓import Foundation.NSString
```

```swift
↓import Foundation.NSString
import Foundation
```

```swift
↓import A.B.C
import A.B
```

```swift
import A.B
↓import A.B.C
```

```swift
import A
#if DEBUG
@testable import KsApi
#else
import KsApi
#endif
↓import A
```

```swift
import A
↓import typealias A.Foo
```

```swift
import A
↓import struct A.Foo
```

```swift
import A
↓import class A.Foo
```

```swift
import A
↓import enum A.Foo
```

```swift
import A
↓import protocol A.Foo
```

```swift
import A
↓import let A.Foo
```

```swift
import A
↓import var A.Foo
```

```swift
import A
↓import func A.Foo
```

```swift
import A
↓import typealias A.B.Foo
```

```swift
import A
↓import struct A.B.Foo
```

```swift
import A
↓import class A.B.Foo
```

```swift
import A
↓import enum A.B.Foo
```

```swift
import A
↓import protocol A.B.Foo
```

```swift
import A
↓import let A.B.Foo
```

```swift
import A
↓import var A.B.Foo
```

```swift
import A
↓import func A.B.Foo
```

```swift
import A.B
↓import typealias A.B.Foo
```

```swift
import A.B
↓import struct A.B.Foo
```

```swift
import A.B
↓import class A.B.Foo
```

```swift
import A.B
↓import enum A.B.Foo
```

```swift
import A.B
↓import protocol A.B.Foo
```

```swift
import A.B
↓import let A.B.Foo
```

```swift
import A.B
↓import var A.B.Foo
```

```swift
import A.B
↓import func A.B.Foo
```

</details>



## Dynamic Inline

Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version
Expand Down
1 change: 1 addition & 0 deletions Source/SwiftLintFramework/Models/MasterRuleList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public let masterRuleList = RuleList(rules: [
DiscouragedObjectLiteralRule.self,
DiscouragedOptionalBooleanRule.self,
DiscouragedOptionalCollectionRule.self,
DuplicateImportsRule.self,
DynamicInlineRule.self,
EmptyCountRule.self,
EmptyEnumArgumentsRule.self,
Expand Down
107 changes: 107 additions & 0 deletions Source/SwiftLintFramework/Rules/Idiomatic/DuplicateImportsRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Foundation
import SourceKittenFramework

public struct DuplicateImportsRule: ConfigurationProviderRule, AutomaticTestableRule {
public var configuration = SeverityConfiguration(.warning)

// List of all possible import kinds
static let importKinds = [
"typealias", "struct", "class",
"enum", "protocol", "let",
"var", "func"
]

public init() {}

public static let description = RuleDescription(
identifier: "duplicate_imports",
name: "Duplicate Imports",
description: "Imports should be unique.",
kind: .idiomatic,
nonTriggeringExamples: DuplicateImportsRuleExamples.nonTriggeringExamples,
triggeringExamples: DuplicateImportsRuleExamples.triggeringExamples
)

private func rangesInConditionalCompilation(file: File) -> [NSRange] {
let contents = file.contents.bridge()

let ranges = file.syntaxMap.tokens
.filter { SyntaxKind(rawValue: $0.type) == .buildconfigKeyword }
.map { NSRange(location: $0.offset, length: $0.length) }
.filter { range in
let keyword = contents.substringWithByteRange(start: range.location, length: range.length)
return ["#if", "#endif"].contains(keyword)
}

return stride(from: 0, to: ranges.count, by: 2).reduce(into: []) { result, rangeIndex in
result.append(NSUnionRange(ranges[rangeIndex], ranges[rangeIndex + 1]))
}
}

public func validate(file: File) -> [StyleViolation] {
let contents = file.contents.bridge()

let ignoredRanges = self.rangesInConditionalCompilation(file: file)

let importKinds = DuplicateImportsRule.importKinds.joined(separator: "|")

// Grammar of import declaration
// attributes(optional) import import-kind(optional) import-path
let regex = "^(\\w\\s)?import(\\s(\(importKinds)))?\\s+[a-zA-Z0-9._]+$"
let importRanges = file.match(pattern: regex)
.filter { $0.1.allSatisfy { [.keyword, .identifier].contains($0) } }
.compactMap { contents.NSRangeToByteRange(start: $0.0.location, length: $0.0.length) }
.filter { importRange -> Bool in
return !importRange.intersects(ignoredRanges)
}

let lines = contents.lines()

let importLines: [Line] = importRanges.compactMap { range in
guard let line = contents.lineAndCharacter(forByteOffset: range.location)?.line
else { return nil }
return lines[line - 1]
}

var violations = [StyleViolation]()

for indexI in 0..<importLines.count {
for indexJ in indexI + 1..<importLines.count {
let firstLine = importLines[indexI]
let secondLine = importLines[indexJ]

guard firstLine.areImportsDuplicated(with: secondLine)
else { continue }

let lineWithDuplicatedImport: Line = {
if firstLine.importIdentifier?.count ?? 0 <= secondLine.importIdentifier?.count ?? 0 {
return secondLine
} else {
return firstLine
}
}()

let location = Location(file: file, characterOffset: lineWithDuplicatedImport.range.location)
let violation = StyleViolation(ruleDescription: type(of: self).description, location: location)
violations.append(violation)
}
}

return violations
}
}

private extension Line {
/// Returns name of the module being imported.
var importIdentifier: Substring? {
return self.content.split(separator: " ").last
}

func areImportsDuplicated(with otherLine: Line) -> Bool {
guard let firstImportIdentifiers = self.importIdentifier?.split(separator: "."),
let secondImportIdentifiers = otherLine.importIdentifier?.split(separator: ".")
else { return false }

return zip(firstImportIdentifiers, secondImportIdentifiers).allSatisfy { $0 == $1 }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
internal struct DuplicateImportsRuleExamples {
static let nonTriggeringExamples: [String] = [
"import A\nimport B\nimport C",
"import A.B\nimport A.C",
"""
#if DEBUG
@testable import KsApi
#else
import KsApi
#endif
""",
"import A // module\nimport B // module"
]

static let triggeringExamples: [String] = {
var list: [String] = [
"import Foundation\nimport Dispatch\n↓import Foundation",
"import Foundation\n↓import Foundation.NSString",
"↓import Foundation.NSString\nimport Foundation",
"↓import A.B.C\nimport A.B",
"import A.B\n↓import A.B.C",
"""
import A
#if DEBUG
@testable import KsApi
#else
import KsApi
#endif
↓import A
"""
]

list += DuplicateImportsRule.importKinds.map { importKind in
return """
import A
↓import \(importKind) A.Foo
"""
}

list += DuplicateImportsRule.importKinds.map { importKind in
return """
import A
↓import \(importKind) A.B.Foo
"""
}

list += DuplicateImportsRule.importKinds.map { importKind in
return """
import A.B
↓import \(importKind) A.B.Foo
"""
}

return list
}()
}
Loading

0 comments on commit ded37c3

Please sign in to comment.