DocCommentTester.java revision 3060:23f76aadbb36
1/*
2 * Copyright (c) 2012, 2015, 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
24import java.io.File;
25import java.io.FileWriter;
26import java.io.IOException;
27import java.io.PrintWriter;
28import java.io.StringWriter;
29import java.io.Writer;
30import java.util.ArrayList;
31import java.util.List;
32import java.util.regex.Matcher;
33import java.util.regex.Pattern;
34
35import javax.lang.model.element.Name;
36import javax.tools.JavaFileObject;
37import javax.tools.StandardJavaFileManager;
38
39import com.sun.source.doctree.*;
40import com.sun.source.tree.ClassTree;
41import com.sun.source.tree.CompilationUnitTree;
42import com.sun.source.tree.MethodTree;
43import com.sun.source.tree.Tree;
44import com.sun.source.tree.VariableTree;
45import com.sun.source.util.DocTreeScanner;
46import com.sun.source.util.DocTrees;
47import com.sun.source.util.JavacTask;
48import com.sun.source.util.TreePath;
49import com.sun.source.util.TreePathScanner;
50import com.sun.tools.javac.api.JavacTool;
51import com.sun.tools.javac.tree.DCTree;
52import com.sun.tools.javac.tree.DCTree.DCDocComment;
53import com.sun.tools.javac.tree.DCTree.DCErroneous;
54import com.sun.tools.javac.tree.DocPretty;
55
56public class DocCommentTester {
57
58    public static void main(String... args) throws Exception {
59        new DocCommentTester().run(args);
60    }
61
62    public void run(String... args) throws Exception {
63        String testSrc = System.getProperty("test.src");
64
65        List<File> files = new ArrayList<File>();
66        for (String arg: args)
67            files.add(new File(testSrc, arg));
68
69        JavacTool javac = JavacTool.create();
70        StandardJavaFileManager fm = javac.getStandardFileManager(null, null, null);
71
72        Iterable<? extends JavaFileObject> fos = fm.getJavaFileObjectsFromFiles(files);
73
74        JavacTask t = javac.getTask(null, fm, null, null, null, fos);
75        final DocTrees trees = DocTrees.instance(t);
76
77        final Checker[] checkers = {
78            new ASTChecker(this, trees),
79            new PosChecker(this, trees),
80            new PrettyChecker(this, trees)
81        };
82
83        DeclScanner d = new DeclScanner() {
84            @Override
85            public Void visitCompilationUnit(CompilationUnitTree tree, Void ignore) {
86                for (Checker c: checkers)
87                    c.visitCompilationUnit(tree);
88                return super.visitCompilationUnit(tree, ignore);
89            }
90
91            @Override
92            void visitDecl(Tree tree, Name name) {
93                TreePath path = getCurrentPath();
94                String dc = trees.getDocComment(path);
95                if (dc != null) {
96                    for (Checker c : checkers) {
97                        try {
98                            System.err.println(path.getLeaf().getKind()
99                                    + " " + name
100                                    + " " + c.getClass().getSimpleName());
101
102                            c.check(path, name);
103
104                            System.err.println();
105                        } catch (Exception e) {
106                            error("Exception " + e);
107                            e.printStackTrace(System.err);
108                        }
109                    }
110                }
111            }
112        };
113
114        Iterable<? extends CompilationUnitTree> units = t.parse();
115        for (CompilationUnitTree unit: units) {
116            d.scan(unit, null);
117        }
118
119        if (errors > 0)
120            throw new Exception(errors + " errors occurred");
121    }
122
123    static abstract class DeclScanner extends TreePathScanner<Void, Void> {
124        abstract void visitDecl(Tree tree, Name name);
125
126        @Override
127        public Void visitClass(ClassTree tree, Void ignore) {
128            super.visitClass(tree, ignore);
129            visitDecl(tree, tree.getSimpleName());
130            return null;
131        }
132
133        @Override
134        public Void visitMethod(MethodTree tree, Void ignore) {
135            super.visitMethod(tree, ignore);
136            visitDecl(tree, tree.getName());
137            return null;
138        }
139
140        @Override
141        public Void visitVariable(VariableTree tree, Void ignore) {
142            super.visitVariable(tree, ignore);
143            visitDecl(tree, tree.getName());
144            return null;
145        }
146    }
147
148    /**
149     * Base class for checkers to check the doc comment on a declaration
150     * (when present.)
151     */
152    abstract class Checker {
153        final DocTrees trees;
154
155        Checker(DocTrees trees) {
156            this.trees = trees;
157        }
158
159        void visitCompilationUnit(CompilationUnitTree tree) { }
160
161        abstract void check(TreePath tree, Name name) throws Exception;
162
163        void error(String msg) {
164            DocCommentTester.this.error(msg);
165        }
166    }
167
168    void error(String msg) {
169        System.err.println("Error: " + msg);
170        errors++;
171    }
172
173    int errors;
174
175    /**
176     * Verify the structure of the DocTree AST by comparing it against golden text.
177     */
178    static class ASTChecker extends Checker {
179        static final String NEWLINE = System.getProperty("line.separator");
180        Printer printer = new Printer();
181        String source;
182
183        ASTChecker(DocCommentTester test, DocTrees t) {
184            test.super(t);
185        }
186
187        @Override
188        void visitCompilationUnit(CompilationUnitTree tree) {
189            try {
190                source = tree.getSourceFile().getCharContent(true).toString();
191            } catch (IOException e) {
192                source = "";
193            }
194        }
195
196        void check(TreePath path, Name name) {
197            StringWriter out = new StringWriter();
198            DocCommentTree dc = trees.getDocCommentTree(path);
199            printer.print(dc, out);
200            out.flush();
201            String found = out.toString().replace(NEWLINE, "\n");
202
203            // Look for the first block comment after the first occurrence of name
204            int start = source.indexOf("\n/*\n", findName(source, name));
205            int end = source.indexOf("\n*/\n", start);
206            String expect = source.substring(start + 4, end + 1);
207            if (!found.equals(expect)) {
208                System.err.println("Expect:\n" + expect);
209                System.err.println("Found:\n" + found);
210                error("AST mismatch for " + name);
211            }
212        }
213
214        /**
215         * This main program is to set up the golden comments used by this
216         * checker.
217         * Usage:
218         *     java DocCommentTester$ASTChecker -o dir file...
219         * The given files are written to the output directory with their
220         * golden comments updated. The intent is that the files should
221         * then be compared with the originals, e.g. with meld, and if the
222         * changes are approved, the new files can be used to replace the old.
223         */
224        public static void main(String... args) throws Exception {
225            List<File> files = new ArrayList<File>();
226            File o = null;
227            for (int i = 0; i < args.length; i++) {
228                String arg = args[i];
229                if (arg.equals("-o"))
230                    o = new File(args[++i]);
231                else if (arg.startsWith("-"))
232                    throw new IllegalArgumentException(arg);
233                else {
234                    files.add(new File(arg));
235                }
236            }
237
238            if (o == null)
239                throw new IllegalArgumentException("no output dir specified");
240            final File outDir = o;
241
242            JavacTool javac = JavacTool.create();
243            StandardJavaFileManager fm = javac.getStandardFileManager(null, null, null);
244            Iterable<? extends JavaFileObject> fos = fm.getJavaFileObjectsFromFiles(files);
245
246            JavacTask t = javac.getTask(null, fm, null, null, null, fos);
247            final DocTrees trees = DocTrees.instance(t);
248
249            DeclScanner d = new DeclScanner() {
250                Printer p = new Printer();
251                String source;
252
253                @Override
254                public Void visitCompilationUnit(CompilationUnitTree tree, Void ignore) {
255                    System.err.println("processing " + tree.getSourceFile().getName());
256                    try {
257                        source = tree.getSourceFile().getCharContent(true).toString();
258                    } catch (IOException e) {
259                        source = "";
260                    }
261                    // remove existing gold by removing all block comments after the first '{'.
262                    int start = source.indexOf("{");
263                    while ((start = source.indexOf("\n/*\n", start)) != -1) {
264                        int end = source.indexOf("\n*/\n");
265                        source = source.substring(0, start + 1) + source.substring(end + 4);
266                    }
267
268                    // process decls in compilation unit
269                    super.visitCompilationUnit(tree, ignore);
270
271                    // write the modified source
272                    File f = new File(tree.getSourceFile().getName());
273                    File outFile = new File(outDir, f.getName());
274                    try {
275                        FileWriter out = new FileWriter(outFile);
276                        try {
277                            out.write(source);
278                        } finally {
279                            out.close();
280                        }
281                    } catch (IOException e) {
282                        System.err.println("Can't write " + tree.getSourceFile().getName()
283                                + " to " + outFile + ": " + e);
284                    }
285                    return null;
286                }
287
288                @Override
289                void visitDecl(Tree tree, Name name) {
290                    DocTree dc = trees.getDocCommentTree(getCurrentPath());
291                    if (dc != null) {
292                        StringWriter out = new StringWriter();
293                        p.print(dc, out);
294                        String found = out.toString();
295
296                        // Look for the empty line after the first occurrence of name
297                        int pos = source.indexOf("\n\n", findName(source, name));
298
299                        // Insert the golden comment
300                        source = source.substring(0, pos)
301                                + "\n/*\n"
302                                + found
303                                + "*/"
304                                + source.substring(pos);
305                    }
306                }
307
308            };
309
310            Iterable<? extends CompilationUnitTree> units = t.parse();
311            for (CompilationUnitTree unit: units) {
312                d.scan(unit, null);
313            }
314        }
315
316        static int findName(String source, Name name) {
317            Pattern p = Pattern.compile("\\s" + name + "[(;]");
318            Matcher m = p.matcher(source);
319            if (!m.find())
320                throw new Error("cannot find " + name);
321            return m.start();
322        }
323
324        static class Printer implements DocTreeVisitor<Void, Void> {
325            PrintWriter out;
326
327            void print(DocTree tree, Writer out) {
328                this.out = (out instanceof PrintWriter)
329                        ? (PrintWriter) out : new PrintWriter(out);
330                tree.accept(this, null);
331                this.out.flush();
332            }
333
334            public Void visitAttribute(AttributeTree node, Void p) {
335                header(node);
336                indent(+1);
337                print("name", node.getName().toString());
338                print("vkind", node.getValueKind().toString());
339                print("value", node.getValue());
340                indent(-1);
341                indent();
342                out.println("]");
343                return null;
344            }
345
346            public Void visitAuthor(AuthorTree node, Void p) {
347                header(node);
348                indent(+1);
349                print("name", node.getName());
350                indent(-1);
351                indent();
352                out.println("]");
353                return null;
354            }
355
356            public Void visitComment(CommentTree node, Void p) {
357                header(node, compress(node.getBody()));
358                return null;
359            }
360
361            public Void visitDeprecated(DeprecatedTree node, Void p) {
362                header(node);
363                indent(+1);
364                print("body", node.getBody());
365                indent(-1);
366                indent();
367                out.println("]");
368                return null;
369            }
370
371            public Void visitDocComment(DocCommentTree node, Void p) {
372                header(node);
373                indent(+1);
374                print("firstSentence", node.getFirstSentence());
375                print("body", node.getBody());
376                print("block tags", node.getBlockTags());
377                indent(-1);
378                indent();
379                out.println("]");
380                return null;
381            }
382
383            public Void visitDocRoot(DocRootTree node, Void p) {
384                header(node, "");
385                return null;
386            }
387
388            public Void visitEndElement(EndElementTree node, Void p) {
389                header(node, node.getName().toString());
390                return null;
391            }
392
393            public Void visitEntity(EntityTree node, Void p) {
394                header(node, node.getName().toString());
395                return null;
396            }
397
398            public Void visitErroneous(ErroneousTree node, Void p) {
399                header(node);
400                indent(+1);
401                print("code", ((DCErroneous) node).diag.getCode());
402                print("body", compress(node.getBody()));
403                indent(-1);
404                indent();
405                out.println("]");
406                return null;
407            }
408
409            public Void visitIdentifier(IdentifierTree node, Void p) {
410                header(node, compress(node.getName().toString()));
411                return null;
412            }
413
414            public Void visitInheritDoc(InheritDocTree node, Void p) {
415                header(node, "");
416                return null;
417            }
418
419            public Void visitLink(LinkTree node, Void p) {
420                header(node);
421                indent(+1);
422                print("reference", node.getReference());
423                print("body", node.getLabel());
424                indent(-1);
425                indent();
426                out.println("]");
427                return null;
428            }
429
430            public Void visitLiteral(LiteralTree node, Void p) {
431                header(node, compress(node.getBody().getBody()));
432                return null;
433            }
434
435            public Void visitParam(ParamTree node, Void p) {
436                header(node);
437                indent(+1);
438                print("name", node.getName());
439                print("description", node.getDescription());
440                indent(-1);
441                indent();
442                out.println("]");
443                return null;
444            }
445
446            public Void visitReference(ReferenceTree node, Void p) {
447                header(node, compress(node.getSignature()));
448                return null;
449            }
450
451            public Void visitReturn(ReturnTree node, Void p) {
452                header(node);
453                indent(+1);
454                print("description", node.getDescription());
455                indent(-1);
456                indent();
457                out.println("]");
458                return null;
459            }
460
461            public Void visitSee(SeeTree node, Void p) {
462                header(node);
463                indent(+1);
464                print("reference", node.getReference());
465                indent(-1);
466                indent();
467                out.println("]");
468                return null;
469            }
470
471            public Void visitSerial(SerialTree node, Void p) {
472                header(node);
473                indent(+1);
474                print("description", node.getDescription());
475                indent(-1);
476                indent();
477                out.println("]");
478                return null;
479            }
480
481            public Void visitSerialData(SerialDataTree node, Void p) {
482                header(node);
483                indent(+1);
484                print("description", node.getDescription());
485                indent(-1);
486                indent();
487                out.println("]");
488                return null;
489            }
490
491            public Void visitSerialField(SerialFieldTree node, Void p) {
492                header(node);
493                indent(+1);
494                print("name", node.getName());
495                print("type", node.getType());
496                print("description", node.getDescription());
497                indent(-1);
498                indent();
499                out.println("]");
500                return null;
501            }
502
503            public Void visitSince(SinceTree node, Void p) {
504                header(node);
505                indent(+1);
506                print("body", node.getBody());
507                indent(-1);
508                indent();
509                out.println("]");
510                return null;
511            }
512
513            public Void visitStartElement(StartElementTree node, Void p) {
514                header(node);
515                indent(+1);
516                indent();
517                out.println("name:" + node.getName());
518                print("attributes", node.getAttributes());
519                indent(-1);
520                indent();
521                out.println("]");
522                return null;
523            }
524
525            public Void visitText(TextTree node, Void p) {
526                header(node, compress(node.getBody()));
527                return null;
528            }
529
530            public Void visitThrows(ThrowsTree node, Void p) {
531                header(node);
532                indent(+1);
533                print("exceptionName", node.getExceptionName());
534                print("description", node.getDescription());
535                indent(-1);
536                indent();
537                out.println("]");
538                return null;
539            }
540
541            public Void visitUnknownBlockTag(UnknownBlockTagTree node, Void p) {
542                header(node);
543                indent(+1);
544                indent();
545                out.println("tag:" + node.getTagName());
546                print("content", node.getContent());
547                indent(-1);
548                indent();
549                out.println("]");
550                return null;
551            }
552
553            public Void visitUnknownInlineTag(UnknownInlineTagTree node, Void p) {
554                header(node);
555                indent(+1);
556                indent();
557                out.println("tag:" + node.getTagName());
558                print("content", node.getContent());
559                indent(-1);
560                indent();
561                out.println("]");
562                return null;
563            }
564
565            public Void visitValue(ValueTree node, Void p) {
566                header(node);
567                indent(+1);
568                print("reference", node.getReference());
569                indent(-1);
570                indent();
571                out.println("]");
572                return null;
573            }
574
575            public Void visitVersion(VersionTree node, Void p) {
576                header(node);
577                indent(+1);
578                print("body", node.getBody());
579                indent(-1);
580                indent();
581                out.println("]");
582                return null;
583            }
584
585            public Void visitOther(DocTree node, Void p) {
586                throw new UnsupportedOperationException("Not supported yet.");
587            }
588
589            void header(DocTree node) {
590                indent();
591                out.println(simpleClassName(node) + "[" + node.getKind() + ", pos:" + ((DCTree) node).pos);
592            }
593
594            void header(DocTree node, String rest) {
595                indent();
596                out.println(simpleClassName(node) + "[" + node.getKind() + ", pos:" + ((DCTree) node).pos
597                        + (rest.isEmpty() ? "" : ", " + rest)
598                        + "]");
599            }
600
601            String simpleClassName(DocTree node) {
602                return node.getClass().getSimpleName().replaceAll("DC(.*)", "$1");
603            }
604
605            void print(String name, DocTree item) {
606                indent();
607                if (item == null)
608                    out.println(name + ": null");
609                else {
610                    out.println(name + ":");
611                    indent(+1);
612                    item.accept(this, null);
613                    indent(-1);
614                }
615            }
616
617            void print(String name, String s) {
618                indent();
619                out.println(name + ": " + s);
620            }
621
622            void print(String name, List<? extends DocTree> list) {
623                indent();
624                if (list == null)
625                    out.println(name + ": null");
626                else if (list.isEmpty())
627                    out.println(name + ": empty");
628                else {
629                    out.println(name + ": " + list.size());
630                    indent(+1);
631                    for (DocTree tree: list) {
632                        tree.accept(this, null);
633                    }
634                    indent(-1);
635                }
636            }
637
638            int indent = 0;
639
640            void indent() {
641                for (int i = 0; i < indent; i++) {
642                    out.print("  ");
643                }
644            }
645
646            void indent(int n) {
647                indent += n;
648            }
649
650            String compress(String s) {
651                s = s.replace("\n", "|").replace(" ", "_");
652                return (s.length() < 32)
653                        ? s
654                        : s.substring(0, 16) + "..." + s.substring(16);
655            }
656
657            String quote(String s) {
658                if (s.contains("\""))
659                    return "'" + s + "'";
660                else if (s.contains("'") || s.contains(" "))
661                    return '"' + s + '"';
662                else
663                    return s;
664            }
665        }
666    }
667
668    /**
669     * Verify the reported tree positions by comparing the characters found
670     * at and after the reported position with the beginning of the pretty-
671     * printed text.
672     */
673    static class PosChecker extends Checker {
674        PosChecker(DocCommentTester test, DocTrees t) {
675            test.super(t);
676        }
677
678        @Override
679        void check(TreePath path, Name name) throws Exception {
680            JavaFileObject fo = path.getCompilationUnit().getSourceFile();
681            final CharSequence cs = fo.getCharContent(true);
682
683            final DCDocComment dc = (DCDocComment) trees.getDocCommentTree(path);
684            DCTree t = (DCTree) trees.getDocCommentTree(path);
685
686            DocTreeScanner scanner = new DocTreeScanner<Void,Void>() {
687                @Override
688                public Void scan(DocTree node, Void ignore) {
689                    if (node != null) {
690                        try {
691                            String expect = getExpectText(node);
692                            long pos = ((DCTree) node).getSourcePosition(dc);
693                            String found = getFoundText(cs, (int) pos, expect.length());
694                            if (!found.equals(expect)) {
695                                System.err.println("expect: " + expect);
696                                System.err.println("found:  " + found);
697                                error("mismatch");
698                            }
699
700                        } catch (StringIndexOutOfBoundsException e) {
701                            error(node.getClass() + ": " + e.toString());
702                                e.printStackTrace();
703                        }
704                    }
705                    return super.scan(node, ignore);
706                }
707            };
708
709            scanner.scan(t, null);
710        }
711
712        String getExpectText(DocTree t) {
713            StringWriter sw = new StringWriter();
714            DocPretty p = new DocPretty(sw);
715            try { p.print(t); } catch (IOException never) { }
716            String s = sw.toString();
717            if (s.length() <= 1)
718                return s;
719            int ws = s.replaceAll("\\s+", " ").indexOf(" ");
720            if (ws != -1) s = s.substring(0, ws);
721            return (s.length() < 5) ? s : s.substring(0, 5);
722        }
723
724        String getFoundText(CharSequence cs, int pos, int len) {
725            return (pos == -1) ? "" : cs.subSequence(pos, Math.min(pos + len, cs.length())).toString();
726        }
727    }
728
729    /**
730     * Verify the pretty printed text against a normalized form of the
731     * original doc comment.
732     */
733    static class PrettyChecker extends Checker {
734
735        PrettyChecker(DocCommentTester test, DocTrees t) {
736            test.super(t);
737        }
738
739        @Override
740        void check(TreePath path, Name name) throws Exception {
741            String raw = trees.getDocComment(path);
742            String normRaw = normalize(raw);
743
744            StringWriter out = new StringWriter();
745            DocPretty dp = new DocPretty(out);
746            dp.print(trees.getDocCommentTree(path));
747            String pretty = out.toString();
748
749            if (!pretty.equals(normRaw)) {
750                error("mismatch");
751                System.err.println("*** expected:");
752                System.err.println(normRaw.replace(" ", "_"));
753                System.err.println("*** found:");
754                System.err.println(pretty.replace(" ", "_"));
755    //            throw new Error();
756            }
757        }
758
759        /**
760         * Normalize white space in places where the tree does not preserve it.
761         */
762        String normalize(String s) {
763            s = s.trim()
764                    .replaceFirst("\\.\\s*\\n *@", ".\n@")
765                    .replaceAll("\\{@docRoot\\s+\\}", "{@docRoot}")
766                    .replaceAll("\\{@inheritDoc\\s+\\}", "{@inheritDoc}")
767                    .replaceAll("(\\{@value\\s+[^}]+)\\s+(\\})", "$1$2")
768                    .replaceAll("\n[ \t]+@", "\n@")
769                    .replaceAll("(\\{@code)(\\x20)(\\s+.*)", "$1$3");
770            return s;
771        }
772    }
773}
774
775