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)
|
||||
[#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
|
||||
|
||||
* 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 SourceKittenFramework
|
||||
|
||||
extension Line {
|
||||
fileprivate var contentRange: NSRange {
|
||||
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
|
||||
private func importModule() -> Substring {
|
||||
func importModule() -> Substring {
|
||||
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")
|
||||
precondition(rangeOfImport != nil)
|
||||
let moduleStart = content.rangeOfCharacter(from: CharacterSet.whitespaces.inverted, options: [],
|
||||
range: rangeOfImport!.upperBound..<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 {
|
||||
|
@ -47,88 +53,16 @@ private extension Sequence where Element == Line {
|
|||
}
|
||||
|
||||
struct SortedImportsRule: CorrectableRule, ConfigurationProviderRule, OptInRule {
|
||||
var configuration = SeverityConfiguration<Self>(.warning)
|
||||
var configuration = SortedImportsConfiguration()
|
||||
|
||||
static let description = RuleDescription(
|
||||
identifier: "sorted_imports",
|
||||
name: "Sorted Imports",
|
||||
description: "Imports should be sorted",
|
||||
kind: .style,
|
||||
nonTriggeringExamples: [
|
||||
Example("import AAA\nimport BBB\nimport CCC\nimport DDD"),
|
||||
Example("import Alamofire\nimport API"),
|
||||
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
|
||||
""")
|
||||
]
|
||||
nonTriggeringExamples: SortedImportsRuleExamples.nonTriggeringExamples,
|
||||
triggeringExamples: SortedImportsRuleExamples.triggeringExamples,
|
||||
corrections: SortedImportsRuleExamples.corrections
|
||||
)
|
||||
|
||||
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
||||
|
@ -136,7 +70,7 @@ struct SortedImportsRule: CorrectableRule, ConfigurationProviderRule, OptInRule
|
|||
return violatingOffsets(inGroups: groups).map { index -> StyleViolation in
|
||||
let location = Location(file: file, characterOffset: index)
|
||||
return StyleViolation(ruleDescription: Self.description,
|
||||
severity: configuration.severity,
|
||||
severity: configuration.severity.severity,
|
||||
location: location)
|
||||
}
|
||||
}
|
||||
|
@ -162,7 +96,7 @@ struct SortedImportsRule: CorrectableRule, ConfigurationProviderRule, OptInRule
|
|||
return groups.flatMap { group in
|
||||
return zip(group, group.dropFirst()).reduce(into: []) { violatingOffsets, groupPair in
|
||||
let (previous, current) = groupPair
|
||||
let isOrderedCorrectly = previous <= current
|
||||
let isOrderedCorrectly = should(previous, comeBefore: current)
|
||||
if isOrderedCorrectly {
|
||||
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] {
|
||||
let groups = importGroups(in: file, filterEnabled: true)
|
||||
|
||||
|
@ -186,7 +138,7 @@ struct SortedImportsRule: CorrectableRule, ConfigurationProviderRule, OptInRule
|
|||
}
|
||||
|
||||
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 {
|
||||
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