Skip to content

Commit

Permalink
Add unit tests
Browse files Browse the repository at this point in the history
Signed-off-by: Ben Sherman <[email protected]>
  • Loading branch information
bentsherman committed Oct 8, 2024
1 parent 5439df7 commit bba88f4
Show file tree
Hide file tree
Showing 6 changed files with 399 additions and 32 deletions.
58 changes: 27 additions & 31 deletions src/main/groovy/nextflow/lsp/ast/ASTNodeStringUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@
*/
package nextflow.lsp.ast;

import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import groovy.lang.groovydoc.Groovydoc;
import nextflow.lsp.ast.ASTNodeCache;
import nextflow.lsp.ast.ASTUtils;
import nextflow.lsp.services.util.FormattingOptions;
import nextflow.lsp.services.util.Formatter;
import nextflow.script.dsl.Constant;
Expand All @@ -39,7 +36,6 @@
import nextflow.script.v2.ProcessNode;
import nextflow.script.v2.WorkflowNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
Expand All @@ -59,9 +55,9 @@
*/
public class ASTNodeStringUtils {

public static String getLabel(ASTNode node, ASTNodeCache ast) {
public static String getLabel(ASTNode node) {
if( node instanceof ClassNode cn )
return classToLabel(cn, ast);
return classToLabel(cn);

if( node instanceof FeatureFlagNode ffn )
return featureFlagToLabel(ffn);
Expand All @@ -73,20 +69,20 @@ public static String getLabel(ASTNode node, ASTNodeCache ast) {
return processToLabel(pn);

if( node instanceof MethodNode mn )
return methodToLabel(mn, ast);
return methodToLabel(mn);

if( node instanceof Variable var )
return variableToLabel(var, ast);
return variableToLabel(var);

return null;
}

private static String classToLabel(ClassNode node, ASTNodeCache ast) {
private static String classToLabel(ClassNode node) {
var builder = new StringBuilder();
if( node.isEnum() )
builder.append("enum ");
else
builder.append("class ");
builder.append("type ");
builder.append(node.getNameWithoutPackage());
return builder.toString();
}
Expand All @@ -98,17 +94,17 @@ private static String featureFlagToLabel(FeatureFlagNode node) {
return builder.toString();
}

private static String workflowToLabel(WorkflowNode wn) {
if( wn.isEntry() )
private static String workflowToLabel(WorkflowNode node) {
if( node.isEntry() )
return "workflow <entry>";
var fmt = new Formatter(new FormattingOptions(2, true, false));
fmt.append("workflow ");
fmt.append(wn.getName());
fmt.append(node.getName());
fmt.append(" {\n");
fmt.incIndent();
fmt.appendIndent();
fmt.append("take:\n");
var takes = asBlockStatements(wn.takes);
var takes = asBlockStatements(node.takes);
if( takes.isEmpty() ) {
fmt.appendIndent();
fmt.append("<none>\n");
Expand All @@ -121,7 +117,7 @@ private static String workflowToLabel(WorkflowNode wn) {
fmt.appendNewLine();
fmt.appendIndent();
fmt.append("emit:\n");
var emits = asBlockStatements(wn.emits);
var emits = asBlockStatements(node.emits);
if( emits.isEmpty() ) {
fmt.appendIndent();
fmt.append("<none>\n");
Expand All @@ -140,19 +136,19 @@ private static String workflowToLabel(WorkflowNode wn) {
return fmt.toString();
}

private static String processToLabel(ProcessNode pn) {
private static String processToLabel(ProcessNode node) {
var fmt = new Formatter(new FormattingOptions(2, true, false));
fmt.append("process ");
fmt.append(pn.getName());
fmt.append(node.getName());
fmt.append(" {\n");
fmt.incIndent();
fmt.appendIndent();
fmt.append("input:\n");
if( asDirectives(pn.inputs).count() == 0 ) {
if( asDirectives(node.inputs).count() == 0 ) {
fmt.appendIndent();
fmt.append("<none>\n");
}
asDirectives(pn.inputs).forEach((call) -> {
asDirectives(node.inputs).forEach((call) -> {
fmt.appendIndent();
fmt.append(call.getMethodAsString());
fmt.append(' ');
Expand All @@ -162,11 +158,11 @@ private static String processToLabel(ProcessNode pn) {
fmt.appendNewLine();
fmt.appendIndent();
fmt.append("output:\n");
if( asDirectives(pn.outputs).count() == 0 ) {
if( asDirectives(node.outputs).count() == 0 ) {
fmt.appendIndent();
fmt.append("<none>\n");
}
asDirectives(pn.outputs).forEach((call) -> {
asDirectives(node.outputs).forEach((call) -> {
fmt.appendIndent();
fmt.append(call.getMethodAsString());
fmt.append(' ');
Expand All @@ -178,7 +174,7 @@ private static String processToLabel(ProcessNode pn) {
return fmt.toString();
}

private static String methodToLabel(MethodNode node, ASTNodeCache ast) {
private static String methodToLabel(MethodNode node) {
var label = getMethodTypeLabel(node);
if( label != null ) {
var builder = new StringBuilder();
Expand All @@ -196,7 +192,7 @@ private static String methodToLabel(MethodNode node, ASTNodeCache ast) {
}
builder.append(node.getName());
builder.append('(');
builder.append(parametersToLabel(node.getParameters(), ast));
builder.append(parametersToLabel(node.getParameters()));
builder.append(')');
var returnType = node.getReturnType();
if( !ClassHelper.OBJECT_TYPE.equals(returnType) && !ClassHelper.VOID_TYPE.equals(returnType) ) {
Expand Down Expand Up @@ -226,17 +222,17 @@ private static String getMethodTypeLabel(MethodNode mn) {
return null;
}

private static String parametersToLabel(Parameter[] params, ASTNodeCache ast) {
private static String parametersToLabel(Parameter[] params) {
return Stream.of(params)
.map(param -> variableToLabel(param, ast))
.map(param -> variableToLabel(param))
.collect(Collectors.joining(", "));
}

private static String variableToLabel(Variable variable, ASTNodeCache ast) {
private static String variableToLabel(Variable variable) {
var builder = new StringBuilder();
builder.append(variable.getName());
var type = variable instanceof ASTNode node
? ASTUtils.getTypeOfNode(node, ast)
var type = variable.getOriginType() != null
? variable.getOriginType()
: variable.getType();
if( type.isArray() )
builder.append("...");
Expand All @@ -253,15 +249,15 @@ public static String getDocumentation(ASTNode node) {
return annotationValueToMarkdown(an, FeatureFlag.class, "description");
}

if( node instanceof WorkflowNode wn )
return groovydocToMarkdown(wn.getGroovydoc());

if( node instanceof FunctionNode fn )
return groovydocToMarkdown(fn.getGroovydoc());

if( node instanceof ProcessNode pn )
return groovydocToMarkdown(pn.getGroovydoc());

if( node instanceof WorkflowNode wn )
return groovydocToMarkdown(wn.getGroovydoc());

if( node instanceof ClassNode cn ) {
var result = groovydocToMarkdown(cn.getGroovydoc());
if( result == null )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public Hover hover(TextDocumentIdentifier textDocument, Position position) {

var builder = new StringBuilder();

var label = ASTNodeStringUtils.getLabel(defNode, ast);
var label = ASTNodeStringUtils.getLabel(defNode);
if( label != null ) {
builder.append("```nextflow\n");
builder.append(label);
Expand Down
185 changes: 185 additions & 0 deletions src/test/groovy/nextflow/lsp/ast/ASTNodeStringUtilsTest.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* Copyright 2013-2024, Seqera Labs
*
* 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 nextflow.lsp.ast

import groovy.lang.groovydoc.Groovydoc
import nextflow.script.dsl.FeatureFlagDsl
import nextflow.script.dsl.ProcessDirectiveDsl
import nextflow.script.types.Channel
import nextflow.script.v2.FeatureFlagNode
import nextflow.script.v2.FunctionNode
import nextflow.script.v2.ProcessNode
import nextflow.script.v2.WorkflowNode
import org.codehaus.groovy.ast.ClassHelper
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.Parameter
import org.codehaus.groovy.ast.stmt.EmptyStatement
import spock.lang.Specification

/**
*
* @author Ben Sherman <[email protected]>
*/
class ASTNodeStringUtilsTest extends Specification {

def 'should get the label and docs for a type' () {
when:
def classNode = Mock(ClassNode) {
isEnum() >> false
getNameWithoutPackage() >> 'Channel'
}
then:
ASTNodeStringUtils.getLabel(classNode) == 'type Channel'

when:
def enumNode = Mock(ClassNode) {
isEnum() >> true
getNameWithoutPackage() >> 'SequenceTypes'
getGroovydoc() >> Mock(Groovydoc) {
isPresent() >> true
getContent() >> '''\
/**
* Enumeration of sequence types.
*/
'''.stripIndent(true)
}
}
then:
ASTNodeStringUtils.getLabel(enumNode) == 'enum SequenceTypes'
ASTNodeStringUtils.getDocumentation(enumNode) == 'Enumeration of sequence types.'
}

def 'should get the label and docs for a feature flag' () {
when:
def ffn = new FeatureFlagNode('nextflow.enable.strict', null)
ffn.target = new ClassNode(FeatureFlagDsl.class).getDeclaredField('strict')

then:
ASTNodeStringUtils.getLabel(ffn) == '(feature flag) nextflow.enable.strict'
ASTNodeStringUtils.getDocumentation(ffn) == 'When `true`, the pipeline is executed in [strict mode](https://nextflow.io/docs/latest/reference/feature-flags.html).'
}

def 'should get the label and docs for a workflow' () {
when:
def entry = Mock(WorkflowNode) {
isEntry() >> true
}
then:
ASTNodeStringUtils.getLabel(entry) == 'workflow <entry>'

when:
def node = Mock(WorkflowNode) {
isEntry() >> false
getName() >> 'FOO'
takes >> EmptyStatement.INSTANCE
emits >> EmptyStatement.INSTANCE
getGroovydoc() >> Mock(Groovydoc) {
isPresent() >> true
getContent() >> '''\
/**
* Run the FOO workflow.
*/
'''.stripIndent(true)
}
}
then:
ASTNodeStringUtils.getLabel(node) == '''
workflow FOO {
take:
<none>
emit:
<none>
}
'''.stripIndent(true).trim()
ASTNodeStringUtils.getDocumentation(node) == 'Run the FOO workflow.'
}

def 'should get the label and docs for a process' () {
when:
def node = Mock(ProcessNode) {
getName() >> 'BAR'
inputs >> EmptyStatement.INSTANCE
outputs >> EmptyStatement.INSTANCE
getGroovydoc() >> Mock(Groovydoc) {
isPresent() >> true
getContent() >> '''\
/**
* Run the BAR process.
*/
'''.stripIndent(true)
}
}
then:
ASTNodeStringUtils.getLabel(node) == '''
process BAR {
input:
<none>
output:
<none>
}
'''.stripIndent(true).trim()
ASTNodeStringUtils.getDocumentation(node) == 'Run the BAR process.'
}

def 'should get the label and docs for a function' () {
when:
def node = Spy(new FunctionNode(
'sayHello',
ClassHelper.OBJECT_TYPE,
[ new Parameter(ClassHelper.OBJECT_TYPE, 'message'), new Parameter(ClassHelper.OBJECT_TYPE, 'target') ] as Parameter[],
null
))
node.getGroovydoc() >> Mock(Groovydoc) {
isPresent() >> true
getContent() >> '''\
/**
* Say hello to someone.
*/
'''.stripIndent(true)
}
then:
ASTNodeStringUtils.getLabel(node) == 'sayHello(message, target)'
ASTNodeStringUtils.getDocumentation(node) == 'Say hello to someone.'
}

def 'should get the label and docs for a built-in function' () {
when:
def node = new ClassNode(Channel.class).getDeclaredMethods('of').first()
then:
ASTNodeStringUtils.getLabel(node) == 'Channel.of(arg0...: T) -> Channel<T>'
ASTNodeStringUtils.getDocumentation(node) == '''
Create a channel that emits each argument.
[Read more](https://nextflow.io/docs/latest/reference/channel.html#of)
'''.stripIndent(true).trim()
}

def 'should get the label and docs for a process directive' () {
when:
def node = new ClassNode(ProcessDirectiveDsl.class).getDeclaredMethods('executor').first()
then:
ASTNodeStringUtils.getLabel(node) == '(process directive) executor'
ASTNodeStringUtils.getDocumentation(node) == '''
The `executor` defines the underlying system where tasks are executed.
[Read more](https://nextflow.io/docs/latest/reference/process.html#executor)
'''.stripIndent(true).trim()
}

}
Loading

0 comments on commit bba88f4

Please sign in to comment.