Compare commits
2 Commits
main
...
rewrite-du
Author | SHA1 | Date |
---|---|---|
![]() |
91677c8307 | |
![]() |
a34d3a78f6 |
|
@ -1,7 +1,6 @@
|
||||||
import Foundation
|
import SwiftSyntax
|
||||||
import SourceKittenFramework
|
|
||||||
|
|
||||||
public struct DuplicateImportsRule: ConfigurationProviderRule, CorrectableRule {
|
public struct DuplicateImportsRule: ConfigurationProviderRule, SwiftSyntaxRule {
|
||||||
public var configuration = SeverityConfiguration(.warning)
|
public var configuration = SeverityConfiguration(.warning)
|
||||||
|
|
||||||
// List of all possible import kinds
|
// List of all possible import kinds
|
||||||
|
@ -23,192 +22,34 @@ public struct DuplicateImportsRule: ConfigurationProviderRule, CorrectableRule {
|
||||||
corrections: DuplicateImportsRuleExamples.corrections
|
corrections: DuplicateImportsRuleExamples.corrections
|
||||||
)
|
)
|
||||||
|
|
||||||
private func rangesInConditionalCompilation(file: SwiftLintFile) -> [ByteRange] {
|
public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
let contents = file.stringView
|
Visitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let ranges = file.syntaxMap.tokens
|
private extension DuplicateImportsRule {
|
||||||
.filter { $0.kind == .buildconfigKeyword }
|
final class Visitor: ViolationsSyntaxVisitor {
|
||||||
.map { $0.range }
|
private var imported: Set<String> = []
|
||||||
.filter { range in
|
override func visitPost(_ node: ImportDeclSyntax) {
|
||||||
return ["#if", "#endif"].contains(contents.substringWithByteRange(range))
|
let ifConfigCondition = node.nearestIfConfigClause()?.condition?.description
|
||||||
|
let pathDescription = [ifConfigCondition, node.path.withoutTrivia().description]
|
||||||
|
.compactMap { $0 }
|
||||||
|
.joined(separator: "/")
|
||||||
|
if imported.contains(pathDescription) {
|
||||||
|
violations.append(node.importTok.positionAfterSkippingLeadingTrivia)
|
||||||
|
} else {
|
||||||
|
imported.insert(pathDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure that each #if has corresponding #endif
|
|
||||||
guard ranges.count.isMultiple(of: 2) else { return [] }
|
|
||||||
|
|
||||||
return stride(from: 0, to: ranges.count, by: 2).reduce(into: []) { result, rangeIndex in
|
|
||||||
result.append(ranges[rangeIndex].union(with: ranges[rangeIndex + 1]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func buildImportLineSlicesByImportSubpath(
|
|
||||||
importLines: [Line]
|
|
||||||
) -> [ImportSubpath: [ImportLineSlice]] {
|
|
||||||
var importLineSlices = [ImportSubpath: [ImportLineSlice]]()
|
|
||||||
|
|
||||||
importLines.forEach { importLine in
|
|
||||||
importLine.importSlices.forEach { slice in
|
|
||||||
importLineSlices[slice.subpath, default: []].append(
|
|
||||||
ImportLineSlice(
|
|
||||||
slice: slice,
|
|
||||||
line: importLine
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return importLineSlices
|
|
||||||
}
|
|
||||||
|
|
||||||
private func findDuplicateImports(
|
|
||||||
file: SwiftLintFile,
|
|
||||||
importLineSlicesGroupedBySubpath: [[ImportLineSlice]]
|
|
||||||
) -> [DuplicateImport] {
|
|
||||||
typealias ImportLocation = Int
|
|
||||||
|
|
||||||
var duplicateImportsByLocation = [ImportLocation: DuplicateImport]()
|
|
||||||
|
|
||||||
importLineSlicesGroupedBySubpath.forEach { linesImportingSubpath in
|
|
||||||
guard linesImportingSubpath.count > 1 else { return }
|
|
||||||
guard let primaryImportIndex = linesImportingSubpath.firstIndex(where: {
|
|
||||||
$0.slice.type == .complete
|
|
||||||
}) else { return }
|
|
||||||
|
|
||||||
linesImportingSubpath.enumerated().forEach { index, importedLine in
|
|
||||||
guard index != primaryImportIndex else { return }
|
|
||||||
let location = Location(
|
|
||||||
file: file,
|
|
||||||
characterOffset: importedLine.line.range.location
|
|
||||||
)
|
|
||||||
duplicateImportsByLocation[importedLine.line.range.location] = DuplicateImport(
|
|
||||||
location: location,
|
|
||||||
range: importedLine.line.range
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array(duplicateImportsByLocation.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct DuplicateImport {
|
|
||||||
let location: Location
|
|
||||||
var range: NSRange
|
|
||||||
}
|
|
||||||
|
|
||||||
private func duplicateImports(file: SwiftLintFile) -> [DuplicateImport] {
|
|
||||||
let contents = file.stringView
|
|
||||||
|
|
||||||
let ignoredRanges = self.rangesInConditionalCompilation(file: file)
|
|
||||||
|
|
||||||
let importKinds = Self.importKinds.joined(separator: "|")
|
|
||||||
|
|
||||||
// Grammar of import declaration
|
|
||||||
// attributes(optional) import import-kind(optional) import-path
|
|
||||||
let regex = "^([a-zA-Z@_]+\\s)?import(\\s(\(importKinds)))?\\s+[a-zA-Z0-9._]+$"
|
|
||||||
let importRanges = file.match(pattern: regex)
|
|
||||||
.filter { $0.1.allSatisfy { [.keyword, .identifier, .attributeBuiltin].contains($0) } }
|
|
||||||
.compactMap { contents.NSRangeToByteRange(start: $0.0.location, length: $0.0.length) }
|
|
||||||
.filter { importRange -> Bool in
|
|
||||||
return !importRange.intersects(ignoredRanges)
|
|
||||||
}
|
|
||||||
|
|
||||||
let lines = file.lines
|
|
||||||
|
|
||||||
let importLines: [Line] = importRanges.compactMap { range in
|
|
||||||
guard let line = contents.lineAndCharacter(forByteOffset: range.location)?.line
|
|
||||||
else { return nil }
|
|
||||||
return lines[line - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
let importLineSlices = buildImportLineSlicesByImportSubpath(importLines: importLines)
|
|
||||||
|
|
||||||
let duplicateImports = findDuplicateImports(
|
|
||||||
file: file,
|
|
||||||
importLineSlicesGroupedBySubpath: Array(importLineSlices.values)
|
|
||||||
)
|
|
||||||
|
|
||||||
return duplicateImports.sorted(by: {
|
|
||||||
$0.range.lowerBound < $1.range.lowerBound
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public func validate(file: SwiftLintFile) -> [StyleViolation] {
|
|
||||||
return duplicateImports(file: file).map { duplicateImport in
|
|
||||||
StyleViolation(
|
|
||||||
ruleDescription: Self.description,
|
|
||||||
severity: configuration.severity,
|
|
||||||
location: duplicateImport.location
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func correct(file: SwiftLintFile) -> [Correction] {
|
|
||||||
let duplicateImports = duplicateImports(file: file).reversed().filter {
|
|
||||||
file.ruleEnabled(violatingRange: $0.range, for: self) != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let violatingRanges = duplicateImports.map(\.range)
|
|
||||||
let correctedFileContents = violatingRanges.reduce(file.stringView.nsString) { contents, range in
|
|
||||||
contents.replacingCharacters(
|
|
||||||
in: range,
|
|
||||||
with: ""
|
|
||||||
).bridge()
|
|
||||||
}
|
|
||||||
|
|
||||||
file.write(correctedFileContents.bridge())
|
|
||||||
|
|
||||||
return duplicateImports.map { duplicateImport in
|
|
||||||
Correction(
|
|
||||||
ruleDescription: Self.description,
|
|
||||||
location: duplicateImport.location
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private typealias ImportSubpath = ArraySlice<String>
|
private extension SyntaxProtocol {
|
||||||
|
func nearestIfConfigClause() -> IfConfigClauseSyntax? {
|
||||||
private struct ImportSlice {
|
guard let parent = parent else {
|
||||||
enum ImportSliceType {
|
return nil
|
||||||
/// For "import A.B.C" parent subpaths are ["A", "B"] and ["A"]
|
|
||||||
case parent
|
|
||||||
|
|
||||||
/// For "import A.B.C" complete subpath is ["A", "B", "C"]
|
|
||||||
case complete
|
|
||||||
}
|
|
||||||
|
|
||||||
let subpath: ImportSubpath
|
|
||||||
let type: ImportSliceType
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct ImportLineSlice {
|
|
||||||
let slice: ImportSlice
|
|
||||||
let line: Line
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Line {
|
|
||||||
/// Returns name of the module being imported.
|
|
||||||
var importIdentifier: Substring? {
|
|
||||||
return self.content.split(separator: " ").last
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For "import A.B.C" returns slices [["A", "B", "C"], ["A", "B"], ["A"]]
|
|
||||||
var importSlices: [ImportSlice] {
|
|
||||||
guard let importIdentifier = importIdentifier else { return [] }
|
|
||||||
|
|
||||||
let importedSubpathParts = importIdentifier.split(separator: ".").map { String($0) }
|
|
||||||
guard !importedSubpathParts.isEmpty else { return [] }
|
|
||||||
|
|
||||||
return [
|
|
||||||
ImportSlice(
|
|
||||||
subpath: importedSubpathParts[0..<importedSubpathParts.count],
|
|
||||||
type: .complete
|
|
||||||
)
|
|
||||||
] + (1..<importedSubpathParts.count).map {
|
|
||||||
ImportSlice(
|
|
||||||
subpath: importedSubpathParts[0..<importedSubpathParts.count - $0],
|
|
||||||
type: .parent
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return parent.as(IfConfigClauseSyntax.self) ?? parent.nearestIfConfigClause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,185 +35,13 @@ internal struct DuplicateImportsRuleExamples {
|
||||||
""")
|
""")
|
||||||
]
|
]
|
||||||
|
|
||||||
static let triggeringExamples = Array(corrections.keys.sorted())
|
static let triggeringExamples = [
|
||||||
|
Example("""
|
||||||
|
import Foundation
|
||||||
|
import Dispatch
|
||||||
|
↓import Foundation
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
|
||||||
static let corrections: [Example: Example] = {
|
static let corrections: [Example: Example] = [:]
|
||||||
var corrections = [
|
|
||||||
Example("""
|
|
||||||
import Foundation
|
|
||||||
import Dispatch
|
|
||||||
↓import Foundation
|
|
||||||
|
|
||||||
"""): Example(
|
|
||||||
"""
|
|
||||||
import Foundation
|
|
||||||
import Dispatch
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
import Foundation
|
|
||||||
↓import Foundation.NSString
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
↓import Foundation.NSString
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
@_implementationOnly import A
|
|
||||||
↓@_implementationOnly import A
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
@_implementationOnly import A
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
@testable import A
|
|
||||||
↓@testable import A
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
@testable import A
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
↓import A.B.C
|
|
||||||
import A.B
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
import A.B
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
import A.B
|
|
||||||
↓import A.B.C
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
import A.B
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
import A
|
|
||||||
#if DEBUG
|
|
||||||
@testable import KsApi
|
|
||||||
#else
|
|
||||||
import KsApi
|
|
||||||
#endif
|
|
||||||
↓import A
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
import A
|
|
||||||
#if DEBUG
|
|
||||||
@testable import KsApi
|
|
||||||
#else
|
|
||||||
import KsApi
|
|
||||||
#endif
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
import Foundation
|
|
||||||
↓import Foundation
|
|
||||||
↓import Foundation
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
↓import A.B.C
|
|
||||||
↓import A.B
|
|
||||||
import A
|
|
||||||
|
|
||||||
""", excludeFromDocumentation: true): Example("""
|
|
||||||
import A
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
import A.B.C
|
|
||||||
↓import A.B.C.D
|
|
||||||
↓import A.B.C.E
|
|
||||||
|
|
||||||
""", excludeFromDocumentation: true): Example("""
|
|
||||||
import A.B.C
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
↓import A.B.C
|
|
||||||
import A
|
|
||||||
↓import A.B
|
|
||||||
|
|
||||||
""", excludeFromDocumentation: true): Example("""
|
|
||||||
import A
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
↓import A.B
|
|
||||||
import A
|
|
||||||
↓import A.B.C
|
|
||||||
|
|
||||||
""", excludeFromDocumentation: true): Example("""
|
|
||||||
import A
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
import A
|
|
||||||
↓import A.B.C
|
|
||||||
↓import A.B
|
|
||||||
|
|
||||||
""", excludeFromDocumentation: true): Example("""
|
|
||||||
import A
|
|
||||||
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
|
|
||||||
DuplicateImportsRule.importKinds.map { importKind in
|
|
||||||
Example("""
|
|
||||||
import A
|
|
||||||
↓import \(importKind) A.Foo
|
|
||||||
|
|
||||||
""")
|
|
||||||
}.forEach {
|
|
||||||
corrections[$0] = Example(
|
|
||||||
"""
|
|
||||||
import A
|
|
||||||
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
|
|
||||||
DuplicateImportsRule.importKinds.map { importKind in
|
|
||||||
Example("""
|
|
||||||
import A
|
|
||||||
↓import \(importKind) A.B.Foo
|
|
||||||
|
|
||||||
""", excludeFromDocumentation: true)
|
|
||||||
}.forEach {
|
|
||||||
corrections[$0] = Example(
|
|
||||||
"""
|
|
||||||
import A
|
|
||||||
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
|
|
||||||
DuplicateImportsRule.importKinds.map { importKind in
|
|
||||||
Example("""
|
|
||||||
import A.B
|
|
||||||
↓import \(importKind) A.B.Foo
|
|
||||||
|
|
||||||
""", excludeFromDocumentation: true)
|
|
||||||
}.forEach {
|
|
||||||
corrections[$0] = Example(
|
|
||||||
"""
|
|
||||||
import A.B
|
|
||||||
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
|
|
||||||
return corrections
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue