/*
* 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.bugpatterns;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.errorprone.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.toType;
import static com.google.errorprone.matchers.method.MethodMatchers.constructor;
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher;
import com.google.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.suppliers.Suppliers;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.tree.JCTree;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.List;
import java.util.Scanner;
/** @author cushon@google.com (Liam Miller-Cushon) */
@BugPattern(
name = "DefaultCharset",
category = JDK,
summary =
"Implicit use of the platform default charset, which can result in e.g. non-ASCII"
+ " characters being silently replaced with '?' in many environments",
severity = WARNING
)
public class DefaultCharset extends BugChecker
implements MethodInvocationTreeMatcher, NewClassTreeMatcher {
enum CharsetFix {
UTF_8_FIX("UTF_8") {
@Override
void addImport(SuggestedFix.Builder fix, VisitorState state) {
fix.addStaticImport("java.nio.charset.StandardCharsets.UTF_8");
}
},
DEFAULT_CHARSET_FIX("Charset.defaultCharset()") {
@Override
void addImport(SuggestedFix.Builder fix, VisitorState state) {
fix.addImport("java.nio.charset.Charset");
}
};
final String replacement;
CharsetFix(String replacement) {
this.replacement = replacement;
}
String replacement() {
return replacement;
}
abstract void addImport(SuggestedFix.Builder fix, VisitorState state);
}
// ignore the constructor that takes FileDescriptor; it's rare and there's no automated fix
private static final Matcher<ExpressionTree> FILE_WRITER =
anyOf(
constructor().forClass(FileWriter.class.getName()).withParameters("java.io.File"),
constructor()
.forClass(FileWriter.class.getName())
.withParameters("java.io.File", "boolean"),
constructor().forClass(FileWriter.class.getName()).withParameters("java.lang.String"),
constructor()
.forClass(FileWriter.class.getName())
.withParameters("java.lang.String", "boolean"));
private static final Matcher<Tree> BUFFERED_WRITER =
toType(ExpressionTree.class, constructor().forClass(BufferedWriter.class.getName()));
private static final Matcher<ExpressionTree> FILE_READER =
anyOf(
constructor().forClass(FileReader.class.getName()).withParameters("java.io.File"),
constructor().forClass(FileReader.class.getName()).withParameters("java.lang.String"));
private static final Matcher<Tree> BUFFERED_READER =
toType(ExpressionTree.class, constructor().forClass(BufferedReader.class.getName()));
private static final Matcher<ExpressionTree> CTOR =
anyOf(
constructor()
.forClass(String.class.getName())
.withParameters(ImmutableList.of(Suppliers.arrayOf(Suppliers.BYTE_TYPE))),
constructor()
.forClass(String.class.getName())
.withParameters(
ImmutableList.of(
Suppliers.arrayOf(Suppliers.BYTE_TYPE),
Suppliers.INT_TYPE,
Suppliers.INT_TYPE)),
constructor()
.forClass(OutputStreamWriter.class.getName())
.withParameters(ImmutableList.of(Suppliers.typeFromClass(OutputStream.class))),
constructor()
.forClass(InputStreamReader.class.getName())
.withParameters(ImmutableList.of(Suppliers.typeFromClass(InputStream.class))));
private static final Matcher<ExpressionTree> BYTESTRING_COPY_FROM =
staticMethod().onClass("com.google.protobuf.ByteString").named("copyFrom");
private static final Matcher<ExpressionTree> STRING_GET_BYTES =
instanceMethod().onExactClass(String.class.getName()).withSignature("getBytes()");
private static final Matcher<ExpressionTree> FILE_NEW_WRITER =
staticMethod()
.onClass(com.google.common.io.Files.class.getName())
.named("newWriter")
.withParameters("java.lang.String");
private static final Matcher<ExpressionTree> PRINT_WRITER =
anyOf(
constructor().forClass(PrintWriter.class.getName()).withParameters(File.class.getName()),
constructor()
.forClass(PrintWriter.class.getName())
.withParameters(String.class.getName()));
private static final Matcher<ExpressionTree> PRINT_WRITER_OUTPUTSTREAM =
anyOf(
constructor()
.forClass(PrintWriter.class.getName())
.withParameters(OutputStream.class.getName()),
constructor()
.forClass(PrintWriter.class.getName())
.withParameters(OutputStream.class.getName(), "boolean"));
private static final Matcher<ExpressionTree> SCANNER_MATCHER =
anyOf(
constructor()
.forClass(Scanner.class.getName())
.withParameters(InputStream.class.getName()),
constructor().forClass(Scanner.class.getName()).withParameters(File.class.getName()),
constructor().forClass(Scanner.class.getName()).withParameters(Path.class.getName()),
constructor()
.forClass(Scanner.class.getName())
.withParameters(ReadableByteChannel.class.getName()));
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (state.isAndroidCompatible()) {
return NO_MATCH;
}
if (STRING_GET_BYTES.matches(tree, state)) {
Description.Builder description = buildDescription(tree);
Tree parent = state.getPath().getParentPath().getLeaf();
if (parent instanceof ExpressionTree
&& BYTESTRING_COPY_FROM.matches((ExpressionTree) parent, state)) {
byteStringFixes(description, tree, (ExpressionTree) parent, state);
} else {
appendCharsets(description, tree, tree.getMethodSelect(), tree.getArguments(), state);
}
return description.build();
}
if (FILE_NEW_WRITER.matches(tree, state)) {
Description.Builder description = buildDescription(tree);
appendCharsets(description, tree, tree.getMethodSelect(), tree.getArguments(), state);
return description.build();
}
return NO_MATCH;
}
private static void byteStringFixes(
Description.Builder description,
MethodInvocationTree tree,
ExpressionTree parent,
VisitorState state) {
description.addFix(byteStringFix(tree, parent, state, ".copyFromUtf8(", "").build());
SuggestedFix.Builder builder =
byteStringFix(
tree, parent, state, ".copyFrom(", ", " + CharsetFix.DEFAULT_CHARSET_FIX.replacement());
CharsetFix.DEFAULT_CHARSET_FIX.addImport(builder, state);
description.addFix(builder.build());
}
private static SuggestedFix.Builder byteStringFix(
MethodInvocationTree tree,
ExpressionTree parent,
VisitorState state,
String prefix,
String suffix) {
return SuggestedFix.builder()
.replace(
/*startPos=*/ state.getEndPosition(ASTHelpers.getReceiver(parent)),
/*endPos=*/ ((JCTree) tree).getStartPosition(),
/*replaceWith=*/ prefix)
.replace(
state.getEndPosition(ASTHelpers.getReceiver(tree)), state.getEndPosition(tree), suffix);
}
@Override
public Description matchNewClass(NewClassTree tree, VisitorState state) {
if (state.isAndroidCompatible()) {
return NO_MATCH;
}
if (CTOR.matches(tree, state)) {
Description.Builder description = buildDescription(tree);
appendCharsets(description, tree, tree.getIdentifier(), tree.getArguments(), state);
return description.build();
}
if (FILE_READER.matches(tree, state)) {
return handleFileReader(tree, state);
}
if (FILE_WRITER.matches(tree, state)) {
return handleFileWriter(tree, state);
}
if (PRINT_WRITER.matches(tree, state)) {
return handlePrintWriter(tree, state);
}
if (PRINT_WRITER_OUTPUTSTREAM.matches(tree, state)) {
return handlePrintWriterOutputStream(tree, state);
}
if (SCANNER_MATCHER.matches(tree, state)) {
return handleScanner(tree, state);
}
return NO_MATCH;
}
private Description handleScanner(NewClassTree tree, VisitorState state) {
Description.Builder description = buildDescription(tree);
for (CharsetFix charsetFix : CharsetFix.values()) {
SuggestedFix.Builder fix =
SuggestedFix.builder()
.postfixWith(
getOnlyElement(tree.getArguments()),
String.format(", %s.name()", charsetFix.replacement()));
charsetFix.addImport(fix, state);
description.addFix(fix.build());
}
return description.build();
}
boolean shouldUseGuava(VisitorState state) {
for (ImportTree importTree : state.getPath().getCompilationUnit().getImports()) {
Symbol sym = ASTHelpers.getSymbol(importTree.getQualifiedIdentifier());
if (sym == null) {
continue;
}
if (sym.getQualifiedName().contentEquals("com.google.common.io.Files")) {
return true;
}
}
return false;
}
private Description handleFileReader(NewClassTree tree, VisitorState state) {
Tree arg = getOnlyElement(tree.getArguments());
Tree parent = state.getPath().getParentPath().getLeaf();
Tree toReplace = BUFFERED_READER.matches(parent, state) ? parent : tree;
Description.Builder description = buildDescription(tree);
fileReaderFix(description, state, arg, toReplace);
return description.build();
}
private void fileReaderFix(
Description.Builder description, VisitorState state, Tree arg, Tree toReplace) {
for (CharsetFix charset : CharsetFix.values()) {
if (shouldUseGuava(state)) {
description.addFix(guavaFileReaderFix(state, arg, toReplace, charset));
} else {
description.addFix(nioFileReaderFix(state, arg, toReplace, charset));
}
}
}
private Fix nioFileReaderFix(VisitorState state, Tree arg, Tree toReplace, CharsetFix charset) {
SuggestedFix.Builder fix = SuggestedFix.builder();
fix.replace(
toReplace,
String.format(
"Files.newBufferedReader(%s, %s)", toPath(state, arg, fix), charset.replacement()));
fix.addImport("java.nio.file.Files");
charset.addImport(fix, state);
variableTypeFix(fix, state, FileReader.class, Reader.class);
return fix.build();
}
private Fix guavaFileReaderFix(
VisitorState state, Tree fileArg, Tree toReplace, CharsetFix charset) {
SuggestedFix.Builder fix = SuggestedFix.builder();
fix.replace(
toReplace,
String.format(
"Files.newReader(%s, %s)", toFile(state, fileArg, fix), charset.replacement()));
fix.addImport("com.google.common.io.Files");
charset.addImport(fix, state);
variableTypeFix(fix, state, FileReader.class, Reader.class);
return fix.build();
}
private void variableTypeFix(
SuggestedFix.Builder fix, VisitorState state, Class<?> original, Class<?> replacement) {
Tree parent = state.getPath().getParentPath().getLeaf();
Symbol sym;
switch (parent.getKind()) {
case VARIABLE:
sym = ASTHelpers.getSymbol((VariableTree) parent);
break;
case ASSIGNMENT:
sym = ASTHelpers.getSymbol(((AssignmentTree) parent).getVariable());
break;
default:
return;
}
if (!ASTHelpers.isSameType(
sym.type, state.getTypeFromString(original.getCanonicalName()), state)) {
return;
}
state
.getPath()
.getCompilationUnit()
.accept(
new TreeScanner<Void, Void>() {
@Override
public Void visitVariable(VariableTree node, Void aVoid) {
if (sym.equals(ASTHelpers.getSymbol(node))) {
fix.replace(node.getType(), replacement.getSimpleName())
.addImport(replacement.getCanonicalName());
}
return null;
}
},
null);
}
private Description handleFileWriter(NewClassTree tree, VisitorState state) {
Iterator<? extends ExpressionTree> it = tree.getArguments().iterator();
Tree fileArg = it.next();
Tree appendMode = it.hasNext() ? it.next() : null;
Tree parent = state.getPath().getParentPath().getLeaf();
Tree toReplace = BUFFERED_WRITER.matches(parent, state) ? parent : tree;
Description.Builder description = buildDescription(tree);
boolean useGuava = shouldUseGuava(state);
for (CharsetFix charset : CharsetFix.values()) {
if (appendMode == null && useGuava) {
description.addFix(guavaFileWriterFix(state, fileArg, toReplace, charset));
} else {
description.addFix(
nioFileWriterFix(state, appendMode, fileArg, toReplace, charset, useGuava));
}
}
return description.build();
}
private Fix guavaFileWriterFix(
VisitorState state, Tree fileArg, Tree toReplace, CharsetFix charset) {
SuggestedFix.Builder fix = SuggestedFix.builder();
fix.replace(
toReplace,
String.format(
"Files.newWriter(%s, %s)", toFile(state, fileArg, fix), charset.replacement()));
fix.addImport("com.google.common.io.Files");
charset.addImport(fix, state);
variableTypeFix(fix, state, FileWriter.class, Writer.class);
return fix.build();
}
private Fix nioFileWriterFix(
VisitorState state,
Tree appendTree,
Tree fileArg,
Tree toReplace,
CharsetFix charset,
boolean qualify) {
SuggestedFix.Builder fix = SuggestedFix.builder();
StringBuilder sb = new StringBuilder();
if (qualify) {
sb.append("java.nio.file.Files");
} else {
sb.append("Files");
fix.addImport("java.nio.file.Files");
}
sb.append(".newBufferedWriter(");
sb.append(toPath(state, fileArg, fix));
sb.append(", ").append(charset.replacement());
charset.addImport(fix, state);
if (appendTree != null) {
sb.append(toAppendMode(fix, appendTree, state));
}
sb.append(")");
fix.replace(toReplace, sb.toString());
variableTypeFix(fix, state, FileWriter.class, Writer.class);
return fix.build();
}
/** Convert a boolean append mode to a StandardOpenOption. */
private String toAppendMode(SuggestedFix.Builder fix, Tree appendArg, VisitorState state) {
// recognize constants to try to avoid `true ? CREATE, APPEND : CREATE`
Boolean value = ASTHelpers.constValue(appendArg, Boolean.class);
if (value != null) {
if (value) {
fix.addStaticImport("java.nio.file.StandardOpenOption.APPEND");
fix.addStaticImport("java.nio.file.StandardOpenOption.CREATE");
return ", CREATE, APPEND";
} else {
// CREATE is the default
return "";
}
}
fix.addImport("java.nio.file.StandardOpenOption");
fix.addStaticImport("java.nio.file.StandardOpenOption.APPEND");
fix.addStaticImport("java.nio.file.StandardOpenOption.CREATE");
return String.format(
", %s ? new StandardOpenOption[] {CREATE, APPEND} : new StandardOpenOption[] {CREATE}",
state.getSourceForNode(appendArg));
}
/** Converts a {@code String} to a {@code File}. */
private Object toFile(VisitorState state, Tree fileArg, SuggestedFix.Builder fix) {
Type type = ASTHelpers.getType(fileArg);
if (ASTHelpers.isSubtype(type, state.getSymtab().stringType, state)) {
fix.addImport("java.io.File");
return String.format("new File(%s)", state.getSourceForNode(fileArg));
} else if (ASTHelpers.isSubtype(type, state.getTypeFromString("java.io.File"), state)) {
return state.getSourceForNode(fileArg);
} else {
throw new AssertionError("unexpected type: " + type);
}
}
/** Convert a {@code String} or {@code File} argument to a {@code Path}. */
private String toPath(VisitorState state, Tree fileArg, SuggestedFix.Builder fix) {
Type type = ASTHelpers.getType(fileArg);
if (ASTHelpers.isSubtype(type, state.getSymtab().stringType, state)) {
fix.addImport("java.nio.file.Paths");
return String.format("Paths.get(%s)", state.getSourceForNode(fileArg));
} else if (ASTHelpers.isSubtype(type, state.getTypeFromString("java.io.File"), state)) {
return String.format("%s.toPath()", state.getSourceForNode(fileArg));
} else {
throw new AssertionError("unexpected type: " + type);
}
}
private void appendCharsets(
Description.Builder description,
Tree tree,
Tree select,
List<? extends ExpressionTree> arguments,
VisitorState state) {
description.addFix(appendCharset(tree, select, arguments, state, CharsetFix.UTF_8_FIX));
description.addFix(
appendCharset(tree, select, arguments, state, CharsetFix.DEFAULT_CHARSET_FIX));
}
private Fix appendCharset(
Tree tree,
Tree select,
List<? extends ExpressionTree> arguments,
VisitorState state,
CharsetFix charset) {
SuggestedFix.Builder fix = SuggestedFix.builder();
if (arguments.isEmpty()) {
fix.replace(
state.getEndPosition(select),
state.getEndPosition(tree),
String.format("(%s)", charset.replacement()));
} else {
fix.postfixWith(Iterables.getLast(arguments), ", " + charset.replacement());
}
charset.addImport(fix, state);
return fix.build();
}
private Description handlePrintWriter(NewClassTree tree, VisitorState state) {
Description.Builder description = buildDescription(tree);
for (CharsetFix charsetFix : CharsetFix.values()) {
SuggestedFix.Builder fix =
SuggestedFix.builder()
.postfixWith(
getOnlyElement(tree.getArguments()),
String.format(", %s.name()", charsetFix.replacement()));
charsetFix.addImport(fix, state);
description.addFix(fix.build());
}
return description.build();
}
private Description handlePrintWriterOutputStream(NewClassTree tree, VisitorState state) {
Tree outputStream = tree.getArguments().get(0);
Description.Builder description = buildDescription(tree);
for (CharsetFix charsetFix : CharsetFix.values()) {
SuggestedFix.Builder fix =
SuggestedFix.builder()
.prefixWith(outputStream, "new BufferedWriter(new OutputStreamWriter(")
.postfixWith(outputStream, String.format(", %s))", charsetFix.replacement()));
charsetFix.addImport(fix, state);
fix.addImport("java.io.BufferedWriter");
fix.addImport("java.io.OutputStreamWriter");
description.addFix(fix.build());
}
return description.build();
}
}