diff --git a/CHANGELOG.md b/CHANGELOG.md index 85d2e4e370d..22c68d2564c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ [Marcelo Fabri](https://github.com/marcelofabri) [#1721](https://github.com/realm/SwiftLint/issues/1721) +* Make `joined_default_parameter` correctable. + [Ornithologist Coder](https://github.com/ornithocoder) + [#1757](https://github.com/realm/SwiftLint/issues/1757) + ##### Bug Fixes * Fix false positive on `force_unwrapping` rule when declaring diff --git a/Rules.md b/Rules.md index 4a4123130cc..9372cbc9721 100644 --- a/Rules.md +++ b/Rules.md @@ -4824,7 +4824,7 @@ func foo(int: Int!) {} Identifier | Enabled by default | Supports autocorrection | Kind --- | --- | --- | --- -`joined_default_parameter` | Disabled | No | idiomatic +`joined_default_parameter` | Disabled | Yes | idiomatic Discouraged explicit usage of the default separator. diff --git a/Source/SwiftLintFramework/Rules/JoinedDefaultRule.swift b/Source/SwiftLintFramework/Rules/JoinedDefaultRule.swift index 45a8e4306aa..954b8a99315 100644 --- a/Source/SwiftLintFramework/Rules/JoinedDefaultRule.swift +++ b/Source/SwiftLintFramework/Rules/JoinedDefaultRule.swift @@ -9,7 +9,7 @@ import Foundation import SourceKittenFramework -public struct JoinedDefaultParameterRule: ASTRule, ConfigurationProviderRule, OptInRule { +public struct JoinedDefaultParameterRule: ASTRule, ConfigurationProviderRule, OptInRule, CorrectableRule { public var configuration = SeverityConfiguration(.warning) public init() {} @@ -28,9 +28,15 @@ public struct JoinedDefaultParameterRule: ASTRule, ConfigurationProviderRule, Op "let foo = bar.joined(separator: ↓\"\")", "let foo = bar.filter(toto)\n" + " .joined(separator: ↓\"\")" + ], + corrections: [ + "let foo = bar.joined(↓separator: \"\")": "let foo = bar.joined()", + "let foo = bar.filter(toto)\n.joined(↓separator: \"\")": "let foo = bar.filter(toto)\n.joined()" ] ) + // MARK: - ASTRule + public func validate(file: File, kind: SwiftExpressionKind, dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] { @@ -38,9 +44,7 @@ public struct JoinedDefaultParameterRule: ASTRule, ConfigurationProviderRule, Op kind == .call, dictionary.name?.hasSuffix(".joined") == true, let defaultSeparatorOffset = defaultSeparatorOffset(dictionary: dictionary, file: file) - else { - return [] - } + else { return [] } return [StyleViolation(ruleDescription: type(of: self).description, severity: configuration.severity, @@ -54,11 +58,66 @@ public struct JoinedDefaultParameterRule: ASTRule, ConfigurationProviderRule, Op let argumentBodyOffset = argument.bodyOffset, let argumentBodyLength = argument.bodyLength, argument.name == "separator" - else { - return nil - } + else { return nil } let body = file.contents.bridge().substringWithByteRange(start: argumentBodyOffset, length: argumentBodyLength) return body == "\"\"" ? argumentBodyOffset : nil } + + // MARK: - CorrectableRule + + public func correct(file: File) -> [Correction] { + let violatingRanges = violationRanges(in: file, dictionary: file.structure.dictionary) + let matches = file.ruleEnabled(violatingRanges: violatingRanges, for: self) + var correctedContents = file.contents + var adjustedLocations: [Int] = [] + + for violatingRange in matches.reversed() { + if let range = file.contents.nsrangeToIndexRange(violatingRange) { + correctedContents = correctedContents.replacingCharacters(in: range, with: "") + adjustedLocations.insert(violatingRange.location, at: 0) + } + } + + file.write(correctedContents) + + return adjustedLocations.map { + Correction(ruleDescription: type(of: self).description, location: Location(file: file, characterOffset: $0)) + } + } + + private func violationRanges(in file: File, dictionary: [String: SourceKitRepresentable]) -> [NSRange] { + return dictionary.substructure.flatMap { subDictionary -> [NSRange] in + let violations = violationRanges(in: file, dictionary: subDictionary) + + guard + // is it calling a method '.joined' and passing a single argument? + subDictionary.kind == SwiftExpressionKind.call.rawValue, + subDictionary.name?.hasSuffix(".joined") == true, + subDictionary.enclosedArguments.count == 1 + else { return violations } + + guard + // is this single argument called 'separator'? + let argument = subDictionary.enclosedArguments.first, + let offset = argument.offset, + let length = argument.length, + argument.name == "separator" + else { return violations } + + guard + // is this single argument the default parameter? + let bodyOffset = argument.bodyOffset, + let bodyLength = argument.bodyLength, + let body = file.contents.bridge().substringWithByteRange(start: bodyOffset, length: bodyLength), + body == "\"\"" + else { return violations } + + guard + let range = file.contents.bridge().byteRangeToNSRange(start: offset, length: length) + else { return violations } + + return violations + [range] + } + } }