11import SwiftSyntax
22
3- @SwiftSyntaxRule
3+ @SwiftSyntaxRule ( explicitRewriter : true )
44struct InvisibleCharacterRule : Rule {
55 var configuration = SeverityConfiguration < Self > ( . error)
66
7+ // swiftlint:disable invisible_character
78 static let description = RuleDescription (
89 identifier: " invisible_character " ,
910 name: " Invisible Character " ,
@@ -30,7 +31,6 @@ struct InvisibleCharacterRule: Rule {
3031 Example ( #"let unicode = "Hello 👋 World""# ) ,
3132 ] ,
3233 triggeringExamples: [
33- // swiftlint:disable invisible_character
3434 Example ( #"let s = "Hello↓World" // U+200B zero-width space"# ) ,
3535 Example ( #"let s = "Hello↓World" // U+200C zero-width non-joiner"# ) ,
3636 Example ( #"let s = "Hello↓World" // U+FEFF formatting character"# ) ,
@@ -44,51 +44,118 @@ struct InvisibleCharacterRule: Rule {
4444 Example ( #"let s = "Test↓String↓Here" // Multiple invisible characters"# ) ,
4545 Example ( #"let s = "Hel↓lo" + "World" // string concatenation with U+200C"# ) ,
4646 Example ( #"let s = "Hel↓lo \(name)" // U+200C in interpolated string"# ) ,
47- // swiftlint:enable invisible_character
47+ ] ,
48+ corrections: [
49+ Example ( #"let s = "HelloWorld""# ) : Example ( #"let s = "HelloWorld""# ) ,
50+ Example ( #"let s = "HelloWorld""# ) : Example ( #"let s = "HelloWorld""# ) ,
51+ Example ( #"let s = "HelloWorld""# ) : Example ( #"let s = "HelloWorld""# ) ,
52+ Example ( #"let url = "https://example.com""# ) : Example ( #"let url = "https://example.com""# ) ,
53+ Example ( """
54+ let multiline = \" \" \"
55+ HelloWorld
56+ \" \" \"
57+ """ ) : Example ( """
58+ let multiline = \" \" \"
59+ HelloWorld
60+ \" \" \"
61+ """ ) ,
62+ Example ( #"let s = "TestStringHere""# ) : Example ( #"let s = "TestStringHere""# ) ,
63+ Example ( #"let s = "Hello" + "World""# ) : Example ( #"let s = "Hello" + "World""# ) ,
64+ Example ( #"let s = "Hello \(name)""# ) : Example ( #"let s = "Hello \(name)""# ) ,
4865 ]
4966 )
67+ // swiftlint:enable invisible_character
5068
5169 private static let invisibleCharacters : [ Unicode . Scalar : String ] = [
5270 " \u{200B} " : " U+200B (zero-width space) " ,
5371 " \u{200C} " : " U+200C (zero-width non-joiner) " ,
5472 " \u{FEFF} " : " U+FEFF (zero-width no-break space) " ,
5573 ]
74+
75+ private static let invisibleCharacterSet : Set < Unicode . Scalar > = Set ( invisibleCharacters. keys)
5676}
5777
5878private extension InvisibleCharacterRule {
79+ static func violatingSegments(
80+ in node: StringLiteralExprSyntax
81+ ) -> [ SyntaxIdentifier : ( segment: StringSegmentSyntax , text: String ) ] {
82+ var result = [ SyntaxIdentifier: ( StringSegmentSyntax, String) ] ( )
83+ for segment in node. segments {
84+ guard let stringSegment = segment. as ( StringSegmentSyntax . self) else {
85+ continue
86+ }
87+ let text = stringSegment. content. text
88+ if text. unicodeScalars. contains ( where: { invisibleCharacterSet. contains ( $0) } ) {
89+ result [ stringSegment. id] = ( stringSegment, text)
90+ }
91+ }
92+ return result
93+ }
94+
5995 final class Visitor : ViolationsSyntaxVisitor < ConfigurationType > {
6096 override func visitPost( _ node: StringLiteralExprSyntax ) {
61- let invisibleCharactersKeys = InvisibleCharacterRule . invisibleCharacters. keys
62-
63- for segment in node. segments {
64- guard let stringSegment = segment. as ( StringSegmentSyntax . self) else {
65- continue
97+ for (segment, text) in violatingSegments ( in: node) . values {
98+ var utf8Offset = 0
99+ for scalar in text. unicodeScalars {
100+ defer { utf8Offset += scalar. utf8Length }
101+ guard let characterName = invisibleCharacters [ scalar] else {
102+ continue
103+ }
104+ violations. append (
105+ ReasonedRuleViolation (
106+ position: segment. content. positionAfterSkippingLeadingTrivia
107+ . advanced ( by: utf8Offset) ,
108+ reason: " String literal should not contain invisible character \( characterName) "
109+ )
110+ )
66111 }
112+ }
113+ }
114+ }
67115
68- let text = stringSegment. content. text
116+ final class Rewriter : ViolationsSyntaxRewriter < ConfigurationType > {
117+ override func visit( _ node: StringLiteralExprSyntax ) -> ExprSyntax {
118+ guard !isDisabled( atStartPositionOf: node) else {
119+ return super. visit ( node)
120+ }
69121
70- // Early exit if no invisible characters present
71- guard text. unicodeScalars. contains ( where: {
72- invisibleCharactersKeys. contains ( $0)
73- } ) else {
74- continue
122+ let segmentsToFix = violatingSegments ( in: node)
123+ guard segmentsToFix. isNotEmpty else {
124+ return super. visit ( node)
125+ }
126+
127+ var correctionCount = 0
128+
129+ let newSegments = node. segments. map { segment -> StringLiteralSegmentListSyntax . Element in
130+ guard let stringSegment = segment. as ( StringSegmentSyntax . self) ,
131+ let ( _, text) = segmentsToFix [ stringSegment. id] else {
132+ return segment
75133 }
76134
77- // Find all invisible characters and their positions
78- var utf8Offset = 0
135+ var cleanedScalars = [ Unicode . Scalar] ( )
136+ cleanedScalars. reserveCapacity ( text. unicodeScalars. count)
137+ var removedCount = 0
138+
79139 for scalar in text. unicodeScalars {
80- if let character = InvisibleCharacterRule . invisibleCharacters [ scalar] {
81- violations. append (
82- ReasonedRuleViolation (
83- position: stringSegment. content. positionAfterSkippingLeadingTrivia
84- . advanced ( by: utf8Offset) ,
85- reason: " String literal should not contain invisible character \( character) "
86- )
87- )
140+ if invisibleCharacterSet. contains ( scalar) {
141+ removedCount += 1
142+ } else {
143+ cleanedScalars. append ( scalar)
88144 }
89- utf8Offset += String ( scalar) . utf8. count
90145 }
146+
147+ correctionCount += removedCount
148+ let cleanedText = String ( String . UnicodeScalarView ( cleanedScalars) )
149+ let newSegment = stringSegment. with (
150+ \. content,
151+ stringSegment. content. with ( \. tokenKind, . stringSegment( cleanedText) )
152+ )
153+ return . stringSegment( newSegment)
91154 }
155+
156+ numberOfCorrections += correctionCount
157+ let newNode = node. with ( \. segments, StringLiteralSegmentListSyntax ( newSegments) )
158+ return super. visit ( newNode)
92159 }
93160 }
94161}
0 commit comments