/* * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions.
*/
/** * Utilities for analyzing snippets. * * Support is provided for the following: * <ul> * <li>creating an instance of {@link JavacTask} suitable for looking up * elements by name, in order to access any corresponding documentation comment, * <li>scanning elements to find all associated snippets, * <li>locating instances of snippets by their {@code id}, * <li>parsing snippets, and * <li>accessing the body of snippets, for any additional analysis. * </ul> * * @apiNote * The utilities do not provide support for compiling and running snippets, * because in general, this requires too much additional context. However, * the utilities do provide support for locating snippets in various ways, * and accessing the body of those snippets, to simplify the task of writing * code to compile and run snippets, where that is appropriate.
*/ publicclass SnippetUtils { /** * Exception used to report a configuration issue that prevents * the test from executing as expected.
*/ publicstaticclass ConfigurationException extends Exception { public ConfigurationException(String message) { super(message);
}
}
/** * Exception used to report that a snippet could not be found.
*/ publicstaticclass SnippetNotFoundException extends Exception { public SnippetNotFoundException(String message) { super(message);
}
}
/** * Exception used to report that a doc comment could not be found.
*/ publicstaticclass DocCommentNotFoundException extends Exception { public DocCommentNotFoundException(String message) { super(message);
}
}
/** * Creates an instance for analysing snippets in one or more JDK modules. * * The source for the modules is derived from the value of the * {@code test.src} system property. * * Any messages, including error messages, will be written to {@code System.err}. * * @param modules the modules * * @throws IllegalArgumentException if no modules are specified * @throws ConfigurationException if the main source directory cannot be found * or if a module's source directory cannot be found
*/ public SnippetUtils(String... modules) throws ConfigurationException { this(findSourceDir(), null, null, Set.of(modules));
}
/** * Creates an instance for analysing snippets in one or more modules. * * @param srcDir the location for the source of the modules; * the location for the source of a specific module should be * in <em>srcDir</em>{@code /}<em>module</em>{@code /share/module} * * @param pw a writer for any text messages that may be generated; * if null, messages will be written to {@code System.err} * * @param dl a diagnostic listener for any diagnostic messages that may be generated; * if null, messages will be written to {@code System.err} * * @param modules the modules * * @throws IllegalArgumentException if no modules are specified * @throws ConfigurationException if {@code srcDir} does not exist * or if a module's source directory cannot be found
*/ public SnippetUtils(Path srcDir, PrintWriter pw, DiagnosticListener<JavaFileObject> dl, Set<String> modules) throws ConfigurationException { if (modules.isEmpty()) { thrownew IllegalArgumentException("no modules specified");
}
if (!Files.exists(srcDir)) { thrownew ConfigurationException("directory not found: " + srcDir);
}
this.srcDir = srcDir;
for (var m : modules) { var moduleSourceDir = getModuleSourceDir(m); if (!Files.exists(moduleSourceDir)) { thrownew ConfigurationException(("cannot find source directory for " + m
+ ": " + moduleSourceDir));
}
}
List<String> opts = new ArrayList<>();
opts.addAll(List.of("--add-modules", String.join(",", modules))); // could use CompilationTask.addModules
modules.forEach(m -> opts.addAll(List.of("--patch-module", m + "=" + getModuleSourceDir(m))));
opts.add("-proc:only");
javacTask = (JavacTask) compiler.getTask(pw, fileManager, dl, opts, null, null);
elements = javacTask.getElements();
elements.getModuleElement("java.base"); // forces module graph to be instantiated, etc
docTrees = DocTrees.instance(javacTask);
}
/** * {@return the source directory for the task used to access snippets}
*/ public Path getSourceDir() { return srcDir;
}
/** * {@return the file manager for the task used to access snippets}
*/ public StandardJavaFileManager getFileManager() { return fileManager;
}
/** * {@return the instance of {@code Elements} for the task used to access snippets}
*/ public Elements getElements() { return elements;
}
/** * {@return the instance of {@code DocTrees} for the task used to access snippets}
*/ public DocTrees getDocTrees() { return docTrees;
}
/** * {@return the doc comment tree for an element} * * @param element the element
*/ public DocCommentTree getDocCommentTree(Element element) { return docTrees.getDocCommentTree(element);
}
/** * {@return the snippet with a given id in a doc comment tree} * * @param tree the doc comment tree * @param id the id * * @throws SnippetNotFoundException if the snippet cannot be found
*/ public SnippetTree getSnippetById(DocCommentTree tree, String id) throws SnippetNotFoundException {
SnippetTree result = new SnippetFinder().scan(tree, id); if (result == null) { thrownew SnippetNotFoundException(id);
} return result;
}
/** * {@return the snippet with a given id in the doc comment tree for an element} * * @param element the element * @param id the id * * @throws DocCommentNotFoundException if the doc comment for the element cannot be found * @throws SnippetNotFoundException if the snippet cannot be found
*/ public SnippetTree getSnippetById(Element element, String id) throws DocCommentNotFoundException, SnippetNotFoundException {
DocCommentTree docCommentTree = getDocCommentTree(element); if (docCommentTree == null) { var name = (element instanceof QualifiedNameable q) ? q.getQualifiedName() : element.getSimpleName(); thrownew DocCommentNotFoundException(element.getKind() + " " + name);
} return getSnippetById(docCommentTree, id);
}
/** * A scanner to locate the tree for a snippet with a given id. * Note: the scanner is use-once.
*/ privatestaticclass SnippetFinder extends DocTreeScanner<SnippetTree,String> { private SnippetTree result; private SnippetTree inSnippet;
@Override public SnippetTree scan(DocTree tree, String id) { // stop scanning once the result has been found return result != null ? result : super.scan(tree, id);
}
@Override public SnippetTree visitAttribute(AttributeTree tree, String id) { if (tree.getName().contentEquals("id")
&& tree.getValue().toString().equals(id)) {
result = inSnippet; return result;
} else { returnnull;
}
}
}
/** * Scans an element and appropriate enclosed elements for doc comments, * and call a handler to handle any snippet trees in those doc comments. * * Only the public and protected members of type elements are scanned. * The enclosed elements of modules and packages are <em>not</em> scanned. * * @param element the element * @param handler the handler * @throws IllegalArgumentException if any inappropriate element is scanned
*/ publicvoid scan(Element element, BiConsumer<Element, SnippetTree> handler) { new ElementScanner(docTrees).scan(element, handler);
}
privatevoid scanDocComment(Element e, DocTreeScanner<Void, Element> treeScanner) {
DocCommentTree dc = trees.getDocCommentTree(e); if (dc != null) {
treeScanner.scan(dc, e);
}
}
}
/** * {@return the string content of an inline or hybrid snippet, or {@code null} for an external snippet} * * @param tree the snippet
*/ public String getBody(SnippetTree tree) {
TextTree body = tree.getBody(); return body == null ? null : body.getBody();
}
/** * {@return the string content of an external or inline snippet} * * @param element the element whose documentation contains the snippet * @param tree the snippet
*/ public String getBody(Element element, SnippetTree tree) throws IOException {
Path externalSnippetPath = getExternalSnippetPath(element, tree); return externalSnippetPath == null ? getBody(tree) : Files.readString(externalSnippetPath);
}
/** * {@return the path for the {@code snippet-files} directory for an element} * * @param element the element * * @return the path
*/ public Path getSnippetFilesDir(Element element) { var moduleElem = elements.getModuleOf(element); var modulePath = getModuleSourceDir(moduleElem);
var packageElem = elements.getPackageOf(element); // null for a module var packagePath = packageElem == null
? modulePath
: modulePath.resolve(packageElem.getQualifiedName().toString().replace(".", File.separator));
return packagePath.resolve("snippet-files");
}
/** * {@return the path for an external snippet, or {@code null} if the snippet is inline} * * @param element the element whose documentation contains the snippet * @param tree the snippet
*/ public Path getExternalSnippetPath(Element element, SnippetTree tree) { var classAttr = getAttr(tree, "class");
String file = (classAttr != null)
? classAttr.replace(".", "/") + ".java"
: getAttr(tree, "file"); return file == null ? null : getSnippetFilesDir(element).resolve(file.replace("/", File.separator));
}
/** * {@return the value of an attribute defined by a snippet} * * @param tree the snippet * @param name the name of the attribute
*/ public String getAttr(SnippetTree tree, String name) { for (DocTree t : tree.getAttributes()) { if (t instanceof AttributeTree at && at.getName().contentEquals(name)) { return at.getValue().toString();
}
} returnnull;
}
/** * {@return the primary source directory for a module} * * The directory is <em>srcDir</em>/<em>module-name</em>/share/classes. * * @param e the module
*/ public Path getModuleSourceDir(ModuleElement e) { return getModuleSourceDir(e.getQualifiedName().toString());
}
/** * {@return the primary source directory for a module} * * The directory is <em>srcDir</em>/<em>moduleName</em>/share/classes. * * @param moduleName the module name
*/ public Path getModuleSourceDir(String moduleName) { return srcDir.resolve(moduleName).resolve("share").resolve("classes");
}
/** * Kinds of fragments of source code.
*/ publicenum SourceKind { /** A module declaration. */
MODULE_INFO, /** A package declaration. */
PACKAGE_INFO, /** A class or interface declaration. */
TYPE_DECL, /** A member declaration for a class or interface. */
MEMBER_DECL, /** A statement, expression or other kind of fragment. */
OTHER
}
/** * Parses a fragment of source code, after trying to infer the kind of the fragment. * * @param body the string to be parsed * @param showDiag a function to handle any diagnostics that may be generated * @return {@code true} if the parse succeeded, and {@code false} otherwise * * @throws IOException if an IO exception occurs
*/ publicboolean parse(String body, Consumer<? super Diagnostic<? extends JavaFileObject>> showDiag) throws IOException {
DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
parse(body, null, collector); var diags = collector.getDiagnostics();
diags.forEach(showDiag); return diags.isEmpty();
}
/** * Parses a fragment of source code, after trying to infer the kind of the fragment. * * @param body the string to be parsed * @param pw a stream for diagnostics, or {@code null} to use {@code System.err} * @param dl a diagnostic listener, or {@code null} to report diagnostics to {@code pw} or {@code System.err} * @throws IOException if an IO exception occurs
*/ publicvoid parse(String body, PrintWriter pw, DiagnosticListener<JavaFileObject> dl) throws IOException {
parse(inferSourceKind(body), body, pw, dl);
}
/** * Parses a fragment of source code of a given kind. * * @param kind the kind of code to be parsed * @param body the string to be parsed * @param pw a stream for diagnostics, or {@code null} to use {@code System.err} * @param dl a diagnostic listener, or {@code null} to report diagnostics to {@code pw} or {@code System.err}. * @throws IOException if an IO exception occurs
*/ publicvoid parse(SourceKind kind, String body, PrintWriter pw, DiagnosticListener<JavaFileObject> dl) throws IOException {
String fileBase = switch (kind) { case MODULE_INFO -> "module-info"; case PACKAGE_INFO -> "package-info"; default -> "C"; // the exact name doesn't matter if just parsing (the filename check for public types comes later on)
};
URI uri = URI.create("mem://%s.java".formatted(fileBase));
String compUnit = switch (kind) { case MODULE_INFO, PACKAGE_INFO, TYPE_DECL -> body; case MEMBER_DECL -> """ class C {
%s
}""".formatted(body); case OTHER -> """ class C { void m() {
%s
;
}
}""".formatted(body);
};
JavaFileObject fo = new SimpleJavaFileObject(uri, JavaFileObject.Kind.SOURCE) { public CharSequence getCharContent(boolean ignoreEncodingErrors) { return compUnit;
}
};
JavaFileManager fm = compiler.getStandardFileManager(dl, null, null);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.