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 }
--- EOF ---