3030import java .util .TreeMap ;
3131import java .util .function .Function ;
3232
33- import jdk .graal .compiler .options .Option ;
34-
35- import com .oracle .svm .util .OriginalClassProvider ;
3633import com .oracle .graal .pointsto .meta .AnalysisField ;
3734import com .oracle .graal .pointsto .meta .AnalysisMethod ;
3835import com .oracle .graal .pointsto .meta .AnalysisType ;
4441import com .oracle .svm .hosted .substitute .SubstitutionField ;
4542import com .oracle .svm .hosted .substitute .SubstitutionMethod ;
4643import com .oracle .svm .hosted .substitute .SubstitutionType ;
44+ import com .oracle .svm .util .OriginalClassProvider ;
4745
46+ import jdk .graal .compiler .options .Option ;
4847import jdk .vm .ci .meta .ResolvedJavaField ;
4948import jdk .vm .ci .meta .ResolvedJavaMethod ;
5049import jdk .vm .ci .meta .ResolvedJavaType ;
5150
51+ /// Feature that reports substitutions discovered during analysis: all with [Options#ReportPerformedSubstitutions],
52+ /// or only user-authored with [Options#ReportPerformedUserSubstitutions].
53+ /// > Note: The feature does not report `@Delete`, `@RecomputeFieldValue`, and `@InjectAccessors` annotations.
54+ ///
55+ /// When enabled via [Options#ReportPerformedSubstitutions] or [Options#ReportPerformedUserSubstitutions],
56+ /// this feature scans the analysis universe after the analysis phase completes and collects substitutions:
57+ /// - type substitutions ([SubstitutionType])
58+ /// - method substitutions ([SubstitutionMethod])
59+ /// - field substitutions ([SubstitutionField])
60+ ///
61+ /// The results are emitted as a CSV report using [ReportUtils] under the [SubstrateOptions#reportsPath()] directory.
62+ /// The report is named:
63+ /// `substitutions_<date>_<time>.csv` with the following columns:
64+ /// location, category (type/method/field), original, annotated
65+ /// where:
66+ /// - location: the originating code source (e.g., JAR URL) of the annotated (replacement) element
67+ /// - category: one of "type", "method", "field"
68+ /// - original: the original element being substituted
69+ /// - annotated: the annotated replacement element
5270@ AutomaticallyRegisteredFeature
5371public class SubstitutionReportFeature implements InternalFeature {
5472
5573 static class Options {
56- @ Option (help = "Report performed substitutions" ) //
74+ @ Option (help = "Write a CSV report of all substitutions discovered during analysis to reports/substitutions<date>-<time>.csv. Columns: location, category (type/method/field), original, annotated." ) //
5775 public static final HostedOptionKey <Boolean > ReportPerformedSubstitutions = new HostedOptionKey <>(false );
76+
77+ @ Option (help = "Write a CSV report of user-authored substitutions originating from the application classpath or module path to reports/substitutions<date>-<time>.csv. Columns: location, category (type/method/field), original, annotated." ) //
78+ public static final HostedOptionKey <Boolean > ReportPerformedUserSubstitutions = new HostedOptionKey <>(false );
5879 }
5980
60- private final boolean enabled = Options .ReportPerformedSubstitutions .getValue ();
81+ private final boolean enabled = Options .ReportPerformedSubstitutions .getValue () || Options .ReportPerformedUserSubstitutions .getValue ();
82+
83+ /// Aggregated substitutions grouped by the [CodeSource] location (typically the JAR URL)
84+ /// of the annotated (replacement) elements.
6185 private final Map <String , Substitutions > substitutions = new TreeMap <>();
6286
87+ /// Only participate in the image build when the reporting option is enabled.
6388 @ Override
6489 public boolean isInConfiguration (IsInConfigurationAccess access ) {
6590 return enabled ;
6691 }
6792
93+ /// After analysis completes, collect all user-provided substitutions and write the CSV report.
6894 @ Override
6995 public void afterAnalysis (AfterAnalysisAccess access ) {
7096 FeatureImpl .AfterAnalysisAccessImpl accessImpl = (FeatureImpl .AfterAnalysisAccessImpl ) access ;
@@ -74,48 +100,61 @@ public void afterAnalysis(AfterAnalysisAccess access) {
74100 reportSubstitutions ();
75101 }
76102
103+ /// Scans all reachable, non-array [AnalysisType] types and records those that are user
104+ /// substitutions.
105+ /// A [SubstitutionType] represents an annotated replacement for an original type.
77106 private void findSubstitutedTypes (FeatureImpl .AfterAnalysisAccessImpl access ) {
78107 for (AnalysisType type : access .getUniverse ().getTypes ()) {
79108 if (type .isReachable () && !type .isArray ()) {
80109 ResolvedJavaType t = type .getWrapped ();
81- if (t instanceof SubstitutionType ) {
82- SubstitutionType subType = ( SubstitutionType ) t ;
83- if (subType .isUserSubstitution ()) {
84- String jarLocation = getTypeClassFileLocation (subType .getAnnotated ());
110+ if (t instanceof SubstitutionType substType ) {
111+ // Only report substitutions authored by users (filter out internal ones).
112+ if (shouldReport ( substType .isUserSubstitution () )) {
113+ String jarLocation = getTypeClassFileLocation (substType .getAnnotated ());
85114 substitutions .putIfAbsent (jarLocation , new Substitutions ());
86- substitutions .get (jarLocation ).addType (subType );
115+ substitutions .get (jarLocation ).addType (substType );
87116 }
88117 }
89118 }
90119 }
91120 }
92121
122+ /// Scans all [AnalysisMethod] methods in the analysis universe and records those that are
123+ /// user substitutions.
124+ /// A [SubstitutionMethod] holds both original and annotated (replacement) methods.
93125 private void findSubstitutedMethods (FeatureImpl .AfterAnalysisAccessImpl access ) {
94126 for (AnalysisMethod method : access .getUniverse ().getMethods ()) {
95- if (method .wrapped instanceof SubstitutionMethod ) {
96- SubstitutionMethod subMethod = (SubstitutionMethod ) method .wrapped ;
97- if (subMethod .isUserSubstitution ()) {
98- String jarLocation = getTypeClassFileLocation (subMethod .getAnnotated ().getDeclaringClass ());
127+ if (method .wrapped instanceof SubstitutionMethod substMethod ) {
128+ if (shouldReport (substMethod .isUserSubstitution ())) {
129+ String jarLocation = getTypeClassFileLocation (substMethod .getAnnotated ().getDeclaringClass ());
99130 substitutions .putIfAbsent (jarLocation , new Substitutions ());
100- substitutions .get (jarLocation ).addMethod (subMethod );
131+ substitutions .get (jarLocation ).addMethod (substMethod );
101132 }
102133 }
103134 }
104135 }
105136
137+ /// Scans all [AnalysisField] fields in the analysis universe and records those that are
138+ /// user substitutions.
139+ /// A [SubstitutionField] holds both original and annotated (replacement) fields.
106140 private void findSubstitutedFields (FeatureImpl .AfterAnalysisAccessImpl access ) {
107141 for (AnalysisField field : access .getUniverse ().getFields ()) {
108- if (field .wrapped instanceof SubstitutionField ) {
109- SubstitutionField subField = (SubstitutionField ) field .wrapped ;
110- if (subField .isUserSubstitution ()) {
111- String jarLocation = getTypeClassFileLocation (subField .getAnnotated ().getDeclaringClass ());
142+ if (field .wrapped instanceof SubstitutionField substField ) {
143+ if (shouldReport (substField .isUserSubstitution ())) {
144+ String jarLocation = getTypeClassFileLocation (substField .getAnnotated ().getDeclaringClass ());
112145 substitutions .putIfAbsent (jarLocation , new Substitutions ());
113- substitutions .get (jarLocation ).addField (subField );
146+ substitutions .get (jarLocation ).addField (substField );
114147 }
115148 }
116149 }
117150 }
118151
152+ /// Emits the CSV report with one row per discovered substitution.
153+ /// The output file will be created under [SubstrateOptions#reportsPath()] with the name
154+ /// `substitutions<date>_<time>.csv`. Rows are grouped by code source location and sorted using
155+ /// human-readable formatting for types, methods, and fields (see
156+ /// [#formatType(ResolvedJavaType)], [#formatMethod(ResolvedJavaMethod)],
157+ /// [#formatField(ResolvedJavaField)]).
119158 private void reportSubstitutions () {
120159 ReportUtils .report ("substitutions performed by native-image" , SubstrateOptions .reportsPath (), "substitutions" , "csv" , pw -> {
121160 pw .println ("location, category (type/method/field), original, annotated" );
@@ -133,28 +172,50 @@ private void reportSubstitutions() {
133172 });
134173 }
135174
175+ private static boolean shouldReport (boolean userSubstitution ) {
176+ return userSubstitution || Options .ReportPerformedSubstitutions .getValue ();
177+ }
178+
179+ /// Fully-qualified type representation.
136180 private static String formatType (ResolvedJavaType t ) {
137181 return t .toJavaName (true );
138182 }
139183
184+ /// Human-readable method representation.
185+ /// Format: `pkg.Class#methodName`
140186 private static String formatMethod (ResolvedJavaMethod method ) {
141187 return method .format ("%H#%n" );
142188 }
143189
190+ /// Human-readable field representation.
191+ /// Format: `pkg.Class.fieldName`
144192 private static String formatField (ResolvedJavaField field ) {
145193 return field .format ("%H.%n" );
146194 }
147195
196+ /// Determines the [CodeSource] (typically the JAR URL) that contains the given type (via
197+ /// [OriginalClassProvider]).
198+ /// Falls back to "unknown" when no code source is available (e.g., dynamically defined
199+ /// classes).
148200 private static String getTypeClassFileLocation (ResolvedJavaType type ) {
149201 Class <?> annotatedClass = OriginalClassProvider .getJavaClass (type );
150202 CodeSource source = annotatedClass .getProtectionDomain ().getCodeSource ();
151- return source == null ? "unknown" : source .getLocation ().toString ();
203+ return source == null || source . getLocation () == null ? "unknown" : source .getLocation ().toString ();
152204 }
153205
206+ /// Formats a single CSV row representing one substitution. The JAR location is quoted to guard
207+ /// against commas in URLs; other columns are formatted using the provided formatter.
154208 private static <T > String formatSubstitution (String jar , String type , T original , T annotated , Function <T , String > formatter ) {
155209 return '\'' + jar + "'," + type + ',' + formatter .apply (original ) + ',' + formatter .apply (annotated );
156210 }
157211
212+ /// Container for collected substitutions for a single code source location.
213+ ///
214+ /// Maps are keyed by the original element and map to the annotated (replacement) element.
215+ /// TreeMaps are used with comparators based on the human-readable formatters (see
216+ /// [#formatType(ResolvedJavaType)], [#formatMethod(ResolvedJavaMethod)],
217+ /// [#formatField(ResolvedJavaField)]) so the output
218+ /// remains stable and easy to consume.
158219 private static final class Substitutions {
159220 private final Map <ResolvedJavaType , ResolvedJavaType > substitutedTypes = new TreeMap <>(Comparator .comparing (SubstitutionReportFeature ::formatType ));
160221 private final Map <ResolvedJavaMethod , ResolvedJavaMethod > substitutedMethods = new TreeMap <>(Comparator .comparing (SubstitutionReportFeature ::formatMethod ));
0 commit comments