Add `grouping` option to the `sorted_imports` rule (#4935)
This commit is contained in:
parent
754127924f
commit
1094a3b70e
|
@ -20,6 +20,11 @@
|
||||||
[SimplyDanny](https://github.com/SimplyDanny)
|
[SimplyDanny](https://github.com/SimplyDanny)
|
||||||
[#4990](https://github.com/realm/SwiftLint/issues/4990)
|
[#4990](https://github.com/realm/SwiftLint/issues/4990)
|
||||||
|
|
||||||
|
* Add `grouping` option to the `sorted_imports` rule allowing
|
||||||
|
to sort groups of imports defined by their preceding attributes
|
||||||
|
(e.g. `@testable`, `@_exported`, ...).
|
||||||
|
[hiltonc](https://github.com/hiltonc)
|
||||||
|
|
||||||
#### Bug Fixes
|
#### Bug Fixes
|
||||||
|
|
||||||
* Do not trigger `prefer_self_in_static_references` rule on `typealias`
|
* Do not trigger `prefer_self_in_static_references` rule on `typealias`
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
struct SortedImportsConfiguration: RuleConfiguration, Equatable {
|
||||||
|
typealias Parent = SortedImportsRule
|
||||||
|
|
||||||
|
enum SortedImportsGroupingConfiguration: String {
|
||||||
|
/// Sorts import lines based on any import attributes (e.g. `@testable`, `@_exported`, etc.), followed by a case
|
||||||
|
/// insensitive comparison of the imported module name.
|
||||||
|
case attributes
|
||||||
|
/// Sorts import lines based on a case insensitive comparison of the imported module name.
|
||||||
|
case names
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) var severity = SeverityConfiguration<Parent>(.warning)
|
||||||
|
private(set) var grouping = SortedImportsGroupingConfiguration.names
|
||||||
|
|
||||||
|
var consoleDescription: String {
|
||||||
|
return "severity: \(severity.consoleDescription)"
|
||||||
|
+ ", grouping: \(grouping)"
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func apply(configuration: Any) throws {
|
||||||
|
guard let configuration = configuration as? [String: Any] else {
|
||||||
|
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let rawGrouping = configuration["grouping"] {
|
||||||
|
guard let rawGrouping = rawGrouping as? String,
|
||||||
|
let grouping = SortedImportsGroupingConfiguration(rawValue: rawGrouping) else {
|
||||||
|
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
|
||||||
|
}
|
||||||
|
self.grouping = grouping
|
||||||
|
}
|
||||||
|
|
||||||
|
if let severityString = configuration["severity"] as? String {
|
||||||
|
try severity.apply(configuration: severityString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,30 +1,36 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import SourceKittenFramework
|
import SourceKittenFramework
|
||||||
|
|
||||||
extension Line {
|
fileprivate extension Line {
|
||||||
fileprivate var contentRange: NSRange {
|
var contentRange: NSRange {
|
||||||
return NSRange(location: range.location, length: content.bridge().length)
|
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
|
// `Line` in this rule always contains word import
|
||||||
// This method returns contents of line that are after import
|
// This method returns contents of line that are after import
|
||||||
private func importModule() -> Substring {
|
func importModule() -> Substring {
|
||||||
return content[importModuleRange()]
|
return content[importModuleRange()]
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func importModuleRange() -> Range<String.Index> {
|
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")
|
let rangeOfImport = content.range(of: "import")
|
||||||
precondition(rangeOfImport != nil)
|
precondition(rangeOfImport != nil)
|
||||||
let moduleStart = content.rangeOfCharacter(from: CharacterSet.whitespaces.inverted, options: [],
|
let moduleStart = content.rangeOfCharacter(from: CharacterSet.whitespaces.inverted, options: [],
|
||||||
range: rangeOfImport!.upperBound..<content.endIndex)
|
range: rangeOfImport!.upperBound..<content.endIndex)
|
||||||
return moduleStart!.lowerBound..<content.endIndex
|
return moduleStart!.lowerBound..<content.endIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case insensitive comparison of contents of the line
|
|
||||||
// after the word `import`
|
|
||||||
fileprivate static func <= (lhs: Line, rhs: Line) -> Bool {
|
|
||||||
return lhs.importModule().lowercased() <= rhs.importModule().lowercased()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension Sequence where Element == Line {
|
private extension Sequence where Element == Line {
|
||||||
|
@ -47,88 +53,16 @@ private extension Sequence where Element == Line {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SortedImportsRule: CorrectableRule, ConfigurationProviderRule, OptInRule {
|
struct SortedImportsRule: CorrectableRule, ConfigurationProviderRule, OptInRule {
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
var configuration = SortedImportsConfiguration()
|
||||||
|
|
||||||
static let description = RuleDescription(
|
static let description = RuleDescription(
|
||||||
identifier: "sorted_imports",
|
identifier: "sorted_imports",
|
||||||
name: "Sorted Imports",
|
name: "Sorted Imports",
|
||||||
description: "Imports should be sorted",
|
description: "Imports should be sorted",
|
||||||
kind: .style,
|
kind: .style,
|
||||||
nonTriggeringExamples: [
|
nonTriggeringExamples: SortedImportsRuleExamples.nonTriggeringExamples,
|
||||||
Example("import AAA\nimport BBB\nimport CCC\nimport DDD"),
|
triggeringExamples: SortedImportsRuleExamples.triggeringExamples,
|
||||||
Example("import Alamofire\nimport API"),
|
corrections: SortedImportsRuleExamples.corrections
|
||||||
Example("import labc\nimport Ldef"),
|
|
||||||
Example("import BBB\n// comment\nimport AAA\nimport CCC"),
|
|
||||||
Example("@testable import AAA\nimport CCC"),
|
|
||||||
Example("import AAA\n@testable import CCC"),
|
|
||||||
Example("""
|
|
||||||
import EEE.A
|
|
||||||
import FFF.B
|
|
||||||
#if os(Linux)
|
|
||||||
import DDD.A
|
|
||||||
import EEE.B
|
|
||||||
#else
|
|
||||||
import CCC
|
|
||||||
import DDD.B
|
|
||||||
#endif
|
|
||||||
import AAA
|
|
||||||
import BBB
|
|
||||||
""")
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("import AAA\nimport ZZZ\nimport ↓BBB\nimport CCC"),
|
|
||||||
Example("import DDD\n// comment\nimport CCC\nimport ↓AAA"),
|
|
||||||
Example("@testable import CCC\nimport ↓AAA"),
|
|
||||||
Example("import CCC\n@testable import ↓AAA"),
|
|
||||||
Example("""
|
|
||||||
import FFF.B
|
|
||||||
import ↓EEE.A
|
|
||||||
#if os(Linux)
|
|
||||||
import DDD.A
|
|
||||||
import EEE.B
|
|
||||||
#else
|
|
||||||
import DDD.B
|
|
||||||
import ↓CCC
|
|
||||||
#endif
|
|
||||||
import AAA
|
|
||||||
import BBB
|
|
||||||
""")
|
|
||||||
],
|
|
||||||
corrections: [
|
|
||||||
Example("import AAA\nimport ZZZ\nimport ↓BBB\nimport CCC"):
|
|
||||||
Example("import AAA\nimport BBB\nimport CCC\nimport ZZZ"),
|
|
||||||
Example("import BBB // comment\nimport ↓AAA"): Example("import AAA\nimport BBB // comment"),
|
|
||||||
Example("import BBB\n// comment\nimport CCC\nimport ↓AAA"):
|
|
||||||
Example("import BBB\n// comment\nimport AAA\nimport CCC"),
|
|
||||||
Example("@testable import CCC\nimport ↓AAA"): Example("import AAA\n@testable import CCC"),
|
|
||||||
Example("import CCC\n@testable import ↓AAA"): Example("@testable import AAA\nimport CCC"),
|
|
||||||
Example("""
|
|
||||||
import FFF.B
|
|
||||||
import ↓EEE.A
|
|
||||||
#if os(Linux)
|
|
||||||
import DDD.A
|
|
||||||
import EEE.B
|
|
||||||
#else
|
|
||||||
import DDD.B
|
|
||||||
import ↓CCC
|
|
||||||
#endif
|
|
||||||
import AAA
|
|
||||||
import BBB
|
|
||||||
"""):
|
|
||||||
Example("""
|
|
||||||
import EEE.A
|
|
||||||
import FFF.B
|
|
||||||
#if os(Linux)
|
|
||||||
import DDD.A
|
|
||||||
import EEE.B
|
|
||||||
#else
|
|
||||||
import CCC
|
|
||||||
import DDD.B
|
|
||||||
#endif
|
|
||||||
import AAA
|
|
||||||
import BBB
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
||||||
|
@ -136,7 +70,7 @@ struct SortedImportsRule: CorrectableRule, ConfigurationProviderRule, OptInRule
|
||||||
return violatingOffsets(inGroups: groups).map { index -> StyleViolation in
|
return violatingOffsets(inGroups: groups).map { index -> StyleViolation in
|
||||||
let location = Location(file: file, characterOffset: index)
|
let location = Location(file: file, characterOffset: index)
|
||||||
return StyleViolation(ruleDescription: Self.description,
|
return StyleViolation(ruleDescription: Self.description,
|
||||||
severity: configuration.severity,
|
severity: configuration.severity.severity,
|
||||||
location: location)
|
location: location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,7 +96,7 @@ struct SortedImportsRule: CorrectableRule, ConfigurationProviderRule, OptInRule
|
||||||
return groups.flatMap { group in
|
return groups.flatMap { group in
|
||||||
return zip(group, group.dropFirst()).reduce(into: []) { violatingOffsets, groupPair in
|
return zip(group, group.dropFirst()).reduce(into: []) { violatingOffsets, groupPair in
|
||||||
let (previous, current) = groupPair
|
let (previous, current) = groupPair
|
||||||
let isOrderedCorrectly = previous <= current
|
let isOrderedCorrectly = should(previous, comeBefore: current)
|
||||||
if isOrderedCorrectly {
|
if isOrderedCorrectly {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -173,6 +107,24 @@ struct SortedImportsRule: CorrectableRule, ConfigurationProviderRule, OptInRule
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// - 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] {
|
func correct(file: SwiftLintFile) -> [Correction] {
|
||||||
let groups = importGroups(in: file, filterEnabled: true)
|
let groups = importGroups(in: file, filterEnabled: true)
|
||||||
|
|
||||||
|
@ -186,7 +138,7 @@ struct SortedImportsRule: CorrectableRule, ConfigurationProviderRule, OptInRule
|
||||||
}
|
}
|
||||||
|
|
||||||
let correctedContents = NSMutableString(string: file.contents)
|
let correctedContents = NSMutableString(string: file.contents)
|
||||||
for group in groups.map({ $0.sorted(by: <=) }) {
|
for group in groups.map({ $0.sorted(by: should(_:comeBefore:)) }) {
|
||||||
guard let first = group.first?.contentRange else {
|
guard let first = group.first?.contentRange else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,233 @@
|
||||||
|
internal struct SortedImportsRuleExamples {
|
||||||
|
private static let groupByAttributesConfiguration = ["grouping": "attributes"]
|
||||||
|
|
||||||
|
static let nonTriggeringExamples = [
|
||||||
|
Example("""
|
||||||
|
import AAA
|
||||||
|
import BBB
|
||||||
|
import CCC
|
||||||
|
import DDD
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import Alamofire
|
||||||
|
import API
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import labc
|
||||||
|
import Ldef
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import BBB
|
||||||
|
// comment
|
||||||
|
import AAA
|
||||||
|
import CCC
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@testable import AAA
|
||||||
|
import CCC
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import AAA
|
||||||
|
@testable import CCC
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import EEE.A
|
||||||
|
import FFF.B
|
||||||
|
#if os(Linux)
|
||||||
|
import DDD.A
|
||||||
|
import EEE.B
|
||||||
|
#else
|
||||||
|
import CCC
|
||||||
|
import DDD.B
|
||||||
|
#endif
|
||||||
|
import AAA
|
||||||
|
import BBB
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@testable import AAA
|
||||||
|
@testable import BBB
|
||||||
|
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true),
|
||||||
|
Example("""
|
||||||
|
@testable import BBB
|
||||||
|
import AAA
|
||||||
|
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true),
|
||||||
|
Example("""
|
||||||
|
@_exported import BBB
|
||||||
|
@testable import AAA
|
||||||
|
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true),
|
||||||
|
Example("""
|
||||||
|
@_exported @testable import BBB
|
||||||
|
import AAA
|
||||||
|
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true)
|
||||||
|
]
|
||||||
|
|
||||||
|
static let triggeringExamples = [
|
||||||
|
Example("""
|
||||||
|
import AAA
|
||||||
|
import ZZZ
|
||||||
|
import ↓BBB
|
||||||
|
import CCC
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import DDD
|
||||||
|
// comment
|
||||||
|
import CCC
|
||||||
|
import ↓AAA
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@testable import CCC
|
||||||
|
import ↓AAA
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import CCC
|
||||||
|
@testable import ↓AAA
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import FFF.B
|
||||||
|
import ↓EEE.A
|
||||||
|
#if os(Linux)
|
||||||
|
import DDD.A
|
||||||
|
import EEE.B
|
||||||
|
#else
|
||||||
|
import DDD.B
|
||||||
|
import ↓CCC
|
||||||
|
#endif
|
||||||
|
import AAA
|
||||||
|
import BBB
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@testable import BBB
|
||||||
|
@testable import ↓AAA
|
||||||
|
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true),
|
||||||
|
Example("""
|
||||||
|
import AAA
|
||||||
|
@testable import ↓BBB
|
||||||
|
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true),
|
||||||
|
Example("""
|
||||||
|
import BBB
|
||||||
|
@testable import ↓AAA
|
||||||
|
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true),
|
||||||
|
Example("""
|
||||||
|
@testable import AAA
|
||||||
|
@_exported import ↓BBB
|
||||||
|
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true),
|
||||||
|
Example("""
|
||||||
|
import AAA
|
||||||
|
@_exported @testable import ↓BBB
|
||||||
|
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true)
|
||||||
|
]
|
||||||
|
|
||||||
|
static let corrections = [
|
||||||
|
Example("""
|
||||||
|
import AAA
|
||||||
|
import ZZZ
|
||||||
|
import ↓BBB
|
||||||
|
import CCC
|
||||||
|
"""):
|
||||||
|
Example("""
|
||||||
|
import AAA
|
||||||
|
import BBB
|
||||||
|
import CCC
|
||||||
|
import ZZZ
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import BBB // comment
|
||||||
|
import ↓AAA
|
||||||
|
"""): Example("""
|
||||||
|
import AAA
|
||||||
|
import BBB // comment
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import BBB
|
||||||
|
// comment
|
||||||
|
import CCC
|
||||||
|
import ↓AAA
|
||||||
|
"""):
|
||||||
|
Example("""
|
||||||
|
import BBB
|
||||||
|
// comment
|
||||||
|
import AAA
|
||||||
|
import CCC
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@testable import CCC
|
||||||
|
import ↓AAA
|
||||||
|
"""): Example("""
|
||||||
|
import AAA
|
||||||
|
@testable import CCC
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import CCC
|
||||||
|
@testable import ↓AAA
|
||||||
|
"""): Example("""
|
||||||
|
@testable import AAA
|
||||||
|
import CCC
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import FFF.B
|
||||||
|
import ↓EEE.A
|
||||||
|
#if os(Linux)
|
||||||
|
import DDD.A
|
||||||
|
import EEE.B
|
||||||
|
#else
|
||||||
|
import DDD.B
|
||||||
|
import ↓CCC
|
||||||
|
#endif
|
||||||
|
import AAA
|
||||||
|
import BBB
|
||||||
|
"""):
|
||||||
|
Example("""
|
||||||
|
import EEE.A
|
||||||
|
import FFF.B
|
||||||
|
#if os(Linux)
|
||||||
|
import DDD.A
|
||||||
|
import EEE.B
|
||||||
|
#else
|
||||||
|
import CCC
|
||||||
|
import DDD.B
|
||||||
|
#endif
|
||||||
|
import AAA
|
||||||
|
import BBB
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@testable import BBB
|
||||||
|
@testable import ↓AAA
|
||||||
|
""", configuration: groupByAttributesConfiguration):
|
||||||
|
Example("""
|
||||||
|
@testable import AAA
|
||||||
|
@testable import BBB
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import AAA
|
||||||
|
@testable import ↓BBB
|
||||||
|
""", configuration: groupByAttributesConfiguration):
|
||||||
|
Example("""
|
||||||
|
@testable import BBB
|
||||||
|
import AAA
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import BBB
|
||||||
|
@testable import ↓AAA
|
||||||
|
""", configuration: groupByAttributesConfiguration):
|
||||||
|
Example("""
|
||||||
|
@testable import AAA
|
||||||
|
import BBB
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
@testable import AAA
|
||||||
|
@_exported import ↓BBB
|
||||||
|
""", configuration: groupByAttributesConfiguration):
|
||||||
|
Example("""
|
||||||
|
@_exported import BBB
|
||||||
|
@testable import AAA
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
import AAA
|
||||||
|
@_exported @testable import ↓BBB
|
||||||
|
""", configuration: groupByAttributesConfiguration):
|
||||||
|
Example("""
|
||||||
|
@_exported @testable import BBB
|
||||||
|
import AAA
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue