1+ import Foundation
12import SwiftSyntax
23
34@SwiftSyntaxRule
45struct 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
2550private 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 }
0 commit comments