diff --git a/CHANGELOG.md b/CHANGELOG.md index 376c93f074..9f7656fc86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,10 @@ #### Enhancements -* None. +* Add new `optional_data_string_conversion` rule to enforce + failable conversions of `Data` -> UTF-8 `String`. + [Sam Rayner](https://github.com/samrayner) + [#5263](https://github.com/realm/SwiftLint/issues/5263) #### Bug Fixes @@ -24,6 +27,11 @@ [SimplyDanny](https://github.com/SimplyDanny) [#5153](https://github.com/realm/SwiftLint/issues/5153) +* Revert `optional_data_string_conversion` enforcing + non-failable conversions of `Data` -> UTF-8 `String`. + [Sam Rayner](https://github.com/samrayner) + [#5263](https://github.com/realm/SwiftLint/issues/5263) + ## 0.55.1: Universal Washing Powder #### Breaking diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index 3c1332980a..10f6ae2ab5 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -140,6 +140,7 @@ public let builtInRules: [any Rule.Type] = [ OpeningBraceRule.self, OperatorFunctionWhitespaceRule.self, OperatorUsageWhitespaceRule.self, + OptionalDataStringConversionRule.self, OptionalEnumCaseMatchingRule.self, OrphanedDocCommentRule.self, OverriddenSuperCallRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift index 24cd22d7a1..d63f1be2a7 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift @@ -5,16 +5,14 @@ struct NonOptionalStringDataConversionRule: Rule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( identifier: "non_optional_string_data_conversion", - name: "Non-Optional String <-> Data Conversion", - description: "Prefer using UTF-8 encoded strings when converting between `String` and `Data`", + name: "Non-Optional String -> Data Conversion", + description: "Prefer using non-optional Data(_:) when converting a UTF-8 String to Data", kind: .lint, nonTriggeringExamples: [ Example("Data(\"foo\".utf8)"), - Example("String(decoding: data, as: UTF8.self)") ], triggeringExamples: [ Example("\"foo\".data(using: .utf8)"), - Example("String(data: data, encoding: .utf8)") ] ) } @@ -31,15 +29,6 @@ private extension NonOptionalStringDataConversionRule { violations.append(node.positionAfterSkippingLeadingTrivia) } } - - override func visitPost(_ node: DeclReferenceExprSyntax) { - if node.baseName.text == "String", - let parent = node.parent?.as(FunctionCallExprSyntax.self), - parent.arguments.map({ $0.label?.text }) == ["data", "encoding"], - parent.arguments.last?.expression.as(MemberAccessExprSyntax.self)?.isUTF8 == true { - violations.append(node.positionAfterSkippingLeadingTrivia) - } - } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/OptionalDataStringConversionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/OptionalDataStringConversionRule.swift new file mode 100644 index 0000000000..70ee8618ad --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/OptionalDataStringConversionRule.swift @@ -0,0 +1,32 @@ +import SwiftSyntax + +@SwiftSyntaxRule +struct OptionalDataStringConversionRule: Rule { + var configuration = SeverityConfiguration(.warning) + static let description = RuleDescription( + identifier: "optional_data_string_conversion", + name: "Optional Data -> String Conversion", + description: "Prefer using failable String(data:encoding:) when converting from `Data` to a UTF-8 `String`", + kind: .lint, + nonTriggeringExamples: [ + Example("String(data: data, encoding: .utf8)") + ], + triggeringExamples: [ + Example("String(decoding: data, as: UTF8.self)") + ] + ) +} + +private extension OptionalDataStringConversionRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: DeclReferenceExprSyntax) { + if node.baseName.text == "String", + let parent = node.parent?.as(FunctionCallExprSyntax.self), + let expr = parent.arguments.last?.expression.as(MemberAccessExprSyntax.self), + expr.base?.description == "UTF8", + expr.declName.baseName.description == "self" { + violations.append(node.positionAfterSkippingLeadingTrivia) + } + } + } +} diff --git a/Source/SwiftLintCore/Extensions/Configuration+Remote.swift b/Source/SwiftLintCore/Extensions/Configuration+Remote.swift index 3337ad1da0..e8eec05c86 100644 --- a/Source/SwiftLintCore/Extensions/Configuration+Remote.swift +++ b/Source/SwiftLintCore/Extensions/Configuration+Remote.swift @@ -94,7 +94,7 @@ internal extension Configuration.FileGraph.FilePath { guard taskResult.2 == nil, // No error (taskResult.1 as? HTTPURLResponse)?.statusCode == 200, - let configStr = (taskResult.0.flatMap { String(decoding: $0, as: UTF8.self) }) + let configStr = (taskResult.0.flatMap { String(data: $0, encoding: .utf8) }) else { return try handleWrongData( urlString: urlString, diff --git a/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift b/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift index c25c4dfb1b..66f7d74df2 100644 --- a/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift +++ b/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift @@ -37,8 +37,9 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, .map({ $0.rawValue }).sorted(by: <).joined(separator: ","), severity.rawValue ] - if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject) { - return String(decoding: jsonData, as: UTF8.self) + if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject), + let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString } queuedFatalError("Could not serialize regex configuration for cache") } diff --git a/Source/swiftlint/Extensions/Configuration+CommandLine.swift b/Source/swiftlint/Extensions/Configuration+CommandLine.swift index d9422a4d42..a803f9573c 100644 --- a/Source/swiftlint/Extensions/Configuration+CommandLine.swift +++ b/Source/swiftlint/Extensions/Configuration+CommandLine.swift @@ -213,8 +213,10 @@ extension Configuration { fileprivate func getFiles(with visitor: LintableFilesVisitor) async throws -> [SwiftLintFile] { if visitor.useSTDIN { let stdinData = FileHandle.standardInput.readDataToEndOfFile() - let stdinString = String(decoding: stdinData, as: UTF8.self) - return [SwiftLintFile(contents: stdinString)] + if let stdinString = String(data: stdinData, encoding: .utf8) { + return [SwiftLintFile(contents: stdinString)] + } + throw SwiftLintError.usageError(description: "stdin isn't a UTF8-encoded string") } if visitor.useScriptInputFiles { let files = try scriptInputFiles() diff --git a/Source/swiftlint/Helpers/LintableFilesVisitor.swift b/Source/swiftlint/Helpers/LintableFilesVisitor.swift index b13993ddc7..2f4cbc9155 100644 --- a/Source/swiftlint/Helpers/LintableFilesVisitor.swift +++ b/Source/swiftlint/Helpers/LintableFilesVisitor.swift @@ -179,8 +179,8 @@ struct LintableFilesVisitor { } private static func loadLogCompilerInvocations(_ path: String) -> [[String]]? { - if let data = FileManager.default.contents(atPath: path) { - let logContents = String(decoding: data, as: UTF8.self) + if let data = FileManager.default.contents(atPath: path), + let logContents = String(data: data, encoding: .utf8) { if logContents.isEmpty { return nil } diff --git a/Source/swiftlint/Helpers/SwiftPMCompilationDB.swift b/Source/swiftlint/Helpers/SwiftPMCompilationDB.swift index 6faa7c29be..b71ea344bf 100644 --- a/Source/swiftlint/Helpers/SwiftPMCompilationDB.swift +++ b/Source/swiftlint/Helpers/SwiftPMCompilationDB.swift @@ -37,7 +37,7 @@ struct SwiftPMCompilationDB: Codable { let pathToReplace = Array(nodes.nodes.keys.filter({ node in node.hasSuffix(suffix) }))[0].dropLast(suffix.count - 1) - let stringFileContents = String(decoding: yaml, as: UTF8.self) + let stringFileContents = String(data: yaml, encoding: .utf8)! .replacingOccurrences(of: pathToReplace, with: "") compilationDB = try decoder.decode(Self.self, from: stringFileContents) } else { diff --git a/Tests/GeneratedTests/GeneratedTests.swift b/Tests/GeneratedTests/GeneratedTests.swift index 25d750903f..fe9c6b38e3 100644 --- a/Tests/GeneratedTests/GeneratedTests.swift +++ b/Tests/GeneratedTests/GeneratedTests.swift @@ -829,6 +829,12 @@ final class OperatorUsageWhitespaceRuleGeneratedTests: SwiftLintTestCase { } } +final class OptionalDataStringConversionRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(OptionalDataStringConversionRule.description) + } +} + final class OptionalEnumCaseMatchingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(OptionalEnumCaseMatchingRule.description) diff --git a/Tests/IntegrationTests/IntegrationTests.swift b/Tests/IntegrationTests/IntegrationTests.swift index 7a116f265f..b4d9dd0def 100644 --- a/Tests/IntegrationTests/IntegrationTests.swift +++ b/Tests/IntegrationTests/IntegrationTests.swift @@ -188,8 +188,8 @@ private func execute(_ args: [String], queue.async(group: group) { stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile() } process.waitUntilExit() group.wait() - let stdout = stdoutData.map { String(decoding: $0, as: UTF8.self) } ?? "" - let stderr = stderrData.map { String(decoding: $0, as: UTF8.self) } ?? "" + let stdout = stdoutData.flatMap { String(data: $0, encoding: .utf8) } ?? "" + let stderr = stderrData.flatMap { String(data: $0, encoding: .utf8) } ?? "" return (process.terminationStatus, stdout, stderr) }