Skip to content

Commit b7cd964

Browse files
committed
Static type checking
1 parent b7d958a commit b7cd964

32 files changed

+4889
-412
lines changed

src/main/java/nextflow/lsp/ast/ASTNodeStringUtils.java

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.stream.Stream;
2121

2222
import groovy.lang.groovydoc.Groovydoc;
23+
import groovy.transform.NamedParams;
2324
import nextflow.lsp.util.Logger;
2425
import nextflow.script.ast.AssignmentExpression;
2526
import nextflow.script.ast.FeatureFlagNode;
@@ -34,12 +35,9 @@
3435
import nextflow.script.dsl.DslScope;
3536
import nextflow.script.dsl.FeatureFlag;
3637
import nextflow.script.dsl.Namespace;
37-
import nextflow.script.dsl.Operator;
38-
import nextflow.script.dsl.OutputDsl;
3938
import nextflow.script.dsl.ProcessDsl;
4039
import nextflow.script.formatter.FormattingOptions;
4140
import nextflow.script.formatter.Formatter;
42-
import nextflow.script.types.TypeChecker;
4341
import nextflow.script.types.TypesEx;
4442
import org.codehaus.groovy.ast.AnnotatedNode;
4543
import org.codehaus.groovy.ast.ASTNode;
@@ -49,12 +47,14 @@
4947
import org.codehaus.groovy.ast.MethodNode;
5048
import org.codehaus.groovy.ast.Parameter;
5149
import org.codehaus.groovy.ast.Variable;
50+
import org.codehaus.groovy.ast.expr.ClassExpression;
5251
import org.codehaus.groovy.ast.expr.Expression;
5352
import org.codehaus.groovy.ast.expr.VariableExpression;
5453
import org.codehaus.groovy.ast.stmt.ExpressionStatement;
5554
import org.codehaus.groovy.runtime.StringGroovyMethods;
5655

5756
import static nextflow.script.ast.ASTUtils.*;
57+
import static nextflow.script.types.TypeCheckingUtils.*;
5858

