Skip to content

Added function call inspection with arity checks #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@
<localInspection language="Cypher" displayName="Cypher EXPLAIN warning inspection" groupPath="Cypher"
groupName="General" enabledByDefault="true" level="WARNING"
implementationClass="com.neueda.jetbrains.plugin.graphdb.jetbrains.inspection.CypherExplainWarningInspection"/>
<localInspection language="Cypher" displayName="Function call inspection" groupPath="Cypher"
groupName="General" enabledByDefault="true" level="ERROR"
implementationClass="com.neueda.jetbrains.plugin.graphdb.language.cypher.inspections.CypherFunctionCallInspection"/>

<codeInsight.parameterInfo language="Cypher" implementationClass="com.neueda.jetbrains.plugin.graphdb.language.cypher.editor.CypherParameterInfoHandler"/>

<codeStyleSettingsProvider implementation="com.neueda.jetbrains.plugin.graphdb.jetbrains.formatter.CypherCodeStyleSettingsProvider"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import com.google.common.collect.Lists;
import com.intellij.codeInsight.lookup.LookupElement;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherBuiltInFunctionElement;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherElementWithSignature.InvokableInformation;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.InvokableInformation;

import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -53,7 +53,7 @@ public final class CypherBuiltInFunctions {
element("extract", "(variable IN list | expression)", ANY.array()),
element("filter", "(variable IN list WHERE predicate)", ANY.array()),
element("tail", "(expression)", ANY.array()),
element("range", "(start, end [, step])", INTEGER.array()),
element("range", "(start, end, step = 1)", INTEGER.array()),
element("reduce", "(accumulator = initial, variable IN list | expression)", ANY.single())
);
private static final List<CypherBuiltInFunctionElement> FUNCTIONS_MATH_NUMERIC = Lists.newArrayList(
Expand Down Expand Up @@ -87,7 +87,7 @@ public final class CypherBuiltInFunctions {
);
private static final List<CypherBuiltInFunctionElement> FUNCTIONS_STRING = Lists.newArrayList(
element("replace", "(original, search, replace)", STRING.single()),
element("substring", "(original, start, length?)", STRING.single()),
element("substring", "(original, start, length = length(original))", STRING.single()),
element("left", "(original, length)", STRING.single()),
element("right", "(original, length)", STRING.single()),
element("ltrim", "(original)", STRING.single()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.intellij.codeInsight.completion.util.ParenthesesInsertHandler;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherElementWithSignature.InvokableInformation;
import com.neueda.jetbrains.plugin.graphdb.platform.GraphIcons;

public class CypherBuiltInFunctionElement implements CypherElement {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements;

import com.intellij.openapi.util.text.StringUtil;

import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public interface CypherElementWithSignature {
Expand All @@ -15,49 +11,4 @@ default InvokableInformation extractInformation(String fullSignature, String nam
return new InvokableInformation(fullSignature, name);
}

final class InvokableInformation {
private final String name;
private final String signature;
private final String returnType;
private final boolean hasParameters;

public InvokableInformation(String fullSignature, String name) {
String signatureWithoutName = StringUtil.trimStart(fullSignature, name);
Matcher m = FULL_SIGNATURE_REGEXP.matcher(signatureWithoutName);
if (m.find()) {
signature = m.group(1);
returnType = m.group(2);
} else {
// should never happen, unless Neo4j changes signature syntax
signature = fullSignature;
returnType = "<?>";
}

this.name = name;
this.hasParameters = !this.signature.startsWith("()");
}

public InvokableInformation(String name, String signature, String returnType) {
this.name = name;
this.signature = signature;
this.returnType = returnType;
this.hasParameters = !Objects.equals(signature, "()");
}

public String getName() {
return name;
}

public String getSignature() {
return signature;
}

public String getReturnType() {
return returnType;
}

public boolean hasParameters() {
return hasParameters;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements;

import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.Range;

import java.util.Objects;
import java.util.regex.Matcher;

public final class InvokableInformation {

private final String name;
private final String signature;
private final String returnType;
private final boolean hasParameters;
private final Range<Integer> arity;

public InvokableInformation(String fullSignature, String name) {
String signatureWithoutName = StringUtil.trimStart(fullSignature, name);
Matcher m = CypherElementWithSignature.FULL_SIGNATURE_REGEXP.matcher(signatureWithoutName);
if (m.find()) {
signature = m.group(1);
returnType = m.group(2);
} else {
// should never happen, unless Neo4j changes signature syntax
signature = fullSignature;
returnType = "<?>";
}

this.name = name;
this.hasParameters = !this.signature.startsWith("()");
this.arity = calculateArity();
}

public InvokableInformation(String name, String signature, String returnType) {
this.name = name;
this.signature = signature;
this.returnType = returnType;
this.hasParameters = !Objects.equals(signature, "()");
this.arity = calculateArity();
}

public String getName() {
return name;
}

public String getSignature() {
return signature;
}

public String getReturnType() {
return returnType;
}

public boolean hasParameters() {
return hasParameters;
}

public Range<Integer> getArity() {
return arity;
}

private Range<Integer> calculateArity() {
if (!hasParameters) {
return new Range<>(0, 0);
}

int from = 0;
int to = 0;
String[] args = signature.substring(1, signature.length() - 1).split(",");
for (String arg : args) {
String p = arg.trim();
if (p.endsWith("...")) {
from++;
to = Integer.MAX_VALUE;
break;
} else if (p.contains("=")) {
to++;
} else {
from++;
to++;
}
}

return new Range<>(from, to);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,12 @@

import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.lang.parameterInfo.*;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.CypherMetadataProviderService;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.atoms.CypherBuiltInFunctions;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherBuiltInFunctionElement;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherElementWithSignature.InvokableInformation;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherProcedureElement;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherUserFunctionElement;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherProcedureInvocation;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.InvokableInformation;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherTypes;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.references.CypherInvocation;
import org.apache.commons.lang.StringUtils;
Expand All @@ -23,7 +16,6 @@

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

public class CypherParameterInfoHandler
Expand Down Expand Up @@ -135,26 +127,13 @@ public String getPresentation(CypherInvocation ci, @NotNull ParameterInfoUIConte
return null;
}

String signature;
if (ci instanceof CypherProcedureInvocation) {
signature = getMetadataService(ci).findProcedure(ci.getFullName())
.map(CypherProcedureElement::getInvokableInformation)
.map(InvokableInformation::getSignature)
.orElse(null);
} else {
signature = CypherBuiltInFunctions.FUNCTIONS.stream()
.filter(f -> Objects.equals(f.getInvokable().getName(), ci.getFullName()))
.findFirst()
.map(CypherBuiltInFunctionElement::getInvokable)
.map(Optional::of)
.orElseGet(() -> getMetadataService(ci).findUserFunction(ci.getFullName())
.map(CypherUserFunctionElement::getInvokableInformation))
.map(InvokableInformation::getSignature)
.orElse(null);
}
String signature = ci.resolve()
.map(InvokableInformation::getSignature)
.orElse(null);

int current = context.getCurrentParameterIndex();


if (signature == null || Objects.equals(signature, "()")) {
context.setUIComponentEnabled(false);
return null;
Expand All @@ -171,8 +150,4 @@ public String getPresentation(CypherInvocation ci, @NotNull ParameterInfoUIConte
context.getDefaultParameterColor());
}

private CypherMetadataProviderService getMetadataService(PsiElement element) {
return ServiceManager.getService(element.getProject(), CypherMetadataProviderService.class);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.neueda.jetbrains.plugin.graphdb.language.cypher.inspections;

import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.LocalInspectionToolSession;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.util.Range;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.InvokableInformation;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherFunctionInvocation;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherProcedureInvocation;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.references.CypherInvocation;
import org.jetbrains.annotations.NotNull;

public class CypherFunctionCallInspection extends LocalInspectionTool {

@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder,
boolean isOnTheFly,
@NotNull LocalInspectionToolSession session) {
return new PsiElementVisitor() {
@Override
public void visitElement(PsiElement element) {
super.visitElement(element);
if (element instanceof CypherInvocation) {
CypherInvocation ci = (CypherInvocation) element;

// Skip functions built into grammar, since they are checked by parser
if (!(ci instanceof CypherFunctionInvocation) && !(ci instanceof CypherProcedureInvocation)) {
return;
}

InvokableInformation invokable = ci.resolve().orElse(null);
if (invokable == null) {
return;
}

int actual = ci.arguments().size();
Range<Integer> expected = invokable.getArity();

if (expected.isWithin(actual)) {
return;
}

String tail = " arguments in call to " + ci.getFullName();
if (actual < expected.getFrom()) {
holder.registerProblem(ci.argumentsToken(), "not enough" + tail);
} else if (actual > expected.getTo()) {
holder.registerProblem(ci.argumentsToken(), "too many" + tail);
}
}
}
};
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package com.neueda.jetbrains.plugin.graphdb.language.cypher.references;


import com.intellij.openapi.components.ServiceManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.CypherMetadataProviderService;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.atoms.CypherBuiltInFunctions;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherBuiltInFunctionElement;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.InvokableInformation;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherProcedureElement;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherUserFunctionElement;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherFunctionArguments;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherFunctionInvocation;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherProcedureInvocation;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

public interface CypherInvocation extends PsiElement {
Expand Down Expand Up @@ -36,6 +44,26 @@ default PsiElement argumentsToken() {
} else {
return this;
}

}

default Optional<InvokableInformation> resolve() {
CypherMetadataProviderService svc = ServiceManager.getService(
getProject(),
CypherMetadataProviderService.class);

if (this instanceof CypherProcedureInvocation) {
return svc.findProcedure(getFullName())
.map(CypherProcedureElement::getInvokableInformation);
}

return CypherBuiltInFunctions.FUNCTIONS.stream()
.filter(f -> Objects.equals(f.getInvokable().getName(), getFullName()))
.findFirst()
.map(CypherBuiltInFunctionElement::getInvokable)
.map(Optional::of)
.orElseGet(() -> svc.findUserFunction(getFullName())
.map(CypherUserFunctionElement::getInvokableInformation));
}

String getFullName();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<html>
<body>
This inspection checks function and procedure calls.
</body>
</html>
Loading