Repository: muter-mutation-testing/muter (swift)

Issue:
[Error]Swift/StringCharacterView.swift:158 Fatal error: String index is out of bounds
Hello.
When I ran Muter, the following error occurred. If anyone knows the solution, please let me know.

### Error Message
```
Analyzing source files to find mutants which can be inserted into your project...
Swift/StringCharacterView.swift:158 Fatal error: String index is out of bounds
zsh: trace trap  muter run
```

### Summay
- Successfully tested
- Occurs in some files, not all　(40/1000 files)
- Error in "ChangeLogicalConnector" or "RelationalOperatorReplacement"


### Target
ex.`return activeFlag && !isNew()`

The above code has been confirmed not to produce any errors by commenting it out.

### Environment
- MacOS: Sonoma 14.1.2
- Xcode: 15.0.1
- Muter: 16

**※Using a translation tool to translate Japanese to English.**

Pull request description:
# What it Does
* Closes #257

Patch:
diff --git a/Sources/muterCore/Extensions/StringExtensions.swift b/Sources/muterCore/Extensions/StringExtensions.swift
index 7fecd9e3..f71c7784 100644
--- a/Sources/muterCore/Extensions/StringExtensions.swift
+++ b/Sources/muterCore/Extensions/StringExtensions.swift
@@ -80,3 +80,36 @@ extension String {
         return String(self[range])
     }
 }
