Skip to content
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

Closed
wants to merge 7 commits into from
Closed

Add rule requiring imports be sorted #991

wants to merge 7 commits into from

Conversation

sberrevoets
Copy link

@sberrevoets sberrevoets commented Dec 15, 2016

Adds an opt-in rule to requiring imports to be sorted, as requested in #900.

@codecov-io
Copy link

codecov-io commented Dec 15, 2016

Current coverage is 82.53% (diff: 92.85%)

Merging #991 into master will increase coverage by 0.45%

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

Powered by Codecov. Last update a056b14...eccdd38

@@ -10,6 +10,9 @@

##### Enhancements

* Add a rule that enforces alphabetical sorting of imports.
Copy link
Collaborator

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)
Copy link
Collaborator

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))
Copy link
Collaborator

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

Copy link
Author

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?

Copy link
Collaborator

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 😅

@ghost
Copy link

ghost commented Dec 16, 2016

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

@sberrevoets
Copy link
Author

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 import.

@jpsim
Copy link
Collaborator

jpsim commented Dec 16, 2016

It's hard to determine exactly which import is in violation if the list is not sorted. For example, in this list:

import ZZZ
import AAA
import BBB
import CCC

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.

@sberrevoets
Copy link
Author

That would work, though in your first example marking AAA would still be a little weird since really it's ZZZ that's out of order. But I agree that it would still be more useful than the current implementation, so I'll change the code to behave like that.

@sberrevoets
Copy link
Author

@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 characterOffset as well.

@marcelofabri
Copy link
Collaborator

@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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


public func validateFile(_ file: File) -> [StyleViolation] {
let pattern = "import\\s+(\\w+)"
let excludingKinds = SyntaxKind.commentAndStringKinds()
Copy link
Collaborator

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

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"import AAA\nimport BBB\nimport CCC\nimport DDD"
],
triggeringExamples: [
"import AAA\nimport ZZZ\nimport BBB\nimport CCC",
Copy link
Collaborator

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() {
Copy link
Collaborator

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)
Copy link
Collaborator

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

@sberrevoets
Copy link
Author

@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+)"
Copy link
Collaborator

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?

Copy link
Collaborator

@marcelofabri marcelofabri Dec 21, 2016

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.

Copy link
Author

@sberrevoets sberrevoets Dec 21, 2016

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
Copy link
Collaborator

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

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, totally right!

Copy link
Collaborator

@marcelofabri marcelofabri left a 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.
Copy link
Collaborator

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+"
Copy link
Collaborator

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.

Copy link
Author

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.

Copy link
Collaborator

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.

@marcelofabri marcelofabri mentioned this pull request Dec 26, 2016
@marcelofabri
Copy link
Collaborator

Merged in #1071. Thanks again for doing this, @sberrevoets! 💯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants