diff --git a/CHANGELOG.md b/CHANGELOG.md index 07e746454f..6c08ac4af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -121,6 +121,9 @@ * Make `empty_count` auto-correctable. [KS1019](https://github.com/KS1019/) +* Add new `no_unnecessary_spaces` rule that No space before the first and after the last argument and exactly one space after every comma. + [u-abyss](https://github.com/u-abyss) + [#5259](https://github.com/realm/SwiftLint/issues/5224) #### Bug Fixes diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index 59b7908a1a..38e60a5364 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -134,6 +134,7 @@ public let builtInRules: [any Rule.Type] = [ NonOptionalStringDataConversionRule.self, NonOverridableClassDeclarationRule.self, NotificationCenterDetachmentRule.self, + NoUnnecessarySpacesRule.self, NumberSeparatorRule.self, ObjectLiteralRule.self, OneDelarationPerFileRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/NoUnnecessarySpacesRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/NoUnnecessarySpacesRule.swift new file mode 100644 index 0000000000..bca94525d0 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/NoUnnecessarySpacesRule.swift @@ -0,0 +1,150 @@ +import SwiftSyntax + +@SwiftSyntaxRule +struct NoUnnecessarySpacesRule: Rule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "no_unnecessary_spaces", + name: "no Unnecessary Spaces", + description: "No space before the first and after the last argument and exactly one space after every comma", + kind: .lint, + nonTriggeringExamples: [ + Example("f()"), + Example("f(true)"), + Example("f(true, false, true)"), + Example("f(a // line comment)"), + Example("f(a /* block comment */)"), + Example("f(true, /* comment */, false // line comment)"), + Example("f(/* comment */ true /* other comment */)"), + Example("f(true /* other comment */, /* comment */ false /* other comment */, false // line comment)"), + Example(""" + f( + /* comment */ + a: true, + b: true, + ) + """), + Example(""" + f( + a: true, // line comment + b: true, // line comment + ) + """) + ], + triggeringExamples: [ + Example("f(↓ )"), + Example("f(↓ )"), + Example("f(↓\t)"), + Example("f(↓ true↓ )"), + Example("f(↓ /* comment */ true /* other comment */ ↓)"), + Example("f(↓ x: 0, y: 0↓ )"), + Example("f(↓ true,↓ false, true↓ )"), + Example("f(↓ true,↓ false,↓ /* other comment */ ↓true↓ )"), + Example(""" + f( + a: true,↓ // line comment + b: true,↓ // line comment + ) + """) + ] + ) +} + +private extension TriviaPiece { + var isLineComment: Bool { + if case .lineComment = self { + return true + } else { + return false + } + } + var isBlockComment: Bool { + if case .blockComment = self { + return true + } else { + return false + } + } + var isSingleSpace: Bool { + if case .spaces(1) = self { + return true + } else { + return false + } + } +} + +private extension NoUnnecessarySpacesRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: FunctionCallExprSyntax) { + guard let leftParen = node.leftParen else { return } + checkSpaces(arguments: node.arguments, leftParen: leftParen) + } + func checkSpaces(arguments: LabeledExprListSyntax?, leftParen: TokenSyntax) { + checkLeftParenTrailingTrivia(leftParen: leftParen) + if let arguments { + arguments.enumerated().forEach { index, arg in + // By trailing trivia in the last argument, the space in front of the right bracket is checked. + if index == arguments.count - 1 { + checkArgumentTrailingTrivia(argument: arguments.last) + } + guard let trailingComma = arg.trailingComma else { return } + checkCommaTrailingTrivia(trailingComma: trailingComma) + } + } + } + + private func checkLeftParenTrailingTrivia(leftParen: TokenSyntax) { + leftParen.trailingTrivia.pieces.enumerated().forEach { index, trivia in + if trivia.isSpaceOrTab && (index == 0 || leftParen.trailingTrivia.count == 1) { + violations.append(leftParen.endPositionBeforeTrailingTrivia) + } else if trivia.isSingleSpace && leftParen.trailingTrivia.count - 1 == index { + return + } else if trivia.isSpaceOrTab { + violations.append(leftParen.endPosition) + } + } + } + + private func checkArgumentTrailingTrivia(argument: LabeledExprListSyntax.Element?) { + if let argument { + guard !argument.trailingTrivia.pieces.isEmpty else { return } + + for index in 0 ..< argument.trailingTrivia.pieces.count { + let trivia = argument.trailingTrivia.pieces[index] + + if index < argument.trailingTrivia.pieces.count - 1 { + let next = argument.trailingTrivia.pieces[index + 1] + if trivia.isSingleSpace && (next.isBlockComment || next.isLineComment) { continue } + } + + if trivia.isSpaceOrTab { + if index == 0 || argument.trailingTrivia.pieces.count == 1 { + violations.append(argument.endPositionBeforeTrailingTrivia) + } else { + violations.append(argument.endPosition) + } + } + } + } + } + + private func checkCommaTrailingTrivia(trailingComma: TokenSyntax) { + for index in 0 ..< trailingComma.trailingTrivia.pieces.count { + let trivia = trailingComma.trailingTrivia.pieces[index] + + if index < trailingComma.trailingTrivia.pieces.count - 1 { + let next = trailingComma.trailingTrivia.pieces[index + 1] + if trivia.isSingleSpace && (next.isBlockComment || next.isLineComment) { continue } + } + + if !trivia.isSingleSpace && (index == 0 || trailingComma.trailingTrivia.count == 1) { + violations.append(trailingComma.endPositionBeforeTrailingTrivia) + } else if !trivia.isSingleSpace && !trivia.isBlockComment && !trivia.isLineComment { + violations.append(trailingComma.endPosition) + } + } + } + } +} diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRuleExamples.swift index 432748d683..48cca20459 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRuleExamples.swift @@ -29,7 +29,7 @@ internal enum OperatorUsageWhitespaceRuleExamples { Example(""" let something = Something() - """ ), + """), Example(""" return path.flatMap { path in return compileCommands[path] ?? diff --git a/Tests/GeneratedTests/GeneratedTests.swift b/Tests/GeneratedTests/GeneratedTests.swift index 5d9f5acaa3..d12375506b 100644 --- a/Tests/GeneratedTests/GeneratedTests.swift +++ b/Tests/GeneratedTests/GeneratedTests.swift @@ -794,6 +794,12 @@ class NotificationCenterDetachmentRuleGeneratedTests: SwiftLintTestCase { } } +class NoUnnecessarySpacesRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(NoUnnecessarySpacesRule.description) + } +} + class NumberSeparatorRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NumberSeparatorRule.description) diff --git a/Tests/SwiftLintFrameworkTests/NoUnnecessarySpacesRuleTests.swift b/Tests/SwiftLintFrameworkTests/NoUnnecessarySpacesRuleTests.swift new file mode 100644 index 0000000000..78044eb593 --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/NoUnnecessarySpacesRuleTests.swift @@ -0,0 +1,8 @@ +@testable import SwiftLintBuiltInRules + +class NoUnnecessarySpacesRuleTests: SwiftLintTestCase { + func testNoUnnecessarySpacesRule() { + let description = NoUnnecessarySpacesRule.description + verifyRule(description) + } +} diff --git a/Tests/SwiftLintTestHelpers/TestHelpers.swift b/Tests/SwiftLintTestHelpers/TestHelpers.swift index df18ff6fed..dc896157e8 100644 --- a/Tests/SwiftLintTestHelpers/TestHelpers.swift +++ b/Tests/SwiftLintTestHelpers/TestHelpers.swift @@ -191,7 +191,7 @@ private func render(violations: [StyleViolation], in contents: String) -> String private func render(locations: [Location], in contents: String) -> String { var contents = StringView(contents).lines.map { $0.content } - for location in locations.sorted(by: > ) { + for location in locations.sorted(by: >) { guard let line = location.line, let character = location.character else { continue } let content = NSMutableString(string: contents[line - 1]) content.insert("↓", at: character - 1)