CheckResourceKeys.java revision 2687:56f8be952a5c
1/*
2 * Copyright (c) 2010, 2014, 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
24/*
25 * @test
26 * @bug 6964768 6964461 6964469 6964487 6964460 6964481 6980021
27 * @summary need test program to validate javac resource bundles
28 */
29
30import java.io.*;
31import java.util.*;
32import javax.tools.*;
33import com.sun.tools.classfile.*;
34import com.sun.tools.javac.code.Lint.LintCategory;
35
36/**
37 * Compare string constants in javac classes against keys in javac resource bundles.
38 */
39public class CheckResourceKeys {
40    /**
41     * Main program.
42     * Options:
43     * -finddeadkeys
44     *      look for keys in resource bundles that are no longer required
45     * -findmissingkeys
46     *      look for keys in resource bundles that are missing
47     *
48     * @throws Exception if invoked by jtreg and errors occur
49     */
50    public static void main(String... args) throws Exception {
51        CheckResourceKeys c = new CheckResourceKeys();
52        if (c.run(args))
53            return;
54
55        if (is_jtreg())
56            throw new Exception(c.errors + " errors occurred");
57        else
58            System.exit(1);
59    }
60
61    static boolean is_jtreg() {
62        return (System.getProperty("test.src") != null);
63    }
64
65    /**
66     * Main entry point.
67     */
68    boolean run(String... args) throws Exception {
69        boolean findDeadKeys = false;
70        boolean findMissingKeys = false;
71
72        if (args.length == 0) {
73            if (is_jtreg()) {
74                findDeadKeys = true;
75                findMissingKeys = true;
76            } else {
77                System.err.println("Usage: java CheckResourceKeys <options>");
78                System.err.println("where options include");
79                System.err.println("  -finddeadkeys      find keys in resource bundles which are no longer required");
80                System.err.println("  -findmissingkeys   find keys in resource bundles that are required but missing");
81                return true;
82            }
83        } else {
84            for (String arg: args) {
85                if (arg.equalsIgnoreCase("-finddeadkeys"))
86                    findDeadKeys = true;
87                else if (arg.equalsIgnoreCase("-findmissingkeys"))
88                    findMissingKeys = true;
89                else
90                    error("bad option: " + arg);
91            }
92        }
93
94        if (errors > 0)
95            return false;
96
97        Set<String> codeStrings = getCodeStrings();
98        Set<String> resourceKeys = getResourceKeys();
99
100        if (findDeadKeys)
101            findDeadKeys(codeStrings, resourceKeys);
102
103        if (findMissingKeys)
104            findMissingKeys(codeStrings, resourceKeys);
105
106        return (errors == 0);
107    }
108
109    /**
110     * Find keys in resource bundles which are probably no longer required.
111     * A key is probably required if there is a string fragment in the code
112     * that is part of the resource key, or if the key is well-known
113     * according to various pragmatic rules.
114     */
115    void findDeadKeys(Set<String> codeStrings, Set<String> resourceKeys) {
116        String[] prefixes = {
117            "compiler.err.", "compiler.warn.", "compiler.note.", "compiler.misc.",
118            "javac."
119        };
120        for (String rk: resourceKeys) {
121            // some keys are used directly, without a prefix.
122            if (codeStrings.contains(rk))
123                continue;
124
125            // remove standard prefix
126            String s = null;
127            for (int i = 0; i < prefixes.length && s == null; i++) {
128                if (rk.startsWith(prefixes[i])) {
129                    s = rk.substring(prefixes[i].length());
130                }
131            }
132            if (s == null) {
133                error("Resource key does not start with a standard prefix: " + rk);
134                continue;
135            }
136
137            if (codeStrings.contains(s))
138                continue;
139
140            // keys ending in .1 are often synthesized
141            if (s.endsWith(".1") && codeStrings.contains(s.substring(0, s.length() - 2)))
142                continue;
143
144            // verbose keys are generated by ClassReader.printVerbose
145            if (s.startsWith("verbose.") && codeStrings.contains(s.substring(8)))
146                continue;
147
148            // mandatory warning messages are synthesized with no characteristic substring
149            if (isMandatoryWarningString(s))
150                continue;
151
152            // check known (valid) exceptions
153            if (knownRequired.contains(rk))
154                continue;
155
156            // check known suspects
157            if (needToInvestigate.contains(rk))
158                continue;
159
160            //check lint description keys:
161            if (s.startsWith("opt.Xlint.desc.")) {
162                String option = s.substring(15);
163                boolean found = false;
164
165                for (LintCategory lc : LintCategory.values()) {
166                    if (option.equals(lc.option))
167                        found = true;
168                }
169
170                if (found)
171                    continue;
172            }
173
174            error("Resource key not found in code: " + rk);
175        }
176    }
177
178    /**
179     * The keys for mandatory warning messages are all synthesized and do not
180     * have a significant recognizable substring to look for.
181     */
182    private boolean isMandatoryWarningString(String s) {
183        String[] bases = { "deprecated", "unchecked", "varargs", "sunapi" };
184        String[] tails = { ".filename", ".filename.additional", ".plural", ".plural.additional", ".recompile" };
185        for (String b: bases) {
186            if (s.startsWith(b)) {
187                String tail = s.substring(b.length());
188                for (String t: tails) {
189                    if (tail.equals(t))
190                        return true;
191                }
192            }
193        }
194        return false;
195    }
196
197    Set<String> knownRequired = new TreeSet<String>(Arrays.asList(
198        // See Resolve.getErrorKey
199        "compiler.err.cant.resolve.args",
200        "compiler.err.cant.resolve.args.params",
201        "compiler.err.cant.resolve.location.args",
202        "compiler.err.cant.resolve.location.args.params",
203        "compiler.misc.cant.resolve.location.args",
204        "compiler.misc.cant.resolve.location.args.params",
205        // JavaCompiler, reports #errors and #warnings
206        "compiler.misc.count.error",
207        "compiler.misc.count.error.plural",
208        "compiler.misc.count.warn",
209        "compiler.misc.count.warn.plural",
210        // Used for LintCategory
211        "compiler.warn.lintOption",
212        // Other
213        "compiler.misc.base.membership"                                 // (sic)
214        ));
215
216
217    Set<String> needToInvestigate = new TreeSet<String>(Arrays.asList(
218        "compiler.misc.fatal.err.cant.close.loader",        // Supressed by JSR308
219        "compiler.err.cant.read.file",                      // UNUSED
220        "compiler.err.illegal.self.ref",                    // UNUSED
221        "compiler.err.io.exception",                        // UNUSED
222        "compiler.err.limit.pool.in.class",                 // UNUSED
223        "compiler.err.name.reserved.for.internal.use",      // UNUSED
224        "compiler.err.no.match.entry",                      // UNUSED
225        "compiler.err.not.within.bounds.explain",           // UNUSED
226        "compiler.err.signature.doesnt.match.intf",         // UNUSED
227        "compiler.err.signature.doesnt.match.supertype",    // UNUSED
228        "compiler.err.type.var.more.than.once",             // UNUSED
229        "compiler.err.type.var.more.than.once.in.result",   // UNUSED
230        "compiler.misc.ccf.found.later.version",            // UNUSED
231        "compiler.misc.non.denotable.type",                 // UNUSED
232        "compiler.misc.unnamed.package",                    // should be required, CR 6964147
233        "compiler.misc.verbose.retro",                      // UNUSED
234        "compiler.misc.verbose.retro.with",                 // UNUSED
235        "compiler.misc.verbose.retro.with.list",            // UNUSED
236        "compiler.warn.proc.type.already.exists",           // TODO in JavacFiler
237        "javac.err.invalid.arg",                            // UNUSED ??
238        "javac.opt.arg.class",                              // UNUSED ??
239        "javac.opt.arg.pathname",                           // UNUSED ??
240        "javac.opt.moreinfo",                               // option commented out
241        "javac.opt.nogj",                                   // UNUSED
242        "javac.opt.printflat",                              // option commented out
243        "javac.opt.printsearch",                            // option commented out
244        "javac.opt.prompt",                                 // option commented out
245        "javac.opt.retrofit",                               // UNUSED
246        "javac.opt.s",                                      // option commented out
247        "javac.opt.scramble",                               // option commented out
248        "javac.opt.scrambleall"                             // option commented out
249        ));
250
251    /**
252     * For all strings in the code that look like they might be fragments of
253     * a resource key, verify that a key exists.
254     */
255    void findMissingKeys(Set<String> codeStrings, Set<String> resourceKeys) {
256        for (String cs: codeStrings) {
257            if (cs.matches("[A-Za-z][^.]*\\..*")) {
258                // ignore filenames (i.e. in SourceFile attribute
259                if (cs.matches(".*\\.java"))
260                    continue;
261                // ignore package and class names
262                if (cs.matches("(com|java|javax|sun)\\.[A-Za-z.]+"))
263                    continue;
264                // explicit known exceptions
265                if (noResourceRequired.contains(cs))
266                    continue;
267                // look for matching resource
268                if (hasMatch(resourceKeys, cs))
269                    continue;
270                error("no match for \"" + cs + "\"");
271            }
272        }
273    }
274    // where
275    private Set<String> noResourceRequired = new HashSet<String>(Arrays.asList(
276            // system properties
277            "application.home", // in Paths.java
278            "env.class.path",
279            "line.separator",
280            "os.name",
281            "user.dir",
282            // file names
283            "ct.sym",
284            "rt.jar",
285            "tools.jar",
286            // -XD option names
287            "process.packages",
288            "ignore.symbol.file",
289            // prefix/embedded strings
290            "compiler.",
291            "compiler.misc.",
292            "opt.Xlint.desc.",
293            "count.",
294            "illegal.",
295            "javac.",
296            "verbose."
297    ));
298
299    /**
300     * Look for a resource that ends in this string fragment.
301     */
302    boolean hasMatch(Set<String> resourceKeys, String s) {
303        for (String rk: resourceKeys) {
304            if (rk.endsWith(s))
305                return true;
306        }
307        return false;
308    }
309
310    /**
311     * Get the set of strings from (most of) the javac classfiles.
312     */
313    Set<String> getCodeStrings() throws IOException {
314        Set<String> results = new TreeSet<String>();
315        JavaCompiler c = ToolProvider.getSystemJavaCompiler();
316        try (JavaFileManager fm = c.getStandardFileManager(null, null, null)) {
317            JavaFileManager.Location javacLoc = findJavacLocation(fm);
318            String[] pkgs = {
319                "javax.annotation.processing",
320                "javax.lang.model",
321                "javax.tools",
322                "com.sun.source",
323                "com.sun.tools.javac"
324            };
325            for (String pkg: pkgs) {
326                for (JavaFileObject fo: fm.list(javacLoc,
327                        pkg, EnumSet.of(JavaFileObject.Kind.CLASS), true)) {
328                    String name = fo.getName();
329                    // ignore resource files, and files which are not really part of javac
330                    if (name.matches(".*resources.[A-Za-z_0-9]+\\.class.*")
331                            || name.matches(".*CreateSymbols\\.class.*"))
332                        continue;
333                    scan(fo, results);
334                }
335            }
336            return results;
337        }
338    }
339
340    // depending on how the test is run, javac may be on bootclasspath or classpath
341    JavaFileManager.Location findJavacLocation(JavaFileManager fm) {
342        JavaFileManager.Location[] locns =
343            { StandardLocation.PLATFORM_CLASS_PATH, StandardLocation.CLASS_PATH };
344        try {
345            for (JavaFileManager.Location l: locns) {
346                JavaFileObject fo = fm.getJavaFileForInput(l,
347                    "com.sun.tools.javac.Main", JavaFileObject.Kind.CLASS);
348                if (fo != null)
349                    return l;
350            }
351        } catch (IOException e) {
352            throw new Error(e);
353        }
354        throw new IllegalStateException("Cannot find javac");
355    }
356
357    /**
358     * Get the set of strings from a class file.
359     * Only strings that look like they might be a resource key are returned.
360     */
361    void scan(JavaFileObject fo, Set<String> results) throws IOException {
362        InputStream in = fo.openInputStream();
363        try {
364            ClassFile cf = ClassFile.read(in);
365            for (ConstantPool.CPInfo cpinfo: cf.constant_pool.entries()) {
366                if (cpinfo.getTag() == ConstantPool.CONSTANT_Utf8) {
367                    String v = ((ConstantPool.CONSTANT_Utf8_info) cpinfo).value;
368                    if (v.matches("[A-Za-z0-9-_.]+"))
369                        results.add(v);
370                }
371            }
372        } catch (ConstantPoolException ignore) {
373        } finally {
374            in.close();
375        }
376    }
377
378    /**
379     * Get the set of keys from the javac resource bundles.
380     */
381    Set<String> getResourceKeys() {
382        Set<String> results = new TreeSet<String>();
383        for (String name : new String[]{"javac", "compiler"}) {
384            ResourceBundle b =
385                    ResourceBundle.getBundle("com.sun.tools.javac.resources." + name);
386            results.addAll(b.keySet());
387        }
388        return results;
389    }
390
391    /**
392     * Report an error.
393     */
394    void error(String msg) {
395        System.err.println("Error: " + msg);
396        errors++;
397    }
398
399    int errors;
400}
401