5959
/**
6060
* Utility methods for retreiving text information for ast nodes.
@@ -82,6 +82,9 @@ public static String getLabel(ASTNode node) {
8282
if( node instanceof MethodNode mn )
8383
return methodToLabel(mn);
8484

85+
if( node instanceof Parameter param )
86+
return parameterToLabel(param);
87+
8588
if( node instanceof Variable var )
8689
return variableToLabel(var);
8790

@@ -150,15 +153,16 @@ private static String workflowToLabel(WorkflowNode node) {
150153

151154
private static void typedOutput(Expression output, Formatter fmt) {
152155
if( output instanceof AssignmentExpression assign ) {
153-
var target = (VariableExpression) assign.getLeftExpression();
154-
fmt.append(target.getText());
155-
if( fmt.hasType(target) ) {
156+
var target = assign.getLeftExpression();
157+
fmt.visit(target);
158+
var type = getType(target);
159+
if( fmt.hasType(type) ) {
156160
fmt.append(": ");
157-
fmt.visitTypeAnnotation(target.getType());
161+
fmt.visitTypeAnnotation(type);
158162
}
159163
}
160164
else {
161-
fmt.visit(output);
165+
fmt.visitTypeAnnotation(getType(output));
162166
}
163167
}
164168

@@ -258,7 +262,7 @@ private static String methodToLabel(MethodNode node) {
258262
if( TypesEx.isNamespace(node) )
259263
return "(namespace) " + name;
260264
var fn = new FieldNode(name, 0xF, node.getReturnType(), node.getDeclaringClass(), null);
261-
return variableToLabel(fn);
265+
return parameterToLabel(fn);
262266
}
263267

264268
var label = methodTypeLabel(node);
@@ -271,17 +275,18 @@ private static String methodToLabel(MethodNode node) {
271275
return builder.toString();
272276
}
273277

278+
var declaringType = node.getDeclaringClass();
274279
var builder = new StringBuilder();
275280
if( node instanceof FunctionNode ) {
276281
builder.append("def ");
277282
}
278-
else if( !isDslFunction(node) && !isNamespaceFunction(node) ) {
279-
builder.append(TypesEx.getName(node.getDeclaringClass()));
283+
else if( isDeclaringTypeVisible(declaringType) ) {
284+
builder.append(TypesEx.getName(declaringType));
280285
builder.append(' ');
281286
}
282287
else if( Logger.isDebugEnabled() ) {
283288
builder.append('[');
284-
builder.append(TypesEx.getName(node.getDeclaringClass()));
289+
builder.append(TypesEx.getName(declaringType));
285290
builder.append("] ");
286291
}
287292
builder.append(node.getName());
@@ -295,49 +300,63 @@ else if( Logger.isDebugEnabled() ) {
295300
return builder.toString();
296301
}
297302

298-
private static boolean isDslFunction(MethodNode mn) {
299-
return mn.getDeclaringClass().implementsInterface(ClassHelper.makeCached(DslScope.class));
300-
}
301-
302-
private static boolean isNamespaceFunction(MethodNode mn) {
303-
return mn.getDeclaringClass().implementsInterface(ClassHelper.makeCached(Namespace.class));
303+
private static boolean isDeclaringTypeVisible(ClassNode declaringType) {
304+
if( declaringType.implementsInterface(ClassHelper.makeCached(DslScope.class)) )
305+
return false;
306+
if( declaringType.implementsInterface(ClassHelper.makeCached(Namespace.class)) )
307+
return false;
308+
return true;
304309
}
305310

306311
private static String methodTypeLabel(MethodNode mn) {
307312
if( mn instanceof FunctionNode )
308313
return null;
309-
if( findAnnotation(mn, Operator.class).isPresent() )
310-
return "operator";
311314
var cn = mn.getDeclaringClass();
312315
if( cn.isPrimaryClassNode() )
313316
return null;
314317
var type = cn.getTypeClass();
315-
if( type == ProcessDsl.DirectiveDsl.class )
316-
return "process directive";
317318
if( type == ProcessDsl.InputDslV1.class )
318319
return "process input";
319320
if( type == ProcessDsl.OutputDslV1.class )
320321
return "process output";
321-
if( type == OutputDsl.class )
322-
return "output directive";
323-
if( type == OutputDsl.IndexDsl.class )
324-
return "output index directive";
325322
return null;
326323
}
327324

328325
public static String parametersToLabel(Parameter[] params) {
329-
return Stream.of(params)
330-
.map(param -> variableToLabel(param))
331-
.collect(Collectors.joining(", "));
326+
var hasNamedParams = params.length > 0 && findAnnotation(params[0], NamedParams.class).isPresent();
327+
var builder = new StringBuilder();
328+
for( int i = hasNamedParams ? 1 : 0; i < params.length; i++ ) {
329+
builder.append(parameterToLabel(params[i]));
330+
if( i < params.length - 1 || hasNamedParams )
331+
builder.append(", ");
332+
}
333+
if( hasNamedParams )
334+
builder.append("[options]");
335+
return builder.toString();
332336
}
333337

334-
private static String variableToLabel(Variable variable) {
338+
private static boolean isNamedParams(Parameter parameter) {
339+
return findAnnotation(parameter, NamedParams.class).isPresent();
340+
}
341+
342+
private static String parameterToLabel(Variable parameter) {
335343
var builder = new StringBuilder();
336-
builder.append(variable.getName());
337-
var type = TypeChecker.getType(variable);
344+
builder.append(parameter.getName());
345+
var type = parameter.getType();
338346
if( type.isArray() )
339347
builder.append("...");
340-
if( !ClassHelper.isObjectType(type) ) {
348+
if( !ClassHelper.isObjectType(type) || type.isGenericsPlaceHolder() ) {
349+
builder.append(": ");
350+
builder.append(TypesEx.getName(type));
351+
}
352+
return builder.toString();
353+
}
354+
355+
private static String variableToLabel(Variable variable) {
356+
var builder = new StringBuilder();
357+
builder.append(variable.getName());
358+
var type = getType(variable);
359+
if( !ClassHelper.isObjectType(type) || type.isGenericsPlaceHolder() ) {
341360
builder.append(": ");
342361
builder.append(TypesEx.getName(type));
343362
}
@@ -377,6 +396,9 @@ public static String getDocumentation(ASTNode node) {
377396
var result = groovydocToMarkdown(mn.getGroovydoc());
378397
if( result == null )
379398
result = annotationValueToMarkdown(mn);
399+
var namedParams = namedParams(mn.getParameters());
400+
if( namedParams != null )
401+
result = (result != null ? result : "") + namedParams;
380402
return result;
381403
}
382404

@@ -395,6 +417,30 @@ private static String annotationValueToMarkdown(AnnotatedNode node) {
395417
.orElse(null);
396418
}
397419

420+
private static String namedParams(Parameter[] parameters) {
421+
if( parameters.length == 0 )
422+
return null;
423+
var param = parameters[0];
424+
if( !TypesEx.isEqual(param.getType(), ClassHelper.MAP_TYPE) )
425+
return null;
426+
var namedParams = asNamedParams(param);
427+
if( namedParams.isEmpty() )
428+
return null;
429+
var builder = new StringBuilder();
430+
builder.append("\n\nAvailable options:\n");
431+
namedParams.forEach((name, an) -> {
432+
var namedParam = asNamedParam(an);
433+
builder.append("\n`");
434+
builder.append(name);
435+
if( !ClassHelper.isObjectType(namedParam.getType()) ) {
436+
builder.append(": ");
437+
builder.append(TypesEx.getName(namedParam.getType()));
438+
}
439+
builder.append("`\n");
440+
});
441+
return builder.toString();
442+
}
443+
398444
private static String groovydocToMarkdown(Groovydoc groovydoc) {
399445
if( groovydoc == null || !groovydoc.isPresent() )
400446
return null;

src/main/java/nextflow/lsp/ast/CompletionHelper.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.util.List;
2121

2222
import nextflow.script.dsl.Constant;
23-
import nextflow.script.types.TypeChecker;
2423
import nextflow.script.types.TypesEx;
2524
import org.codehaus.groovy.ast.ClassHelper;
2625
import org.codehaus.groovy.ast.ClassNode;
@@ -35,6 +34,7 @@
3534

3635
import static nextflow.lsp.ast.CompletionUtils.*;
3736
import static nextflow.script.ast.ASTUtils.*;
37+
import static nextflow.script.types.TypeCheckingUtils.*;
3838

3939
/**
4040
* Helper class for collecting completion proposals.
@@ -71,7 +71,7 @@ public boolean isIncomplete() {
7171
}
7272

7373
public void addItemsFromObjectScope(Expression object, String namePrefix) {
74-
ClassNode cn = TypeChecker.getType(object);
74+
ClassNode cn = TypesEx.normalize(getType(object));
7575
while( cn != null && !ClassHelper.isObjectType(cn) ) {
7676
var isStatic = object instanceof ClassExpression;
7777

@@ -105,7 +105,7 @@ public void addItemsFromObjectScope(Expression object, String namePrefix) {
105105
}
106106

107107
public void addMethodsFromObjectScope(Expression object, String namePrefix) {
108-
ClassNode cn = TypeChecker.getType(object);
108+
ClassNode cn = TypesEx.normalize(getType(object));
109109
while( cn != null && !ClassHelper.isObjectType(cn) ) {
110110
var isStatic = object instanceof ClassExpression;
111111

src/main/java/nextflow/lsp/ast/CompletionUtils.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import nextflow.script.ast.WorkflowNode;
2222
import nextflow.script.dsl.OutputDsl;
2323
import nextflow.script.dsl.ProcessDsl;
24-
import nextflow.script.types.TypeChecker;
2524
import nextflow.script.types.TypesEx;
2625
import org.codehaus.groovy.ast.ASTNode;
2726
import org.codehaus.groovy.ast.ClassNode;
@@ -35,6 +34,8 @@
3534
import org.eclipse.lsp4j.MarkupContent;
3635
import org.eclipse.lsp4j.MarkupKind;
3736

37+
import static nextflow.script.types.TypeCheckingUtils.*;
38+
3839
/**
3940
* Utility methods for retreiving completion information for ast nodes.
4041
*
@@ -94,7 +95,7 @@ else if( node instanceof MethodNode mn ) {
9495
result.setDescription(methodDescription(mn));
9596
}
9697
else if( node instanceof Variable variable ) {
97-
var type = TypeChecker.getType(variable);
98+
var type = getType(variable);
9899
result.setDescription(TypesEx.getName(type));
99100
}
100101
return result;

src/main/java/nextflow/lsp/ast/LanguageServerASTUtils.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,27 @@
1818
import java.util.Collections;
1919
import java.util.Iterator;
2020

21+
import nextflow.script.ast.ASTNodeMarker;
2122
import nextflow.script.ast.FeatureFlagNode;
2223
import nextflow.script.ast.IncludeEntryNode;
23-
import nextflow.script.types.TypeChecker;
24+
import nextflow.script.ast.ProcessNode;
25+
import nextflow.script.ast.WorkflowNode;
2426
import nextflow.script.types.Types;
2527
import org.codehaus.groovy.ast.ASTNode;
2628
import org.codehaus.groovy.ast.ClassNode;
29+
import org.codehaus.groovy.ast.FieldNode;
2730
import org.codehaus.groovy.ast.MethodNode;
31+
import org.codehaus.groovy.ast.Parameter;
2832
import org.codehaus.groovy.ast.Variable;
2933
import org.codehaus.groovy.ast.expr.ClassExpression;
3034
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
35+
import org.codehaus.groovy.ast.expr.MapEntryExpression;
3136
import org.codehaus.groovy.ast.expr.MethodCallExpression;
3237
import org.codehaus.groovy.ast.expr.PropertyExpression;
3338
import org.codehaus.groovy.ast.expr.VariableExpression;
3439

3540
import static nextflow.script.ast.ASTUtils.*;
41+
import static nextflow.script.types.TypeCheckingUtils.*;
3642

3743
/**
3844
* Utility methods for querying an AST.
@@ -51,18 +57,32 @@ public static ASTNode getDefinition(ASTNode node) {
5157
if( node instanceof VariableExpression ve )
5258
return getDefinitionFromVariable(ve.getAccessedVariable());
5359

54-
if( node instanceof MethodCallExpression mce )
55-
return TypeChecker.inferMethodTarget(mce);
60+
if( node instanceof MethodCallExpression mce ) {
61+
var mn = (MethodNode) mce.getNodeMetaData(ASTNodeMarker.METHOD_TARGET);
62+
if( mn != null )
63+
return mn;
64+
return resolveMethodCall(mce);
65+
}
5666

57-
if( node instanceof PropertyExpression pe )
58-
return TypeChecker.inferPropertyTarget(pe);
67+
if( node instanceof PropertyExpression pe ) {
68+
var fn = (FieldNode) pe.getNodeMetaData(ASTNodeMarker.PROPERTY_TARGET);
69+
if( fn != null )
70+
return fn;
71+
return resolveProperty(pe);
72+
}
5973

6074
if( node instanceof ClassExpression ce )
6175
return ce.getType().redirect();
6276

6377
if( node instanceof ConstructorCallExpression cce )
6478
return cce.getType().redirect();
6579

80+
if( node instanceof MapEntryExpression ) {
81+
var namedParam = (Parameter) node.getNodeMetaData("_NAMED_PARAM");
82+
if( namedParam != null )
83+
return namedParam;
84+
}
85+
6686
if( node instanceof FeatureFlagNode ffn )
6787
return ffn.target != null ? ffn : null;
6888

src/main/java/nextflow/lsp/services/SemanticTokensVisitor.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import nextflow.lsp.util.Positions;
2323
import nextflow.lsp.util.LanguageServerUtils;
24+
import nextflow.script.ast.ASTNodeMarker;
2425
import nextflow.script.dsl.Constant;
2526
import nextflow.script.parser.TokenPosition;
2627
import nextflow.script.types.TypesEx;
@@ -125,7 +126,8 @@ public SemanticTokens getTokens() {
125126
public void visitMethodCallExpression(MethodCallExpression node) {
126127
if( !node.isImplicitThis() )
127128
visit(node.getObjectExpression());
128-
append(node.getMethod(), SemanticTokenTypes.Function);
129+
if( node.getNodeMetaData(ASTNodeMarker.METHOD_TARGET) != null )
130+
append(node.getMethod(), SemanticTokenTypes.Function);
129131
visit(node.getArguments());
130132
}
131133

src/main/java/nextflow/lsp/services/config/ConfigSpecVisitor.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import nextflow.script.control.Phases;
2929
import nextflow.script.types.TypesEx;
3030
import org.codehaus.groovy.ast.ASTNode;
31+
import org.codehaus.groovy.ast.ClassHelper;
3132
import org.codehaus.groovy.control.SourceUnit;
3233
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
3334
import org.codehaus.groovy.control.messages.WarningMessage;
@@ -96,13 +97,13 @@ public void visitConfigAssign(ConfigAssignNode node) {
9697
return;
9798
}
9899
// validate type
99-
if( typeChecking ) {
100-
var expectedType = option.type();
101-
var actualType = node.value.getType().getTypeClass();
102-
if( expectedType != null && !TypesEx.isAssignableFrom(expectedType, actualType) ) {
103-
var message = "Type mismatch for config option '" + fqName + "' -- expected a " + TypesEx.getName(expectedType) + " but received a " + TypesEx.getName(actualType);
104-
addWarning(message, String.join(".", node.names), node.getLineNumber(), node.getColumnNumber());
105-
}
100+
if( !typeChecking )
101+
return;
102+
var expectedType = option.type() != null ? ClassHelper.makeCached(option.type()) : ClassHelper.dynamicType();
103+
var actualType = node.value.getType();
104+
if( !TypesEx.isAssignableFrom(expectedType, actualType) ) {
105+
var message = "Config option '" + fqName + "' with type " + TypesEx.getName(expectedType) + " cannot be assigned to value with type " + TypesEx.getName(actualType);
106+
addWarning(message, String.join(".", node.names), node.getLineNumber(), node.getColumnNumber());
106107
}
107108
}
108109

0 commit comments

Comments
 (0)