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

org.checkerframework.framework.stub.RemoveAnnotationsForInference 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.44.0
Show newest version
package org.checkerframework.framework.stub;

import com.github.javaparser.ParseResult;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.Position;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.ArrayInitializerExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MarkerAnnotationExpr;
import com.github.javaparser.ast.expr.MemberValuePair;
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.ast.nodeTypes.NodeWithAnnotations;
import com.github.javaparser.ast.visitor.GenericListVisitorAdapter;
import com.github.javaparser.utils.CollectionStrategy;
import com.github.javaparser.utils.ParserCollectionStrategy;
import com.github.javaparser.utils.PositionUtils;
import com.github.javaparser.utils.ProjectRoot;
import com.github.javaparser.utils.SourceRoot;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.reflect.ClassPath;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import org.checkerframework.javacutil.BugInCF;
import org.plumelib.util.CollectionsPlume;

/**
 * Process Java source files to remove annotations that ought to be inferred.
 *
 * 

Removes annotations from all files in the given directories. Modifies the files in place. * *

Does not remove trusted annotations: those that the checker trusts rather than verifies. * *

Does not remove annotations at locations where inference does no work: * *

    *
  • within the scope of a relevant @SuppressWarnings *
  • within the scope of @IgnoreInWholeProgramInference or an annotation meta-annotated with * that, such as @Option *
* * After removing annotations, javac may issue "warning: [cast] redundant cast to ..." if {@code * -Alint:cast} (or {@code -Alint:all} which implies it) is passed to javac. You can suppress the * warning by passing {@code -Alint:-cast} to javac. */ public class RemoveAnnotationsForInference { /** * Processes each provided command-line argument; see {@link RemoveAnnotationsForInference class * documentation} for details. * * @param args command-line arguments: directories to process */ public static void main(String[] args) { if (args.length < 1) { System.err.println("Usage: provide one or more directory names to process"); System.exit(1); } for (String arg : args) { process(arg); } } /** * Maps from simple names to fully-qualified names of annotations. (Actually, it includes every * class on the classpath.) */ static Multimap simpleToFullyQualified = ArrayListMultimap.create(); static { try { ClassPath cp = ClassPath.from(RemoveAnnotationsForInference.class.getClassLoader()); for (ClassPath.ClassInfo ci : cp.getTopLevelClasses()) { // There is no way to determine whether `ci` represents an annotation, without loading it. // I could filter using a heuristic: only include classes in a package named "qual". simpleToFullyQualified.put(ci.getSimpleName(), ci.getName()); } } catch (IOException e) { throw new BugInCF(e); } } /** * Process each file in the given directory; see the {@link RemoveAnnotationsForInference class * documentation} for details. * * @param dir directory to process */ private static void process(String dir) { Path root = JavaStubifier.dirnameToPath(dir); RemoveAnnotationsCallback rac = new RemoveAnnotationsCallback(); CollectionStrategy strategy = new ParserCollectionStrategy(); // Required to include directories that contain a module-info.java, which don't parse by // default. strategy.getParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_11); ProjectRoot projectRoot = strategy.collect(root); for (SourceRoot sourceRoot : projectRoot.getSourceRoots()) { try { sourceRoot.parse("", rac); } catch (IOException e) { throw new BugInCF(e); } } } /** * Callback to process each Java file; see the {@link RemoveAnnotationsForInference class * documentation} for details. */ private static class RemoveAnnotationsCallback implements SourceRoot.Callback { /** The visitor instance. */ private final RemoveAnnotationsVisitor rav = new RemoveAnnotationsVisitor(); @Override public Result process(Path localPath, Path absolutePath, ParseResult result) { Optional opt = result.getResult(); if (opt.isPresent()) { CompilationUnit cu = opt.get(); List removals = rav.visit(cu, null); removeAnnotations(absolutePath, removals); } return Result.DONT_SAVE; } } // An earlier implementation used ModifierVisitor. However, JavaParser's unparser can change // the structure of the program. For example, it changes `protected @Nullable Object x;` to // `@Nullable protected Object x;` which yields a type.anno.before.modifier error. /** * Rewrites the file in place, removing the given annotations from it. * * @param absolutePath the path to the file * @param removals the annotations to remove */ static void removeAnnotations(Path absolutePath, List removals) { if (removals.isEmpty()) { return; } List lines; try { lines = Files.readAllLines(absolutePath); } catch (IOException e) { System.out.printf("Problem reading %s: %s%n", absolutePath, e.getMessage()); System.exit(1); throw new Error("unreachable"); } PositionUtils.sortByBeginPosition(removals); Collections.reverse(removals); // This code (correctly) assumes that no element of `removals` is contained within another. for (AnnotationExpr removal : removals) { Position begin = removal.getBegin().get(); Position end = removal.getEnd().get(); int beginLine = begin.line - 1; int beginColumn = begin.column - 1; int endLine = end.line - 1; int endColumn = end.column; // a JavaParser range is inclusive of the character at "end" if (beginLine == endLine) { String line = lines.get(beginLine); String prefix = line.substring(0, beginColumn); String suffix = line.substring(endColumn); // Remove whitespace to beautify formatting. suffix = CharMatcher.whitespace().trimLeadingFrom(suffix); if (suffix.startsWith("[")) { prefix = CharMatcher.whitespace().trimTrailingFrom(prefix); } String newLine = prefix + suffix; replaceLine(lines, beginLine, newLine); } else { String newLastLine = lines.get(endLine).substring(0, endColumn); replaceLine(lines, endLine, newLastLine); for (int lineno = endLine - 1; lineno > beginLine; lineno--) { lines.remove(lineno); } String newFirstLine = lines.get(beginLine).substring(0, beginColumn); replaceLine(lines, beginLine, newFirstLine); } } try { PrintWriter pw = new PrintWriter(absolutePath.toString()); for (String line : lines) { pw.println(line); } pw.close(); } catch (IOException e) { throw new Error(e); } } /** * If {@code newLine} is blank, removes the given line. Otherwise replaces the given line. * * @param lines the list in which to do replacement or removal * @param lineno the index of the line to be removed or replaced * @param newLine the new line for index {@code lineno} */ static void replaceLine(List lines, int lineno, String newLine) { if (isBlank(newLine)) { lines.remove(lineno); } else { lines.set(lineno, newLine); } } // TODO: Put the following utility methods in StringsPlume. /** * Returns true if the string contains only white space codepoints, otherwise false. * *

In Java 11, use {@code String.isBlank()} instead. * * @param s a string * @return true if the string contains only white space codepoints, otherwise false */ static boolean isBlank(String s) { return s.chars().allMatch(Character::isWhitespace); } /** * Visits one compilation unit, collecting the annotations that should be removed. See the {@link * RemoveAnnotationsForInference class documentation} for more details. * *

The annotations will be removed from the source code by the {@link #removeAnnotations} * method. */ private static class RemoveAnnotationsVisitor extends GenericListVisitorAdapter { /** * Returns the argument if it should be removed from source code. * * @param n an annotation * @param superResult the result of processing the subcomponents of n * @return the argument to remove it, or superResult to retain it */ List processAnnotation(AnnotationExpr n, List superResult) { if (n == null) { return superResult; } String name = n.getNameAsString(); // Retain annotations defined in the JDK. if (isJdkAnnotation(name)) { return superResult; } // Retain trusted annotations. if (isTrustedAnnotation(name)) { return superResult; } // Retain annotations for which warnings are suppressed. if (isSuppressed(n)) { return superResult; } // The default behavior is to remove the annotation. // Don't include superResult, which is contained within `n`. return Collections.singletonList(n); } // There are three JavaParser AST nodes that represent annotations @Override public List visit(final MarkerAnnotationExpr n, final Void arg) { return processAnnotation(n, super.visit(n, arg)); } @Override public List visit(final NormalAnnotationExpr n, final Void arg) { return processAnnotation(n, super.visit(n, arg)); } @Override public List visit(final SingleMemberAnnotationExpr n, final Void arg) { return processAnnotation(n, super.visit(n, arg)); } } /** * Returns true if the given annotation is defined in the JDK. * * @param name the annotation's name (simple or fully-qualified) * @return true if the given annotation is defined in the JDK */ static boolean isJdkAnnotation(String name) { return name.equals("Serial") || name.equals("java.io.Serial") || name.equals("Deprecated") || name.equals("java.lang.Deprecated") || name.equals("FunctionalInterface") || name.equals("java.lang.FunctionalInterface") || name.equals("Override") || name.equals("java.lang.Override") || name.equals("SafeVarargs") || name.equals("java.lang.SafeVarargs") || name.equals("Documented") || name.equals("java.lang.annotation.Documented") || name.equals("Inherited") || name.equals("java.lang.annotation.Inherited") || name.equals("Native") || name.equals("java.lang.annotation.Native") || name.equals("Repeatable") || name.equals("java.lang.annotation.Repeatable") || name.equals("Retention") || name.equals("java.lang.annotation.Retention") || name.equals("SuppressWarnings") || name.equals("java.lang.SuppressWarnings") || name.equals("Target") || name.equals("java.lang.annotation.Target"); } /** * Returns true if the given annotation is trusted, not checked/verified. * * @param name the annotation's name (simple or fully-qualified) * @return true if the given annotation is trusted, not verified */ static boolean isTrustedAnnotation(String name) { // This list was determined by grepping for "trusted" in `qual` directories. return name.equals("Untainted") || name.equals("org.checkerframework.checker.tainting.qual.Untainted") || name.equals("InternedDistinct") || name.equals("org.checkerframework.checker.interning.qual.InternedDistinct") || name.equals("ReturnsReceiver") || name.equals("org.checkerframework.checker.builder.qual.ReturnsReceiver") || name.equals("TerminatesExecution") || name.equals("org.checkerframework.dataflow.qual.TerminatesExecution") || name.equals("Covariant") || name.equals("org.checkerframework.framework.qual.Covariant") || name.equals("NonLeaked") || name.equals("org.checkerframework.common.aliasing.qual.NonLeaked") || name.equals("LeakedToResult") || name.equals("org.checkerframework.common.aliasing.qual.LeakedToResult"); } // This approach searches upward to find all the active warning suppressions. // An alternative, more efficient approach would be to track the current set of warning // suppressions, using a stack. // There are two problems with the alternative approach (and besides, this approach is fast // enough as it is). // 1. JavaParser sometimes visits members before the annotation, so there was not a chance to // observe the annotation and place it on the suppression stack. This should be fixed for // ModifierVisitor (but not for other visitors such as GenericListVisitorAdapter) in // JavaParser release 3.19.0. // 2. A user might write an annotation before @SuppressWarnings, as in: // @Interned @SuppressWarnings("interning") // The {@code @Interned} annotation is visited before the {@code @SuppressWarnings} // annotation is. This could be addressed by searching just the parent's annotations. /** * Returns true if warnings about the given annotation are suppressed. * *

Its heuristic is to look for a {@code @SuppressWarnings} annotation on a containing program * element, whose string is one of the elements of the annotation's fully-qualified name. * * @param arg an annotation * @return true if warnings about the given annotation are suppressed */ private static boolean isSuppressed(AnnotationExpr arg) { String name = arg.getNameAsString(); // If it's a simple name for which we know a fully-qualified name, // try all fully-qualified names that it could expand to. Collection names; if (simpleToFullyQualified.containsKey(name)) { names = simpleToFullyQualified.get(name); } else { names = Collections.singletonList(name); } Iterator itor = new Node.ParentsVisitor(arg); while (itor.hasNext()) { Node n = itor.next(); if (n instanceof NodeWithAnnotations) { for (AnnotationExpr ae : ((NodeWithAnnotations) n).getAnnotations()) { if (suppresses(ae, names)) { return true; } } } } return false; } /** * Returns true if {@code suppressor} suppresses warnings regarding {@code suppressees}. * * @param suppressor an annotation that might be {@code @SuppressWarnings} or like it * @param suppressees an annotation for which warnings might be suppressed. This is actually a * list: if the annotation was written unqualified, it contains all the fully-qualified names * that the unqualified annotation might stand for. * @return true if {@code suppressor} suppresses warnings regarding {@code suppressees} */ static boolean suppresses(AnnotationExpr suppressor, Collection suppressees) { List suppressWarningsStrings = suppressWarningsStrings(suppressor); if (suppressWarningsStrings == null) { return false; } List checkerNames = CollectionsPlume.mapList( RemoveAnnotationsForInference::checkerName, suppressWarningsStrings); // "allcheckers" suppresses all warnings. if (checkerNames.contains("allcheckers")) { return true; } // Try every element of suppressee's fully-qualified name. for (String suppressee : suppressees) { for (String fqPart : suppressee.split("\\.")) { if (checkerNames.contains(fqPart)) { return true; } } } return false; } /** * Given a @SuppressWarnings annotation, returns its strings. Given an annotation that suppresses * warnings, returns strings for what it suppresses. Otherwise, returns null. * * @param n an annotation * @return the (effective) arguments to {@code @SuppressWarnings}, or null */ private static List suppressWarningsStrings(AnnotationExpr n) { String name = n.getNameAsString(); if (name.equals("SuppressWarnings") || name.equals("java.lang.SuppressWarnings")) { if (n instanceof MarkerAnnotationExpr) { return Collections.emptyList(); } else if (n instanceof NormalAnnotationExpr) { NodeList pairs = ((NormalAnnotationExpr) n).getPairs(); assert pairs.size() == 1; MemberValuePair pair = pairs.get(0); assert pair.getName().asString().equals("value"); return annotationElementStrings(pair.getValue()); } else if (n instanceof SingleMemberAnnotationExpr) { return annotationElementStrings(((SingleMemberAnnotationExpr) n).getMemberValue()); } else { throw new BugInCF("Unexpected AnnotationExpr of type %s: %s", n.getClass(), n); } } if (name.equals("IgnoreInWholeProgramInference") || name.equals("org.checkerframework.framework.qual.IgnoreInWholeProgramInference") || name.equals("Inject") || name.equals("javax.inject.Inject") || name.equals("Singleton") || name.equals("javax.inject.Singleton") || name.equals("Option") || name.equals("org.plumelib.options.Option")) { return Collections.singletonList("allcheckers"); } return null; } /** * Given an annotation argument for an element of type String[], return a list of strings. * * @param e an annotation argument * @return the strings expressed by {@code e} */ private static List annotationElementStrings(Expression e) { if (e instanceof StringLiteralExpr) { return Collections.singletonList(((StringLiteralExpr) e).asString()); } else if (e instanceof ArrayInitializerExpr) { NodeList values = ((ArrayInitializerExpr) e).getValues(); List result = new ArrayList<>(values.size()); for (Expression v : values) { if (v instanceof StringLiteralExpr) { result.add(((StringLiteralExpr) v).asString()); } else { throw new BugInCF("Unexpected annotation element of type %s: %s", v.getClass(), v); } } return result; } else { throw new BugInCF("Unexpected %s: %s", e.getClass(), e); } } /** * Returns the "checker name" part of a SuppressWarnings string: the part before the colon, or the * whole thing if it contains no colon. * * @param s a SuppressWarnings string: the argument to {@code @SuppressWarnings} * @return the part of s before the colon, or the whole thing if it contains no colon */ private static String checkerName(String s) { int colonPos = s.indexOf(":"); if (colonPos == -1) { return s; } else { return s.substring(colonPos + 1); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy