From f95768d2e61d1b2b61f0a6122a551aead9b85499 Mon Sep 17 00:00:00 2001 From: Marcelo Fabri Date: Sat, 8 Feb 2020 13:57:17 -0800 Subject: [PATCH] Add `tuple_pattern` opt-in rule (#3086) * Add `tuple_pattern` opt-in rule Fixes #2203 * Remove unused import --- CHANGELOG.md | 6 ++ .../Models/MasterRuleList.swift | 1 + .../Rules/Idiomatic/TuplePatternRule.swift | 89 +++++++++++++++++++ SwiftLint.xcodeproj/project.pbxproj | 4 + Tests/LinuxMain.swift | 7 ++ .../AutomaticRuleTests.generated.swift | 6 ++ 6 files changed, 113 insertions(+) create mode 100644 Source/SwiftLintFramework/Rules/Idiomatic/TuplePatternRule.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index d1ddc0ee92..5e40400a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,12 @@ [SwiftSyntax](https://github.com/apple/swift-syntax). [Marcelo Fabri](https://github.com/marcelofabri) +* Add `tuple_pattern` opt-in rule to warn against using + assigning variables through a tuple pattern when the left side + of the assignment contains labels. + [Marcelo Fabri](https://github.com/marcelofabri) + [#2203](https://github.com/realm/SwiftLint/issues/2203) + * Add `return_value_from_void_function` opt-in rule to warn against using `return ` in a function that is `Void`. [Marcelo Fabri](https://github.com/marcelofabri) diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift index 3b4c42e6de..4247848078 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -167,6 +167,7 @@ public let masterRuleList = RuleList(rules: [ TrailingNewlineRule.self, TrailingSemicolonRule.self, TrailingWhitespaceRule.self, + TuplePatternRule.self, TypeBodyLengthRule.self, TypeContentsOrderRule.self, TypeNameRule.self, diff --git a/Source/SwiftLintFramework/Rules/Idiomatic/TuplePatternRule.swift b/Source/SwiftLintFramework/Rules/Idiomatic/TuplePatternRule.swift new file mode 100644 index 0000000000..80253bd94a --- /dev/null +++ b/Source/SwiftLintFramework/Rules/Idiomatic/TuplePatternRule.swift @@ -0,0 +1,89 @@ +import SourceKittenFramework +#if canImport(SwiftSyntax) +import SwiftSyntax +#endif + +public struct TuplePatternRule: ConfigurationProviderRule, SyntaxRule, AutomaticTestableRule { + public var configuration = SeverityConfiguration(.warning) + + public init() {} + + public static let description = RuleDescription( + identifier: "tuple_pattern", + name: "Tuple Pattern", + description: "Assigning variables through a tuple pattern is only permitted if the left-hand side of the " + + "assignment is unlabeled.", + kind: .idiomatic, + minSwiftVersion: .fiveDotOne, + nonTriggeringExamples: [ + Example("let (a, b) = (y: 4, x: 5.0)"), + Example("let (a, b) = (4, 5.0)"), + Example("let (a, b) = (a: 4, b: 5.0)"), + Example("let (a, b) = tuple") + ], + triggeringExamples: [ + Example("let ↓(x: a, y: b) = (y: 4, x: 5.0)"), + Example("let ↓(x: Int, y: Double) = (y: 4, x: 5.0)"), + Example("let ↓(x: Int, y: Double) = (y: 4, x: 5.0)") + ] + ) + + public func validate(file: SwiftLintFile) -> [StyleViolation] { + #if canImport(SwiftSyntax) + return validate(file: file, visitor: PatternBindingVisitor()) + #else + return [] + #endif + } +} + +#if canImport(SwiftSyntax) +private class PatternBindingVisitor: SyntaxRuleVisitor { + private var positions = [AbsolutePosition]() + + func visit(_ node: PatternBindingSyntax) -> SyntaxVisitorContinueKind { + if let tuplePattern = node.pattern as? TuplePatternSyntax, + case let leftSideLabels = tuplePattern.labels, + !leftSideLabels.compactMap({ $0 }).isEmpty, + let rightSideLabels = node.initializer?.tupleElementList?.labels, + leftSideLabels != rightSideLabels { + positions.append(node.positionAfterSkippingLeadingTrivia) + } + return .visitChildren + } + + func violations(for rule: TuplePatternRule, in file: SwiftLintFile) -> [StyleViolation] { + return positions.map { position in + StyleViolation(ruleDescription: type(of: rule).description, + severity: rule.configuration.severity, + location: Location(file: file, byteOffset: ByteCount(position.utf8Offset))) + } + } +} + +private extension TuplePatternSyntax { + var labels: [String?] { + return elements.map { element in + element.labelName?.withoutTrivia().text + } + } +} + +private extension InitializerClauseSyntax { + var tupleElementList: TupleElementListSyntax? { + if let expr = value as? TupleExprSyntax { + return expr.elementList + } + + return nil + } +} + +private extension TupleElementListSyntax { + var labels: [String?] { + return map { element in + element.label?.withoutTrivia().text + } + } +} +#endif diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index 1db531758c..af5448fb83 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -307,6 +307,7 @@ D46252541DF63FB200BE2CA1 /* NumberSeparatorRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46252531DF63FB200BE2CA1 /* NumberSeparatorRule.swift */; }; D466B620233D229F0068190B /* FlatMapOverMapReduceRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D466B61F233D229F0068190B /* FlatMapOverMapReduceRule.swift */; }; D467275823DD971300DE73B6 /* VoidFunctionInTernaryConditionRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D467275723DD971300DE73B6 /* VoidFunctionInTernaryConditionRule.swift */; }; + D467275A23DE71D200DE73B6 /* TuplePatternRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D467275923DE71D200DE73B6 /* TuplePatternRule.swift */; }; D46A317F1F1CEDCD00AF914A /* UnneededParenthesesInClosureArgumentRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46A317E1F1CEDCD00AF914A /* UnneededParenthesesInClosureArgumentRule.swift */; }; D46C7C3E23BF2F6A007C517F /* PreferSelfTypeOverTypeOfSelfRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46C7C3D23BF2F6A007C517F /* PreferSelfTypeOverTypeOfSelfRule.swift */; }; D46E041D1DE3712C00728374 /* TrailingCommaRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46E041C1DE3712C00728374 /* TrailingCommaRule.swift */; }; @@ -838,6 +839,7 @@ D46252531DF63FB200BE2CA1 /* NumberSeparatorRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberSeparatorRule.swift; sourceTree = ""; }; D466B61F233D229F0068190B /* FlatMapOverMapReduceRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlatMapOverMapReduceRule.swift; sourceTree = ""; }; D467275723DD971300DE73B6 /* VoidFunctionInTernaryConditionRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoidFunctionInTernaryConditionRule.swift; sourceTree = ""; }; + D467275923DE71D200DE73B6 /* TuplePatternRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuplePatternRule.swift; sourceTree = ""; }; D46A317E1F1CEDCD00AF914A /* UnneededParenthesesInClosureArgumentRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnneededParenthesesInClosureArgumentRule.swift; sourceTree = ""; }; D46C7C3D23BF2F6A007C517F /* PreferSelfTypeOverTypeOfSelfRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferSelfTypeOverTypeOfSelfRule.swift; sourceTree = ""; }; D46E041C1DE3712C00728374 /* TrailingCommaRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrailingCommaRule.swift; sourceTree = ""; }; @@ -1377,6 +1379,7 @@ D44254251DB9C12300492EA4 /* SyntacticSugarRule.swift */, 7551DF6C21382C9A00AA1F4D /* ToggleBoolRule.swift */, E87E4A041BFB927C00FCFE46 /* TrailingSemicolonRule.swift */, + D467275923DE71D200DE73B6 /* TuplePatternRule.swift */, E88DEA911B099B1F00A66CB0 /* TypeNameRule.swift */, D4130D981E16CC1300242361 /* TypeNameRuleExamples.swift */, D4DE9131207B4731000FFAA8 /* UnavailableFunctionRule.swift */, @@ -2324,6 +2327,7 @@ E83530C61ED6328A00FBAF79 /* FileNameRule.swift in Sources */, 3BB47D831C514E8100AE6A10 /* RegexConfiguration.swift in Sources */, D401D9261ED85EF0005DA5D4 /* RuleKind.swift in Sources */, + D467275A23DE71D200DE73B6 /* TuplePatternRule.swift in Sources */, 622AD800216ACE6300A002C6 /* XCTSpecificMatcherRuleExamples.swift in Sources */, 82EB7886215BAE790042E0FD /* TypeContentsOrderRuleExamples.swift in Sources */, 7551DF6D21382C9A00AA1F4D /* ToggleBoolRule.swift in Sources */, diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index f2654359f8..ce6d6b5851 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1450,6 +1450,12 @@ extension TrailingWhitespaceTests { ] } +extension TuplePatternRuleTests { + static var allTests: [(String, (TuplePatternRuleTests) -> () throws -> Void)] = [ + ("testWithDefaultConfiguration", testWithDefaultConfiguration) + ] +} + extension TypeBodyLengthRuleTests { static var allTests: [(String, (TypeBodyLengthRuleTests) -> () throws -> Void)] = [ ("testWithDefaultConfiguration", testWithDefaultConfiguration) @@ -1852,6 +1858,7 @@ XCTMain([ testCase(TrailingCommaRuleTests.allTests), testCase(TrailingSemicolonRuleTests.allTests), testCase(TrailingWhitespaceTests.allTests), + testCase(TuplePatternRuleTests.allTests), testCase(TypeBodyLengthRuleTests.allTests), testCase(TypeContentsOrderRuleTests.allTests), testCase(TypeNameRuleTests.allTests), diff --git a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift index f740ae939b..c2f9981777 100644 --- a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift +++ b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift @@ -720,6 +720,12 @@ class TrailingSemicolonRuleTests: XCTestCase { } } +class TuplePatternRuleTests: XCTestCase { + func testWithDefaultConfiguration() { + verifyRule(TuplePatternRule.description) + } +} + class TypeBodyLengthRuleTests: XCTestCase { func testWithDefaultConfiguration() { verifyRule(TypeBodyLengthRule.description)