/*
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.errorprone.fixes;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.getLast;
import static com.google.errorprone.util.ASTHelpers.getAnnotation;
import static com.google.errorprone.util.ASTHelpers.getAnnotationWithSimpleName;
import static com.google.errorprone.util.ASTHelpers.getModifiers;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.sun.source.tree.Tree.Kind.ASSIGNMENT;
import static com.sun.source.tree.Tree.Kind.NEW_ARRAY;
import static com.sun.source.tree.Tree.Kind.PARENTHESIZED;
import static com.sun.tools.javac.code.TypeTag.CLASS;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.CharStreams;
import com.google.errorprone.VisitorState;
import com.google.errorprone.apply.DescriptionBasedDiff;
import com.google.errorprone.apply.ImportOrganizer;
import com.google.errorprone.apply.SourceFile;
import com.google.errorprone.fixes.SuggestedFix.Builder;
import com.google.errorprone.util.ASTHelpers;
import com.google.errorprone.util.ErrorProneToken;
import com.google.errorprone.util.ErrorProneTokens;
import com.google.errorprone.util.FindIdentifiers;
import com.sun.source.doctree.DocTree;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.DocTreePath;
import com.sun.source.util.JavacTask;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.api.JavacTaskImpl;
import com.sun.tools.javac.api.JavacTool;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Kinds.KindSelector;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.main.Arguments;
import com.sun.tools.javac.parser.Tokens;
import com.sun.tools.javac.parser.Tokens.TokenKind;
import com.sun.tools.javac.tree.DCTree;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.tree.TreeScanner;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Options;
import com.sun.tools.javac.util.Position;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.SimpleTypeVisitor8;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.SimpleJavaFileObject;
/** Factories for constructing {@link Fix}es. */
public class SuggestedFixes {
/** Parse a modifier token into a {@link Modifier}. */
@Nullable
private static Modifier getTokModifierKind(ErrorProneToken tok) {
switch (tok.kind()) {
case PUBLIC:
return Modifier.PUBLIC;
case PROTECTED:
return Modifier.PROTECTED;
case PRIVATE:
return Modifier.PRIVATE;
case ABSTRACT:
return Modifier.ABSTRACT;
case STATIC:
return Modifier.STATIC;
case FINAL:
return Modifier.FINAL;
case TRANSIENT:
return Modifier.TRANSIENT;
case VOLATILE:
return Modifier.VOLATILE;
case SYNCHRONIZED:
return Modifier.SYNCHRONIZED;
case NATIVE:
return Modifier.NATIVE;
case STRICTFP:
return Modifier.STRICTFP;
case DEFAULT:
return Modifier.DEFAULT;
default:
return null;
}
}
/** Add modifiers to the given class, method, or field declaration. */
@Nullable
public static SuggestedFix addModifiers(Tree tree, VisitorState state, Modifier... modifiers) {
ModifiersTree originalModifiers = getModifiers(tree);
if (originalModifiers == null) {
return null;
}
Set<Modifier> toAdd =
Sets.difference(new TreeSet<>(Arrays.asList(modifiers)), originalModifiers.getFlags());
if (originalModifiers.getFlags().isEmpty()) {
int pos =
state.getEndPosition(originalModifiers) != Position.NOPOS
? state.getEndPosition(originalModifiers) + 1
: ((JCTree) tree).getStartPosition();
int base = ((JCTree) tree).getStartPosition();
java.util.Optional<Integer> insert =
state
.getTokensForNode(tree)
.stream()
.map(token -> token.pos() + base)
.filter(thisPos -> thisPos >= pos)
.findFirst();
int insertPos = insert.orElse(pos); // shouldn't ever be able to get to the else
return SuggestedFix.replace(insertPos, insertPos, Joiner.on(' ').join(toAdd) + " ");
}
// a map from modifiers to modifier position (or -1 if the modifier is being added)
// modifiers are sorted in Google Java Style order
Map<Modifier, Integer> modifierPositions = new TreeMap<>();
for (Modifier mod : toAdd) {
modifierPositions.put(mod, -1);
}
List<ErrorProneToken> tokens = state.getTokensForNode(originalModifiers);
int base = ((JCTree) originalModifiers).getStartPosition();
for (ErrorProneToken tok : tokens) {
Modifier mod = getTokModifierKind(tok);
if (mod != null) {
modifierPositions.put(mod, base + tok.pos());
}
}
SuggestedFix.Builder fix = SuggestedFix.builder();
// walk the map of all modifiers, and accumulate a list of new modifiers to insert
// beside an existing modifier
List<Modifier> modifiersToWrite = new ArrayList<>();
for (Modifier mod : modifierPositions.keySet()) {
int p = modifierPositions.get(mod);
if (p == -1) {
modifiersToWrite.add(mod);
} else if (!modifiersToWrite.isEmpty()) {
fix.replace(p, p, Joiner.on(' ').join(modifiersToWrite) + " ");
modifiersToWrite.clear();
}
}
if (!modifiersToWrite.isEmpty()) {
fix.postfixWith(originalModifiers, " " + Joiner.on(' ').join(modifiersToWrite));
}
return fix.build();
}
/** Remove modifiers from the given class, method, or field declaration. */
@Nullable
public static SuggestedFix removeModifiers(Tree tree, VisitorState state, Modifier... modifiers) {
Set<Modifier> toRemove = ImmutableSet.copyOf(modifiers);
ModifiersTree originalModifiers = getModifiers(tree);
if (originalModifiers == null) {
return null;
}
SuggestedFix.Builder fix = SuggestedFix.builder();
List<ErrorProneToken> tokens = state.getTokensForNode(originalModifiers);
int basePos = ((JCTree) originalModifiers).getStartPosition();
boolean empty = true;
for (ErrorProneToken tok : tokens) {
Modifier mod = getTokModifierKind(tok);
if (toRemove.contains(mod)) {
empty = false;
fix.replace(basePos + tok.pos(), basePos + tok.endPos() + 1, "");
break;
}
}
if (empty) {
return null;
}
return fix.build();
}
/**
* Returns a human-friendly name of the given {@link Symbol.TypeSymbol} for use in fixes.
*
* <ul>
* <li>If the type is already imported, its simple name is used.
* <li>If an enclosing type is imported, that enclosing type is used as a qualified.
* <li>Otherwise the outermost enclosing type is imported and used as a qualifier.
* </ul>
*/
public static String qualifyType(VisitorState state, SuggestedFix.Builder fix, Symbol sym) {
if (sym.getKind() == ElementKind.TYPE_PARAMETER) {
return sym.getSimpleName().toString();
}
Deque<String> names = new ArrayDeque<>();
for (Symbol curr = sym; curr != null; curr = curr.owner) {
names.addFirst(curr.getSimpleName().toString());
Symbol found =
FindIdentifiers.findIdent(curr.getSimpleName().toString(), state, KindSelector.VAL_TYP);
if (found == curr) {
break;
}
if (curr.owner != null && curr.owner.getKind() == ElementKind.PACKAGE) {
if (importClash(state, curr)) {
names.addFirst(curr.owner.getQualifiedName().toString());
break;
} else {
fix.addImport(curr.getQualifiedName().toString());
break;
}
}
}
return Joiner.on('.').join(names);
}
private static boolean importClash(VisitorState state, Symbol sym) {
for (ImportTree importTree : state.getPath().getCompilationUnit().getImports()) {
if (((MemberSelectTree) importTree.getQualifiedIdentifier())
.getIdentifier()
.contentEquals(sym.getSimpleName())
&& !sym.equals(ASTHelpers.getSymbol(importTree.getQualifiedIdentifier()))) {
return true;
}
}
return false;
}
/** Returns a human-friendly name of the given type for use in fixes. */
public static String qualifyType(VisitorState state, SuggestedFix.Builder fix, TypeMirror type) {
return type.accept(
new SimpleTypeVisitor8<String, SuggestedFix.Builder>() {
@Override
protected String defaultAction(TypeMirror e, Builder builder) {
return e.toString();
}
@Override
public String visitArray(ArrayType t, Builder builder) {
return t.getComponentType().accept(this, builder) + "[]";
}
@Override
public String visitDeclared(DeclaredType t, Builder builder) {
String baseType = qualifyType(state, builder, ((Type) t).tsym);
if (t.getTypeArguments().isEmpty()) {
return baseType;
}
StringBuilder b = new StringBuilder(baseType);
b.append('<');
boolean started = false;
for (TypeMirror arg : t.getTypeArguments()) {
if (started) {
b.append(',');
}
b.append(arg.accept(this, builder));
started = true;
}
b.append('>');
return b.toString();
}
},
fix);
}
/** Replaces the leaf doctree in the given path with {@code replacement}. */
public static void replaceDocTree(
SuggestedFix.Builder fix, DocTreePath docPath, String replacement) {
DocTree leaf = docPath.getLeaf();
checkArgument(
leaf instanceof DCTree.DCEndPosTree, "no end position information for %s", leaf.getKind());
DCTree.DCEndPosTree<?> node = (DCTree.DCEndPosTree<?>) leaf;
DCTree.DCDocComment comment = (DCTree.DCDocComment) docPath.getDocComment();
fix.replace((int) node.getSourcePosition(comment), node.getEndPos(comment), replacement);
}
/**
* Fully qualifies a javadoc reference, e.g. for replacing {@code {@link List}} with {@code {@link
* java.util.List}}
*
* @param fix the fix builder to add to
* @param docPath the path to a {@link DCTree.DCReference} element
*/
public static void qualifyDocReference(
SuggestedFix.Builder fix, DocTreePath docPath, VisitorState state) {
DocTree leaf = docPath.getLeaf();
checkArgument(
leaf.getKind() == DocTree.Kind.REFERENCE,
"expected a path to a reference, got %s instead",
leaf.getKind());
DCTree.DCReference reference = (DCTree.DCReference) leaf;
Symbol sym = (Symbol) JavacTrees.instance(state.context).getElement(docPath);
if (sym == null) {
return;
}
String refString = reference.toString();
String qualifiedName;
int idx = refString.indexOf('#');
if (idx >= 0) {
qualifiedName = sym.owner.getQualifiedName() + refString.substring(idx, refString.length());
} else {
qualifiedName = sym.getQualifiedName().toString();
}
replaceDocTree(fix, docPath, qualifiedName);
}
/**
* Returns a {@link Fix} that adds members defined by {@code firstMember} (and optionally {@code
* otherMembers}) to the end of the class referenced by {@code classTree}. This method should only
* be called once per {@link ClassTree} as the suggestions will otherwise collide.
*/
public static Fix addMembers(
ClassTree classTree, VisitorState state, String firstMember, String... otherMembers) {
checkNotNull(classTree);
int classEndPosition = state.getEndPosition(classTree);
StringBuilder stringBuilder = new StringBuilder();
for (String memberSnippet : Lists.asList(firstMember, otherMembers)) {
stringBuilder.append("\n\n").append(memberSnippet);
}
stringBuilder.append('\n');
return SuggestedFix.replace(
classEndPosition - 1, classEndPosition - 1, stringBuilder.toString());
}
/**
* Renames the given {@link VariableTree} and its usages in the current compilation unit to {@code
* replacement}.
*/
public static SuggestedFix renameVariable(
VariableTree tree, final String replacement, VisitorState state) {
String name = tree.getName().toString();
int typeLength = state.getSourceForNode(tree.getType()).length();
int pos =
((JCTree) tree).getStartPosition() + state.getSourceForNode(tree).indexOf(name, typeLength);
final SuggestedFix.Builder fix =
SuggestedFix.builder().replace(pos, pos + name.length(), replacement);
final Symbol.VarSymbol sym = getSymbol(tree);
((JCTree) state.getPath().getCompilationUnit())
.accept(
new TreeScanner() {
@Override
public void visitIdent(JCTree.JCIdent tree) {
if (sym.equals(getSymbol(tree))) {
fix.replace(tree, replacement);
}
}
});
return fix.build();
}
/** Be warned, only changes method name at the declaration. */
public static SuggestedFix renameMethod(
MethodTree tree, final String replacement, VisitorState state) {
// Search tokens from beginning of method tree to beginning of method body.
int basePos = ((JCTree) tree).getStartPosition();
int endPos =
tree.getBody() != null
? ((JCTree) tree.getBody()).getStartPosition()
: state.getEndPosition(tree);
List<ErrorProneToken> methodTokens =
ErrorProneTokens.getTokens(
state.getSourceCode().subSequence(basePos, endPos).toString(), state.context);
for (ErrorProneToken token : methodTokens) {
if (token.kind() == TokenKind.IDENTIFIER && token.name().equals(tree.getName())) {
int nameStartPosition = basePos + token.pos();
int nameEndPosition = basePos + token.endPos();
return SuggestedFix.builder()
.replace(nameStartPosition, nameEndPosition, replacement)
.build();
}
}
// Method name not found.
throw new AssertionError();
}
/** Deletes the given exceptions from a method's throws clause. */
public static Fix deleteExceptions(
MethodTree tree, final VisitorState state, List<ExpressionTree> toDelete) {
List<? extends ExpressionTree> trees = tree.getThrows();
if (toDelete.size() == trees.size()) {
return SuggestedFix.replace(
getThrowsPosition(tree, state) - 1, state.getEndPosition(getLast(trees)), "");
}
String replacement =
FluentIterable.from(tree.getThrows())
.filter(Predicates.not(Predicates.in(toDelete)))
.transform(
new Function<ExpressionTree, String>() {
@Override
@Nullable
public String apply(ExpressionTree input) {
return state.getSourceForNode(input);
}
})
.join(Joiner.on(", "));
return SuggestedFix.replace(
((JCTree) tree.getThrows().get(0)).getStartPosition(),
state.getEndPosition(getLast(tree.getThrows())),
replacement);
}
private static int getThrowsPosition(MethodTree tree, VisitorState state) {
for (ErrorProneToken token : state.getTokensForNode(tree)) {
if (token.kind() == Tokens.TokenKind.THROWS) {
return ((JCTree) tree).getStartPosition() + token.pos();
}
}
throw new AssertionError();
}
/**
* Returns a fix that adds a {@code @SuppressWarnings(warningToSuppress)} to the closest
* suppressible element to the node pointed at by {@code state.getPath()}.
*
* <p>If the closest suppressible element already has a @SuppressWarning annotation,
* warningToSuppress will be added to the value in {@code @SuppressWarnings} instead.
*
* <p>In the event that a suppressible element couldn't be found (e.g.: the state is pointing at a
* CompilationUnit, or some other internal inconsistency has occurred), or the enclosing
* suppressible element already has a {@code @SuppressWarnings} annotation with {@code
* warningToSuppress}, this method will return null.
*/
@Nullable
public static Fix addSuppressWarnings(VisitorState state, String warningToSuppress) {
Builder fixBuilder = SuggestedFix.builder();
addSuppressWarnings(fixBuilder, state, warningToSuppress);
return fixBuilder.isEmpty() ? null : fixBuilder.build();
}
/**
* Modifies {@code fixBuilder} to either create a new {@code @SuppressWarnings} element on the
* closest suppressible node, or add {@code warningToSuppress} to that node if there's already a
* {@code SuppressWarnings} annotation there.
*
* @see #addSuppressWarnings(VisitorState, String)
*/
public static void addSuppressWarnings(
Builder fixBuilder, VisitorState state, String warningToSuppress) {
// Find the nearest tree to add @SuppressWarnings to.
Tree suppressibleNode = suppressibleNode(state.getPath());
if (suppressibleNode == null) {
return;
}
SuppressWarnings existingAnnotation = getAnnotation(suppressibleNode, SuppressWarnings.class);
// If we have an existing @SuppressWarnings on the element, extend its value
if (existingAnnotation != null) {
// Add warning to the existing annotation
String[] values = existingAnnotation.value();
if (Arrays.asList(values).contains(warningToSuppress)) {
// The nearest suppress warnings already contains this thing, so we can't add another thing
return;
}
AnnotationTree suppressAnnotationTree =
getAnnotationWithSimpleName(
findAnnotationsTree(suppressibleNode), SuppressWarnings.class.getSimpleName());
if (suppressAnnotationTree == null) {
// This is weird, bail out
return;
}
fixBuilder.merge(
addValuesToAnnotationArgument(
suppressAnnotationTree,
"value",
ImmutableList.of(state.getTreeMaker().Literal(CLASS, warningToSuppress).toString()),
state));
} else {
// Otherwise, add a suppress annotation to the element
fixBuilder.prefixWith(suppressibleNode, "@SuppressWarnings(\"" + warningToSuppress + "\")\n");
}
}
private static List<? extends AnnotationTree> findAnnotationsTree(Tree tree) {
ModifiersTree maybeModifiers = getModifiers(tree);
return maybeModifiers == null ? ImmutableList.of() : maybeModifiers.getAnnotations();
}
@Nullable
private static Tree suppressibleNode(TreePath path) {
return StreamSupport.stream(path.spliterator(), false)
.filter(
tree ->
tree instanceof MethodTree
|| tree instanceof ClassTree
|| tree instanceof VariableTree)
.findFirst()
.orElse(null);
}
/**
* Returns a fix that appends {@code newValues} to the {@code parameterName} argument for {@code
* annotation}, regardless of whether there is already an argument.
*
* <p>N.B.: {@code newValues} are source-code strings, not string literal values.
*/
public static Builder addValuesToAnnotationArgument(
AnnotationTree annotation,
String parameterName,
Collection<String> newValues,
VisitorState state) {
if (annotation.getArguments().isEmpty()) {
String parameterPrefix = parameterName.equals("value") ? "" : (parameterName + " = ");
return SuggestedFix.builder()
.replace(
annotation,
annotation
.toString()
.replaceFirst("\\(\\)", "(" + parameterPrefix + newArgument(newValues) + ")"));
}
Optional<ExpressionTree> maybeExistingArgument = findArgument(annotation, parameterName);
if (!maybeExistingArgument.isPresent()) {
return SuggestedFix.builder()
.prefixWith(
annotation.getArguments().get(0),
parameterName + " = " + newArgument(newValues) + ", ");
}
ExpressionTree existingArgument = maybeExistingArgument.get();
if (!existingArgument.getKind().equals(NEW_ARRAY)) {
return SuggestedFix.builder()
.replace(
existingArgument, newArgument(state.getSourceForNode(existingArgument), newValues));
}
NewArrayTree newArray = (NewArrayTree) existingArgument;
if (newArray.getInitializers().isEmpty()) {
return SuggestedFix.builder().replace(newArray, newArgument(newValues));
} else {
return SuggestedFix.builder()
.postfixWith(getLast(newArray.getInitializers()), ", " + Joiner.on(", ").join(newValues));
}
}
private static String newArgument(String existingParameters, Collection<String> initializers) {
return newArgument(
new ImmutableList.Builder<String>().add(existingParameters).addAll(initializers).build());
}
private static String newArgument(Collection<String> initializers) {
StringBuilder expression = new StringBuilder();
if (initializers.size() > 1) {
expression.append('{');
}
Joiner.on(", ").appendTo(expression, initializers);
if (initializers.size() > 1) {
expression.append('}');
}
return expression.toString();
}
private static Optional<ExpressionTree> findArgument(
AnnotationTree annotation, String parameter) {
for (ExpressionTree argument : annotation.getArguments()) {
if (argument.getKind().equals(ASSIGNMENT)) {
AssignmentTree assignment = (AssignmentTree) argument;
if (assignment.getVariable().toString().equals(parameter)) {
ExpressionTree expression = assignment.getExpression();
while (expression.getKind().equals(PARENTHESIZED)) {
expression = ((ParenthesizedTree) expression).getExpression();
}
return Optional.of(expression);
}
}
}
return Optional.absent();
}
/**
* Returns true if the current compilation would succeed with the given fix applied. Note that
* calling this method is very expensive as it requires rerunning the entire compile, so it should
* be used with restraint.
*/
public static boolean compilesWithFix(Fix fix, VisitorState state) {
if (fix.isEmpty()) {
return true;
}
JCCompilationUnit compilationUnit = (JCCompilationUnit) state.getPath().getCompilationUnit();
JavaFileObject modifiedFile = compilationUnit.getSourceFile();
JavacTaskImpl javacTask = (JavacTaskImpl) state.context.get(JavacTask.class);
if (javacTask == null) {
throw new IllegalArgumentException("No JavacTask in context.");
}
Arguments arguments = Arguments.instance(javacTask.getContext());
List<JavaFileObject> fileObjects = new ArrayList<>(arguments.getFileObjects());
for (int i = 0; i < fileObjects.size(); i++) {
final JavaFileObject oldFile = fileObjects.get(i);
if (modifiedFile.toUri().equals(oldFile.toUri())) {
DescriptionBasedDiff diff =
DescriptionBasedDiff.create(compilationUnit, ImportOrganizer.STATIC_FIRST_ORGANIZER);
diff.handleFix(fix);
SourceFile fixSource;
try {
fixSource =
new SourceFile(
modifiedFile.getName(),
modifiedFile.getCharContent(false /*ignoreEncodingErrors*/));
} catch (IOException e) {
return false;
}
diff.applyDifferences(fixSource);
fileObjects.set(
i,
new SimpleJavaFileObject(modifiedFile.toUri(), Kind.SOURCE) {
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return fixSource.getAsSequence();
}
});
break;
}
}
DiagnosticCollector<JavaFileObject> diagnosticListener = new DiagnosticCollector<>();
Context context = new Context();
Options.instance(context).putAll(Options.instance(javacTask.getContext()));
context.put(Arguments.class, arguments);
JavacTask newTask =
JavacTool.create()
.getTask(
CharStreams.nullWriter(),
state.context.get(JavaFileManager.class),
diagnosticListener,
ImmutableList.of(),
arguments.getClassNames(),
fileObjects,
context);
try {
newTask.analyze();
} catch (Throwable e) {
e.printStackTrace();
}
return countErrors(diagnosticListener) == 0;
}
private static long countErrors(DiagnosticCollector<JavaFileObject> diagnosticCollector) {
return diagnosticCollector
.getDiagnostics()
.stream()
.filter(d -> d.getKind() == Diagnostic.Kind.ERROR)
.count();
}
}