/* * 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.psi.GoFunctionOrMethodDeclaration; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import static com.goide.GoConstants.TESTING_PATH; import static com.goide.inspections.GoPlaceholderChecker.PrintfArgumentType.*; public class GoPlaceholderChecker { // This holds the name of the known formatting functions and position of the string to be formatted private static final Map<String, Integer> FORMATTING_FUNCTIONS = ContainerUtil.newHashMap( Pair.pair("errorf", 0), Pair.pair("fatalf", 0), Pair.pair("fprintf", 1), Pair.pair("fscanf", 1), Pair.pair("logf", 0), Pair.pair("panicf", 0), Pair.pair("printf", 0), Pair.pair("scanf", 0), Pair.pair("skipf", 0), Pair.pair("sprintf", 0), Pair.pair("sscanf", 1)); private static final Set<String> PRINTING_FUNCTIONS = ContainerUtil.newHashSet( "error", "error", "fatal", "fprint", "fprintln", "log", "panic", "panicln", "print", "println", "sprint", "sprintln" ); protected enum PrintfArgumentType { ANY(-1), BOOL(1), INT(2), RUNE(3), STRING(4), FLOAT(5), COMPLEX(6), POINTER(7); private int myMask; PrintfArgumentType(int mask) { myMask = mask; } public int getValue() { return myMask; } } enum PrintVerb { Percent('%', "", 0), b('b', " -+.0", INT.getValue() | FLOAT.getValue() | COMPLEX.getValue()), c('c', "-", RUNE.getValue() | INT.getValue()), d('d', " -+.0", INT.getValue()), e('e', " -+.0", FLOAT.getValue() | COMPLEX.getValue()), E('E', " -+.0", FLOAT.getValue() | COMPLEX.getValue()), f('f', " -+.0", FLOAT.getValue() | COMPLEX.getValue()), F('F', " -+.0", FLOAT.getValue() | COMPLEX.getValue()), g('g', " -+.0", FLOAT.getValue() | COMPLEX.getValue()), G('G', " -+.0", FLOAT.getValue() | COMPLEX.getValue()), o('o', " -+.0#", INT.getValue()), p('p', "-#", POINTER.getValue()), q('q', " -+.0#", RUNE.getValue() | INT.getValue() | STRING.getValue()), s('s', " -+.0", STRING.getValue()), t('t', "-", BOOL.getValue()), T('T', "-", ANY.getValue()), U('U', "-#", RUNE.getValue() | INT.getValue()), V('v', " -+.0#", ANY.getValue()), x('x', " -+.0#", RUNE.getValue() | INT.getValue() | STRING.getValue()), X('X', " -+.0#", RUNE.getValue() | INT.getValue() | STRING.getValue()); private char myVerb; private String myFlags; private int myMask; PrintVerb(char verb, String flags, int mask) { myVerb = verb; myFlags = flags; myMask = mask; } public char getVerb() { return myVerb; } @NotNull public String getFlags() { return myFlags; } public int getMask() { return myMask; } @Nullable public static PrintVerb getByVerb(char verb) { for (PrintVerb v : values()) { if (verb == v.getVerb()) return v; } return null; } } public static boolean isFormattingFunction(String functionName) { return FORMATTING_FUNCTIONS.containsKey(functionName); } public static boolean isPrintingFunction(String functionName) { return PRINTING_FUNCTIONS.contains(functionName); } static class Placeholder { private final String placeholder; private final int startPos; private final State state; private final PrintVerb verb; private final List<Integer> arguments; private final String flags; enum State { OK, MISSING_VERB_AT_END, ARGUMENT_INDEX_NOT_NUMERIC } Placeholder(State state, int startPos, String placeholder, String flags, List<Integer> arguments, PrintVerb verb) { this.placeholder = placeholder; this.startPos = startPos; this.verb = verb; this.state = state; this.arguments = arguments; this.flags = flags; } public String getPlaceholder() { return placeholder; } public int getPosition() { return arguments.get(arguments.size() - 1); } public State getState() { return state; } public PrintVerb getVerb() { return verb; } public List<Integer> getArguments() { return arguments; } public String getFlags() { return flags; } public int getStartPos() { return startPos; } } @NotNull public static List<Placeholder> parsePrintf(@NotNull String placeholderText) { List<Placeholder> placeholders = ContainerUtil.newArrayList(); int argNum = 1; int w; for (int i = 0; i < placeholderText.length(); i += w) { w = 1; if (placeholderText.charAt(i) == '%') { FormatState state = parsePrintfVerb(placeholderText.substring(i), i, argNum); w = state.format.length(); // We are not interested in %% which prints % if (state.state == Placeholder.State.OK && state.verb == PrintVerb.Percent) { // Special magic case for allowing things like %*% to pass (which are sadly valid expressions) if (state.format.length() == 2) continue; } placeholders.add(state.toPlaceholder()); // We only consider ok states as valid to increase the number of arguments. Should we? if (state.state != Placeholder.State.OK) continue; if (!state.indexed) { if (!state.argNums.isEmpty()) { int maxArgNum = Collections.max(state.argNums); if (argNum < maxArgNum) { argNum = maxArgNum; } } } else { argNum = state.argNums.get(state.argNums.size() - 1); } argNum++; } } return placeholders; } protected static int getPlaceholderPosition(@NotNull GoFunctionOrMethodDeclaration function) { Integer position = FORMATTING_FUNCTIONS.get(StringUtil.toLowerCase(function.getName())); if (position != null) { String importPath = function.getContainingFile().getImportPath(false); if ("fmt".equals(importPath) || "log".equals(importPath) || TESTING_PATH.equals(importPath)) { return position; } } return -1; } private static class FormatState { @Nullable private PrintVerb verb; // the format verb: 'd' for "%d" private String format; // the full format directive from % through verb, "%.3d" @NotNull private String flags = ""; // the list of # + etc private boolean indexed; // whether an indexing expression appears: %[1]d private final int startPos; // index of the first character of the placeholder in the formatting string // the successive argument numbers that are consumed, adjusted to refer to actual arg in call private final List<Integer> argNums = ContainerUtil.newArrayList(); // Keep track of the parser state private Placeholder.State state; // Used only during parse. private int argNum; // Which argument we're expecting to format now private int nBytes = 1; // number of bytes of the format string consumed FormatState(String format, int startPos, int argNum) { this.format = format; this.startPos = startPos; this.argNum = argNum; } @NotNull private Placeholder toPlaceholder() { return new Placeholder(state, startPos, format, flags, argNums, verb); } } @NotNull private static FormatState parsePrintfVerb(@NotNull String format, int startPos, int argNum) { FormatState state = new FormatState(format, startPos, argNum); parseFlags(state); if (!parseIndex(state)) return state; // There may be a width if (!parseNum(state)) return state; if (!parsePrecision(state)) return state; // Now a verb, possibly prefixed by an index (which we may already have) if (!parseIndex(state)) return state; if (state.nBytes == format.length()) { state.state = Placeholder.State.MISSING_VERB_AT_END; return state; } state.verb = PrintVerb.getByVerb(state.format.charAt(state.nBytes)); if (state.verb != null && state.verb != PrintVerb.Percent) state.argNums.add(state.argNum); state.nBytes++; state.format = state.format.substring(0, state.nBytes); state.state = Placeholder.State.OK; return state; } public static boolean hasPlaceholder(@Nullable String formatString) { return formatString != null && StringUtil.containsChar(formatString, '%'); } private static void parseFlags(@NotNull FormatState state) { String knownFlags = "#0+- "; StringBuilder flags = new StringBuilder(state.flags); while (state.nBytes < state.format.length()) { if (StringUtil.containsChar(knownFlags, state.format.charAt(state.nBytes))) { flags.append(state.format.charAt(state.nBytes)); } else { state.flags = flags.toString(); return; } state.nBytes++; } state.flags = flags.toString(); } private static void scanNum(@NotNull FormatState state) { while (state.nBytes < state.format.length()) { if (!StringUtil.isDecimalDigit(state.format.charAt(state.nBytes))) { return; } state.nBytes++; } } private static boolean parseIndex(@NotNull FormatState state) { if (state.nBytes == state.format.length() || state.format.charAt(state.nBytes) != '[') return true; state.indexed = true; state.nBytes++; int start = state.nBytes; scanNum(state); if (state.nBytes == state.format.length() || state.nBytes == start || state.format.charAt(state.nBytes) != ']') { state.state = Placeholder.State.ARGUMENT_INDEX_NOT_NUMERIC; return false; } int arg; try { arg = Integer.parseInt(state.format.substring(start, state.nBytes)); } catch (NumberFormatException ignored) { state.state = Placeholder.State.ARGUMENT_INDEX_NOT_NUMERIC; return false; } state.nBytes++; state.argNum = arg; return true; } private static boolean parseNum(@NotNull FormatState state) { if (state.nBytes < state.format.length() && state.format.charAt(state.nBytes) == '*') { state.nBytes++; state.argNums.add(state.argNum); state.argNum++; } else { scanNum(state); } return true; } private static boolean parsePrecision(@NotNull FormatState state) { if (state.nBytes < state.format.length() && state.format.charAt(state.nBytes) == '.') { state.flags += '.'; state.nBytes++; if (!parseIndex(state)) return false; if (!parseNum(state)) return false; } return true; } }