-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add rule requiring imports be sorted #991
Conversation
Current coverage is 82.53% (diff: 92.85%)@@ master #991 diff @@
==========================================
Files 136 139 +3
Lines 6413 6694 +281
Methods 0 0
Messages 0 0
Branches 0 0
==========================================
+ Hits 5264 5525 +261
- Misses 1149 1169 +20
Partials 0 0
|
@@ -10,6 +10,9 @@ | |||
|
|||
##### Enhancements | |||
|
|||
* Add a rule that enforces alphabetical sorting of imports. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing the 2 trailing whitespaces
@@ -10,6 +10,9 @@ | |||
|
|||
##### Enhancements | |||
|
|||
* Add a rule that enforces alphabetical sorting of imports. | |||
[Scott Berrevoets](https://github.com/sberrevoets) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add the issue number too?
return [ | ||
StyleViolation(ruleDescription: type(of: self).description, | ||
severity: configuration.severity, | ||
location: Location(file: file, byteOffset: lastImportLocation)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should use characterOffset
, because the range is coming from a regex match. When the offset comes from SourceKit (for example, from a SyntaxToken
) you should use byteOffset
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used https://github.com/realm/SwiftLint/blob/master/Source/SwiftLintFramework/Rules/EmptyCountRule.swift#L39 for inspiration, where this also uses byteOffset
. Is that different somehow or should it be changed there as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be changed there as well, but let @jpsim confirm that. I could be wrong about this 😅
@sberrevoets 🤔 Maybe it could treat comment lines as separators to allow you to group the import statements and then alphabetise within each group? This is the most common use case I've seen for not having them all alphabetised at the file level. Or maybe that's a configurable option? |
To be honest, I've never seen that style. I've seen groupings within imports before (by having a blank line between groups), but that was mostly in the Objective-C world where every individual file had to be imported, and still at the top of the file. If we were to go down that path I think I'd use a blank line as group separator, or even any line that doesn't start with |
Why not just report violations for items that aren't "greater" than the ones before it? It would generate these violations: import ZZZ
↓import AAA
import BBB
import CCC import AAA
import BBB
import ZZZ
↓import CCC import BBB
↓import AAA
import ZZZ
↓import CCC Which IMHO is generally more useful than the current algorithm. |
That would work, though in your first example marking |
@jpsim Updated to mark violating lines the way you suggested. @marcelofabri I noticed you changed the example I based mine on so I changed this PR to use |
@sberrevoets Thanks for updating this. I've also opened #997 to prevent us to do merge something like that again 😅 |
@@ -10,6 +10,10 @@ | |||
|
|||
##### Enhancements | |||
|
|||
* Add a rule that enforces alphabetical sorting of imports. | |||
[Scott Berrevoets](https://github.com/sberrevoets) | |||
[#991](https://github.com/realm/SwiftLint/pull/991) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reference the issue number this solves (#900): https://github.com/realm/SwiftLint/blob/master/CONTRIBUTING.md#tracking-changes
|
||
public func validateFile(_ file: File) -> [StyleViolation] { | ||
let pattern = "import\\s+(\\w+)" | ||
let excludingKinds = SyntaxKind.commentAndStringKinds() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of excluding comments and strings, you should be matching a single keyword followed by a single identifier, as highlighted by getting the syntax information from imports:
$ sourcekitten syntax --text "import AAA"
[
{
"type" : "source.lang.swift.syntaxtype.keyword",
"offset" : 0,
"length" : 6
},
{
"type" : "source.lang.swift.syntaxtype.identifier",
"offset" : 7,
"length" : 3
}
]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a similar check in https://github.com/realm/SwiftLint/blob/master/Source/SwiftLintFramework/Rules/AttributesRule.swift#L67-L72, it you need inspiration ;)
"import AAA\nimport BBB\nimport CCC\nimport DDD" | ||
], | ||
triggeringExamples: [ | ||
"import AAA\nimport ZZZ\nimport BBB\nimport CCC", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be inserting the ↓
character where you expect the violation to be reported.
@@ -263,6 +263,10 @@ class RulesTests: XCTestCase { | |||
verifyRule(ReturnArrowWhitespaceRule.description) | |||
} | |||
|
|||
func testSortedImports() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you should add this to allTests
in the end of this file to make it run on Linux as well
@@ -13,6 +13,7 @@ | |||
* Add a rule that enforces alphabetical sorting of imports. | |||
[Scott Berrevoets](https://github.com/sberrevoets) | |||
[#991](https://github.com/realm/SwiftLint/pull/991) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only the issue should be listed here, not the pull request
@jpsim @marcelofabri Thanks for the patience and guiding me through! I've incorporated your comments in the latest updates. |
) | ||
|
||
public func validateFile(_ file: File) -> [StyleViolation] { | ||
let pattern = "import (\\w+)" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry to nitpick, but could you change the regex to import\\s+\\w+
, since the rule shouldn't assume that there's only one space? Also, the capture group doesn't seem to be used, so it can be remove to make the regex faster.
This would probably make the offset calculation harder, but IMO we can trigger in the import
itself (i.e. the regex match itself). What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, the current implementation would give incorrect results if you had one import with multiple spaces and another one with just one, since the whole match is used for comparison.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous implementation allowed for any number of spaces, but for exactly the reason you pointed out I decided to stick with 1 since I don't see why someone would (intentionally) use more than 1 space.
However, I like the idea of the module name triggering the warning, so I've pushed a commit that adds some complexity in finding the module name. It assumes the last character before it is a space, which means import\nMyModule
will fail, but IMO that's an acceptable compromise. If you think we should just trigger on import
I can live with that also.
I've removed the capture group from the regex as well.
return nil | ||
} | ||
|
||
let characterOffset = range.location + moduleCharacterOffset |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this use moduleStartIndex
as well? Then you can remove moduleCharacterOffset
as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, totally right!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this 👏
@@ -10,6 +10,10 @@ | |||
|
|||
##### Enhancements | |||
|
|||
* Add a rule that enforces alphabetical sorting of imports. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is now in the wrong changelog section due to 0.14 being released.
) | ||
|
||
public func validateFile(_ file: File) -> [StyleViolation] { | ||
let pattern = "import\\s+\\w+" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think of this implementation instead?
let importRanges = file.matchPattern("import\\s+\\w+",
withSyntaxKinds: [.keyword, .identifier])
let nsstringContents = file.contents.bridge()
let importLength = 6
let modulesAndOffsets: [(String, Int)] = importRanges.map { range in
let rangeAfterImport = NSRange(location: range.location + importLength,
length: range.length - importLength)
let module = nsstringContents.substring(with: rangeAfterImport)
.trimmingCharacters(in: .whitespacesAndNewlines)
let offset = NSMaxRange(range) - module.bridge().length
return (module, offset)
}
let modulePairs = zip(modulesAndOffsets, modulesAndOffsets.dropFirst())
let violatingOffsets = modulePairs.flatMap { previous, current in
return current < previous ? current.1 : nil
}
return violatingOffsets.map {
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, characterOffset: $0))
}
It avoids the flatMap
/previousMatch
side-effect mutation, avoids some of the bridging operations, leverages the existing matchPattern(_:withSyntaxKind:)
method and avoids the cutsey use of defer
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be completely honest, for me personally the hit on readability wouldn't be worth the benefits in this case:
- Not needing
previousMatch
is definitely nice, no argument there - The bridging is reduced to once for every
import
- which is a decent win but probably not very noticeable considering the matches are short and there aren't many of them - I wasn't aware of
matchPattern(_:withSyntaxKind:)
, but it appears that would even work with the current implementation (which I agree would definitely be nicer!) - I personally don't have a huge problem with the
defer
, but I can see how that can require a double-take
That said, I'm totally onboard with following the general coding practices of the existing codebase (which your proposal seems more in line with) so I've pushed another commit with that implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually agree with all your points, I didn't share that sample thinking it was necessarily superior, just had some different ideas and I wanted to spark a discussion.
Merged in #1071. Thanks again for doing this, @sberrevoets! 💯 |
Adds an opt-in rule to requiring imports to be sorted, as requested in #900.