Skip to content

Commit

Permalink
Add TestCaseAccessibilityRule (realm#3376)
Browse files Browse the repository at this point in the history
Co-authored-by: JP Simard <jp@jpsim.com>
  • Loading branch information
keith and jpsim authored Oct 12, 2020
1 parent da408b5 commit 5d6e25a
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 8 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions Source/SwiftLintFramework/Helpers/XCTestHelpers.swift
Original file line number Diff line number Diff line change
@@ -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))
}
}
1 change: 1 addition & 0 deletions Source/SwiftLintFramework/Models/MasterRuleList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ public let masterRuleList = RuleList(rules: [
SwitchCaseAlignmentRule.self,
SwitchCaseOnNewlineRule.self,
SyntacticSugarRule.self,
TestCaseAccessibilityRule.self,
TodoRule.self,
ToggleBoolRule.self,
TrailingClosureRule.self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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()"
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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 {}
}
""")
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ public struct OverridenSuperCallConfiguration: RuleConfiguration, Equatable {
"viewWillDisappear(_:)",
//XCTestCase
"setUp()",
"tearDown()"
"setUpWithError()",
"tearDown()",
"tearDownWithError()"
]

var severityConfiguration = SeverityConfiguration(.warning)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
public struct TestCaseAccessibilityConfiguration: RuleConfiguration, Equatable {
public private(set) var severityConfiguration = SeverityConfiguration(.warning)
public private(set) var allowedPrefixes: Set<String> = []

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
}
}
16 changes: 16 additions & 0 deletions SwiftLint.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -771,10 +775,14 @@
BC8757392195CDD500CA7A74 /* ModifierOrderRuleExamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierOrderRuleExamples.swift; sourceTree = "<group>"; };
BCB68282216213130078E4C3 /* CompilerProtocolInitRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompilerProtocolInitRuleTests.swift; sourceTree = "<group>"; };
BF48D2D61CBCCA5F0080BDAE /* TrailingWhitespaceConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrailingWhitespaceConfiguration.swift; sourceTree = "<group>"; };
C225E844252E8B1B00EDE3E7 /* TestCaseAccessibilityRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCaseAccessibilityRule.swift; sourceTree = "<group>"; };
C225E845252E8B1E00EDE3E7 /* TestCaseAccessibilityRuleExamples.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCaseAccessibilityRuleExamples.swift; sourceTree = "<group>"; };
C225E848252E8B2F00EDE3E7 /* XCTestHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestHelpers.swift; sourceTree = "<group>"; };
C25EBBDD210787B200E27603 /* PrefixedTopLevelConstantRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefixedTopLevelConstantRuleTests.swift; sourceTree = "<group>"; };
C25EBBE021078D5B00E27603 /* GlobTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobTests.swift; sourceTree = "<group>"; };
C25EBBE321078DC700E27603 /* Glob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glob.swift; sourceTree = "<group>"; };
C26330352073DAA200D7B4FD /* LowerACLThanParentRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowerACLThanParentRule.swift; sourceTree = "<group>"; };
C26522A1252E93AC00BF9C0C /* TestCaseAccessibilityConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCaseAccessibilityConfiguration.swift; sourceTree = "<group>"; };
C28B2B3B2106DF210009A0FE /* PrefixedConstantRuleConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefixedConstantRuleConfiguration.swift; sourceTree = "<group>"; };
C2A8D075243C0D0300642BC9 /* IBInspectableInExtensionRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IBInspectableInExtensionRule.swift; sourceTree = "<group>"; };
C2B3C15F2106F78100088928 /* ConfigurationAliasesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationAliasesTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand All @@ -1146,6 +1155,7 @@
C25EBBE321078DC700E27603 /* Glob.swift */,
D4AB0EA11F8993DD00CEC380 /* NamespaceCollector.swift */,
4DCB8E7D1CBE43640070FCF0 /* RegexHelpers.swift */,
C225E848252E8B2F00EDE3E7 /* XCTestHelpers.swift */,
);
path = Helpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
7 changes: 7 additions & 0 deletions Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,12 @@ class SyntacticSugarRuleTests: XCTestCase {
}
}

class TestCaseAccessibilityRuleTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(TestCaseAccessibilityRule.description)
}
}

class ToggleBoolRuleTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(ToggleBoolRule.description)
Expand Down

0 comments on commit 5d6e25a

Please sign in to comment.