From ba44d40f429ecf14728e885c7d91529717d78ac6 Mon Sep 17 00:00:00 2001 From: Valentin Aebi Date: Fri, 4 Jul 2025 14:55:31 +0200 Subject: [PATCH] Native Image IDE reports Main Author: Valentin Aebi Co-authored-by: Tom Shull --- .../graal/compiler/ide/AcceptAllFilter.java | 41 ++ .../jdk/graal/compiler/ide/ClassFilter.java | 65 +++ .../graal/compiler/ide/CompositeFilter.java | 43 ++ .../src/jdk/graal/compiler/ide/IDEReport.java | 385 ++++++++++++++++++ .../jdk/graal/compiler/ide/PrefixFilter.java | 43 ++ .../ide/QualifiedStacktraceElement.java | 38 ++ .../compiler/nodes/InliningIDEReporting.java | 75 ++++ .../graal/compiler/nodes/StructuredGraph.java | 15 +- .../compiler/replacements/PEGraphDecoder.java | 8 +- substratevm/mx.substratevm/suite.py | 1 + .../pointsto/ide/AnalysisIDEReporting.java | 363 +++++++++++++++++ .../pointsto/results/StrengthenGraphs.java | 4 + .../pointsto/results/TypeFlowSimplifier.java | 31 ++ .../svm/hosted/NativeImageGenerator.java | 11 +- .../ClassInitializationSupport.java | 2 +- .../hosted/classinitialization/InitKind.java | 2 +- .../svm/hosted/ide/IDEReportingFeature.java | 131 ++++++ .../svm/hosted/meta/UniverseBuilder.java | 3 + .../hosted/snippets/ReflectionPlugins.java | 38 +- 19 files changed, 1284 insertions(+), 15 deletions(-) create mode 100644 compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/AcceptAllFilter.java create mode 100644 compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/ClassFilter.java create mode 100644 compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/CompositeFilter.java create mode 100644 compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/IDEReport.java create mode 100644 compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/PrefixFilter.java create mode 100644 compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/QualifiedStacktraceElement.java create mode 100644 compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InliningIDEReporting.java create mode 100644 substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/ide/AnalysisIDEReporting.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ide/IDEReportingFeature.java diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/AcceptAllFilter.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/AcceptAllFilter.java new file mode 100644 index 000000000000..ed68a4dad7e5 --- /dev/null +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/AcceptAllFilter.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025, 2025, 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 jdk.graal.compiler.ide; + +/** + * An implementation of {@link ClassFilter} that accepts all classes. + *

+ * This filter always returns {@code true} for any given class name, effectively allowing reports on + * all classes. + */ +public enum AcceptAllFilter implements ClassFilter { + INSTANCE; + + @Override + public boolean shouldBeReported(String className) { + return true; + } + +} diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/ClassFilter.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/ClassFilter.java new file mode 100644 index 000000000000..20f7547265b3 --- /dev/null +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/ClassFilter.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025, 2025, 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 jdk.graal.compiler.ide; + +import java.util.ArrayList; + +/** + * The ClassFilter interface provides a way to filter classes based on their names. Implementations + * of this interface can be used to determine whether a class should be reported or not. + * + * @see IDEReport + */ +public sealed interface ClassFilter permits PrefixFilter, CompositeFilter, AcceptAllFilter { + + /** + * Checks whether a class with the given name should be included in the reports. + * + * @param className the name of the class to check + * @return true if the class should be included in reports, false otherwise + */ + boolean shouldBeReported(String className); + + /** + * Creates a ClassFilter instance based on a filter description. The filter description is a + * comma-separated list of prefixes. + * + * @param filterDescr the filter description + * @return a ClassFilter instance that matches the given description + */ + static ClassFilter parseFilterDescr(String filterDescr) { + var individualFilterDescr = filterDescr.split(","); + var filters = new ArrayList(individualFilterDescr.length); + for (var descr : individualFilterDescr) { + filters.add(new PrefixFilter(descr)); + } + return switch (filters.size()) { + case 0 -> AcceptAllFilter.INSTANCE; + case 1 -> filters.getFirst(); + default -> new CompositeFilter(filters); + }; + } + +} diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/CompositeFilter.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/CompositeFilter.java new file mode 100644 index 000000000000..5ee46eb232cd --- /dev/null +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/CompositeFilter.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025, 2025, 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 jdk.graal.compiler.ide; + +import java.util.List; + +/** + * A composite {@link ClassFilter} that delegates the filtering decision to a list of underlying + * ClassFilters. A class is considered to be reported if at least one of the underlying filters + * returns true. + * + * @param filters the list of ClassFilters to be used for filtering + */ +public record CompositeFilter(List filters) implements ClassFilter { + + @Override + public boolean shouldBeReported(String className) { + return filters.stream().anyMatch(f -> f.shouldBeReported(className)); + } + +} diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/IDEReport.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/IDEReport.java new file mode 100644 index 000000000000..994e14750e01 --- /dev/null +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/IDEReport.java @@ -0,0 +1,385 @@ +/* + * Copyright (c) 2025, 2025, 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 jdk.graal.compiler.ide; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.graalvm.collections.EconomicMap; + +import jdk.graal.compiler.core.common.GraalOptions; +import jdk.graal.compiler.graph.NodeSourcePosition; +import jdk.graal.compiler.graph.SourceLanguagePosition; +import jdk.graal.compiler.options.Option; +import jdk.graal.compiler.options.OptionKey; +import jdk.graal.compiler.util.json.JsonFormatter; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaType; + +/** + * The IDEReport class is responsible for generating reports for IDE plugins. It provides methods to + * save various types of reports, such as line reports, unreachable range reports, class reports, + * field reports, and method reports. + *

+ * The reports are stored in a JSON file, which can be used by an IDE plugin to display the + * information to the user. + */ +public final class IDEReport { + + // TODO rename filename into filepath (also in IDE plugin) + + private static final String REPORT_KIND_K = "kind"; + private static final String FILENAME_K = "filename"; + private static final String LINE_K = "line"; + private static final String START_LINE_K = "start-line"; + private static final String END_LINE_K = "end-line"; + private static final String MSG_K = "msg"; + private static final String INLINE_CTX_K = "inlinectx"; + private static final String CLASS_K = "class"; + private static final String FIELD_K = "field"; + private static final String MTH_NAME_K = "mthname"; + private static final String MTH_SIG_K = "mthsig"; + + private static final String REPORT_KIND_LINE = "LINE"; + private static final String REPORT_KIND_UNREACHABLE_RANGE = "UNREACHABLE"; + private static final String REPORT_KIND_CLASS = "CLASS"; + private static final String REPORT_KIND_CLASS_FIELD = "FIELD"; + private static final String REPORT_KIND_METHOD = "METHOD"; + + private static final String REPORTS_LIST_TOPLEVEL_KEY = "reports"; + private static final String USED_METHODS_TOPLEVEL_KEY = "used_methods"; + + public static final class Options { + private Options() { + } + + @Option(help = "Print build report for the Native Image Intellij plugin.")// + public static final OptionKey IDEReport = new OptionKey<>(false) { + @Override + protected void onValueUpdate(EconomicMap, Object> values, Boolean oldValue, Boolean newValue) { + if (newValue) { + GraalOptions.TrackNodeSourcePosition.update(values, true); + } + } + }; + + @Option(help = "Print build report for the Native Image Intellij plugin for the specified set of files.")// + public static final OptionKey IDEReportFiltered = new OptionKey<>("") { + @Override + protected void onValueUpdate(EconomicMap, Object> values, String oldValue, String newValue) { + if (newValue != null) { + Options.IDEReport.update(values, true); + } + } + }; + + } + + private static IDEReport instance = null; + + private final ClassFilter filter; + + private final ConcurrentLinkedQueue> reportsCollector = new ConcurrentLinkedQueue<>(); + private final ConcurrentLinkedQueue> compiledMethodsCollector = new ConcurrentLinkedQueue<>(); + private final ConcurrentLinkedQueue> inlinedMethodsCollector = new ConcurrentLinkedQueue<>(); + + private IDEReport(ClassFilter filter) { + this.filter = filter; + } + + public static void createInstance(String filterDescr) { + instance = new IDEReport(ClassFilter.parseFilterDescr(filterDescr)); + } + + public static int getLineNumber(NodeSourcePosition nodeSourcePosition) { + SourceLanguagePosition sourceLanguage = nodeSourcePosition.getSourceLanguage(); + if (sourceLanguage == null) { + return nodeSourcePosition.getMethod().asStackTraceElement(nodeSourcePosition.getBCI()).getLineNumber(); + } + return sourceLanguage.getLineNumber(); + } + + public static String getFilePath(ResolvedJavaMethod mth) { + return getFilePath(mth.getDeclaringClass()); + } + + public static String getFilePath(ResolvedJavaType type) { + var srcFileName = type.getSourceFileName(); + if (srcFileName == null) { + return null; + } + var javaNameWithSlashes = type.toJavaName().replace('.', '/'); + var idxOfLastSlash = javaNameWithSlashes.lastIndexOf('/'); + return javaNameWithSlashes.substring(0, idxOfLastSlash + 1) + srcFileName; + } + + public static List getInliningTrace(NodeSourcePosition srcPos) { + if (srcPos == null || srcPos.getCaller() == null) { + return null; + } + var trace = new ArrayList(); + for (var callerPos = srcPos.getCaller(); callerPos != null; callerPos = callerPos.getCaller()) { + var filepath = getFilePath(callerPos.getMethod()); + if (filepath == null) { + filepath = ""; + } + trace.add(new QualifiedStacktraceElement(filepath, getLineNumber(callerPos))); + } + return trace; + } + + public static void runIfEnabled(Consumer action) { + if (instance != null) { + action.accept(instance); + } + } + + /** + * Use {@link IDEReport#isEnabled()} only in situations where + * {@link IDEReport#runIfEnabled(Consumer)} cannot be used instead + */ + public static boolean isEnabled() { + return instance != null; + } + + /** + * Saves a report specific to the given source code line. This is a generic method that can be + * used for inserting arbitrary reports. + */ + public void saveLineReport(String filepath, String fullyQualifiedClassName, int line, String msg, List inliningTrace) { + if (anyIsNull(filepath, msg) || anyIsNonPositive(line) || !shouldReportClass(fullyQualifiedClassName)) { + return; + } + msg = preprocessMsg(msg); + var data = new HashMap(); + data.put(REPORT_KIND_K, REPORT_KIND_LINE); + data.put(FILENAME_K, filepath); + data.put(LINE_K, line); + data.put(MSG_K, msg); + terminateAndSaveReport(data, inliningTrace); + } + + /** + * Saves a report indicating that the given range of source code lines is unreachable. + */ + public void saveUnreachableRangeReport(String filepath, String fullyQualifiedClassName, int startLine, int endLine, String msg, List inliningTrace) { + if (anyIsNull(filepath, msg) || anyIsNonPositive(startLine, endLine) || !shouldReportClass(fullyQualifiedClassName)) { + return; + } + msg = preprocessMsg(msg); + var data = new HashMap(); + data.put(REPORT_KIND_K, REPORT_KIND_UNREACHABLE_RANGE); + data.put(FILENAME_K, filepath); + data.put(START_LINE_K, startLine); + data.put(END_LINE_K, endLine); + data.put(MSG_K, msg); + terminateAndSaveReport(data, inliningTrace); + } + + /** + * Saves a class-level report, used, e.g., to present information about class initialization. + */ + public void saveClassReport(String filepath, String fullyQualifiedClassName, String msg) { + if (anyIsNull(filepath, fullyQualifiedClassName) || !shouldReportClass(fullyQualifiedClassName)) { + return; + } + msg = preprocessMsg(msg); + var data = new HashMap(); + data.put(REPORT_KIND_K, REPORT_KIND_CLASS); + data.put(FILENAME_K, filepath); + data.put(CLASS_K, fullyQualifiedClassName); + data.put(MSG_K, msg); + terminateAndSaveReport(data, null); + } + + /** + * Saves a field report that can be used, e.g., to specify that a field has a constant value. + */ + public void saveFieldReport(String filepath, String fullyQualifiedClassName, String fieldName, String msg) { + if (anyIsNull(filepath, fullyQualifiedClassName, fieldName, msg) || !shouldReportClass(fullyQualifiedClassName)) { + return; + } + msg = preprocessMsg(msg); + var data = new HashMap(); + data.put(REPORT_KIND_K, REPORT_KIND_CLASS_FIELD); + data.put(FILENAME_K, filepath); + data.put(CLASS_K, fullyQualifiedClassName); + data.put(FIELD_K, fieldName); + data.put(MSG_K, msg); + terminateAndSaveReport(data, null); + } + + public void saveMethodReport(String filepath, String fullyQualifiedClassName, String methodName, String methodSignature, String msg) { + if (anyIsNull(filepath, fullyQualifiedClassName, methodName, methodSignature) || !shouldReportClass(fullyQualifiedClassName)) { + return; + } + msg = preprocessMsg(msg); + var data = new HashMap(); + data.put(REPORT_KIND_K, REPORT_KIND_METHOD); + data.put(FILENAME_K, filepath); + data.put(CLASS_K, fullyQualifiedClassName); + data.put(MTH_NAME_K, methodName); + data.put(MTH_SIG_K, methodSignature); + data.put(MSG_K, msg); + terminateAndSaveReport(data, null); + } + + public void saveMethodCompiled(String filepath, String fullyQualifiedClassName, String methodName, String methodSignature) { + if (anyIsNull(filepath, fullyQualifiedClassName, methodName, methodSignature) || !shouldReportClass(fullyQualifiedClassName)) { + return; + } + var data = new HashMap(); + data.put(FILENAME_K, filepath); + data.put(CLASS_K, fullyQualifiedClassName); + data.put(MTH_NAME_K, methodName); + data.put(MTH_SIG_K, methodSignature); + compiledMethodsCollector.add(data); + } + + public void saveMethodInlined(String filepath, String fullyQualifiedClassName, String methodName, String methodSignature) { + if (anyIsNull(filepath, fullyQualifiedClassName, methodName, methodSignature) || !shouldReportClass(fullyQualifiedClassName)) { + return; + } + var data = new HashMap(); + data.put(FILENAME_K, filepath); + data.put(CLASS_K, fullyQualifiedClassName); + data.put(MTH_NAME_K, methodName); + data.put(MTH_SIG_K, methodSignature); + inlinedMethodsCollector.add(data); + } + + private void terminateAndSaveReport(Map data, List inliningTrace) { + if (inliningTrace != null) { + data.put(INLINE_CTX_K, mkInlineCtxJson(inliningTrace)); + } + reportsCollector.add(data); + } + + private boolean shouldReportClass(String qualifiedClassName) { + return filter == null || filter.shouldBeReported(qualifiedClassName); + } + + /** + * Prints the reports to a JSON file. + * + * @param reportsDir the directory to write the reports to + */ + public void print(Path reportsDir) { + var reportName = "native_image_ide_report_" + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME).replace(':', '-') + ".json"; + var usedMethods = compiledMethodsCollector.stream().distinct().collect(Collectors.toList()); + var inlinedOnlyMethods = new HashSet<>(inlinedMethodsCollector); + usedMethods.forEach(inlinedOnlyMethods::remove); + for (var inlinedOnlyMethodReportData : inlinedOnlyMethods) { + var methodName = inlinedOnlyMethodReportData.get(MTH_NAME_K); + var report = new HashMap<>(inlinedOnlyMethodReportData); + report.put(REPORT_KIND_K, REPORT_KIND_METHOD); + report.put(MSG_K, "Method " + methodName + " is not part of the produced image since all its invocations are inlined"); + reportsCollector.add(report); + } + usedMethods.addAll(inlinedOnlyMethods); + var uniqueReports = reportsCollector.stream().distinct().toList(); + var allData = EconomicMap.of( + REPORTS_LIST_TOPLEVEL_KEY, uniqueReports, + USED_METHODS_TOPLEVEL_KEY, usedMethods); + var json = JsonFormatter.formatJsonPretty(allData); + var path = reportsDir.resolve(reportName); + try { + Files.createDirectories(reportsDir); + } catch (IOException e) { + throw new RuntimeException(e); + } + try (var fw = new FileWriter(path.toFile()); var pw = new PrintWriter(fw)) { + pw.println(json); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private List> mkInlineCtxJson(List stackTraceElements) { + var result = new ArrayList>(); + for (var ste : stackTraceElements) { + result.add(Map.of( + FILENAME_K, ste.qualifiedFileName(), + LINE_K, ste.line())); + } + return result; + } + + private static String preprocessMsg(String msg) { + return mkEndlExplicit(replaceSpecialMethodNames(msg)); + } + + private static String mkEndlExplicit(String msg) { + var sb = new StringBuilder(); + for (int i = 0; i < msg.length(); i++) { + var c = msg.charAt(i); + if (c == '\n') { + sb.append("\\n"); + } else if (c == '\r') { + sb.append("\\r"); + } else { + sb.append(c); + } + } + return sb.toString(); + } + + private static String replaceSpecialMethodNames(String rawMsg) { + return rawMsg.replace("", "constructor") + .replace("", "class initializer"); + } + + public static boolean anyIsNull(Object... objects) { + for (Object object : objects) { + if (object == null) { + return true; + } + } + return false; + } + + public static boolean anyIsNonPositive(int... numbers) { + for (int number : numbers) { + if (number <= 0) { + return true; + } + } + return false; + } + +} diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/PrefixFilter.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/PrefixFilter.java new file mode 100644 index 000000000000..47eb009e2ba5 --- /dev/null +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/PrefixFilter.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025, 2025, 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 jdk.graal.compiler.ide; + +/** + * A {@link ClassFilter} implementation that filters classes based on whether their names start with + * a given prefix. + *

+ * This filter is case-sensitive and considers the entire package name as part of the class name. + *

+ * Instances of this class are immutable and can be safely reused. + * + * @param prefix the prefix to match against class names + */ +public record PrefixFilter(String prefix) implements ClassFilter { + + @Override + public boolean shouldBeReported(String className) { + return className.startsWith(prefix()); + } +} diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/QualifiedStacktraceElement.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/QualifiedStacktraceElement.java new file mode 100644 index 000000000000..eac200864381 --- /dev/null +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/ide/QualifiedStacktraceElement.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025, 2025, 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 jdk.graal.compiler.ide; + +/** + * Represents a qualified stack trace element, containing information about the file name and line + * number. + *

+ * This record is used to provide a concise representation of a stack trace element, with the file + * name being fully qualified. + * + * @param qualifiedFileName the fully qualified name of the file + * @param line the line number within the file + */ +public record QualifiedStacktraceElement(String qualifiedFileName, int line) { +} diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InliningIDEReporting.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InliningIDEReporting.java new file mode 100644 index 000000000000..536dfdea85a8 --- /dev/null +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/InliningIDEReporting.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025, 2025, 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 jdk.graal.compiler.nodes; + +import static jdk.graal.compiler.ide.IDEReport.getFilePath; +import static jdk.graal.compiler.ide.IDEReport.getInliningTrace; +import static jdk.graal.compiler.ide.IDEReport.getLineNumber; +import static jdk.graal.compiler.ide.IDEReport.runIfEnabled; + +import jdk.graal.compiler.graph.NodeSourcePosition; +import jdk.graal.compiler.ide.IDEReport; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +/** + * Provides methods for reporting inlining decisions to the IDE. + */ +public final class InliningIDEReporting { + + private InliningIDEReporting() { + } + + /** + * Reports inlining of a method to the IDE. + * + * @param srcPos the source position of the inlined method call + * @param callee the method being inlined + * @param phase the phase during which the inlining occurred + */ + public static void reportInlining(NodeSourcePosition srcPos, ResolvedJavaMethod callee, String phase) { + var calleeName = callee.getName(); + if (calleeName == null){ + return; + } + IDEReport.runIfEnabled(ideReport -> { + var calleeDeclFilePath = getFilePath(callee.getDeclaringClass()); + if (calleeDeclFilePath != null) { + ideReport.saveMethodInlined(calleeDeclFilePath, callee.getDeclaringClass().toJavaName(), calleeName, callee.getSignature().toMethodDescriptor()); + } + }); + if (srcPos == null || srcPos.getMethod() == null) { + return; + } + runIfEnabled(ideReport -> { + var invocationSiteFilePath = getFilePath(srcPos.getMethod().getDeclaringClass()); + if (invocationSiteFilePath != null) { + var className = srcPos.getMethod().getDeclaringClass().toJavaName(); + var line = getLineNumber(srcPos); + ideReport.saveLineReport(invocationSiteFilePath, className, line, "Call to " + calleeName + " is inlined by phase " + phase, getInliningTrace(srcPos)); + } + }); + } + +} diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/StructuredGraph.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/StructuredGraph.java index ffeaf0c0e350..341b860f7b82 100644 --- a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/StructuredGraph.java +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/StructuredGraph.java @@ -691,13 +691,13 @@ public InliningLog getInliningLog() { } /** - * Notifies this graph of an inlining decision for {@code invoke}. + * Notifies this graph of an inlining decision for {@code invokable}. * * An inlining decision can be either positive or negative. A positive inlining decision must be * logged after replacing an {@link Invoke} with a graph. In this case, the node replacement map * and the {@link InliningLog} of the inlined graph must be provided. * - * @param invoke the invocation to which the inlining decision pertains + * @param invokable the invocation to which the inlining decision pertains * @param positive {@code true} if the invocation was inlined, {@code false} otherwise * @param phase name of the phase doing the inlining * @param replacements the node replacement map used by inlining, ignored if @@ -709,19 +709,22 @@ public InliningLog getInliningLog() { * @param inlineeMethod the actual method considered for inlining * @param reason format string that along with {@code args} provides the reason for decision */ - public void notifyInliningDecision(Invokable invoke, boolean positive, String phase, EconomicMap replacements, + public void notifyInliningDecision(Invokable invokable, boolean positive, String phase, EconomicMap replacements, InliningLog calleeInliningLog, OptimizationLog calleeOptimizationLog, ResolvedJavaMethod inlineeMethod, String reason, Object... args) { if (inliningLog != null) { - inliningLog.addDecision(invoke, positive, phase, replacements, calleeInliningLog, inlineeMethod, reason, args); + inliningLog.addDecision(invokable, positive, phase, replacements, calleeInliningLog, inlineeMethod, reason, args); } if (positive && calleeOptimizationLog != null && optimizationLog.isStructuredOptimizationLogEnabled()) { - FixedNode invokeNode = invoke.asFixedNodeOrNull(); + FixedNode invokeNode = invokable.asFixedNodeOrNull(); optimizationLog.inline(calleeOptimizationLog, true, invokeNode == null ? null : invokeNode.getNodeSourcePosition()); } if (getDebug().hasCompilationListener()) { String message = String.format(reason, args); - getDebug().notifyInlining(invoke.getContextMethod(), inlineeMethod, positive, message, invoke.bci()); + getDebug().notifyInlining(invokable.getContextMethod(), inlineeMethod, positive, message, invokable.bci()); + } + if (positive && invokable instanceof Invoke invoke) { + InliningIDEReporting.reportInlining(invoke.asNode().getNodeSourcePosition(), invokable.getTargetMethod(), phase); } } diff --git a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/replacements/PEGraphDecoder.java b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/replacements/PEGraphDecoder.java index b7032d3c8e26..ab380a5506e0 100644 --- a/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/replacements/PEGraphDecoder.java +++ b/compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/replacements/PEGraphDecoder.java @@ -41,6 +41,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import jdk.graal.compiler.nodes.InliningIDEReporting; import org.graalvm.collections.Pair; import jdk.graal.compiler.api.replacements.Fold; @@ -1445,15 +1446,18 @@ protected void finishInlining(MethodScope is) { plugin.notifyAfterInline(inlineMethod); } + final var phase = "PEGraphDecoder"; if (methodScope.inliningLog != null) { assert inlineScope.inliningLog != null : "all inlinees should have an inlining log if the root requires it"; - methodScope.inliningLog.inlineByTransfer(invoke, invokeData.callTarget, inlineScope.inliningLog, "PEGraphDecoder", - "inlined during decoding"); + methodScope.inliningLog.inlineByTransfer(invoke, invokeData.callTarget, inlineScope.inliningLog, phase, + "inlined during decoding"); } if (methodScope.optimizationLog != null) { assert inlineScope.optimizationLog != null : "all inlinees should have an optimization log if the root requires it"; methodScope.optimizationLog.inline(inlineScope.optimizationLog, false, null); } + + InliningIDEReporting.reportInlining(invokeNode.getNodeSourcePosition(), invokeData.callTarget.targetMethod(), phase); } @SuppressWarnings("unchecked") diff --git a/substratevm/mx.substratevm/suite.py b/substratevm/mx.substratevm/suite.py index f96d232bf879..de494ea28f5c 100644 --- a/substratevm/mx.substratevm/suite.py +++ b/substratevm/mx.substratevm/suite.py @@ -2404,6 +2404,7 @@ "com.oracle.graal.pointsto.util", "com.oracle.graal.pointsto.meta", "com.oracle.graal.pointsto.flow", + "com.oracle.graal.pointsto.ide", "com.oracle.graal.pointsto.flow.builder", "com.oracle.graal.pointsto.nodes", "com.oracle.graal.pointsto.phases", diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/ide/AnalysisIDEReporting.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/ide/AnalysisIDEReporting.java new file mode 100644 index 000000000000..7eaefbbee370 --- /dev/null +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/ide/AnalysisIDEReporting.java @@ -0,0 +1,363 @@ +/* + * Copyright (c) 2025, 2025, 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 com.oracle.graal.pointsto.ide; + +import static jdk.graal.compiler.ide.IDEReport.getFilePath; +import static jdk.graal.compiler.ide.IDEReport.getInliningTrace; +import static jdk.graal.compiler.ide.IDEReport.getLineNumber; +import static jdk.graal.compiler.ide.IDEReport.runIfEnabled; + +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import com.oracle.graal.pointsto.flow.TypeFlow; +import com.oracle.graal.pointsto.meta.AnalysisField; +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.graal.pointsto.meta.AnalysisUniverse; +import com.oracle.graal.pointsto.meta.PointsToAnalysisField; +import com.oracle.graal.pointsto.meta.PointsToAnalysisMethod; +import com.oracle.graal.pointsto.typestate.PrimitiveConstantTypeState; + +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.ide.IDEReport; +import jdk.graal.compiler.nodes.AbstractBeginNode; +import jdk.graal.compiler.nodes.IfNode; +import jdk.graal.compiler.nodes.Invoke; +import jdk.graal.compiler.nodes.InvokeWithExceptionNode; +import jdk.vm.ci.meta.JavaKind; +import jdk.vm.ci.meta.JavaType; +import jdk.vm.ci.meta.ResolvedJavaMethod; + +/** + * Utility class providing methods for reporting analysis results to an IDE. + *

+ * The methods in this class are used to report various analysis results, such as unreachable code, + * devirtualized invocations, and constant parameter and return types, to an IDE. + * + * @see IDEReport + */ +public class AnalysisIDEReporting { + + private AnalysisIDEReporting() { + } + + /** + * Reports an unreachable branch in the given {@link IfNode}. + * + * @param ifNode the IfNode containing the unreachable branch + * @param unreachableBranch the unreachable branch node + */ + public static void reportUnreachableBranch(IfNode ifNode, AbstractBeginNode unreachableBranch, boolean isNullCheck, boolean isTypeCheck) { + var isExplicitCheck = !isImplicitCheck(ifNode); + if (isExplicitCheck || (!isNullCheck && !isTypeCheck)) { + runIfEnabled(ideReport -> { + var srcPos = ifNode.getNodeSourcePosition(); + if (srcPos == null) { + return; + } + var srcFile = getFilePath(srcPos.getMethod()); + var range = rangeOfBranch(unreachableBranch, ifNode); + if (range != null) { + var className = srcPos.getMethod().getDeclaringClass().toJavaName(); + ideReport.saveUnreachableRangeReport(srcFile, className, range.startLine(), range.endLine(), "Branch is unreachable", getInliningTrace(srcPos)); + } + }); + } + } + + /** + * Reports an unreachable node in the graph. + * + * @param unreachableNode the unreachable node + */ + public static void reportUnreachableNode(Node unreachableNode) { + if (unreachableNode == null) { + return; + } + runIfEnabled(ideReport -> { + var srcPos = unreachableNode.getNodeSourcePosition(); + if (srcPos == null) { + return; + } + var srcFile = getFilePath(srcPos.getMethod()); + var range = rangeOfBranch(unreachableNode, unreachableNode.graph().getNode(0)); + if (range != null) { + var className = srcPos.getMethod().getDeclaringClass().toJavaName(); + ideReport.saveUnreachableRangeReport(srcFile, className, range.startLine(), range.endLine(), "Code is unreachable", getInliningTrace(srcPos)); + } + }); + } + + /** + * Reports a devirtualized method invocation. + * + * @param method the method containing the invocation + * @param invoke the invocation node + * @param singleCallee the single callee method + */ + public static void reportDevirtualizedInvoke(PointsToAnalysisMethod method, Invoke invoke, AnalysisMethod singleCallee) { + runIfEnabled(ideReport -> { + var targetMethod = invoke.getTargetMethod(); + if (!(targetMethod instanceof AnalysisMethod)) { + targetMethod = method.getUniverse().lookup(targetMethod); + } + if (singleCallee.equals(targetMethod)) { + /* Report would add no additional value in this case */ + return; + } + var srcFile = getFilePath(invoke.getContextMethod()); + var nodeSourcePosition = ((InvokeWithExceptionNode) invoke).getNodeSourcePosition(); + var className = nodeSourcePosition.getMethod().getDeclaringClass().toJavaName(); + int line = getLineNumber(nodeSourcePosition); + ideReport.saveLineReport(srcFile, className, line, "Invocation of " + singleCallee.getName() + " has been devirtualized to " + singleCallee.getQualifiedName(), + getInliningTrace(nodeSourcePosition)); + }); + } + + /** + * Reports any constant parameter and return types for the given method. + * + * @param m the method to report on + */ + public static void reportConstantParamAndReturnTypes(AnalysisMethod m) { + if (!(m instanceof PointsToAnalysisMethod method)) { + /* We only support reports from points-to analysis at the moment. */ + return; + } + var lineNumberTable = method.getLineNumberTable(); + if (lineNumberTable == null) { + return; + } + runIfEnabled(ideReport -> { + var line = lineNumberTable.getLineNumbers()[0] - 1; + var filename = getFilePath(method); + var paramFlows = method.getTypeFlow().getMethodFlowsGraph().getParameters(); + ResolvedJavaMethod.Parameter[] params = null; + try { + params = method.getParameters(); + } catch (UnsupportedOperationException | NoClassDefFoundError ignored) { + /* Ignore errors coming, e.g., from NonBytecodeMethods, and linking errors. */ + } + boolean shiftParamsToAccountForReceiver; + if (params == null || params.length == paramFlows.length) { + shiftParamsToAccountForReceiver = false; + } else if (params.length == paramFlows.length - 1) { + shiftParamsToAccountForReceiver = true; + } else { + params = null; + shiftParamsToAccountForReceiver = false; + } + for (var i = 0; i < paramFlows.length; i++) { + var paramFlow = paramFlows[i]; + if (paramFlow != null) { + String paramDescr; + if (i == 0 && shiftParamsToAccountForReceiver) { + paramDescr = "Receiver"; + } else { + var paramIdx = shiftParamsToAccountForReceiver ? i - 1 : i; + var paramName = params != null ? params[paramIdx].getName() : ("arg" + paramIdx); + paramDescr = "Parameter " + paramName; + } + reportParamOrReturnType(paramFlow, paramDescr, method, filename, method.getQualifiedName(), line, ideReport); + } + } + reportParamOrReturnType(method.getTypeFlow().getReturn(), "Return value", method, filename, method.getQualifiedName(), line, ideReport); + }); + } + + /** + * Reports possible return types for the given invocation. + * + * @param invoke the invocation node + * @param callees the possible callee methods + */ + public static void reportPossibleReturnTypes(Invoke invoke, Collection callees) { + runIfEnabled(ideReport -> { + var possibleReturnValues = new HashSet(); + var possibleReturnTypes = new HashSet(); + foreachCallee: for (var calleeRaw : callees) { + var callee = (PointsToAnalysisMethod) calleeRaw; + var returnFlow = callee.getTypeFlow().getReturn(); + if (returnFlow == null) { + continue foreachCallee; + } + var constValue = extractConstant(returnFlow); + if (constValue != null) { + possibleReturnValues.add(constValue); + } else if (!returnFlow.isPrimitiveFlow()) { + var exactType = returnFlow.getState().exactType(); + if (exactType == null) { + continue foreachCallee; + } + possibleReturnTypes.add(exactType); + } + } + var staticReturnType = invoke.getTargetMethod().getSignature().getReturnType(invoke.getContextType()); + possibleReturnTypes.removeIf(rt -> rt.toJavaName().equals(staticReturnType.toJavaName())); + var filePath = getFilePath(invoke.getContextMethod()); + var srcPos = invoke.asNode().getNodeSourcePosition(); + var line = getLineNumber(srcPos); + var mthName = invoke.getTargetMethod().getName(); + String msg; + if (possibleReturnValues.size() == 1) { + msg = mthName + " always returns " + possibleReturnValues.iterator().next(); + } else if (possibleReturnTypes.size() == 1) { + msg = mthName + " always returns a value of type " + possibleReturnTypes.iterator().next().toJavaName(); + } else if (!possibleReturnValues.isEmpty() && possibleReturnTypes.isEmpty()) { + msg = mthName + " always returns one of the following values: " + String.join(", ", possibleReturnValues); + } else if (!possibleReturnTypes.isEmpty()) { + msg = mthName + " always returns a value of one of the following types: " + new TreeSet<>(possibleReturnTypes).stream().map(AnalysisType::toJavaName).collect(Collectors.joining(", ")); + } else { + return; + } + var className = srcPos.getMethod().getDeclaringClass().toJavaName(); + ideReport.saveLineReport(filePath, className, line, msg, getInliningTrace(srcPos)); + }); + } + + private static void reportParamOrReturnType(TypeFlow paramOrReturnFlow, String flowPositionDescr, PointsToAnalysisMethod mth, String filename, String className, int line, IDEReport ideReport) { + if (paramOrReturnFlow == null) { + return; + } + var constValue = extractConstant(paramOrReturnFlow); + if (constValue != null) { + ideReport.saveLineReport(filename, className, line, flowPositionDescr + " of method " + mth.getName() + " is constant: " + constValue, null); + } else if (!paramOrReturnFlow.isPrimitiveFlow()) { + var staticType = resolveToAnalysisType(paramOrReturnFlow.getDeclaredType(), mth.getUniverse()); + var exactType = paramOrReturnFlow.getState().exactType(); + if (exactType != null && !exactType.equals(staticType)) { + ideReport.saveLineReport(filename, className, line, flowPositionDescr + " of method " + mth.getName() + " has actual type " + exactType.toJavaName(), null); + } + } + } + + /** + * Reports a constant field. + * + * @param field the field to report on + */ + public static void maybeReportConstantField(AnalysisField field) { + runIfEnabled(ideReport -> { + var fieldSinkFlow = ((PointsToAnalysisField) field).getSinkFlow(); + if (field.getDeclaringClass().getSourceFileName() == null) { + return; + } + var filePath = getFilePath(field.getDeclaringClass()); + var constant = extractConstant(fieldSinkFlow); + if (constant != null) { + ideReport.saveFieldReport(filePath, field.getDeclaringClass().toJavaName(), field.getName(), "Field " + field.getName() + " has constant value " + constant); + } else if (field.getStorageKind().isObject()) { + var exactType = fieldSinkFlow.getState().exactType(); + if (exactType != null && !exactType.equals(field.getType())) { + ideReport.saveFieldReport(filePath, field.getDeclaringClass().toJavaName(), field.getName(), + "Values of field " + field.getName() + " are always of type " + exactType.toJavaName()); + } + } + }); + } + + private static String extractConstant(TypeFlow flow) { + if (flow.isSaturated()) { + return null; + } + var typeState = flow.getState(); + if (typeState instanceof PrimitiveConstantTypeState cstTypeState) { + var value = cstTypeState.getValue(); + if (flow.getDeclaredType().getJavaKind().equals(JavaKind.Boolean)) { + return value == 0 ? "false" : "true"; + } + return Long.toString(value); + } + var constant = typeState.asConstant(); + if (constant == null) { + return null; + } + var type = typeState.exactType(); + if (type == null) { + type = flow.getDeclaredType(); + } + if (type != null && type.toJavaName().equals("java.lang.String")) { + return "\"" + constant.toValueString() + "\""; + } + return constant.toValueString(); + } + + private static Range rangeOfBranch(Node branchBegin, Node decisionNode) { + var rangeLines = linesOf(nodesReachableFrom(branchBegin, null)); + rangeLines.removeAll(linesOf(nodesReachableFrom(decisionNode, branchBegin))); + if (rangeLines.isEmpty()) { + return null; + } + var start = rangeLines.stream().mapToInt(x -> x).min().getAsInt(); + var end = start; + while (rangeLines.contains(end)) { + end++; + } + return new Range(start, end - 1); + } + + private static Set linesOf(Set allNodes) { + return allNodes.stream() + .filter(n -> n.getNodeSourcePosition() != null) + .map(n -> getLineNumber(n.getNodeSourcePosition())) + .filter(l -> l > 0) + .collect(Collectors.toSet()); + } + + private static Set nodesReachableFrom(Node start, Node skippedNode) { + // BFS + var reachableNodes = new HashSet(); + var worklist = new LinkedList(); + worklist.addLast(start); + while (!worklist.isEmpty()) { + var curr = worklist.removeFirst(); + if (curr != skippedNode && reachableNodes.add(curr)) { + for (var succ : curr.successors()) { + worklist.addLast(succ); + } + } + } + return reachableNodes; + } + + private record Range(int startLine, int endLine) { + } + + private static AnalysisType resolveToAnalysisType(JavaType type, AnalysisUniverse universe) { + return type instanceof AnalysisType analysisType ? analysisType : universe.lookup(type); + } + + private static boolean isImplicitCheck(IfNode ifNode) { + var truePos = getLineNumber(ifNode.trueSuccessor().getNodeSourcePosition()); + var falsePos = getLineNumber(ifNode.falseSuccessor().getNodeSourcePosition()); + return truePos == falsePos; + } + +} diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/StrengthenGraphs.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/StrengthenGraphs.java index 60a06dfb5146..adc41003dbf7 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/StrengthenGraphs.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/StrengthenGraphs.java @@ -36,6 +36,7 @@ import com.oracle.graal.pointsto.api.HostVM; import com.oracle.graal.pointsto.api.PointstoOptions; import com.oracle.graal.pointsto.flow.AnalysisParsedGraph; +import com.oracle.graal.pointsto.ide.AnalysisIDEReporting; import com.oracle.graal.pointsto.infrastructure.Universe; import com.oracle.graal.pointsto.meta.AnalysisMethod; import com.oracle.graal.pointsto.meta.AnalysisType; @@ -215,6 +216,9 @@ public final void applyResults(AnalysisMethod method) { if (beforeCounters != null) { beforeCounters.collect(graph); } + + AnalysisIDEReporting.reportConstantParamAndReturnTypes(method); + try (var s = debug.scope("StrengthenGraphs", graph); var a = debug.activate()) { new AnalysisStrengthenGraphsPhase(method, graph).apply(graph, bb.getProviders(method)); } catch (Throwable ex) { diff --git a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/TypeFlowSimplifier.java b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/TypeFlowSimplifier.java index 3f1a68d7224c..e90294c98109 100644 --- a/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/TypeFlowSimplifier.java +++ b/substratevm/src/com.oracle.graal.pointsto/src/com/oracle/graal/pointsto/results/TypeFlowSimplifier.java @@ -27,15 +27,19 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.function.Supplier; import org.graalvm.collections.EconomicSet; import com.oracle.graal.pointsto.PointsToAnalysis; +import com.oracle.graal.pointsto.flow.FilterTypeFlow; import com.oracle.graal.pointsto.flow.InvokeTypeFlow; import com.oracle.graal.pointsto.flow.MethodFlowsGraph; import com.oracle.graal.pointsto.flow.MethodTypeFlow; +import com.oracle.graal.pointsto.flow.NullCheckTypeFlow; import com.oracle.graal.pointsto.flow.PrimitiveFilterTypeFlow; import com.oracle.graal.pointsto.flow.TypeFlow; +import com.oracle.graal.pointsto.ide.AnalysisIDEReporting; import com.oracle.graal.pointsto.meta.AnalysisMethod; import com.oracle.graal.pointsto.meta.AnalysisType; import com.oracle.graal.pointsto.meta.PointsToAnalysisField; @@ -82,6 +86,7 @@ import jdk.graal.compiler.nodes.java.LoadFieldNode; import jdk.graal.compiler.nodes.java.LoadIndexedNode; import jdk.graal.compiler.nodes.java.MethodCallTargetNode; +import jdk.graal.compiler.nodes.spi.CoreProviders; import jdk.graal.compiler.nodes.spi.SimplifierTool; import jdk.graal.compiler.nodes.util.GraphUtil; import jdk.vm.ci.meta.Constant; @@ -211,10 +216,25 @@ private void handleLoadIndexed(LoadIndexedNode node, SimplifierTool tool) { updateStampUsingPiNode(node, newStampOrConstant, node, tool); } + private void reportUnreachable(IfNode node, AbstractBeginNode unreachableBranch) { + var unreachableBranchFlow = getNodeFlow(unreachableBranch); + var isNullCheck = unreachableBranchFlow instanceof NullCheckTypeFlow nullCheck && !nullCheck.isBlockingNull(); + var isTypeCheck = unreachableBranchFlow instanceof FilterTypeFlow filterTypeFlow && !filterTypeFlow.isAssignable(); + AnalysisIDEReporting.reportUnreachableBranch(node, unreachableBranch, isNullCheck, isTypeCheck); + } + private void handleIf(IfNode node, SimplifierTool tool) { boolean trueUnreachable = isUnreachable(node.trueSuccessor()); boolean falseUnreachable = isUnreachable(node.falseSuccessor()); + if (trueUnreachable) { + reportUnreachable(node, node.trueSuccessor()); + } + + if (falseUnreachable) { + reportUnreachable(node, node.falseSuccessor()); + } + if (trueUnreachable && falseUnreachable) { super.makeUnreachable(node, tool, () -> super.location(node) + ": both successors of IfNode are unreachable"); @@ -246,6 +266,12 @@ private void handleFixedGuard(FixedGuardNode node, SimplifierTool tool) { } } + @Override + protected void makeUnreachable(FixedNode node, CoreProviders providers, Supplier message) { + AnalysisIDEReporting.reportUnreachableNode(node); + super.makeUnreachable(node, providers, message); + } + private boolean isUnreachable(Node branch) { TypeFlow branchFlow = getNodeFlow(branch); if (branchFlow != null && !methodFlow.isSaturated(analysis, branchFlow)) { @@ -429,12 +455,16 @@ private void handleInvoke(Invoke invoke, SimplifierTool tool) { } Object newStampOrConstant = strengthenStampFromTypeFlow(node, nodeFlow, anchorPointAfterInvoke, tool); updateStampUsingPiNode(node, newStampOrConstant, anchorPointAfterInvoke, tool); + + AnalysisIDEReporting.reportPossibleReturnTypes(invoke, callees); } /** * The invoke always has a null receiver, so it can be removed. */ private void invokeWithNullReceiver(Invoke invoke) { + var fixedNode = invoke.asFixedNode(); + AnalysisIDEReporting.reportUnreachableNode(fixedNode); FixedNode replacement = strengthenGraphs.createInvokeWithNullReceiverReplacement(graph); ((FixedWithNextNode) invoke.predecessor()).setNext(replacement); GraphUtil.killCFG(invoke.asFixedNode()); @@ -445,6 +475,7 @@ private void invokeWithNullReceiver(Invoke invoke) { * allows later inlining of the callee. */ private void devirtualizeInvoke(AnalysisMethod singleCallee, Invoke invoke) { + AnalysisIDEReporting.reportDevirtualizedInvoke(methodFlow.getMethod(), invoke, singleCallee); if (ImageBuildStatistics.Options.CollectImageBuildStatistics.getValue(graph.getOptions())) { ImageBuildStatistics.counters().incDevirtualizedInvokeCounter(); } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java index b7ced2130d15..9baa298d58d4 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/NativeImageGenerator.java @@ -31,6 +31,7 @@ import static jdk.graal.compiler.hotspot.JVMCIVersionCheck.OPEN_LABSJDK_RELEASE_URL_PATTERN; import static jdk.graal.compiler.replacements.StandardGraphBuilderPlugins.registerInvocationPlugins; +import java.io.File; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Method; @@ -53,8 +54,6 @@ import java.util.function.BooleanSupplier; import java.util.function.Function; -import com.oracle.svm.core.imagelayer.LayeredImageOptions; -import com.oracle.svm.hosted.reflect.ReflectionFeature; import org.graalvm.collections.EconomicSet; import org.graalvm.collections.Pair; import org.graalvm.nativeimage.ImageInfo; @@ -160,6 +159,7 @@ import com.oracle.svm.core.hub.DynamicHub; import com.oracle.svm.core.image.ImageHeapLayouter; import com.oracle.svm.core.imagelayer.ImageLayerBuildingSupport; +import com.oracle.svm.core.imagelayer.LayeredImageOptions; import com.oracle.svm.core.jdk.ServiceCatalogSupport; import com.oracle.svm.core.layeredimagesingleton.LayeredImageSingletonSupport; import com.oracle.svm.core.meta.MethodOffset; @@ -253,6 +253,7 @@ import com.oracle.svm.hosted.phases.SubstrateClassInitializationPlugin; import com.oracle.svm.hosted.phases.VerifyDeoptLIRFrameStatesPhase; import com.oracle.svm.hosted.phases.VerifyNoGuardsPhase; +import com.oracle.svm.hosted.reflect.ReflectionFeature; import com.oracle.svm.hosted.reflect.proxy.ProxyRenamingSubstitutionProcessor; import com.oracle.svm.hosted.snippets.SubstrateGraphBuilderPlugins; import com.oracle.svm.hosted.substitute.AnnotationSubstitutionProcessor; @@ -285,6 +286,7 @@ import jdk.graal.compiler.debug.DebugDumpScope; import jdk.graal.compiler.debug.Indent; import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.ide.IDEReport; import jdk.graal.compiler.java.BciBlockMapping; import jdk.graal.compiler.lir.phases.LIRSuites; import jdk.graal.compiler.loop.phases.ConvertDeoptimizeToGuardPhase; @@ -537,6 +539,7 @@ public void run(Map entryPoints, doRun(entryPoints, javaMainSupport, imageName, k, harnessSubstitutions); } finally { reporter.ensureCreationStageEndCompleted(); + IDEReport.runIfEnabled(ideReport -> ideReport.print(new File("").toPath().resolve("ide-reports"))); } } @@ -564,6 +567,10 @@ protected void doRun(Map entryPoints, JavaMainSupport j OptionValues options = HostedOptionValues.singleton(); + if (IDEReport.Options.IDEReport.getValue(options)) { + IDEReport.createInstance(IDEReport.Options.IDEReportFiltered.getValue(options)); + } + try (DebugContext debug = new Builder(options, new GraalDebugHandlersFactory(GraalAccess.getOriginalSnippetReflection())).build(); DebugCloseable _ = () -> featureHandler.forEachFeature(Feature::cleanup)) { setupNativeImage(options, entryPoints, javaMainSupport, imageName, harnessSubstitutions, debug); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationSupport.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationSupport.java index e7b7006e88b3..0881f5fb24c0 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationSupport.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/ClassInitializationSupport.java @@ -216,7 +216,7 @@ InitKind specifiedInitKindFor(Class clazz) { * Returns the computed init kind for {@code clazz}, which can differ from the configured init * kind returned by {@link #specifiedInitKindFor(Class)}. */ - InitKind computedInitKindFor(Class clazz) { + public InitKind computedInitKindFor(Class clazz) { return classInitKinds.get(clazz); } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/InitKind.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/InitKind.java index 7e25d7c017ff..96865a6a3946 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/InitKind.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/classinitialization/InitKind.java @@ -28,7 +28,7 @@ * The initialization kind for a class. The order of the enum values matters, {@link #max} depends * on it. */ -enum InitKind { +public enum InitKind { /** Class is initialized during image building, so it is already initialized at runtime. */ BUILD_TIME, /** Class should be initialized at runtime and not during image building. */ diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ide/IDEReportingFeature.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ide/IDEReportingFeature.java new file mode 100644 index 000000000000..caabd4a6ed50 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/ide/IDEReportingFeature.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2025, 2025, 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 com.oracle.svm.hosted.ide; + +import java.util.Collection; + +import com.oracle.graal.pointsto.constraints.UnsupportedFeatureException; +import com.oracle.svm.core.feature.AutomaticallyRegisteredFeature; +import com.oracle.svm.core.feature.InternalFeature; +import com.oracle.svm.hosted.FeatureImpl; +import com.oracle.svm.hosted.classinitialization.ClassInitializationSupport; +import com.oracle.svm.hosted.classinitialization.SimulateClassInitializerSupport; +import com.oracle.svm.hosted.meta.HostedMethod; + +import jdk.graal.compiler.ide.IDEReport; + +/** + * A feature that provides reporting functionality for IDEs. + *

+ * In particular, the feature provides information about class initialization and compiled methods. + * + * @see IDEReport + */ +@AutomaticallyRegisteredFeature +public class IDEReportingFeature implements InternalFeature { + + private ClassInitializationSupport classInitializationSupport; + + @Override + public boolean isInConfiguration(IsInConfigurationAccess access) { + return IDEReport.isEnabled(); + } + + @Override + public void duringSetup(DuringSetupAccess access) { + var accessImpl = (FeatureImpl.DuringSetupAccessImpl) access; + classInitializationSupport = accessImpl.getHostVM().getClassInitializationSupport(); + } + + @Override + public void afterAnalysis(AfterAnalysisAccess access) { + var accessImpl = (FeatureImpl.AfterAnalysisAccessImpl) access; + saveClassInitializationModes(accessImpl); + } + + @Override + public void afterCompilation(AfterCompilationAccess access) { + var accessImpl = (FeatureImpl.AfterCompilationAccessImpl) access; + saveCompiledMethods(accessImpl.getUniverse().getMethods()); + } + + /** + * Saves class initialization results to the IDE report. + * + * @param access access to the analysis results + */ + private void saveClassInitializationModes(FeatureImpl.AfterAnalysisAccessImpl access) { + IDEReport.runIfEnabled(ideReport -> { + forallTypes: for (var aType : access.getUniverse().getTypes()) { + var clazz = aType.getJavaClass(); + if (clazz == null) { + continue forallTypes; + } + var classInitKind = classInitializationSupport.computedInitKindFor(clazz); + if (classInitKind == null) { + continue forallTypes; + } + var kind = classInitKind.toString(); + if (kind.equals("RUN_TIME")) { + var type = access.getMetaAccess().optionalLookupJavaType(clazz); + if (type.isPresent() && SimulateClassInitializerSupport.singleton().isSuccessfulSimulation(type.get())) { + kind = "SIMULATED"; + } + } + try { + var type = access.getMetaAccess().lookupJavaType(clazz); + if (type.getSourceFileName() != null) { + var filePath = IDEReport.getFilePath(type); + ideReport.saveClassReport(filePath, type.toJavaName(), "Initialization mode of " + type.toJavaName(false) + " is " + kind); + } + } catch (UnsupportedFeatureException e) { + // skip + } + } + }); + } + + /** + * Saves compiled methods to the IDE report. + * + * @param allPossiblyCompiledMethods collection of compiled methods + */ + private static void saveCompiledMethods(Collection allPossiblyCompiledMethods) { + for (var method : allPossiblyCompiledMethods) { + if (method.isCompiled()) { + IDEReport.runIfEnabled(ideReport -> { + var declaringClass = method.getDeclaringClass(); + var filepath = IDEReport.getFilePath(method); + if (declaringClass == null || filepath == null) { + return; + } + ideReport.saveMethodCompiled(filepath, declaringClass.toJavaName(), method.getName(), + method.getSignature().toMethodDescriptor()); + }); + } + } + } + +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/UniverseBuilder.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/UniverseBuilder.java index d80e10ff1cea..414c6f0bf527 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/UniverseBuilder.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/meta/UniverseBuilder.java @@ -54,6 +54,7 @@ import com.oracle.graal.pointsto.ObjectScanner.OtherReason; import com.oracle.graal.pointsto.ObjectScanner.ScanReason; import com.oracle.graal.pointsto.constraints.UnsupportedFeatures; +import com.oracle.graal.pointsto.ide.AnalysisIDEReporting; import com.oracle.graal.pointsto.infrastructure.ResolvedSignature; import com.oracle.graal.pointsto.infrastructure.WrappedConstantPool; import com.oracle.graal.pointsto.meta.AnalysisField; @@ -437,6 +438,8 @@ private void makeField(AnalysisField aField) { */ HostedType type = lookupType(aField.getType()); + AnalysisIDEReporting.maybeReportConstantField(aField); + HostedField hField = new HostedField(aField, holder, type); assert !hUniverse.fields.containsKey(aField); hUniverse.fields.put(aField, hField); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java index 3d13ed4553a5..1da351f210b2 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/snippets/ReflectionPlugins.java @@ -24,6 +24,8 @@ */ package com.oracle.svm.hosted.snippets; +import static jdk.graal.compiler.ide.IDEReport.getInliningTrace; + import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; @@ -50,7 +52,6 @@ import org.graalvm.nativeimage.hosted.RuntimeReflection; import org.graalvm.nativeimage.impl.RuntimeClassInitializationSupport; -import com.oracle.svm.util.OriginalClassProvider; import com.oracle.graal.pointsto.meta.AnalysisUniverse; import com.oracle.svm.core.MissingRegistrationUtils; import com.oracle.svm.core.ParsingReason; @@ -71,10 +72,13 @@ import com.oracle.svm.hosted.substitute.AnnotationSubstitutionProcessor; import com.oracle.svm.hosted.substitute.SubstitutionReflectivityFilter; import com.oracle.svm.util.ModuleSupport; +import com.oracle.svm.util.OriginalClassProvider; import com.oracle.svm.util.ReflectionUtil; import com.oracle.svm.util.TypeResult; import jdk.graal.compiler.debug.GraalError; +import jdk.graal.compiler.graph.Node; +import jdk.graal.compiler.ide.IDEReport; import jdk.graal.compiler.nodes.ConstantNode; import jdk.graal.compiler.nodes.ValueNode; import jdk.graal.compiler.nodes.graphbuilderconf.ClassInitializationPlugin; @@ -433,7 +437,14 @@ private boolean processClassForName(GraphBuilderContext b, ResolvedJavaMethod ta Object classNameValue = unbox(b, nameNode, JavaKind.Object); Object initializeValue = unbox(b, initializeNode, JavaKind.Boolean); - if (!(classNameValue instanceof String) || !(initializeValue instanceof Boolean)) { + if (!(classNameValue instanceof String)) { + makeReflectionReport(nameNode, + "Call to forName: class name cannot be resolved at build time by Native Image. Please check that its actual values are listed in the reachability-metadata.json file."); + return false; + } + + if (!(initializeValue instanceof Boolean)) { + makeReflectionReport(initializeNode, "Call to forName: the 'initialize' argument cannot be resolved at build time by Native Image."); return false; } String className = (String) classNameValue; @@ -449,6 +460,7 @@ private boolean processClassForName(GraphBuilderContext b, ResolvedJavaMethod ta return false; } Throwable e = typeResult.getException(); + makeReflectionReport(nameNode, "Call to forName: class " + className + " not found, the generated code will throw a " + e.getClass().getName() + "."); return throwException(b, targetMethod, null, arguments, e.getClass(), e.getMessage(), true); } Class clazz = typeResult.get(); @@ -464,6 +476,7 @@ private boolean processClassForName(GraphBuilderContext b, ResolvedJavaMethod ta if (initialize) { classInitializationPlugin.apply(b, b.getMetaAccess().lookupJavaType(clazz), () -> null); } + makeReflectionReport(nameNode, "Call to forName: access to class " + className + " has been resolved at build time."); return true; } @@ -474,8 +487,10 @@ private boolean processClassForName(GraphBuilderContext b, ResolvedJavaMethod ta * e.g., Class.forName calls. */ private boolean processClassGetClassLoader(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver) { - Object classValue = unbox(b, receiver.get(false), JavaKind.Object); + var rec = receiver.get(false); + Object classValue = unbox(b, rec, JavaKind.Object); if (!(classValue instanceof Class)) { + makeReflectionReport(rec, "Loader could not be resolved a build time: receiver is not an instance of Class."); return false; } Class clazz = (Class) classValue; @@ -493,11 +508,15 @@ private boolean processClassGetClassLoader(GraphBuilderContext b, ResolvedJavaMe } if (result != null) { + // GR-71548: be more precise than "unnamed loader" + var msg = loader == null ? ("Loader is null.") : ("Loader has been resolved at build time: " + (loader.getName() != null ? loader.getName() : "unnamed loader") + "."); + makeReflectionReport(rec, msg); b.addPush(JavaKind.Object, ConstantNode.forConstant(result, b.getMetaAccess())); traceConstant(b, targetMethod, clazz, EMPTY_ARGUMENTS, result, false); return true; } + makeReflectionReport(rec, "Loader for class " + clazz.getName() + " could not be resolved at build time."); return false; } @@ -781,6 +800,19 @@ private JavaConstant pushConstant(GraphBuilderContext b, ResolvedJavaMethod targ return intrinsicConstant; } + private static void makeReflectionReport(Node positionNode, String msg) { + IDEReport.runIfEnabled(ideReport -> { + var srcPos = positionNode.getNodeSourcePosition(); + if (srcPos == null) { + return; + } + var filePath = IDEReport.getFilePath(srcPos.getMethod().getDeclaringClass()); + var line = IDEReport.getLineNumber(srcPos); + var className = srcPos.getMethod().getDeclaringClass().toJavaName(); + ideReport.saveLineReport(filePath, className, line, msg, getInliningTrace(srcPos)); + }); + } + private boolean throwException(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Object receiver, Object[] arguments, Class exceptionClass, String originalMessage, boolean subjectToStrictDynamicAccessInference) { /* Get the exception throwing method that has a message parameter. */