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