1 /*
  2  * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 import java.io.File;
 25 import java.io.IOException;
 26 import java.io.UncheckedIOException;
 27 import java.lang.Runtime.Version;
 28 import java.net.URI;
 29 import java.nio.file.*;
 30 import java.util.*;
 31 import java.util.regex.Matcher;
 32 import java.util.regex.Pattern;
 33 import java.util.stream.Collectors;
 34 import javax.lang.model.element.*;
 35 import javax.lang.model.util.ElementFilter;
 36 import javax.lang.model.util.Elements;
 37 import javax.lang.model.util.Types;
 38 import javax.tools.*;
 39 import javax.tools.JavaFileManager.Location;
 40 import com.sun.source.tree.*;
 41 import com.sun.source.util.JavacTask;
 42 import com.sun.source.util.TreePathScanner;
 43 import com.sun.source.util.Trees;
 44 import com.sun.tools.javac.api.JavacTaskImpl;
 45 import com.sun.tools.javac.code.Flags;
 46 import com.sun.tools.javac.code.Symbol;
 47 import com.sun.tools.javac.util.Pair;
 48 import jtreg.SkippedException;
 49 
 50 /*
 51 This checker checks the values of the `@since` tag found in the documentation comment for an element against
 52 the release in which the element first appeared.
 53 The source code containing the documentation comments is read from `src.zip` in the release of JDK used to run the test.
 54 The releases used to determine the expected value of `@since` tags are taken from the historical data built into `javac`.
 55 
 56 The `@since` checker works as a two-step process:
 57 In the first step, we process JDKs 9-current, only classfiles,
 58   producing a map `<unique-Element-ID`> => `<version(s)-where-it-was-introduced>`.
 59     - "version(s)", because we handle versioning of Preview API, so there may be two versions
 60      (we use a class with two fields for preview and stable),
 61     one when it was introduced as a preview, and one when it went out of preview. More on that below.
 62     - For each Element, we compute the unique ID, look into the map, and if there's nothing,
 63      record the current version as the originating version.
 64     - At the end of this step we have a map of the Real since values
 65 
 66 In the second step, we look at "effective" `@since` tags in the mainline sources, from `src.zip`
 67  (if the test run doesn't have it, we throw a `jtreg.SkippedException`)
 68     - We only check the specific MODULE whose name was passed as an argument in the test.
 69       In that module, we look for unqualified exports and test those packages.
 70     - The `@since` checker verifies that for every API element, the real since value and
 71       the effective since value are the same, and reports an error if they are not.
 72 
 73 Important note : We only check code written since JDK 9 as the releases used to determine the expected value
 74                  of @since tags are taken from the historical data built into javac which only goes back that far
 75 
 76 note on rules for Real and effective `@since:
 77 
 78 Real since value of an API element is computed as the oldest release in which the given API element was introduced.
 79 That is:
 80 - for modules, packages, classes and interfaces, the release in which the element with the given qualified name was introduced
 81 - for constructors, the release in which the constructor with the given VM descriptor was introduced
 82 - for methods and fields, the release in which the given method or field with the given VM descriptor became a member
 83   of its enclosing class or interface, whether direct or inherited
 84 
 85 Effective since value of an API element is computed as follows:
 86 - if the given element has a @since tag in its javadoc, it is used
 87 - in all other cases, return the effective since value of the enclosing element
 88 
 89 
 90 Special Handling for preview method, as per JEP 12:
 91 - When an element is still marked as preview, the `@since` should be the first JDK release where the element was added.
 92 - If the element is no longer marked as preview, the `@since` should be the first JDK release where it was no longer preview.
 93 
 94 note on legacy preview: Until JDK 14, the preview APIs were not marked in any machine-understandable way.
 95                         It was deprecated, and had a comment in the javadoc.
 96                         and the use of `@PreviewFeature` only became standard in JDK 17.
 97                         So the checker has an explicit knowledge of these preview elements.
 98 
 99 note: The `<unique-Element-ID>` for methods looks like
100       `method: <erased-return-descriptor> <binary-name-of-enclosing-class>.<method-name>(<ParameterDescriptor>)`.
101 it is somewhat inspired from the VM Method Descriptors. But we use the erased return so that methods
102 that were later generified remain the same.
103 
104 usage: the checker is run from a module specific test
105         `@run main SinceChecker <moduleName> [--exclude package1,package2 | --exclude package1 package2]`
106 */
107 
108 public class SinceChecker {
109     private final Map<String, Set<String>> LEGACY_PREVIEW_METHODS = new HashMap<>();
110     private final Map<String, IntroducedIn> classDictionary = new HashMap<>();
111     private final JavaCompiler tool;
112     private int errorCount = 0;
113 
114     // packages to skip during the test
115     private static final Set<String> EXCLUDE_LIST = new HashSet<>();
116 
117     public static class IntroducedIn {
118         public String introducedPreview;
119         public String introducedStable;
120     }
121 
122     public static void main(String[] args) throws Exception {
123         if (args.length == 0) {
124             throw new IllegalArgumentException("Test module not specified");
125         }
126         String moduleName = args[0];
127         boolean excludeFlag = false;
128 
129         for (int i = 1; i < args.length; i++) {
130             if ("--exclude".equals(args[i])) {
131                 excludeFlag = true;
132                 continue;
133             }
134 
135             if (excludeFlag) {
136                 if (args[i].contains(",")) {
137                     EXCLUDE_LIST.addAll(Arrays.asList(args[i].split(",")));
138                 } else {
139                     EXCLUDE_LIST.add(args[i]);
140                 }
141             }
142         }
143 
144         SinceChecker sinceCheckerTestHelper = new SinceChecker(moduleName);
145         sinceCheckerTestHelper.checkModule(moduleName);
146     }
147 
148     private void error(String message) {
149         System.err.println(message);
150         errorCount++;
151     }
152 
153     private SinceChecker(String moduleName) throws IOException {
154         tool = ToolProvider.getSystemJavaCompiler();
155         for (int i = 9; i <= Runtime.version().feature(); i++) {
156             DiagnosticListener<? super JavaFileObject> noErrors = d -> {
157                 if (!d.getCode().equals("compiler.err.module.not.found")) {
158                     error(d.getMessage(null));
159                 }
160             };
161             JavacTask ct = (JavacTask) tool.getTask(null,
162                     null,
163                     noErrors,
164                     List.of("--add-modules", moduleName, "--release", String.valueOf(i)),
165                     null,
166                     Collections.singletonList(SimpleJavaFileObject.forSource(URI.create("myfo:/Test.java"), "")));
167             ct.analyze();
168 
169             String version = String.valueOf(i);
170             Elements elements = ct.getElements();
171             elements.getModuleElement("java.base"); // forces module graph to be instantiated
172             elements.getAllModuleElements().forEach(me ->
173                     processModuleElement(me, version, ct));
174         }
175     }
176 
177     private void processModuleElement(ModuleElement moduleElement, String releaseVersion, JavacTask ct) {
178         processElement(moduleElement, moduleElement, ct.getTypes(), releaseVersion);
179         for (ModuleElement.ExportsDirective ed : ElementFilter.exportsIn(moduleElement.getDirectives())) {
180             if (ed.getTargetModules() == null) {
181                 processPackageElement(ed.getPackage(), releaseVersion, ct);
182             }
183         }
184     }
185 
186     private void processPackageElement(PackageElement pe, String releaseVersion, JavacTask ct) {
187         processElement(pe, pe, ct.getTypes(), releaseVersion);
188         List<TypeElement> typeElements = ElementFilter.typesIn(pe.getEnclosedElements());
189         for (TypeElement te : typeElements) {
190             processClassElement(te, releaseVersion, ct.getTypes(), ct.getElements());
191         }
192     }
193 
194     /// JDK documentation only contains public and protected declarations
195     private boolean isDocumented(Element te) {
196         Set<Modifier> mod = te.getModifiers();
197         return mod.contains(Modifier.PUBLIC) || mod.contains(Modifier.PROTECTED);
198     }
199 
200     private boolean isMember(Element e) {
201         var kind = e.getKind();
202         return kind.isField() || switch (kind) {
203             case METHOD, CONSTRUCTOR -> true;
204             default -> false;
205         };
206     }
207 
208     private void processClassElement(TypeElement te, String version, Types types, Elements elements) {
209         if (!isDocumented(te)) {
210             return;
211         }
212         processElement(te.getEnclosingElement(), te, types, version);
213         elements.getAllMembers(te).stream()
214                 .filter(this::isDocumented)
215                 .filter(this::isMember)
216                 .forEach(element -> processElement(te, element, types, version));
217         te.getEnclosedElements().stream()
218                 .filter(element -> element.getKind().isDeclaredType())
219                 .map(TypeElement.class::cast)
220                 .forEach(nestedClass -> processClassElement(nestedClass, version, types, elements));
221     }
222 
223     private void processElement(Element explicitOwner, Element element, Types types, String version) {
224         String uniqueId = getElementName(explicitOwner, element, types);
225         IntroducedIn introduced = classDictionary.computeIfAbsent(uniqueId, _ -> new IntroducedIn());
226         if (isPreview(element, uniqueId, version)) {
227             if (introduced.introducedPreview == null) {
228                 introduced.introducedPreview = version;
229             }
230         } else {
231             if (introduced.introducedStable == null) {
232                 introduced.introducedStable = version;
233             }
234         }
235     }
236 
237     private boolean isPreview(Element el, String uniqueId, String currentVersion) {
238         while (el != null) {
239             Symbol s = (Symbol) el;
240             if ((s.flags() & Flags.PREVIEW_API) != 0) {
241                 return true;
242             }
243             el = el.getEnclosingElement();
244         }
245 
246         return LEGACY_PREVIEW_METHODS.getOrDefault(currentVersion, Set.of())
247                 .contains(uniqueId);
248     }
249 
250     private void checkModule(String moduleName) throws Exception {
251         Path home = Paths.get(System.getProperty("java.home"));
252         Path srcZip = home.resolve("lib").resolve("src.zip");
253         if (Files.notExists(srcZip)) {
254             //possibly running over an exploded JDK build, attempt to find a
255             //co-located full JDK image with src.zip:
256             Path testJdk = Paths.get(System.getProperty("test.jdk"));
257             srcZip = testJdk.getParent().resolve("images").resolve("jdk").resolve("lib").resolve("src.zip");
258         }
259         if (!Files.isReadable(srcZip)) {
260             throw new SkippedException("Skipping Test because src.zip wasn't found or couldn't be read");
261         }
262         URI uri = URI.create("jar:" + srcZip.toUri());
263         try (FileSystem zipFO = FileSystems.newFileSystem(uri, Collections.emptyMap())) {
264             Path root = zipFO.getRootDirectories().iterator().next();
265             Path moduleDirectory = root.resolve(moduleName);
266             try (StandardJavaFileManager fm =
267                          tool.getStandardFileManager(null, null, null)) {
268                 JavacTask ct = (JavacTask) tool.getTask(null,
269                         fm,
270                         null,
271                         List.of("--add-modules", moduleName, "-d", "."),
272                         null,
273                         Collections.singletonList(SimpleJavaFileObject.forSource(URI.create("myfo:/Test.java"), "")));
274                 ct.analyze();
275                 Elements elements = ct.getElements();
276                 elements.getModuleElement("java.base");
277                 try (EffectiveSourceSinceHelper javadocHelper = EffectiveSourceSinceHelper.create(ct, List.of(root), this)) {
278                     processModuleCheck(elements.getModuleElement(moduleName), ct, moduleDirectory, javadocHelper);
279                 } catch (Exception e) {
280                     e.printStackTrace();
281                     error("Initiating javadocHelper Failed " + e);
282                 }
283                 if (errorCount > 0) {
284                     throw new Exception("The `@since` checker found " + errorCount + " problems");
285                 }
286             }
287         }
288     }
289 
290     private boolean isExcluded(ModuleElement.ExportsDirective ed ){
291         return EXCLUDE_LIST.stream().anyMatch(excludePackage ->
292             ed.getPackage().toString().equals(excludePackage) ||
293             ed.getPackage().toString().startsWith(excludePackage + "."));
294     }
295 
296     private void processModuleCheck(ModuleElement moduleElement, JavacTask ct, Path moduleDirectory, EffectiveSourceSinceHelper javadocHelper) {
297         if (moduleElement == null) {
298             error("Module element: was null because `elements.getModuleElement(moduleName)` returns null." +
299                     "fixes are needed for this Module");
300         }
301         String moduleVersion = getModuleVersionFromFile(moduleDirectory);
302         checkModuleOrPackage(javadocHelper, moduleVersion, moduleElement, ct, "Module: ");
303         for (ModuleElement.ExportsDirective ed : ElementFilter.exportsIn(moduleElement.getDirectives())) {
304             if (ed.getTargetModules() == null) {
305                 String packageVersion = getPackageVersionFromFile(moduleDirectory, ed);
306                 if (packageVersion != null && !isExcluded(ed)) {
307                     checkModuleOrPackage(javadocHelper, packageVersion, ed.getPackage(), ct, "Package: ");
308                     analyzePackageCheck(ed.getPackage(), ct, javadocHelper);
309                 } // Skip the package if packageVersion is null
310             }
311         }
312     }
313 
314     private void checkModuleOrPackage(EffectiveSourceSinceHelper javadocHelper, String moduleVersion, Element moduleElement, JavacTask ct, String elementCategory) {
315         String id = getElementName(moduleElement, moduleElement, ct.getTypes());
316         var elementInfo = classDictionary.get(id);
317         if (elementInfo == null) {
318             error("Element :" + id + " was not mapped");
319             return;
320         }
321         String version = elementInfo.introducedStable;
322         if (moduleVersion == null) {
323             error("Unable to retrieve `@since` for " + elementCategory + id);
324         } else {
325             String position = javadocHelper.getElementPosition(id);
326             checkEquals(position, moduleVersion, version, id);
327         }
328     }
329 
330     private String getModuleVersionFromFile(Path moduleDirectory) {
331         Path moduleInfoFile = moduleDirectory.resolve("module-info.java");
332         String version = null;
333         if (Files.exists(moduleInfoFile)) {
334             try {
335                 String moduleInfoContent = Files.readString(moduleInfoFile);
336                 var extractedVersion = extractSinceVersionFromText(moduleInfoContent);
337                 if (extractedVersion != null) {
338                     version = extractedVersion.toString();
339                 }
340             } catch (IOException e) {
341                 error("module-info.java not found or couldn't be opened AND this module has no unqualified exports");
342             }
343         }
344         return version;
345     }
346 
347     private String getPackageVersionFromFile(Path moduleDirectory, ModuleElement.ExportsDirective ed) {
348         Path pkgInfo = moduleDirectory.resolve(ed.getPackage()
349                         .getQualifiedName()
350                         .toString()
351                         .replace(".", File.separator)
352                 )
353                 .resolve("package-info.java");
354 
355         if (!Files.exists(pkgInfo)) {
356             return null; // Skip if the file does not exist
357         }
358 
359         String packageTopVersion = null;
360         try {
361             String packageContent = Files.readString(pkgInfo);
362             var extractedVersion = extractSinceVersionFromText(packageContent);
363             if (extractedVersion != null) {
364                 packageTopVersion = extractedVersion.toString();
365             } else {
366                 error(ed.getPackage().getQualifiedName() + ": package-info.java exists but doesn't contain @since");
367             }
368         } catch (IOException e) {
369             error(ed.getPackage().getQualifiedName() + ": package-info.java couldn't be opened");
370         }
371         return packageTopVersion;
372     }
373 
374     private void analyzePackageCheck(PackageElement pe, JavacTask ct, EffectiveSourceSinceHelper javadocHelper) {
375         List<TypeElement> typeElements = ElementFilter.typesIn(pe.getEnclosedElements());
376         for (TypeElement te : typeElements) {
377             analyzeClassCheck(te, null, javadocHelper, ct.getTypes(), ct.getElements());
378         }
379     }
380 
381     private boolean isNotCommonRecordMethod(TypeElement te, Element element, Types types) {
382         var isRecord = te.getKind() == ElementKind.RECORD;
383         if (!isRecord) {
384             return true;
385         }
386         String uniqueId = getElementName(te, element, types);
387         boolean isCommonMethod = uniqueId.endsWith(".toString()") ||
388                 uniqueId.endsWith(".hashCode()") ||
389                 uniqueId.endsWith(".equals(java.lang.Object)");
390         if (isCommonMethod) {
391             return false;
392         }
393         for (var parameter : te.getEnclosedElements()) {
394             if (parameter.getKind() == ElementKind.RECORD_COMPONENT) {
395                 if (uniqueId.endsWith(String.format("%s.%s()", te.getSimpleName(), parameter.getSimpleName().toString()))) {
396                     return false;
397                 }
398             }
399         }
400         return true;
401     }
402 
403     private void analyzeClassCheck(TypeElement te, String version, EffectiveSourceSinceHelper javadocHelper,
404                                    Types types, Elements elementUtils) {
405         String currentjdkVersion = String.valueOf(Runtime.version().feature());
406         if (!isDocumented(te)) {
407             return;
408         }
409         checkElement(te.getEnclosingElement(), te, types, javadocHelper, version, elementUtils);
410         te.getEnclosedElements().stream().filter(this::isDocumented)
411                 .filter(this::isMember)
412                 .filter(element -> isNotCommonRecordMethod(te, element, types))
413                 .forEach(element -> checkElement(te, element, types, javadocHelper, version, elementUtils));
414         te.getEnclosedElements().stream()
415                 .filter(element -> element.getKind().isDeclaredType())
416                 .map(TypeElement.class::cast)
417                 .forEach(nestedClass -> analyzeClassCheck(nestedClass, currentjdkVersion, javadocHelper, types, elementUtils));
418     }
419 
420     private void checkElement(Element explicitOwner, Element element, Types types,
421                               EffectiveSourceSinceHelper javadocHelper, String currentVersion, Elements elementUtils) {
422         String uniqueId = getElementName(explicitOwner, element, types);
423 
424         if (element.getKind() == ElementKind.METHOD &&
425                 element.getEnclosingElement().getKind() == ElementKind.ENUM &&
426                 (uniqueId.contains(".values()") || uniqueId.contains(".valueOf(java.lang.String)"))) {
427             //mandated enum type methods
428             return;
429         }
430         String sinceVersion = null;
431         var effectiveSince = javadocHelper.effectiveSinceVersion(explicitOwner, element, types, elementUtils);
432         if (effectiveSince == null) {
433             // Skip the element if the java file doesn't exist in src.zip
434             return;
435         }
436         sinceVersion = effectiveSince.toString();
437         IntroducedIn mappedVersion = classDictionary.get(uniqueId);
438         if (mappedVersion == null) {
439             error("Element: " + uniqueId + " was not mapped");
440             return;
441         }
442         String realMappedVersion = null;
443         try {
444             realMappedVersion = isPreview(element, uniqueId, currentVersion) ?
445                     mappedVersion.introducedPreview :
446                     mappedVersion.introducedStable;
447         } catch (Exception e) {
448             error("For element " + element + "mappedVersion" + mappedVersion + " is null " + e);
449         }
450         String position = javadocHelper.getElementPosition(uniqueId);
451         checkEquals(position, sinceVersion, realMappedVersion, uniqueId);
452     }
453 
454     private Version extractSinceVersionFromText(String documentation) {
455         Pattern pattern = Pattern.compile("@since\\s+(\\d+(?:\\.\\d+)?)");
456         Matcher matcher = pattern.matcher(documentation);
457         if (matcher.find()) {
458             String versionString = matcher.group(1);
459             try {
460                 if (versionString.equals("1.0")) {
461                     versionString = "1"; //ended up being necessary
462                 } else if (versionString.startsWith("1.")) {
463                     versionString = versionString.substring(2);
464                 }
465                 return Version.parse(versionString);
466             } catch (NumberFormatException ex) {
467                 error("`@since` value that cannot be parsed: " + versionString);
468                 return null;
469             }
470         } else {
471             return null;
472         }
473     }
474 
475     private void checkEquals(String prefix, String sinceVersion, String mappedVersion, String name) {
476         if (sinceVersion == null || mappedVersion == null) {
477             error(name + ": NULL value for either real or effective `@since` . real/mapped version is="
478                     + mappedVersion + " while the `@since` in the source code is= " + sinceVersion);
479             return;
480         }
481         if (Integer.parseInt(sinceVersion) < 9) {
482             sinceVersion = "9";
483         }
484         if (!sinceVersion.equals(mappedVersion)) {
485             String message = getWrongSinceMessage(prefix, sinceVersion, mappedVersion, name);
486             error(message);
487         }
488     }
489     private static String getWrongSinceMessage(String prefix, String sinceVersion, String mappedVersion, String elementSimpleName) {
490         String message;
491         if (mappedVersion.equals("9")) {
492             message = elementSimpleName + ": `@since` version is " + sinceVersion + " but the element exists before JDK 10";
493         } else {
494             message = elementSimpleName + ": `@since` version: " + sinceVersion + "; should be: " + mappedVersion;
495         }
496         return prefix + message;
497     }
498 
499     private static String getElementName(Element owner, Element element, Types types) {
500         String prefix = "";
501         String suffix = "";
502         ElementKind kind = element.getKind();
503         if (kind.isField()) {
504             TypeElement te = (TypeElement) owner;
505             prefix = "field";
506             suffix = ": " + te.getQualifiedName() + ":" + element.getSimpleName();
507         } else if (kind == ElementKind.METHOD || kind == ElementKind.CONSTRUCTOR) {
508             prefix = "method";
509             TypeElement te = (TypeElement) owner;
510             ExecutableElement executableElement = (ExecutableElement) element;
511             String returnType = types.erasure(executableElement.getReturnType()).toString();
512             String methodName = executableElement.getSimpleName().toString();
513             String descriptor = executableElement.getParameters().stream()
514                     .map(p -> types.erasure(p.asType()).toString())
515                     .collect(Collectors.joining(",", "(", ")"));
516             suffix = ": " + returnType + " " + te.getQualifiedName() + "." + methodName + descriptor;
517         } else if (kind.isDeclaredType()) {
518             if (kind.isClass()) {
519                 prefix = "class";
520             } else if (kind.isInterface()) {
521                 prefix = "interface";
522             }
523             suffix = ": " + ((TypeElement) element).getQualifiedName();
524         } else if (kind == ElementKind.PACKAGE) {
525             prefix = "package";
526             suffix = ": " + ((PackageElement) element).getQualifiedName();
527         } else if (kind == ElementKind.MODULE) {
528             prefix = "module";
529             suffix = ": " + ((ModuleElement) element).getQualifiedName();
530         }
531         return prefix + suffix;
532     }
533 
534     //these were preview in before the introduction of the @PreviewFeature
535     {
536         LEGACY_PREVIEW_METHODS.put("9", Set.of(
537                 "module: jdk.nio.mapmode",
538                 "module: java.transaction.xa",
539                 "module: jdk.unsupported.desktop",
540                 "module: jdk.jpackage",
541                 "module: java.net.http"
542         ));
543         LEGACY_PREVIEW_METHODS.put("10", Set.of(
544                 "module: jdk.nio.mapmode",
545                 "module: java.transaction.xa",
546                 "module: java.net.http",
547                 "module: jdk.unsupported.desktop",
548                 "module: jdk.jpackage"
549         ));
550         LEGACY_PREVIEW_METHODS.put("11", Set.of(
551                 "module: jdk.nio.mapmode",
552                 "module: jdk.jpackage"
553         ));
554         LEGACY_PREVIEW_METHODS.put("12", Set.of(
555                 "module: jdk.nio.mapmode",
556                 "module: jdk.jpackage",
557                 "method: com.sun.source.tree.ExpressionTree com.sun.source.tree.BreakTree.getValue()",
558                 "method: java.util.List com.sun.source.tree.CaseTree.getExpressions()",
559                 "method: com.sun.source.tree.Tree com.sun.source.tree.CaseTree.getBody()",
560                 "method: com.sun.source.tree.CaseTree.CaseKind com.sun.source.tree.CaseTree.getCaseKind()",
561                 "class: com.sun.source.tree.CaseTree.CaseKind",
562                 "field: com.sun.source.tree.CaseTree.CaseKind:STATEMENT",
563                 "field: com.sun.source.tree.CaseTree.CaseKind:RULE",
564                 "field: com.sun.source.tree.Tree.Kind:SWITCH_EXPRESSION",
565                 "interface: com.sun.source.tree.SwitchExpressionTree",
566                 "method: com.sun.source.tree.ExpressionTree com.sun.source.tree.SwitchExpressionTree.getExpression()",
567                 "method: java.util.List com.sun.source.tree.SwitchExpressionTree.getCases()",
568                 "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)",
569                 "method: java.lang.Object com.sun.source.util.TreeScanner.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)",
570                 "method: java.lang.Object com.sun.source.util.SimpleTreeVisitor.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)"
571         ));
572 
573         LEGACY_PREVIEW_METHODS.put("13", Set.of(
574                 "module: jdk.nio.mapmode",
575                 "module: jdk.jpackage",
576                 "method: java.util.List com.sun.source.tree.CaseTree.getExpressions()",
577                 "method: com.sun.source.tree.Tree com.sun.source.tree.CaseTree.getBody()",
578                 "method: com.sun.source.tree.CaseTree.CaseKind com.sun.source.tree.CaseTree.getCaseKind()",
579                 "class: com.sun.source.tree.CaseTree.CaseKind",
580                 "field: com.sun.source.tree.CaseTree.CaseKind:STATEMENT",
581                 "field: com.sun.source.tree.CaseTree.CaseKind:RULE",
582                 "field: com.sun.source.tree.Tree.Kind:SWITCH_EXPRESSION",
583                 "interface: com.sun.source.tree.SwitchExpressionTree",
584                 "method: com.sun.source.tree.ExpressionTree com.sun.source.tree.SwitchExpressionTree.getExpression()",
585                 "method: java.util.List com.sun.source.tree.SwitchExpressionTree.getCases()",
586                 "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)",
587                 "method: java.lang.Object com.sun.source.util.TreeScanner.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)",
588                 "method: java.lang.Object com.sun.source.util.SimpleTreeVisitor.visitSwitchExpression(com.sun.source.tree.SwitchExpressionTree,java.lang.Object)",
589                 "method: java.lang.String java.lang.String.stripIndent()",
590                 "method: java.lang.String java.lang.String.translateEscapes()",
591                 "method: java.lang.String java.lang.String.formatted(java.lang.Object[])",
592                 "class: javax.swing.plaf.basic.motif.MotifLookAndFeel",
593                 "field: com.sun.source.tree.Tree.Kind:YIELD",
594                 "interface: com.sun.source.tree.YieldTree",
595                 "method: com.sun.source.tree.ExpressionTree com.sun.source.tree.YieldTree.getValue()",
596                 "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitYield(com.sun.source.tree.YieldTree,java.lang.Object)",
597                 "method: java.lang.Object com.sun.source.util.SimpleTreeVisitor.visitYield(com.sun.source.tree.YieldTree,java.lang.Object)",
598                 "method: java.lang.Object com.sun.source.util.TreeScanner.visitYield(com.sun.source.tree.YieldTree,java.lang.Object)"
599         ));
600 
601         LEGACY_PREVIEW_METHODS.put("14", Set.of(
602                 "module: jdk.jpackage",
603                 "class: javax.swing.plaf.basic.motif.MotifLookAndFeel",
604                 "field: jdk.jshell.Snippet.SubKind:RECORD_SUBKIND",
605                 "class: javax.lang.model.element.RecordComponentElement",
606                 "method: javax.lang.model.type.TypeMirror javax.lang.model.element.RecordComponentElement.asType()",
607                 "method: java.lang.Object javax.lang.model.element.ElementVisitor.visitRecordComponent(javax.lang.model.element.RecordComponentElement,java.lang.Object)",
608                 "class: javax.lang.model.util.ElementScanner14",
609                 "class: javax.lang.model.util.AbstractElementVisitor14",
610                 "class: javax.lang.model.util.SimpleElementVisitor14",
611                 "method: java.lang.Object javax.lang.model.util.ElementKindVisitor6.visitTypeAsRecord(javax.lang.model.element.TypeElement,java.lang.Object)",
612                 "class: javax.lang.model.util.ElementKindVisitor14",
613                 "method: javax.lang.model.element.RecordComponentElement javax.lang.model.util.Elements.recordComponentFor(javax.lang.model.element.ExecutableElement)",
614                 "method: java.util.List javax.lang.model.util.ElementFilter.recordComponentsIn(java.lang.Iterable)",
615                 "method: java.util.Set javax.lang.model.util.ElementFilter.recordComponentsIn(java.util.Set)",
616                 "method: java.util.List javax.lang.model.element.TypeElement.getRecordComponents()",
617                 "field: javax.lang.model.element.ElementKind:RECORD",
618                 "field: javax.lang.model.element.ElementKind:RECORD_COMPONENT",
619                 "field: javax.lang.model.element.ElementKind:BINDING_VARIABLE",
620                 "field: com.sun.source.tree.Tree.Kind:RECORD",
621                 "field: sun.reflect.annotation.TypeAnnotation.TypeAnnotationTarget:RECORD_COMPONENT",
622                 "class: java.lang.reflect.RecordComponent",
623                 "class: java.lang.runtime.ObjectMethods",
624                 "field: java.lang.annotation.ElementType:RECORD_COMPONENT",
625                 "method: boolean java.lang.Class.isRecord()",
626                 "method: java.lang.reflect.RecordComponent[] java.lang.Class.getRecordComponents()",
627                 "class: java.lang.Record",
628                 "interface: com.sun.source.tree.PatternTree",
629                 "field: com.sun.source.tree.Tree.Kind:BINDING_PATTERN",
630                 "method: com.sun.source.tree.PatternTree com.sun.source.tree.InstanceOfTree.getPattern()",
631                 "interface: com.sun.source.tree.BindingPatternTree",
632                 "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitBindingPattern(com.sun.source.tree.BindingPatternTree,java.lang.Object)"
633         ));
634 
635         LEGACY_PREVIEW_METHODS.put("15", Set.of(
636                 "module: jdk.jpackage",
637                 "field: jdk.jshell.Snippet.SubKind:RECORD_SUBKIND",
638                 "class: javax.lang.model.element.RecordComponentElement",
639                 "method: javax.lang.model.type.TypeMirror javax.lang.model.element.RecordComponentElement.asType()",
640                 "method: java.lang.Object javax.lang.model.element.ElementVisitor.visitRecordComponent(javax.lang.model.element.RecordComponentElement,java.lang.Object)",
641                 "class: javax.lang.model.util.ElementScanner14",
642                 "class: javax.lang.model.util.AbstractElementVisitor14",
643                 "class: javax.lang.model.util.SimpleElementVisitor14",
644                 "method: java.lang.Object javax.lang.model.util.ElementKindVisitor6.visitTypeAsRecord(javax.lang.model.element.TypeElement,java.lang.Object)",
645                 "class: javax.lang.model.util.ElementKindVisitor14",
646                 "method: javax.lang.model.element.RecordComponentElement javax.lang.model.util.Elements.recordComponentFor(javax.lang.model.element.ExecutableElement)",
647                 "method: java.util.List javax.lang.model.util.ElementFilter.recordComponentsIn(java.lang.Iterable)",
648                 "method: java.util.Set javax.lang.model.util.ElementFilter.recordComponentsIn(java.util.Set)",
649                 "method: java.util.List javax.lang.model.element.TypeElement.getRecordComponents()",
650                 "field: javax.lang.model.element.ElementKind:RECORD",
651                 "field: javax.lang.model.element.ElementKind:RECORD_COMPONENT",
652                 "field: javax.lang.model.element.ElementKind:BINDING_VARIABLE",
653                 "field: com.sun.source.tree.Tree.Kind:RECORD",
654                 "field: sun.reflect.annotation.TypeAnnotation.TypeAnnotationTarget:RECORD_COMPONENT",
655                 "class: java.lang.reflect.RecordComponent",
656                 "class: java.lang.runtime.ObjectMethods",
657                 "field: java.lang.annotation.ElementType:RECORD_COMPONENT",
658                 "class: java.lang.Record",
659                 "method: boolean java.lang.Class.isRecord()",
660                 "method: java.lang.reflect.RecordComponent[] java.lang.Class.getRecordComponents()",
661                 "field: javax.lang.model.element.Modifier:SEALED",
662                 "field: javax.lang.model.element.Modifier:NON_SEALED",
663                 "method: javax.lang.model.element.TypeElement:getPermittedSubclasses:()",
664                 "method: java.util.List com.sun.source.tree.ClassTree.getPermitsClause()",
665                 "method: boolean java.lang.Class.isSealed()",
666                 "method: java.lang.constant.ClassDesc[] java.lang.Class.permittedSubclasses()",
667                 "interface: com.sun.source.tree.PatternTree",
668                 "field: com.sun.source.tree.Tree.Kind:BINDING_PATTERN",
669                 "method: com.sun.source.tree.PatternTree com.sun.source.tree.InstanceOfTree.getPattern()",
670                 "interface: com.sun.source.tree.BindingPatternTree",
671                 "method: java.lang.Object com.sun.source.tree.TreeVisitor.visitBindingPattern(com.sun.source.tree.BindingPatternTree,java.lang.Object)"
672         ));
673 
674         LEGACY_PREVIEW_METHODS.put("16", Set.of(
675                 "field: jdk.jshell.Snippet.SubKind:RECORD_SUBKIND",
676                 "field: javax.lang.model.element.Modifier:SEALED",
677                 "field: javax.lang.model.element.Modifier:NON_SEALED",
678                 "method: javax.lang.model.element.TypeElement:getPermittedSubclasses:()",
679                 "method: java.util.List com.sun.source.tree.ClassTree.getPermitsClause()",
680                 "method: boolean java.lang.Class.isSealed()",
681                 "method: java.lang.constant.ClassDesc[] java.lang.Class.permittedSubclasses()"
682         ));
683 
684         // java.lang.foreign existed since JDK 19 and wasn't annotated - went out of preview in JDK 22
685         LEGACY_PREVIEW_METHODS.put("19", Set.of(
686                 "package: java.lang.foreign"
687         ));
688         LEGACY_PREVIEW_METHODS.put("20", Set.of(
689                 "package: java.lang.foreign"
690         ));
691         LEGACY_PREVIEW_METHODS.put("21", Set.of(
692                 "package: java.lang.foreign"
693         ));
694     }
695 
696     /**
697      * Helper to find javadoc and resolve @inheritDoc and the effective since version.
698      */
699 
700     private final class EffectiveSourceSinceHelper implements AutoCloseable {
701         private static final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
702         private final JavaFileManager baseFileManager;
703         private final StandardJavaFileManager fm;
704         private final Set<String> seenLookupElements = new HashSet<>();
705         private final Map<String, Version> signature2Source = new HashMap<>();
706         private final Map<String, String> signature2Location = new HashMap<>();
707 
708         /**
709          * Create the helper.
710          *
711          * @param mainTask JavacTask from which the further Elements originate
712          * @param sourceLocations paths where source files should be searched
713          * @param validator enclosing class of the helper, typically the object invoking this method
714          * @return a EffectiveSourceSinceHelper
715          */
716 
717         public static EffectiveSourceSinceHelper create(JavacTask mainTask, Collection<? extends Path> sourceLocations, SinceChecker validator) {
718             StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null);
719             try {
720                 fm.setLocationFromPaths(StandardLocation.MODULE_SOURCE_PATH, sourceLocations);
721                 return validator.new EffectiveSourceSinceHelper(mainTask, fm);
722             } catch (IOException ex) {
723                 try {
724                     fm.close();
725                 } catch (IOException closeEx) {
726                     ex.addSuppressed(closeEx);
727                 }
728                 throw new UncheckedIOException(ex);
729             }
730         }
731 
732         private EffectiveSourceSinceHelper(JavacTask mainTask, StandardJavaFileManager fm) {
733             this.baseFileManager = ((JavacTaskImpl) mainTask).getContext().get(JavaFileManager.class);
734             this.fm = fm;
735         }
736 
737         public Version effectiveSinceVersion(Element owner, Element element, Types typeUtils, Elements elementUtils) {
738             String handle = getElementName(owner, element, typeUtils);
739             Version since = signature2Source.get(handle);
740 
741             if (since == null) {
742                 try {
743                     Element lookupElement = switch (element.getKind()) {
744                         case MODULE, PACKAGE -> element;
745                         default -> elementUtils.getOutermostTypeElement(element);
746                     };
747 
748                     if (lookupElement == null)
749                         return null;
750 
751                     String lookupHandle = getElementName(owner, element, typeUtils);
752 
753                     if (!seenLookupElements.add(lookupHandle)) {
754                         //we've already processed this top-level, don't try to compute
755                         //the values again:
756                         return null;
757                     }
758 
759                     Pair<JavacTask, CompilationUnitTree> source = findSource(lookupElement, elementUtils);
760 
761                     if (source == null)
762                         return null;
763 
764                     fillElementCache(source.fst, source.snd, source.fst.getTypes(), source.fst.getElements());
765                     since = signature2Source.get(handle);
766 
767                 } catch (IOException ex) {
768                     error("JavadocHelper failed for " + element);
769                 }
770             }
771 
772             return since;
773         }
774 
775         private String getElementPosition(String signature) {
776             return signature2Location.getOrDefault(signature, "");
777         }
778 
779         //where:
780         private void fillElementCache(JavacTask task, CompilationUnitTree cut, Types typeUtils, Elements elementUtils) {
781             Trees trees = Trees.instance(task);
782             String fileName = cut.getSourceFile().getName();
783 
784             new TreePathScanner<Void, Void>() {
785                 @Override
786                 public Void visitMethod(MethodTree node, Void p) {
787                     handleDeclaration(node, fileName);
788                     return null;
789                 }
790 
791                 @Override
792                 public Void visitClass(ClassTree node, Void p) {
793                     handleDeclaration(node, fileName);
794                     return super.visitClass(node, p);
795                 }
796 
797                 @Override
798                 public Void visitVariable(VariableTree node, Void p) {
799                     handleDeclaration(node, fileName);
800                     return null;
801                 }
802 
803                 @Override
804                 public Void visitModule(ModuleTree node, Void p) {
805                     handleDeclaration(node, fileName);
806                     return null;
807                 }
808 
809                 @Override
810                 public Void visitBlock(BlockTree node, Void p) {
811                     return null;
812                 }
813 
814                 @Override
815                 public Void visitPackage(PackageTree node, Void p) {
816                     if (cut.getSourceFile().isNameCompatible("package-info", JavaFileObject.Kind.SOURCE)) {
817                         handleDeclaration(node, fileName);
818                     }
819                     return super.visitPackage(node, p);
820                 }
821 
822                 private void handleDeclaration(Tree node, String fileName) {
823                     Element currentElement = trees.getElement(getCurrentPath());
824 
825                     if (currentElement != null) {
826                         long startPosition = trees.getSourcePositions().getStartPosition(cut, node);
827                         long lineNumber = cut.getLineMap().getLineNumber(startPosition);
828                         String filePathWithLineNumber = String.format("src%s:%s ", fileName, lineNumber);
829 
830                         signature2Source.put(getElementName(currentElement.getEnclosingElement(), currentElement, typeUtils), computeSinceVersion(currentElement, typeUtils, elementUtils));
831                         signature2Location.put(getElementName(currentElement.getEnclosingElement(), currentElement, typeUtils), filePathWithLineNumber);
832                     }
833                 }
834             }.scan(cut, null);
835         }
836 
837         private Version computeSinceVersion(Element element, Types types,
838                                             Elements elementUtils) {
839             String docComment = elementUtils.getDocComment(element);
840             Version version = null;
841             if (docComment != null) {
842                 version = extractSinceVersionFromText(docComment);
843             }
844 
845             if (version != null) {
846                 return version; //explicit @since has an absolute priority
847             }
848 
849             if (element.getKind() != ElementKind.MODULE) {
850                 version = effectiveSinceVersion(element.getEnclosingElement().getEnclosingElement(), element.getEnclosingElement(), types, elementUtils);
851             }
852 
853             return version;
854         }
855 
856         private Pair<JavacTask, CompilationUnitTree> findSource(Element forElement, Elements elementUtils) throws IOException {
857             String moduleName = elementUtils.getModuleOf(forElement).getQualifiedName().toString();
858             String binaryName = switch (forElement.getKind()) {
859                 case MODULE -> "module-info";
860                 case PACKAGE -> ((QualifiedNameable) forElement).getQualifiedName() + ".package-info";
861                 default -> elementUtils.getBinaryName((TypeElement) forElement).toString();
862             };
863             Location packageLocationForModule = fm.getLocationForModule(StandardLocation.MODULE_SOURCE_PATH, moduleName);
864             JavaFileObject jfo = fm.getJavaFileForInput(packageLocationForModule,
865                     binaryName,
866                     JavaFileObject.Kind.SOURCE);
867 
868             if (jfo == null)
869                 return null;
870 
871             List<JavaFileObject> jfos = Arrays.asList(jfo);
872             JavaFileManager patchFM = moduleName != null
873                     ? new PatchModuleFileManager(baseFileManager, jfo, moduleName)
874                     : baseFileManager;
875             JavacTaskImpl task = (JavacTaskImpl) compiler.getTask(null, patchFM, d -> {
876             }, null, null, jfos);
877             Iterable<? extends CompilationUnitTree> cuts = task.parse();
878 
879             task.enter();
880 
881             return Pair.of(task, cuts.iterator().next());
882         }
883 
884         @Override
885         public void close() throws IOException {
886             fm.close();
887         }
888 
889         /**
890          * Manages files within a patch module.
891          * Provides custom behavior for handling file locations within a patch module.
892          * Includes methods to specify module locations, infer module names and determine
893          * if a location belongs to the patch module path.
894          */
895         private static final class PatchModuleFileManager
896                 extends ForwardingJavaFileManager<JavaFileManager> {
897 
898             private final JavaFileObject file;
899             private final String moduleName;
900 
901             public PatchModuleFileManager(JavaFileManager fileManager,
902                                           JavaFileObject file,
903                                           String moduleName) {
904                 super(fileManager);
905                 this.file = file;
906                 this.moduleName = moduleName;
907             }
908 
909             @Override
910             public Location getLocationForModule(Location location,
911                                                  JavaFileObject fo) throws IOException {
912                 return fo == file
913                         ? PATCH_LOCATION
914                         : super.getLocationForModule(location, fo);
915             }
916 
917             @Override
918             public String inferModuleName(Location location) throws IOException {
919                 return location == PATCH_LOCATION
920                         ? moduleName
921                         : super.inferModuleName(location);
922             }
923 
924             @Override
925             public boolean hasLocation(Location location) {
926                 return location == StandardLocation.PATCH_MODULE_PATH ||
927                         super.hasLocation(location);
928             }
929 
930             private static final Location PATCH_LOCATION = new Location() {
931                 @Override
932                 public String getName() {
933                     return "PATCH_LOCATION";
934                 }
935 
936                 @Override
937                 public boolean isOutputLocation() {
938                     return false;
939                 }
940 
941                 @Override
942                 public boolean isModuleOrientedLocation() {
943                     return false;
944                 }
945             };
946         }
947     }
948 }