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