diff --git a/CHANGELOG.md b/CHANGELOG.md index 50835855b43..4309e638689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ ### Enhancements +* Add the opt-in `imports_at_top` rule to require file-scope imports to appear + before other declarations. + [udithaAnuranjana](https://github.com/udithaAnuranjana) + [#6892](https://github.com/realm/SwiftLint/issues/6892) + * Add autocorrection to the `multiline_call_arguments` rule, expanding single-line and multi-line calls to one-argument-per-line, including nested calls whose closing `)` would otherwise be stranded with the last argument. diff --git a/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift b/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift index 8b055fa8a5c..8aafc68b0b2 100644 --- a/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift +++ b/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift @@ -1,6 +1,10 @@ import Foundation import PackagePlugin +#if canImport(XcodeProjectPlugin) +import XcodeProjectPlugin +#endif + @main struct SwiftLintBuildToolPlugin: BuildToolPlugin { func createBuildCommands( @@ -89,8 +93,6 @@ private func addedToEnvironment(_ new: [String: String]) -> [String: String] { #if canImport(XcodeProjectPlugin) -import XcodeProjectPlugin - // swiftlint:disable:next no_grouping_extension extension SwiftLintBuildToolPlugin: XcodeBuildToolPlugin { func createBuildCommands( diff --git a/Plugins/SwiftLintCommandPlugin/CommandContext.swift b/Plugins/SwiftLintCommandPlugin/CommandContext.swift index 3f3f6daef05..cc8bc832430 100644 --- a/Plugins/SwiftLintCommandPlugin/CommandContext.swift +++ b/Plugins/SwiftLintCommandPlugin/CommandContext.swift @@ -1,5 +1,9 @@ import PackagePlugin +#if canImport(XcodeProjectPlugin) +import XcodeProjectPlugin +#endif + protocol CommandContext { var tool: String { get throws } @@ -54,8 +58,6 @@ extension PluginContext: CommandContext { #if canImport(XcodeProjectPlugin) -import XcodeProjectPlugin - extension XcodePluginContext: CommandContext { var tool: String { get throws { diff --git a/Plugins/SwiftLintCommandPlugin/SwiftLintCommandPlugin.swift b/Plugins/SwiftLintCommandPlugin/SwiftLintCommandPlugin.swift index fb208c4d07f..d57eb89497b 100644 --- a/Plugins/SwiftLintCommandPlugin/SwiftLintCommandPlugin.swift +++ b/Plugins/SwiftLintCommandPlugin/SwiftLintCommandPlugin.swift @@ -1,6 +1,10 @@ import Foundation import PackagePlugin +#if canImport(XcodeProjectPlugin) +import XcodeProjectPlugin +#endif + private let commandsNotExpectingPaths: Set = [ "docs", "generate-docs", @@ -23,8 +27,6 @@ struct SwiftLintCommandPlugin: CommandPlugin { #if canImport(XcodeProjectPlugin) -import XcodeProjectPlugin - extension SwiftLintCommandPlugin: XcodeCommandPlugin { func performCommand(context: XcodePluginContext, arguments: [String]) throws { try lintFiles(context: context, arguments: arguments) diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index 51b2a4eee41..f4567579b8c 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -92,6 +92,7 @@ public let builtInRules: [any Rule.Type] = [ ImplicitOptionalInitializationRule.self, ImplicitReturnRule.self, ImplicitlyUnwrappedOptionalRule.self, + ImportsAtTopRule.self, InclusiveLanguageRule.self, IncompatibleConcurrencyAnnotationRule.self, IndentationWidthRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ImportsAtTopRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ImportsAtTopRule.swift new file mode 100644 index 00000000000..86998691647 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ImportsAtTopRule.swift @@ -0,0 +1,155 @@ +import SwiftLintCore +import SwiftSyntax + +@SwiftSyntaxRule(optIn: true) +struct ImportsAtTopRule: Rule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "imports_at_top", + name: "Imports at Top", + description: "Imports should appear at the beginning of the file", + kind: .style, + nonTriggeringExamples: #examples([ + """ + struct Foo {} + """, + """ + import Foundation + import UIKit + @testable import MyModule + + struct Foo {} + """, + """ + // File header comment + + import Foundation + import UIKit + + struct Foo {} + """, + """ + #if canImport(UIKit) + import UIKit + #else + import AppKit + #endif + + import Foundation + + struct Foo {} + """, + """ + #if canImport(UIKit) + #if DEBUG + import UIKit + #endif + #endif + + import Foundation + + struct Foo {} + """, + ]), + triggeringExamples: #examples([ + """ + import Foundation + + struct Foo {} + + ↓import UIKit + """, + """ + struct FooTests {} + + @testable ↓import MyModule + """, + """ + struct Foo {} + + #if canImport(UIKit) + ↓import UIKit + #else + ↓import AppKit + #endif + """, + """ + #if DEBUG + struct DebugHelper {} + + ↓import Testing + #endif + """, + """ + #if DEBUG + struct DebugHelper {} + + #if canImport(UIKit) + ↓import UIKit + #endif + #endif + """, + ]) + ) +} + +private extension ImportsAtTopRule { + final class Visitor: ViolationsSyntaxVisitor { + override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all } + + override func visitPost(_ node: SourceFileSyntax) { + var hasSeenNonImportItem = false + checkImportPlacement( + in: node.statements, + hasSeenNonImportItem: &hasSeenNonImportItem + ) + } + + private func checkImportPlacement(in statements: CodeBlockItemListSyntax, hasSeenNonImportItem: inout Bool) { + for statement in statements { + if let importDecl = statement.item.as(ImportDeclSyntax.self) { + if hasSeenNonImportItem { + violations.append( + importDecl.importKeyword.positionAfterSkippingLeadingTrivia + ) + } + continue + } + + if let ifConfig = statement.item.as(IfConfigDeclSyntax.self) { + checkImportPlacement( + in: ifConfig, + hasSeenNonImportItem: &hasSeenNonImportItem + ) + continue + } + + hasSeenNonImportItem = true + } + } + + private func checkImportPlacement(in ifConfig: IfConfigDeclSyntax, hasSeenNonImportItem: inout Bool) { + let hadSeenNonImportItemBeforeIf = hasSeenNonImportItem + var anyClauseHasSeenNonImportItem = false + + for clause in ifConfig.clauses { + guard let statements = clause.elements?.as(CodeBlockItemListSyntax.self) else { + continue + } + + var clauseHasSeenNonImportItem = hadSeenNonImportItemBeforeIf + + checkImportPlacement( + in: statements, + hasSeenNonImportItem: &clauseHasSeenNonImportItem + ) + + if clauseHasSeenNonImportItem { + anyClauseHasSeenNonImportItem = true + } + } + hasSeenNonImportItem = hadSeenNonImportItemBeforeIf || anyClauseHasSeenNonImportItem + } + } +} diff --git a/Tests/GeneratedTests/GeneratedTests_04.swift b/Tests/GeneratedTests/GeneratedTests_04.swift index 52c56efca3b..69e550e537e 100644 --- a/Tests/GeneratedTests/GeneratedTests_04.swift +++ b/Tests/GeneratedTests/GeneratedTests_04.swift @@ -129,6 +129,14 @@ struct ImplicitlyUnwrappedOptionalRuleGeneratedTests { } } +@Suite(.rulesRegistered) +struct ImportsAtTopRuleGeneratedTests { + @Test + func withDefaultConfiguration() { + verifyRule(ImportsAtTopRule.description) + } +} + @Suite(.rulesRegistered) struct InclusiveLanguageRuleGeneratedTests { @Test @@ -200,11 +208,3 @@ struct LastWhereRuleGeneratedTests { verifyRule(LastWhereRule.description) } } - -@Suite(.rulesRegistered) -struct LeadingWhitespaceRuleGeneratedTests { - @Test - func withDefaultConfiguration() { - verifyRule(LeadingWhitespaceRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_05.swift b/Tests/GeneratedTests/GeneratedTests_05.swift index 6e42efeddb7..1843544e483 100644 --- a/Tests/GeneratedTests/GeneratedTests_05.swift +++ b/Tests/GeneratedTests/GeneratedTests_05.swift @@ -9,6 +9,14 @@ import Testing @testable import SwiftLintBuiltInRules @testable import SwiftLintCore +@Suite(.rulesRegistered) +struct LeadingWhitespaceRuleGeneratedTests { + @Test + func withDefaultConfiguration() { + verifyRule(LeadingWhitespaceRule.description) + } +} + @Suite(.rulesRegistered) struct LegacyCGGeometryFunctionsRuleGeneratedTests { @Test @@ -200,11 +208,3 @@ struct MultilineParametersBracketsRuleGeneratedTests { verifyRule(MultilineParametersBracketsRule.description) } } - -@Suite(.rulesRegistered) -struct MultilineParametersRuleGeneratedTests { - @Test - func withDefaultConfiguration() { - verifyRule(MultilineParametersRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_06.swift b/Tests/GeneratedTests/GeneratedTests_06.swift index 25430d5f6d5..9a4afb1e903 100644 --- a/Tests/GeneratedTests/GeneratedTests_06.swift +++ b/Tests/GeneratedTests/GeneratedTests_06.swift @@ -9,6 +9,14 @@ import Testing @testable import SwiftLintBuiltInRules @testable import SwiftLintCore +@Suite(.rulesRegistered) +struct MultilineParametersRuleGeneratedTests { + @Test + func withDefaultConfiguration() { + verifyRule(MultilineParametersRule.description) + } +} + @Suite(.rulesRegistered) struct MultipleClosuresWithTrailingClosureRuleGeneratedTests { @Test @@ -200,11 +208,3 @@ struct OrphanedDocCommentRuleGeneratedTests { verifyRule(OrphanedDocCommentRule.description) } } - -@Suite(.rulesRegistered) -struct OverriddenSuperCallRuleGeneratedTests { - @Test - func withDefaultConfiguration() { - verifyRule(OverriddenSuperCallRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_07.swift b/Tests/GeneratedTests/GeneratedTests_07.swift index 37a1913ba47..f1a50dd1f25 100644 --- a/Tests/GeneratedTests/GeneratedTests_07.swift +++ b/Tests/GeneratedTests/GeneratedTests_07.swift @@ -9,6 +9,14 @@ import Testing @testable import SwiftLintBuiltInRules @testable import SwiftLintCore +@Suite(.rulesRegistered) +struct OverriddenSuperCallRuleGeneratedTests { + @Test + func withDefaultConfiguration() { + verifyRule(OverriddenSuperCallRule.description) + } +} + @Suite(.rulesRegistered) struct OverrideInExtensionRuleGeneratedTests { @Test @@ -200,11 +208,3 @@ struct QuickDiscouragedPendingTestRuleGeneratedTests { verifyRule(QuickDiscouragedPendingTestRule.description) } } - -@Suite(.rulesRegistered) -struct RawValueForCamelCasedCodableEnumRuleGeneratedTests { - @Test - func withDefaultConfiguration() { - verifyRule(RawValueForCamelCasedCodableEnumRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_08.swift b/Tests/GeneratedTests/GeneratedTests_08.swift index 9a9e613da4e..06f02dc630d 100644 --- a/Tests/GeneratedTests/GeneratedTests_08.swift +++ b/Tests/GeneratedTests/GeneratedTests_08.swift @@ -9,6 +9,14 @@ import Testing @testable import SwiftLintBuiltInRules @testable import SwiftLintCore +@Suite(.rulesRegistered) +struct RawValueForCamelCasedCodableEnumRuleGeneratedTests { + @Test + func withDefaultConfiguration() { + verifyRule(RawValueForCamelCasedCodableEnumRule.description) + } +} + @Suite(.rulesRegistered) struct ReduceBooleanRuleGeneratedTests { @Test @@ -200,11 +208,3 @@ struct SortedFirstLastRuleGeneratedTests { verifyRule(SortedFirstLastRule.description) } } - -@Suite(.rulesRegistered) -struct SortedImportsRuleGeneratedTests { - @Test - func withDefaultConfiguration() { - verifyRule(SortedImportsRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_09.swift b/Tests/GeneratedTests/GeneratedTests_09.swift index 63724b01916..f891cb0f102 100644 --- a/Tests/GeneratedTests/GeneratedTests_09.swift +++ b/Tests/GeneratedTests/GeneratedTests_09.swift @@ -9,6 +9,14 @@ import Testing @testable import SwiftLintBuiltInRules @testable import SwiftLintCore +@Suite(.rulesRegistered) +struct SortedImportsRuleGeneratedTests { + @Test + func withDefaultConfiguration() { + verifyRule(SortedImportsRule.description) + } +} + @Suite(.rulesRegistered) struct StatementPositionRuleGeneratedTests { @Test @@ -200,11 +208,3 @@ struct UnhandledThrowingTaskRuleGeneratedTests { verifyRule(UnhandledThrowingTaskRule.description) } } - -@Suite(.rulesRegistered) -struct UnneededBreakInSwitchRuleGeneratedTests { - @Test - func withDefaultConfiguration() { - verifyRule(UnneededBreakInSwitchRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_10.swift b/Tests/GeneratedTests/GeneratedTests_10.swift index 6e8193c3998..6b51b6d07cf 100644 --- a/Tests/GeneratedTests/GeneratedTests_10.swift +++ b/Tests/GeneratedTests/GeneratedTests_10.swift @@ -9,6 +9,14 @@ import Testing @testable import SwiftLintBuiltInRules @testable import SwiftLintCore +@Suite(.rulesRegistered) +struct UnneededBreakInSwitchRuleGeneratedTests { + @Test + func withDefaultConfiguration() { + verifyRule(UnneededBreakInSwitchRule.description) + } +} + @Suite(.rulesRegistered) struct UnneededEscapingRuleGeneratedTests { @Test @@ -200,11 +208,3 @@ struct VoidFunctionInTernaryConditionRuleGeneratedTests { verifyRule(VoidFunctionInTernaryConditionRule.description) } } - -@Suite(.rulesRegistered) -struct VoidReturnRuleGeneratedTests { - @Test - func withDefaultConfiguration() { - verifyRule(VoidReturnRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_11.swift b/Tests/GeneratedTests/GeneratedTests_11.swift index 61a9b2b37e1..8d40b52c40d 100644 --- a/Tests/GeneratedTests/GeneratedTests_11.swift +++ b/Tests/GeneratedTests/GeneratedTests_11.swift @@ -9,6 +9,14 @@ import Testing @testable import SwiftLintBuiltInRules @testable import SwiftLintCore +@Suite(.rulesRegistered) +struct VoidReturnRuleGeneratedTests { + @Test + func withDefaultConfiguration() { + verifyRule(VoidReturnRule.description) + } +} + @Suite(.rulesRegistered) struct WeakDelegateRuleGeneratedTests { @Test diff --git a/Tests/IntegrationTests/Resources/default_rule_configurations.yml b/Tests/IntegrationTests/Resources/default_rule_configurations.yml index cc2f59c8b26..875c1600ab2 100644 --- a/Tests/IntegrationTests/Resources/default_rule_configurations.yml +++ b/Tests/IntegrationTests/Resources/default_rule_configurations.yml @@ -529,6 +529,11 @@ implicitly_unwrapped_optional: meta: opt-in: true correctable: false +imports_at_top: + severity: warning + meta: + opt-in: true + correctable: false inclusive_language: severity: warning meta: