Compare commits
2 Commits
main
...
rewrite-du
Author | SHA1 | Date |
---|---|---|
![]() |
91677c8307 | |
![]() |
a34d3a78f6 |
|
@ -1,7 +1,6 @@
|
|||
import Foundation
|
||||
import SourceKittenFramework
|
||||
import SwiftSyntax
|
||||
|
||||
public struct DuplicateImportsRule: ConfigurationProviderRule, CorrectableRule {
|
||||
public struct DuplicateImportsRule: ConfigurationProviderRule, SwiftSyntaxRule {
|
||||
public var configuration = SeverityConfiguration(.warning)
|
||||
|
||||
// List of all possible import kinds
|
||||
|
@ -23,192 +22,34 @@ public struct DuplicateImportsRule: ConfigurationProviderRule, CorrectableRule {
|
|||
corrections: DuplicateImportsRuleExamples.corrections
|
||||
)
|
||||
|
||||
private func rangesInConditionalCompilation(file: SwiftLintFile) -> [ByteRange] {
|
||||
let contents = file.stringView
|
||||
public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||
Visitor(viewMode: .sourceAccurate)
|
||||
}
|
||||
}
|
||||
|
||||
let ranges = file.syntaxMap.tokens
|
||||
.filter { $0.kind == .buildconfigKeyword }
|
||||
.map { $0.range }
|
||||
.filter { range in
|
||||
return ["#if", "#endif"].contains(contents.substringWithByteRange(range))
|
||||
private extension DuplicateImportsRule {
|
||||
final class Visitor: ViolationsSyntaxVisitor {
|
||||
private var imported: Set<String> = []
|
||||
override func visitPost(_ node: ImportDeclSyntax) {
|
||||
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 struct ImportSlice {
|
||||
enum ImportSliceType {
|
||||
/// 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
|
||||
)
|
||||
private extension SyntaxProtocol {
|
||||
func nearestIfConfigClause() -> IfConfigClauseSyntax? {
|
||||
guard let parent = parent else {
|
||||
return nil
|
||||
}
|
||||
|
||||
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] = {
|
||||
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
|
||||
}()
|
||||
static let corrections: [Example: Example] = [:]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue