Files
SwiftLint/Source/SwiftLintFramework/Rules/Idiomatic/RedundantOptionalInitializationRule.swift
Zev Eisenberg fcf848608e Add Inline test failure messages (#3040)
* Add Example wrapper in order to display test failures inline when running in Xcode.
* Stop using Swift 5.1-only features so we can compile on Xcode 10.2.
* Wrap strings in Example.
* Add Changelog entry.
* Wrap all examples in Example struct.
* Better and more complete capturing of line numbers.
* Fix broken test.
* Better test traceability.
* Address or disable linting warnings.
* Add documentation comments.
* Disable linter for a few cases.
* Limit mutability and add copy-and-mutate utility functions.
* Limit scope of mutability.
2020-02-02 10:35:37 +02:00

172 lines
6.0 KiB
Swift

import Foundation
import SourceKittenFramework
public struct RedundantOptionalInitializationRule: SubstitutionCorrectableASTRule, ConfigurationProviderRule,
AutomaticTestableRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "redundant_optional_initialization",
name: "Redundant Optional Initialization",
description: "Initializing an optional variable with nil is redundant.",
kind: .idiomatic,
nonTriggeringExamples: [
Example("var myVar: Int?\n"),
Example("let myVar: Int? = nil\n"),
Example("var myVar: Int? = 0\n"),
Example("func foo(bar: Int? = 0) { }\n"),
Example("var myVar: Optional<Int>\n"),
Example("let myVar: Optional<Int> = nil\n"),
Example("var myVar: Optional<Int> = 0\n"),
// properties with body should be ignored
Example("""
var foo: Int? {
if bar != nil { }
return 0
}
"""),
// properties with a closure call
Example("""
var foo: Int? = {
if bar != nil { }
return 0
}()
"""),
// lazy variables need to be initialized
Example("lazy var test: Int? = nil"),
// local variables
Example("""
func funcName() {
var myVar: String?
}
"""),
Example("""
func funcName() {
let myVar: String? = nil
}
""")
],
triggeringExamples: triggeringExamples,
corrections: corrections
)
private static let triggeringExamples: [Example] = {
let commonExamples = [
Example("var myVar: Int?↓ = nil\n"),
Example("var myVar: Optional<Int>↓ = nil\n"),
Example("var myVar: Int?↓=nil\n"),
Example("var myVar: Optional<Int>↓=nil\n)")
]
guard SwiftVersion.current >= .fourDotOne else {
return commonExamples
}
return commonExamples + [
Example("""
func funcName() {
var myVar: String?↓ = nil
}
""")
]
}()
private static let corrections: [Example: Example] = {
var corrections = [
Example("var myVar: Int?↓ = nil\n"): Example("var myVar: Int?\n"),
Example("var myVar: Optional<Int>↓ = nil\n"): Example("var myVar: Optional<Int>\n"),
Example("var myVar: Int?↓=nil\n"): Example("var myVar: Int?\n"),
Example("var myVar: Optional<Int>↓=nil\n"): Example("var myVar: Optional<Int>\n"),
Example("class C {\n#if true\nvar myVar: Int?↓ = nil\n#endif\n}"):
Example("class C {\n#if true\nvar myVar: Int?\n#endif\n}")
]
guard SwiftVersion.current >= .fourDotOne else {
return corrections
}
corrections[Example("""
func foo() {
var myVar: String?↓ = nil
}
""")] = Example("""
func foo() {
var myVar: String?
}
""")
return corrections
}()
private let pattern = "\\s*=\\s*nil\\b"
public func validate(file: SwiftLintFile, kind: SwiftDeclarationKind,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
return violationRanges(in: file, kind: kind, dictionary: dictionary).map {
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, characterOffset: $0.location))
}
}
public func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? {
return (violationRange, "")
}
public func violationRanges(in file: SwiftLintFile, kind: SwiftDeclarationKind,
dictionary: SourceKittenDictionary) -> [NSRange] {
guard SwiftDeclarationKind.variableKinds.contains(kind),
let type = dictionary.typeName,
typeIsOptional(type),
!dictionary.enclosedSwiftAttributes.contains(.lazy),
dictionary.isMutableVariable(file: file),
let range = range(for: dictionary, file: file),
let match = file.match(pattern: pattern, with: [.keyword], range: range).first,
match.location == range.location + range.length - match.length else {
return []
}
return [match]
}
private func range(for dictionary: SourceKittenDictionary, file: SwiftLintFile) -> NSRange? {
guard let offset = dictionary.offset,
let length = dictionary.length else {
return nil
}
let contents = file.stringView
if let bodyOffset = dictionary.bodyOffset {
return contents.byteRangeToNSRange(ByteRange(location: offset, length: bodyOffset - offset))
} else {
return contents.byteRangeToNSRange(ByteRange(location: offset, length: length))
}
}
private func typeIsOptional(_ type: String) -> Bool {
return type.hasSuffix("?") || type.hasPrefix("Optional<")
}
}
extension SourceKittenDictionary {
fileprivate func isMutableVariable(file: SwiftLintFile) -> Bool {
return setterAccessibility != nil || (isLocal && isVariable(file: file))
}
private var isLocal: Bool {
return accessibility == nil && setterAccessibility == nil
}
private func isVariable(file: SwiftLintFile) -> Bool {
guard let byteRange = byteRange,
case let contents = file.stringView,
let range = contents.byteRangeToNSRange(byteRange),
!file.match(pattern: "\\Avar\\b", with: [.keyword], range: range).isEmpty else {
return false
}
return true
}
}