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