Skip to content

Commit

Permalink
Add tuple_pattern opt-in rule (#3086)
Browse files Browse the repository at this point in the history
* Add `tuple_pattern` opt-in rule

Fixes #2203

* Remove unused import
  • Loading branch information
marcelofabri authored Feb 8, 2020
1 parent bdede7b commit f95768d
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <expression>` in a function that is `Void`.
[Marcelo Fabri](https://github.com/marcelofabri)
Expand Down
1 change: 1 addition & 0 deletions Source/SwiftLintFramework/Models/MasterRuleList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ public let masterRuleList = RuleList(rules: [
TrailingNewlineRule.self,
TrailingSemicolonRule.self,
TrailingWhitespaceRule.self,
TuplePatternRule.self,
TypeBodyLengthRule.self,
TypeContentsOrderRule.self,
TypeNameRule.self,
Expand Down
89 changes: 89 additions & 0 deletions Source/SwiftLintFramework/Rules/Idiomatic/TuplePatternRule.swift
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions SwiftLint.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -838,6 +839,7 @@
D46252531DF63FB200BE2CA1 /* NumberSeparatorRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberSeparatorRule.swift; sourceTree = "<group>"; };
D466B61F233D229F0068190B /* FlatMapOverMapReduceRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlatMapOverMapReduceRule.swift; sourceTree = "<group>"; };
D467275723DD971300DE73B6 /* VoidFunctionInTernaryConditionRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoidFunctionInTernaryConditionRule.swift; sourceTree = "<group>"; };
D467275923DE71D200DE73B6 /* TuplePatternRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuplePatternRule.swift; sourceTree = "<group>"; };
D46A317E1F1CEDCD00AF914A /* UnneededParenthesesInClosureArgumentRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnneededParenthesesInClosureArgumentRule.swift; sourceTree = "<group>"; };
D46C7C3D23BF2F6A007C517F /* PreferSelfTypeOverTypeOfSelfRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferSelfTypeOverTypeOfSelfRule.swift; sourceTree = "<group>"; };
D46E041C1DE3712C00728374 /* TrailingCommaRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrailingCommaRule.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
7 changes: 7 additions & 0 deletions Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,12 @@ class TrailingSemicolonRuleTests: XCTestCase {
}
}

class TuplePatternRuleTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(TuplePatternRule.description)
}
}

class TypeBodyLengthRuleTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(TypeBodyLengthRule.description)
Expand Down

0 comments on commit f95768d

Please sign in to comment.