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