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