diff --git a/make/CompileJavaModules.gmk b/make/CompileJavaModules.gmk index 1b6e95d1afb..b585fb8d5db 100644 --- a/make/CompileJavaModules.gmk +++ b/make/CompileJavaModules.gmk @@ -29,6 +29,7 @@ include MakeFileStart.gmk include JavaCompilation.gmk include Modules.gmk +include ToolsJdk.gmk include CopyFiles.gmk @@ -68,6 +69,19 @@ MODULESOURCEPATH := $(call GetModuleSrcPath) # Add imported modules to the modulepath MODULEPATH := $(call PathList, $(IMPORT_MODULES_CLASSES)) +################################################################################ +# Setup preprocessor flags +# The output directory must be present in GENERATED_PREVIEW_SUBDIRS in Modules.gmk. +# Temporarily restrict this to java.base, but it can be expanded later. + +ifeq ($(MODULE), java.base) + PREPROCESSOR_FLAGS := \ + -Xplugin:"GenValueClassPlugin $(SUPPORT_OUTPUTDIR)/gensrc-valueclasses" + + PROCESSOR_PATH += $(VALUETYPE_GENSRC_PROCESSOR_PATH) + DEPENDS += $(BUILD_VALUETYPE_GENSRC) +endif + ################################################################################ # Copy zh_HK properties files from zh_TW (needed by some modules) @@ -107,6 +121,7 @@ $(eval $(call SetupJavaCompilation, $(MODULE), \ MODULE := $(MODULE), \ SRC := $(wildcard $(MODULE_SRC_DIRS)), \ INCLUDES := $(JDK_USER_DEFINED_FILTER), \ + DEPENDS := $(DEPENDS), \ FAIL_NO_SRC := $(FAIL_NO_SRC), \ BIN := $(COMPILATION_OUTPUTDIR), \ HEADERS := $(SUPPORT_OUTPUTDIR)/headers, \ @@ -120,9 +135,11 @@ $(eval $(call SetupJavaCompilation, $(MODULE), \ EXCLUDE_PATTERNS := -files, \ KEEP_ALL_TRANSLATIONS := $(KEEP_ALL_TRANSLATIONS), \ TARGET_RELEASE := $(TARGET_RELEASE), \ + PROCESSOR_PATH := $(PROCESSOR_PATH), \ JAVAC_FLAGS := \ $(DOCLINT) \ $(JAVAC_FLAGS) \ + $(PREPROCESSOR_FLAGS) \ --module-source-path $(MODULESOURCEPATH) \ --module-path $(MODULEPATH) \ --system none, \ diff --git a/make/CompileToolsJdk.gmk b/make/CompileToolsJdk.gmk index c291dbdba0a..62e59bee734 100644 --- a/make/CompileToolsJdk.gmk +++ b/make/CompileToolsJdk.gmk @@ -45,7 +45,8 @@ $(eval $(call SetupJavaCompilation, BUILD_TOOLS_JDK, \ build/tools/deps \ build/tools/docs \ build/tools/jigsaw \ - build/tools/depend, \ + build/tools/depend \ + build/tools/valhalla, \ BIN := $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes, \ DISABLED_WARNINGS := dangling-doc-comments options, \ JAVAC_FLAGS := \ @@ -155,4 +156,26 @@ endif ################################################################################ +$(eval $(call SetupJavaCompilation, BUILD_VALUETYPE_GENSRC, \ + TARGET_RELEASE := $(TARGET_RELEASE_BOOTJDK), \ + SRC := $(TOPDIR)/make/jdk/src/classes, \ + INCLUDES := build/tools/valhalla/valuetypes, \ + BIN := $(BUILDTOOLS_OUTPUTDIR)/valuetype_gensrc, \ + DISABLED_WARNINGS := options, \ + JAVAC_FLAGS := \ + --add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ + --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \ +)) + +VALUETYPE_GENSRC_SERVICE_PROVIDER := $(BUILDTOOLS_OUTPUTDIR)/valuetype_gensrc/META-INF/services/com.sun.source.util.Plugin + +$(VALUETYPE_GENSRC_SERVICE_PROVIDER): + $(call MakeDir, $(BUILDTOOLS_OUTPUTDIR)/valuetype_gensrc/META-INF/services) + $(ECHO) build.tools.valhalla.valuetypes.GenValueClassPlugin > $@ + +TARGETS += $(BUILD_VALUETYPE_GENSRC) $(VALUETYPE_GENSRC_SERVICE_PROVIDER) + +################################################################################ + include MakeFileEnd.gmk diff --git a/make/ToolsJdk.gmk b/make/ToolsJdk.gmk index b04d7820c91..07c066d561f 100644 --- a/make/ToolsJdk.gmk +++ b/make/ToolsJdk.gmk @@ -142,5 +142,16 @@ PANDOC_HTML_MANPAGE_FILTER := $(BUILDTOOLS_OUTPUTDIR)/manpages/pandoc-html-manpa ################################################################################ +# Annotation processor for generating preview sources of annotated value classes. + +VALUETYPE_GENSRC_PROCESSOR_NAME := build.tools.valhalla.valuetypes.GenValueClasses +VALUETYPE_GENSRC_PROCESSOR_PATH := $(BUILDTOOLS_OUTPUTDIR)/valuetype_gensrc + +# Same trick as BUILD_TOOLS_JDK but for the annotation processor. +BUILD_VALUETYPE_GENSRC := $(call SetupJavaCompilationCompileTarget, \ + BUILD_VALUETYPE_GENSRC, $(BUILDTOOLS_OUTPUTDIR)/valuetype_gensrc) + +################################################################################ + endif # include guard include MakeIncludeEnd.gmk diff --git a/make/common/JavaCompilation.gmk b/make/common/JavaCompilation.gmk index 33f5d10535a..f8e4434cbcb 100644 --- a/make/common/JavaCompilation.gmk +++ b/make/common/JavaCompilation.gmk @@ -161,7 +161,10 @@ endef # EXTRA_FILES List of extra source files to include in compilation. Can be used to # specify files that need to be generated by other rules first. # HEADERS path to directory where all generated c-headers are written. -# DEPENDS Extra dependency +# DEPENDS Extra dependencies +# PROCESSOR_PATH Annotation processor and plugin path (see --processor-path). +# Needed when specifying annotation processors not found via serivce discovery, +# and required for plugins when they are used alongside annotation processors. # KEEP_DUPS Do not remove duplicate file names from different source roots. # FAIL_NO_SRC Set to false to not fail the build if no source files are found, # default is true. @@ -288,6 +291,7 @@ define SetupJavaCompilationBody endif $1_AUGMENTED_CLASSPATH := $$($1_CLASSPATH) + $1_AUGMENTED_PROCESSOR_PATH := $$($1_PROCESSOR_PATH) $1_API_TARGET := $$($1_BIN)$$($1_MODULE_SUBDIR)/_the.$1_pubapi $1_API_INTERNAL := $$($1_BIN)$$($1_MODULE_SUBDIR)/_the.$1_internalapi @@ -302,13 +306,18 @@ define SetupJavaCompilationBody # including the compilation output on the classpath, so that incremental # compilations in unnamed module can refer to other classes from the same # source root, which are not being recompiled in this compilation: - $1_AUGMENTED_CLASSPATH += $$(BUILDTOOLS_OUTPUTDIR)/depend $$($1_BIN) + $1_AUGMENTED_CLASSPATH += $$($1_BIN) + $1_AUGMENTED_PROCESSOR_PATH += $$(BUILDTOOLS_OUTPUTDIR)/depend endif ifneq ($$($1_AUGMENTED_CLASSPATH), ) $1_FLAGS += -cp $$(call PathList, $$($1_AUGMENTED_CLASSPATH)) endif + ifneq ($$($1_AUGMENTED_PROCESSOR_PATH), ) + $1_FLAGS += --processor-path $$(call PathList, $$($1_AUGMENTED_PROCESSOR_PATH)) + endif + # Make sure the dirs exist, or that one of the EXTRA_FILES, that may not # exist yet, is in it. $$(foreach d, $$($1_SRC), \ diff --git a/make/jdk/src/classes/build/tools/valhalla/valuetypes/GenValueClassPlugin.java b/make/jdk/src/classes/build/tools/valhalla/valuetypes/GenValueClassPlugin.java new file mode 100644 index 00000000000..7346dbbc794 --- /dev/null +++ b/make/jdk/src/classes/build/tools/valhalla/valuetypes/GenValueClassPlugin.java @@ -0,0 +1,225 @@ +package build.tools.valhalla.valuetypes; + +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.util.JavacTask; +import com.sun.source.util.Plugin; +import com.sun.source.util.TaskEvent; +import com.sun.source.util.TaskListener; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.JCTree.JCClassDecl; +import com.sun.tools.javac.tree.TreeInfo; +import com.sun.tools.javac.tree.TreeScanner; + +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; + +/** + * Plugin for generating preview sources of classes annotated as value classes + * for preview mode. + * + *

