1 /*
   2  * Copyright (c) 2010, 2011, 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  */
  29 
  30 import java.io.*;
  31 import java.util.*;
  32 import javax.tools.*;
  33 import com.sun.tools.classfile.*;
  34 
  35 /**
  36  * Compare string constants in javac classes against keys in javac resource bundles.
  37  */
  38 public class CheckResourceKeys {
  39     /**
  40      * Main program.
  41      * Options:
  42      * -finddeadkeys
  43      *      look for keys in resource bundles that are no longer required
  44      * -findmissingkeys
  45      *      look for keys in resource bundles that are missing
  46      *
  47      * @throws Exception if invoked by jtreg and errors occur
  48      */
  49     public static void main(String... args) throws Exception {
  50         CheckResourceKeys c = new CheckResourceKeys();
  51         if (c.run(args))
  52             return;
  53 
  54         if (is_jtreg())
  55             throw new Exception(c.errors + " errors occurred");
  56         else
  57             System.exit(1);
  58     }
  59 
  60     static boolean is_jtreg() {
  61         return (System.getProperty("test.src") != null);
  62     }
  63 
  64     /**
  65      * Main entry point.
  66      */
  67     boolean run(String... args) throws Exception {
  68         boolean findDeadKeys = false;
  69         boolean findMissingKeys = false;
  70 
  71         if (args.length == 0) {
  72             if (is_jtreg()) {
  73                 findDeadKeys = true;
  74                 findMissingKeys = true;
  75             } else {
  76                 System.err.println("Usage: java CheckResourceKeys <options>");
  77                 System.err.println("where options include");
  78                 System.err.println("  -finddeadkeys      find keys in resource bundles which are no longer required");
  79                 System.err.println("  -findmissingkeys   find keys in resource bundles that are required but missing");
  80                 return true;
  81             }
  82         } else {
  83             for (String arg: args) {
  84                 if (arg.equalsIgnoreCase("-finddeadkeys"))
  85                     findDeadKeys = true;
  86                 else if (arg.equalsIgnoreCase("-findmissingkeys"))
  87                     findMissingKeys = true;
  88                 else
  89                     error("bad option: " + arg);
  90             }
  91         }
  92 
  93         if (errors > 0)
  94             return false;
  95 
  96         Set<String> codeStrings = getCodeStrings();
  97         Set<String> resourceKeys = getResourceKeys();
  98 
  99         if (findDeadKeys)
 100             findDeadKeys(codeStrings, resourceKeys);
 101 
 102         if (findMissingKeys)
 103             findMissingKeys(codeStrings, resourceKeys);
 104 
 105         return (errors == 0);
 106     }
 107 
 108     /**
 109      * Find keys in resource bundles which are probably no longer required.
 110      * A key is probably required if there is a string fragment in the code
 111      * that is part of the resource key, or if the key is well-known
 112      * according to various pragmatic rules.
 113      */
 114     void findDeadKeys(Set<String> codeStrings, Set<String> resourceKeys) {
 115         String[] prefixes = {
 116             "compiler.err.", "compiler.warn.", "compiler.note.", "compiler.misc.",
 117             "javac."
 118         };
 119         for (String rk: resourceKeys) {
 120             // some keys are used directly, without a prefix.
 121             if (codeStrings.contains(rk))
 122                 continue;
 123 
 124             // remove standard prefix
 125             String s = null;
 126             for (int i = 0; i < prefixes.length && s == null; i++) {
 127                 if (rk.startsWith(prefixes[i])) {
 128                     s = rk.substring(prefixes[i].length());
 129                 }
 130             }
 131             if (s == null) {
 132                 error("Resource key does not start with a standard prefix: " + rk);
 133                 continue;
 134             }
 135 
 136             if (codeStrings.contains(s))
 137                 continue;
 138 
 139             // keys ending in .1 are often synthesized
 140             if (s.endsWith(".1") && codeStrings.contains(s.substring(0, s.length() - 2)))
 141                 continue;
 142 
 143             // verbose keys are generated by ClassReader.printVerbose
 144             if (s.startsWith("verbose.") && codeStrings.contains(s.substring(8)))
 145                 continue;
 146 
 147             // mandatory warning messages are synthesized with no characteristic substring
 148             if (isMandatoryWarningString(s))
 149                 continue;
 150 
 151             // check known (valid) exceptions
 152             if (knownRequired.contains(rk))
 153                 continue;
 154 
 155             // check known suspects
 156             if (needToInvestigate.contains(rk))
 157                 continue;
 158 
 159             error("Resource key not found in code: " + rk);
 160         }
 161     }
 162 
 163     /**
 164      * The keys for mandatory warning messages are all synthesized and do not
 165      * have a significant recognizable substring to look for.
 166      */
 167     private boolean isMandatoryWarningString(String s) {
 168         String[] bases = { "deprecated", "unchecked", "varargs", "sunapi" };
 169         String[] tails = { ".filename", ".filename.additional", ".plural", ".plural.additional", ".recompile" };
 170         for (String b: bases) {
 171             if (s.startsWith(b)) {
 172                 String tail = s.substring(b.length());
 173                 for (String t: tails) {
 174                     if (tail.equals(t))
 175                         return true;
 176                 }
 177             }
 178         }
 179         return false;
 180     }
 181 
 182     Set<String> knownRequired = new TreeSet<String>(Arrays.asList(
 183         // See Resolve.getErrorKey
 184         "compiler.err.cant.resolve.args",
 185         "compiler.err.cant.resolve.args.params",
 186         "compiler.err.cant.resolve.location.args",
 187         "compiler.err.cant.resolve.location.args.params",
 188         // JavaCompiler, reports #errors and #warnings
 189         "compiler.misc.count.error",
 190         "compiler.misc.count.error.plural",
 191         "compiler.misc.count.warn",
 192         "compiler.misc.count.warn.plural",
 193         // Used for LintCategory
 194         "compiler.warn.lintOption",
 195         // Other
 196         "compiler.misc.base.membership"                                 // (sic)
 197         ));
 198 
 199 
 200     Set<String> needToInvestigate = new TreeSet<String>(Arrays.asList(
 201         "compiler.err.cant.read.file",                      // UNUSED
 202         "compiler.err.illegal.self.ref",                    // UNUSED
 203         "compiler.err.io.exception",                        // UNUSED
 204         "compiler.err.limit.pool.in.class",                 // UNUSED
 205         "compiler.err.name.reserved.for.internal.use",      // UNUSED
 206         "compiler.err.no.match.entry",                      // UNUSED
 207         "compiler.err.not.within.bounds.explain",           // UNUSED
 208         "compiler.err.signature.doesnt.match.intf",         // UNUSED
 209         "compiler.err.signature.doesnt.match.supertype",    // UNUSED
 210         "compiler.err.type.var.more.than.once",             // UNUSED
 211         "compiler.err.type.var.more.than.once.in.result",   // UNUSED
 212         "compiler.misc.ccf.found.later.version",            // UNUSED
 213         "compiler.misc.non.denotable.type",                 // UNUSED
 214         "compiler.misc.unnamed.package",                    // should be required, CR 6964147
 215         "compiler.misc.verbose.retro",                      // UNUSED
 216         "compiler.misc.verbose.retro.with",                 // UNUSED
 217         "compiler.misc.verbose.retro.with.list",            // UNUSED
 218         "compiler.warn.proc.type.already.exists",           // TODO in JavacFiler
 219         "javac.err.invalid.arg",                            // UNUSED ??
 220         "javac.opt.arg.class",                              // UNUSED ??
 221         "javac.opt.arg.pathname",                           // UNUSED ??
 222         "javac.opt.moreinfo",                               // option commented out
 223         "javac.opt.nogj",                                   // UNUSED
 224         "javac.opt.printflat",                              // option commented out
 225         "javac.opt.printsearch",                            // option commented out
 226         "javac.opt.prompt",                                 // option commented out
 227         "javac.opt.retrofit",                               // UNUSED
 228         "javac.opt.s",                                      // option commented out
 229         "javac.opt.scramble",                               // option commented out
 230         "javac.opt.scrambleall"                             // option commented out
 231         ));
 232 
 233     /**
 234      * For all strings in the code that look like they might be fragments of
 235      * a resource key, verify that a key exists.
 236      */
 237     void findMissingKeys(Set<String> codeStrings, Set<String> resourceKeys) {
 238         for (String cs: codeStrings) {
 239             if (cs.matches("[A-Za-z][^.]*\\..*")) {
 240                 // ignore filenames (i.e. in SourceFile attribute
 241                 if (cs.matches(".*\\.java"))
 242                     continue;
 243                 // ignore package and class names
 244                 if (cs.matches("(com|java|javax|sun)\\.[A-Za-z.]+"))
 245                     continue;
 246                 // explicit known exceptions
 247                 if (noResourceRequired.contains(cs))
 248                     continue;
 249                 // look for matching resource
 250                 if (hasMatch(resourceKeys, cs))
 251                     continue;
 252                 error("no match for \"" + cs + "\"");
 253             }
 254         }
 255     }
 256     // where
 257     private Set<String> noResourceRequired = new HashSet<String>(Arrays.asList(
 258             // system properties
 259             "application.home", // in Paths.java
 260             "env.class.path",
 261             "line.separator",
 262             "user.dir",
 263             // file names
 264             "ct.sym",
 265             "rt.jar",
 266             "tools.jar",
 267             // -XD option names
 268             "process.packages",
 269             "ignore.symbol.file",
 270             // prefix/embedded strings
 271             "compiler.",
 272             "compiler.misc.",
 273             "count.",
 274             "illegal.",
 275             "javac.",
 276             "verbose."
 277     ));
 278 
 279     /**
 280      * Look for a resource that ends in this string fragment.
 281      */
 282     boolean hasMatch(Set<String> resourceKeys, String s) {
 283         for (String rk: resourceKeys) {
 284             if (rk.endsWith(s))
 285                 return true;
 286         }
 287         return false;
 288     }
 289 
 290     /**
 291      * Get the set of strings from (most of) the javac classfiles.
 292      */
 293     Set<String> getCodeStrings() throws IOException {
 294         Set<String> results = new TreeSet<String>();
 295         JavaCompiler c = ToolProvider.getSystemJavaCompiler();
 296         JavaFileManager fm = c.getStandardFileManager(null, null, null);
 297         JavaFileManager.Location javacLoc = findJavacLocation(fm);
 298         String[] pkgs = {
 299             "javax.annotation.processing",
 300             "javax.lang.model",
 301             "javax.tools",
 302             "com.sun.source",
 303             "com.sun.tools.javac"
 304         };
 305         for (String pkg: pkgs) {
 306             for (JavaFileObject fo: fm.list(javacLoc,
 307                     pkg, EnumSet.of(JavaFileObject.Kind.CLASS), true)) {
 308                 String name = fo.getName();
 309                 // ignore resource files, and files which are not really part of javac
 310                 if (name.contains("resources")
 311                         || name.contains("Launcher.class")
 312                         || name.contains("CreateSymbols.class"))
 313                     continue;
 314                 scan(fo, results);
 315             }
 316         }
 317         return results;
 318     }
 319 
 320     // depending on how the test is run, javac may be on bootclasspath or classpath
 321     JavaFileManager.Location findJavacLocation(JavaFileManager fm) {
 322         JavaFileManager.Location[] locns =
 323             { StandardLocation.PLATFORM_CLASS_PATH, StandardLocation.CLASS_PATH };
 324         try {
 325             for (JavaFileManager.Location l: locns) {
 326                 JavaFileObject fo = fm.getJavaFileForInput(l,
 327                     "com.sun.tools.javac.Main", JavaFileObject.Kind.CLASS);
 328                 if (fo != null)
 329                     return l;
 330             }
 331         } catch (IOException e) {
 332             throw new Error(e);
 333         }
 334         throw new IllegalStateException("Cannot find javac");
 335     }
 336 
 337     /**
 338      * Get the set of strings from a class file.
 339      * Only strings that look like they might be a resource key are returned.
 340      */
 341     void scan(JavaFileObject fo, Set<String> results) throws IOException {
 342         InputStream in = fo.openInputStream();
 343         try {
 344             ClassFile cf = ClassFile.read(in);
 345             for (ConstantPool.CPInfo cpinfo: cf.constant_pool.entries()) {
 346                 if (cpinfo.getTag() == ConstantPool.CONSTANT_Utf8) {
 347                     String v = ((ConstantPool.CONSTANT_Utf8_info) cpinfo).value;
 348                     if (v.matches("[A-Za-z0-9-_.]+"))
 349                         results.add(v);
 350                 }
 351             }
 352         } catch (ConstantPoolException ignore) {
 353         } finally {
 354             in.close();
 355         }
 356     }
 357 
 358     /**
 359      * Get the set of keys from the javac resource bundles.
 360      */
 361     Set<String> getResourceKeys() {
 362         Set<String> results = new TreeSet<String>();
 363         for (String name : new String[]{"javac", "compiler"}) {
 364             ResourceBundle b =
 365                     ResourceBundle.getBundle("com.sun.tools.javac.resources." + name);
 366             results.addAll(b.keySet());
 367         }
 368         return results;
 369     }
 370 
 371     /**
 372      * Report an error.
 373      */
 374     void error(String msg) {
 375         System.err.println("Error: " + msg);
 376         errors++;
 377     }
 378 
 379     int errors;
 380 }