Skip to content

Commit 67a54b6

Browse files
LaurenWhiteallevato
authored andcommitted
Implement never use implicitly unwrapped optionals (swiftlang#58)
1 parent 301f11a commit 67a54b6

File tree

4 files changed

+124
-2
lines changed

4 files changed

+124
-2
lines changed

Diff for: tools/swift-format/Sources/Core/Context.swift

+8
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,19 @@ public class Context {
1515

1616
/// The URL of the file being linted or formatted.
1717
public let fileURL: URL
18+
19+
/// Indicates whether the file imports XCTest, and is test code
20+
public var importsXCTest: Bool
21+
22+
/// Indicates whether the visitor has already determined a value for importsXCTest
23+
public var didSetImportsXCTest: Bool
1824

1925
/// Creates a new Context with the provided configuration, diagnostic engine, and file URL.
2026
public init(configuration: Configuration, diagnosticEngine: DiagnosticEngine?, fileURL: URL) {
2127
self.configuration = configuration
2228
self.diagnosticEngine = diagnosticEngine
2329
self.fileURL = fileURL
30+
self.importsXCTest = false
31+
self.didSetImportsXCTest = false
2432
}
2533
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Core
2+
import Foundation
3+
import SwiftSyntax
4+
5+
/// Visitor that determines if the target source file imports XCTest
6+
private final class ImportsXCTestVisitor: SyntaxVisitor {
7+
let context: Context
8+
9+
init(context: Context) {
10+
self.context = context
11+
}
12+
13+
override func visit(_ node: SourceFileSyntax) {
14+
for statement in node.statements {
15+
guard let importDecl = statement.item as? ImportDeclSyntax else { continue }
16+
for component in importDecl.path {
17+
guard component.name.text == "XCTest" else { continue }
18+
context.importsXCTest = true
19+
context.didSetImportsXCTest = true
20+
return
21+
}
22+
}
23+
context.didSetImportsXCTest = true
24+
}
25+
}
26+
27+
/// Sets the appropriate value of the importsXCTest field in the Context class, which
28+
/// indicates whether the file contains test code or not.
29+
///
30+
/// This setter will only run the visitor if another rule hasn't already called this function to
31+
/// determine if the source file imports XCTest.
32+
///
33+
/// - Parameters:
34+
/// - context: The context information of the target source file.
35+
/// - sourceFile: The file to be visited.
36+
func setImportsXCTest(context: Context, sourceFile: SourceFileSyntax) {
37+
guard !context.didSetImportsXCTest else { return }
38+
ImportsXCTestVisitor(context: context).visit(sourceFile)
39+
}

Diff for: tools/swift-format/Sources/Rules/NeverUseImplicitlyUnwrappedOptionals.swift

+34-2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,45 @@ import SwiftSyntax
66
///
77
/// Certain properties (e.g. `@IBOutlet`) tied to the UI lifecycle are ignored.
88
///
9-
/// This rule does not apply to test code, defined as code which matches one or more of:
10-
/// * Parent directory named "Tests"
9+
/// This rule does not apply to test code, defined as code which:
1110
/// * Contains the line `import XCTest`
1211
///
12+
/// TODO: Create exceptions for other UI elements (ex: viewDidLoad)
13+
///
1314
/// Lint: Declaring a property with an implicitly unwrapped type yields a lint error.
1415
///
1516
/// - SeeAlso: https://google.github.io/swift#implicitly-unwrapped-optionals
1617
public final class NeverUseImplicitlyUnwrappedOptionals: SyntaxLintRule {
18+
19+
// Checks if "XCTest" is an import statement
20+
public override func visit(_ node: SourceFileSyntax) {
21+
setImportsXCTest(context: context, sourceFile: node)
22+
super.visit(node)
23+
}
24+
25+
public override func visit(_ node: VariableDeclSyntax) {
26+
guard !context.importsXCTest else { return }
27+
// Ignores IBOutlet variables
28+
if let attributes = node.attributes {
29+
for attribute in attributes {
30+
if attribute.attributeName.text == "IBOutlet" { return }
31+
}
32+
}
33+
// Finds type annotation for variable(s)
34+
for binding in node.bindings {
35+
guard let nodeTypeAnnotation = binding.typeAnnotation else { continue }
36+
diagnoseImplicitWrapViolation(nodeTypeAnnotation.type)
37+
}
38+
}
39+
40+
func diagnoseImplicitWrapViolation(_ type: TypeSyntax) {
41+
guard let violation = type as? ImplicitlyUnwrappedOptionalTypeSyntax else { return }
42+
diagnose(.doNotUseImplicitUnwrapping(identifier: "\(violation.wrappedType)"), on: type)
43+
}
44+
}
1745

46+
extension Diagnostic.Message {
47+
static func doNotUseImplicitUnwrapping(identifier: String) -> Diagnostic.Message {
48+
return .init(.warning, "use \(identifier) or \(identifier)? instead of \(identifier)!")
49+
}
1850
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Foundation
2+
import SwiftSyntax
3+
import XCTest
4+
5+
@testable import Rules
6+
7+
public class NeverUseImplicitlyUnwrappedOptionalsTests: DiagnosingTestCase {
8+
public func testInvalidVariableUnwrapping() {
9+
let input =
10+
"""
11+
import Core
12+
import Foundation
13+
import SwiftSyntax
14+
15+
var foo: Int?
16+
var s: String!
17+
var c, d, e: Float
18+
@IBOutlet var button: UIButton!
19+
"""
20+
performLint(NeverUseImplicitlyUnwrappedOptionals.self, input: input)
21+
XCTAssertNotDiagnosed(.doNotUseImplicitUnwrapping(identifier: "Int"))
22+
XCTAssertDiagnosed(.doNotUseImplicitUnwrapping(identifier: "String"))
23+
XCTAssertNotDiagnosed(.doNotUseImplicitUnwrapping(identifier: "Float"))
24+
XCTAssertNotDiagnosed(.doNotUseImplicitUnwrapping(identifier: "UIButton"))
25+
}
26+
public func testIgnoreTestCode() {
27+
let input =
28+
"""
29+
import XCTest
30+
31+
var s: String!
32+
"""
33+
performLint(NeverUseImplicitlyUnwrappedOptionals.self, input: input)
34+
XCTAssertNotDiagnosed(.doNotUseImplicitUnwrapping(identifier: "String"))
35+
}
36+
37+
#if !os(macOS)
38+
static let allTests = [
39+
NeverUseImplicitlyUnwrappedOptionalsTests.testInvalidVariableUnwrapping,
40+
]
41+
#endif
42+
43+
}

0 commit comments

Comments
 (0)