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 "No Fallthrough Only" Rule #2194

Merged
merged 7 commits into from
Jun 12, 2018

Conversation

dabelknap
Copy link
Contributor

SwiftLint already has a "Fallthrough" rule, which can be used to prohibit the use of fallthrough statements in switches. At Google, we allow the use of fallthrough statements, but only if they are accompanied by other statements in the case. In other words, a case may not contain only a fallthrough. The "No Fallthrough Only" rule enforces this usage of fallthrough statements.

@jpsim
Copy link
Collaborator

jpsim commented May 9, 2018

Thanks for the contribution! I feel like this is a case where it'd be ok for this rule to be enabled by default rather than opt-in. It's reasonable for a style linter to be opinionated when there's no technical downside, such as is the case here.

@dabelknap can you think of a reason why someone would not want this, other than personal stylistic preference? In other words, are there situations where code cannot be clearly expressed without resorting to a case statement comprising of only a single fallthrough expression?

"""
switch {
case 1:
fallthrough
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 violation marks () to the examples?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How are those marks used exactly? Do you place them at the leading or trailing edge of the violation?

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 place them where the violation is reported. In this case, as an user, I'd expect the violation to be triggered on ↓fallthrough, but reading the code I believe it'll be triggered on ↓case 1:.

Adding the marker also makes the unit tests to verify that the violation is being reported on the expected offset.

kind: .idiomatic,
nonTriggeringExamples: [
"""
switch {
Copy link
Collaborator

Choose a reason for hiding this comment

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

many of these examples have compiler warnings or errors. Can you refactor them to compile cleanly, as to not distract from what's actually relevant in this rule?

name: "No Fallthrough Only",
description: "Fallthroughs can only be used if the `case` contains at least one other statement.",
kind: .idiomatic,
nonTriggeringExamples: [
Copy link
Collaborator

Choose a reason for hiding this comment

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

It'd be nice to have more complex test cases here, such as using pattern matching, or default or break statements.

return []
}

let pattern = "case[^:]+:\\s*" + // match start of case
Copy link
Collaborator

Choose a reason for hiding this comment

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

This rule could be more robust if we avoided regular expressions altogether, and instead operated on the dictionary that's being passed as a parameter in this function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It isn't clear to me how to use the dictionary to accomplish this. I am able to get the length and offset for the entire case block, but it doesn't seem to provide any additional breakdown. Do you have a suggestion for how to approach this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry I wasn't clear. You should take a look at how SwitchCaseAlignmentRule is implemented.

For example, see how it filters the dictionary for case statements in a type-safe & AST-safe manner:

let caseStatements = dictionary.substructure.filter { subDict in
// includes both `case` and `default` statements
return subDict.kind.flatMap(StatementKind.init) == .case
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure how this helps in my situation. The case statements exist as nodes on the dictionary, but it doesn't give me information about the individual tokens within the case statement. I tried grabbing the offset and length from the case node in the dictionary, and then used it to return the tokens in this range from the SyntaxMap. However, I can't think of an unambiguous way of filtering out the case keyword and any additional expressions before the colon.

@SwiftLintBot
Copy link

SwiftLintBot commented May 9, 2018

31 Warnings
⚠️ This PR introduced a violation in Firefox: /Users/distiller/project/osscheck/Firefox/Providers/Profile.swift:869:17: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Firefox: /Users/distiller/project/osscheck/Firefox/Storage/SQL/SQLiteBookmarksModel.swift:674:17: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Firefox: /Users/distiller/project/osscheck/Firefox/Storage/SQL/SQLiteBookmarksSyncing.swift:249:21: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Firefox: /Users/distiller/project/osscheck/Firefox/Sync/BookmarkPayload.swift:80:13: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Firefox: /Users/distiller/project/osscheck/Firefox/SyncTelemetry/SyncPingCentre.swift:38:13: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/core/DebuggerSupport.swift:100:9: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/core/DebuggerSupport.swift:102:9: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/core/DebuggerSupport.swift:104:9: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/core/DebuggerSupport.swift:112:9: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/IndexPath.swift:260:34: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/IndexPath.swift:273:25: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/IndexPath.swift:275:25: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/IndexPath.swift:310:25: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/IndexPath.swift:323:25: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/IndexPath.swift:325:25: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/IndexPath.swift:327:25: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/Data.swift:152:22: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/Data.swift:153:26: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/Data.swift:231:22: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/Data.swift:314:22: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/Data.swift:315:26: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/Data.swift:539:22: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/Data.swift:540:26: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in Swift: /Users/distiller/project/osscheck/Swift/stdlib/public/SDK/Foundation/Data.swift:578:13: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in WordPress: /Users/distiller/project/osscheck/WordPress/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift:534:13: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in WordPress: /Users/distiller/project/osscheck/WordPress/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift:845:17: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in WordPress: /Users/distiller/project/osscheck/WordPress/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift:847:17: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in WordPress: /Users/distiller/project/osscheck/WordPress/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift:849:17: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in WordPress: /Users/distiller/project/osscheck/WordPress/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationDetailsViewController.swift:853:17: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in WordPress: /Users/distiller/project/osscheck/WordPress/WordPress/Classes/ViewRelated/NUX/SignupUsernameTableViewController.swift:132:13: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
⚠️ This PR introduced a violation in WordPress: /Users/distiller/project/osscheck/WordPress/WordPress/Classes/ViewRelated/NUX/SiteCreationDomainsTableViewController.swift:171:13: warning: No Fallthrough Only Violation: Fallthroughs can only be used if the case contains at least one other statement. (no_fallthrough_only)
12 Messages
📖 Linting Aerial with this PR took 0.42s vs 0.38s on master (10% slower)
📖 Linting Alamofire with this PR took 3.74s vs 3.2s on master (16% slower)
📖 Linting Firefox with this PR took 14.3s vs 12.01s on master (19% slower)
📖 Linting Kickstarter with this PR took 23.36s vs 17.96s on master (30% slower)
📖 Linting Moya with this PR took 2.85s vs 2.27s on master (25% slower)
📖 Linting Nimble with this PR took 2.39s vs 2.15s on master (11% slower)
📖 Linting Quick with this PR took 0.82s vs 0.56s on master (46% slower)
📖 Linting Realm with this PR took 4.74s vs 3.46s on master (36% slower)
📖 Linting SourceKitten with this PR took 1.2s vs 1.17s on master (2% slower)
📖 Linting Sourcery with this PR took 6.14s vs 5.16s on master (18% slower)
📖 Linting Swift with this PR took 34.96s vs 30.19s on master (15% slower)
📖 Linting WordPress with this PR took 16.9s vs 16.46s on master (2% slower)

Generated by 🚫 Danger

@codecov-io
Copy link

codecov-io commented May 9, 2018

Codecov Report

Merging #2194 into master will increase coverage by 0.02%.
The diff coverage is 98.03%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2194      +/-   ##
==========================================
+ Coverage   92.08%   92.11%   +0.02%     
==========================================
  Files         284      285       +1     
  Lines       14330    14381      +51     
==========================================
+ Hits        13196    13247      +51     
  Misses       1134     1134
Impacted Files Coverage Δ
Tests/SwiftLintFrameworkTests/RulesTests.swift 100% <100%> (ø) ⬆️
...iftLintFramework/Rules/NoFallthroughOnlyRule.swift 97.95% <97.95%> (ø)
...SwiftLintFramework/Extensions/File+SwiftLint.swift 97.23% <0%> (+0.46%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6f9591e...e6517d9. Read the comment docs.

@jpsim
Copy link
Collaborator

jpsim commented May 11, 2018

Maybe @marcelofabri can improve this even further, but here are some ideas of how we can rely less on regular expressions and more on the parsed tree from SourceKit to detect fallthrough-only case bodies:

diff --git a/Source/SwiftLintFramework/Rules/NoFallthroughRuleOnlyRule.swift b/Source/SwiftLintFramework/Rules/NoFallthroughRuleOnlyRule.swift
index 1070702b..b555aa50 100644
--- a/Source/SwiftLintFramework/Rules/NoFallthroughRuleOnlyRule.swift
+++ b/Source/SwiftLintFramework/Rules/NoFallthroughRuleOnlyRule.swift
@@ -45,7 +45,7 @@ public struct NoFallthroughOnlyRule: ASTRule, OptInRule, ConfigurationProviderRu
             """
             switch {
             case 1:
-                fallthrough
+                ↓fallthrough
             case 2:
                 var a = 1
             }
@@ -55,7 +55,7 @@ public struct NoFallthroughOnlyRule: ASTRule, OptInRule, ConfigurationProviderRu
             case 1:
                 var a = 2
             case 2:
-                fallthrough
+                ↓fallthrough
             case 3:
                 var a = 3
             }
@@ -63,7 +63,7 @@ public struct NoFallthroughOnlyRule: ASTRule, OptInRule, ConfigurationProviderRu
             """
             switch {
             case 1: // comment
-                fallthrough
+                ↓fallthrough
             }
             """,
             """
@@ -71,7 +71,7 @@ public struct NoFallthroughOnlyRule: ASTRule, OptInRule, ConfigurationProviderRu
             case 1: /* multi
                 line
                 comment */
-                fallthrough
+                ↓fallthrough
             case 2:
                 var a = 2
             }
@@ -83,27 +83,33 @@ public struct NoFallthroughOnlyRule: ASTRule, OptInRule, ConfigurationProviderRu
                          kind: StatementKind,
                          dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
 
-        guard kind == StatementKind.case else {
-            return []
-        }
-
-        guard
+        guard kind == .case,
             let length = dictionary.length,
-            let offset = dictionary.offset
+            let offset = dictionary.offset,
+            case let nsstring = file.contents.bridge(),
+            let range = nsstring.byteRangeToNSRange(start: offset, length: length),
+            let colonLocation = file.match(pattern: ":", range: range).first?.0.location
         else {
             return []
         }
 
-        let pattern = "case[^:]+:\\s*" + // match start of case
-            "((//.*\\n)|" + // match double-slash comments, or
-            "(/\\*(.|\\n)*\\*/))*" + // match block comments (zero or more consecutive comments)
-            "\\s*fallthrough" // look for fallthrough immediately following case and consecutive comments
-        let range = file.contents.bridge().byteRangeToNSRange(start: offset, length: length)
+        let caseBodyRange = NSRange(location: colonLocation,
+                                    length: range.length + range.location - colonLocation)
+        let nonCommentCaseBody = file.match(pattern: "\\w+", range: caseBodyRange).filter { _, syntaxKinds in
+            return !Set(syntaxKinds).subtracting(SyntaxKind.commentKinds).isEmpty
+        }
+
+        guard nonCommentCaseBody.count == 1 else {
+            return []
+        }
 
-        return file.match(pattern: pattern, range: range).map { nsRange, _ in
-            StyleViolation(ruleDescription: type(of: self).description,
-                           severity: configuration.severity,
-                           location: Location(file: file, characterOffset: nsRange.location))
+        let nsRange = nonCommentCaseBody[0].0
+        if nsstring.substring(with: nsRange) == "fallthrough" && nonCommentCaseBody[0].1 == [.keyword] {
+            return [StyleViolation(ruleDescription: type(of: self).description,
+                                   severity: configuration.severity,
+                                   location: Location(file: file, characterOffset: nsRange.location))]
         }
+
+        return []
     }
 }

This also adds the violation markers () to the triggering examples where violations should be reported.

@dabelknap
Copy link
Contributor Author

Thanks for the code suggestion. I'm realizing that using the colon for delineating the case body might not work:

case MyFun(x: 1, y: 2):
    var a = 1
    ...

My original solution would not have handled this properly either.

@dabelknap
Copy link
Contributor Author

Just checking in on this PR. Is there anything else that needs to be done to get this approved?

@marcelofabri
Copy link
Collaborator

marcelofabri commented Jun 10, 2018

@dabelknap Sorry for the delay. This looks good, but we need a changelog entry in the format described in CONTRIBUTING.md.

@dabelknap
Copy link
Contributor Author

@marcelofabri Do you have any idea why merging from master would break the OSSCheck?

@marcelofabri
Copy link
Collaborator

We have a rule on Danger to avoid merge commits. Can you please rebase instead?

@dabelknap dabelknap force-pushed the no-fallthrough-only branch from dc25cc3 to e6517d9 Compare June 11, 2018 16:49
@dabelknap
Copy link
Contributor Author

@marcelofabri Yep, done.

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! 💯

@marcelofabri marcelofabri merged commit 3eba5e9 into realm:master Jun 12, 2018
@marcelofabri marcelofabri mentioned this pull request Jun 12, 2018
2 tasks
@jpsim
Copy link
Collaborator

jpsim commented Jun 23, 2018

Thanks to both of you for driving this home! 💯

@iliaskarim
Copy link

iliaskarim commented Apr 8, 2019

are there situations where code cannot be clearly expressed without resorting to a case statement comprising of only a single fallthrough expression?

how about if a case should do the same as the default in which case there would only be a fallthrough statement as cases cannot be combined with the default?

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.

6 participants