diff --git a/CHANGELOG.md b/CHANGELOG.md index 3149c5480e..31cd83515b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,9 @@ #### Enhancements -* None. +* Add `test_case_accessibility` rule. + [Keith Smiley](https://github.com/keith) + [#3376](https://github.com/realm/SwiftLint/issues/3376) #### Bug Fixes diff --git a/Source/SwiftLintFramework/Helpers/XCTestHelpers.swift b/Source/SwiftLintFramework/Helpers/XCTestHelpers.swift new file mode 100644 index 0000000000..09d05844fd --- /dev/null +++ b/Source/SwiftLintFramework/Helpers/XCTestHelpers.swift @@ -0,0 +1,14 @@ +import SourceKittenFramework + +private let testVariableNames: Set = [ + "allTests" +] + +enum XCTestHelpers { + static func isXCTestMember(kind: SwiftDeclarationKind, name: String, + attributes: [SwiftDeclarationAttributeKind]) -> Bool { + return attributes.contains(.override) + || (kind == .functionMethodInstance && name.hasPrefix("test")) + || ([.varStatic, .varClass].contains(kind) && testVariableNames.contains(name)) + } +} diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift index 4448524327..059c091daa 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -162,6 +162,7 @@ public let masterRuleList = RuleList(rules: [ SwitchCaseAlignmentRule.self, SwitchCaseOnNewlineRule.self, SyntacticSugarRule.self, + TestCaseAccessibilityRule.self, TodoRule.self, ToggleBoolRule.self, TrailingClosureRule.self, diff --git a/Source/SwiftLintFramework/Rules/Lint/EmptyXCTestMethodRule.swift b/Source/SwiftLintFramework/Rules/Lint/EmptyXCTestMethodRule.swift index 155970e28b..d5a775a423 100644 --- a/Source/SwiftLintFramework/Rules/Lint/EmptyXCTestMethodRule.swift +++ b/Source/SwiftLintFramework/Rules/Lint/EmptyXCTestMethodRule.swift @@ -33,8 +33,9 @@ public struct EmptyXCTestMethodRule: Rule, OptInRule, ConfigurationProviderRule, return dictionary.substructure.compactMap { subDictionary -> StyleViolation? in guard let kind = subDictionary.declarationKind, - SwiftDeclarationKind.functionKinds.contains(kind), - let name = subDictionary.name, isXCTestMethod(name), + let name = subDictionary.name, + XCTestHelpers.isXCTestMember(kind: kind, name: name, + attributes: subDictionary.enclosedSwiftAttributes), let offset = subDictionary.offset, subDictionary.enclosedVarParameters.isEmpty, subDictionary.substructure.isEmpty else { return nil } @@ -44,8 +45,4 @@ public struct EmptyXCTestMethodRule: Rule, OptInRule, ConfigurationProviderRule, location: Location(file: file, byteOffset: offset)) } } - - private func isXCTestMethod(_ method: String) -> Bool { - return method.hasPrefix("test") || method == "setUp()" || method == "tearDown()" - } } diff --git a/Source/SwiftLintFramework/Rules/Lint/TestCaseAccessibilityRule.swift b/Source/SwiftLintFramework/Rules/Lint/TestCaseAccessibilityRule.swift new file mode 100644 index 0000000000..a223ee86f7 --- /dev/null +++ b/Source/SwiftLintFramework/Rules/Lint/TestCaseAccessibilityRule.swift @@ -0,0 +1,52 @@ +import SourceKittenFramework + +public struct TestCaseAccessibilityRule: Rule, OptInRule, ConfigurationProviderRule, AutomaticTestableRule { + public var configuration = TestCaseAccessibilityConfiguration() + + public init() {} + + public static let description = RuleDescription( + identifier: "test_case_accessibility", + name: "Test case accessibility", + description: "Test cases should only contain private non-test members.", + kind: .lint, + nonTriggeringExamples: TestCaseAccessibilityRuleExamples.nonTriggeringExamples, + triggeringExamples: TestCaseAccessibilityRuleExamples.triggeringExamples + ) + + public func validate(file: SwiftLintFile) -> [StyleViolation] { + return testClasses(in: file).flatMap { violations(in: file, for: $0) } + } + + // MARK: - Private + + private func testClasses(in file: SwiftLintFile) -> [SourceKittenDictionary] { + let dict = file.structureDictionary + return dict.substructure.filter { dictionary in + dictionary.declarationKind == .class && dictionary.inheritedTypes.contains("XCTestCase") + } + } + + private func violations(in file: SwiftLintFile, + for dictionary: SourceKittenDictionary) -> [StyleViolation] { + return dictionary.substructure.compactMap { subDictionary -> StyleViolation? in + guard + let kind = subDictionary.declarationKind, + kind != .varLocal, + let name = subDictionary.name, + !isXCTestMember(kind: kind, name: name, attributes: subDictionary.enclosedSwiftAttributes), + let offset = subDictionary.offset, + subDictionary.accessibility?.isPrivate != true else { return nil } + + return StyleViolation(ruleDescription: Self.description, + severity: configuration.severity, + location: Location(file: file, byteOffset: offset)) + } + } + + private func isXCTestMember(kind: SwiftDeclarationKind, name: String, + attributes: [SwiftDeclarationAttributeKind]) -> Bool { + return XCTestHelpers.isXCTestMember(kind: kind, name: name, attributes: attributes) + || configuration.allowedPrefixes.contains(where: name.hasPrefix) + } +} diff --git a/Source/SwiftLintFramework/Rules/Lint/TestCaseAccessibilityRuleExamples.swift b/Source/SwiftLintFramework/Rules/Lint/TestCaseAccessibilityRuleExamples.swift new file mode 100644 index 0000000000..c870dbf94c --- /dev/null +++ b/Source/SwiftLintFramework/Rules/Lint/TestCaseAccessibilityRuleExamples.swift @@ -0,0 +1,89 @@ +internal struct TestCaseAccessibilityRuleExamples { + static let nonTriggeringExamples = [ + // Valid XCTestCase class + + Example(""" + let foo: String? + + class FooTests: XCTestCase { + static let allTests: [String] = [] + + private let foo: String { + let nestedMember = "hi" + return nestedMember + } + + override static func setUp() { + super.setUp() + } + + override func setUp() { + super.setUp() + } + + override func setUpWithError() throws { + try super.setUpWithError() + } + + override static func tearDown() { + super.tearDown() + } + + override func tearDown() { + super.tearDown() + } + + override func tearDownWithError() { + try super.tearDownWithError() + } + + override func someFutureXCTestFunction() { + super.someFutureXCTestFunction() + } + + func testFoo() { + XCTAssertTrue(true) + } + } + """), + + // Not an XCTestCase class + + Example(""" + class Foobar { + func setUp() {} + + func tearDown() {} + + func testFoo() {} + } + """) + ] + + static let triggeringExamples = [ + Example(""" + class FooTests: XCTestCase { + ↓var foo: String? + ↓let bar: String? + + ↓static func foo() {} + + ↓func setUp(withParam: String) {} + + ↓func foobar() {} + + ↓func not_testBar() {} + + ↓enum Nested {} + + ↓static func testFoo() {} + + ↓static func allTests() {} + } + + final class BarTests: XCTestCase { + ↓class Nested {} + } + """) + ] +} diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/OverridenSuperCallConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/OverridenSuperCallConfiguration.swift index fb0e992b28..ac5aa40588 100644 --- a/Source/SwiftLintFramework/Rules/RuleConfigurations/OverridenSuperCallConfiguration.swift +++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/OverridenSuperCallConfiguration.swift @@ -29,7 +29,9 @@ public struct OverridenSuperCallConfiguration: RuleConfiguration, Equatable { "viewWillDisappear(_:)", //XCTestCase "setUp()", - "tearDown()" + "setUpWithError()", + "tearDown()", + "tearDownWithError()" ] var severityConfiguration = SeverityConfiguration(.warning) diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/TestCaseAccessibilityConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/TestCaseAccessibilityConfiguration.swift new file mode 100644 index 0000000000..c4bf77fbf2 --- /dev/null +++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/TestCaseAccessibilityConfiguration.swift @@ -0,0 +1,27 @@ +public struct TestCaseAccessibilityConfiguration: RuleConfiguration, Equatable { + public private(set) var severityConfiguration = SeverityConfiguration(.warning) + public private(set) var allowedPrefixes: Set = [] + + public var consoleDescription: String { + return severityConfiguration.consoleDescription + + ", allowed_prefixes: [\(allowedPrefixes)]" + } + + public mutating func apply(configuration: Any) throws { + guard let configuration = configuration as? [String: Any] else { + throw ConfigurationError.unknownConfiguration + } + + if let severityString = configuration["severity"] as? String { + try severityConfiguration.apply(configuration: severityString) + } + + if let allowedPrefixes = configuration["allowed_prefixes"] as? [String] { + self.allowedPrefixes = Set(allowedPrefixes) + } + } + + public var severity: ViolationSeverity { + return severityConfiguration.severity + } +} diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index 980fd1979b..dcf2a9f92e 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -246,10 +246,14 @@ BC87573B2195CF2A00CA7A74 /* ModifierOrderRuleExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC8757392195CDD500CA7A74 /* ModifierOrderRuleExamples.swift */; }; BCB68283216213130078E4C3 /* CompilerProtocolInitRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB68282216213130078E4C3 /* CompilerProtocolInitRuleTests.swift */; }; BFF028AE1CBCF8A500B38A9D /* TrailingWhitespaceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF48D2D61CBCCA5F0080BDAE /* TrailingWhitespaceConfiguration.swift */; }; + C225E846252E8B2200EDE3E7 /* TestCaseAccessibilityRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = C225E844252E8B1B00EDE3E7 /* TestCaseAccessibilityRule.swift */; }; + C225E847252E8B2200EDE3E7 /* TestCaseAccessibilityRuleExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = C225E845252E8B1E00EDE3E7 /* TestCaseAccessibilityRuleExamples.swift */; }; + C225E849252E8B3B00EDE3E7 /* XCTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C225E848252E8B2F00EDE3E7 /* XCTestHelpers.swift */; }; C25EBBDF2107884200E27603 /* PrefixedTopLevelConstantRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C25EBBDD210787B200E27603 /* PrefixedTopLevelConstantRuleTests.swift */; }; C25EBBE221078D5F00E27603 /* GlobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C25EBBE021078D5B00E27603 /* GlobTests.swift */; }; C25EBBE521078DCE00E27603 /* Glob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C25EBBE321078DC700E27603 /* Glob.swift */; }; C26330382073DAC500D7B4FD /* LowerACLThanParentRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26330352073DAA200D7B4FD /* LowerACLThanParentRule.swift */; }; + C26522A2252E93AC00BF9C0C /* TestCaseAccessibilityConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26522A1252E93AC00BF9C0C /* TestCaseAccessibilityConfiguration.swift */; }; C28B2B3D2106DF730009A0FE /* PrefixedConstantRuleConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28B2B3B2106DF210009A0FE /* PrefixedConstantRuleConfiguration.swift */; }; C2A8D076243C0D0300642BC9 /* IBInspectableInExtensionRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A8D075243C0D0300642BC9 /* IBInspectableInExtensionRule.swift */; }; C2B3C1612106F78C00088928 /* ConfigurationAliasesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B3C15F2106F78100088928 /* ConfigurationAliasesTests.swift */; }; @@ -771,10 +775,14 @@ BC8757392195CDD500CA7A74 /* ModifierOrderRuleExamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierOrderRuleExamples.swift; sourceTree = ""; }; BCB68282216213130078E4C3 /* CompilerProtocolInitRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompilerProtocolInitRuleTests.swift; sourceTree = ""; }; BF48D2D61CBCCA5F0080BDAE /* TrailingWhitespaceConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrailingWhitespaceConfiguration.swift; sourceTree = ""; }; + C225E844252E8B1B00EDE3E7 /* TestCaseAccessibilityRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCaseAccessibilityRule.swift; sourceTree = ""; }; + C225E845252E8B1E00EDE3E7 /* TestCaseAccessibilityRuleExamples.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCaseAccessibilityRuleExamples.swift; sourceTree = ""; }; + C225E848252E8B2F00EDE3E7 /* XCTestHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestHelpers.swift; sourceTree = ""; }; C25EBBDD210787B200E27603 /* PrefixedTopLevelConstantRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefixedTopLevelConstantRuleTests.swift; sourceTree = ""; }; C25EBBE021078D5B00E27603 /* GlobTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobTests.swift; sourceTree = ""; }; C25EBBE321078DC700E27603 /* Glob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glob.swift; sourceTree = ""; }; C26330352073DAA200D7B4FD /* LowerACLThanParentRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowerACLThanParentRule.swift; sourceTree = ""; }; + C26522A1252E93AC00BF9C0C /* TestCaseAccessibilityConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCaseAccessibilityConfiguration.swift; sourceTree = ""; }; C28B2B3B2106DF210009A0FE /* PrefixedConstantRuleConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefixedConstantRuleConfiguration.swift; sourceTree = ""; }; C2A8D075243C0D0300642BC9 /* IBInspectableInExtensionRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IBInspectableInExtensionRule.swift; sourceTree = ""; }; C2B3C15F2106F78100088928 /* ConfigurationAliasesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationAliasesTests.swift; sourceTree = ""; }; @@ -1128,6 +1136,7 @@ 3BCC04CF1C4F56D3006073C3 /* SeverityLevelsConfiguration.swift */, 725094881D0855760039B353 /* StatementModeConfiguration.swift */, 787CDE38208E7D41005F3D2F /* SwitchCaseAlignmentConfiguration.swift */, + C26522A1252E93AC00BF9C0C /* TestCaseAccessibilityConfiguration.swift */, D450D1DA21F1992E00E60010 /* TrailingClosureConfiguration.swift */, D40F83871DE9179200524C62 /* TrailingCommaConfiguration.swift */, BF48D2D61CBCCA5F0080BDAE /* TrailingWhitespaceConfiguration.swift */, @@ -1146,6 +1155,7 @@ C25EBBE321078DC700E27603 /* Glob.swift */, D4AB0EA11F8993DD00CEC380 /* NamespaceCollector.swift */, 4DCB8E7D1CBE43640070FCF0 /* RegexHelpers.swift */, + C225E848252E8B2F00EDE3E7 /* XCTestHelpers.swift */, ); path = Helpers; sourceTree = ""; @@ -1243,6 +1253,8 @@ B89F3BC91FD5ED9000931E59 /* RequiredEnumCaseRule.swift */, D450D1D021EC4A6900E60010 /* StrongIBOutletRule.swift */, D40E041B1F46E3B30043BC4E /* SuperfluousDisableCommandRule.swift */, + C225E844252E8B1B00EDE3E7 /* TestCaseAccessibilityRule.swift */, + C225E845252E8B1E00EDE3E7 /* TestCaseAccessibilityRuleExamples.swift */, E88DEA811B0990A700A66CB0 /* TodoRule.swift */, D47421F324E14760009AE788 /* UnneededNotificationCenterRemovalRule.swift */, 7565E5F02262BA0900B0597C /* UnusedCaptureListRule.swift */, @@ -2080,6 +2092,7 @@ D4F10614229A2F5E00FDE319 /* NoFallthroughOnlyRuleExamples.swift in Sources */, 3ABE19CF20B7CE32009C2EC2 /* MultilineFunctionChainsRule.swift in Sources */, 82FE253F20F604AD00295958 /* VerticalWhitespaceOpeningBracesRule.swift in Sources */, + C26522A2252E93AC00BF9C0C /* TestCaseAccessibilityConfiguration.swift in Sources */, 827169B51F48D712003FB9AF /* NoGroupingExtensionRule.swift in Sources */, D41B57781ED8CEE0007B0470 /* ExtensionAccessModifierRule.swift in Sources */, E881985C1BEA978500333A11 /* TrailingNewlineRule.swift in Sources */, @@ -2137,6 +2150,7 @@ 623675B01F960C5C009BE6F3 /* QuickDiscouragedPendingTestRule.swift in Sources */, 8F4E30D52519092800EED8CB /* UnusedDeclarationRuleExamples.swift in Sources */, 287F8B642230843000BDC504 /* NSLocalizedStringRequireBundleRule.swift in Sources */, + C225E847252E8B2200EDE3E7 /* TestCaseAccessibilityRuleExamples.swift in Sources */, D47079AD1DFE2FA700027086 /* EmptyParametersRule.swift in Sources */, E87E4A091BFB9CAE00FCFE46 /* SyntaxKind+SwiftLint.swift in Sources */, 3B0B14541C505D6300BE82F7 /* SeverityConfiguration.swift in Sources */, @@ -2191,6 +2205,7 @@ E889D8C71F1D357B00058332 /* Configuration+Merging.swift in Sources */, D44254271DB9C15C00492EA4 /* SyntacticSugarRule.swift in Sources */, D4EA77C81F817FD200C315FB /* UnneededBreakInSwitchRule.swift in Sources */, + C225E846252E8B2200EDE3E7 /* TestCaseAccessibilityRule.swift in Sources */, D4D383852145F550000235BD /* StaticOperatorRule.swift in Sources */, CC6D285B2292F0600052B682 /* IndentationWidthConfiguration.swift in Sources */, 006204DC1E1E492F00FFFBE1 /* VerticalWhitespaceConfiguration.swift in Sources */, @@ -2409,6 +2424,7 @@ A1A6F3F21EE319ED00A9F9E2 /* ObjectLiteralConfiguration.swift in Sources */, D489B548231233A40090BAA0 /* ContainsOverFilterCountRule.swift in Sources */, D4B0228E1E0CC608007E5297 /* ClassDelegateProtocolRule.swift in Sources */, + C225E849252E8B3B00EDE3E7 /* XCTestHelpers.swift in Sources */, D4369C572430830000505BB9 /* ComputedAccessorsOrderRuleExamples.swift in Sources */, E881985F1BEA987C00333A11 /* TypeNameRule.swift in Sources */, D40AD08A1E032F9700F48C30 /* UnusedClosureParameterRule.swift in Sources */, diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 0342d2e533..e69a8a4491 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1450,6 +1450,12 @@ extension SyntacticSugarRuleTests { ] } +extension TestCaseAccessibilityRuleTests { + static var allTests: [(String, (TestCaseAccessibilityRuleTests) -> () throws -> Void)] = [ + ("testWithDefaultConfiguration", testWithDefaultConfiguration) + ] +} + extension TodoRuleTests { static var allTests: [(String, (TodoRuleTests) -> () throws -> Void)] = [ ("testTodo", testTodo), @@ -1900,6 +1906,7 @@ XCTMain([ testCase(SwitchCaseAlignmentRuleTests.allTests), testCase(SwitchCaseOnNewlineRuleTests.allTests), testCase(SyntacticSugarRuleTests.allTests), + testCase(TestCaseAccessibilityRuleTests.allTests), testCase(TodoRuleTests.allTests), testCase(ToggleBoolRuleTests.allTests), testCase(TrailingClosureConfigurationTests.allTests), diff --git a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift index e8c848e3e3..63c9a66add 100644 --- a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift +++ b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift @@ -708,6 +708,12 @@ class SyntacticSugarRuleTests: XCTestCase { } } +class TestCaseAccessibilityRuleTests: XCTestCase { + func testWithDefaultConfiguration() { + verifyRule(TestCaseAccessibilityRule.description) + } +} + class ToggleBoolRuleTests: XCTestCase { func testWithDefaultConfiguration() { verifyRule(ToggleBoolRule.description)