Skip to content

Commit a658059

Browse files
chagongCopilot
andauthored
fix: honor source encodings when resolving breakpoints (#647)
* fix: honor source encodings when resolving breakpoints Use Eclipse resource decoding for workspace files and BOM-aware decoding for external sources. Exercise breakpoint class-name resolution with UTF-16, UTF-8, and configured resource charsets in headless JDT tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: keep encoding fix notes out of public README Restore README.md to the PR base without changing the source-encoding fix or regression tests. Keep contributor-facing verification and implementation rationale in the PR description because there is no suitable existing contributor guide. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: chagong <831821+chagong@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 89e59b7 commit a658059

4 files changed

Lines changed: 217 additions & 19 deletions

File tree

‎com.microsoft.java.debug.plugin/pom.xml‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010
<artifactId>com.microsoft.java.debug.plugin</artifactId>
1111
<packaging>eclipse-plugin</packaging>
1212
<name>${base.name} :: Debugger Plugin</name>
13+
<dependencies>
14+
<dependency>
15+
<groupId>junit</groupId>
16+
<artifactId>junit</artifactId>
17+
<version>4.13.1</version>
18+
<scope>test</scope>
19+
</dependency>
20+
</dependencies>
1321
<build>
1422
<plugins>
1523
<plugin>
@@ -33,6 +41,52 @@
3341
<groupId>org.apache.maven.plugins</groupId>
3442
<artifactId>maven-checkstyle-plugin</artifactId>
3543
</plugin>
44+
<plugin>
45+
<groupId>org.eclipse.tycho</groupId>
46+
<artifactId>target-platform-configuration</artifactId>
47+
<version>${tycho-version}</version>
48+
<configuration>
49+
<filters>
50+
<filter>
51+
<type>eclipse-plugin</type>
52+
<id>org.eclipse.jdt.core.compiler.batch</id>
53+
<restrictTo>
54+
<!-- Match the JDT model in the Eclipse 4.38 target. -->
55+
<versionRange>[3.44.0,3.45.0)</versionRange>
56+
</restrictTo>
57+
</filter>
58+
</filters>
59+
</configuration>
60+
</plugin>
61+
<plugin>
62+
<groupId>org.eclipse.tycho</groupId>
63+
<artifactId>tycho-surefire-plugin</artifactId>
64+
<version>${tycho-version}</version>
65+
<configuration>
66+
<providerHint>junit4</providerHint>
67+
<importAdditionalPackagesOptional>true</importAdditionalPackagesOptional>
68+
<dependencies>
69+
<dependency>
70+
<type>eclipse-plugin</type>
71+
<artifactId>org.eclipse.m2e.jdt</artifactId>
72+
<version>0.0.0</version>
73+
</dependency>
74+
<dependency>
75+
<type>eclipse-plugin</type>
76+
<artifactId>org.eclipse.buildship.core</artifactId>
77+
<version>0.0.0</version>
78+
</dependency>
79+
</dependencies>
80+
</configuration>
81+
<executions>
82+
<execution>
83+
<goals>
84+
<goal>plugin-test</goal>
85+
<goal>verify</goal>
86+
</goals>
87+
</execution>
88+
</executions>
89+
</plugin>
3690
<plugin>
3791
<groupId>org.apache.maven.plugins</groupId>
3892
<artifactId>maven-dependency-plugin</artifactId>

‎com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java‎

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@
1111

1212
package com.microsoft.java.debug.plugin.internal;
1313

14-
import java.io.BufferedReader;
15-
import java.io.FileInputStream;
1614
import java.io.IOException;
1715
import java.io.InputStreamReader;
1816
import java.net.URI;
1917
import java.net.URISyntaxException;
18+
import java.nio.charset.Charset;
2019
import java.nio.file.Files;
2120
import java.nio.file.Paths;
2221
import java.util.ArrayList;
@@ -35,6 +34,9 @@
3534
import java.util.stream.Collectors;
3635
import java.util.stream.Stream;
3736

37+
import org.apache.commons.io.ByteOrderMark;
38+
import org.apache.commons.io.IOUtils;
39+
import org.apache.commons.io.input.BOMInputStream;
3840
import org.apache.commons.lang3.StringUtils;
3941
import org.eclipse.core.resources.IFile;
4042
import org.eclipse.core.resources.IResource;
@@ -312,7 +314,9 @@ private CompilationUnit asCompilationUnit(String uri) {
312314
// For file uri, read the file contents directly and pass them to the ast
313315
// parser.
314316
if (filePath != null && Files.isRegularFile(Paths.get(filePath))) {
315-
String source = readFile(filePath);
317+
IFile resource = (IFile) JDTUtils.findResource(JDTUtils.toURI(uri),
318+
ResourcesPlugin.getWorkspace().getRoot()::findFilesForLocationURI);
319+
String source = readFile(filePath, resource);
316320
parser.setSource(source.toCharArray());
317321
/**
318322
* See the java doc for { @link ASTParser#setResolveBindings(boolean) }.
@@ -326,8 +330,6 @@ private CompilationUnit asCompilationUnit(String uri) {
326330
* setEnvironment(String [], String [], String [], boolean)
327331
* and a unit name setUnitName(String).
328332
*/
329-
IFile resource = (IFile) JDTUtils.findResource(JDTUtils.toURI(uri),
330-
ResourcesPlugin.getWorkspace().getRoot()::findFilesForLocationURI);
331333
if (resource != null && JdtUtils.isJavaProject(resource.getProject())) {
332334
parser.setProject(JavaCore.create(resource.getProject()));
333335
} else {
@@ -498,22 +500,21 @@ private static IClassFile resolveClassFile(String uriString) {
498500
return null;
499501
}
500502

501-
private static String readFile(String filePath) {
502-
StringBuilder builder = new StringBuilder();
503-
try (BufferedReader bufferReader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath)))) {
504-
final int BUFFER_SIZE = 4096;
505-
char[] buffer = new char[BUFFER_SIZE];
506-
while (true) {
507-
int read = bufferReader.read(buffer, 0, BUFFER_SIZE);
508-
if (read == -1) {
509-
break;
510-
}
511-
builder.append(new String(buffer, 0, read));
503+
private static String readFile(String filePath, IFile resource) {
504+
try {
505+
if (resource != null) {
506+
return resource.readString();
507+
}
508+
try (BOMInputStream input = BOMInputStream.builder().setPath(Paths.get(filePath))
509+
.setByteOrderMarks(ByteOrderMark.UTF_8, ByteOrderMark.UTF_16LE, ByteOrderMark.UTF_16BE).get();
510+
InputStreamReader reader = new InputStreamReader(input,
511+
input.hasBOM() ? input.getBOMCharsetName() : Charset.defaultCharset().name())) {
512+
return IOUtils.toString(reader);
512513
}
513-
} catch (IOException e) {
514-
// do nothing.
514+
} catch (IOException | CoreException e) {
515+
logger.log(Level.SEVERE, "Failed to read source file: " + filePath, e);
516+
return "";
515517
}
516-
return builder.toString();
517518
}
518519

519520
private static String resolveSystemLibraryVersion(IJavaProject project, IVMInstall vmInstall)
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Microsoft Corporation and others.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* http://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Microsoft Corporation - initial API and implementation
10+
*******************************************************************************/
11+
12+
package com.microsoft.java.debug.plugin.internal;
13+
14+
import static org.junit.Assert.assertEquals;
15+
16+
import java.io.ByteArrayInputStream;
17+
import java.nio.charset.Charset;
18+
import java.nio.charset.StandardCharsets;
19+
import java.nio.file.Files;
20+
import java.nio.file.Path;
21+
22+
import org.eclipse.core.resources.IFile;
23+
import org.eclipse.core.resources.IProject;
24+
import org.eclipse.core.resources.IProjectDescription;
25+
import org.eclipse.core.resources.ResourcesPlugin;
26+
import org.eclipse.jdt.core.IClasspathEntry;
27+
import org.eclipse.jdt.core.IJavaProject;
28+
import org.eclipse.jdt.core.JavaCore;
29+
import org.junit.After;
30+
import org.junit.Rule;
31+
import org.junit.Test;
32+
import org.junit.rules.TemporaryFolder;
33+
import org.junit.runner.RunWith;
34+
import org.junit.runners.Parameterized;
35+
import org.junit.runners.Parameterized.Parameters;
36+
37+
import com.microsoft.java.debug.core.JavaBreakpointLocation;
38+
import com.microsoft.java.debug.core.protocol.Types.SourceBreakpoint;
39+
40+
@RunWith(Parameterized.class)
41+
public class JdtSourceLookUpProviderTest {
42+
private enum SourceKind {
43+
EXTERNAL, WORKSPACE, FILE_CHARSET, PROJECT_CHARSET
44+
}
45+
46+
private static final String PACKAGE_NAME = "d\u00e9mo"; // A non-ASCII identifier exposes incorrect decoding.
47+
private static final String SOURCE = String.join("\r\n",
48+
"package " + PACKAGE_NAME + ";",
49+
"",
50+
"public class A {",
51+
" public static void main(String[] args) {",
52+
" int x = 1;",
53+
" System.out.println(x);",
54+
" }",
55+
"}",
56+
"");
57+
58+
@Rule
59+
public TemporaryFolder temporaryFolder = new TemporaryFolder();
60+
61+
private final Charset charset;
62+
private final boolean withBom;
63+
private final SourceKind sourceKind;
64+
private IProject project;
65+
66+
public JdtSourceLookUpProviderTest(Charset charset, boolean withBom, SourceKind sourceKind) {
67+
this.charset = charset;
68+
this.withBom = withBom;
69+
this.sourceKind = sourceKind;
70+
}
71+
72+
@Parameters(name = "{index}: {0}, BOM={1}, {2}")
73+
public static Object[][] encodings() {
74+
return new Object[][] {
75+
{StandardCharsets.UTF_16LE, true, SourceKind.EXTERNAL},
76+
{StandardCharsets.UTF_16BE, true, SourceKind.EXTERNAL},
77+
{StandardCharsets.UTF_8, true, SourceKind.EXTERNAL},
78+
{StandardCharsets.UTF_8, false, SourceKind.EXTERNAL},
79+
{Charset.defaultCharset(), false, SourceKind.EXTERNAL},
80+
{StandardCharsets.UTF_16LE, true, SourceKind.WORKSPACE},
81+
{StandardCharsets.UTF_16BE, true, SourceKind.WORKSPACE},
82+
{StandardCharsets.UTF_8, true, SourceKind.WORKSPACE},
83+
{StandardCharsets.UTF_16LE, false, SourceKind.FILE_CHARSET},
84+
{StandardCharsets.UTF_16BE, false, SourceKind.FILE_CHARSET},
85+
{StandardCharsets.UTF_8, false, SourceKind.FILE_CHARSET},
86+
{StandardCharsets.ISO_8859_1, false, SourceKind.FILE_CHARSET},
87+
{StandardCharsets.ISO_8859_1, false, SourceKind.PROJECT_CHARSET},
88+
{StandardCharsets.UTF_16LE, false, SourceKind.PROJECT_CHARSET}
89+
};
90+
}
91+
92+
@After
93+
public void deleteProject() throws Exception {
94+
if (project != null && project.exists()) {
95+
project.delete(true, true, null);
96+
}
97+
}
98+
99+
@Test
100+
public void resolvesBreakpointClassName() throws Exception {
101+
byte[] contents = (withBom ? "\uFEFF" + SOURCE : SOURCE).getBytes(charset);
102+
String uri;
103+
if (sourceKind == SourceKind.EXTERNAL) {
104+
Path file = temporaryFolder.getRoot().toPath().resolve("A.java");
105+
Files.write(file, contents);
106+
uri = file.toUri().toASCIIString();
107+
} else {
108+
IFile file = createWorkspaceFile(contents);
109+
uri = file.getLocationURI().toASCIIString();
110+
}
111+
112+
JavaBreakpointLocation[] locations = new JdtSourceLookUpProvider().getBreakpointLocations(uri,
113+
new SourceBreakpoint[] {new SourceBreakpoint(5, -1)});
114+
115+
assertEquals(1, locations.length);
116+
assertEquals(PACKAGE_NAME + ".A", locations[0].className());
117+
assertEquals(5, locations[0].lineNumber());
118+
assertEquals(5, locations[0].availableBreakpointLocations()[0].line);
119+
}
120+
121+
private IFile createWorkspaceFile(byte[] contents) throws Exception {
122+
project = ResourcesPlugin.getWorkspace().getRoot().getProject("source-encoding-test");
123+
project.create(null);
124+
project.open(null);
125+
project.setDefaultCharset(StandardCharsets.UTF_8.name(), null);
126+
IProjectDescription description = project.getDescription();
127+
description.setNatureIds(new String[] {JavaCore.NATURE_ID});
128+
project.setDescription(description, null);
129+
IJavaProject javaProject = JavaCore.create(project);
130+
javaProject.setRawClasspath(new IClasspathEntry[] {JavaCore.newSourceEntry(project.getFullPath())},
131+
project.getFullPath().append("bin"), null);
132+
133+
IFile file = project.getFile("A.java");
134+
file.create(new ByteArrayInputStream(contents), true, null);
135+
if (sourceKind == SourceKind.FILE_CHARSET) {
136+
file.setCharset(charset.name(), null);
137+
} else if (sourceKind == SourceKind.PROJECT_CHARSET) {
138+
project.setDefaultCharset(charset.name(), null);
139+
}
140+
return file;
141+
}
142+
}

‎com.microsoft.java.debug.target/com.microsoft.java.debug.tp.target‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<locations>
55
<location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="planner" includeSource="true" type="InstallableUnit">
66
<unit id="org.eclipse.platform.feature.group" version="0.0.0"/>
7+
<unit id="org.eclipse.jdt.core.compiler.batch" version="0.0.0"/>
78
<repository location="https://download.eclipse.org/eclipse/updates/4.38"/>
89
</location>
910
<location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="planner" includeSource="true" type="InstallableUnit">

0 commit comments

Comments
 (0)