diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift index 5c0834415..ee74d5a79 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -40,6 +40,7 @@ public let masterRuleList = RuleList( rules: ClosingBraceRule.self, NestingRule.self, OpeningBraceRule.self, OperatorFunctionWhitespaceRule.self, + ParametersListLengthRule.self, ReturnArrowWhitespaceRule.self, StatementPositionRule.self, TodoRule.self, diff --git a/Source/SwiftLintFramework/Rules/ParametersListLengthRule.swift b/Source/SwiftLintFramework/Rules/ParametersListLengthRule.swift new file mode 100644 index 000000000..d14b0a0b7 --- /dev/null +++ b/Source/SwiftLintFramework/Rules/ParametersListLengthRule.swift @@ -0,0 +1,105 @@ +// +// ParemeterListLengthRule.swift +// SwiftLint +// +// Created by Denis Lebedev on 26/01/2016. +// Copyright © 2016 Realm. All rights reserved. +// + +import Foundation +import SourceKittenFramework + +public struct ParametersListLengthRule: ASTRule, ConfigProviderRule { + public var config = SeverityLevelsConfig(warning: 5, error: 8) + + public init() {} + + public static let description = RuleDescription( + identifier: "parameters_list_length", + name: "Parameter List Length", + description: "Length of parameter list should be short.", + nonTriggeringExamples: [ + "func f2(p1: Int, p2: Int) { }", + "func f(a: Int, b: Int, c: Int, d: Int, x: Int = 42) {}", + "func f(a: [Int], b: Int, c: Int, d: Int, f: Int) -> [Int] {\n" + + "let s = a.flatMap { $0 as? [String: Int] } ?? []}}" + ], + triggeringExamples: [ + "func f(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {}", + "func f(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int = 2, g: Int) {}", + ] + ) + + public func validateFile(file: File, kind: SwiftDeclarationKind, + dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] { + if !functionKinds.contains(kind) { + return [] + } + + let nameOffset = Int(dictionary["key.nameoffset"] as? Int64 ?? 0) + let length = Int(dictionary["key.namelength"] as? Int64 ?? 0) + let substructure = dictionary["key.substructure"] as? [SourceKitRepresentable] ?? [] + + let funcDeclarationRange = Range(start: nameOffset, end: nameOffset + length) + + let allParameters = allFunctionParameters(substructure, range: funcDeclarationRange) + let defaultParameters = defaultFunctionParameters(file, range: funcDeclarationRange) + + let parametersCount = allParameters - defaultParameters + + for parameter in config.params where parametersCount > parameter.value { + let offset = Int(dictionary["key.offset"] as? Int64 ?? 0) + return [StyleViolation(ruleDescription: self.dynamicType.description, + severity: parameter.severity, + location: Location(file: file, characterOffset: offset), + reason: "{Parameters list should have \(config.warning) or less parameters: " + + "currently it has \(parametersCount)")] + } + + return [] + } + + private func allFunctionParameters(structure: [SourceKitRepresentable], + range: Range) -> Int { + + var count = 0 + for e in structure { + guard let subDict = e as? [String: SourceKitRepresentable], + key = subDict["key.kind"] as? String, + offset = subDict["key.offset"] as? Int64 else { + continue + } + + guard range ~= Int(offset) else { + return count + } + + if SwiftDeclarationKind(rawValue: key) == .VarParameter { + count += 1 + } + } + return count + } + + private func defaultFunctionParameters(file: File, range: Range) -> Int { + let funcDeclaration = file.contents[range] + return funcDeclaration.characters.filter { $0 == "=" }.count + } + + private let functionKinds: [SwiftDeclarationKind] = [ + .FunctionAccessorAddress, + .FunctionAccessorDidset, + .FunctionAccessorGetter, + .FunctionAccessorMutableaddress, + .FunctionAccessorSetter, + .FunctionAccessorWillset, + .FunctionConstructor, + .FunctionDestructor, + .FunctionFree, + .FunctionMethodClass, + .FunctionMethodInstance, + .FunctionMethodStatic, + .FunctionOperator, + .FunctionSubscript + ] +} diff --git a/Source/SwiftLintFrameworkTests/RulesTests.swift b/Source/SwiftLintFrameworkTests/RulesTests.swift index 140d1cf81..df0b77347 100644 --- a/Source/SwiftLintFrameworkTests/RulesTests.swift +++ b/Source/SwiftLintFrameworkTests/RulesTests.swift @@ -91,6 +91,10 @@ class RulesTests: XCTestCase { verifyRule(OperatorFunctionWhitespaceRule.description) } + func testParametersListLength() { + verifyRule(ParametersListLengthRule.description) + } + func testReturnArrowWhitespace() { verifyRule(ReturnArrowWhitespaceRule.description) } diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index e610372d7..75009c8aa 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 1F11B3CF1C252F23002E8FA8 /* ClosingBraceRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F11B3CE1C252F23002E8FA8 /* ClosingBraceRule.swift */; }; 24E17F721B14BB3F008195BE /* File+Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E17F701B1481FF008195BE /* File+Cache.swift */; }; 2E02005F1C54BF680024D09D /* CyclomaticComplexityRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E02005E1C54BF680024D09D /* CyclomaticComplexityRule.swift */; }; + 2E5761AA1C573B83003271AF /* ParametersListLengthRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E5761A91C573B83003271AF /* ParametersListLengthRule.swift */; }; 3B0B14541C505D6300BE82F7 /* SeverityConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B0B14531C505D6300BE82F7 /* SeverityConfig.swift */; }; 3B1150CA1C31FC3F00D83B1E /* Yaml+SwiftLint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1150C91C31FC3F00D83B1E /* Yaml+SwiftLint.swift */; }; 3B12C9C11C3209CB000B423F /* test.yml in Resources */ = {isa = PBXBuildFile; fileRef = 3B12C9BF1C3209AC000B423F /* test.yml */; }; @@ -164,6 +165,7 @@ 1F11B3CE1C252F23002E8FA8 /* ClosingBraceRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosingBraceRule.swift; sourceTree = ""; }; 24E17F701B1481FF008195BE /* File+Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "File+Cache.swift"; sourceTree = ""; }; 2E02005E1C54BF680024D09D /* CyclomaticComplexityRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CyclomaticComplexityRule.swift; sourceTree = ""; }; + 2E5761A91C573B83003271AF /* ParametersListLengthRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParametersListLengthRule.swift; sourceTree = ""; }; 3B0B14531C505D6300BE82F7 /* SeverityConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeverityConfig.swift; sourceTree = ""; }; 3B1150C91C31FC3F00D83B1E /* Yaml+SwiftLint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Yaml+SwiftLint.swift"; sourceTree = ""; }; 3B12C9BF1C3209AC000B423F /* test.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = test.yml; sourceTree = ""; }; @@ -177,7 +179,6 @@ 3BB47D821C514E8100AE6A10 /* RegexConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegexConfig.swift; sourceTree = ""; }; 3BB47D841C51D80000AE6A10 /* NSRegularExpression+SwiftLint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+SwiftLint.swift"; sourceTree = ""; }; 3BB47D861C51DE6E00AE6A10 /* CustomRulesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomRulesTests.swift; sourceTree = ""; }; - 3BCC04C51C4EFA52006073C3 /* RuleConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuleConfig.swift; sourceTree = ""; }; 3BCC04CC1C4F5694006073C3 /* ConfigurationError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationError.swift; sourceTree = ""; }; 3BCC04CF1C4F56D3006073C3 /* SeverityLevelsConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeverityLevelsConfig.swift; sourceTree = ""; }; 3BCC04D01C4F56D3006073C3 /* NameConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NameConfig.swift; sourceTree = ""; }; @@ -568,6 +569,7 @@ E81CDE701C00FEAA00B430F6 /* ValidDocsRule.swift */, E88DEA931B099C0900A66CB0 /* VariableNameRule.swift */, 2E02005E1C54BF680024D09D /* CyclomaticComplexityRule.swift */, + 2E5761A91C573B83003271AF /* ParametersListLengthRule.swift */, ); path = Rules; sourceTree = ""; @@ -844,6 +846,7 @@ 3BCC04D11C4F56D3006073C3 /* SeverityLevelsConfig.swift in Sources */, E86396C51BADAC15002C9E88 /* XcodeReporter.swift in Sources */, 3B1DF0121C5148140011BCED /* CustomRules.swift in Sources */, + 2E5761AA1C573B83003271AF /* ParametersListLengthRule.swift in Sources */, E86396C91BADB2B9002C9E88 /* JSONReporter.swift in Sources */, E881985A1BEA96EA00333A11 /* OperatorFunctionWhitespaceRule.swift in Sources */, 3BCC04D21C4F56D3006073C3 /* NameConfig.swift in Sources */,