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, & 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[]{"javac", "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 }