/*
* 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.LocalQuickFixBase;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import org.intellij.erlang.psi.*;
import org.intellij.erlang.psi.impl.ErlangPsiImplUtil;
import org.intellij.erlang.quickfixes.ErlangCreateFunctionQuickFix;
import org.intellij.erlang.quickfixes.ErlangExportFunctionFix;
import org.intellij.erlang.sdk.ErlangSdkRelease;
import org.intellij.erlang.sdk.ErlangSdkType;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Map;
public class ErlangUndefinedCallbackFunctionInspection extends ErlangInspectionBase {
public static final String FIX_MESSAGE = "Implement and export all callbacks";
@Override
protected void checkFile(@NotNull ErlangFile file, @NotNull ProblemsHolder problemsHolder) {
ErlangSdkRelease release = ErlangSdkType.getRelease(file);
boolean supportOptionalCallbacks = release == null || !ErlangSdkRelease.V_18_0.isNewerThan(release);
for (ErlangBehaviour behaviour : file.getBehaviours()) {
ErlangModuleRef behaviourRef = behaviour.getModuleRef();
ErlangFile behaviourModule = ErlangPsiImplUtil.resolveToFile(behaviourRef);
if (behaviourModule == null) continue;
List<ErlangCallbackSpec> undefinedCallbacks = ContainerUtil.newArrayList();
Map<String, ErlangCallbackSpec> callbackMap = behaviourModule.getCallbackMap();
for (ErlangCallbackSpec spec : callbackMap.values()) {
if (supportOptionalCallbacks && spec.isOptional()) continue;
String name = ErlangPsiImplUtil.getCallbackSpecName(spec);
int arity = ErlangPsiImplUtil.getCallbackSpecArity(spec);
ErlangFunction function = name != null ? file.getFunction(name, arity) : null;
if (function == null || !function.isExported()) {
undefinedCallbacks.add(spec);
}
}
if (undefinedCallbacks.isEmpty()) continue;
boolean multiple = undefinedCallbacks.size() != 1;
StringBuilder builder = new StringBuilder();
builder.append("Undefined or non-exported callback function").append(multiple ? "s" : "").append(": ");
for (ErlangCallbackSpec spec : undefinedCallbacks) {
builder.append("'").append(ErlangPsiImplUtil.createFunctionPresentationFromCallbackSpec(spec)).append("', ");
}
String message = builder.substring(0, builder.length() - 2);
registerProblem(problemsHolder, behaviourRef, message,
new CreateAndExportFunctionsFix(undefinedCallbacks, file.getProject()));
}
}
private static class CreateAndExportFunctionsFix extends LocalQuickFixBase {
private final List<SmartPsiElementPointer<ErlangCallbackSpec>> myCallbackSpecs;
public CreateAndExportFunctionsFix(@NotNull List<ErlangCallbackSpec> callbackSpecs, @NotNull Project project) {
super(FIX_MESSAGE);
final SmartPointerManager manager = SmartPointerManager.getInstance(project);
myCallbackSpecs = ContainerUtil.map(callbackSpecs, manager::createSmartPsiElementPointer);
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor problemDescriptor) {
PsiElement problemElement = problemDescriptor.getPsiElement();
PsiFile containingFile = problemElement != null ? problemElement.getContainingFile() : null;
ErlangFile file = ObjectUtils.tryCast(containingFile, ErlangFile.class);
if (file == null) return;
addCallbackImplementations(project, file);
exportAddedCallbackImplementations(project, file);
}
private void addCallbackImplementations(@NotNull Project project, @NotNull ErlangFile file) {
PsiDocumentManager manager = PsiDocumentManager.getInstance(project);
Document document = manager.getDocument(file);
if (document == null) return;
for (SmartPsiElementPointer<ErlangCallbackSpec> pointer : myCallbackSpecs) {
ErlangCallbackSpec spec = pointer.getElement();
assert spec != null;
String name = ErlangPsiImplUtil.getCallbackSpecName(spec);
int arity = ErlangPsiImplUtil.getCallbackSpecArity(spec);
if (name == null || file.getFunction(name, arity) != null) continue;
PsiElement lastChild = file.getLastChild();
int textOffset = lastChild == null ? 0 : lastChild.getTextRange().getEndOffset();
String newFunction = "\n\n" + name + "(" + createVariableList(spec) + ") ->\n" +
ErlangCreateFunctionQuickFix.FUNCTION_BODY_DEFAULT_TEXT;
document.insertString(textOffset, newFunction);
manager.commitDocument(document);
CodeStyleManager.getInstance(project).reformatText(file, textOffset, textOffset + newFunction.length());
}
}
private void exportAddedCallbackImplementations(@NotNull Project project, @NotNull ErlangFile file) {
for (SmartPsiElementPointer<ErlangCallbackSpec> pointer : myCallbackSpecs) {
ErlangCallbackSpec spec = pointer.getElement();
assert spec != null;
String name = ErlangPsiImplUtil.getCallbackSpecName(spec);
int arity = ErlangPsiImplUtil.getCallbackSpecArity(spec);
ErlangFunction function = name != null ? file.getFunction(name, arity) : null;
if (function != null && !function.isExported()) {
new ErlangExportFunctionFix(function).invoke(project, file, null, function, null);
}
}
}
@NotNull
private static String createVariableList(@NotNull ErlangCallbackSpec spec) {
List<ErlangType> arguments = ErlangPsiImplUtil.getCallBackSpecArguments(spec);
if (arguments.isEmpty()) return "";
StringBuilder variables = new StringBuilder();
String separator = ", ";
for (ErlangType type : arguments) {
ErlangQVar qVar = type.getQVar();
variables.append(qVar != null ? qVar.getName() : "_").append(separator);
}
return variables.substring(0, variables.length() - separator.length());
}
}
}