Skip to content

Commit 62d96a1

Browse files
committed
Report "soft errors" as warnings
Signed-off-by: Ben Sherman <bentshermann@gmail.com>
1 parent 17618f5 commit 62d96a1

File tree

9 files changed

+101
-30
lines changed

9 files changed

+101
-30
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import nextflow.lsp.util.Positions;
4242
import nextflow.script.control.ParanoidWarning;
4343
import nextflow.script.control.RelatedInformationAware;
44+
import nextflow.script.control.SeverityAware;
4445
import nextflow.script.formatter.FormattingOptions;
4546
import nextflow.util.PathUtils;
4647
import org.eclipse.lsp4j.CallHierarchyIncomingCall;
@@ -419,7 +420,10 @@ protected void publishDiagnostics(Set<URI> changedUris) {
419420
continue;
420421
}
421422

422-
var diagnostic = new Diagnostic(range, message, DiagnosticSeverity.Error, "nextflow");
423+
var severity = error instanceof SeverityAware sa && sa.isSoftError()
424+
? DiagnosticSeverity.Warning
425+
: DiagnosticSeverity.Error;
426+
var diagnostic = new Diagnostic(range, message, severity, "nextflow");
423427
if( error instanceof RelatedInformationAware ria )
424428
diagnostic.setRelatedInformation(relatedInformation(ria, uri));
425429
diagnostics.add(diagnostic);

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public void visitConfigAssign(ConfigAssignNode node) {
176176
.map(t -> ClassHelper.makeCached(t).getPlainNodeReference())
177177
.toList();
178178
var actualType = inferredType(node.value, names);
179-
if( !isAssignableFromAny(expectedTypes, actualType) ) {
179+
if( !isAnyAssignableFrom(expectedTypes, actualType) ) {
180180
var validTypes = expectedTypes.stream()
181181
.map(cn -> TypesEx.getName(cn))
182182
.collect(Collectors.joining(", "));
@@ -199,8 +199,8 @@ private ClassNode inferredType(Expression node, List<String> scopes) {
199199
return type;
200200
}
201201

202-
private boolean isAssignableFromAny(List<ClassNode> targetTypes, ClassNode sourceType) {
203-
if( targetTypes.isEmpty() )
202+
private boolean isAnyAssignableFrom(List<ClassNode> targetTypes, ClassNode sourceType) {
203+
if( targetTypes.isEmpty() || ClassHelper.isObjectType(sourceType) )
204204
return true;
205205
for( var targetType : targetTypes ) {
206206
if( TypesEx.isAssignableFrom(targetType, sourceType) )

src/main/java/nextflow/lsp/services/script/ResolvePluginIncludeVisitor.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import nextflow.script.ast.ScriptVisitorSupport;
2424
import nextflow.script.control.PhaseAware;
2525
import nextflow.script.control.Phases;
26+
import nextflow.script.control.SeverityAware;
2627
import org.codehaus.groovy.ast.ASTNode;
2728
import org.codehaus.groovy.ast.MethodNode;
2829
import org.codehaus.groovy.control.SourceUnit;
@@ -96,7 +97,7 @@ public void addError(String message, ASTNode node) {
9697
sourceUnit.getErrorCollector().addErrorAndContinue(errorMessage);
9798
}
9899

99-
private class ResolveIncludeError extends SyntaxException implements PhaseAware {
100+
private class ResolveIncludeError extends SyntaxException implements PhaseAware, SeverityAware {
100101

101102
public ResolveIncludeError(String message, ASTNode node) {
102103
super(message, node);
@@ -106,5 +107,10 @@ public ResolveIncludeError(String message, ASTNode node) {
106107
public int getPhase() {
107108
return Phases.INCLUDE_RESOLUTION;
108109
}
110+
111+
@Override
112+
public boolean isSoftError() {
113+
return true;
114+
}
109115
}
110116
}

src/main/java/nextflow/lsp/spec/PluginSpecCache.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ private static List<Map> pluginDefinitions(Map release) {
147147
public void setCurrentVersions(URI uri, List<PluginRef> refs) {
148148
if( uri.getPath() == null || !uri.getPath().endsWith("nextflow.config") )
149149
return;
150+
if( refs.isEmpty() )
151+
return;
150152
var parent = Path.of(uri).getParent();
151153
this.pluginsMap.put(parent, refs);
152154
}

src/main/java/nextflow/script/control/ReturnStatementVisitor.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public void addError(String message, ASTNode node) {
114114
sourceUnit.getErrorCollector().addErrorAndContinue(errorMessage);
115115
}
116116

117-
private class TypeError extends SyntaxException implements PhaseAware {
117+
private class TypeError extends SyntaxException implements PhaseAware, SeverityAware {
118118

119119
public TypeError(String message, ASTNode node) {
120120
super(message, node);
@@ -124,5 +124,10 @@ public TypeError(String message, ASTNode node) {
124124
public int getPhase() {
125125
return Phases.TYPE_CHECKING;
126126
}
127+
128+
@Override
129+
public boolean isSoftError() {
130+
return true;
131+
}
127132
}
128133
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2013-2026, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package nextflow.script.control;
17+
18+
/**
19+
* Interface used by errors that might be reported as
20+
* warnings by the language server.
21+
*
22+
* @author Ben Sherman <bentshermann@gmail.com>
23+
*/
24+
public interface SeverityAware {
25+
boolean isSoftError();
26+
}

src/main/java/nextflow/script/control/TypeCheckingVisitorEx.java

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -304,9 +304,7 @@ public void visitOutput(OutputNode node) {
304304
return;
305305
}
306306
var type = node.getType();
307-
var elementType = CHANNEL_TYPE.equals(type) || VALUE_TYPE.equals(type)
308-
? elementType(type)
309-
: type;
307+
var elementType = dataflowElementType(type);
310308
for( var stmt : asBlockStatements(node.body) ) {
311309
var call = asMethodCallX(stmt);
312310
if( checkPublishStatements(call, elementType) )
@@ -402,7 +400,7 @@ else if( target instanceof TupleExpression te )
402400
applyTupleAssignment(te, sourceType);
403401
}
404402
else {
405-
addError("Assignment target with type " + TypesEx.getName(targetType) + " cannot be assigned to value with type " + TypesEx.getName(sourceType), node);
403+
addSoftError("Assignment target with type " + TypesEx.getName(targetType) + " cannot be assigned to value with type " + TypesEx.getName(sourceType), node);
406404
}
407405
}
408406

@@ -502,7 +500,6 @@ else if( node.getNodeMetaData(ASTNodeMarker.METHOD_TARGET) instanceof MethodNode
502500
}
503501
else {
504502
checkArguments(parameters, arguments);
505-
506503
if( arguments.size() > 0 && arguments.get(0) instanceof MapExpression me )
507504
checkNamedParams(mn.getParameters()[0], me);
508505
}
@@ -513,7 +510,7 @@ else if( node.getNodeMetaData(ASTNodeMarker.METHOD_TARGET) instanceof MethodNode
513510
}
514511
}
515512
else if( experimental && node.getNodeMetaData(ASTNodeMarker.METHOD_OVERLOADS) != null ) {
516-
addError(String.format("Function `%s` (with multiple signatures) was called with incorrect number of arguments and/or incorrect argument types", node.getMethodAsString()), node.getMethod());
513+
addSoftError(String.format("Function `%s` (with multiple signatures) was called with incorrect number of arguments and/or incorrect argument types", node.getMethodAsString()), node.getMethod());
517514
}
518515
else if( experimental && !node.isImplicitThis() ) {
519516
var className = className(receiver);
@@ -609,10 +606,10 @@ private void checkArguments(Parameter[] parameters, List<Expression> arguments)
609606
.map(p -> p.getType())
610607
.toArray(ClassNode[]::new);
611608
var returnType = ClassHelper.dynamicType();
612-
addError("Closure with signature " + TypesEx.getName(parameterTypes, returnType) + " is not compatible with expected signature: " + TypesEx.getName(paramType), argument);
609+
addSoftError("Closure with signature " + TypesEx.getName(parameterTypes, returnType) + " is not compatible with expected signature: " + TypesEx.getName(paramType), argument);
613610
}
614611
else {
615-
addError("Argument with type " + TypesEx.getName(argType) + " is not compatible with parameter of type " + TypesEx.getName(paramType), argument);
612+
addSoftError("Argument with type " + TypesEx.getName(argType) + " is not compatible with parameter of type " + TypesEx.getName(paramType), argument);
616613
}
617614
}
618615
}
@@ -642,7 +639,7 @@ private void checkNamedParams(Parameter param, MapExpression args) {
642639
var namedParam = asNamedParam(namedParams.get(name));
643640
var argType = getType(value);
644641
if( !TypesEx.isAssignableFrom(namedParam.getType(), argType) )
645-
addError("Named param `" + name + "` expects a " + TypesEx.getName(namedParam.getType()) + " but received a " + TypesEx.getName(argType), value);
642+
addSoftError("Named param `" + name + "` expects a " + TypesEx.getName(namedParam.getType()) + " but received a " + TypesEx.getName(argType), value);
646643
entry.putNodeMetaData("_NAMED_PARAM", namedParam);
647644
}
648645
}
@@ -705,7 +702,7 @@ private boolean checkProcessCall(MethodCallExpression node) {
705702
var argType = getType(arguments.get(i));
706703
var elementType = dataflowElementType(argType);
707704
if( !TypesEx.isAssignableFrom(paramType, elementType) )
708-
addError("Argument with type " + TypesEx.getName(elementType) + " is not compatible with process input of type " + TypesEx.getName(paramType), arguments.get(i));
705+
addSoftError("Argument with type " + TypesEx.getName(elementType) + " is not compatible with process input of type " + TypesEx.getName(paramType), arguments.get(i));
709706
}
710707

711708
var numChannelArgs = arguments.stream().filter((arg) -> CHANNEL_TYPE.equals(getType(arg))).count();
@@ -861,7 +858,7 @@ public void visitBinaryExpression(BinaryExpression node) {
861858
var lhsOps = resolveOpsType(lhsType);
862859
var rhsOps = resolveOpsType(rhsType);
863860

864-
ClassNode resultType = null;
861+
ClassNode resultType = null;
865862
switch( op.getType() ) {
866863
case Types.POWER:
867864
resultType = resolveOpResultType(lhsType, rhsType, lhsOps, rhsOps, "power");
@@ -956,7 +953,7 @@ public void visitBinaryExpression(BinaryExpression node) {
956953
if( resultType != null )
957954
node.putNodeMetaData(ASTNodeMarker.INFERRED_TYPE, resultType);
958955
else
959-
addError(String.format("The `%s` operator is not defined for operands with types %s and %s", op.getText(), TypesEx.getName(lhsType), TypesEx.getName(rhsType)), node);
956+
addSoftError(String.format("The `%s` operator is not defined for operands with types %s and %s", op.getText(), TypesEx.getName(lhsType), TypesEx.getName(rhsType)), node);
960957
}
961958

962959
/**
@@ -1034,9 +1031,6 @@ private void applyConditionalExpression(TernaryExpression node) {
10341031
var trueType = getType(trueExpr);
10351032
var falseType = getType(falseExpr);
10361033

1037-
if( ClassHelper.isDynamicTyped(trueType) || ClassHelper.isDynamicTyped(falseType) )
1038-
return;
1039-
10401034
ClassNode resultType;
10411035
boolean nullable = true;
10421036
if( isNullConstant(trueExpr) && isNullConstant(falseExpr) ) {
@@ -1048,12 +1042,15 @@ else if( !isNullConstant(trueExpr) && isNullConstant(falseExpr) ) {
10481042
else if( isNullConstant(trueExpr) && !isNullConstant(falseExpr) ) {
10491043
resultType = falseType;
10501044
}
1045+
else if( ClassHelper.isDynamicTyped(trueType) || ClassHelper.isDynamicTyped(falseType) ) {
1046+
return;
1047+
}
10511048
else if( TypesEx.isEqual(trueType, falseType) ) {
10521049
resultType = trueType;
10531050
nullable = isNullable(trueType) || isNullable(falseType);
10541051
}
10551052
else {
1056-
addError(String.format("Conditional expression has inconsistent types -- true branch has type %s but false branch has type %s", TypesEx.getName(trueType), TypesEx.getName(falseType)), node);
1053+
addSoftError(String.format("Conditional expression has inconsistent types -- true branch has type %s but false branch has type %s", TypesEx.getName(trueType), TypesEx.getName(falseType)), node);
10571054
return;
10581055
}
10591056

@@ -1100,7 +1097,7 @@ public void visitListExpression(ListExpression node) {
11001097
elementType = type;
11011098
}
11021099
else if( !TypesEx.isEqual(elementType, type) ) {
1103-
addError(String.format("List expression has inconsistent element types -- some elements have type %s while others have type %s", TypesEx.getName(elementType), TypesEx.getName(type)), node);
1100+
addSoftError(String.format("List expression has inconsistent element types -- some elements have type %s while others have type %s", TypesEx.getName(elementType), TypesEx.getName(type)), node);
11041101
break;
11051102
}
11061103
}
@@ -1130,7 +1127,7 @@ public void visitRangeExpression(RangeExpression node) {
11301127
var rhsType = getType(rhs);
11311128

11321129
if( !TypesEx.isEqual(lhsType, rhsType) ) {
1133-
addError("Lower bound and upper bound of range expression should have the same type", node);
1130+
addSoftError("Lower bound and upper bound of range expression should have the same type", node);
11341131
node.putNodeMetaData(ASTNodeMarker.INFERRED_TYPE, ClassHelper.dynamicType());
11351132
return;
11361133
}
@@ -1144,7 +1141,7 @@ public void visitRangeExpression(RangeExpression node) {
11441141
node.putNodeMetaData(ASTNodeMarker.INFERRED_TYPE, resultType);
11451142
}
11461143
else {
1147-
addError("Range expression with elements of type " + TypesEx.getName(lhsType) + " is not supported", node);
1144+
addSoftError("Range expression with elements of type " + TypesEx.getName(lhsType) + " is not supported", node);
11481145
node.putNodeMetaData(ASTNodeMarker.INFERRED_TYPE, ClassHelper.dynamicType());
11491146
}
11501147
}
@@ -1182,7 +1179,7 @@ private void resolveUnaryOpOrFail(Expression operand, String op, String method,
11821179
if( resultType != null )
11831180
node.putNodeMetaData(ASTNodeMarker.INFERRED_TYPE, resultType);
11841181
else
1185-
addError(String.format("The `%s` operator is not defined for an operand with type %s", op, TypesEx.getName(type)), node);
1182+
addSoftError(String.format("The `%s` operator is not defined for an operand with type %s", op, TypesEx.getName(type)), node);
11861183
}
11871184

11881185
@Override
@@ -1198,7 +1195,7 @@ public void visitCastExpression(CastExpression node) {
11981195
return;
11991196
var opsType = resolveOpsType(targetType);
12001197
if( resolveOpResultType(sourceType, opsType, "ofType") == null )
1201-
addError(String.format("Value of type %s cannot be cast to %s", TypesEx.getName(sourceType), TypesEx.getName(targetType)), node);
1198+
addSoftError(String.format("Value of type %s cannot be cast to %s", TypesEx.getName(sourceType), TypesEx.getName(targetType)), node);
12021199
}
12031200

12041201
private boolean checkRecordCast(ClassNode targetType, ClassNode sourceType, ASTNode node) {
@@ -1322,15 +1319,33 @@ public void addError(String message, ASTNode node) {
13221319
sourceUnit.getErrorCollector().addErrorAndContinue(errorMessage);
13231320
}
13241321

1325-
private class TypeError extends SyntaxException implements PhaseAware {
1322+
public void addSoftError(String message, ASTNode node) {
1323+
var cause = new TypeError(message, node, true);
1324+
var errorMessage = new SyntaxErrorMessage(cause, sourceUnit);
1325+
sourceUnit.getErrorCollector().addErrorAndContinue(errorMessage);
1326+
}
13261327

1327-
public TypeError(String message, ASTNode node) {
1328+
private class TypeError extends SyntaxException implements PhaseAware, SeverityAware {
1329+
1330+
boolean softError;
1331+
1332+
public TypeError(String message, ASTNode node, boolean softError) {
13281333
super(message, node);
1334+
this.softError = softError;
1335+
}
1336+
1337+
public TypeError(String message, ASTNode node) {
1338+
this(message, node, false);
13291339
}
13301340

13311341
@Override
13321342
public int getPhase() {
13331343
return Phases.TYPE_CHECKING;
13341344
}
1345+
1346+
@Override
1347+
public boolean isSoftError() {
1348+
return softError;
1349+
}
13351350
}
13361351
}

src/test/groovy/nextflow/script/types/TypeCheckingTest.groovy

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,19 @@ class TypeCheckingTest extends Specification {
375375
"true ? 42 : null" | null
376376
}
377377

378+
def 'should resolve a ternary expression' () {
379+
when:
380+
def exp = parseExpression(
381+
'''
382+
workflow {
383+
true ? 42 : null
384+
}
385+
'''
386+
)
387+
then:
388+
checkType(exp, Integer)
389+
}
390+
378391
def 'should check an elvis expression' () {
379392
when:
380393
def exp = parseExpression(

src/test/groovy/nextflow/script/types/TypesTest.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class TypesTest extends Specification {
7272
when:
7373
param = cn.getDeclaredMethods('reduce')[0].getParameters().last()
7474
then:
75-
TypesEx.getName(param.getType()) == '(R, E) -> R'
75+
TypesEx.getName(param.getType()) in ['(E, E) -> E', '(R, E) -> R']
7676
}
7777

7878
def 'should determine whether a type is assignable to another type' () {

0 commit comments

Comments
 (0)