Skip to content

Commit 14b970d

Browse files
archiecobbsVicente Romero
authored and
Vicente Romero
committed
8296656: java.lang.NoClassDefFoundError exception on running fully legitimate code
8287885: Local classes cause ClassLoader error if the type names are similar but not same Reviewed-by: vromero
1 parent 80e2d52 commit 14b970d

File tree

10 files changed

+231
-3
lines changed

10 files changed

+231
-3
lines changed

src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java

+5
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@ public enum LintCategory {
240240
*/
241241
OPTIONS("options"),
242242

243+
/**
244+
* Warn when any output file is written to more than once.
245+
*/
246+
OUTPUT_FILE_CLASH("output-file-clash"),
247+
243248
/**
244249
* Warn about issues regarding method overloads.
245250
*/

src/jdk.compiler/share/classes/com/sun/tools/javac/file/BaseFileManager.java

+41-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
import java.nio.charset.CodingErrorAction;
4242
import java.nio.charset.IllegalCharsetNameException;
4343
import java.nio.charset.UnsupportedCharsetException;
44+
import java.nio.file.Files;
45+
import java.nio.file.NoSuchFileException;
4446
import java.nio.file.Path;
4547
import java.util.Collection;
4648
import java.util.HashMap;
@@ -54,10 +56,12 @@
5456
import javax.tools.JavaFileObject;
5557
import javax.tools.JavaFileObject.Kind;
5658

59+
import com.sun.tools.javac.code.Lint.LintCategory;
5760
import com.sun.tools.javac.main.Option;
5861
import com.sun.tools.javac.main.OptionHelper;
5962
import com.sun.tools.javac.main.OptionHelper.GrumpyHelper;
6063
import com.sun.tools.javac.resources.CompilerProperties.Errors;
64+
import com.sun.tools.javac.resources.CompilerProperties.Warnings;
6165
import com.sun.tools.javac.util.Abort;
6266
import com.sun.tools.javac.util.Context;
6367
import com.sun.tools.javac.util.DefinedBy;
@@ -87,9 +91,12 @@ public void setContext(Context context) {
8791
options = Options.instance(context);
8892
classLoaderClass = options.get("procloader");
8993

90-
// Avoid initializing Lint
94+
// Detect Lint options, but use Options.isLintSet() to avoid initializing the Lint class
9195
boolean warn = options.isLintSet("path");
9296
locations.update(log, warn, FSInfo.instance(context));
97+
synchronized (this) {
98+
outputFilesWritten = options.isLintSet("output-file-clash") ? new HashSet<>() : null;
99+
}
93100

94101
// Setting this option is an indication that close() should defer actually closing
95102
// the file manager until after a specified period of inactivity.
@@ -133,6 +140,9 @@ protected Locations createLocations() {
133140

134141
protected final Locations locations;
135142

143+
// This is non-null when output file clash detection is enabled
144+
private HashSet<Path> outputFilesWritten;
145+
136146
/**
137147
* A flag for clients to use to indicate that this file manager should
138148
* be closed when it is no longer required.
@@ -467,6 +477,11 @@ public void flushCache(JavaFileObject file) {
467477
contentCache.remove(file);
468478
}
469479

480+
public synchronized void resetOutputFilesWritten() {
481+
if (outputFilesWritten != null)
482+
outputFilesWritten.clear();
483+
}
484+
470485
protected final Map<JavaFileObject, ContentCacheEntry> contentCache = new HashMap<>();
471486

472487
protected static class ContentCacheEntry {
@@ -512,4 +527,29 @@ protected static <T> Collection<T> nullCheck(Collection<T> it) {
512527
Objects.requireNonNull(t);
513528
return it;
514529
}
530+
531+
// Output File Clash Detection
532+
533+
/** Record the fact that we have started writing to an output file.
534+
*/
535+
// Note: individual files can be accessed concurrently, so we synchronize here
536+
synchronized void newOutputToPath(Path path) throws IOException {
537+
538+
// Is output file clash detection enabled?
539+
if (outputFilesWritten == null)
540+
return;
541+
542+
// Get the "canonical" version of the file's path; we are assuming
543+
// here that two clashing files will resolve to the same real path.
544+
Path realPath;
545+
try {
546+
realPath = path.toRealPath();
547+
} catch (NoSuchFileException e) {
548+
return; // should never happen except on broken filesystems
549+
}
550+
551+
// Check whether we've already opened this file for output
552+
if (!outputFilesWritten.add(realPath))
553+
log.warning(LintCategory.OUTPUT_FILE_CLASH, Warnings.OutputFileClash(path));
554+
}
515555
}

src/jdk.compiler/share/classes/com/sun/tools/javac/file/JavacFileManager.java

+1
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,7 @@ public void close() throws IOException {
738738
pathsAndContainersByLocationAndRelativeDirectory.clear();
739739
nonIndexingContainersByLocation.clear();
740740
contentCache.clear();
741+
resetOutputFilesWritten();
741742
}
742743

743744
@Override @DefinedBy(Api.COMPILER)

src/jdk.compiler/share/classes/com/sun/tools/javac/file/PathFileObject.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,9 @@ public OutputStream openOutputStream() throws IOException {
448448
fileManager.updateLastUsedTime();
449449
fileManager.flushCache(this);
450450
ensureParentDirectoriesExist();
451-
return Files.newOutputStream(path);
451+
OutputStream output = Files.newOutputStream(path);
452+
fileManager.newOutputToPath(path);
453+
return output;
452454
}
453455

454456
@Override @DefinedBy(Api.COMPILER)
@@ -483,7 +485,9 @@ public Writer openWriter() throws IOException {
483485
fileManager.updateLastUsedTime();
484486
fileManager.flushCache(this);
485487
ensureParentDirectoriesExist();
486-
return new OutputStreamWriter(Files.newOutputStream(path), fileManager.getEncodingName());
488+
Writer writer = new OutputStreamWriter(Files.newOutputStream(path), fileManager.getEncodingName());
489+
fileManager.newOutputToPath(path);
490+
return writer;
487491
}
488492

489493
@Override @DefinedBy(Api.COMPILER)

src/jdk.compiler/share/classes/com/sun/tools/javac/resources/compiler.properties

+4
Original file line numberDiff line numberDiff line change
@@ -1618,6 +1618,10 @@ compiler.misc.x.print.rounds=\
16181618
compiler.warn.file.from.future=\
16191619
Modification date is in the future for file {0}
16201620

1621+
# 0: path
1622+
compiler.warn.output.file.clash=\
1623+
output file written more than once: {0}
1624+
16211625
#####
16221626

16231627
## The following string will appear before all messages keyed as:

src/jdk.compiler/share/classes/com/sun/tools/javac/resources/javac.properties

+4
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,10 @@ javac.opt.Xlint.desc.opens=\
221221
javac.opt.Xlint.desc.options=\
222222
Warn about issues relating to use of command line options.
223223

224+
javac.opt.Xlint.desc.output-file-clash=\
225+
Warn when an output file is overwritten during compilation. This can occur, for example,\n\
226+
\ on case-insensitive filesystems. Covers class files, native header files, and source files.
227+
224228
javac.opt.Xlint.desc.overloads=\
225229
Warn about issues regarding method overloads.
226230

src/jdk.compiler/share/man/javac.1

+3
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,9 @@ constructors in public and protected classes in exported packages.
736736
\f[V]options\f[R]: Warns about the issues relating to use of command
737737
line options.
738738
.IP \[bu] 2
739+
\f[V]output-file-clash\f[R]: Warns if any output file is overwritten during compilation.
740+
This can occur, for example, on case-insensitive filesystems.
741+
.IP \[bu] 2
739742
\f[V]overloads\f[R]: Warns about the issues related to method overloads.
740743
.IP \[bu] 2
741744
\f[V]overrides\f[R]: Warns about the issues related to method overrides.

test/langtools/tools/javac/diags/examples.not-yet.txt

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ compiler.warn.incubating.modules # requires adjusted clas
118118
compiler.warn.invalid.archive.file
119119
compiler.warn.is.preview # difficult to produce reliably despite future changes to java.base
120120
compiler.warn.is.preview.reflective # difficult to produce reliably despite future changes to java.base
121+
compiler.warn.output.file.clash # this warning is not generated on Linux
121122
compiler.warn.override.bridge
122123
compiler.warn.position.overflow # CRTable: caused by files with long lines >= 1024 chars
123124
compiler.warn.proc.type.already.exists # JavacFiler: just mentioned in TODO
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/**
25+
* @test
26+
* @bug 8287885 8296656 7016187
27+
* @summary Verify proper function of the "output-file-clash" lint flag
28+
* @requires os.family == "mac"
29+
* @library /tools/lib
30+
* @modules jdk.compiler/com.sun.tools.javac.api
31+
* jdk.compiler/com.sun.tools.javac.main
32+
* jdk.compiler/com.sun.tools.javac.util
33+
* @build toolbox.ToolBox toolbox.JavacTask
34+
* @run main OutputFileClashTest
35+
*/
36+
37+
import java.io.IOException;
38+
import java.nio.file.Path;
39+
import java.nio.file.Paths;
40+
import java.util.List;
41+
import java.util.regex.Pattern;
42+
43+
import toolbox.TestRunner;
44+
import toolbox.ToolBox;
45+
import toolbox.JavacTask;
46+
import toolbox.Task;
47+
48+
public class OutputFileClashTest extends TestRunner {
49+
50+
protected ToolBox tb;
51+
52+
public OutputFileClashTest() {
53+
super(System.err);
54+
tb = new ToolBox();
55+
}
56+
57+
protected void runTests() throws Exception {
58+
runTests(m -> new Object[] { Paths.get(m.getName()) });
59+
}
60+
61+
Path[] findJavaFiles(Path... paths) throws IOException {
62+
return tb.findJavaFiles(paths);
63+
}
64+
65+
// Note: in these tests, it's indeterminate which output file gets written first.
66+
// So we compare the log output to a regex that matches the error either way.
67+
68+
@Test
69+
public void testBug8287885(Path base) throws Exception {
70+
testClash(base,
71+
"""
72+
public class Test {
73+
void method1() {
74+
enum ABC { A, B, C; }; // becomes "Test$1ABC.class"
75+
}
76+
void method2() {
77+
enum Abc { A, B, C; }; // becomes "Test$1Abc.class"
78+
}
79+
}
80+
""",
81+
"- compiler.warn.output.file.clash: .*Test\\$1(ABC|Abc)\\.class");
82+
}
83+
84+
@Test
85+
public void testBug8296656(Path base) throws Exception {
86+
testClash(base,
87+
"""
88+
public class Test {
89+
@interface Annotation {
90+
interface foo { }
91+
@interface Foo { }
92+
}
93+
}
94+
""",
95+
"- compiler.warn.output.file.clash: .*Test\\$Annotation\\$(foo|Foo)\\.class");
96+
}
97+
98+
@Test
99+
public void testCombiningAcuteAccent(Path base) throws Exception {
100+
testClash(base,
101+
"""
102+
public class Test {
103+
interface Cafe\u0301 { // macos normalizes "e" + U0301 -> U00e9
104+
}
105+
interface Caf\u00e9 {
106+
}
107+
}
108+
""",
109+
"- compiler.warn.output.file.clash: .*Test\\$Caf.*\\.class");
110+
}
111+
112+
private void testClash(Path base, String javaSource, String regex) throws Exception {
113+
114+
// Compile source
115+
Path src = base.resolve("src");
116+
tb.writeJavaFiles(src, javaSource);
117+
Path classes = base.resolve("classes");
118+
tb.createDirectories(classes);
119+
Path headers = base.resolve("headers");
120+
tb.createDirectories(headers);
121+
List<String> log = new JavacTask(tb, Task.Mode.CMDLINE)
122+
.options("-XDrawDiagnostics", "-Werror", "-Xlint:output-file-clash")
123+
.outdir(classes)
124+
.headerdir(headers)
125+
.files(findJavaFiles(src))
126+
.run(Task.Expect.FAIL)
127+
.writeAll()
128+
.getOutputLines(Task.OutputKind.DIRECT);
129+
130+
// Find expected error line
131+
Pattern pattern = Pattern.compile(regex);
132+
if (!log.stream().anyMatch(line -> pattern.matcher(line).matches()))
133+
throw new Exception("expected error not found: \"" + regex + "\"");
134+
}
135+
136+
public static void main(String... args) throws Exception {
137+
new OutputFileClashTest().runTests();
138+
}
139+
}

test/langtools/tools/lib/toolbox/JavacTask.java

+27
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public class JavacTask extends AbstractTask<JavacTask> {
5555
private List<Path> classpath;
5656
private List<Path> sourcepath;
5757
private Path outdir;
58+
private Path headerdir;
5859
private List<String> options;
5960
private List<String> classes;
6061
private List<String> files;
@@ -169,6 +170,26 @@ public JavacTask outdir(Path outdir) {
169170
return this;
170171
}
171172

173+
/**
174+
* Sets the native header output directory.
175+
* @param headerdir the native header output directory
176+
* @return this task object
177+
*/
178+
public JavacTask headerdir(String headerdir) {
179+
this.headerdir = Paths.get(headerdir);
180+
return this;
181+
}
182+
183+
/**
184+
* Sets the native header output directory.
185+
* @param headerdir the native header output directory
186+
* @return this task object
187+
*/
188+
public JavacTask headerdir(Path headerdir) {
189+
this.headerdir = headerdir;
190+
return this;
191+
}
192+
172193
/**
173194
* Sets the options.
174195
* @param options the options
@@ -353,6 +374,8 @@ private int runAPI(PrintWriter pw) throws IOException {
353374
fileManager = internalFileManager = compiler.getStandardFileManager(null, null, null);
354375
if (outdir != null)
355376
setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Collections.singletonList(outdir));
377+
if (headerdir != null)
378+
setLocationFromPaths(StandardLocation.NATIVE_HEADER_OUTPUT, Collections.singletonList(headerdir));
356379
if (classpath != null)
357380
setLocationFromPaths(StandardLocation.CLASS_PATH, classpath);
358381
if (sourcepath != null)
@@ -422,6 +445,10 @@ private List<String> getAllArgs() {
422445
args.add("-d");
423446
args.add(outdir.toString());
424447
}
448+
if (headerdir != null) {
449+
args.add("-h");
450+
args.add(headerdir.toString());
451+
}
425452
if (classpath != null) {
426453
args.add("-classpath");
427454
args.add(toSearchPath(classpath));

0 commit comments

Comments
 (0)