SwiftLint/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRule.swift

156 lines
6.0 KiB
Swift

import Foundation
import SourceKittenFramework
fileprivate extension Line {
var contentRange: NSRange {
return NSRange(location: range.location, length: content.bridge().length)
}
// `Line` in this rule always contains word import
// This method returns contents of line that are before import
func importAttributes() -> String {
return content[importAttributesRange()].trimmingCharacters(in: .whitespaces)
}
// `Line` in this rule always contains word import
// This method returns contents of line that are after import
func importModule() -> Substring {
return content[importModuleRange()]
}
func importAttributesRange() -> Range<String.Index> {
let rangeOfImport = content.range(of: "import")
precondition(rangeOfImport != nil)
return content.startIndex..<rangeOfImport!.lowerBound
}
func importModuleRange() -> Range<String.Index> {
let rangeOfImport = content.range(of: "import")
precondition(rangeOfImport != nil)
let moduleStart = content.rangeOfCharacter(from: CharacterSet.whitespaces.inverted, options: [],
range: rangeOfImport!.upperBound..<content.endIndex)
return moduleStart!.lowerBound..<content.endIndex
}
}
private extension Sequence where Element == Line {
// Groups lines, so that lines that are one after the other
// will end up in same group.
func grouped() -> [[Line]] {
return reduce(into: [[]]) { result, line in
guard let last = result.last?.last else {
result = [[line]]
return
}
if last.index == line.index - 1 {
result[result.count - 1].append(line)
} else {
result.append([line])
}
}
}
}
struct SortedImportsRule: CorrectableRule, ConfigurationProviderRule, OptInRule {
var configuration = SortedImportsConfiguration()
static let description = RuleDescription(
identifier: "sorted_imports",
name: "Sorted Imports",
description: "Imports should be sorted",
kind: .style,
nonTriggeringExamples: SortedImportsRuleExamples.nonTriggeringExamples,
triggeringExamples: SortedImportsRuleExamples.triggeringExamples,
corrections: SortedImportsRuleExamples.corrections
)
func validate(file: SwiftLintFile) -> [StyleViolation] {
let groups = importGroups(in: file, filterEnabled: false)
return violatingOffsets(inGroups: groups).map { index -> StyleViolation in
let location = Location(file: file, characterOffset: index)
return StyleViolation(ruleDescription: Self.description,
severity: configuration.severity.severity,
location: location)
}
}
private func importGroups(in file: SwiftLintFile, filterEnabled: Bool) -> [[Line]] {
var importRanges = file.match(pattern: "import\\s+\\w+", with: [.keyword, .identifier])
if filterEnabled {
importRanges = file.ruleEnabled(violatingRanges: importRanges, for: self)
}
let contents = file.stringView
let lines = file.lines
let importLines: [Line] = importRanges.compactMap { range in
guard let line = contents.lineAndCharacter(forCharacterOffset: range.location)?.line
else { return nil }
return lines[line - 1]
}
return importLines.grouped()
}
private func violatingOffsets(inGroups groups: [[Line]]) -> [Int] {
return groups.flatMap { group in
return zip(group, group.dropFirst()).reduce(into: []) { violatingOffsets, groupPair in
let (previous, current) = groupPair
let isOrderedCorrectly = should(previous, comeBefore: current)
if isOrderedCorrectly {
return
}
let distance = current.content.distance(from: current.content.startIndex,
to: current.importModuleRange().lowerBound)
violatingOffsets.append(current.range.location + distance)
}
}
}
/// - returns: whether `lhs` should come before `rhs` based on a comparison of the contents of the import lines
private func should(_ lhs: Line, comeBefore rhs: Line) -> Bool {
switch configuration.grouping {
case .attributes:
let lhsAttributes = lhs.importAttributes()
let rhsAttributes = rhs.importAttributes()
if lhsAttributes != rhsAttributes {
if lhsAttributes.isEmpty != rhsAttributes.isEmpty {
return rhsAttributes.isEmpty
}
return lhsAttributes < rhsAttributes
}
case .names:
break
}
return lhs.importModule().lowercased() <= rhs.importModule().lowercased()
}
func correct(file: SwiftLintFile) -> [Correction] {
let groups = importGroups(in: file, filterEnabled: true)
let corrections = violatingOffsets(inGroups: groups).map { characterOffset -> Correction in
let location = Location(file: file, characterOffset: characterOffset)
return Correction(ruleDescription: Self.description, location: location)
}
guard corrections.isNotEmpty else {
return []
}
let correctedContents = NSMutableString(string: file.contents)
for group in groups.map({ $0.sorted(by: should(_:comeBefore:)) }) {
guard let first = group.first?.contentRange else {
continue
}
let result = group.map { $0.content }.joined(separator: "\n")
let union = group.dropFirst().reduce(first) { result, line in
return NSUnionRange(result, line.contentRange)
}
correctedContents.replaceCharacters(in: union, with: result)
}
file.write(correctedContents.bridge())
return corrections
}
}