Skip to content

Commit 2b91a5a

Browse files
Add detection of leading-dot to optional_data_string_conversion rule (#6372)
Co-authored-by: Danny Mösch <danny.moesch@icloud.com>
1 parent 63e0372 commit 2b91a5a

4 files changed

Lines changed: 97 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040
[theamodhshetty](https://github.com/theamodhshetty)
4141
[#5741](https://github.com/realm/SwiftLint/issues/5741)
4242

43+
* Add detection of cases such as `String.init(decoding: data, as: UTF8.self)` and
44+
`let text: String = .init(decoding: data, as: UTF8.self)` to
45+
`optional_data_string_conversion` rule.
46+
[nadeemnali](https://github.com/nadeemnali)
47+
[#6359](https://github.com/realm/SwiftLint/issues/6359)
48+
4349
### Bug Fixes
4450

4551
* Add an `ignore_attributes` option to `implicit_optional_initialization` so
@@ -72,7 +78,7 @@
7278
`redundant_self` rule.
7379
[SimplyDanny](https://github.com/SimplyDanny)
7480
[#6553](https://github.com/realm/SwiftLint/issues/6553)
75-
81+
7682
* Respect existing environment variables when setting `BUILD_WORKSPACE_DIRECTORY`
7783
in build tool plugins.
7884
[SimplyDanny](https://github.com/SimplyDanny)

Source/SwiftLintBuiltInRules/Rules/Lint/OptionalDataStringConversionRule.swift

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import Foundation
12
import SwiftSyntax
23

34
@SwiftSyntaxRule
45
struct OptionalDataStringConversionRule: Rule {
5-
var configuration = SeverityConfiguration<Self>(.warning)
6+
var configuration = OptionalDataStringConversionConfiguration()
67

78
static let description = RuleDescription(
89
identifier: "optional_data_string_conversion",
@@ -15,23 +16,90 @@ struct OptionalDataStringConversionRule: Rule {
1516
Example("String(UTF8.self)"),
1617
Example("String(a, b, c, UTF8.self)"),
1718
Example("String(decoding: data, encoding: UTF8.self)"),
19+
// Additional non-triggering examples to maximize coverage:
20+
Example("String(data: data, encoding: .ascii)"),
21+
Example("String(bytes: data, encoding: .utf16LittleEndian)"),
22+
Example("String(decoding: data, as: UTF16.self)"),
23+
Example("String.init(bytes: data, encoding: .utf8)"),
24+
Example("let text: String = .init(bytes: data, encoding: .utf8)"),
25+
Example("let text: String = .init(data)"),
26+
Example("let text: Int = .init(decoding: data, as: UTF8.self)"),
27+
Example("let n: Int = .init(0)"),
28+
Example("String(repeating: \"a\", count: 3)"),
29+
Example("String(format: \"%d\", 3)"),
30+
// Default behavior (allow_implicit_init == false): implicit leading-dot init without type
31+
Example("let text = .init(decoding: data, as: UTF8.self)"),
1832
],
1933
triggeringExamples: [
20-
Example("String(decoding: data, as: UTF8.self)")
34+
Example("↓String(decoding: data, as: UTF8.self)"),
35+
Example("↓String.init(decoding: data, as: UTF8.self)"),
36+
Example("let text: String = ↓.init(decoding: data, as: UTF8.self)"),
37+
// With allow_implicit_init enabled, implicit leading-dot init also triggers
38+
Example(
39+
"let text = ↓.init(decoding: data, as: UTF8.self)",
40+
configuration: ["allow_implicit_init": true]
41+
),
42+
Example(
43+
"f(↓.init(decoding: data, as: UTF8.self))",
44+
configuration: ["allow_implicit_init": true]
45+
),
2146
]
2247
)
2348
}
2449

2550
private extension OptionalDataStringConversionRule {
2651
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
27-
override func visitPost(_ node: DeclReferenceExprSyntax) {
28-
if node.baseName.text == "String",
29-
let parent = node.parent?.as(FunctionCallExprSyntax.self),
30-
parent.arguments.map(\.label?.text) == ["decoding", "as"],
31-
let expr = parent.arguments.last?.expression.as(MemberAccessExprSyntax.self),
32-
expr.base?.description == "UTF8",
33-
expr.declName.baseName.description == "self" {
34-
violations.append(node.positionAfterSkippingLeadingTrivia)
52+
override func visitPost(_ node: FunctionCallExprSyntax) {
53+
// Only consider calls with labels `decoding` and `as`
54+
guard node.arguments.map(\.label?.text) == ["decoding", "as"] else {
55+
return
56+
}
57+
// Check that the `as:` argument is `UTF8.self`
58+
guard let lastExpr = node.arguments.last?.expression.as(MemberAccessExprSyntax.self),
59+
lastExpr.base?.description == "UTF8",
60+
lastExpr.declName.baseName.description == "self" else {
61+
return
62+
}
63+
64+
// Called expression can be:
65+
// 1) DeclReferenceExprSyntax("String") -> String(decoding:as:)
66+
// 2) MemberAccessExprSyntax(base: DeclReferenceExprSyntax("String"), declName: "init") -> String.init(...)
67+
// 3) MemberAccessExprSyntax(base: nil, declName: "init") -> .init(...) (leading-dot)
68+
let called = node.calledExpression
69+
70+
// Case 1: direct `String(...)`
71+
if let declRef = called.as(DeclReferenceExprSyntax.self), declRef.baseName.text == "String" {
72+
violations.append(called.positionAfterSkippingLeadingTrivia)
73+
return
74+
}
75+
76+
// Case 2 and 3: `.init` or `String.init`
77+
guard let member = called.as(MemberAccessExprSyntax.self), member.declName.baseName.text == "init" else {
78+
return
79+
}
80+
81+
// Case 2: `String.init(...)`
82+
if let baseDecl = member.base?.as(DeclReferenceExprSyntax.self), baseDecl.baseName.text == "String" {
83+
violations.append(called.positionAfterSkippingLeadingTrivia)
84+
return
85+
}
86+
87+
// Case 3: leading-dot `.init(...)`
88+
// This is ambiguous in general. If configuration.allowImplicitInit is true,
89+
// we trigger everywhere. Otherwise, we only trigger if the call is used to
90+
// initialize a variable that has an explicit `String` type annotation:
91+
// let x: String = .init(...)
92+
guard member.base == nil else { return }
93+
94+
if configuration.allowImplicitInit {
95+
violations.append(called.positionAfterSkippingLeadingTrivia)
96+
return
97+
}
98+
99+
// Check if the binding has an explicit `String` type annotation
100+
if let binding = node.parent?.parent?.as(PatternBindingSyntax.self),
101+
binding.typeAnnotation?.type.description.trimmingCharacters(in: .whitespacesAndNewlines) == "String" {
102+
violations.append(called.positionAfterSkippingLeadingTrivia)
35103
}
36104
}
37105
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import SwiftLintCore
2+
3+
@AutoConfigParser
4+
struct OptionalDataStringConversionConfiguration: SeverityBasedRuleConfiguration { // swiftlint:disable:this type_name
5+
@ConfigurationElement(key: "severity")
6+
private(set) var severityConfiguration = SeverityConfiguration<Parent>.warning
7+
8+
// When true, also flag leading-dot `.init(decoding:as:)` without explicit `String` type annotation.
9+
@ConfigurationElement(key: "allow_implicit_init")
10+
private(set) var allowImplicitInit = false
11+
}

Tests/IntegrationTests/Resources/default_rule_configurations.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,7 @@ operator_usage_whitespace:
844844
correctable: true
845845
optional_data_string_conversion:
846846
severity: warning
847+
allow_implicit_init: false
847848
meta:
848849
opt-in: false
849850
correctable: false

0 commit comments

Comments
 (0)