Skip to content

Commit

Permalink
Add a new non_optional_string_data_conversion rule (#5264)
Browse files Browse the repository at this point in the history
  • Loading branch information
ben-p-commits committed Dec 2, 2023
1 parent 0d54c2a commit 544e1c6
Show file tree
Hide file tree
Showing 11 changed files with 75 additions and 16 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
[leonardosrodrigues0](https://github.com/leonardosrodrigues0)
[#5265](https://github.com/realm/SwiftLint/issues/5265)

* Add new `non_optional_string_data_conversion` rule to enforce
non-failable conversions of UTF-8 `String` <-> `Data`.
[Ben P](https://github.com/ben-p-commits)
[#5263](https://github.com/realm/SwiftLint/issues/5263)

#### Bug Fixes

* Fix some false positives in the `opening_brace` rule.
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 @@ -130,6 +130,7 @@ public let builtInRules: [any Rule.Type] = [
NoGroupingExtensionRule.self,
NoMagicNumbersRule.self,
NoSpaceInMethodCallRule.self,
NonOptionalStringDataConversionRule.self,
NonOverridableClassDeclarationRule.self,
NotificationCenterDetachmentRule.self,
NumberSeparatorRule.self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import SwiftSyntax

@SwiftSyntaxRule
struct NonOptionalStringDataConversionRule: Rule {
var configuration = SeverityConfiguration<Self>(.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`",
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)")
]
)
}

private extension NonOptionalStringDataConversionRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override func visitPost(_ node: MemberAccessExprSyntax) {
if node.base?.is(StringLiteralExprSyntax.self) == true,
node.declName.baseName.text == "data",
let parent = node.parent?.as(FunctionCallExprSyntax.self),
let argument = parent.arguments.onlyElement,
argument.label?.text == "using",
argument.expression.as(MemberAccessExprSyntax.self)?.isUTF8 == true {
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)
}
}
}
}

private extension MemberAccessExprSyntax {
var isUTF8: Bool {
declName.baseName.text == "utf8"
}
}
2 changes: 1 addition & 1 deletion Source/SwiftLintCore/Extensions/Configuration+Remote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(data: $0, encoding: .utf8) })
let configStr = (taskResult.0.flatMap { String(decoding: $0, as: UTF8.self) })
else {
return try handleWrongData(
urlString: urlString,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@ public struct RegexConfiguration<Parent: Rule>: SeverityBasedRuleConfiguration,
.map({ $0.rawValue }).sorted(by: <).joined(separator: ","),
severity.rawValue
]
if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject),
let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString
if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject) {
return String(decoding: jsonData, as: UTF8.self)
}
queuedFatalError("Could not serialize regex configuration for cache")
}
Expand Down
6 changes: 2 additions & 4 deletions Source/swiftlint/Extensions/Configuration+CommandLine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,8 @@ extension Configuration {
fileprivate func getFiles(with visitor: LintableFilesVisitor) async throws -> [SwiftLintFile] {
if visitor.useSTDIN {
let stdinData = FileHandle.standardInput.readDataToEndOfFile()
if let stdinString = String(data: stdinData, encoding: .utf8) {
return [SwiftLintFile(contents: stdinString)]
}
throw SwiftLintError.usageError(description: "stdin isn't a UTF8-encoded string")
let stdinString = String(decoding: stdinData, as: UTF8.self)
return [SwiftLintFile(contents: stdinString)]
} else if visitor.useScriptInputFiles {
let files = try scriptInputFiles()
guard visitor.forceExclude else {
Expand Down
4 changes: 2 additions & 2 deletions Source/swiftlint/Helpers/LintableFilesVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ struct LintableFilesVisitor {
}

private static func loadLogCompilerInvocations(_ path: String) -> [[String]]? {
if let data = FileManager.default.contents(atPath: path),
let logContents = String(data: data, encoding: .utf8) {
if let data = FileManager.default.contents(atPath: path) {
let logContents = String(decoding: data, as: UTF8.self)
if logContents.isEmpty {
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion Source/swiftlint/Helpers/SwiftPMCompilationDB.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(data: yaml, encoding: .utf8)!
let stringFileContents = String(decoding: yaml, as: UTF8.self)
.replacingOccurrences(of: pathToReplace, with: "")
compilationDB = try decoder.decode(Self.self, from: stringFileContents)
} else {
Expand Down
6 changes: 6 additions & 0 deletions Tests/GeneratedTests/GeneratedTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,12 @@ class NoSpaceInMethodCallRuleGeneratedTests: SwiftLintTestCase {
}
}

class NonOptionalStringDataConversionRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(NonOptionalStringDataConversionRule.description)
}
}

class NonOverridableClassDeclarationRuleGeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule(NonOverridableClassDeclarationRule.description)
Expand Down
4 changes: 2 additions & 2 deletions Tests/IntegrationTests/IntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ private func execute(_ args: [String],
queue.async(group: group) { stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile() }
process.waitUntilExit()
group.wait()
let stdout = stdoutData.flatMap { String(data: $0, encoding: .utf8) } ?? ""
let stderr = stderrData.flatMap { String(data: $0, encoding: .utf8) } ?? ""
let stdout = stdoutData.map { String(decoding: $0, as: UTF8.self) } ?? ""
let stderr = stderrData.map { String(decoding: $0, as: UTF8.self) } ?? ""
return (process.terminationStatus, stdout, stderr)
}

Expand Down
6 changes: 3 additions & 3 deletions Tests/SwiftLintFrameworkTests/SwiftLintFileTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class SwiftLintFileTests: SwiftLintTestCase {

override func setUp() async throws {
try await super.setUp()
try "let i = 2".data(using: .utf8)!.write(to: tempFile)
try Data("let i = 2".utf8).write(to: tempFile)
}

override func tearDown() async throws {
Expand Down Expand Up @@ -40,12 +40,12 @@ class SwiftLintFileTests: SwiftLintTestCase {
file.write("let j = 2")

XCTAssertEqual(file.contents, "let j = 2")
XCTAssertEqual(FileManager.default.contents(atPath: tempFile.path), "let j = 2".data(using: .utf8))
XCTAssertEqual(FileManager.default.contents(atPath: tempFile.path), Data("let j = 2".utf8))

file.append("2")

XCTAssertEqual(file.contents, "let j = 22")
XCTAssertEqual(FileManager.default.contents(atPath: tempFile.path), "let j = 22".data(using: .utf8))
XCTAssertEqual(FileManager.default.contents(atPath: tempFile.path), Data("let j = 22".utf8))
}

func testFileNotTouchedIfNothingAppended() throws {
Expand Down

0 comments on commit 544e1c6

Please sign in to comment.