1 /*
  2  * Copyright (c) 2010, 2019, 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  * @enablePreview
 29  * @modules jdk.compiler/com.sun.tools.javac.code
 30  *          jdk.compiler/com.sun.tools.javac.resources:open
 31  *          java.base/jdk.internal.classfile.impl
 32  */
 33 
 34 import java.io.*;
 35 import java.util.*;
 36 import java.util.regex.*;
 37 import javax.tools.*;
 38 import java.lang.classfile.*;
 39 import java.lang.classfile.constantpool.*;
 40 import com.sun.tools.javac.code.Lint.LintCategory;
 41 
 42 /**
 43  * Compare string constants in javac classes against keys in javac resource bundles.
 44  */
 45 public class CheckResourceKeys {
 46     /**
 47      * Main program.
 48      * Options:
 49      * -finddeadkeys
 50      *      look for keys in resource bundles that are no longer required
 51      * -findmissingkeys
 52      *      look for keys in resource bundles that are missing
 53      * -checkformats
 54      *      validate MessageFormat patterns in resource bundles
 55      *
 56      * @throws Exception if invoked by jtreg and errors occur
 57      */
 58     public static void main(String... args) throws Exception {
 59         CheckResourceKeys c = new CheckResourceKeys();
 60         if (c.run(args))
 61             return;
 62 
 63         if (is_jtreg())
 64             throw new Exception(c.errors + " errors occurred");
 65         else
 66             System.exit(1);
 67     }
 68 
 69     static boolean is_jtreg() {
 70         return (System.getProperty("test.src") != null);
 71     }
 72 
 73     /**
 74      * Main entry point.
 75      */
 76     boolean run(String... args) throws Exception {
 77         boolean findDeadKeys = false;
 78         boolean findMissingKeys = false;
 79         boolean checkFormats = false;
 80 
 81         if (args.length == 0) {
 82             if (is_jtreg()) {
 83                 findDeadKeys = true;
 84                 findMissingKeys = true;
 85                 checkFormats = true;
 86             } else {
 87                 System.err.println("Usage: java CheckResourceKeys <options>");
 88                 System.err.println("where options include");
 89                 System.err.println("  -finddeadkeys      find keys in resource bundles which are no longer required");
 90                 System.err.println("  -findmissingkeys   find keys in resource bundles that are required but missing");
 91                 System.err.println("  -checkformats      validate MessageFormat patterns in resource bundles");
 92                 return true;
 93             }
 94         } else {
 95             for (String arg: args) {
 96                 if (arg.equalsIgnoreCase("-finddeadkeys"))
 97                     findDeadKeys = true;
 98                 else if (arg.equalsIgnoreCase("-findmissingkeys"))
 99                     findMissingKeys = true;
100                 else if (arg.equalsIgnoreCase("-checkformats"))
101                     checkFormats = true;
102                 else
103                     error("bad option: " + arg);
104             }
105         }
106 
107         if (errors > 0)
108             return false;
109 
110         Set<String> codeStrings = getCodeStrings();
111         Set<String> resourceKeys = getResourceKeys();
112 
113         if (findDeadKeys)
114             findDeadKeys(codeStrings, resourceKeys);
115 
116         if (findMissingKeys)
117             findMissingKeys(codeStrings, resourceKeys);
118 
119         if (checkFormats)
120             checkFormats(getMessageFormatBundles());
121 
122         return (errors == 0);
123     }
124 
125     /**
126      * Find keys in resource bundles which are probably no longer required.
127      * A key is probably required if there is a string fragment in the code
128      * that is part of the resource key, or if the key is well-known
129      * according to various pragmatic rules.
130      */
131     void findDeadKeys(Set<String> codeStrings, Set<String> resourceKeys) {
132         String[] prefixes = {
133             "compiler.err.", "compiler.warn.", "compiler.note.", "compiler.misc.",
134             "javac.",
135             "launcher.err."
136         };
137         for (String rk: resourceKeys) {
138             // some keys are used directly, without a prefix.
139             if (codeStrings.contains(rk))
140                 continue;
141 
142             // remove standard prefix
143             String s = null;
144             for (int i = 0; i < prefixes.length && s == null; i++) {
145                 if (rk.startsWith(prefixes[i])) {
146                     s = rk.substring(prefixes[i].length());
147                 }
148             }
149             if (s == null) {
150                 error("Resource key does not start with a standard prefix: " + rk);
151                 continue;
152             }
153 
154             if (codeStrings.contains(s))
155                 continue;
156 
157             // keys ending in .1 are often synthesized
158             if (s.endsWith(".1") && codeStrings.contains(s.substring(0, s.length() - 2)))
159                 continue;
160 
161             // verbose keys are generated by ClassReader.printVerbose
162             if (s.startsWith("verbose.") && codeStrings.contains(s.substring(8)))
163                 continue;
164 
165             // mandatory warning messages are synthesized with no characteristic substring
166             if (isMandatoryWarningString(s))
167                 continue;
168 
169             // check known (valid) exceptions
170             if (knownRequired.contains(rk))
171                 continue;
172 
173             // check known suspects
174             if (needToInvestigate.contains(rk))
175                 continue;
176 
177             //check lint description keys:
178             if (s.startsWith("opt.Xlint.desc.")) {
179                 String option = s.substring(15);
180                 boolean found = false;
181 
182                 for (LintCategory lc : LintCategory.values()) {
183                     if (option.equals(lc.option))
184                         found = true;
185                 }
186 
187                 if (found)
188                     continue;
189             }
190 
191             error("Resource key not found in code: " + rk);
192         }
193     }
194 
195     /**
196      * The keys for mandatory warning messages are all synthesized and do not
197      * have a significant recognizable substring to look for.
198      */
199     private boolean isMandatoryWarningString(String s) {
200         String[] bases = { "deprecated", "unchecked", "varargs" };
201         String[] tails = { ".filename", ".filename.additional", ".plural", ".plural.additional", ".recompile" };
202         for (String b: bases) {
203             if (s.startsWith(b)) {
204                 String tail = s.substring(b.length());
205                 for (String t: tails) {
206                     if (tail.equals(t))
207                         return true;
208                 }
209             }
210         }
211         return false;
212     }
213 
214     Set<String> knownRequired = new TreeSet<String>(Arrays.asList(
215         // See Resolve.getErrorKey
216         "compiler.err.cant.resolve.args",
217         "compiler.err.cant.resolve.args.params",
218         "compiler.err.cant.resolve.location.args",
219         "compiler.err.cant.resolve.location.args.params",
220         "compiler.misc.cant.resolve.location.args",
221         "compiler.misc.cant.resolve.location.args.params",
222         // JavaCompiler, reports #errors and #warnings
223         "compiler.misc.count.error",
224         "compiler.misc.count.error.plural",
225         "compiler.misc.count.warn",
226         "compiler.misc.count.warn.plural",
227         // Used for LintCategory
228         "compiler.warn.lintOption",
229         // Other
230         "compiler.misc.base.membership"                                 // (sic)
231         ));
232 
233 
234     Set<String> needToInvestigate = new TreeSet<String>(Arrays.asList(
235         "compiler.misc.fatal.err.cant.close.loader",        // Supressed by JSR308
236         "compiler.err.cant.read.file",                      // UNUSED
237         "compiler.err.illegal.self.ref",                    // UNUSED
238         "compiler.err.io.exception",                        // UNUSED
239         "compiler.err.limit.pool.in.class",                 // UNUSED
240         "compiler.err.name.reserved.for.internal.use",      // UNUSED
241         "compiler.err.no.match.entry",                      // UNUSED
242         "compiler.err.not.within.bounds.explain",           // UNUSED
243         "compiler.err.signature.doesnt.match.intf",         // UNUSED
244         "compiler.err.signature.doesnt.match.supertype",    // UNUSED
245         "compiler.err.type.var.more.than.once",             // UNUSED
246         "compiler.err.type.var.more.than.once.in.result",   // UNUSED
247         "compiler.misc.non.denotable.type",                 // UNUSED
248         "compiler.misc.unnamed.package",                    // should be required, CR 6964147
249         "compiler.warn.proc.type.already.exists",           // TODO in JavacFiler
250         "javac.opt.arg.class",                              // UNUSED ??
251         "javac.opt.arg.pathname",                           // UNUSED ??
252         "javac.opt.moreinfo",                               // option commented out
253         "javac.opt.nogj",                                   // UNUSED
254         "javac.opt.printsearch",                            // option commented out
255         "javac.opt.prompt",                                 // option commented out
256         "javac.opt.s"                                       // option commented out
257         ));
258 
259     /**
260      * For all strings in the code that look like they might be fragments of
261      * a resource key, verify that a key exists.
262      */
263     void findMissingKeys(Set<String> codeStrings, Set<String> resourceKeys) {
264         for (String cs: codeStrings) {
265             if (cs.matches("[A-Za-z][^.]*\\..*")) {
266                 // ignore filenames (i.e. in SourceFile attribute
267                 if (cs.matches(".*\\.java"))
268                     continue;
269                 // ignore package and class names
270                 if (cs.matches("(com|java|javax|jdk|sun)\\.[A-Za-z.]+"))
271                     continue;
272                 if (cs.matches("(java|javax|sun)\\."))
273                     continue;
274                 // ignore debug flag names
275                 if (cs.startsWith("debug."))
276                     continue;
277                 // ignore should-stop flag names
278                 if (cs.startsWith("should-stop."))
279                     continue;
280                 // ignore diagsformat flag names
281                 if (cs.startsWith("diags."))
282                     continue;
283                 // explicit known exceptions
284                 if (noResourceRequired.contains(cs))
285                     continue;
286                 // look for matching resource
287                 if (hasMatch(resourceKeys, cs))
288                     continue;
289                 error("no match for \"" + cs + "\"");
290             }
291         }
292     }
293     // where
294     private Set<String> noResourceRequired = new HashSet<String>(Arrays.asList(
295             // module names
296             "jdk.compiler",
297             "jdk.javadoc",
298             // system properties
299             "application.home", // in Paths.java
300             "env.class.path",
301             "line.separator",
302             "os.name",
303             "user.dir",
304             // file names
305             "ct.sym",
306             "rt.jar",
307             "jfxrt.jar",
308             "module-info.class",
309             "module-info.sig",
310             "jrt-fs.jar",
311             // -XD option names
312             "process.packages",
313             "ignore.symbol.file",
314             "fileManager.deferClose",
315             // prefix/embedded strings
316             "compiler.",
317             "compiler.misc.",
318             "compiler.misc.tree.tag.",
319             "opt.Xlint.desc.",
320             "count.",
321             "illegal.",
322             "java.",
323             "javac.",
324             "verbose.",
325             "locn."
326     ));
327 
328     void checkFormats(List<ResourceBundle> messageFormatBundles) {
329         for (ResourceBundle bundle : messageFormatBundles) {
330             for (String key : bundle.keySet()) {
331                 final String pattern = bundle.getString(key);
332                 try {
333                     validateMessageFormatPattern(pattern);
334                 } catch (IllegalArgumentException e) {
335                     error("Invalid MessageFormat pattern for resource \""
336                         + key + "\": " + e.getMessage());
337                 }
338             }
339         }
340     }
341 
342     /**
343      * Do some basic validation of a {@link java.text.MessageFormat} format string.
344      *
345      * <p>
346      * This checks for balanced braces and unnecessary quoting.
347      * Code cut, pasted, &amp; simplified from {@link java.text.MessageFormat#applyPattern}.
348      *
349      * @throws IllegalArgumentException if {@code pattern} is invalid
350      * @throws IllegalArgumentException if {@code pattern} is null
351      */
352     public static void validateMessageFormatPattern(String pattern) {
353 
354         // Check for null
355         if (pattern == null)
356             throw new IllegalArgumentException("null pattern");
357 
358         // Replicate the quirky lexical analysis of MessageFormat's parsing algorithm
359         final int SEG_RAW = 0;
360         final int SEG_INDEX = 1;
361         final int SEG_TYPE = 2;
362         final int SEG_MODIFIER = 3;
363         int part = SEG_RAW;
364         int braceStack = 0;
365         int quotedStartPos = -1;
366         for (int i = 0; i < pattern.length(); i++) {
367             final char ch = pattern.charAt(i);
368             if (part == SEG_RAW) {
369                 if (ch == '\'') {
370                     if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '\'')
371                         i++;
372                     else if (quotedStartPos == -1)
373                         quotedStartPos = i;
374                     else {
375                         validateMessageFormatQuoted(pattern.substring(quotedStartPos + 1, i));
376                         quotedStartPos = -1;
377                     }
378                 } else if (ch == '{' && quotedStartPos == -1)
379                     part = SEG_INDEX;
380                 continue;
381             }
382             if (quotedStartPos != -1) {
383                 if (ch == '\'') {
384                     validateMessageFormatQuoted(pattern.substring(quotedStartPos + 1, i));
385                     quotedStartPos = -1;
386                 }
387                 continue;
388             }
389             switch (ch) {
390             case ',':
391                 if (part < SEG_MODIFIER)
392                     part++;
393                 break;
394             case '{':
395                 braceStack++;
396                 break;
397             case '}':
398                 if (braceStack == 0)
399                     part = SEG_RAW;
400                 else
401                     braceStack--;
402                 break;
403             case '\'':
404                 quotedStartPos = i;
405                 break;
406             default:
407                 break;
408             }
409         }
410         if (part != SEG_RAW)
411             throw new IllegalArgumentException("unmatched braces");
412         if (quotedStartPos != -1)
413             throw new IllegalArgumentException("unmatched quote starting at offset " + quotedStartPos);
414     }
415 
416     /**
417      * Validate the content of a quoted substring in a {@link java.text.MessageFormat} pattern.
418      *
419      * <p>
420      * We expect this content to contain at least one special character. Otherwise,
421      * it was probably meant to be something in single quotes but somebody forgot
422      * to escape the single quotes by doulbing them; and even if intentional,
423      * it's still bogus because the single quotes are just going to get discarded
424      * and so they were unnecessary in the first place.
425      */
426     static void validateMessageFormatQuoted(String quoted) {
427         if (quoted.matches("[^'{},]+"))
428             throw new IllegalArgumentException("unescaped single quotes around \"" + quoted + "\"");
429     }
430 
431     /**
432      * Look for a resource that ends in this string fragment.
433      */
434     boolean hasMatch(Set<String> resourceKeys, String s) {
435         for (String rk: resourceKeys) {
436             if (rk.endsWith(s))
437                 return true;
438         }
439         return false;
440     }
441 
442     /**
443      * Get the set of strings from (most of) the javac classfiles.
444      */
445     Set<String> getCodeStrings() throws IOException {
446         Set<String> results = new TreeSet<String>();
447         JavaCompiler c = ToolProvider.getSystemJavaCompiler();
448         try (JavaFileManager fm = c.getStandardFileManager(null, null, null)) {
449             JavaFileManager.Location javacLoc = findJavacLocation(fm);
450             String[] pkgs = {
451                 "javax.annotation.processing",
452                 "javax.lang.model",
453                 "javax.tools",
454                 "com.sun.source",
455                 "com.sun.tools.javac"
456             };
457             for (String pkg: pkgs) {
458                 for (JavaFileObject fo: fm.list(javacLoc,
459                         pkg, EnumSet.of(JavaFileObject.Kind.CLASS), true)) {
460                     String name = fo.getName();
461                     // ignore resource files, and files which are not really part of javac
462                     if (name.matches(".*resources.[A-Za-z_0-9]+\\.class.*")
463                             || name.matches(".*CreateSymbols\\.class.*"))
464                         continue;
465                     scan(fo, results);
466                 }
467             }
468             return results;
469         }
470     }
471 
472     // depending on how the test is run, javac may be on bootclasspath or classpath
473     JavaFileManager.Location findJavacLocation(JavaFileManager fm) {
474         JavaFileManager.Location[] locns =
475             { StandardLocation.PLATFORM_CLASS_PATH, StandardLocation.CLASS_PATH };
476         try {
477             for (JavaFileManager.Location l: locns) {
478                 JavaFileObject fo = fm.getJavaFileForInput(l,
479                     "com.sun.tools.javac.Main", JavaFileObject.Kind.CLASS);
480                 if (fo != null)
481                     return l;
482             }
483         } catch (IOException e) {
484             throw new Error(e);
485         }
486         throw new IllegalStateException("Cannot find javac");
487     }
488 
489     /**
490      * Get the set of strings from a class file.
491      * Only strings that look like they might be a resource key are returned.
492      */
493     void scan(JavaFileObject fo, Set<String> results) throws IOException {
494         try (InputStream in = fo.openInputStream()) {
495             ClassModel cm = ClassFile.of().parse(in.readAllBytes());
496             for (PoolEntry pe : cm.constantPool()) {
497                 if (pe instanceof Utf8Entry entry) {
498                     String v = entry.stringValue();
499                     if (v.matches("[A-Za-z0-9-_.]+"))
500                         results.add(v);
501                 }
502             }
503         } catch (ConstantPoolException ignore) {
504         }
505     }
506 
507     /**
508      * Get the set of keys from the javac resource bundles.
509      */
510     Set<String> getResourceKeys() {
511         Module jdk_compiler = ModuleLayer.boot().findModule("jdk.compiler").get();
512         Set<String> results = new TreeSet<String>();
513         for (String name : new String[]{"javac", "compiler", "launcher"}) {
514             ResourceBundle b =
515                     ResourceBundle.getBundle("com.sun.tools.javac.resources." + name, jdk_compiler);
516             results.addAll(b.keySet());
517         }
518         return results;
519     }
520 
521     /**
522      * Get resource bundles containing MessageFormat strings.
523      */
524     List<ResourceBundle> getMessageFormatBundles() {
525         Module jdk_compiler = ModuleLayer.boot().findModule("jdk.compiler").get();
526         List<ResourceBundle> results = new ArrayList<>();
527         for (String name : new String[]{"compiler", "launcher"}) {
528             ResourceBundle b =
529                     ResourceBundle.getBundle("com.sun.tools.javac.resources." + name, jdk_compiler);
530             results.add(b);
531         }
532         return results;
533     }
534 
535     /**
536      * Report an error.
537      */
538     void error(String msg) {
539         System.err.println("Error: " + msg);
540         errors++;
541     }
542 
543     int errors;
544 }