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