Skip to content

Commit 6a57b9b

Browse files
nikarhFylmTM
authored andcommitted
Added function call inspection with arity checks (#42)
* Added function call inspection with arity checks
1 parent 869af40 commit 6a57b9b

File tree

10 files changed

+246
-83
lines changed

10 files changed

+246
-83
lines changed

graph-database-support-plugin/src/main/resources/META-INF/plugin.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@
157157
<localInspection language="Cypher" displayName="Cypher EXPLAIN warning inspection" groupPath="Cypher"
158158
groupName="General" enabledByDefault="true" level="WARNING"
159159
implementationClass="com.neueda.jetbrains.plugin.graphdb.jetbrains.inspection.CypherExplainWarningInspection"/>
160+
<localInspection language="Cypher" displayName="Function call inspection" groupPath="Cypher"
161+
groupName="General" enabledByDefault="true" level="ERROR"
162+
implementationClass="com.neueda.jetbrains.plugin.graphdb.language.cypher.inspections.CypherFunctionCallInspection"/>
163+
160164
<codeInsight.parameterInfo language="Cypher" implementationClass="com.neueda.jetbrains.plugin.graphdb.language.cypher.editor.CypherParameterInfoHandler"/>
161165

162166
<codeStyleSettingsProvider implementation="com.neueda.jetbrains.plugin.graphdb.jetbrains.formatter.CypherCodeStyleSettingsProvider"/>

language/cypher/src/main/java/com/neueda/jetbrains/plugin/graphdb/language/cypher/completion/metadata/atoms/CypherBuiltInFunctions.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import com.google.common.collect.Lists;
44
import com.intellij.codeInsight.lookup.LookupElement;
55
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherBuiltInFunctionElement;
6-
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherElementWithSignature.InvokableInformation;
6+
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.InvokableInformation;
77

88
import java.util.ArrayList;
99
import java.util.List;
@@ -53,7 +53,7 @@ public final class CypherBuiltInFunctions {
5353
element("extract", "(variable IN list | expression)", ANY.array()),
5454
element("filter", "(variable IN list WHERE predicate)", ANY.array()),
5555
element("tail", "(expression)", ANY.array()),
56-
element("range", "(start, end [, step])", INTEGER.array()),
56+
element("range", "(start, end, step = 1)", INTEGER.array()),
5757
element("reduce", "(accumulator = initial, variable IN list | expression)", ANY.single())
5858
);
5959
private static final List<CypherBuiltInFunctionElement> FUNCTIONS_MATH_NUMERIC = Lists.newArrayList(
@@ -87,7 +87,7 @@ public final class CypherBuiltInFunctions {
8787
);
8888
private static final List<CypherBuiltInFunctionElement> FUNCTIONS_STRING = Lists.newArrayList(
8989
element("replace", "(original, search, replace)", STRING.single()),
90-
element("substring", "(original, start, length?)", STRING.single()),
90+
element("substring", "(original, start, length = length(original))", STRING.single()),
9191
element("left", "(original, length)", STRING.single()),
9292
element("right", "(original, length)", STRING.single()),
9393
element("ltrim", "(original)", STRING.single()),

language/cypher/src/main/java/com/neueda/jetbrains/plugin/graphdb/language/cypher/completion/metadata/elements/CypherBuiltInFunctionElement.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import com.intellij.codeInsight.completion.util.ParenthesesInsertHandler;
44
import com.intellij.codeInsight.lookup.LookupElement;
55
import com.intellij.codeInsight.lookup.LookupElementBuilder;
6-
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherElementWithSignature.InvokableInformation;
76
import com.neueda.jetbrains.plugin.graphdb.platform.GraphIcons;
87

98
public class CypherBuiltInFunctionElement implements CypherElement {
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
package com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements;
22

3-
import com.intellij.openapi.util.text.StringUtil;
4-
5-
import java.util.Objects;
6-
import java.util.regex.Matcher;
73
import java.util.regex.Pattern;
84

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

18-
final class InvokableInformation {
19-
private final String name;
20-
private final String signature;
21-
private final String returnType;
22-
private final boolean hasParameters;
23-
24-
public InvokableInformation(String fullSignature, String name) {
25-
String signatureWithoutName = StringUtil.trimStart(fullSignature, name);
26-
Matcher m = FULL_SIGNATURE_REGEXP.matcher(signatureWithoutName);
27-
if (m.find()) {
28-
signature = m.group(1);
29-
returnType = m.group(2);
30-
} else {
31-
// should never happen, unless Neo4j changes signature syntax
32-
signature = fullSignature;
33-
returnType = "<?>";
34-
}
35-
36-
this.name = name;
37-
this.hasParameters = !this.signature.startsWith("()");
38-
}
39-
40-
public InvokableInformation(String name, String signature, String returnType) {
41-
this.name = name;
42-
this.signature = signature;
43-
this.returnType = returnType;
44-
this.hasParameters = !Objects.equals(signature, "()");
45-
}
46-
47-
public String getName() {
48-
return name;
49-
}
50-
51-
public String getSignature() {
52-
return signature;
53-
}
54-
55-
public String getReturnType() {
56-
return returnType;
57-
}
58-
59-
public boolean hasParameters() {
60-
return hasParameters;
61-
}
62-
}
6314
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements;
2+
3+
import com.intellij.openapi.util.text.StringUtil;
4+
import com.intellij.util.Range;
5+
6+
import java.util.Objects;
7+
import java.util.regex.Matcher;
8+
9+
public final class InvokableInformation {
10+
11+
private final String name;
12+
private final String signature;
13+
private final String returnType;
14+
private final boolean hasParameters;
15+
private final Range<Integer> arity;
16+
17+
public InvokableInformation(String fullSignature, String name) {
18+
String signatureWithoutName = StringUtil.trimStart(fullSignature, name);
19+
Matcher m = CypherElementWithSignature.FULL_SIGNATURE_REGEXP.matcher(signatureWithoutName);
20+
if (m.find()) {
21+
signature = m.group(1);
22+
returnType = m.group(2);
23+
} else {
24+
// should never happen, unless Neo4j changes signature syntax
25+
signature = fullSignature;
26+
returnType = "<?>";
27+
}
28+
29+
this.name = name;
30+
this.hasParameters = !this.signature.startsWith("()");
31+
this.arity = calculateArity();
32+
}
33+
34+
public InvokableInformation(String name, String signature, String returnType) {
35+
this.name = name;
36+
this.signature = signature;
37+
this.returnType = returnType;
38+
this.hasParameters = !Objects.equals(signature, "()");
39+
this.arity = calculateArity();
40+
}
41+
42+
public String getName() {
43+
return name;
44+
}
45+
46+
public String getSignature() {
47+
return signature;
48+
}
49+
50+
public String getReturnType() {
51+
return returnType;
52+
}
53+
54+
public boolean hasParameters() {
55+
return hasParameters;
56+
}
57+
58+
public Range<Integer> getArity() {
59+
return arity;
60+
}
61+
62+
private Range<Integer> calculateArity() {
63+
if (!hasParameters) {
64+
return new Range<>(0, 0);
65+
}
66+
67+
int from = 0;
68+
int to = 0;
69+
String[] args = signature.substring(1, signature.length() - 1).split(",");
70+
for (String arg : args) {
71+
String p = arg.trim();
72+
if (p.endsWith("...")) {
73+
from++;
74+
to = Integer.MAX_VALUE;
75+
break;
76+
} else if (p.contains("=")) {
77+
to++;
78+
} else {
79+
from++;
80+
to++;
81+
}
82+
}
83+
84+
return new Range<>(from, to);
85+
}
86+
}

language/cypher/src/main/java/com/neueda/jetbrains/plugin/graphdb/language/cypher/editor/CypherParameterInfoHandler.java

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,12 @@
22

33
import com.intellij.codeInsight.lookup.LookupElement;
44
import com.intellij.lang.parameterInfo.*;
5-
import com.intellij.openapi.components.ServiceManager;
65
import com.intellij.psi.PsiElement;
76
import com.intellij.psi.tree.IElementType;
87
import com.intellij.psi.util.PsiTreeUtil;
98
import com.intellij.util.ArrayUtil;
109
import com.intellij.util.containers.ContainerUtil;
11-
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.CypherMetadataProviderService;
12-
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.atoms.CypherBuiltInFunctions;
13-
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherBuiltInFunctionElement;
14-
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherElementWithSignature.InvokableInformation;
15-
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherProcedureElement;
16-
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherUserFunctionElement;
17-
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherProcedureInvocation;
10+
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.InvokableInformation;
1811
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherTypes;
1912
import com.neueda.jetbrains.plugin.graphdb.language.cypher.references.CypherInvocation;
2013
import org.apache.commons.lang.StringUtils;
@@ -23,7 +16,6 @@
2316

2417
import java.util.List;
2518
import java.util.Objects;
26-
import java.util.Optional;
2719
import java.util.Set;
2820

2921
public class CypherParameterInfoHandler
@@ -135,26 +127,13 @@ public String getPresentation(CypherInvocation ci, @NotNull ParameterInfoUIConte
135127
return null;
136128
}
137129

138-
String signature;
139-
if (ci instanceof CypherProcedureInvocation) {
140-
signature = getMetadataService(ci).findProcedure(ci.getFullName())
141-
.map(CypherProcedureElement::getInvokableInformation)
142-
.map(InvokableInformation::getSignature)
143-
.orElse(null);
144-
} else {
145-
signature = CypherBuiltInFunctions.FUNCTIONS.stream()
146-
.filter(f -> Objects.equals(f.getInvokable().getName(), ci.getFullName()))
147-
.findFirst()
148-
.map(CypherBuiltInFunctionElement::getInvokable)
149-
.map(Optional::of)
150-
.orElseGet(() -> getMetadataService(ci).findUserFunction(ci.getFullName())
151-
.map(CypherUserFunctionElement::getInvokableInformation))
152-
.map(InvokableInformation::getSignature)
153-
.orElse(null);
154-
}
130+
String signature = ci.resolve()
131+
.map(InvokableInformation::getSignature)
132+
.orElse(null);
155133

156134
int current = context.getCurrentParameterIndex();
157135

136+
158137
if (signature == null || Objects.equals(signature, "()")) {
159138
context.setUIComponentEnabled(false);
160139
return null;
@@ -171,8 +150,4 @@ public String getPresentation(CypherInvocation ci, @NotNull ParameterInfoUIConte
171150
context.getDefaultParameterColor());
172151
}
173152

174-
private CypherMetadataProviderService getMetadataService(PsiElement element) {
175-
return ServiceManager.getService(element.getProject(), CypherMetadataProviderService.class);
176-
}
177-
178153
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.neueda.jetbrains.plugin.graphdb.language.cypher.inspections;
2+
3+
import com.intellij.codeInspection.LocalInspectionTool;
4+
import com.intellij.codeInspection.LocalInspectionToolSession;
5+
import com.intellij.codeInspection.ProblemsHolder;
6+
import com.intellij.psi.PsiElement;
7+
import com.intellij.psi.PsiElementVisitor;
8+
import com.intellij.util.Range;
9+
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.InvokableInformation;
10+
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherFunctionInvocation;
11+
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherProcedureInvocation;
12+
import com.neueda.jetbrains.plugin.graphdb.language.cypher.references.CypherInvocation;
13+
import org.jetbrains.annotations.NotNull;
14+
15+
public class CypherFunctionCallInspection extends LocalInspectionTool {
16+
17+
@NotNull
18+
@Override
19+
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder,
20+
boolean isOnTheFly,
21+
@NotNull LocalInspectionToolSession session) {
22+
return new PsiElementVisitor() {
23+
@Override
24+
public void visitElement(PsiElement element) {
25+
super.visitElement(element);
26+
if (element instanceof CypherInvocation) {
27+
CypherInvocation ci = (CypherInvocation) element;
28+
29+
// Skip functions built into grammar, since they are checked by parser
30+
if (!(ci instanceof CypherFunctionInvocation) && !(ci instanceof CypherProcedureInvocation)) {
31+
return;
32+
}
33+
34+
InvokableInformation invokable = ci.resolve().orElse(null);
35+
if (invokable == null) {
36+
return;
37+
}
38+
39+
int actual = ci.arguments().size();
40+
Range<Integer> expected = invokable.getArity();
41+
42+
if (expected.isWithin(actual)) {
43+
return;
44+
}
45+
46+
String tail = " arguments in call to " + ci.getFullName();
47+
if (actual < expected.getFrom()) {
48+
holder.registerProblem(ci.argumentsToken(), "not enough" + tail);
49+
} else if (actual > expected.getTo()) {
50+
holder.registerProblem(ci.argumentsToken(), "too many" + tail);
51+
}
52+
}
53+
}
54+
};
55+
}
56+
}

language/cypher/src/main/java/com/neueda/jetbrains/plugin/graphdb/language/cypher/references/CypherInvocation.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package com.neueda.jetbrains.plugin.graphdb.language.cypher.references;
22

33

4+
import com.intellij.openapi.components.ServiceManager;
45
import com.intellij.psi.PsiElement;
56
import com.intellij.psi.util.PsiTreeUtil;
7+
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.CypherMetadataProviderService;
8+
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.atoms.CypherBuiltInFunctions;
9+
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherBuiltInFunctionElement;
10+
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.InvokableInformation;
11+
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherProcedureElement;
12+
import com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.elements.CypherUserFunctionElement;
613
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherFunctionArguments;
714
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherFunctionInvocation;
815
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherProcedureInvocation;
916

1017
import java.util.Collections;
1118
import java.util.List;
19+
import java.util.Objects;
1220
import java.util.Optional;
1321

1422
public interface CypherInvocation extends PsiElement {
@@ -36,6 +44,26 @@ default PsiElement argumentsToken() {
3644
} else {
3745
return this;
3846
}
47+
48+
}
49+
50+
default Optional<InvokableInformation> resolve() {
51+
CypherMetadataProviderService svc = ServiceManager.getService(
52+
getProject(),
53+
CypherMetadataProviderService.class);
54+
55+
if (this instanceof CypherProcedureInvocation) {
56+
return svc.findProcedure(getFullName())
57+
.map(CypherProcedureElement::getInvokableInformation);
58+
}
59+
60+
return CypherBuiltInFunctions.FUNCTIONS.stream()
61+
.filter(f -> Objects.equals(f.getInvokable().getName(), getFullName()))
62+
.findFirst()
63+
.map(CypherBuiltInFunctionElement::getInvokable)
64+
.map(Optional::of)
65+
.orElseGet(() -> svc.findUserFunction(getFullName())
66+
.map(CypherUserFunctionElement::getInvokableInformation));
3967
}
4068

4169
String getFullName();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<html>
2+
<body>
3+
This inspection checks function and procedure calls.
4+
</body>
5+
</html>

0 commit comments

Comments
 (0)