Skip to content

Commit

Permalink
Add rule to prevent use of JSONDecoder.decode(_:from:) (#4)
Browse files Browse the repository at this point in the history
We recently added some error logging inside `Decodable.decode(_:)` which
has already proven very useful. We want to enforce its usage everywhere
so we can get as much coverage as possible. on our error logging. To
that end, we need to warn against using `JSONDecoder.decode(_:from:)`.
  • Loading branch information
Killectro authored Apr 9, 2024
1 parent 6700615 commit 12255d6
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 3 deletions.
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ disabled_rules:
- type_contents_order
- unused_capture_list
- vertical_whitespace_between_cases
- json_decoding

attributes:
always_on_line_above:
Expand Down
1 change: 1 addition & 0 deletions Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public let builtInRules: [any Rule.Type] = [
InertDeferRule.self,
InvalidSwiftLintCommandRule.self,
IsDisjointRule.self,
JSONDecodingRule.self,
JoinedDefaultParameterRule.self,
LargeTupleRule.self,
LastWhereRule.self,
Expand Down
76 changes: 76 additions & 0 deletions Source/SwiftLintBuiltInRules/Rules/Whatnot/JSONDecodingRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import SwiftLintCore
import SwiftSyntax

@SwiftSyntaxRule(foldExpressions: true)
struct JSONDecodingRule: Rule {
var configuration = SeverityConfiguration<Self>(.warning)

static let description = RuleDescription(
identifier: "json_decoding",
name: "JSON Decoding",
description: "Don't use JSONDecoder.decode directly",
kind: .lint,
nonTriggeringExamples: [
Example("""
T.decode(data)
"""),
Example("""
let decoder = JSONDecoder()
MyType.decode(data, using: decoder)
"""),
Example("""
MyType.decode(data, using: JSONDecoder().snakeCase())
"""),
Example("""
let container = try decoder.container(keyedBy: CodingKeys.self)
let myType = try container.decode(MyType.self, forKey: .myType)
"""),
Example("""
let container = try decoder.singleValueContainer().decode(MyType.self)
""")
],
triggeringExamples: [
Example("""
JSONDecoder().↓decode(MyType.self, from: data)
"""),
Example("""
let decoder = JSONDecoder()
decoder.↓decode(MyType.self, from: data)
"""),
Example("""
JSONDecoder().snakeCase().↓decode(Self.self, from: data)
""")
]
)
}

private extension JSONDecodingRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override func visitPost(_ node: FunctionCallExprSyntax) {
guard let member = node.calledExpression.as(MemberAccessExprSyntax.self),
member.declName.baseName.text == "decode",
let argument = node.arguments.first?.expression.as(MemberAccessExprSyntax.self),
let modelName = argument.base?.description,
argument.declName.baseName.text == "self",
let fromArgument = node.arguments.first(where: { $0.label?.text == "from" })
else { return }

violations.append(reason(
modelName: modelName,
dataName: fromArgument.expression.description,
position: member.declName.positionAfterSkippingLeadingTrivia
))
}

func reason(modelName: String, dataName: String, position: AbsolutePosition) -> ReasonedRuleViolation {
.init(
position: position,
reason: """
Use `\(modelName).decode(\(dataName))` instead. It will automatically trigger \
error logs if the decoding fails
""",
severity: .warning
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ private extension TextConcatenationRule {

if isTextInit {
return funcCall
} else {
return recursivelySearchForTextInitializerCall(funcCall.calledExpression)
}
} else if let memberAccess = node.as(MemberAccessExprSyntax.self), let base = memberAccess.base {
return recursivelySearchForTextInitializerCall(funcCall.calledExpression)
}
if let memberAccess = node.as(MemberAccessExprSyntax.self), let base = memberAccess.base {
return recursivelySearchForTextInitializerCall(base)
}

Expand Down
6 changes: 6 additions & 0 deletions Tests/GeneratedTests/GeneratedTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,12 @@ class IsDisjointRuleGeneratedTests: SwiftLintTestCase {
}
}

class JSONDecodingRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(JSONDecodingRule.description)
}
}

class JoinedDefaultParameterRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(JoinedDefaultParameterRule.description)
Expand Down

0 comments on commit 12255d6

Please sign in to comment.