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, & 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 }