Classes seen by this plugin (annotated with {@code @MigratedValueClass} + * will have their source files re-written into the specified output directory + * for compilation as preview classes. Note that more than one class in a given + * source file may be annotated. + * + *

Class re-writing is achieved by injecting the "value" keyword in front of + * class declarations for all annotated elements in the original source file. + * + *

Note that there are two annotations in use for value classes, but since + * we must generate sources for abstract classes, we only process one of them. + *

+ */ +public final class GenValueClassPlugin implements Plugin { + private static final String VALUE_CLASS_ANNOTATION = "@jdk.internal.MigratedValueClass"; + + @Override + public String getName() { + return "GenValueClassPlugin"; + } + + @Override + public void init(JavacTask task, String... args) { + if (args.length == 0) { + throw new IllegalArgumentException("Plugin " + getName() + ": missing output directory argument"); + } + Path outDir = Path.of(args[0]); + if (!Files.isDirectory(outDir)) { + throw new IllegalArgumentException("Plugin " + getName() + ": no such output directory: " + outDir); + } + task.addTaskListener(new ValueClassGenerator(outDir)); + } + + private record ValueClassGenerator(Path outDir) implements TaskListener { + @Override + public void finished(TaskEvent e) { + CompilationUnitTree compilation = e.getCompilationUnit(); + + List classes = new ArrayList<>(); + new TreeScanner() { + @Override + public void visitClassDef(JCClassDecl cls) { + boolean hasAnnotation = cls.getModifiers().getAnnotations().stream() + .peek(a -> System.out.println("--> " + a)) + .anyMatch(a -> a.toString().equals(VALUE_CLASS_ANNOTATION)); + if (hasAnnotation) { + classes.add(cls); + } + super.visitClassDef(cls); + } + }.scan((JCTree) compilation); + + if (!classes.isEmpty()) { + Path srcPath = filePath(compilation.getSourceFile()); + String moduleName = compilation.getModule().getName().toString(); + String packageName = compilation.getPackage().toString(); + generateValueClassSource(srcPath, moduleName, packageName, classes); + } + } + + /** + * Write a transformed version of the given Java source file with the + * {@code value} keyword inserted before the class declaration of each + * annotated type element. + */ + private void generateValueClassSource( + Path srcPath, String moduleName, String packageName, List classes) { + + System.out.println("Module: " + moduleName); + System.out.println("Package: " + packageName); + System.out.println("Classes: " + classes.stream().map(c -> c.type.toString()).toList()); + + Path relPath = moduleRelativePath(srcPath, packageName); + Path outPath = outDir.resolve(moduleName).resolve(relPath); + + System.out.println("Out path: " + outPath); + + try { + Files.createDirectories(outPath.getParent()); + + List insertPositions = + classes.stream().map(this::valueKeywordInsertPosition).sorted().toList(); + + System.out.println("Insert positions: " + insertPositions); + + // For partial rebuilds, generated sources may still exist, so we overwrite them. + try (Reader reader = new InputStreamReader(Files.newInputStream(srcPath)); + Writer output = new OutputStreamWriter( + Files.newOutputStream(outPath, CREATE, TRUNCATE_EXISTING))) { + int curPos = 0; + for (int nxtPos : insertPositions) { + int nextChunkLen = nxtPos - curPos; + long written = new LimitedReader(reader, nextChunkLen).transferTo(output); + if (written != nextChunkLen) { + throw new IOException("Unexpected number of characters transferred." + + " Expected " + nextChunkLen + " but was " + written); + } + curPos = nxtPos; + // curPos is at the end of the modifier section, so add a leading space. + // curPos ---v + // [modifiers] class... -->> [modifiers] value class... + output.write(" value"); + } + // Trailing section to end-of-file transferred from original reader. + reader.transferTo(output); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private Path moduleRelativePath(Path srcPath, String pkgName) { + Path relPath = Path.of(pkgName.replace('.', File.separatorChar)).resolve(srcPath.getFileName()); + if (!srcPath.endsWith(relPath)) { + throw new IllegalStateException(String.format( + "Expected trailing path %s for source file %s", relPath, srcPath)); + } + return relPath; + } + + /** + * Returns the character offset in the original source file at which to insert + * the {@code value} keyword. The offset is the end of the modifiers section, + * which must immediately precede the class declaration. + */ + private int valueKeywordInsertPosition(JCClassDecl classDecl) { + int pos = TreeInfo.getStartPos(classDecl.getModifiers()); + if (pos == Diagnostic.NOPOS) { + throw new IllegalStateException("Missing position information: " + classDecl); + } + return pos; + } + } + + private static Path filePath(FileObject file) { + return Path.of(file.toUri()); + } + + /** + * A forwarding reader which guarantees to read no more than + * {@code maxCharCount} characters from the underlying stream. + */ + private static final class LimitedReader extends Reader { + // These are short-lived, no need to null the delegate when closed. + private final Reader delegate; + // This should never go negative. + private int remainingChars; + + /** + * Creates a limited reader which reads up to {@code maxCharCount} chars + * from the given stream. + * + * @param delegate underlying reader + * @param maxCharCount maximum chars to read (can be 0) + */ + LimitedReader(Reader delegate, int maxCharCount) { + this.delegate = Objects.requireNonNull(delegate); + this.remainingChars = Math.max(maxCharCount, 0); + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + if (remainingChars > 0) { + int readLimit = Math.min(remainingChars, len); + int count = delegate.read(cbuf, off, readLimit); + // Only update remainingChars if something was read. + if (count > 0) { + if (count > remainingChars) { + throw new IOException( + "Underlying Reader exceeded requested read limit." + + " Expected at most " + readLimit + " but read " + count); + } + remainingChars -= count; + } + // Can return 0 or -1 here (the underlying reader could finish first). + return count; + } else if (remainingChars == 0) { + return -1; + } else { + throw new AssertionError("Remaining character count should never be negative!"); + } + } + + @Override + public void close() { + // Do not close the delegate since this is conceptually just a view. + } + } +} diff --git a/make/jdk/src/classes/build/tools/valhalla/valuetypes/GenValueClasses.java b/make/jdk/src/classes/build/tools/valhalla/valuetypes/GenValueClasses.java new file mode 100644 index 00000000000..a00857bb2af --- /dev/null +++ b/make/jdk/src/classes/build/tools/valhalla/valuetypes/GenValueClasses.java @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package build.tools.valhalla.valuetypes; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.util.TreePath; +import com.sun.source.util.Trees; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; +import static java.util.stream.Collectors.groupingBy; + +/** + * Annotation processor for generating preview sources of classes annotated as + * value classes for preview mode. + * + *

Classes seen by this processor (annotated with {@code @MigratedValueClass} + * will have their source files re-written into the specified output directory + * for compilation as preview classes. Note that more than one class in a given + * source file may be annotated. + * + *

Class re-writing is achieved by injecting the "value" keyword in front of + * class declarations for all annotated elements in the original source file. + * + *

Note that there are two annotations in use for value classes, but since + * we must generate sources for abstract classes, we only process one of them. + *

    + *
  • {@code @jdk.internal.ValueBased} appears on concrete value classes. + *
  • {@code @jdk.internal.MigratedValueClass} appears on concrete and + * abstract value classes. + *
+ */ +@SupportedAnnotationTypes(GenValueClasses.MIGRATED_VALUE_CLASS_ANNOTATION) +@SupportedOptions("valueclasses.outdir") +public final class GenValueClasses extends AbstractProcessor { + static final String MIGRATED_VALUE_CLASS_ANNOTATION = "jdk.internal.MigratedValueClass"; + + // Matches preprocessor option flag in CompileJavaModules.gmk. + private static final String OUTDIR_OPTION_KEY = "valueclasses.outdir"; + + private ProcessingEnvironment processingEnv = null; + private Path outDir = null; + private Trees trees = null; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + this.processingEnv = processingEnv; + String outDir = this.processingEnv.getOptions().get(OUTDIR_OPTION_KEY); + if (outDir != null) { + // No need to convert '/' for Windows in build tools. + this.outDir = Path.of(outDir); + this.trees = Trees.instance(this.processingEnv); + } else { + processingEnv.getMessager().printError( + "Must specify -A" + OUTDIR_OPTION_KEY + "="); + } + } + + /** + * Override to return latest version, since the runtime in which this is + * compiled doesn't know about development source versions. + */ + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment env) { + // Don't do anything if there was an error in init(). + if (outDir != null) { + // We don't have direct access to MigratedValueClass classes here. + Optional valueClassAnnotation = + getAnnotation(annotations, MIGRATED_VALUE_CLASS_ANNOTATION); + if (valueClassAnnotation.isPresent()) { + getAnnotatedTypes(env, valueClassAnnotation.get()).stream() + .collect(groupingBy(this::javaSourceFile)) + .forEach(this::generateValueClassSource); + } + } + // We may not be the only annotation processor to consume this annotation. + return false; + } + + /** Find the annotation element by name in the given set. */ + private static Optional getAnnotation( + Set annotations, String name) { + return annotations.stream() + .filter(e -> e.getQualifiedName().toString().equals(name)) + .findFirst(); + } + + /** Find the type elements (classes) annotated with the given annotation element. */ + private Set getAnnotatedTypes(RoundEnvironment env, TypeElement annotation) { + Set types = new HashSet<>(); + for (Element e : env.getElementsAnnotatedWith(annotation)) { + if (!e.getKind().isClass()) { + processingEnv.getMessager().printError("Unexpected element kind (" + e.getKind() + ")", e); + continue; + } + TypeElement type = (TypeElement) e; + if (type.getQualifiedName().isEmpty()) { + processingEnv.getMessager().printError("Unexpected empty name", e); + continue; + } + types.add(type); + } + return types; + } + + /** + * Write a transformed version of the given Java source file with the + * {@code value} keyword inserted before the class declaration of each + * annotated type element. + */ + private void generateValueClassSource(Path srcPath, List classes) { + // We know there's at least one element per source file (by construction). + TypeElement classElement = classes.getFirst(); + Path relPath = moduleRelativePath(srcPath, packageName(classElement)); + Path outPath = outDir.resolve(moduleName(classElement)).resolve(relPath); + try { + Files.createDirectories(outPath.getParent()); + + List insertPositions = + classes.stream().map(this::valueKeywordInsertPosition).sorted().toList(); + + // For partial rebuilds, generated sources may still exist, so we overwrite them. + try (Reader reader = new InputStreamReader(Files.newInputStream(srcPath)); + Writer output = new OutputStreamWriter( + Files.newOutputStream(outPath, CREATE, TRUNCATE_EXISTING))) { + long curPos = 0; + for (long nxtPos : insertPositions) { + int nextChunkLen = Math.toIntExact(nxtPos - curPos); + long written = new LimitedReader(reader, nextChunkLen).transferTo(output); + if (written != nextChunkLen) { + throw new IOException("Unexpected number of characters transferred." + + " Expected " + nextChunkLen + " but was " + written); + } + curPos = nxtPos; + // curPos is at the end of the modifier section, so add a leading space. + // curPos ---v + // [modifiers] class... -->> [modifiers] value class... + output.write(" value"); + } + // Trailing section to end-of-file transferred from original reader. + reader.transferTo(output); + } + } catch (IOException e) { + processingEnv.getMessager().printError("Failed to write value class source: " + outPath); + throw new RuntimeException(e); + } + } + + /** + * Returns the character offset in the original source file at which to insert + * the {@code value} keyword. The offset is the end of the modifiers section, + * which must immediately precede the class declaration. + */ + private long valueKeywordInsertPosition(TypeElement classElement) { + TreePath classDecl = trees.getPath(classElement); + ClassTree classTree = (ClassTree) classDecl.getLeaf(); + CompilationUnitTree compilationUnit = classDecl.getCompilationUnit(); + // Since annotations are held as "modifiers", and since we only process + // elements with annotations, the positions of the modifiers section must + // be well-defined. + long pos = trees.getSourcePositions().getEndPosition(compilationUnit, classTree.getModifiers()); + if (pos == Diagnostic.NOPOS) { + throw new IllegalStateException("Missing position information: " + classElement); + } + return pos; + } + + private Path moduleRelativePath(Path srcPath, String pkgName) { + Path relPath = Path.of(pkgName.replace('.', File.separatorChar)).resolve(srcPath.getFileName()); + if (!srcPath.endsWith(relPath)) { + throw new IllegalStateException(String.format( + "Expected trailing path %s for source file %s", relPath, srcPath)); + } + return relPath; + } + + private String moduleName(TypeElement type) { + return processingEnv.getElementUtils().getModuleOf(type).getQualifiedName().toString(); + } + + private String packageName(TypeElement type) { + return processingEnv.getElementUtils().getPackageOf(type).getQualifiedName().toString(); + } + + private Path javaSourceFile(TypeElement type) { + return filePath(processingEnv.getElementUtils().getFileObjectOf(type)); + } + + private static Path filePath(FileObject file) { + return Path.of(file.toUri()); + } + + /** + * A forwarding reader which guarantees to read no more than + * {@code maxCharCount} characters from the underlying stream. + */ + private static final class LimitedReader extends Reader { + // These are short-lived, no need to null the delegate when closed. + private final Reader delegate; + // This should never go negative. + private int remainingChars; + + /** + * Creates a limited reader which reads up to {@code maxCharCount} chars + * from the given stream. + * + * @param delegate underlying reader + * @param maxCharCount maximum chars to read (can be 0) + */ + LimitedReader(Reader delegate, int maxCharCount) { + this.delegate = Objects.requireNonNull(delegate); + this.remainingChars = Math.max(maxCharCount, 0); + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + if (remainingChars > 0) { + int readLimit = Math.min(remainingChars, len); + int count = delegate.read(cbuf, off, readLimit); + // Only update remainingChars if something was read. + if (count > 0) { + if (count > remainingChars) { + throw new IOException( + "Underlying Reader exceeded requested read limit." + + " Expected at most " + readLimit + " but read " + count); + } + remainingChars -= count; + } + // Can return 0 or -1 here (the underlying reader could finish first). + return count; + } else if (remainingChars == 0) { + return -1; + } else { + throw new AssertionError("Remaining character count should never be negative!"); + } + } + + @Override + public void close() { + // Do not close the delegate since this is conceptually just a view. + } + } +} diff --git a/make/modules/java.base/Gensrc.gmk b/make/modules/java.base/Gensrc.gmk index 0406c5b7033..e8236f0b0e4 100644 --- a/make/modules/java.base/Gensrc.gmk +++ b/make/modules/java.base/Gensrc.gmk @@ -37,7 +37,6 @@ include gensrc/GensrcMisc.gmk include gensrc/GensrcModuleLoaderMap.gmk include gensrc/GensrcRegex.gmk include gensrc/GensrcScopedMemoryAccess.gmk -include gensrc/GensrcValueClasses.gmk include gensrc/GensrcVarHandles.gmk ################################################################################ diff --git a/make/modules/java.base/gensrc/GensrcValueClasses.gmk b/make/modules/java.base/gensrc/GensrcValueClasses.gmk deleted file mode 100644 index 6a5b6864b67..00000000000 --- a/make/modules/java.base/gensrc/GensrcValueClasses.gmk +++ /dev/null @@ -1,76 +0,0 @@ -# -# Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. -# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. -# -# This code is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License version 2 only, as -# published by the Free Software Foundation. Oracle designates this -# particular file as subject to the "Classpath" exception as provided -# by Oracle in the LICENSE file that accompanied this code. -# -# This code 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 General Public License -# version 2 for more details (a copy is included in the LICENSE file that -# accompanied this code). -# -# You should have received a copy of the GNU General Public License version -# 2 along with this work; if not, write to the Free Software Foundation, -# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA -# or visit www.oracle.com if you need additional information or have any -# questions. -# - -################################################################################ -# Generate the value class replacements for selected java.base source files - -java.base-VALUE_CLASS-REPLACEMENTS := \ - java/lang/Byte.java \ - java/lang/Short.java \ - java/lang/Integer.java \ - java/lang/Long.java \ - java/lang/Float.java \ - java/lang/Double.java \ - java/lang/Boolean.java \ - java/lang/Character.java \ - java/lang/Number.java \ - java/lang/Record.java \ - java/util/Optional.java \ - java/util/OptionalInt.java \ - java/util/OptionalLong.java \ - java/util/OptionalDouble.java \ - java/time/LocalDate.java \ - java/time/LocalDateTime.java \ - java/time/LocalTime.java \ - java/time/Duration.java \ - java/time/Instant.java \ - java/time/MonthDay.java \ - java/time/ZonedDateTime.java \ - java/time/OffsetDateTime.java \ - java/time/OffsetTime.java \ - java/time/YearMonth.java \ - java/time/Year.java \ - java/time/Period.java \ - java/time/chrono/ChronoLocalDateImpl.java \ - java/time/chrono/MinguoDate.java \ - java/time/chrono/HijrahDate.java \ - java/time/chrono/JapaneseDate.java \ - java/time/chrono/ThaiBuddhistDate.java \ - # - -java.base-VALUE-CLASS-FILES := \ - $(foreach f, $(java.base-VALUE_CLASS-REPLACEMENTS), $(addprefix $(TOPDIR)/src/java.base/share/classes/, $(f))) - -$(eval $(call SetupTextFileProcessing, JAVA_BASE_VALUECLASS_REPLACEMENTS, \ - SOURCE_FILES := $(java.base-VALUE-CLASS-FILES), \ - SOURCE_BASE_DIR := $(TOPDIR)/src/java.base/share/classes, \ - OUTPUT_DIR := $(SUPPORT_OUTPUTDIR)/gensrc-valueclasses/java.base/, \ - REPLACEMENTS := \ - public final class => public final value class ; \ - public abstract class => public abstract value class ; \ - abstract class ChronoLocalDateImpl => abstract value class ChronoLocalDateImpl, \ -)) - -TARGETS += $(JAVA_BASE_VALUECLASS_REPLACEMENTS) diff --git a/test/jdk/valhalla/valuetypes/GenValueClassesTest.java b/test/jdk/valhalla/valuetypes/GenValueClassesTest.java new file mode 100644 index 00000000000..4e098b15391 --- /dev/null +++ b/test/jdk/valhalla/valuetypes/GenValueClassesTest.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.spi.ToolProvider; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test + * @summary Tests build tool GenValueClasses annotation processor + * @library /tools/lib + * @run junit GenValueClassesTest + */ +public class GenValueClassesTest { + private static final ToolProvider JAVAC_TOOL = ToolProvider.findFirst("javac") + .orElseThrow(() -> new RuntimeException("javac tool not found")); + + // We cannot access compiled build tools in JTREG tests, but we can find the + // sources and compile them. See findBuildToolsSrcRoot() for more details. + private static final Path SRC_ROOT = Path.of("make", "jdk", "src", "classes"); + private static final Path PROCESSOR_SRC = Path.of( + "build", "tools", "valhalla", "valuetypes", "GenValueClasses.java"); + + // Compile the annotation processor once for all test cases. + @TempDir + private static Path processorDir = null; + + @BeforeAll + static void compileAnnotationProcessor() { + compile(findBuildToolsSrcRoot().resolve(PROCESSOR_SRC), processorDir); + } + + @TempDir + Path testDir = null; + + @Test + public void simpleValueClass() throws IOException { + String simpleValueClass = + """ + package test; + + @jdk.internal.MigratedValueClass + public /*VALUE*/ class SimpleValueClass { + } + """; + Path relPath = writeTestSource("SimpleValueClass", simpleValueClass); + compileTestClass(relPath); + String transformedSrc = readTransformedSource(relPath); + assertEquals(simpleValueClass.replace("/*VALUE*/", "value"), transformedSrc); + assertTrue(transformedSrc.contains(" value class SimpleValueClass ")); + } + + @Test + public void nestedValueClass() throws IOException { + String nestedValueClass = + """ + package test; + + public class NestedValueClass { + @jdk.internal.MigratedValueClass + private static final /*VALUE*/ class Nested { + } + } + """; + Path relPath = writeTestSource("NestedValueClass", nestedValueClass); + compileTestClass(relPath); + String transformedSrc = readTransformedSource(relPath); + assertEquals(nestedValueClass.replace("/*VALUE*/", "value"), transformedSrc); + assertTrue(transformedSrc.contains(" value class Nested ")); + } + + @Test + public void multipleValueClasses() throws IOException { + String multipleValueClasses = + """ + package test; + + @jdk.internal.MigratedValueClass + public /*VALUE*/ class MultipleValueClasses { + + @jdk.internal.MigratedValueClass + private static final /*VALUE*/ class First { } + + static final class Second { } + + @jdk.internal.MigratedValueClass + private static /*VALUE*/ class Third { } + } + """; + + Path relPath = writeTestSource("MultipleValueClasses", multipleValueClasses); + compileTestClass(relPath); + String transformedSrc = readTransformedSource(relPath); + assertEquals(multipleValueClasses.replace("/*VALUE*/", "value"), transformedSrc); + assertTrue(transformedSrc.contains(" value class MultipleValueClasses ")); + assertTrue(transformedSrc.contains(" value class First ")); + assertFalse(transformedSrc.contains(" value class Second ")); + assertTrue(transformedSrc.contains(" value class Third ")); + } + + @Test + public void multilineClassDeclaration() throws IOException { + // A slightly extreme case to show that the annotation processor can cope + // with multiline class declarations and interleaved comments. The value + // keyword is inserted after the last modifier with a leading space. + String multilineClassDeclaration = + """ + package test; + + @jdk.internal.MigratedValueClass + public + /* Some comment */ + final /*VALUE*/ + /* Some other comment */ + class + + MultilineClassDeclaration { + } + """; + Path relPath = writeTestSource("MultilineClassDeclaration", multilineClassDeclaration); + compileTestClass(relPath); + String transformedSrc = readTransformedSource(relPath); + assertEquals(multilineClassDeclaration.replace("/*VALUE*/", "value"), transformedSrc); + } + + private Path writeTestSource(String className, String source) throws IOException { + // Remove the VALUE tokens to leave "clean" source (otherwise the + // token will persist in the transformed output and get messy). + // Tokens have a leading space to match how the keyword is injected. + assertTrue(source.contains(" /*VALUE*/"), "invalid test source"); + String actualSrc = source.replace(" /*VALUE*/", ""); + + Path relPath = Path.of("test", className + ".java"); + Path srcFile = testDir.resolve("src").resolve(relPath); + Files.createDirectories(srcFile.getParent()); + Files.writeString(srcFile, actualSrc); + return relPath; + } + + private String readTransformedSource(Path relPath) throws IOException { + return Files.readString(testDir.resolve("out").resolve(relPath)); + } + + private void compileTestClass(Path srcPath) { + compile(testDir.resolve("src").resolve(srcPath), processorDir.resolve("compiled"), + "--add-exports", "java.base/jdk.internal=ALL-UNNAMED", + "-Avalueclasses.outdir=" + testDir.resolve("out"), + "--processor-path", processorDir.toString(), + "-processor", "build.tools.valhalla.valuetypes.GenValueClasses"); + } + + // NOTE: Since the in-memory compiler is limited and doesn't support getting + // the Path of the "memo:" URIs is uses, it cannot be used for testing this + // annotation processor, so we must perform on-disk compilation. + private static void compile(Path srcFile, Path outDir, String... extraArgs) { + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + List allArgs = new ArrayList<>(); + allArgs.add("-d"); + allArgs.add(outDir.toString()); + allArgs.addAll(Arrays.asList(extraArgs)); + allArgs.add(srcFile.toString()); + int exitValue = JAVAC_TOOL.run( + new PrintWriter(out), + new PrintWriter(err), + allArgs.toArray(String[]::new)); + Assertions.assertEquals(0, exitValue, String.format( + """ + Compilation failed: %s + Stdout: %s + Stderr: %s + """, srcFile, out, err)); + } + + /** + * The source root is {@code make/jdk/src/classes} from the JDK root, but + * this may not be available in all test environments (and if it isn't, the + * test should be skipped). + * + *

Similar to {@code test/langtools/tools/all/RunCodingRules.java}, we + * attempt to locate this directory by walking from the {@code test.src} + * directory. + */ + private static Path findBuildToolsSrcRoot() { + Path testSrc = Path.of(System.getProperty("test.src", ".")); + for (Path d = testSrc; d != null; d = d.getParent()) { + if (Files.exists(d.resolve("TEST.ROOT"))) { + d = d.getParent(); + Path srcRoot = d.resolve(SRC_ROOT); + if (!Files.isDirectory(srcRoot)) { + break; + } + return srcRoot; + } + } + return Assumptions.abort("Build tools source root not found; skipping test"); + } +}