Skip to content

Commit

Permalink
Merge pull request realm#2567 from realm/mf-unused-newValue
Browse files Browse the repository at this point in the history
Add unused_setter_argument rule
  • Loading branch information
marcelofabri authored Jan 18, 2019
2 parents 90232d5 + 8f93b21 commit e347582
Show file tree
Hide file tree
Showing 7 changed files with 368 additions and 0 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
[Samuel Susla](https://github.com/sammy-sc)
[#1881](https://github.com/realm/SwiftLint/issues/1881)

* Add `unused_setter_value` rule to validate that setter arguments are
used in properties.
[Marcelo Fabri](https://github.com/marcelofabri)
[#1136](https://github.com/realm/SwiftLint/issues/1136)

#### Bug Fixes

* Fix false positives on `identical_operands` rule when the right side of the
Expand Down
111 changes: 111 additions & 0 deletions Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
* [Unused Import](#unused-import)
* [Unused Optional Binding](#unused-optional-binding)
* [Unused Private Declaration](#unused-private-declaration)
* [Unused Setter Value](#unused-setter-value)
* [Valid IBInspectable](#valid-ibinspectable)
* [Vertical Parameter Alignment](#vertical-parameter-alignment)
* [Vertical Parameter Alignment On Call](#vertical-parameter-alignment-on-call)
Expand Down Expand Up @@ -22252,6 +22253,116 @@ private let ↓kConstant = 0



## Unused Setter Value

Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version
--- | --- | --- | --- | --- | ---
`unused_setter_value` | Enabled | No | lint | No | 3.0.0

Setter value is not used.

### Examples

<details>
<summary>Non Triggering Examples</summary>

```swift
var aValue: String {
get {
return Persister.shared.aValue
}
set {
Persister.shared.aValue = newValue
}
}
```

```swift
var aValue: String {
set {
Persister.shared.aValue = newValue
}
get {
return Persister.shared.aValue
}
}
```

```swift
var aValue: String {
get {
return Persister.shared.aValue
}
set(value) {
Persister.shared.aValue = value
}
}
```

</details>
<details>
<summary>Triggering Examples</summary>

```swift
var aValue: String {
get {
return Persister.shared.aValue
}
↓set {
Persister.shared.aValue = aValue
}
}
```

```swift
var aValue: String {
↓set {
Persister.shared.aValue = aValue
}
get {
return Persister.shared.aValue
}
}
```

```swift
var aValue: String {
get {
return Persister.shared.aValue
}
↓set {
Persister.shared.aValue = aValue
}
}
```

```swift
var aValue: String {
get {
let newValue = Persister.shared.aValue
return newValue
}
↓set {
Persister.shared.aValue = aValue
}
}
```

```swift
var aValue: String {
get {
return Persister.shared.aValue
}
↓set(value) {
Persister.shared.aValue = aValue
}
}
```

</details>



## Valid IBInspectable

Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version
Expand Down
1 change: 1 addition & 0 deletions Source/SwiftLintFramework/Models/MasterRuleList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ public let masterRuleList = RuleList(rules: [
UnusedImportRule.self,
UnusedOptionalBindingRule.self,
UnusedPrivateDeclarationRule.self,
UnusedSetterValueRule.self,
ValidIBInspectableRule.self,
VerticalParameterAlignmentOnCallRule.self,
VerticalParameterAlignmentRule.self,
Expand Down
234 changes: 234 additions & 0 deletions Source/SwiftLintFramework/Rules/Lint/UnusedSetterValueRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import Foundation
import SourceKittenFramework

public struct UnusedSetterValueRule: ConfigurationProviderRule, AutomaticTestableRule {
public var configuration = SeverityConfiguration(.warning)

public init() {}

public static let description = RuleDescription(
identifier: "unused_setter_value",
name: "Unused Setter Value",
description: "Setter value is not used.",
kind: .lint,
nonTriggeringExamples: [
"""
var aValue: String {
get {
return Persister.shared.aValue
}
set {
Persister.shared.aValue = newValue
}
}
""",
"""
var aValue: String {
set {
Persister.shared.aValue = newValue
}
get {
return Persister.shared.aValue
}
}
""",
"""
var aValue: String {
get {
return Persister.shared.aValue
}
set(value) {
Persister.shared.aValue = value
}
}
"""
],
triggeringExamples: [
"""
var aValue: String {
get {
return Persister.shared.aValue
}
↓set {
Persister.shared.aValue = aValue
}
}
""",
"""
var aValue: String {
↓set {
Persister.shared.aValue = aValue
}
get {
return Persister.shared.aValue
}
}
""",
"""
var aValue: String {
get {
return Persister.shared.aValue
}
↓set {
Persister.shared.aValue = aValue
}
}
""",
"""
var aValue: String {
get {
let newValue = Persister.shared.aValue
return newValue
}
↓set {
Persister.shared.aValue = aValue
}
}
""",
"""
var aValue: String {
get {
return Persister.shared.aValue
}
↓set(value) {
Persister.shared.aValue = aValue
}
}
"""
]
)

public func validate(file: File) -> [StyleViolation] {
let setTokens = file.rangesAndTokens(matching: "\\bset\\b").keywordTokens()

let violatingLocations = setTokens.compactMap { setToken -> Int? in
// the last element is the deepest structure
guard let dict = declarations(forByteOffset: setToken.offset, structure: file.structure).last,
let bodyOffset = dict.bodyOffset, let bodyLength = dict.bodyLength,
case let contents = file.contents.bridge(),
let propertyRange = contents.byteRangeToNSRange(start: bodyOffset, length: bodyLength),
let getToken = findGetToken(in: propertyRange, file: file, propertyStructure: dict) else {
return nil
}

let argument = findNamedArgument(after: setToken, file: file)

let propertyEndOffset = bodyOffset + bodyLength
let setterByteRange: NSRange
if setToken.offset > getToken.offset { // get {} set {}
let startOfBody: Int
if let argumentToken = argument?.token {
startOfBody = argumentToken.offset + argumentToken.length
} else {
startOfBody = setToken.offset
}
setterByteRange = NSRange(location: startOfBody,
length: propertyEndOffset - startOfBody)
} else { // set {} get {}
let startOfBody: Int
if let argumentToken = argument?.token {
startOfBody = argumentToken.offset + argumentToken.length
} else {
startOfBody = setToken.offset
}
setterByteRange = NSRange(location: startOfBody,
length: getToken.offset - startOfBody)
}

guard let setterRange = contents.byteRangeToNSRange(start: setterByteRange.location,
length: setterByteRange.length) else {
return nil
}

let argumentName = argument?.name ?? "newValue"
guard file.match(pattern: "\\b\(argumentName)\\b", with: [.identifier], range: setterRange).isEmpty else {
return nil
}

return setToken.offset
}

return violatingLocations.map { offset in
return StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, byteOffset: offset))
}
}

private func findNamedArgument(after token: SyntaxToken,
file: File) -> (name: String, token: SyntaxToken)? {
guard let firstToken = file.syntaxMap.tokens.first(where: { $0.offset > token.offset }),
SyntaxKind(rawValue: firstToken.type) == .identifier else {
return nil
}

let declaration = file.structure.structures(forByteOffset: firstToken.offset)
.first(where: { $0.offset == firstToken.offset && $0.length == firstToken.length })

guard let name = declaration?.name else {
return nil
}

return (name, firstToken)
}

private func findGetToken(in range: NSRange, file: File,
propertyStructure: [String: SourceKitRepresentable]) -> SyntaxToken? {
let getTokens = file.rangesAndTokens(matching: "\\bget\\b", range: range).keywordTokens()
return getTokens.first(where: { token -> Bool in
// the last element is the deepest structure
guard let dict = declarations(forByteOffset: token.offset, structure: file.structure).last,
propertyStructure.isEqualTo(dict) else {
return false
}

return true
})
}

private func declarations(forByteOffset byteOffset: Int,
structure: Structure) -> [[String: SourceKitRepresentable]] {
var results = [[String: SourceKitRepresentable]]()
let allowedKinds = SwiftDeclarationKind.variableKinds.subtracting([.varParameter])

func parse(dictionary: [String: SourceKitRepresentable], parentKind: SwiftDeclarationKind?) {
// Only accepts declarations which contains a body and contains the
// searched byteOffset
guard let kindString = dictionary.kind,
let kind = SwiftDeclarationKind(rawValue: kindString),
let bodyOffset = dictionary.bodyOffset,
let bodyLength = dictionary.bodyLength,
case let byteRange = NSRange(location: bodyOffset, length: bodyLength),
NSLocationInRange(byteOffset, byteRange) else {
return
}

if parentKind != .protocol && allowedKinds.contains(kind) {
results.append(dictionary)
}

for dictionary in dictionary.substructure {
parse(dictionary: dictionary, parentKind: kind)
}
}

for dictionary in structure.dictionary.substructure {
parse(dictionary: dictionary, parentKind: nil)
}

return results
}
}

private extension Array where Element == (NSRange, [SyntaxToken]) {
func keywordTokens() -> [SyntaxToken] {
return compactMap { _, tokens in
guard let token = tokens.last,
SyntaxKind(rawValue: token.type) == .keyword else {
return nil
}

return token
}
}
}
Loading

0 comments on commit e347582

Please sign in to comment.