Add `grouping` option to the `sorted_imports` rule (#4935)

This commit is contained in:
Hilton Campbell 2023-05-19 12:49:07 -07:00 committed by GitHub
parent 754127924f
commit 1094a3b70e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 316 additions and 89 deletions

View File

@ -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`

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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
""")
]
}