+
+extension String {
+    /// Converts a UTF-8 byte offset to a character offset in the given string.
+    ///
+    /// - Parameters:
+    ///   - utf8Offset: The UTF-8 byte offset to be converted.
+    /// - Returns: The corresponding character offset if the conversion is successful, or `nil` if can't convert to corresponding char offset.
+    ///
+    /// - Complexity: O(*n*), where *n* is the number of characters in the string.
+    func convertToCharOffset(from utf8Offset: Int) -> Int? {
+        guard utf8Offset >= 0 && utf8Offset <= utf8.count else {
+            return nil
+        }
+
+        var charOffset = 0
+        var currentUtf8Offset = 0
+
+        for char in self {
+            if currentUtf8Offset == utf8Offset {
+                return charOffset
+            }
+            
+            if currentUtf8Offset > utf8Offset {
+                return nil
+            }
+            
+            currentUtf8Offset += char.utf8.count
+            charOffset += 1
+        }
+
+        return nil
+    }
+}
diff --git a/Sources/muterCore/MutationOperators/TokenAwareVisitor.swift b/Sources/muterCore/MutationOperators/TokenAwareVisitor.swift
index a0b7251e..ec9752fd 100644
--- a/Sources/muterCore/MutationOperators/TokenAwareVisitor.swift
+++ b/Sources/muterCore/MutationOperators/TokenAwareVisitor.swift
@@ -72,30 +72,21 @@ class TokenAwareVisitor: MuterVisitor {
     ) -> CodeBlockItemListSyntax {
         let codeBlockItemListSyntax = node.codeBlockItemListSyntax
         let codeBlockDescription = codeBlockItemListSyntax.description
-        let mutatedSyntaxDescription = mutatedSyntax.description
-        let nodePosition = node.offsetInCodeBlockItemListSyntax(sourceCodeInfo)
+        let utf8Offset = node.offsetInCodeBlockItemListSyntax(sourceCodeInfo)
         
-        var nodeStartRange = codeBlockDescription.index(
+        guard let nodePositionOffset = codeBlockDescription.convertToCharOffset(from: utf8Offset) else {
+            return super.transform(node: node, mutatedSyntax: mutatedSyntax, at: mutationRange)
+        }
+
+        let nodeStartRange = codeBlockDescription.index(
             codeBlockDescription.startIndex,
-            offsetBy: nodePosition
+            offsetBy: nodePositionOffset
         )
-        var nodeEndRange = codeBlockDescription.index(
+        
+        let nodeEndRange = codeBlockDescription.index(
             codeBlockDescription.startIndex,
-            offsetBy: nodePosition + mutatedSyntaxDescription.count
+            offsetBy: nodePositionOffset + mutatedSyntax.description.count
         )
-        
-        let operatorInCodeBlock = String(codeBlockDescription[nodeStartRange ..< nodeEndRange])
-        let oppositeOperator = oppositeOperatorMapping[operatorInCodeBlock.trimmed]
-        if oppositeOperator != mutatedSyntaxDescription.trimmed,
-           let range = tryFixOperatorRange(
-               for: mutatedSyntax,
-               in: codeBlockDescription,
-               at: nodePosition
-           ) {
-            nodeStartRange = range.start
-            nodeEndRange = range.end
-        }
-
         let mutationRangeInCodeBlock = nodeStartRange ..< nodeEndRange
 
         return super.transform(
@@ -104,32 +95,6 @@ class TokenAwareVisitor: MuterVisitor {
             at: mutationRangeInCodeBlock
         )
     }
-
-    // This is a sliding window to try to find the correct String.Index since sometimes the utf8Offset is wrong.
-    private func tryFixOperatorRange(
-        for mutatedSyntax: SyntaxProtocol,
-        in codeBlockDescription: String,
-        at nodePosition: Int
-    ) -> (start: String.Index, end: String.Index)? {
-        let mutatedSyntaxDescription = mutatedSyntax.description
-        let op = oppositeOperatorMapping[mutatedSyntaxDescription.trimmed]
-        for i in 0 ... codeBlockDescription.count {
-            let start = codeBlockDescription.index(
-                codeBlockDescription.startIndex,
-                offsetBy: nodePosition - i
-            )
-            let end = codeBlockDescription.index(
-                codeBlockDescription.startIndex,
-                offsetBy: (nodePosition + mutatedSyntaxDescription.count) - i
-            )
-
-            if String(codeBlockDescription[start ..< end]).trimmed == op {
-                return (start: start, end: end)
-            }
-        }
-
-        return nil
-    }
 }
 
 private extension SyntaxProtocol {


Test patch:
diff --git a/Tests/MutationOperators/ChangeLogicalConnectorOperatorTests.swift b/Tests/MutationOperators/ChangeLogicalConnectorOperatorTests.swift
index 448f5668..96136da1 100644
--- a/Tests/MutationOperators/ChangeLogicalConnectorOperatorTests.swift
+++ b/Tests/MutationOperators/ChangeLogicalConnectorOperatorTests.swift
@@ -32,6 +32,26 @@ final class ChangeLogicalConnectorOperatorTests: MuterTestCase {
 
         let actualSchemata = visitor.schemataMappings
         let expectedSchemata = try SchemataMutationMapping.make(
+            (
+                source: "\n    // バルーンの表示判定\n    return false && false",
+                schemata: [
+                    .make(
+                        filePath: sourceWithLogicalOperators.path,
+                        mutationOperatorId: .logicalOperator,
+                        syntaxMutation: "\n    // バルーンの表示判定\n    return false || false",
+                        position: MutationPosition(
+                            utf8Offset: 256,
+                            line: 15,
+                            column: 18
+                        ),
+                        snapshot: .make(
+                            before: "&&",
+                            after: "||",
+                            description: "changed && to ||"
+                        )
+                    ),
+                ]
+            ),
             (
                 source: "\n    return false && false",
                 schemata: [
diff --git a/Tests/MutationOperators/__Snapshots__/ChangeLogicalConnectorOperatorTests/test_rewriter.1.txt b/Tests/MutationOperators/__Snapshots__/ChangeLogicalConnectorOperatorTests/test_rewriter.1.txt
index 2b9125ec..8c88db9f 100644
--- a/Tests/MutationOperators/__Snapshots__/ChangeLogicalConnectorOperatorTests/test_rewriter.1.txt
+++ b/Tests/MutationOperators/__Snapshots__/ChangeLogicalConnectorOperatorTests/test_rewriter.1.txt
@@ -21,3 +21,18 @@ func someOtherCode() -> Bool {
     return true || true
   }
 }
+
+func isEnabledToShow() -> Bool {
+  // バルーンの表示判定
+  if ProcessInfo.processInfo.environment[
+    "sampleWithLogicalOperators_ChangeLogicalConnector_15_18_256"] != nil
+  {
+    // バルーンの表示判定
+    return false || false
+    // バルーンの表示判定
+  } else {
+    // バルーンの表示判定
+    return false && false
+    // バルーンの表示判定
+  }
+}
diff --git a/Tests/fixtures/MutationExamples/LogicalOperator/sampleWithLogicalOperators.swift b/Tests/fixtures/MutationExamples/LogicalOperator/sampleWithLogicalOperators.swift
index 8b80b77d..638eff0d 100644
--- a/Tests/fixtures/MutationExamples/LogicalOperator/sampleWithLogicalOperators.swift
+++ b/Tests/fixtures/MutationExamples/LogicalOperator/sampleWithLogicalOperators.swift
@@ -9,3 +9,8 @@ func someCode() -> Bool {
 func someOtherCode() -> Bool {
     return true || true
 }
+
+func isEnabledToShow() -> Bool {
+    // バルーンの表示判定
+    return false && false
+}
diff --git a/Tests/muterCore/StringExtensionsTests.swift b/Tests/muterCore/StringExtensionsTests.swift
new file mode 100644
index 00000000..869eab37
--- /dev/null
+++ b/Tests/muterCore/StringExtensionsTests.swift
@@ -0,0 +1,19 @@
+import XCTest
+@testable import muterCore
+
+final class StringExtensionsTests: XCTestCase {
+    func testUTF8OffsetToCharOffset() {
+        let string = "バルーンの表示判定"
+
+        // Test valid offset
+        XCTAssertEqual(string.convertToCharOffset(from: 3), 1)
+
+        // Test invalid offset (negative)
+        XCTAssertNil(string.convertToCharOffset(from: -1))
+
+        // Test invalid offset (beyond string length)
+        XCTAssertNil(string.convertToCharOffset(from: string.utf8.count + 1))
+        
+        XCTAssertNil(string.convertToCharOffset(from: 2))
+    }
+}