Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
[nadeemnali](https://github.com/nadeemnali)
[#6359](https://github.com/realm/SwiftLint/issues/6359)

* Add new default `invisible_character` rule that detects invisible characters
like zero-width space (U+200B), zero-width non-joiner (U+200C),
and FEFF formatting character (U+FEFF) in string literals, which can cause hard-to-debug issues.
[kapitoshka438](https://github.com/kapitoshka438)
[#6045](https://github.com/realm/SwiftLint/issues/6045)

### Bug Fixes

* Add an `ignore_attributes` option to `implicit_optional_initialization` so
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 @@ -96,6 +96,7 @@ public let builtInRules: [any Rule.Type] = [
IncompatibleConcurrencyAnnotationRule.self,
IndentationWidthRule.self,
InvalidSwiftLintCommandRule.self,
InvisibleCharacterRule.self,
IsDisjointRule.self,
JoinedDefaultParameterRule.self,
LargeTupleRule.self,
Expand Down
154 changes: 154 additions & 0 deletions Source/SwiftLintBuiltInRules/Rules/Lint/InvisibleCharacterRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import SwiftSyntax

@SwiftSyntaxRule(correctable: true)
struct InvisibleCharacterRule: Rule {
var configuration = InvisibleCharacterConfiguration()

// swiftlint:disable invisible_character
static let description = RuleDescription(
identifier: "invisible_character",
name: "Invisible Character",
description: """
Disallows invisible characters like zero-width space (U+200B), \
zero-width non-joiner (U+200C), and FEFF formatting character (U+FEFF) \
in string literals as they can cause hard-to-debug issues.
""",
kind: .lint,
nonTriggeringExamples: [
Example(#"let s = "HelloWorld""#),
Example(#"let s = "Hello World""#),
Example(#"let url = "https://example.com/api""#),
Example(##"let s = #"Hello World"#"##),
Example("""
let multiline = \"\"\"
Hello
World
\"\"\"
"""),
Example(#"let empty = """#),
Example(#"let tab = "Hello\tWorld""#),
Example(#"let newline = "Hello\nWorld""#),
Example(#"let unicode = "Hello 👋 World""#),
],
triggeringExamples: [
Comment thread
SimplyDanny marked this conversation as resolved.
Example(#"let s = "Hello↓​World" // U+200B zero-width space"#),
Example(#"let s = "Hello↓‌World" // U+200C zero-width non-joiner"#),
Example(#"let s = "Hello↓World" // U+FEFF formatting character"#),
Example(#"let url = "https://example↓​.com" // U+200B in URL"#),
Example("""
// U+200B in multiline string
let multiline = \"\"\"
Hello↓​World
\"\"\"
"""),
Example(#"let s = "Test↓​String↓Here" // Multiple invisible characters"#),
Example(#"let s = "Hel↓‌lo" + "World" // string concatenation with U+200C"#),
Example(#"let s = "Hel↓‌lo \(name)" // U+200C in interpolated string"#),
Example("""
//
// additional_code_points: ["00AD"]
//
let s = "Hello↓­World"
""",
configuration: [
"additional_code_points": ["00AD"],
]
),
Example("""
//
// additional_code_points: ["200D"]
//
let s = "Hello↓‍World"
""",
configuration: [
"additional_code_points": ["200D"],
]
),
],
corrections: [
Example(#"let s = "Hello​World""#): Example(#"let s = "HelloWorld""#),
Example(#"let s = "Hello‌World""#): Example(#"let s = "HelloWorld""#),
Example(#"let s = "HelloWorld""#): Example(#"let s = "HelloWorld""#),
Example(#"let url = "https://example​.com""#): Example(#"let url = "https://example.com""#),
Example("""
let multiline = \"\"\"
Hello​World
\"\"\"
"""): Example("""
let multiline = \"\"\"
HelloWorld
\"\"\"
"""),
Example(#"let s = "Test​StringHere""#): Example(#"let s = "TestStringHere""#),
Example(#"let s = "Hel‌lo" + "World""#): Example(#"let s = "Hello" + "World""#),
Example(#"let s = "Hel‌lo \(name)""#): Example(#"let s = "Hello \(name)""#),
Example(
#"let s = "Hello­World""#,
configuration: [
"additional_code_points": ["00AD"],
]
): Example(
#"let s = "HelloWorld""#,
configuration: [
"additional_code_points": ["00AD"],
]
),
Example(
#"let s = "Hello‍World""#,
configuration: [
"additional_code_points": ["200D"],
]
): Example(
#"let s = "HelloWorld""#,
configuration: [
"additional_code_points": ["200D"],
]
),
]
)
// swiftlint:enable invisible_character
}

private extension InvisibleCharacterRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override func visitPost(_ node: StringLiteralExprSyntax) {
let violatingCharacters = configuration.violatingCharacters
for segment in node.segments {
guard let stringSegment = segment.as(StringSegmentSyntax.self) else {
continue
}
let text = stringSegment.content.text
let scalars = text.unicodeScalars
guard scalars.contains(where: { violatingCharacters.contains($0) }) else {
continue
}
var utf8Offset = 0

for scalar in scalars {
defer {
utf8Offset += scalar.utf8.count
}
guard violatingCharacters.contains(scalar) else {
continue
}

let characterName = InvisibleCharacterConfiguration.defaultCharacterDescriptions[scalar]
?? scalar.escaped(asASCII: true)

let position = stringSegment.content.positionAfterSkippingLeadingTrivia.advanced(by: utf8Offset)
violations.append(
ReasonedRuleViolation(
position: position,
reason: "String literal should not contain invisible character \(characterName)",
correction: .init(
start: position,
end: position.advanced(by: scalar.utf8.count),
replacement: ""
)
)
)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import SwiftLintCore

@AutoConfigParser
struct InvisibleCharacterConfiguration: SeverityBasedRuleConfiguration {
static let defaultCharacterDescriptions: [UnicodeScalar: String] = [
"\u{200B}": "U+200B (zero-width space)",
"\u{200C}": "U+200C (zero-width non-joiner)",
"\u{FEFF}": "U+FEFF (zero-width no-break space)",
]

@ConfigurationElement(key: "severity")
private(set) var severityConfiguration = SeverityConfiguration<Parent>.error
@ConfigurationElement(
key: "additional_code_points",
postprocessor: {
$0.formUnion(defaultCharacterDescriptions.keys)
}
)
private(set) var violatingCharacters = Set<UnicodeScalar>()
}

extension UnicodeScalar: AcceptableByConfigurationElement {
public init(fromAny value: Any, context ruleID: String) throws(Issue) {
guard let hexCode = value as? String,
let codePoint = UInt32(hexCode, radix: 16),
let scalar = Self(codePoint) else {
throw .invalidConfiguration(
ruleID: ruleID,
message: "\(value) is not a valid Unicode scalar code point."
)
}
self = scalar
}

public func asOption() -> OptionType {
.string(.init(value, radix: 16, uppercase: true))
}
}
12 changes: 6 additions & 6 deletions Tests/GeneratedTests/GeneratedTests_04.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ final class InvalidSwiftLintCommandRuleGeneratedTests: SwiftLintTestCase {
}
}

final class InvisibleCharacterRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(InvisibleCharacterRule.description)
}
}

final class IsDisjointRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(IsDisjointRule.description)
Expand Down Expand Up @@ -150,9 +156,3 @@ final class LeadingWhitespaceRuleGeneratedTests: SwiftLintTestCase {
verifyRule(LeadingWhitespaceRule.description)
}
}

final class LegacyCGGeometryFunctionsRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(LegacyCGGeometryFunctionsRule.description)
}
}
12 changes: 6 additions & 6 deletions Tests/GeneratedTests/GeneratedTests_05.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
@testable import SwiftLintCore
import TestHelpers

final class LegacyCGGeometryFunctionsRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(LegacyCGGeometryFunctionsRule.description)
}
}

final class LegacyConstantRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(LegacyConstantRule.description)
Expand Down Expand Up @@ -150,9 +156,3 @@ final class NSLocalizedStringKeyRuleGeneratedTests: SwiftLintTestCase {
verifyRule(NSLocalizedStringKeyRule.description)
}
}

final class NSLocalizedStringRequireBundleRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(NSLocalizedStringRequireBundleRule.description)
}
}
12 changes: 6 additions & 6 deletions Tests/GeneratedTests/GeneratedTests_06.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
@testable import SwiftLintCore
import TestHelpers

final class NSLocalizedStringRequireBundleRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(NSLocalizedStringRequireBundleRule.description)
}
}

final class NSNumberInitAsFunctionReferenceRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(NSNumberInitAsFunctionReferenceRule.description)
Expand Down Expand Up @@ -150,9 +156,3 @@ final class PatternMatchingKeywordsRuleGeneratedTests: SwiftLintTestCase {
verifyRule(PatternMatchingKeywordsRule.description)
}
}

final class PeriodSpacingRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(PeriodSpacingRule.description)
}
}
12 changes: 6 additions & 6 deletions Tests/GeneratedTests/GeneratedTests_07.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
@testable import SwiftLintCore
import TestHelpers

final class PeriodSpacingRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(PeriodSpacingRule.description)
}
}

final class PreferAssetSymbolsRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(PreferAssetSymbolsRule.description)
Expand Down Expand Up @@ -150,9 +156,3 @@ final class ReduceIntoRuleGeneratedTests: SwiftLintTestCase {
verifyRule(ReduceIntoRule.description)
}
}

final class RedundantDiscardableLetRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(RedundantDiscardableLetRule.description)
}
}
12 changes: 6 additions & 6 deletions Tests/GeneratedTests/GeneratedTests_08.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
@testable import SwiftLintCore
import TestHelpers

final class RedundantDiscardableLetRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(RedundantDiscardableLetRule.description)
}
}

final class RedundantNilCoalescingRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(RedundantNilCoalescingRule.description)
Expand Down Expand Up @@ -150,9 +156,3 @@ final class StaticOverFinalClassRuleGeneratedTests: SwiftLintTestCase {
verifyRule(StaticOverFinalClassRule.description)
}
}

final class StrictFilePrivateRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(StrictFilePrivateRule.description)
}
}
12 changes: 6 additions & 6 deletions Tests/GeneratedTests/GeneratedTests_09.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
@testable import SwiftLintCore
import TestHelpers

final class StrictFilePrivateRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(StrictFilePrivateRule.description)
}
}

final class StrongIBOutletRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(StrongIBOutletRule.description)
Expand Down Expand Up @@ -150,9 +156,3 @@ final class UnneededParenthesesInClosureArgumentRuleGeneratedTests: SwiftLintTes
verifyRule(UnneededParenthesesInClosureArgumentRule.description)
}
}

final class UnneededSynthesizedInitializerRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(UnneededSynthesizedInitializerRule.description)
}
}
6 changes: 6 additions & 0 deletions Tests/GeneratedTests/GeneratedTests_10.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
@testable import SwiftLintCore
import TestHelpers

final class UnneededSynthesizedInitializerRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(UnneededSynthesizedInitializerRule.description)
}
}

final class UnneededThrowsRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(UnneededThrowsRule.description)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,12 @@ invalid_swiftlint_command:
meta:
opt-in: false
correctable: false
invisible_character:
severity: error
additional_code_points: ["200B", "200C", "FEFF"]
meta:
opt-in: false
correctable: true
is_disjoint:
severity: warning
meta:
Expand Down
Loading