diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b15562a26..cb1171e9de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ ### Enhancements +* Add `multiple_statements` opt-in rule + that triggers when there are multiple statements on the same line. + [Ahmad Zaghloul](https://github.com/AhmedZaghloul19) + * Exclude types with a `@Suite` attribute and functions annotated with `@Test` from `no_magic_numbers` rule. Also treat a type as a `@Suite` if it contains `@Test` functions. [SimplyDanny](https://github.com/SimplyDanny) diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index 6d25c0e66a..c4e9cfd536 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -121,6 +121,7 @@ public let builtInRules: [any Rule.Type] = [ MultilineParametersBracketsRule.self, MultilineParametersRule.self, MultipleClosuresWithTrailingClosureRule.self, + MultipleStatementsRule.self, NSLocalizedStringKeyRule.self, NSLocalizedStringRequireBundleRule.self, NSNumberInitAsFunctionReferenceRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/MultipleStatementsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/MultipleStatementsRule.swift new file mode 100644 index 0000000000..3ccb3882c7 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/MultipleStatementsRule.swift @@ -0,0 +1,130 @@ +import SwiftSyntax + +@SwiftSyntaxRule(explicitRewriter: true, optIn: true) +struct MultipleStatementsRule: Rule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "multiple_statements", + name: "Multiple Statements", + description: "Every statement should be on its own line", + kind: .idiomatic, + nonTriggeringExamples: [ + Example( + """ + let a = 1; + let b = 2; + """ + ), + Example( + """ + var x = 10 + var y = 20; + """ + ), + Example( + """ + let a = 1; + return a + """ + ), + Example( + """ + if b { return }; + let a = 1 + """ + ), + ], + triggeringExamples: [ + Example("let a = 1; return a"), + Example("if b { return }; let a = 1"), + Example("if b { return }; if c { return }"), + Example("let a = 1; let b = 2; let c = 0"), + Example("var x = 10; var y = 20"), + Example("let x = 10; var y = 20"), + ], + corrections: [ + Example("let a = 0↓; let b = 0"): + Example( + """ + let a = 0 + let b = 0 + """ + ), + Example("let a = 0↓; let b = 0↓; let c = 0"): + Example( + """ + let a = 0 + let b = 0 + let c = 0 + """ + ), + Example("let a = 0↓; print(\"Hello\")"): + Example( + """ + let a = 0 + print(\"Hello\") + """ + ), + ] + ) +} + +private extension MultipleStatementsRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: TokenSyntax) { + if node.isThereStatementAfterSemicolon { + violations.append(node.positionAfterSkippingLeadingTrivia) + } + } + } + + final class Rewriter: ViolationsSyntaxRewriter { + override func visit(_ node: TokenSyntax) -> TokenSyntax { + guard node.isThereStatementAfterSemicolon else { + return super.visit(node) + } + + correctionPositions.append(node.positionAfterSkippingLeadingTrivia) + let newNode = TokenSyntax( + .unknown(""), + leadingTrivia: node.leadingTrivia, + trailingTrivia: .newlines(1), + presence: .present + ) + return super.visit(newNode) + } + } +} + +private extension TokenSyntax { + var isThereStatementAfterSemicolon: Bool { + guard tokenKind == .semicolon, + !trailingTrivia.isEmpty else { return false } + + if let nextToken = nextToken(viewMode: .sourceAccurate), + isFollowedOnlyByWhitespaceOrNewline { + return nextToken.leadingTrivia.containsNewlines() == false + } + return true + } + + var isFollowedOnlyByWhitespaceOrNewline: Bool { + guard let nextToken = nextToken(viewMode: .sourceAccurate), + !nextToken.trailingTrivia.isEmpty else { + return true + } + return nextToken.leadingTrivia.allSatisfy { $0.isWhitespaceOrNewline } + } +} + +private extension TriviaPiece { + var isWhitespaceOrNewline: Bool { + switch self { + case .spaces, .tabs, .newlines, .carriageReturns, .carriageReturnLineFeeds: + return true + default: + return false + } + } +} diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Remote.swift b/Source/SwiftLintFramework/Configuration/Configuration+Remote.swift index e8eec05c86..6225eaf2e1 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Remote.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Remote.swift @@ -86,7 +86,10 @@ internal extension Configuration.FileGraph.FilePath { // Block main thread until timeout is reached / task is done while true { if taskDone { break } - if Date().timeIntervalSince(startDate) > timeout { task.cancel(); break } + if Date().timeIntervalSince(startDate) > timeout { + task.cancel() + break + } usleep(50_000) // Sleep for 50 ms } diff --git a/Tests/GeneratedTests/GeneratedTests.swift b/Tests/GeneratedTests/GeneratedTests.swift index 31c22d3903..211ee6f82b 100644 --- a/Tests/GeneratedTests/GeneratedTests.swift +++ b/Tests/GeneratedTests/GeneratedTests.swift @@ -715,6 +715,12 @@ final class MultipleClosuresWithTrailingClosureRuleGeneratedTests: SwiftLintTest } } +final class MultipleStatementsRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(MultipleStatementsRule.description) + } +} + final class NSLocalizedStringKeyRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(NSLocalizedStringKeyRule.description) diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index 44a66483b2..aa784c32b1 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -564,6 +564,10 @@ multiple_closures_with_trailing_closure: severity: warning meta: opt-in: false +multiple_statements_declaration: + severity: warning + meta: + opt-in: true nesting: type_level: warning: 1