/*
* Copyright 2013-2016 Sergey Ignatov, Alexander Zolotov, Florin Patan
*
* 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.goide.inspections;
import com.goide.inspections.GoPlaceholderChecker.Placeholder;
import com.goide.inspections.GoPlaceholderChecker.PrintVerb;
import com.goide.psi.*;
import com.goide.psi.impl.GoPsiImplUtil;
import com.goide.psi.impl.GoTypeUtil;
import com.intellij.codeInspection.LocalInspectionToolSession;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.ElementManipulators;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
public class GoPlaceholderCountInspection extends GoInspectionBase {
@NotNull
@Override
protected GoVisitor buildGoVisitor(@NotNull ProblemsHolder holder, @NotNull LocalInspectionToolSession session) {
return new GoVisitor() {
@Override
public void visitCallExpr(@NotNull GoCallExpr o) {
PsiReference psiReference = o.getExpression().getReference();
PsiElement resolved = psiReference != null ? psiReference.resolve() : null;
if (!(resolved instanceof GoFunctionOrMethodDeclaration)) return;
String functionName = StringUtil.toLowerCase(((GoFunctionOrMethodDeclaration)resolved).getName());
if (functionName == null) return;
if (GoPlaceholderChecker.isFormattingFunction(functionName)) {
checkPrintf(holder, o, (GoFunctionOrMethodDeclaration)resolved);
}
else if (GoPlaceholderChecker.isPrintingFunction(functionName)) {
checkPrint(holder, o, (GoFunctionOrMethodDeclaration)resolved);
}
}
};
}
private static void checkPrint(@NotNull ProblemsHolder holder,
@NotNull GoCallExpr callExpr,
@NotNull GoFunctionOrMethodDeclaration declaration) {
List<GoExpression> arguments = callExpr.getArgumentList().getExpressionList();
GoExpression firstArg = ContainerUtil.getFirstItem(arguments);
if (firstArg == null) return;
if (GoTypeUtil.isString(firstArg.getGoType(null))) {
String firstArgText = resolve(firstArg);
if (GoPlaceholderChecker.hasPlaceholder(firstArgText)) {
String message = "Possible formatting directive in <code>#ref</code> #loc";
holder.registerProblem(firstArg, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
return;
}
}
// TODO florin: Check first argument for os.Std* output
// Ref code: https://github.com/golang/go/blob/79f7ccf2c3931745aeb97c5c985b6ac7b44befb4/src/cmd/vet/print.go#L617
String declarationName = declaration.getName();
boolean isLn = declarationName != null && declarationName.endsWith("ln");
for (GoExpression argument : arguments) {
GoType goType = argument.getGoType(null);
if (isLn && GoTypeUtil.isString(goType)) {
String argText = resolve(argument);
if (argText != null && argText.endsWith("\\n")) {
String message = "Function already ends with new line #loc";
TextRange range = TextRange.from(argText.length() - 1, 2);
// TODO florin: add quickfix to remove trailing \n
// TODO florin: add quickfix to convert \n to a separate argument
holder.registerProblem(argument, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, range);
}
}
else if (GoTypeUtil.isFunction(goType)) {
String message = argument instanceof GoCallExpr ? "Final return type of <code>#ref</code> is a function not a function call #loc"
: "Argument <code>#ref</code> is not a function call #loc";
// TODO florin: add quickfix to convert to function call if possible
holder.registerProblem(argument, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
}
}
}
private static void checkPrintf(@NotNull ProblemsHolder holder,
@NotNull GoCallExpr callExpr,
@NotNull GoFunctionOrMethodDeclaration declaration) {
int placeholderPosition = GoPlaceholderChecker.getPlaceholderPosition(declaration);
List<GoExpression> arguments = callExpr.getArgumentList().getExpressionList();
if (arguments.isEmpty()) return;
int callArgsNum = arguments.size();
if (placeholderPosition < 0 || callArgsNum <= placeholderPosition) return;
GoExpression placeholder = arguments.get(placeholderPosition);
if (!GoTypeUtil.isString(placeholder.getGoType(null))) {
String message = "Value used for formatting text does not appear to be a string #loc";
holder.registerProblem(placeholder, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
return;
}
String placeholderText = resolve(placeholder);
if (placeholderText == null) return;
if (!GoPlaceholderChecker.hasPlaceholder(placeholderText) && callArgsNum > placeholderPosition) {
String message = "Value used for formatting text does not appear to contain a placeholder #loc";
holder.registerProblem(placeholder, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
return;
}
callArgsNum--;
List<Placeholder> placeholders = GoPlaceholderChecker.parsePrintf(placeholderText);
for (Placeholder fmtPlaceholder : placeholders) {
if (!checkPrintfArgument(holder, placeholder, callExpr, arguments, callArgsNum, placeholderPosition, fmtPlaceholder)) return;
}
if (hasErrors(holder, placeholder, placeholders)) return;
// TODO florin check to see if we are skipping any argument from the formatting string
int maxArgsNum = computeMaxArgsNum(placeholders, placeholderPosition);
if (GoPsiImplUtil.hasVariadic(callExpr.getArgumentList()) && maxArgsNum >= callArgsNum) {
return;
}
if (maxArgsNum != callArgsNum) {
int expect = maxArgsNum - placeholderPosition;
int numArgs = callArgsNum - placeholderPosition;
String message = String.format("Got %d placeholder(s) for %d arguments(s) #loc", expect, numArgs);
holder.registerProblem(placeholder, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
}
// TODO florin: check if all arguments are strings and add quickfix to replace with Println and string concat
}
private static int computeMaxArgsNum(@NotNull List<Placeholder> placeholders, int firstArg) {
int maxArgsNum = 0;
for (Placeholder placeholder : placeholders) {
List<Integer> arguments = placeholder.getArguments();
if (!arguments.isEmpty()) {
int max = Collections.max(arguments);
if (maxArgsNum < max) {
maxArgsNum = max;
}
}
}
return maxArgsNum + firstArg;
}
private static boolean hasErrors(@NotNull ProblemsHolder holder,
@NotNull GoExpression formatPlaceholder,
@NotNull List<Placeholder> placeholders) {
for (Placeholder placeholder : placeholders) {
Placeholder.State state = placeholder.getState();
if (state == Placeholder.State.MISSING_VERB_AT_END) {
String message = "Missing verb at end of format string in <code>#ref</code> call #loc";
holder.registerProblem(formatPlaceholder, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
return true;
}
if (state == Placeholder.State.ARGUMENT_INDEX_NOT_NUMERIC) {
String message = "Illegal syntax for <code>#ref</code> argument index, expecting a number #loc";
holder.registerProblem(formatPlaceholder, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
return true;
}
}
return false;
}
private static boolean checkPrintfArgument(@NotNull ProblemsHolder holder,
@NotNull GoExpression placeholder,
@NotNull GoCallExpr callExpr,
@NotNull List<GoExpression> arguments,
int callArgsNum,
int firstArg,
@NotNull Placeholder fmtPlaceholder) {
PrintVerb v = fmtPlaceholder.getVerb();
if (v == null) {
String message = "Unrecognized formatting verb <code>#ref</code> call #loc";
TextRange range = TextRange.from(fmtPlaceholder.getStartPos() + 1, fmtPlaceholder.getPlaceholder().length());
// TODO florin: add quickfix to suggest correct printf verbs (maybe take type into account when type info available?)
holder.registerProblem(placeholder, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, range);
return false;
}
String flags = fmtPlaceholder.getFlags();
for (int i = 0; i < flags.length(); i++) {
char flag = flags.charAt(i);
if (v.getFlags().indexOf(flag) == -1) {
String message = String.format("Unrecognized <code>#ref</code> flag for verb %s: %s call #loc", v.getVerb(), flag);
TextRange range = TextRange.from(fmtPlaceholder.getStartPos() + 1, fmtPlaceholder.getPlaceholder().length());
// TODO florin: add quickfix to suggest correct printf verbs (maybe take type into account when type info available?)
// TODO florin: cover with tests
holder.registerProblem(placeholder, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, range);
return false;
}
}
List<Integer> args = fmtPlaceholder.getArguments();
// Verb is good. If len(state.argNums)>trueArgs, we have something like %.*s and all
// but the final arg must be an integer.
int trueArgs = v == PrintVerb.Percent ? 0 : 1;
int nargs = args.size();
for (int i = 0; i < nargs - trueArgs; i++) {
if (!checkArgumentIndex(holder, placeholder, callExpr, fmtPlaceholder, callArgsNum)) return false;
// TODO florin: add argument matching when type comparison can be done
// Ref code: https://github.com/golang/go/blob/79f7ccf2c3931745aeb97c5c985b6ac7b44befb4/src/cmd/vet/print.go#L484
}
if (v == PrintVerb.Percent) return true;
if (!checkArgumentIndex(holder, placeholder, callExpr, fmtPlaceholder, callArgsNum)) return false;
int argNum = args.get(args.size() - 1);
GoExpression expression = arguments.get(argNum + firstArg - 1);
if (GoTypeUtil.isFunction(expression.getGoType(null)) && v != PrintVerb.p && v != PrintVerb.T) {
String message = "Argument <code>#ref</code> is not a function call #loc";
if (expression instanceof GoCallExpr) {
message = "Final return type of <code>#ref</code> is a function not a function call #loc";
}
// TODO florin: add quickfix for this to transform it into a function call
holder.registerProblem(expression, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
return false;
}
// TODO florin: add argument matching when type comparison can be done
// Ref code: https://github.com/golang/go/blob/79f7ccf2c3931745aeb97c5c985b6ac7b44befb4/src/cmd/vet/print.go#L502
return true;
}
private static boolean checkArgumentIndex(@NotNull ProblemsHolder holder,
@NotNull GoExpression placeholder,
@NotNull GoCallExpr callExpr,
@NotNull Placeholder fmtPlaceholder,
int callArgsNum) {
int argNum = fmtPlaceholder.getPosition();
if (argNum < 0) return false;
if (argNum == 0) {
TextRange range = TextRange.create(fmtPlaceholder.getStartPos() + 3, fmtPlaceholder.getStartPos() + 4);
// TODO florin: add quickfix to suggest placeholder value
holder.registerProblem(placeholder, "Index value [0] is not allowed #loc", ProblemHighlightType.GENERIC_ERROR_OR_WARNING, range);
return false;
}
if (argNum < callArgsNum) return true;
if (GoPsiImplUtil.hasVariadic(callExpr.getArgumentList())) return false;
if (argNum == callArgsNum) return true;
// There are bad indexes in the format or there are fewer arguments than the format needs
// This is the argument number relative to the format: Printf("%s", "hi") will give 1 for the "hi"
int arg = fmtPlaceholder.getPosition();
String message = String.format("Got %d placeholder(s) for %d arguments(s)", arg, callArgsNum);
holder.registerProblem(placeholder, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
return false;
}
@Nullable
private static String resolve(@NotNull GoExpression argument) {
String argumentValue = getValue(argument);
if (argumentValue != null) {
return argumentValue;
}
PsiReference reference = argument.getReference();
PsiElement resolved = reference != null ? reference.resolve() : null;
String value = null;
if (resolved instanceof GoVarDefinition) {
value = getValue(((GoVarDefinition)resolved).getValue());
}
else if (resolved instanceof GoConstDefinition) {
value = getValue(((GoConstDefinition)resolved).getValue());
}
return value;
}
// todo: implement ConstEvaluator
@Nullable
private static String getValue(@Nullable GoExpression expression) {
if (expression instanceof GoStringLiteral) {
return ElementManipulators.getValueText(expression);
}
if (expression instanceof GoAddExpr) {
StringBuilder result = new StringBuilder();
for (GoExpression expr : ((GoAddExpr)expression).getExpressionList()) {
String value = getValue(expr);
if (value == null) return null;
result.append(value);
}
return StringUtil.nullize(result.toString());
}
if (expression instanceof GoParenthesesExpr) {
return getValue(((GoParenthesesExpr)expression).getExpression());
}
return null;
}}