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

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

package org.checkerframework.framework.util;

import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.PolyNull;
import org.checkerframework.checker.regex.qual.Regex;
import org.checkerframework.checker.signature.qual.FullyQualifiedName;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.SystemUtil;
import org.checkerframework.javacutil.UserError;
import org.plumelib.util.CollectionsPlume;

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.URL;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.StringJoiner;
import java.util.jar.JarInputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;

/**
 * This class behaves similarly to javac. CheckerMain does the following:
 *
 * 
    *
  • add the {@code javac.jar} to the runtime classpath of the process that runs the Checker * Framework. *
  • parse and implement any special options used by the Checker Framework, e.g., using * "shortnames" for annotation processors *
  • pass all remaining command-line arguments to the real javac *
* * To debug this class, use the {@code -AoutputArgsToFile=FILENAME} command-line argument or {@code * -AoutputArgsToFile=-} to output to standard out. * *

"To run the Checker Framework" really means to run java, where the program being run is javac * and javac is passed a {@code -processor} command-line argument that mentions a Checker Framework * checker. There are 5 relevant classpaths: The classpath and bootclasspath when running java, and * the classpath, bootclasspath, and processorpath used by javac. The latter three are the only * important ones. * *

Note for developers: Try to limit the work done (and options interpreted) by CheckerMain, * because its functionality is not available to users who choose not to use the Checker Framework * javac script. */ public class CheckerMain { /** * Any exception thrown by the Checker Framework escapes to the command line. * * @param args command-line arguments */ public static void main(String[] args) { File pathToThisJar = new File(findPathTo(CheckerMain.class, false)); ArrayList alargs = new ArrayList<>(Arrays.asList(args)); CheckerMain program = new CheckerMain(pathToThisJar, alargs); int exitStatus = program.invokeCompiler(); System.exit(exitStatus); } /** The path to the javacJar to use. */ 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; /** The path to checker-util.jar. */ protected final File checkerUtilJar; /** Compilation bootclasspath. */ private final List compilationBootclasspath; private final List runtimeClasspath; private final List jvmOpts; /** * Each element is either a classpath element (a directory or jar file) or is a classpath * (containing elements separated by File.pathSeparator). To produce the final classpath, * concatenate them all (separated by File.pathSeparator). */ private final List cpOpts; /** Processor path options. */ private final List ppOpts; /** Arguments to the Checker Framework. */ private final List toolOpts; /** Command-line argument files (specified with @ on the command line). */ private final List argListFiles; /** * Option name for specifying an alternative checker-qual.jar location. The accompanying value * MUST be the path to the jar file (NOT the path to its encompassing directory) */ public static final String CHECKER_QUAL_PATH_OPT = "-checkerQualJar"; /** * Option name for specifying an alternative checker-util.jar location. The accompanying value * MUST be the path to the jar file (NOT the path to its encompassing directory) */ public static final String CHECKER_UTIL_PATH_OPT = "-checkerUtilJar"; /** * Option name for specifying an alternative javac.jar location. The accompanying value MUST be * the path to the jar file (NOT the path to its encompassing directory) */ public static final String JAVAC_PATH_OPT = "-javacJar"; /** * Option name for specifying an alternative jdk.jar location. The accompanying value MUST be * the path to the jar file (NOT the path to its encompassing directory) */ public static final String JDK_PATH_OPT = "-jdkJar"; /** * 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(File checkerJar, List args) { this.checkerJar = checkerJar; File searchPath = checkerJar.getParentFile(); replaceShorthandProcessor(args); argListFiles = collectArgFiles(args); this.checkerQualJar = extractFileArg( CHECKER_QUAL_PATH_OPT, new File(searchPath, "checker-qual.jar"), args); this.checkerUtilJar = extractFileArg( CHECKER_UTIL_PATH_OPT, new File(searchPath, "checker-util.jar"), args); this.javacJar = extractFileArg(JAVAC_PATH_OPT, new File(searchPath, "javac.jar"), args); this.compilationBootclasspath = createCompilationBootclasspath(args); this.runtimeClasspath = createRuntimeClasspath(args); this.jvmOpts = extractJvmOpts(args); this.cpOpts = createCpOpts(args); this.ppOpts = createPpOpts(args); this.toolOpts = args; assertValidState(); } /** Assert that required jars exist. */ protected void assertValidState() { if (SystemUtil.jreVersion == 8) { assertFilesExist(javacJar, checkerJar, checkerQualJar, checkerUtilJar); } else { assertFilesExist(checkerJar, checkerQualJar, checkerUtilJar); } } public void addToClasspath(List cpOpts) { this.cpOpts.addAll(cpOpts); } public void addToProcessorpath(List ppOpts) { this.ppOpts.addAll(ppOpts); } public void addToRuntimeClasspath(List runtimeClasspathOpts) { this.runtimeClasspath.addAll(runtimeClasspathOpts); } protected List createRuntimeClasspath(List argsList) { return new ArrayList<>(Arrays.asList(javacJar.getAbsolutePath())); } /** * Returns the compilation bootclasspath from {@code argsList}. * * @param argsList args to add * @return the compilation bootclasspath from {@code argsList} */ protected List createCompilationBootclasspath(List argsList) { return extractBootClassPath(argsList); } protected List createCpOpts(List argsList) { List extractedOpts = extractCpOpts(argsList); extractedOpts.add(0, this.checkerQualJar.getAbsolutePath()); extractedOpts.add(0, this.checkerUtilJar.getAbsolutePath()); return extractedOpts; } /** * Returns processor path options. * *

This method assumes that createCpOpts has already been run. * * @param argsList arguments * @return processor path options */ protected List createPpOpts(List argsList) { List extractedOpts = new ArrayList<>(extractPpOpts(argsList)); if (extractedOpts.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. extractedOpts.addAll(this.cpOpts); } extractedOpts.add(0, this.checkerJar.getAbsolutePath()); extractedOpts.add(0, this.checkerUtilJar.getAbsolutePath()); return extractedOpts; } /** * 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(List args) { List argListFiles = new ArrayList<>(); for (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 @PolyNull String extractArg( String argumentName, @PolyNull String alternative, List args) { int i = args.indexOf(argumentName); if (i == -1) { return alternative; } else if (i == args.size() - 1) { throw new BugInCF( "Command line contains " + argumentName + " but no value following it"); } 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(String argumentName, File alternative, List args) { 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( @Regex(1) Pattern pattern, boolean allowEmpties, List args) { List matchedArgs = new ArrayList<>(); int i = 0; while (i < args.size()) { Matcher matcher = pattern.matcher(args.get(i)); if (matcher.matches()) { String group1 = matcher.group(1); if (group1 == null) { throw new BugInCF("Regex didn't capture group 1: " + pattern); } String arg = group1.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 {@code * -Xbootclasspath/p:} command-line 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 {@code -Xbootclasspath/p:} or {@code -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(List args) { return extractOptWithPattern(BOOT_CLASS_PATH_REGEX, false, args); } /** Matches all {@code -J} arguments. */ protected static final Pattern JVM_OPTS_REGEX = Pattern.compile("^(?:-J)(.*)$"); /** * Remove all {@code -J} arguments from {@code args} and add them to the returned list (without * the {@code -J} prefix). * * @param args the arguments to extract from * @return all {@code -J} arguments (without the {@code -J} prefix) or an empty list if there * were none */ protected static List extractJvmOpts(List args) { return extractOptWithPattern(JVM_OPTS_REGEX, false, args); } /** * Return the last {@code -cp} or {@code -classpath} option. If no {@code -cp} or {@code * -classpath} arguments were present, then return the CLASSPATH environment variable (if set) * followed by the current directory. * *

Also removes all {@code -cp} and {@code -classpath} options from args. * * @param args a list of arguments to extract from; is side-effected by this * @return collection of classpaths to concatenate to use when calling javac.jar */ protected static List extractCpOpts(List args) { List actualArgs = new ArrayList<>(); String lastCpArg = null; for (int i = 0; i < args.size(); i++) { if ((args.get(i).equals("-cp") || args.get(i).equals("-classpath")) && (i + 1 < args.size())) { args.remove(i); // Every classpath entry overrides the one before it. lastCpArg = args.remove(i); // re-process whatever is currently at element i i--; } } // The logic below is exactly what the javac script does. If no command-line classpath is // specified, use the "CLASSPATH" environment variable followed by the current directory. if (lastCpArg == null) { String systemClassPath = System.getenv("CLASSPATH"); if (systemClassPath != null && !systemClassPath.trim().isEmpty()) { actualArgs.add(systemClassPath.trim()); } actualArgs.add("."); } else { actualArgs.add(lastCpArg); } return actualArgs; } /** * Remove the {@code -processorpath} options and their arguments from args. Return the last * argument. * * @param args a list of arguments to extract from * @return the arguments that should be put on the processorpath when calling javac.jar */ protected static List extractPpOpts(List args) { String path = null; for (int i = 0; i < args.size(); i++) { if (args.get(i).equals("-processorpath") && (i + 1 < args.size())) { args.remove(i); path = args.remove(i); // re-process whatever is currently at element i i--; } } if (path != null) { return Collections.singletonList(path); } else { return Collections.emptyList(); } } protected void addMainToArgs(List args) { args.add("com.sun.tools.javac.Main"); } /** Invoke the compiler with all relevant jars on its classpath and/or bootclasspath. */ public List getExecArguments() { List args = new ArrayList<>(jvmOpts.size() + cpOpts.size() + toolOpts.size() + 7); // TODO: do we need java.exe on Windows? String java = "java"; args.add(java); if (SystemUtil.jreVersion == 8) { args.add("-Xbootclasspath/p:" + String.join(File.pathSeparator, runtimeClasspath)); } else { // See comments in build.gradle args.addAll( // Keep this list in sync with the lists in checker-framework/build.gradle in // compilerArgsForRunningCFs, the sections with labels // "javac-jdk11-non-modularized", "maven", and "sbt" in the manual, and in the // checker-framework-gradle-plugin, CheckerFrameworkPlugin#applyToProject Arrays.asList( // These are required in Java 17+ because the --illegal-access option is // set to deny by default. None of these packages are accessed via // reflection, so the module only needs to be exported, but not opened. "--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", "--add-exports", "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", "--add-exports", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", "--add-exports", "jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", "--add-exports", "jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED", "--add-exports", "jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", "--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", "--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", // Required because the Checker Framework reflectively accesses private // members in com.sun.tools.javac.comp. "--add-opens", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED")); } args.add("-classpath"); args.add(String.join(File.pathSeparator, runtimeClasspath)); args.add("-ea"); // com.sun.tools needs to be enabled separately args.add("-ea:com.sun.tools..."); args.addAll(jvmOpts); addMainToArgs(args); if (!argsListHasClassPath(argListFiles)) { args.add("-classpath"); args.add(quote(concatenatePaths(cpOpts))); } if (!argsListHasProcessorPath(argListFiles)) { args.add("-processorpath"); args.add(quote(concatenatePaths(ppOpts))); } if (SystemUtil.jreVersion == 8) { // No classes on the compilation bootclasspath will be loaded // during compilation, but the classes are read by the compiler // without loading them. The compiler assumes that any class on // this bootclasspath will be on the bootclasspath of the JVM used // to later run the classfiles that Javac produces. args.add( "-Xbootclasspath/p:" + String.join(File.pathSeparator, compilationBootclasspath)); } args.addAll(toolOpts); return args; } /** Given a list of paths, concatenate them to form a single path. Also expand wildcards. */ private String concatenatePaths(List paths) { List elements = new ArrayList<>(); for (String path : paths) { for (String element : SystemUtil.PATH_SEPARATOR_SPLITTER.split(path)) { elements.addAll(expandWildcards(element)); } } return String.join(File.pathSeparator, elements); } /** The string "/*" (on Unix). */ private static final String FILESEP_STAR = File.separator + "*"; /** * Given a path element that might be a wildcard, return a list of the elements it expands to. * If the element isn't a wildcard, return a singleton list containing the argument. Since the * original argument list is placed after 'com.sun.tools.javac.Main' in the new command line, * the JVM doesn't do wildcard expansion of jar files in any classpaths in the original argument * list. * * @param pathElement an element of a classpath * @return all elements of a classpath with wildcards expanded */ private List expandWildcards(String pathElement) { if (pathElement.equals("*")) { return jarFiles("."); } else if (pathElement.endsWith(FILESEP_STAR)) { return jarFiles(pathElement.substring(0, pathElement.length() - 1)); } else if (pathElement.isEmpty()) { return Collections.emptyList(); } else { return Collections.singletonList(pathElement); } } /** * Returns all the .jar and .JAR files in the given directory. * * @param directory a directory * @return all the .jar and .JAR files in the given directory */ private List jarFiles(String directory) { File dir = new File(directory); String[] jarFiles = dir.list((d, name) -> name.endsWith(".jar") || name.endsWith(".JAR")); if (jarFiles == null) { return Collections.emptyList(); } // concat directory with jar file path to give full path for (int i = 0; i < jarFiles.length; i++) { jarFiles[i] = directory + jarFiles[i]; } return Arrays.asList(jarFiles); } /** Invoke the compiler with all relevant jars on its classpath and/or bootclasspath. */ 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 { @SuppressWarnings("builder:required.method.not.called") // called when needed PrintWriter writer = (outputFilename.equals("-") ? new PrintWriter(System.out) : 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); try (BufferedReader br = new BufferedReader(new FileReader(inputFilename))) { String line; while ((line = br.readLine()) != null) { writer.print(line); writer.print(" "); } } } else { writer.print(arg); writer.print(" "); } writer.flush(); } if (!outputFilename.equals("-")) { 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(List argListFiles) { for (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(List argListFiles) { for (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(List files) { List content = new ArrayList<>(); for (File file : files) { try { content.addAll(Files.readAllLines(file.toPath())); } catch (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(@Nullable Class cls, boolean errIfFromDirectory) throws IllegalStateException { if (cls == null) { cls = CheckerMain.class; } String name = cls.getName(); String classFileName; /* name is something like pakkage.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"; } URL classFileUrl = cls.getResource(classFileName); if (classFileUrl == null) { throw new BugInCF("Cannot find resource " + classFileName); } if (classFileUrl.getProtocol().equals("file")) { if (errIfFromDirectory) { return classFileUrl.toString(); } else { throw new IllegalStateException( "This class has been loaded from a directory and not from a jar file."); } } String uri = classFileUrl.toString(); 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 URI has no" + " \"!\": " + uri); } try { String fileName = URLDecoder.decode( uri.substring("jar:file:".length(), idx), Charset.defaultCharset().name()); return new File(fileName).getAbsolutePath(); } catch (UnsupportedEncodingException e) { throw new BugInCF("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(File... expectedFiles) { List missingFiles = new ArrayList<>(); for (File file : expectedFiles) { if (file == null) { throw new RuntimeException("Null passed to assertFilesExist"); } if (!file.exists()) { missingFiles.add(file); } } if (!missingFiles.isEmpty()) { if (missingFiles.size() == 1) { File missingFile = missingFiles.get(0); if (missingFile.getName().equals("javac.jar")) { throw new UserError( "Could not find " + missingFile.getAbsolutePath() + ". This may be because you built the Checker Framework under" + " Java 11 but are running it under Java 8."); } } List missingAbsoluteFilenames = CollectionsPlume.mapList(File::getAbsolutePath, missingFiles); throw new RuntimeException( "The following files could not be located: " + String.join(", ", missingAbsoluteFilenames)); } } private static String quote(String str) { if (str.contains(" ")) { if (str.contains("\"")) { throw new BugInCF( "Don't know how to quote a string containing a double-quote character " + str); } return "\"" + str + "\""; } return str; } /////////////////////////////////////////////////////////////////////////// /// Shorthand checker names /// /** Processor shorthand is enabled for processors in this directory in checker.jar. */ protected static final String CHECKER_BASE_DIR_NAME = "org/checkerframework/checker/"; /** Processor shorthand is enabled for processors in this directory in checker.jar. */ protected static final String COMMON_BASE_DIR_NAME = "org/checkerframework/common/"; /** * 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 * @return true if the fully-qualified version of {@code processorString} is in {@code * fullyQualifiedCheckerNames} */ public static boolean matchesCheckerOrSubcheckerFromList( String processorString, List<@FullyQualifiedName String> 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(List args) { for (int i = 0; i < args.size(); i++) { int nextIndex = i + 1; if (args.size() > nextIndex) { if (args.get(i).equals("-processor")) { 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. * * @return fully qualified names of the checkers found in checker.jar */ private List<@FullyQualifiedName String> getAllCheckerClassNames() { ArrayList<@FullyQualifiedName String> checkerClassNames = new ArrayList<>(); try (FileInputStream fis = new FileInputStream(checkerJar); JarInputStream checkerJarIs = new JarInputStream(fis)) { ZipEntry entry; while ((entry = checkerJarIs.getNextEntry()) != null) { String name = entry.getName(); // Checkers ending in "Subchecker" are not included in this list used by // CheckerMain. if ((name.startsWith(CHECKER_BASE_DIR_NAME) || name.startsWith(COMMON_BASE_DIR_NAME)) && name.endsWith("Checker.class")) { // Forward slash is used instead of File.separator because checker.jar uses / as // the separator. @SuppressWarnings("signature") // string manipulation @FullyQualifiedName String fqName = String.join( ".", name.substring(0, name.length() - ".class".length()) .split("/")); checkerClassNames.add(fqName); } } } catch (IOException e) { // Issue a warning instead of aborting execution. System.err.printf( "Could not read %s. Shorthand processor names will not work.%n", checkerJar); } 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; often just one * processor * @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( String processorsString, List<@FullyQualifiedName String> fullyQualifiedCheckerNames, boolean allowSubcheckers) { StringJoiner result = new StringJoiner(","); for (String processor : SystemUtil.COMMA_SPLITTER.split(processorsString)) { if (!processor.contains(".")) { // Not already fully qualified processor = unshorthandProcessorName( processor, fullyQualifiedCheckerNames, allowSubcheckers); } result.add(processor); } return result.toString(); } /** * 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. * * @param processorName a processor name, possibly in shorthand * @param fullyQualifiedCheckerNames all checker names * @param allowSubcheckers whether to match subcheckers as well as checkers * @return the fully-qualified version of {@code processorName} in {@code * fullyQualifiedCheckerNames}, or else {@code processorName} itself */ private static String unshorthandProcessorName( String processorName, List<@FullyQualifiedName String> fullyQualifiedCheckerNames, boolean allowSubcheckers) { for (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 (processorName.equalsIgnoreCase(checkerName) || processorName.equalsIgnoreCase(checkerNameShort)) { return name; } } } return processorName; // 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. * * @param processorName a string identifying one processor * @param fullyQualifiedCheckerNames a list of fully-qualified checker names to match * processorName against * @param allowSubcheckers whether to match against fully qualified checker names ending with * "Subchecker" * @return true if the shorthand processor name can be expanded to a checker in {@code * fullyQualifiedCheckerNames} */ public static boolean matchesFullyQualifiedProcessor( String processorName, List<@FullyQualifiedName String> fullyQualifiedCheckerNames, boolean allowSubcheckers) { return !processorName.equals( unshorthandProcessorName( processorName, fullyQualifiedCheckerNames, allowSubcheckers)); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy