1// © 2016 and later: Unicode, Inc. and others.
2// License & terms of use: http://www.unicode.org/copyright.html#License
3/**
4*******************************************************************************
5* Copyright (C) 2002-2010, International Business Machines Corporation and    *
6* others. All Rights Reserved.                                                *
7*******************************************************************************
8*/
9/**
10 * This is a tool to check the tags on ICU4J files.  In particular, we're looking for:
11 *
12 * - methods that have no tags
13 * - custom tags: @draft, @stable, @internal?
14 * - standard tags: @since, @deprecated
15 *
16 * Syntax of tags:
17 * '@draft ICU X.X.X'
18 * '@stable ICU X.X.X'
19 * '@internal'
20 * '@since  (don't use)'
21 * '@obsolete ICU X.X.X'
22 * '@deprecated to be removed in ICU X.X. [Use ...]'
23 *
24 * flags names of classes and their members that have no tags or incorrect syntax.
25 *
26 * Requires JDK 1.4 or later
27 *
28 * Use build.xml 'checktags' ant target, or
29 * run from directory containing CheckTags.class as follows:
30 * javadoc -classpath ${JAVA_HOME}/lib/tools.jar -doclet CheckTags -sourcepath ${ICU4J_src} [packagenames]
31 */
32
33package com.ibm.icu.dev.tool.docs;
34
35import com.sun.javadoc.ClassDoc;
36import com.sun.javadoc.ConstructorDoc;
37import com.sun.javadoc.ExecutableMemberDoc;
38import com.sun.javadoc.ProgramElementDoc;
39import com.sun.javadoc.RootDoc;
40import com.sun.javadoc.Tag;
41
42public class CheckTags {
43    RootDoc root;
44    boolean log;
45    boolean brief;
46    boolean isShort;
47    DocStack stack = new DocStack();
48
49    class DocNode {
50        private String header;
51        private boolean printed;
52        private boolean reportError;
53        private int errorCount;
54
55        public void reset(String header, boolean reportError) {
56            this.header = header;
57            this.printed = false;
58            this.errorCount = 0;
59            this.reportError = reportError;
60        }
61        public String toString() {
62            return header +
63                " printed: " + printed +
64                " reportError: " + reportError +
65                " errorCount: " + errorCount;
66        }
67    }
68
69    class DocStack {
70        private DocNode[] stack;
71        private int index;
72        private boolean newline;
73
74        public void push(String header, boolean reportError) {
75            if (stack == null) {
76                stack = new DocNode[5];
77            } else {
78                if (index == stack.length) {
79                    DocNode[] temp = new DocNode[stack.length * 2];
80                    System.arraycopy(stack, 0, temp, 0, index);
81                    stack = temp;
82                }
83            }
84            if (stack[index] == null) {
85                stack[index] = new DocNode();
86            }
87            //  System.out.println("reset [" + index + "] header: " + header + " report: " + reportError);
88            stack[index++].reset(header, reportError);
89        }
90
91        public void pop() {
92            if (index == 0) {
93                throw new IndexOutOfBoundsException();
94            }
95            --index;
96
97            int ec = stack[index].errorCount; // index already decremented
98            if (ec > 0 || index == 0) { // always report for outermost element
99                if (stack[index].reportError) {
100                    output("(" + ec + (ec == 1 ? " error" : " errors") + ")", false, true, index);
101                }
102
103                // propagate to parent
104                if (index > 0) {
105                    stack[index-1].errorCount += ec;
106                }
107            }
108            if (index == 0) {
109                System.out.println(); // always since we always report number of errors
110            }
111        }
112
113        public void output(String msg, boolean error, boolean newline) {
114            output(msg, error, newline, index-1);
115        }
116
117        void output(String msg, boolean error, boolean newline, int ix) {
118            DocNode last = stack[ix];
119            if (error) {
120                last.errorCount += 1;
121        }
122
123            boolean show = !brief || last.reportError;
124            // boolean nomsg = show && brief && error;
125            //            System.out.println(">>> " + last + " error: " + error + " show: " + show + " nomsg: " + nomsg);
126
127            if (show) {
128                if (isShort || (brief && error)) {
129                    msg = null; // nuke error messages if we're brief, just report headers and totals
130                }
131                for (int i = 0; i <= ix;) {
132                    DocNode n = stack[i];
133                    if (n.printed) {
134                        if (msg != null || !last.printed) { // since index > 0 last is not null
135                            if (this.newline && i == 0) {
136                                System.out.println();
137                                this.newline = false;
138                            }
139                            System.out.print("  ");
140                        }
141                        ++i;
142                    } else {
143                        System.out.print(n.header);
144                        n.printed = true;
145                        this.newline = true;
146                        i = 0;
147                    }
148                }
149
150                if (msg != null) {
151                    if (index == 0 && this.newline) {
152                        System.out.println();
153                    }
154                    if (error) {
155                        System.out.print("*** ");
156                    }
157                    System.out.print(msg);
158                }
159            }
160
161            this.newline = newline;
162        }
163    }
164
165    public static boolean start(RootDoc root) {
166        return new CheckTags(root).run();
167    }
168
169    public static int optionLength(String option) {
170        if (option.equals("-log")) {
171            return 1;
172        } else if (option.equals("-brief")) {
173            return 1;
174        } else if (option.equals("-short")) {
175            return 1;
176        }
177        return 0;
178    }
179
180    CheckTags(RootDoc root) {
181        this.root = root;
182
183        String[][] options = root.options();
184        for (int i = 0; i < options.length; ++i) {
185            String opt = options[i][0];
186            if (opt.equals("-log")) {
187                this.log = true;
188            } else if (opt.equals("-brief")) {
189                this.brief = true;
190            } else if (opt.equals("-short")) {
191                this.isShort = true;
192            }
193        }
194    }
195
196    boolean run() {
197        doDocs(root.classes(), "Package", true);
198        return false;
199    }
200
201    static final String[] tagKinds = {
202        "@internal", "@draft", "@stable", "@since", "@deprecated", "@author", "@see", "@version",
203        "@param", "@return", "@throws", "@obsolete", "@exception", "@serial", "@provisional"
204    };
205
206    static final int UNKNOWN = -1;
207    static final int INTERNAL = 0;
208    static final int DRAFT = 1;
209    static final int STABLE = 2;
210    static final int SINCE = 3;
211    static final int DEPRECATED = 4;
212    static final int AUTHOR = 5;
213    static final int SEE = 6;
214    static final int VERSION = 7;
215    static final int PARAM = 8;
216    static final int RETURN = 9;
217    static final int THROWS = 10;
218    static final int OBSOLETE = 11;
219    static final int EXCEPTION = 12;
220    static final int SERIAL = 13;
221    static final int PROVISIONAL = 14;
222
223    static int tagKindIndex(String kind) {
224        for (int i = 0; i < tagKinds.length; ++i) {
225            if (kind.equals(tagKinds[i])) {
226                return i;
227            }
228        }
229        return UNKNOWN;
230    }
231
232    static final String[] icuTagNames = {
233        "@icu", "@icunote", "@icuenhanced"
234    };
235    static final int ICU = 0;
236    static final int ICUNOTE = 1;
237    static final int ICUENHANCED = 2;
238    static int icuTagIndex(String name) {
239        for (int i = 0; i < icuTagNames.length; ++i) {
240            if (icuTagNames[i].equals(name)) {
241                return i;
242            }
243        }
244        return UNKNOWN;
245    }
246
247    boolean newline = false;
248
249    void output(String msg, boolean error, boolean newline) {
250        stack.output(msg, error, newline);
251    }
252
253    void log() {
254        output(null, false, false);
255    }
256
257    void logln() {
258        output(null, false, true);
259    }
260
261    void log(String msg) {
262        output(msg, false, false);
263    }
264
265    void logln(String msg) {
266        output(msg, false, true);
267    }
268
269    void err(String msg) {
270        output(msg, true, false);
271    }
272
273    void errln(String msg) {
274        output(msg, true, true);
275    }
276
277    void tagErr(String msg, Tag tag) {
278        // Tag.position() requires JDK 1.4, build.xml tests for this
279        if (msg.length() > 0) {
280            msg += ": ";
281        }
282        errln(msg + tag.toString() + " [" + tag.position() + "]");
283    };
284
285    void tagErr(Tag tag) {
286        tagErr("", tag);
287    }
288
289    void doDocs(ProgramElementDoc[] docs, String header, boolean reportError) {
290        if (docs != null && docs.length > 0) {
291            stack.push(header, reportError);
292            for (int i = 0; i < docs.length; ++i) {
293                doDoc(docs[i]);
294            }
295            stack.pop();
296        }
297    }
298
299    void doDoc(ProgramElementDoc doc) {
300        if (doc != null && (doc.isPublic() || doc.isProtected())
301            && !(doc instanceof ConstructorDoc && ((ConstructorDoc)doc).isSynthetic())) {
302
303            // unfortunately, in JDK 1.4.1 MemberDoc.isSynthetic is not properly implemented for
304            // synthetic constructors.  So you'll have to live with spurious errors or 'implement'
305            // the synthetic constructors...
306
307            boolean isClass = doc.isClass() || doc.isInterface();
308            String header;
309            if (!isShort || isClass) {
310                header = "--- ";
311            } else {
312                header = "";
313            }
314            header += (isClass ? doc.qualifiedName() : doc.name());
315            if (doc instanceof ExecutableMemberDoc) {
316                header += ((ExecutableMemberDoc)doc).flatSignature();
317            }
318            if (!isShort || isClass) {
319                header += " ---";
320            }
321            stack.push(header, isClass);
322            if (log) {
323                logln();
324            }
325            boolean recurse = doTags(doc);
326            if (recurse && isClass) {
327                ClassDoc cdoc = (ClassDoc)doc;
328                doDocs(cdoc.fields(), "Fields", !brief);
329                doDocs(cdoc.constructors(), "Constructors", !brief);
330                doDocs(cdoc.methods(), "Methods", !brief);
331            }
332            stack.pop();
333        }
334    }
335
336    /** Return true if subelements of this doc should be checked */
337    boolean doTags(ProgramElementDoc doc) {
338        boolean foundRequiredTag = false;
339        boolean foundDraftTag = false;
340        boolean foundProvisionalTag = false;
341        boolean foundDeprecatedTag = false;
342        boolean foundObsoleteTag = false;
343        boolean foundInternalTag = false;
344        boolean foundStableTag = false;
345        boolean retainAll = false;
346
347        // first check inline tags
348        for (Tag tag : doc.inlineTags()) {
349            int index = icuTagIndex(tag.name());
350            if (index >= 0) {
351                String text = tag.text().trim();
352                switch (index) {
353                case ICU: {
354                    if (doc.isClass() || doc.isInterface()) {
355                        tagErr("tag should appear only in member docs", tag);
356                    }
357                } break;
358                case ICUNOTE: {
359                    if (text.length() > 0) {
360                        tagErr("tag should not contain text", tag);
361                    }
362                } break;
363                case ICUENHANCED: {
364                    if (text.length() == 0) {
365                        tagErr("text should name related jdk class", tag);
366                    }
367                    if (!(doc.isClass() || doc.isInterface())) {
368                        tagErr("tag should appear only in class/interface docs", tag);
369                    }
370                } break;
371                default:
372                    tagErr("unrecognized tag index for tag", tag);
373                    break;
374                }
375            }
376        }
377
378        // next check regular tags
379        for (Tag tag : doc.tags()) {
380            String kind = tag.kind();
381            int ix = tagKindIndex(kind);
382
383            switch (ix) {
384            case UNKNOWN:
385                errln("unknown kind: " + kind);
386                break;
387
388            case INTERNAL:
389                foundRequiredTag = true;
390                foundInternalTag = true;
391                break;
392
393            case DRAFT:
394                foundRequiredTag = true;
395                foundDraftTag = true;
396                if (tag.text().indexOf("ICU 2.8") != -1 &&
397                    tag.text().indexOf("(retain") == -1) { // catch both retain and retainAll
398                    tagErr(tag);
399                    break;
400                }
401                if (tag.text().indexOf("ICU") != 0) {
402                    tagErr(tag);
403                    break;
404                }
405                retainAll |= (tag.text().indexOf("(retainAll)") != -1);
406                break;
407
408            case PROVISIONAL:
409                foundProvisionalTag = true;
410                break;
411
412            case DEPRECATED:
413                foundDeprecatedTag = true;
414                if (tag.text().indexOf("ICU") == 0) {
415                    foundRequiredTag = true;
416                }
417                break;
418
419            case OBSOLETE:
420                if (tag.text().indexOf("ICU") != 0) {
421                    tagErr(tag);
422                }
423                foundObsoleteTag = true;
424                foundRequiredTag = true;
425                break;
426
427            case STABLE:
428                {
429                    String text = tag.text();
430                    if (text.length() != 0 && text.indexOf("ICU") != 0) {
431                        tagErr(tag);
432                    }
433                    foundRequiredTag = true;
434                    foundStableTag = true;
435                }
436                break;
437
438            case SINCE:
439                tagErr(tag);
440                break;
441
442            case EXCEPTION:
443                logln("You really ought to use @throws, you know... :-)");
444
445            case AUTHOR:
446            case SEE:
447            case PARAM:
448            case RETURN:
449            case THROWS:
450            case SERIAL:
451                break;
452
453            case VERSION:
454                tagErr(tag);
455                break;
456
457            default:
458                errln("unknown index: " + ix);
459            }
460        }
461        if (!foundRequiredTag) {
462            errln("missing required tag [" + doc.position() + "]");
463        }
464        if (foundInternalTag && !foundDeprecatedTag) {
465            errln("internal tag missing deprecated");
466        }
467        if (foundDraftTag && !(foundDeprecatedTag || foundProvisionalTag)) {
468            errln("draft tag missing deprecated or provisional");
469        }
470        if (foundObsoleteTag && !foundDeprecatedTag) {
471            errln("obsolete tag missing deprecated");
472        }
473        if (foundStableTag && foundDeprecatedTag) {
474            logln("stable deprecated");
475        }
476
477        return !retainAll;
478    }
479}
480