All Downloads are FREE. Search and download functionalities are using the official Maven repository.

framework.src.org.checkerframework.framework.util.CheckerMain Maven / Gradle / Ivy

Go to download

The Checker Framework enhances Java’s type system to make it more powerful and useful. This lets software developers detect and prevent errors in their Java programs. The Checker Framework includes compiler plug-ins ("checkers") that find bugs or verify their absence. It also permits you to write your own compiler plug-ins.

There is a newer version: 3.42.0
Show newest version
package org.checkerframework.framework.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarInputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;

/**
 * This class functions essentially the same as the jsr308-langtools javac
 * script EXCEPT that it adds the appropriate jdk.jar to the bootclasspath and
 * adds checker.jar to the processor passed to javac.
 * 

* * To debug it, use the -AoutputArgsToFile=... command-line argument. */ public class CheckerMain { /** * Invoke the JSR 308 Type Annotations Compiler. * Any exception thrown by the Checker Framework escapes to the command line. */ public static void main(String[] args) { final File pathToThisJar = new File(findPathTo(CheckerMain.class, false)); final CheckerMain program = new CheckerMain(pathToThisJar, args); final int exitStatus = program.invokeCompiler(); System.exit(exitStatus); } /** * The path to the annotated jdk jar to use */ protected final File jdkJar; /** * The path to the jsr308 Langtools Type Annotations Compiler */ protected final File javacJar; /** * The path to the jar containing CheckerMain.class (i.e. checker.jar) */ protected final File checkerJar; /** * The path to checker-qual.jar */ protected final File checkerQualJar; private final List compilationBootclasspath; private final List runtimeBootClasspath; private final List jvmOpts; private final List cpOpts; private final List ppOpts; private final List toolOpts; private final List argListFiles; /** * Construct all the relevant file locations and Java version given the path to this jar and * a set of directories in which to search for jars. */ public CheckerMain(final File checkerJar, final String [] args) { this.checkerJar = checkerJar; final File searchPath = checkerJar.getParentFile(); this.checkerQualJar = new File(searchPath, "checker-qual.jar"); final List argsList = new ArrayList(args.length); for (String arg : args) { argsList.add(arg.trim()); } replaceShorthandProcessor(argsList); argListFiles = collectArgFiles(argsList); this.javacJar = extractFileArg(PluginUtil.JAVAC_PATH_OPT, new File(searchPath, "javac.jar"), argsList); final String jdkJarName = PluginUtil.getJdkJarName(); this.jdkJar = extractFileArg(PluginUtil.JDK_PATH_OPT, new File(searchPath, jdkJarName), argsList); this.compilationBootclasspath = createCompilationBootclasspath(argsList); this.runtimeBootClasspath = createRuntimeBootclasspath(argsList); this.jvmOpts = extractJvmOpts(argsList); this.cpOpts = createCpOpts(argsList); this.ppOpts = createPpOpts(argsList); this.toolOpts = argsList; assertValidState(); } protected void assertValidState() { assertFilesExist(Arrays.asList(javacJar, jdkJar, checkerJar, checkerQualJar)); } public void addToClasspath(List cpOpts) { this.cpOpts.addAll(cpOpts); } public void addToProcessorpath(List ppOpts) { this.ppOpts.addAll(ppOpts); } public void addToRuntimeBootclasspath(List runtimeBootClasspathOpts) { this.runtimeBootClasspath.addAll(runtimeBootClasspathOpts); } protected List createRuntimeBootclasspath(final List argsList) { return new ArrayList(Arrays.asList(javacJar.getAbsolutePath())); } protected List createCompilationBootclasspath(final List argsList) { final List extractedBcp = extractBootClassPath(argsList); extractedBcp.add(0, jdkJar.getAbsolutePath()); return extractedBcp; } protected List createCpOpts(final List argsList) { final List extractedOps = extractCpOpts(argsList); extractedOps.add(0, this.checkerQualJar.getAbsolutePath()); return extractedOps; } // Assumes that createCpOpts has already been run. protected List createPpOpts(final List argsList) { final List extractedOps = extractPpOpts(argsList); if (extractedOps.isEmpty()) { // If processorpath is not provided, then javac uses the classpath. // CheckerMain always supplies a processorpath, so if the user // didn't specify a processorpath, then use the classpath. extractedOps.addAll(this.cpOpts); } extractedOps.add(0, this.checkerJar.getAbsolutePath()); return extractedOps; } /** * Return the arguments that start with @ and therefore * are files that contain javac arguments. * @param args a list of command-line arguments; is not modified * @return a List of files representing all arguments that started with @ */ protected List collectArgFiles(final List args) { final List argListFiles = new ArrayList(); for (final String arg : args) { if (arg.startsWith("@")) { argListFiles.add( new File(arg.substring(1)) ); } } return argListFiles; } /** * Remove the argument given by argumentName and the subsequent value from the list args if present. * Return the subsequent value. * @param argumentName a command-line option name whose argument to extract * @param alternative default value to return if argumentName does not appear in args * @param args the current list of arguments * @return the string that follows argumentName if argumentName is in args, or alternative if * argumentName is not present in args */ protected static String extractArg(final String argumentName, final String alternative, final List args) { int i = args.indexOf(argumentName); if (i == -1) { return alternative; } else if (i == args.size() - 1) { throw new RuntimeException("Argument " + argumentName + " specified but given no value!"); } else { args.remove(i); return args.remove(i); } } /** * Remove the argument given by argumentName and the subsequent value from the list args if present. * Return the subsequent value wrapped as a File. * @param argumentName argument to extract * @param alternative file to return if argumentName is not found in args * @param args the current list of arguments * @return the string that follows argumentName wrapped as a File if argumentName is in args or alternative if * argumentName is not present in args */ protected static File extractFileArg(final String argumentName, final File alternative, final List args) { final String filePath = extractArg(argumentName, null, args); if (filePath == null) { return alternative; } else { return new File(filePath); } } /** * Find all args that match the given pattern and extract their index 1 group. Add all the index 1 groups to the * returned list. Remove all matching args from the input args list. * @param pattern a pattern with at least one matching group * @param allowEmpties whether or not to add empty group(1) matches to the returned list * @param args the arguments to extract from * @return a list of arguments from the first group that matched the pattern for each input args or the empty list * if there were none */ protected static List extractOptWithPattern(final Pattern pattern, boolean allowEmpties, final List args) { final List matchedArgs = new ArrayList(); int i = 0; while (i < args.size()) { final Matcher matcher = pattern.matcher(args.get(i)); if (matcher.matches()) { final String arg = matcher.group(1).trim(); if (!arg.isEmpty() || allowEmpties) { matchedArgs.add(arg); } args.remove(i); } else { i++; } } return matchedArgs; } /** * A pattern to match bootclasspath prepend entries, used to construct one -Xbootclasspath/p: argument */ protected static final Pattern BOOT_CLASS_PATH_REGEX = Pattern.compile("^(?:-J)?-Xbootclasspath/p:(.*)$"); // TODO: Why does this treat -J and -J-X the same? They have different semantics, don't they? /** * Remove all -Xbootclasspath/p: or -J-Xbootclasspath/p: arguments from args and add them to the returned list * @param args the arguments to extract from * @return all non-empty arguments matching BOOT_CLASS_PATH_REGEX or an empty list if there were none */ protected static List extractBootClassPath(final List args) { return extractOptWithPattern(BOOT_CLASS_PATH_REGEX, false, args); } /** * Matches all -J arguments */ protected static final Pattern JVM_OPTS_REGEX = Pattern.compile("^(?:-J)(.*)$"); /** * Remove all -J arguments from args and add them to the returned list * @param args the arguments to extract from * @return all -J arguments (without the -J prefix) or an empty list if there were none */ protected static List extractJvmOpts(final List args) { return extractOptWithPattern(JVM_OPTS_REGEX, false, args); } /** * Remove the -cp and -classpath options and their arguments from args. * Return the last argument. If no -cp or -classpath arguments were * present then return the CLASSPATH environment variable followed by * the current directory. * * @param args a list of arguments to extract from * @return the arguments that should be put on the classpath when calling javac.jar */ protected static List extractCpOpts(final List args) { List actualArgs = new ArrayList(); String path = null; for (int i=0; i extractPpOpts(final List args) { List actualArgs = new ArrayList(); String path = null; for (int i=0; i args) { args.add("com.sun.tools.javac.Main"); } /** * Invoke the JSR308 Type Annotations Compiler with all relevant jars on its classpath or boot classpath */ public List getExecArguments() { List args = new ArrayList(jvmOpts.size() + cpOpts.size() + toolOpts.size() + 7); final String java = PluginUtil.getJavaCommand(System.getProperty("java.home"), System.out); args.add(java); args.add("-Xbootclasspath/p:" + PluginUtil.join(File.pathSeparator, runtimeBootClasspath)); args.add("-ea"); // com.sun.tools needs to be enabled separately args.add("-ea:com.sun.tools..."); args.addAll(jvmOpts); addMainArgs(args); args.add("-Xbootclasspath/p:" + PluginUtil.join(File.pathSeparator, compilationBootclasspath)); if (!argsListHasClassPath(argListFiles)) { args.add("-classpath"); args.add(quote(PluginUtil.join(File.pathSeparator, cpOpts))); } if (!argsListHasProcessorPath(argListFiles)) { args.add("-processorpath"); args.add(quote(PluginUtil.join(File.pathSeparator, ppOpts))); } args.addAll(toolOpts); return args; } /** * Invoke the JSR308 Type Annotations Compiler with all relevant jars on its classpath or boot classpath */ public int invokeCompiler() { List args = getExecArguments(); for (int i = 0; i < args.size(); i++) { String arg = args.get(i); if (arg.startsWith("-AoutputArgsToFile=")) { String fileName = arg.substring(19); args.remove(i); outputArgumentsToFile(fileName, args); break; } } // Actually invoke the compiler return ExecUtil.execute(args.toArray(new String[args.size()]), System.out, System.err); } private static void outputArgumentsToFile(String outputFilename, List args) { if (outputFilename != null) { String errorMessage = null; try { PrintWriter writer = new PrintWriter(outputFilename, "UTF-8"); for (int i = 0; i < args.size(); i++) { String arg = args.get(i); // We would like to include the filename of the argfile instead of its contents. // The problem is that the file will sometimes disappear by the time the user can // look at or run the resulting script. Maven deletes the argfile very shortly // after it has been handed off to javac, for example. Ideally we would print // the argfile filename as a comment but the resulting file couldn't then be run as // a script on Unix or Windows. if (arg.startsWith("@")) { // Read argfile and include its parameters in the output file. String inputFilename = arg.substring(1); BufferedReader br = new BufferedReader(new FileReader(inputFilename)); String line; while ((line = br.readLine()) != null) { writer.print(line); writer.print(" "); } br.close(); } else { writer.print(arg); writer.print(" "); } } writer.close(); } catch (IOException e) { errorMessage = e.toString(); } if (errorMessage != null) { System.err.println("Failed to output command-line arguments to file " + outputFilename + " due to exception: " + errorMessage); } } } /** * Returns true if some @arglist file sets the classpath. * @param argListFiles command-line argument files (specified with @ on the command line) */ private static boolean argsListHasClassPath(final List argListFiles) { for (final String arg : expandArgFiles(argListFiles)) { if (arg.contains("-classpath") || arg.contains("-cp")) { return true; } } return false; } /** * Returns true if some @arglist file sets the processorpath. * @param argListFiles command-line argument files (specified with @ on the command line) */ private static boolean argsListHasProcessorPath(final List argListFiles) { for (final String arg : expandArgFiles(argListFiles)) { if (arg.contains("-processorpath")) { return true; } } return false; } /** * Return all the lines in all the files. * @param files a list of files * @return a list of all the lines in all the files */ protected static List expandArgFiles(final List files) { final List content = new ArrayList(); for (final File file : files) { try { content.addAll(PluginUtil.readFile(file)); } catch (final IOException exc) { throw new RuntimeException("Could not open file: " + file.getAbsolutePath(), exc); } } return content; } /** * Find the jar file or directory containing the .class file from which cls was loaded * @param cls the class whose .class file we wish to locate; if null, CheckerMain.class. * @param errIfFromDirectory If false, throw an exception if the file was loaded from a directory */ public static String findPathTo(Class cls, boolean errIfFromDirectory) throws IllegalStateException { if (cls == null) { cls = CheckerMain.class; } String name = cls.getName(); String classFileName; /* name is something like package.name.ContainingClass$ClassName. We need to turn this into ContainingClass$ClassName.class. */ { int idx = name.lastIndexOf('.'); classFileName = (idx == -1 ? name : name.substring(idx+1)) + ".class"; } String uri = cls.getResource(classFileName).toString(); if (uri.startsWith("file:")) { if (errIfFromDirectory) { return uri; } else { throw new IllegalStateException("This class has been loaded from a directory and not from a jar file."); } } if (!uri.startsWith("jar:file:")) { int idx = uri.indexOf(':'); String protocol = idx == -1 ? "(unknown)" : uri.substring(0, idx); throw new IllegalStateException("This class has been loaded remotely via the " + protocol + " protocol. Only loading from a jar on the local file system is supported."); } int idx = uri.indexOf('!'); // Sanity check if (idx == -1) throw new IllegalStateException("You appear to have loaded this class from a local jar file, but I can't make sense of the URL!"); try { String fileName = URLDecoder.decode(uri.substring("jar:file:".length(), idx), Charset.defaultCharset().name()); return new File(fileName).getAbsolutePath(); } catch (UnsupportedEncodingException e) { throw new InternalError("Default charset doesn't exist. Your VM is borked."); } } /** * Assert that all files in the list exist and if they don't, throw a RuntimeException with a list of the files * that do not exist. * * @param expectedFiles files that must exist */ private static void assertFilesExist(final List expectedFiles) { final List missingFiles = new ArrayList(); for (final File file : expectedFiles) { if (file == null) { throw new RuntimeException("Null passed to assertFilesExist"); } if (!file.exists()) { missingFiles.add(file); } } if (!missingFiles.isEmpty()) { List missingAbsoluteFilenames = new ArrayList(missingFiles.size()); for (File missingFile : missingFiles) { missingAbsoluteFilenames.add(missingFile.getAbsolutePath()); } throw new RuntimeException("The following files could not be located: " + PluginUtil.join(", ", missingAbsoluteFilenames)); } } private static String quote(final String str) { if (str.contains(" ")) { if (str.contains("\"")) { throw new RuntimeException("Don't know how to quote " + str); } return "\"" + str + "\""; } return str; } /////////////////////////////////////////////////////////////////////////// /// Shorthand checker names /// /** * All "built-in" Checker Framework checkers, except SubtypingChecker, start with this package file path. * Framework Checkers, except for SubtypingChecker, are excluded from processor shorthand. */ protected static final String CHECKER_BASE_PACKAGE = "org.checkerframework.checker"; // Forward slash is used instead of File.separator because checker.jar uses / as the separator. protected static final String CHECKER_BASE_DIR_NAME = CHECKER_BASE_PACKAGE.replace(".", "/"); protected static final String FULLY_QUALIFIED_SUBTYPING_CHECKER = org.checkerframework.common.subtyping.SubtypingChecker.class.getCanonicalName(); protected static final String SUBTYPING_CHECKER_NAME = org.checkerframework.common.subtyping.SubtypingChecker.class.getSimpleName(); /** * Returns true if processorString, once transformed into * fully-qualified form, is present in fullyQualifiedCheckerNames. * Used by SourceChecker to determine whether a class is annotated for * any processor that is being run. * * @param processorString the name of a single processor, not a comma-separated list of processors * @param fullyQualifiedCheckerNames a list of fully-qualified checker names */ public static boolean matchesCheckerOrSubcheckerFromList(final String processorString, List fullyQualifiedCheckerNames) { if (processorString.contains(",")) { return false; // Do not process strings containing multiple processors. } return fullyQualifiedCheckerNames.contains(unshorthandProcessorNames(processorString, fullyQualifiedCheckerNames, true)); } /** * For every "-processor" argument in args, replace its immediate successor argument using * unabbreviateProcessorNames */ protected void replaceShorthandProcessor(final List args) { for (int i = 0; i < args.size(); i++) { final int nextIndex = i + 1; if (args.size() > nextIndex) { if (args.get(i).equals("-processor")) { final String replacement = unshorthandProcessorNames(args.get(nextIndex), getAllCheckerClassNames(), false); args.remove(nextIndex); args.add(nextIndex, replacement); } } } } /** * Returns the list of fully qualified names of the checkers found in checker.jar. * This covers only checkers with the name ending in "Checker". * Checkers with a name ending in "Subchecker" are not included in the returned list. * Note however that it is possible for a checker with the name ending in "Checker" to be used as a subchecker. */ private List getAllCheckerClassNames() { ArrayList checkerClassNames = new ArrayList(); try { final JarInputStream checkerJarIs = new JarInputStream(new FileInputStream(checkerJar)); ZipEntry entry; while ((entry = checkerJarIs.getNextEntry()) != null) { final String name = entry.getName(); if (name.startsWith(CHECKER_BASE_DIR_NAME) && name.endsWith("Checker.class")) { // Checkers ending in "Subchecker" are not included in this list used by CheckerMain. // Forward slash is used instead of File.separator because checker.jar uses / as the separator. checkerClassNames.add(PluginUtil.join(".", name.substring(0, name.length() - ".class".length()).split("/"))); } } checkerJarIs.close(); } catch (IOException e) { throw new RuntimeException("Could not read " + checkerJar, e); } return checkerClassNames; } /** * Takes a string of comma-separated processor names, and * expands any shorthands to fully-qualified names from the * fullyQualifiedCheckerNames list. For example: * *

     * NullnessChecker → org.checkerframework.checker.nullness.NullnessChecker
     * nullness → org.checkerframework.checker.nullness.NullnessChecker
     * NullnessChecker,RegexChecker → org.checkerframework.checker.nullness.NullnessChecker,org.checkerframework.checker.regex.RegexChecker
     * 
* * Note, a processor entry only gets replaced if it contains NO "." (i.e., it is not qualified by a * package name) and can be found under the package org.checkerframework.checker in checker.jar. * * @param processorsString a comma-separated string identifying processors * @param fullyQualifiedCheckerNames a list of fully-qualified checker names to match processorsString against * @param allowSubcheckers whether to match against fully qualified checker names ending with "Subchecker" * @return processorsString where all shorthand references to Checker Framework built-in checkers * are replaced with fully-qualified references */ protected static String unshorthandProcessorNames(final String processorsString, List fullyQualifiedCheckerNames, boolean allowSubcheckers) { final String[] processors = processorsString.split(","); for (int i = 0; i < processors.length; i++) { if (processors[i].equals(SUBTYPING_CHECKER_NAME)) { // Allow "subtyping" as well. processors[i] = FULLY_QUALIFIED_SUBTYPING_CHECKER; } else { if (!processors[i].contains(".")) { // Not already fully qualified processors[i] = unshorthandProcessorName(processors[i], fullyQualifiedCheckerNames, allowSubcheckers); } } } return PluginUtil.join(",", processors); } /** * Given a processor name, tries to expand it to a checker in the fullyQualifiedCheckerNames list. * Returns that expansion, or the argument itself if the expansion fails. */ private static String unshorthandProcessorName(final String processor, List fullyQualifiedCheckerNames, boolean allowSubcheckers) { for (final String name : fullyQualifiedCheckerNames) { boolean tryMatch = false; String[] checkerPath = name.substring(0, name.length() - "Checker".length()).split("\\."); String checkerNameShort = checkerPath[checkerPath.length - 1]; String checkerName = checkerNameShort + "Checker"; if (name.endsWith("Checker")) { checkerPath = name.substring(0, name.length() - "Checker".length()).split("\\."); checkerNameShort = checkerPath[checkerPath.length - 1]; checkerName = checkerNameShort + "Checker"; tryMatch = true; } else if (allowSubcheckers && name.endsWith("Subchecker")) { checkerPath = name.substring(0, name.length() - "Subchecker".length()).split("\\."); checkerNameShort = checkerPath[checkerPath.length - 1]; checkerName = checkerNameShort + "Subchecker"; tryMatch = true; } if (tryMatch) { if (processor.equalsIgnoreCase(checkerName) || processor.equalsIgnoreCase(checkerNameShort)) { return name; } } } return processor; // If not matched, return the input string. } /** * Given a shorthand processor name, returns true if it can be expanded to a checker in the fullyQualifiedCheckerNames list. * Does not match the subtyping checker. * * @param processor a string identifying one processor * @param fullyQualifiedCheckerNames a list of fully-qualified checker names to match processor against * @param allowSubcheckers whether to match against fully qualified checker names ending with "Subchecker" */ public static boolean matchesFullyQualifiedProcessor(final String processor, List fullyQualifiedCheckerNames, boolean allowSubcheckers) { return !processor.equals(unshorthandProcessorName(processor, fullyQualifiedCheckerNames, allowSubcheckers)); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy