From 6577b59cd7c04aa35c78c932d0949d27b5782717 Mon Sep 17 00:00:00 2001 From: Marcelo Fabri Date: Sun, 18 Dec 2016 00:14:00 -0200 Subject: [PATCH 1/3] Add unused_enumerated rule --- CHANGELOG.md | 5 + .../Models/MasterRuleList.swift | 1 + .../Rules/UnusedEnumeratedRule.swift | 92 +++++++++++++++++++ SwiftLint.xcodeproj/project.pbxproj | 4 + .../SwiftLintFrameworkTests/RulesTests.swift | 5 + 5 files changed, 107 insertions(+) create mode 100644 Source/SwiftLintFramework/Rules/UnusedEnumeratedRule.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index cb1eab11ce..53045734c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,6 +106,11 @@ [JP Simard](https://github.com/jpsim) [#982](https://github.com/realm/SwiftLint/issues/982) +* Add `unused_enumerated` rule that warns against unused indexes when using + `.enumerated()` on a for loop, e.g. `for (_, foo) in bar.enumerated()`. + [Marcelo Fabri](https://github.com/marcelofabri) + [#619](https://github.com/realm/SwiftLint/issues/619) + ##### Bug Fixes * Fix `weak_delegate` rule reporting a violation for variables containing diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift index 378866275c..19393fdb5b 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -96,6 +96,7 @@ public let masterRuleList = RuleList(rules: TypeBodyLengthRule.self, TypeNameRule.self, UnusedClosureParameterRule.self, + UnusedEnumeratedRule.self, ValidDocsRule.self, ValidIBInspectableRule.self, VariableNameRule.self, diff --git a/Source/SwiftLintFramework/Rules/UnusedEnumeratedRule.swift b/Source/SwiftLintFramework/Rules/UnusedEnumeratedRule.swift new file mode 100644 index 0000000000..64a0551fed --- /dev/null +++ b/Source/SwiftLintFramework/Rules/UnusedEnumeratedRule.swift @@ -0,0 +1,92 @@ +// +// UnusedEnumeratedRule.swift +// SwiftLint +// +// Created by Marcelo Fabri on 12/17/16. +// Copyright © 2016 Realm. All rights reserved. +// + +import Foundation +import SourceKittenFramework + +public struct UnusedEnumeratedRule: ASTRule, ConfigurationProviderRule { + public var configuration = SeverityConfiguration(.warning) + + public init() {} + + public static let description = RuleDescription( + identifier: "unused_enumerated", + name: "Unused Enumerated", + description: "When the index is not used, .enumerated() can be removed.", + nonTriggeringExamples: [ + "for (idx, foo) in bar.enumerated() { }\n", + "for (_, foo) in bar.enumerated().something() { }\n", + "for (_, foo) in bar.something() { }\n", + "for foo in bar.enumerated() { }\n", + "for foo in bar { }\n" + ], + triggeringExamples: [ + "for (↓_, foo) in bar.enumerated() { }\n", + "for (↓_, foo) in abc.bar.enumerated() { }\n", + "for (↓_, foo) in abc.something().enumerated() { }\n" + ] + ) + + public func validateFile(_ file: File, + kind: StatementKind, + dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] { + + guard kind == .forEach, + isEnumeratedCall(dictionary), + let byteRange = byteRangeForVariables(dictionary), + let firstToken = file.syntaxMap.tokensIn(byteRange).first, + firstToken.length == 1, + SyntaxKind(rawValue: firstToken.type) == .keyword else { + return [] + } + + return [ + StyleViolation(ruleDescription: type(of: self).description, + severity: configuration.severity, + location: Location(file: file, byteOffset: firstToken.offset)) + ] + } + + private func isEnumeratedCall(_ dictionary: [String: SourceKitRepresentable]) -> Bool { + let substructure = dictionary["key.substructure"] as? [SourceKitRepresentable] ?? [] + for subItem in substructure { + guard let subDict = subItem as? [String: SourceKitRepresentable], + let kindString = subDict["key.kind"] as? String, + SwiftExpressionKind(rawValue: kindString) == .call, + let name = subDict["key.name"] as? String else { + continue + } + + if name.hasSuffix(".enumerated") { + return true + } + } + + return false + } + + private func byteRangeForVariables(_ dictionary: [String: SourceKitRepresentable]) -> NSRange? { + guard let elements = dictionary["key.elements"] as? [SourceKitRepresentable] else { + return nil + } + + let expectedKind = "source.lang.swift.structure.elem.id" + for element in elements { + guard let subDict = element as? [String: SourceKitRepresentable], + subDict["key.kind"] as? String == expectedKind, + let offset = (subDict["key.offset"] as? Int64).map({ Int($0) }), + let length = (subDict["key.length"] as? Int64).map({ Int($0) }) else { + continue + } + + return NSRange(location: offset, length: length) + } + + return nil + } +} diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index afec74369e..03d99fb806 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -87,6 +87,7 @@ D40F83881DE9179200524C62 /* TrailingCommaConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40F83871DE9179200524C62 /* TrailingCommaConfiguration.swift */; }; D41E7E0B1DF9DABB0065259A /* RedundantStringEnumValueRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41E7E0A1DF9DABB0065259A /* RedundantStringEnumValueRule.swift */; }; D4348EEA1C46122C007707FB /* FunctionBodyLengthRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4348EE91C46122C007707FB /* FunctionBodyLengthRuleTests.swift */; }; + D43B04641E0620AB004016AF /* UnusedEnumeratedRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43B04631E0620AB004016AF /* UnusedEnumeratedRule.swift */; }; D43DB1081DC573DA00281215 /* ImplicitGetterRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43DB1071DC573DA00281215 /* ImplicitGetterRule.swift */; }; D44254201DB87CA200492EA4 /* ValidIBInspectableRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D442541E1DB87C3D00492EA4 /* ValidIBInspectableRule.swift */; }; D44254271DB9C15C00492EA4 /* SyntacticSugarRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44254251DB9C12300492EA4 /* SyntacticSugarRule.swift */; }; @@ -318,6 +319,7 @@ D40F83871DE9179200524C62 /* TrailingCommaConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrailingCommaConfiguration.swift; sourceTree = ""; }; D41E7E0A1DF9DABB0065259A /* RedundantStringEnumValueRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedundantStringEnumValueRule.swift; sourceTree = ""; }; D4348EE91C46122C007707FB /* FunctionBodyLengthRuleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionBodyLengthRuleTests.swift; sourceTree = ""; }; + D43B04631E0620AB004016AF /* UnusedEnumeratedRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnusedEnumeratedRule.swift; sourceTree = ""; }; D43DB1071DC573DA00281215 /* ImplicitGetterRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImplicitGetterRule.swift; sourceTree = ""; }; D442541E1DB87C3D00492EA4 /* ValidIBInspectableRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidIBInspectableRule.swift; sourceTree = ""; }; D44254251DB9C12300492EA4 /* SyntacticSugarRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyntacticSugarRule.swift; sourceTree = ""; }; @@ -753,6 +755,7 @@ E88DEA8D1B0999CD00A66CB0 /* TypeBodyLengthRule.swift */, E88DEA911B099B1F00A66CB0 /* TypeNameRule.swift */, D40AD0891E032F9700F48C30 /* UnusedClosureParameterRule.swift */, + D43B04631E0620AB004016AF /* UnusedEnumeratedRule.swift */, E81CDE701C00FEAA00B430F6 /* ValidDocsRule.swift */, D442541E1DB87C3D00492EA4 /* ValidIBInspectableRule.swift */, E88DEA931B099C0900A66CB0 /* VariableNameRule.swift */, @@ -1130,6 +1133,7 @@ 4A9A3A3A1DC1D75F00DF5183 /* HTMLReporter.swift in Sources */, D40F83881DE9179200524C62 /* TrailingCommaConfiguration.swift in Sources */, 3B5B9FE11C444DA20009AD27 /* Array+SwiftLint.swift in Sources */, + D43B04641E0620AB004016AF /* UnusedEnumeratedRule.swift in Sources */, E881985D1BEA97EB00333A11 /* TrailingWhitespaceRule.swift in Sources */, E832F10B1B17E2F5003F265F /* NSFileManager+SwiftLint.swift in Sources */, E816194C1BFBF35D00946723 /* SwiftDeclarationKind+SwiftLint.swift in Sources */, diff --git a/Tests/SwiftLintFrameworkTests/RulesTests.swift b/Tests/SwiftLintFrameworkTests/RulesTests.swift index e44976d3be..3c996537bc 100644 --- a/Tests/SwiftLintFrameworkTests/RulesTests.swift +++ b/Tests/SwiftLintFrameworkTests/RulesTests.swift @@ -375,6 +375,10 @@ class RulesTests: XCTestCase { verifyRule(UnusedClosureParameterRule.description) } + func testUnusedEnumerated() { + verifyRule(UnusedEnumeratedRule.description) + } + // swiftlint:disable:next todo // FIXME: https://github.com/jpsim/SourceKitten/issues/269 // func testValidDocs() { @@ -458,6 +462,7 @@ extension RulesTests { ("testTypeBodyLength", testTypeBodyLength), // ("testTypeName", testTypeName), ("testUnusedClosureParameter", testUnusedClosureParameter), + ("testUnusedEnumerated", testUnusedEnumerated), ("testValidIBInspectable", testValidIBInspectable), // ("testVariableName", testVariableName), ("testVoidReturn", testVoidReturn), From 88e724bf6f524c15e6ca6a15c96d30dfd548caf9 Mon Sep 17 00:00:00 2001 From: Marcelo Fabri Date: Sun, 18 Dec 2016 00:25:35 -0200 Subject: [PATCH 2/3] Add extra check to validate token content --- .../SwiftLintFramework/Rules/UnusedEnumeratedRule.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Source/SwiftLintFramework/Rules/UnusedEnumeratedRule.swift b/Source/SwiftLintFramework/Rules/UnusedEnumeratedRule.swift index 64a0551fed..721ff05714 100644 --- a/Source/SwiftLintFramework/Rules/UnusedEnumeratedRule.swift +++ b/Source/SwiftLintFramework/Rules/UnusedEnumeratedRule.swift @@ -41,7 +41,8 @@ public struct UnusedEnumeratedRule: ASTRule, ConfigurationProviderRule { let byteRange = byteRangeForVariables(dictionary), let firstToken = file.syntaxMap.tokensIn(byteRange).first, firstToken.length == 1, - SyntaxKind(rawValue: firstToken.type) == .keyword else { + SyntaxKind(rawValue: firstToken.type) == .keyword, + isUnderscore(file: file, token: firstToken) else { return [] } @@ -89,4 +90,9 @@ public struct UnusedEnumeratedRule: ASTRule, ConfigurationProviderRule { return nil } + + private func isUnderscore(file: File, token: SyntaxToken) -> Bool { + let contents = file.contents.bridge() + return contents.substringWithByteRange(start: token.offset, length: token.length) == "_" + } } From 86fad3c65a7535bbddd83edd4490047c68d50526 Mon Sep 17 00:00:00 2001 From: Marcelo Fabri Date: Sun, 18 Dec 2016 00:44:20 -0200 Subject: [PATCH 3/3] Add extra example --- Source/SwiftLintFramework/Rules/UnusedEnumeratedRule.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/SwiftLintFramework/Rules/UnusedEnumeratedRule.swift b/Source/SwiftLintFramework/Rules/UnusedEnumeratedRule.swift index 721ff05714..99b41c93f4 100644 --- a/Source/SwiftLintFramework/Rules/UnusedEnumeratedRule.swift +++ b/Source/SwiftLintFramework/Rules/UnusedEnumeratedRule.swift @@ -23,7 +23,8 @@ public struct UnusedEnumeratedRule: ASTRule, ConfigurationProviderRule { "for (_, foo) in bar.enumerated().something() { }\n", "for (_, foo) in bar.something() { }\n", "for foo in bar.enumerated() { }\n", - "for foo in bar { }\n" + "for foo in bar { }\n", + "for (idx, _) in bar.enumerated() { }\n" ], triggeringExamples: [ "for (↓_, foo) in bar.enumerated() { }\n",