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