Analyzing Documentation Comments: Example3

Download

/*
 * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   - Neither the name of Oracle nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * This source code is provided to illustrate the usage of a given feature
 * or technique and has been deliberately simplified. Additional steps
 * required for a production-quality application, such as security checks,
 * input validation and proper error handling, might not be present in
 * this sample code.
 */

package p;

import com.sun.source.doctree.AttributeTree;
import com.sun.source.doctree.DocCommentTree;
import com.sun.source.doctree.StartElementTree;
import com.sun.source.util.DocSourcePositions;
import com.sun.source.util.DocTreeScanner;
import com.sun.source.util.DocTrees;
import com.sun.source.util.JavacTask;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ModuleElement;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleElementVisitor14;
import javax.tools.ToolProvider;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Example3 {
    public static void main(String... args) throws IOException {
        try {
            var ok = new Example3().run(args);
            if (!ok) {
                System.exit(1);
            }
        } catch (IOException e) {
            System.err.println("IO Exception: " + e.getMessage());
            System.exit(2);
        }
    }

    public boolean run(String... args) throws IOException {
        var options = List.of("-proc:only");

        var files = findJavaFiles(Stream.of(args)
                .map(Path::of)
                .toList());

        var c = ToolProvider.getSystemJavaCompiler();
        var fm = c.getStandardFileManager(null, Locale.getDefault(), StandardCharsets.UTF_8);
        var fileObjects = fm.getJavaFileObjectsFromPaths(files);
        var t = (JavacTask) c.getTask(null, fm, null, options, null, fileObjects);
        t.setProcessors(List.of(new AbstractProcessor() {
            @Override
            public Set<String> getSupportedAnnotationTypes() {
                return Set.of("*");
            }

            @Override
            public SourceVersion getSupportedSourceVersion() {
                return SourceVersion.RELEASE_17;
            }

            @Override
            public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
                roundEnv.getRootElements().forEach(this::processRootElement);
                return false;
            }

            private void processRootElement(Element e) {
                var pe = elements.getPackageOf(e);
                if (pe != null) {
                    var me = elements.getModuleOf(e);
                    if (me != null) {
                        var exported = me.getDirectives().stream()
                                .filter(d -> d.getKind() == ModuleElement.DirectiveKind.EXPORTS)
                                .map(ModuleElement.ExportsDirective.class::cast)
                                .anyMatch(ed -> ed.getPackage() == pe);
                        if (!exported) {
                            return;
                        }
                    }
                }

                out.println("*** root element " + e);
                var linkScanner = new LinkScanner();
                declScanner.scan(e, linkScanner);
                linkScanner.urls.forEach((u -> out.println("   " + u)));
            }
        }));

        elements = t.getElements();
        docTrees = DocTrees.instance(t);
        positions = docTrees.getSourcePositions();
        t.call();

        if (errors == 0) {
            return true;
        } else {
            log.println(errors + " errors");
            return false;
        }

    }

    private PrintStream out = System.out;
    private PrintStream log = System.err;

    private Path userDir = Path.of(System.getProperty("user.dir"));
    private int errors;
    private Elements elements;
    private DocTrees docTrees;
    private DocSourcePositions positions;
    private DeclScanner declScanner = new DeclScanner();

    List<Path> findJavaFiles(List<Path> files) throws IOException {
        List<Path> list = new ArrayList<>();
        for (var f: files) {
            if (Files.isRegularFile(f) && f.getFileName().toString().endsWith(".java")) {
                list.add(f);
            } else if (Files.isDirectory(f)) {
                Files.walkFileTree(f, new SimpleFileVisitor<>() {
                    @Override
                    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                        return dir.getFileName().toString().equals("internal")
                                ? FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE;
                    }

                    @Override
                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                        if (file.getFileName().toString().endsWith(".java")) {
                            list.add(file);
                        }
                        return FileVisitResult.CONTINUE;
                    }
                });
            }
        }
        list.sort(Comparator.naturalOrder());
        return list;
    }

    void error(Path file, String message) {
        log.println(file + ": " + message);
        errors++;
    }

    class DeclScanner extends SimpleElementVisitor14<Void, LinkScanner> {
        public void scan(Element e, LinkScanner linkScanner) {
            e.accept(this, linkScanner);
        }

        public Void defaultAction(Element e, LinkScanner s) {
            var dct = docTrees.getDocCommentTree(e);
            if (dct != null) {
                s.scan(e, dct);
            }
            return null;
        }

        @Override
        public Void visitType(TypeElement e, LinkScanner specScanner) {
            defaultAction(e, specScanner);
            e.getEnclosedElements().stream()
                    .forEachOrdered(ee -> ee.accept(this, specScanner));
            return null;
        }
    }

    class LinkScanner extends DocTreeScanner<Void, Void> {
        final Set<String> urls = new TreeSet<>();

        private Element element;
        private DocCommentTree docComment;

        private StartElementTree startTree;

        LinkScanner() {
        }

        void scan(Element element, DocCommentTree docComment) {
            this.element = element;
            this.docComment = docComment;
            startTree = null;
            docComment.accept(this, null);
        }

        @Override
        public Void visitStartElement(StartElementTree tree, Void p) {
            if (matches(tree.getName(), "a")) {
                startTree = tree;

                try {
                    // visit attributes
                    super.visitStartElement(tree, p);
                } finally {
                    startTree = null;
                }
            }

            return null;
        }

        @Override
        public Void visitAttribute(AttributeTree tree, Void p) {
            if (startTree != null && matches(tree.getName(), "href")) {
                var url = tree.getValue().stream().map(Object::toString).collect(Collectors.joining());
                urls.add(url);
            }
            return null;
        }

        private boolean matches(Name tagName, String s) {
            return tagName.toString().equalsIgnoreCase(s);
        }
    }
}