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) [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`

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

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