diff --git a/CHANGELOG.md b/CHANGELOG.md index 6daad7d50..01b6a70ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,12 @@ [Zsolt Kovács](https://github.com/lordzsolt) [#3103](https://github.com/realm/SwiftLint/issues/3103) +* Add `prefer_zero_over_explicit_init` opt-in rule to enforce using + `.zero` instead of calling constructor with zero arguments + (e.g. `CGPoint(x: 0, y: 0)`) when using CoreGraphics types. + [Anton Nazarov](https://github.com/MortyMerr) + [#3190](https://github.com/realm/SwiftLint/issues/3190) + #### Bug Fixes * Fix UnusedImportRule breaking transitive imports. diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift index 09134193f..7e2ea598d 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -124,6 +124,7 @@ public let masterRuleList = RuleList(rules: [ OverrideInExtensionRule.self, PatternMatchingKeywordsRule.self, PreferSelfTypeOverTypeOfSelfRule.self, + PreferZeroOverExplicitInitRule.self, PrefixedTopLevelConstantRule.self, PrivateActionRule.self, PrivateOutletRule.self, diff --git a/Source/SwiftLintFramework/Rules/Idiomatic/PreferZeroOverExplicitInitRule.swift b/Source/SwiftLintFramework/Rules/Idiomatic/PreferZeroOverExplicitInitRule.swift new file mode 100644 index 000000000..e930b18fd --- /dev/null +++ b/Source/SwiftLintFramework/Rules/Idiomatic/PreferZeroOverExplicitInitRule.swift @@ -0,0 +1,71 @@ +import Foundation +import SourceKittenFramework + +public struct PreferZeroOverExplicitInitRule: OptInRule, ConfigurationProviderRule, SubstitutionCorrectableRule, + AutomaticTestableRule { + public var configuration = SeverityConfiguration(.warning) + private var pattern: String { + let zero = "\\s*:\\s*0(.0*)?\\s*" + let type = "(\(["CGPoint", "CGSize", "CGVector", "CGRect"].joined(separator: "|")))" + let firstArg = "(\(["x", "dx", "width"].joined(separator: "|")))" + let secondArg = "(\(["y", "dy", "height"].joined(separator: "|")))" + let thirdAndFourthArg = "(\\,\\s*width\(zero)\\,\\s*height\(zero))?" + return "\(type)\\(\\s*\(firstArg)\(zero)\\,\\s*\(secondArg)\(zero)\(thirdAndFourthArg)\\)" + } + + public static let description = RuleDescription( + identifier: "prefer_zero_over_explicit_init", + name: "Prefer Zero Over Explicit Init", + description: "Prefer `.zero` over explicit init with zero parameters (e.g. `CGPoint(x: 0, y: 0)`)", + kind: .idiomatic, + nonTriggeringExamples: [ + Example("CGRect(x: 0, y: 0, width: 0, height: 1)"), + Example("CGPoint(x: 0, y: -1"), + Example("CGSize(width: 2, height: 4)"), + Example("CGVector(dx: -5, dy: 0)") + ], + triggeringExamples: [ + Example("↓CGPoint(x: 0, y: 0)"), + Example("↓CGPoint(x: 0.000000, y: 0)"), + Example("↓CGPoint(x: 0.000000, y: 0.000)"), + Example("↓CGRect(x: 0, y: 0, width: 0, height: 0)"), + Example("↓CGSize(width: 0, height: 0)"), + Example("↓CGVector(dx: 0, dy: 0)") + ], + corrections: [ + Example("↓CGPoint(x: 0, y: 0)"): Example("CGPoint.zero"), + Example("↓CGRect(x: 0, y: 0, width: 0, height: 0)"): Example("CGRect.zero"), + Example("↓CGSize(width: 0, height: 0.000)"): Example("CGSize.zero"), + Example("↓CGVector(dx: 0, dy: 0)"): Example("CGVector.zero") + ] + ) + + public init() {} + + public func validate(file: SwiftLintFile) -> [StyleViolation] { + violationRanges(in: file).map { + StyleViolation( + ruleDescription: Self.description, + severity: configuration.severity, + location: Location(file: file, characterOffset: $0.location) + ) + } + } + + public func violationRanges(in file: SwiftLintFile) -> [NSRange] { + return file.matchesAndSyntaxKinds(matching: pattern) + .filter { + $0.1 == [.identifier, .identifier, .number, .identifier, .number] || + $0.1 == [ + .identifier, .identifier, .number, .identifier, .number, .identifier, .number, .identifier, .number + ] + } + .map { $0.0.range(at: 0) } + } + + public func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? { + let declaration = file.stringView.substring(with: violationRange) + guard let typeEndIndex = declaration.firstIndex(of: "(") else { return nil } + return (violationRange, "\(declaration.prefix(upTo: typeEndIndex)).zero") + } +} diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index e611ff400..68a817cea 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -164,6 +164,7 @@ 77DFF0E923442DE30041EEB4 /* RawValueForCamelCasedCodableEnumRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7723A4DE23442D7100F38590 /* RawValueForCamelCasedCodableEnumRule.swift */; }; 787CDE39208E7D41005F3D2F /* SwitchCaseAlignmentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787CDE38208E7D41005F3D2F /* SwitchCaseAlignmentConfiguration.swift */; }; 787CDE3B208F9C34005F3D2F /* SwitchCaseAlignmentRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787CDE3A208F9C34005F3D2F /* SwitchCaseAlignmentRuleTests.swift */; }; + 78B9051D24564AF600C81B0A /* PreferZeroOverExplicitInitRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B9051A245646CD00C81B0A /* PreferZeroOverExplicitInitRule.swift */; }; 78F032461D7C877E00BE709A /* OverriddenSuperCallRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F032441D7C877800BE709A /* OverriddenSuperCallRule.swift */; }; 78F032481D7D614300BE709A /* OverridenSuperCallConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F032471D7D614300BE709A /* OverridenSuperCallConfiguration.swift */; }; 7C0C2E7A1D2866CB0076435A /* ExplicitInitRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C0C2E791D2866CB0076435A /* ExplicitInitRule.swift */; }; @@ -683,6 +684,7 @@ 7723A4DE23442D7100F38590 /* RawValueForCamelCasedCodableEnumRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawValueForCamelCasedCodableEnumRule.swift; sourceTree = ""; }; 787CDE38208E7D41005F3D2F /* SwitchCaseAlignmentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchCaseAlignmentConfiguration.swift; sourceTree = ""; }; 787CDE3A208F9C34005F3D2F /* SwitchCaseAlignmentRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchCaseAlignmentRuleTests.swift; sourceTree = ""; }; + 78B9051A245646CD00C81B0A /* PreferZeroOverExplicitInitRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferZeroOverExplicitInitRule.swift; sourceTree = ""; }; 78F032441D7C877800BE709A /* OverriddenSuperCallRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverriddenSuperCallRule.swift; sourceTree = ""; }; 78F032471D7D614300BE709A /* OverridenSuperCallConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverridenSuperCallConfiguration.swift; sourceTree = ""; }; 7C0C2E791D2866CB0076435A /* ExplicitInitRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExplicitInitRule.swift; sourceTree = ""; }; @@ -1381,6 +1383,7 @@ 827169B41F48D712003FB9AF /* NoGroupingExtensionRule.swift */, D4B022971E102EE8007E5297 /* ObjectLiteralRule.swift */, D403A4A21F4DB5510020CA02 /* PatternMatchingKeywordsRule.swift */, + 78B9051A245646CD00C81B0A /* PreferZeroOverExplicitInitRule.swift */, 1E3C2D701EE36C6F00C8386D /* PrivateOverFilePrivateRule.swift */, 24B4DF0B1D6DFA370097803B /* RedundantNilCoalescingRule.swift */, 18B90B6A21ADD99800B60749 /* RedundantObjcAttributeRule.swift */, @@ -2356,6 +2359,7 @@ 6C15818D237026AC00F582A2 /* GitHubActionsLoggingReporter.swift in Sources */, 62FE5D32200CABDD00F68793 /* DiscouragedOptionalCollectionExamples.swift in Sources */, D41C09BD23C1B99D00F105C4 /* OrphanedDocCommentRule.swift in Sources */, + 78B9051D24564AF600C81B0A /* PreferZeroOverExplicitInitRule.swift in Sources */, D49896F12026B36C00814A83 /* RedundantSetAccessControlRule.swift in Sources */, E4A6CF752363CBFB00DD5B18 /* RandomAccessCollection+Swiftlint.swift in Sources */, 29FFC37A1F15764D007E4825 /* FileLengthRuleConfiguration.swift in Sources */, diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 545dfdcb9..cffe2ca50 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1094,6 +1094,12 @@ extension PreferSelfTypeOverTypeOfSelfRuleTests { ] } +extension PreferZeroOverExplicitInitRuleTests { + static var allTests: [(String, (PreferZeroOverExplicitInitRuleTests) -> () throws -> Void)] = [ + ("testWithDefaultConfiguration", testWithDefaultConfiguration) + ] +} + extension PrefixedTopLevelConstantRuleTests { static var allTests: [(String, (PrefixedTopLevelConstantRuleTests) -> () throws -> Void)] = [ ("testDefaultConfiguration", testDefaultConfiguration), @@ -1826,6 +1832,7 @@ XCTMain([ testCase(OverrideInExtensionRuleTests.allTests), testCase(PatternMatchingKeywordsRuleTests.allTests), testCase(PreferSelfTypeOverTypeOfSelfRuleTests.allTests), + testCase(PreferZeroOverExplicitInitRuleTests.allTests), testCase(PrefixedTopLevelConstantRuleTests.allTests), testCase(PrivateActionRuleTests.allTests), testCase(PrivateOutletRuleTests.allTests), diff --git a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift index 198c53ae7..88f853d78 100644 --- a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift +++ b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift @@ -522,6 +522,12 @@ class PreferSelfTypeOverTypeOfSelfRuleTests: XCTestCase { } } +class PreferZeroOverExplicitInitRuleTests: XCTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferZeroOverExplicitInitRule.description) + } +} + class PrivateActionRuleTests: XCTestCase { func testWithDefaultConfiguration() { verifyRule(PrivateActionRule.description)