/*
* Copyright 2012-2014 Sergey Ignatov
*
* 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 org.intellij.erlang.inspection;
import com.intellij.codeInspection.LocalInspectionToolSession;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiReference;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import org.intellij.erlang.psi.*;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
//TODO make this inspection work when using -import attribute for io, io_lib
public class ErlangIoFormatInspection extends ErlangInspectionBase {
private static final Pattern CONTROL_SEQUENCE_PATTERN;
static {
String fieldWidth = "(?:(\\*)|(?:-?\\d+))?";
String precision = "(?:\\.(?:(\\*)|(?:\\d+))?)";
String oneOfErlangEscapeSequences = "(?:\\\\(?:[bdefnrstv'\"\\\\]|(?:[0-7]{1,3})|(?:x[0-9a-fA-F]{2})|(?:x\\{[0-9a-fA-F]+\\})|(?:\\^[a-zA-Z])))";
String paddingCharacter = "(?:\\.(?:(\\*)|" + oneOfErlangEscapeSequences + "|.))?";
String controlSequenceModifier = "[tl]?";
String controlSequenceType = "(?:([cfegswpWPBX#bx\\+i])|([~n]))";
CONTROL_SEQUENCE_PATTERN = Pattern.compile("~" + fieldWidth + "(?:" + precision + paddingCharacter + ")?" + controlSequenceModifier + controlSequenceType);
}
private static final Set<String> MODULE_NAMES = ContainerUtil.set("io", "io_lib");
private static final Set<String> FUNCTION_NAMES = ContainerUtil.set("format", "fwrite");
@NotNull
@Override
protected ErlangVisitor buildErlangVisitor(@NotNull final ProblemsHolder holder, @NotNull LocalInspectionToolSession session) {
return new ErlangVisitor() {
@Override
public void visitGlobalFunctionCallExpression(@NotNull ErlangGlobalFunctionCallExpression o) {
ErlangFunctionCallExpression expression = o.getFunctionCallExpression();
List<ErlangExpression> expressionList = expression.getArgumentList().getExpressionList();
int size = expressionList.size();
if (size < 2) return;
ErlangModule module = ObjectUtils.tryCast(o.getModuleRef().getReference().resolve(), ErlangModule.class);
if (module == null || !MODULE_NAMES.contains(module.getName())) return;
PsiReference ref = expression.getReference();
ErlangFunction function = ObjectUtils.tryCast(ref != null ? ref.resolve() : null, ErlangFunction.class);
if (function == null || !FUNCTION_NAMES.contains(function.getName())) return;
List<ErlangExpression> reverse = ContainerUtil.reverse(expressionList);
ErlangListExpression args = ObjectUtils.tryCast(reverse.get(0), ErlangListExpression.class);
ErlangStringLiteral formatLiteral = ObjectUtils.tryCast(reverse.get(1), ErlangStringLiteral.class);
String formatString = formatLiteral != null ? formatLiteral.getString().getText() : null;
if (formatString == null || formatString.length() < 2) return;
int expectedArgumentsCount;
try {
expectedArgumentsCount = getExpectedFormatArgsCount(formatString);
} catch (InvalidControlSequenceException e) {
registerProblem(holder, formatLiteral, "Invalid control sequence",
TextRange.create(e.getInvalidSequenceStartIdx(), formatString.length() - 1), null);
return;
}
int passedArgumentsCount = args != null ? args.getExpressionList().size() : -1;
if (passedArgumentsCount >= 0 && expectedArgumentsCount != passedArgumentsCount) {
String problemDescription = "Wrong number of arguments in format call, should be " + expectedArgumentsCount;
registerProblem(holder, formatLiteral, problemDescription);
}
}
};
}
private static int getExpectedFormatArgsCount(String formatString) throws InvalidControlSequenceException {
int expectedArgumentsCount = 0;
int previousMatchEnd = 0;
Matcher matcher = CONTROL_SEQUENCE_PATTERN.matcher(formatString);
while (matcher.find()) {
checkNoControlSequencePresent(formatString, previousMatchEnd, matcher.start());
for (int i = 1; i < 5; i++) {
if (matcher.group(i) != null) expectedArgumentsCount++;
}
String controlSequenceType = matcher.group(4);
if ("P".equals(controlSequenceType)) expectedArgumentsCount++;
if (controlSequenceType == null && matcher.group(5) == null) {
throw new InvalidControlSequenceException(matcher.start());
}
previousMatchEnd = matcher.end();
}
checkNoControlSequencePresent(formatString, previousMatchEnd, formatString.length());
return expectedArgumentsCount;
}
private static void checkNoControlSequencePresent(String formatString, int begin, int end) throws InvalidControlSequenceException {
int controlSequenceStart = StringUtil.indexOf(formatString, '~', begin, end);
if (controlSequenceStart != -1) {
throw new InvalidControlSequenceException(controlSequenceStart);
}
}
private static class InvalidControlSequenceException extends Exception {
private int myInvalidSequenceStartIdx;
InvalidControlSequenceException(int invalidSequenceStartIdx) {
myInvalidSequenceStartIdx = invalidSequenceStartIdx;
}
private int getInvalidSequenceStartIdx() {
return myInvalidSequenceStartIdx;
}
}
}