Repository: SonarSource/sonarlint-core
Pull request #1322: SLCORE-1009 Support file paths with non-ASCII characters
Author: login: nquinquenel
Created: 2025-04-10T08:23:37Z | Merged: 2025-04-14T08:40:44Z

[SLCORE-1009](https://sonarsource.atlassian.net/browse/SLCORE-1009)

Handle faulty Path.of() calls at the following locations:
- Returning path from backend input files
- Checking for symbolic links
- During git blame computation

[SLCORE-1009]: https://sonarsource.atlassian.net/browse/SLCORE-1009?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/api/ClientInputFileTests.java b/backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/api/ClientInputFileTests.java
index a5fce97c88..7f43c6e6ec 100644
--- a/backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/api/ClientInputFileTests.java
+++ b/backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/api/ClientInputFileTests.java
@@ -19,7 +19,6 @@
  */
 package org.sonarsource.sonarlint.core.analysis.api;
 
-import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
 import java.nio.charset.Charset;
@@ -32,17 +31,16 @@
 class ClientInputFileTests {
 
   @Test
-  void testDefaults(@TempDir Path tempDir) throws IOException {
+  void testDefaults(@TempDir Path tempDir) {
     var path = tempDir.resolve("Foo.java");
-    ClientInputFile underTest = new ClientInputFile() {
-
+    var underTest = new ClientInputFile() {
       @Override
       public boolean isTest() {
         return false;
       }
 
       @Override
-      public InputStream inputStream() throws IOException {
+      public InputStream inputStream() {
         return null;
       }
 
@@ -67,7 +65,7 @@ public Charset getCharset() {
       }
 
       @Override
-      public String contents() throws IOException {
+      public String contents() {
         return null;
       }
 
diff --git a/backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/SonarLintInputFileTests.java b/backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/SonarLintInputFileTests.java
index 274492562e..ef2b239911 100644
--- a/backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/SonarLintInputFileTests.java
+++ b/backend/analysis-engine/src/test/java/org/sonarsource/sonarlint/core/analysis/container/analysis/filesystem/SonarLintInputFileTests.java
@@ -22,7 +22,6 @@
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
@@ -32,7 +31,6 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 import org.sonar.api.batch.fs.InputFile.Status;
-import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile;
 import testutils.FileUtils;
 import testutils.InMemoryTestClientInputFile;
 import testutils.OnDiskTestClientInputFile;
@@ -46,8 +44,8 @@ class SonarLintInputFileTests {
   void testGetters(@TempDir Path path) throws IOException {
     var filePath = path.resolve("foo.php");
     Files.write(filePath, "test string".getBytes(StandardCharsets.UTF_8));
-    ClientInputFile inputFile = new OnDiskTestClientInputFile(filePath, "file", false, StandardCharsets.UTF_8);
-    InputStream fileInputStream = Files.newInputStream(filePath);
+    var inputFile = new OnDiskTestClientInputFile(filePath, "file", false, StandardCharsets.UTF_8);
+    var fileInputStream = Files.newInputStream(filePath);
     var file = new SonarLintInputFile(inputFile, f -> new FileMetadata().readMetadata(fileInputStream, StandardCharsets.UTF_8, filePath.toUri(), null));
 
     assertThat(file.contents()).isEqualTo("test string");
@@ -66,12 +64,11 @@ void testGetters(@TempDir Path path) throws IOException {
     try (var reader = new BufferedReader(new InputStreamReader(stream))) {
       assertThat(reader.lines().collect(Collectors.joining())).isEqualTo("test string");
     }
-
   }
 
   @Test
   void checkValidPointer() {
-    ClientInputFile inputFile = new InMemoryTestClientInputFile("foo", "src/Foo.php", null, false, null);
+    var inputFile = new InMemoryTestClientInputFile("foo", "src/Foo.php", null, false, null);
     var metadata = new FileMetadata.Metadata(2, new int[] {0, 10}, 16);
     var file = new SonarLintInputFile(inputFile, f -> metadata);
     assertThat(file.newPointer(1, 0).line()).isEqualTo(1);
@@ -84,7 +81,7 @@ void checkValidPointer() {
 
   @Test
   void selectLine() {
-    ClientInputFile inputFile = new InMemoryTestClientInputFile("foo", "src/Foo.php", null, false, null);
+    var inputFile = new InMemoryTestClientInputFile("foo", "src/Foo.php", null, false, null);
     var metadata = new FileMetadata().readMetadata(new ByteArrayInputStream("bla bla a\nabcde\n\nabc".getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8,
       URI.create("file://foo.php"), null);
     var file = new SonarLintInputFile(inputFile, f -> metadata);
@@ -103,7 +100,7 @@ void selectLine() {
 
   @Test
   void testRangeOverlap() {
-    ClientInputFile inputFile = new InMemoryTestClientInputFile("foo", "src/Foo.php", null, false, null);
+    var inputFile = new InMemoryTestClientInputFile("foo", "src/Foo.php", null, false, null);
     var metadata = new FileMetadata.Metadata(2, new int[] {0, 10}, 16);
     var file = new SonarLintInputFile(inputFile, f -> metadata);
 
diff --git a/backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/FileUtils.java b/backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/FileUtils.java
new file mode 100644
index 0000000000..31a148e1f9
--- /dev/null
+++ b/backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/FileUtils.java
@@ -0,0 +1,40 @@
+/*
+ * SonarLint Core - Commons
+ * Copyright (C) 2016-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarsource.sonarlint.core.commons.util;
+
+import java.net.URI;
+import java.nio.file.Path;
+
+public class FileUtils {
+
+  public static Path getFilePathFromUri(URI uri) {
+    try {
+      // In case the path contains non-ASCII characters, Path.of() will fail, we should first try to encode the path via toURL()
+      return Path.of(uri.toURL().getPath());
+    } catch (Exception e) {
+      return Path.of(uri);
+    }
+  }
+
+  private FileUtils() {
+    // utility class
+  }
+
+}
diff --git a/backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/NativeGitWrapper.java b/backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/NativeGitWrapper.java
index 4c08d6c197..80fa08cfd3 100644
--- a/backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/NativeGitWrapper.java
+++ b/backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/git/NativeGitWrapper.java
@@ -33,6 +33,7 @@
 import org.sonarsource.sonarlint.core.commons.SonarLintBlameResult;
 import org.sonarsource.sonarlint.core.commons.Version;
 import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger;
+import org.sonarsource.sonarlint.core.commons.util.FileUtils;
 
 import static java.lang.String.format;
 import static org.sonarsource.sonarlint.core.commons.util.git.BlameParser.parseBlameOutput;
@@ -56,7 +57,7 @@ public SonarLintBlameResult blameFromNativeCommand(Path projectBaseDir, Set<URI>
     var blameResult = new BlameResult();
     getNativeGitExecutable().ifPresent(gitExecutable -> {
       for (var fileUri : fileUris) {
-        var filePath = Path.of(fileUri).toAbsolutePath().toString();
+        var filePath = FileUtils.getFilePathFromUri(fileUri).toAbsolutePath().toString();
         var filePathUnix = filePath.replace("\\", "/");
         var yearAgo = Instant.now().minus(blameHistoryWindow);
         var thresholdDate = thresholdDateFromNewCodeDefinition.isAfter(yearAgo) ? yearAgo : thresholdDateFromNewCodeDefinition;
@@ -69,7 +70,7 @@ public SonarLintBlameResult blameFromNativeCommand(Path projectBaseDir, Set<URI>
     return new SonarLintBlameResult(blameResult, projectBaseDir);
   }
 
-  private boolean isCompatibleGitVersion(String gitVersionCommandOutput) {
+  private static boolean isCompatibleGitVersion(String gitVersionCommandOutput) {
     var gitVersion = whitespaceRegex
       .splitAsStream(gitVersionCommandOutput)
       .skip(2)
diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/BackendInputFile.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/BackendInputFile.java
index 7119efe275..0ae192c134 100644
--- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/BackendInputFile.java
+++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/BackendInputFile.java
@@ -24,10 +24,10 @@
 import java.net.URI;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
-import java.nio.file.Path;
 import org.jetbrains.annotations.Nullable;
 import org.sonarsource.sonarlint.core.analysis.api.ClientInputFile;
 import org.sonarsource.sonarlint.core.commons.api.SonarLanguage;
+import org.sonarsource.sonarlint.core.commons.util.FileUtils;
 import org.sonarsource.sonarlint.core.fs.ClientFile;
 
 public class BackendInputFile implements ClientInputFile {
@@ -39,7 +39,7 @@ public BackendInputFile(ClientFile clientFile) {
 
   @Override
   public String getPath() {
-    return Path.of(clientFile.getUri()).toAbsolutePath().toString();
+    return FileUtils.getFilePathFromUri(clientFile.getUri()).toAbsolutePath().toString();
   }
 
   @Override
diff --git a/backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/FileExclusionService.java b/backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/FileExclusionService.java
index 3d29927df2..fe68421d47 100644
--- a/backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/FileExclusionService.java
+++ b/backend/core/src/main/java/org/sonarsource/sonarlint/core/fs/FileExclusionService.java
@@ -44,6 +44,7 @@
 import org.sonarsource.sonarlint.core.commons.SmartCancelableLoadingCache;
 import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger;
 import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor;
+import org.sonarsource.sonarlint.core.commons.util.FileUtils;
 import org.sonarsource.sonarlint.core.event.BindingConfigChangedEvent;
 import org.sonarsource.sonarlint.core.file.PathTranslationService;
 import org.sonarsource.sonarlint.core.file.WindowsShortcutUtils;
@@ -259,7 +260,7 @@ private List<ClientFile> filterOutExcludedFiles(String configurationScopeId, Pat
         // symbolic links or Windows shortcuts should be near zero and this is less error-prone than excluding the
         try {
           var uri = file.getUri();
-          if (Files.isSymbolicLink(Path.of(uri))) {
+          if (Files.isSymbolicLink(FileUtils.getFilePathFromUri(uri))) {
             filteredURIsFromSymbolicLink.add(uri);
             return false;
           } else if (WindowsShortcutUtils.isWindowsShortcut(uri)) {
diff --git a/backend/core/src/test/java/org/sonarsource/sonarlint/core/analysis/BackendInputFileTests.java b/backend/core/src/test/java/org/sonarsource/sonarlint/core/analysis/BackendInputFileTests.java
new file mode 100644
index 0000000000..4d2ecc9b40
--- /dev/null
+++ b/backend/core/src/test/java/org/sonarsource/sonarlint/core/analysis/BackendInputFileTests.java
@@ -0,0 +1,54 @@
+/*
+ * SonarLint Core - Implementation
+ * Copyright (C) 2016-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarsource.sonarlint.core.analysis;
+
+import java.io.File;
+import java.net.URI;
+import java.nio.file.Path;
+import org.junit.jupiter.api.Test;
+import org.sonarsource.sonarlint.core.fs.ClientFile;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class BackendInputFileTests {
+
+  @Test
+  void ascii_path_should_be_the_same() {
+    var path = Path.of("/test/file.php");
+    var pathAsString = path.toString().replace(File.separatorChar, '/');
+    var clientFile = new ClientFile(URI.create("file://" + pathAsString), "configScopeId", path, false, null, null, null, true);
+
+    var inputFile = new BackendInputFile(clientFile);
+
+    assertThat(inputFile.getPath().replace(File.separatorChar, '/')).endsWith(pathAsString);
+  }
+
+  @Test
+  void non_ascii_path_should_be_the_same() {
+    var path = Path.of("/中文字符/file.php");
+    var pathAsString = path.toString().replace(File.separatorChar, '/');
+    var clientFile = new ClientFile(URI.create("file://" + pathAsString), "configScopeId", path, false, null, null, null, true);
+
+    var inputFile = new BackendInputFile(clientFile);
+
+    assertThat(inputFile.getPath().replace(File.separatorChar, '/')).endsWith(pathAsString);
+  }
+
+}
diff --git a/medium-tests/src/test/java/mediumtest/analysis/AnalysisMediumTests.java b/medium-tests/src/test/java/mediumtest/analysis/AnalysisMediumTests.java
index b160d2c771..5be471a70d 100644
--- a/medium-tests/src/test/java/mediumtest/analysis/AnalysisMediumTests.java
+++ b/medium-tests/src/test/java/mediumtest/analysis/AnalysisMediumTests.java
@@ -22,6 +22,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.net.URI;
+import java.net.URLDecoder;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
@@ -1066,6 +1067,68 @@ void it_should_use_enterprise_csharp_analyzer_when_connected_to_community_non_re
     assertThat(result.shouldUseEnterpriseAnalyzer()).isTrue();
   }
 
+  @SonarLintTest
+  void it_should_analyze_xml_file_in_non_ascii_directory(SonarLintTestHarness harness, @TempDir Path baseDir) throws IOException {
+    var directoryPath = baseDir.resolve("中文字符");
+    Files.createDirectory(directoryPath);
+    var filePath = createFile(directoryPath, "pom.xml",
+      """
+        <?xml version="1.0" encoding="UTF-8"?>
+        <project>
+          <modelVersion>4.0.0</modelVersion>
+          <groupId>com.foo</groupId>
+          <artifactId>bar</artifactId>
+          <version>${pom.version}</version>
+        </project>""");
+    var fileUri = filePath.toUri();
+    var client = harness.newFakeClient()
+      .withInitialFs(CONFIG_SCOPE_ID, baseDir, List.of(new ClientFileDto(fileUri, baseDir.relativize(filePath), CONFIG_SCOPE_ID, false, null, filePath, null, null, true)))
+      .build();
+    var backend = harness.newBackend()
+      .withUnboundConfigScope(CONFIG_SCOPE_ID)
+      .withStandaloneEmbeddedPluginAndEnabledLanguage(TestPlugin.XML)
+      .start(client);
+    var analysisId = UUID.randomUUID();
+
+    var result = backend.getAnalysisService()
+      .analyzeFilesAndTrack(new AnalyzeFilesAndTrackParams(CONFIG_SCOPE_ID, analysisId, List.of(fileUri), Map.of(), false, System.currentTimeMillis())).join();
+
+    assertThat(result.getFailedAnalysisFiles()).isEmpty();
+    var raisedIssueDto = awaitRaisedIssuesNotification(client, CONFIG_SCOPE_ID);
+    assertThat(raisedIssueDto).isNotEmpty();
+  }
+
+  @SonarLintTest
+  void it_should_analyze_xml_file_in_non_ascii_directory_and_non_decoded_uri(SonarLintTestHarness harness, @TempDir Path baseDir) throws IOException {
+    var directoryPath = baseDir.resolve("中文字符");
+    Files.createDirectory(directoryPath);
+    var filePath = createFile(directoryPath, "pom.xml",
+      """
+        <?xml version="1.0" encoding="UTF-8"?>
+        <project>
+          <modelVersion>4.0.0</modelVersion>
+          <groupId>com.foo</groupId>
+          <artifactId>bar</artifactId>
+          <version>${pom.version}</version>
+        </project>""");
+    var fileUri = URI.create(URLDecoder.decode(filePath.toUri().toString(), StandardCharsets.UTF_8));
+    var client = harness.newFakeClient()
+      .withInitialFs(CONFIG_SCOPE_ID, baseDir, List.of(new ClientFileDto(fileUri, baseDir.relativize(filePath), CONFIG_SCOPE_ID, false, null, filePath, null, null, true)))
+      .build();
+    var backend = harness.newBackend()
+      .withUnboundConfigScope(CONFIG_SCOPE_ID)
+      .withStandaloneEmbeddedPluginAndEnabledLanguage(TestPlugin.XML)
+      .start(client);
+    var analysisId = UUID.randomUUID();
+
+    var result = backend.getAnalysisService()
+      .analyzeFilesAndTrack(new AnalyzeFilesAndTrackParams(CONFIG_SCOPE_ID, analysisId, List.of(fileUri), Map.of(), false, System.currentTimeMillis())).join();
+
+    assertThat(result.getFailedAnalysisFiles()).isEmpty();
+    var raisedIssueDto = awaitRaisedIssuesNotification(client, CONFIG_SCOPE_ID);
+    assertThat(raisedIssueDto).isNotEmpty();
+  }
+
   private ClientInputFile prepareInputFile(String relativePath, String content, final boolean isTest, Charset encoding,
     @Nullable SonarLanguage language, Path baseDir) throws IOException {
     final var file = new File(baseDir.toFile(), relativePath);
diff --git a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/SonarLintBackendFixture.java b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/SonarLintBackendFixture.java
index 7b250efafa..24a2217408 100644
--- a/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/SonarLintBackendFixture.java
+++ b/test-utils/src/main/java/org/sonarsource/sonarlint/core/test/utils/SonarLintBackendFixture.java
@@ -24,6 +24,7 @@
 import java.io.PipedOutputStream;
 import java.net.URI;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.time.Duration;
@@ -586,12 +587,13 @@ private static SonarLintTestRpcServer createTestBackend(SonarLintRpcClientDelega
 
         @Override
         public void write(@NotNull byte[] b) throws IOException {
-          mem.append(new String(b));
-          flushIfNeeded();
+          var content = new String(b, StandardCharsets.UTF_8);
+          mem.append(content);
+          flushIfNeeded(content);
           super.write(b);
         }
 
-        private void flushIfNeeded() {
+        private void flushIfNeeded(String b) {
           int cr = mem.indexOf("\r\n");
           if (cr != -1 && nextContentSize < 0) {
             var contentLength = mem.substring(0, cr);
@@ -599,8 +601,15 @@ private void flushIfNeeded() {
             nextContentSize = Integer.parseInt(contentLength.substring("Content-Length: ".length()));
           }
           if (nextContentSize > 0 && mem.length() >= nextContentSize + 2) {
-            var content = mem.substring(2, nextContentSize + 2);
-            mem.replace(0, nextContentSize + 2, "");
+            var content = b.trim();
+            var bytes = mem.toString().getBytes(StandardCharsets.UTF_8);
+            var relevantBytes = new byte[nextContentSize];
+            System.arraycopy(bytes, 0, relevantBytes, 0, nextContentSize);
+            // Because of non-ASCII characters, a character might be longer than one byte, which makes Content-Length irrelevant
+            // As a workaround, we can directly extract the String from the byte array
+            var relevantString = new String(relevantBytes, StandardCharsets.UTF_8);
+
+            mem.replace(0, relevantString.length() + 2, "");
             nextContentSize = -1;
             System.out.println("--> " + content);
           }
@@ -614,7 +623,7 @@ private void flushIfNeeded() {
         public synchronized int read(byte[] b, int off, int len) throws IOException {
           int readLength = super.read(b, off, len);
           if (readLength > 0) {
-            System.out.println("<-- " + new String(b, off, readLength));
+            System.out.println("<-- " + new String(b, off, readLength, StandardCharsets.UTF_8));
           }
           return readLength;
         }


Reviews:
user: login: damien-urruty-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T15:04:23Z
user: login: nquinquenel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T15:21:12Z
user: login: damien-urruty-sonarsource
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T08:19:38Z
user: login: damien-urruty-sonarsource
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T08:19:44Z

Review comments:
diff_hunk: @@ -0,0 +1,40 @@
+/*
+ * SonarLint Core - Commons
+ * Copyright (C) 2016-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarsource.sonarlint.core.commons.util;
+
+import java.net.URI;
+import java.nio.file.Path;
+
+public class FileUtils {
+
+  public static Path getFilePathFromUri(URI uri) {
+    try {
+      // In case the path contains non-ASCII characters, Path.of() will fail, we should first try to encode the path via toURL()
+      return Path.of(uri.toURL().getPath());
+    } catch (Exception e) {
+      return Path.of(uri);
+    }
+  }
path: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/FileUtils.java
user: login: damien-urruty-sonarsource
body: I think this is just a workaround. By design, we are supposed to support URIs that cannot be mapped to Paths, so the real fix would be to make sure we never convert a URI to a Path. This is to allow support of custom file systems., or remote workspaces. Could we do without converting? Otherwise, there is a way to get a relative path from the ClientFile, is this sufficient?
created_at: 2025-04-11T15:04:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,40 @@
+/*
+ * SonarLint Core - Commons
+ * Copyright (C) 2016-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarsource.sonarlint.core.commons.util;
+
+import java.net.URI;
+import java.nio.file.Path;
+
+public class FileUtils {
+
+  public static Path getFilePathFromUri(URI uri) {
+    try {
+      // In case the path contains non-ASCII characters, Path.of() will fail, we should first try to encode the path via toURL()
+      return Path.of(uri.toURL().getPath());
+    } catch (Exception e) {
+      return Path.of(uri);
+    }
+  }
path: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/FileUtils.java
user: login: nquinquenel
body: I agree that ideally, we should not do such mapping to a path, but here I'm really targeting [SLCORE-1009](https://sonarsource.atlassian.net/browse/SLCORE-1009). This issue of URI with custom schemes is another [topic](https://sonarsource.atlassian.net/browse/SLCORE-1215) IMO. Also this is the contract of the `ClientInputFile.getPath()` method to return the absolute path. I'm not exactly sure how it's handled on the analyzer side, but I'd expect it to be quite some work to switch to another API method, so for now this fix allows to make it work with path having non-ASCII characters. It feels like a good win, and not so dirty actually 🤷🏻 
created_at: 2025-04-11T15:21:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,40 @@
+/*
+ * SonarLint Core - Commons
+ * Copyright (C) 2016-2025 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarsource.sonarlint.core.commons.util;
+
+import java.net.URI;
+import java.nio.file.Path;
+
+public class FileUtils {
+
+  public static Path getFilePathFromUri(URI uri) {
+    try {
+      // In case the path contains non-ASCII characters, Path.of() will fail, we should first try to encode the path via toURL()
+      return Path.of(uri.toURL().getPath());
+    } catch (Exception e) {
+      return Path.of(uri);
+    }
+  }
path: backend/commons/src/main/java/org/sonarsource/sonarlint/core/commons/util/FileUtils.java
user: login: damien-urruty-sonarsource
body: I think it works as a short-term solution. Let's keep the more definitive fix for the second ticket you mentioned.
created_at: 2025-04-14T08:19:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqube-next[bot]
created_at: 2025-04-11T14:39:50Z
body: ## [![Quality Gate passed](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://next.sonarqube.com/sonarqube/dashboard?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&pullRequest=1322) **Quality Gate passed**  
Issues  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&pullRequest=1322&resolved=false) [0 New issues](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&pullRequest=1322&resolved=false)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/fixed-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&fixedInPullRequest=1322) [6 Fixed issues](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&fixedInPullRequest=1322)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/accepted-16px.png '')](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&pullRequest=1322&issueStatuses=ACCEPTED) [3 Accepted issues](https://next.sonarqube.com/sonarqube/project/issues?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&pullRequest=1322&issueStatuses=ACCEPTED)

Measures  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/security_hotspots?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&pullRequest=1322) [0 Security Hotspots](https://next.sonarqube.com/sonarqube/security_hotspots?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&pullRequest=1322)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&pullRequest=1322&metric=new_coverage&view=list) [94.4% Coverage on New Code](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&pullRequest=1322&metric=new_coverage&view=list)  
[![](https://raw.githubusercontent.com/SonarSource/sonarqube-static-resources/master/v104/common/passed-16px.png '')](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&pullRequest=1322&metric=new_duplicated_lines_density&view=list) [0.0% Duplication on New Code](https://next.sonarqube.com/sonarqube/component_measures?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&pullRequest=1322&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube](https://next.sonarqube.com/sonarqube/dashboard?id=org.sonarsource.sonarlint.core%3Asonarlint-core-parent&pullRequest=1322)


author_association: NONE