diff --git a/modules/nf-lang/src/main/antlr/ScriptLexer.g4 b/modules/nf-lang/src/main/antlr/ScriptLexer.g4 index 1861bf7278..aa9057ca92 100644 --- a/modules/nf-lang/src/main/antlr/ScriptLexer.g4 +++ b/modules/nf-lang/src/main/antlr/ScriptLexer.g4 @@ -54,6 +54,10 @@ options { superClass = AbstractLexer; } +channels { + COMMENT // The COMMENT channel will contain all comments, and are separatly introduced into the AST during creation +} + @header { package nextflow.script.parser; @@ -830,18 +834,18 @@ NL : LineTerminator /* { this.ignoreTokenInsideParens(); } */ // Multiple-line comments (including groovydoc comments) ML_COMMENT - : '/*' .*? '*/' /* { this.ignoreMultiLineCommentConditionally(); } */ -> type(NL) + : '/*' .*? '*/' /* { this.ignoreMultiLineCommentConditionally(); } */ -> channel(COMMENT) ; // Single-line comments SL_COMMENT - : '//' ~[\r\n\uFFFF]* /* { this.ignoreTokenInsideParens(); } */ -> type(NL) + : '//' ~[\r\n\uFFFF]* /* { this.ignoreTokenInsideParens(); } */ -> channel(COMMENT) ; // Script-header comments. // The very first characters of the file may be "#!". If so, ignore the first line. SH_COMMENT - : '#!' { require(errorIgnored || 0 == this.tokenIndex, "Shebang comment should appear at the first line", -2, true); } ShCommand (LineTerminator '#!' ShCommand)* -> type(NL) + : '#!' { require(errorIgnored || 0 == this.tokenIndex, "Shebang comment should appear at the first line", -2, true); } ShCommand (LineTerminator '#!' ShCommand)* -> channel(COMMENT) ; // Unexpected characters will be handled by groovy parser later. diff --git a/modules/nf-lang/src/main/antlr/ScriptParser.g4 b/modules/nf-lang/src/main/antlr/ScriptParser.g4 index f35740d7b9..26a4501d1f 100644 --- a/modules/nf-lang/src/main/antlr/ScriptParser.g4 +++ b/modules/nf-lang/src/main/antlr/ScriptParser.g4 @@ -211,7 +211,7 @@ processWhen ; processExec - : (SCRIPT | SHELL | EXEC) COLON nls blockStatements + : execType=(SCRIPT | SHELL | EXEC) COLON nls blockStatements ; processStub diff --git a/modules/nf-lang/src/main/java/nextflow/script/ast/WorkflowNode.java b/modules/nf-lang/src/main/java/nextflow/script/ast/WorkflowNode.java index 9e3453488d..a53de1b284 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/ast/WorkflowNode.java +++ b/modules/nf-lang/src/main/java/nextflow/script/ast/WorkflowNode.java @@ -45,7 +45,14 @@ public class WorkflowNode extends MethodNode { public final Statement publishers; public WorkflowNode(String name, Statement takes, Statement main, Statement emits, Statement publishers) { - super(name, 0, dummyReturnType(emits), dummyParams(takes), ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE); + super( + name != null ? name : "", // getText causes an exception if name is null, ugly hack for now + 0, + dummyReturnType(emits), + dummyParams(takes), + ClassNode.EMPTY_ARRAY, + EmptyStatement.INSTANCE + ); this.takes = takes; this.main = main; this.emits = emits; @@ -53,7 +60,7 @@ public WorkflowNode(String name, Statement takes, Statement main, Statement emit } public boolean isEntry() { - return getName() == null; + return getName() == ""; } public boolean isCodeSnippet() { diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/Compiler.java b/modules/nf-lang/src/main/java/nextflow/script/control/Compiler.java index fce9af3c2d..7268ef80e9 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/control/Compiler.java +++ b/modules/nf-lang/src/main/java/nextflow/script/control/Compiler.java @@ -99,8 +99,10 @@ public void compile(SourceUnit source) { source.buildAST(); } catch( RecognitionException e ) { + System.err.println("RecognitionException: " + e.getMessage()); } catch( CompilationFailedException e ) { + System.err.println("CompilationFailedException: " + e.getMessage()); } } diff --git a/modules/nf-lang/src/main/java/nextflow/script/formatter/Formatter.java b/modules/nf-lang/src/main/java/nextflow/script/formatter/Formatter.java index c907c2bb72..595e09064f 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/formatter/Formatter.java +++ b/modules/nf-lang/src/main/java/nextflow/script/formatter/Formatter.java @@ -15,10 +15,13 @@ */ package nextflow.script.formatter; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import nextflow.script.ast.ASTNodeMarker; +import nextflow.script.parser.CommentWriter; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.ClassNode; @@ -60,6 +63,7 @@ import org.codehaus.groovy.ast.stmt.TryCatchStatement; import org.codehaus.groovy.runtime.DefaultGroovyMethods; import org.codehaus.groovy.syntax.Types; +import org.stringtemplate.v4.compiler.STParser.compoundElement_return; import static nextflow.script.ast.ASTUtils.*; @@ -72,12 +76,24 @@ public class Formatter extends CodeVisitorSupport { private FormattingOptions options; + private CommentWriter commentWriter; + private StringBuilder builder = new StringBuilder(); private int indentCount = 0; public Formatter(FormattingOptions options) { this.options = options; + this.commentWriter = null; + } + + public Formatter(FormattingOptions options, CommentWriter commentWriter) { + this.options = options; + this.commentWriter = commentWriter; + } + + public boolean exit() { + return commentWriter.verifyAllWritten(); } public void append(char c) { @@ -88,46 +104,225 @@ public void append(String str) { builder.append(str); } - public void appendIndent() { + private char peekBuilder() { + return builder.charAt(builder.length() - 1); + } + + public void appendSpace() { + if (Character.isWhitespace(peekBuilder())) { + // We never want to append double space, or a space directly after an indent of a newline + return; + } else { + append(' '); + } + } + + public void appendPadding(int padding) { + append(" ".repeat(padding)); + } + + public void appendIndent() {} + + public void _appendIndent() { var str = options.insertSpaces() ? " ".repeat(options.tabSize() * indentCount) : "\t".repeat(indentCount); builder.append(str); } - public void appendNewLine() { + boolean isFirst = true; + private void appendNL() { + if (isFirst) { + isFirst = false; + return; + } builder.append('\n'); } - public void appendLeadingComments(ASTNode node) { - var comments = (List) node.getNodeMetaData(ASTNodeMarker.LEADING_COMMENTS); - if( comments == null || comments.isEmpty() ) - return; + private void appendNLs(int i) { + builder.append("\n".repeat(i)); + } - for( var line : DefaultGroovyMethods.asReversed(comments) ) { - if( "\n".equals(line) ) { - append(line); - } - else { - appendIndent(); - append(line.stripLeading()); - } + public void appendNewLine() { + appendNL(); + _appendIndent(); + } + + public void appendBlanks(int i) { + appendNLs(i); + } + + /* + * Write a single comment while keeping track of breaking lines and WS before and after + */ + public boolean writeComment( + CommentWriter.Comment comment, + boolean newlineBefore, + boolean newlineAfter, + boolean makeWSBefore, + boolean makeWSAfter + ) { + var commentAndIsSLC = comment.write(); + String commentText = commentAndIsSLC.getV1(); + boolean isSLC = commentAndIsSLC.getV2(); + + if( newlineBefore ) { + appendNewLine(); + } else if( makeWSBefore ) { + appendSpace(); } + + append(commentText); + + if( isSLC || newlineAfter ) { + return true; + } + + if( makeWSAfter ) { + appendSpace(); + } + + return false; + } + + public void writeComments( + Map> comments, + String key, + boolean newlineBefore, + boolean makeWSBefore, + boolean makeWSAfter, + boolean MLCOnOwnLines, + boolean inExpression + ) { + if (comments.containsKey(key)) { + writeComments( + comments.get(key), + newlineBefore, + makeWSBefore, + makeWSAfter, + MLCOnOwnLines, + inExpression + ); + } + } + + public void writeComments( + List comments, + boolean newlineBefore, + boolean makeWSBefore, + boolean makeWSAfter, + boolean MLCOnOwnLines, + boolean inExpression + ) { + if (comments.isEmpty()) return; + /* + * These are are the four cases: + * - WSBefore && WSAfter: + * The first comment writes WS before and after, + * the rest are responsible for writing the WS after + * + * - WSBefore = true, WSAfter = false: + * Each comment should only write WS before + * + * - WSBefore = false, WSAfter = true: + * Each comment should only write WS after + * + * + * - WSBefore = false, WSAfter = false: + * The first comment writes no whitespace at all (if it is not alone), + * the rest of the comment only write WS before + */ + Iterator it = comments.iterator(); + var breakNext = writeComment( + it.next(), newlineBefore, MLCOnOwnLines, makeWSBefore, makeWSAfter + ); + while( it.hasNext() ) + breakNext = writeComment( + it.next(), breakNext, MLCOnOwnLines, !makeWSAfter, makeWSAfter + ); + + if (breakNext && inExpression) { + // If we inside an expression where the next token will not break the line + // we are required to break the line here already + appendNewLine(); + } + } + + public void appendLeadingComments( + ASTNode node, + boolean newlineBefore, + boolean makeWSBefore, + boolean makeWSAfter, + boolean MLCOnOwnLines, + boolean inExpression + ) { + var comments = commentWriter.getLeadingComments(node); + writeComments( + comments, + newlineBefore, + makeWSBefore, + makeWSAfter, + MLCOnOwnLines, + inExpression + ); } - public boolean hasTrailingComment(ASTNode node) { - var comment = (String) node.getNodeMetaData(ASTNodeMarker.TRAILING_COMMENT); - return comment != null; + public void appendLeadingComments(ASTNode node) { + appendLeadingComments(node, true, false, false, true, false); } - public void appendTrailingComment(ASTNode node) { - var comment = (String) node.getNodeMetaData(ASTNodeMarker.TRAILING_COMMENT); - if( comment != null ) { - append(' '); - append(comment); + public void appendLeadingCommentsExpression(ASTNode node) { + appendLeadingComments(node, false, false, true, false, true); + + } + + public void appendLeadingCommentsStatement(ASTNode node) { + appendLeadingComments(node, true, false, false, true, false); + } + + public void appendTrailingComments( + ASTNode node, + boolean newlineBefore, + boolean makeWSBefore, + boolean makeWSAfter, + boolean MLCOnOwnLines, + boolean inExpression + ) { + var comments = commentWriter.getTrailingComments(node); + if (comments.containsKey("STANDARD")) { + writeComments( + comments.get("STANDARD"), + newlineBefore, + makeWSBefore, + makeWSAfter, + MLCOnOwnLines, + inExpression + ); } } + public void appendTrailingInside( + Map> comments, + String key + ) { + writeComments(comments, key, false, true, true, false, true); + } + + public void appendWithinComments( + Map> comments, + String key + ) { + writeComments(comments, key, false, true, false, false, true); + } + + public void appendTrailingComments(ASTNode node) { + appendTrailingComments(node, false, true, false, false, false); + } + + public void appendTrailingCommentsExpression(ASTNode node) { + appendTrailingComments(node, false, true, true, false, true); + } + public void incIndent() { indentCount++; } @@ -149,29 +344,35 @@ public void visitIfElse(IfStatement node) { protected void visitIfElse(IfStatement node, boolean preIndent) { appendLeadingComments(node); - if( preIndent ) - appendIndent(); - append("if ("); + appendNewLine(); + append("if"); + appendSpace(); + append('('); visit(node.getBooleanExpression()); - append(") {\n"); + append(')'); + appendSpace(); + append("{"); incIndent(); visit(node.getIfBlock()); decIndent(); - appendIndent(); - append("}\n"); + appendNewLine(); + append("}"); if( node.getElseBlock() instanceof IfStatement is ) { - appendIndent(); - append("else "); + appendNewLine(); + append("else"); + appendSpace(); visitIfElse(is, false); } else if( !(node.getElseBlock() instanceof EmptyStatement) ) { - appendIndent(); - append("else {\n"); + appendNewLine(); + append("else"); + appendSpace(); + append('{'); incIndent(); visit(node.getElseBlock()); decIndent(); - appendIndent(); - append("}\n"); + appendNewLine(); + append("}"); } } @@ -182,10 +383,10 @@ public void visitExpressionStatement(ExpressionStatement node) { var cre = currentRootExpr; currentRootExpr = node.getExpression(); appendLeadingComments(node); - appendIndent(); + appendNewLine(); visitStatementLabels(node); visit(node.getExpression()); - appendNewLine(); + appendTrailingComments(node); currentRootExpr = cre; } @@ -194,7 +395,8 @@ private void visitStatementLabels(ExpressionStatement node) { return; for( var label : DefaultGroovyMethods.asReversed(node.getStatementLabels()) ) { append(label); - append(": "); + append(":"); + appendSpace(); } } @@ -203,36 +405,41 @@ public void visitReturnStatement(ReturnStatement node) { var cre = currentRootExpr; currentRootExpr = node.getExpression(); appendLeadingComments(node); - appendIndent(); - append("return "); - visit(node.getExpression()); appendNewLine(); + append("return"); + appendSpace(); + visit(node.getExpression()); currentRootExpr = cre; } @Override public void visitAssertStatement(AssertStatement node) { appendLeadingComments(node); - appendIndent(); - append("assert "); + appendNewLine(); + append("assert"); + appendSpace(); visit(node.getBooleanExpression()); if( !(node.getMessageExpression() instanceof ConstantExpression ce && ce.isNullExpression()) ) { - append(" : "); + appendSpace(); + append(":"); + appendSpace(); visit(node.getMessageExpression()); } - appendNewLine(); } @Override public void visitTryCatchFinally(TryCatchStatement node) { appendLeadingComments(node); - appendIndent(); - append("try {\n"); + appendNewLine(); + append("try"); + appendSpace(); + append('{'); incIndent(); + appendNewLine(); visit(node.getTryStatement()); decIndent(); - appendIndent(); - append("}\n"); + appendNewLine(); + append("}"); for( var catchStatement : node.getCatchStatements() ) { visit(catchStatement); } @@ -241,32 +448,35 @@ public void visitTryCatchFinally(TryCatchStatement node) { @Override public void visitThrowStatement(ThrowStatement node) { appendLeadingComments(node); - appendIndent(); - append("throw "); - visit(node.getExpression()); appendNewLine(); + append("throw"); + appendSpace(); + visit(node.getExpression()); } @Override public void visitCatchStatement(CatchStatement node) { appendLeadingComments(node); - appendIndent(); - append("catch ("); + appendNewLine(); + append("catch"); + appendSpace(); + append('('); var variable = node.getVariable(); var type = variable.getType(); if( !ClassHelper.isObjectType(type) ) { append(type.getNameWithoutPackage()); - append(' '); + appendSpace(); } append(variable.getName()); - - append(") {\n"); + append(')'); + appendSpace(); + append('{'); incIndent(); visit(node.getCode()); decIndent(); - appendIndent(); - append("}\n"); + appendNewLine(); + append("}"); } // expressions @@ -312,14 +522,13 @@ else if( node.isSafe() ) incIndent(); visitArguments(parenArgs, wrap); if( wrap ) { - appendNewLine(); decIndent(); - appendIndent(); + appendNewLine(); } append(')'); } if( lastClosureArg ) { - append(' '); + appendSpace(); visit(args.get(args.size() - 1)); } inWrappedMethodChain = iwmc; @@ -332,7 +541,8 @@ else if( node.isSafe() ) @Override public void visitConstructorCallExpression(ConstructorCallExpression node) { - append("new "); + append("new"); + appendSpace(); visitTypeAnnotation(node.getType()); append('('); visitArguments(asMethodCallArguments(node), false); @@ -340,14 +550,14 @@ public void visitConstructorCallExpression(ConstructorCallExpression node) { } public void visitDirective(MethodCallExpression call) { - appendIndent(); + appendLeadingComments(call); + appendNewLine(); append(call.getMethodAsString()); var arguments = asMethodCallArguments(call); if( !arguments.isEmpty() ) { - append(' '); + appendSpace(); visitArguments(arguments, false); } - appendNewLine(); } public void visitArguments(List args, boolean wrap) { @@ -358,7 +568,8 @@ public void visitArguments(List args, boolean wrap) { visitPositionalArgs(positionalArgs, wrap); if( hasNamedArgs ) { if( positionalArgs.size() > 0 ) - append(wrap ? "," : ", "); + append(','); + if (!wrap) appendSpace(); var mapX = (MapExpression)args.get(0); visitNamedArgs(mapX.getMapEntryExpressions(), wrap); } @@ -370,14 +581,18 @@ public void visitArguments(List args, boolean wrap) { @Override public void visitBinaryExpression(BinaryExpression node) { + appendLeadingCommentsExpression(node); if( node instanceof DeclarationExpression ) { - append("def "); + appendNewLine(); // A declaration expression is treated as a statement here, so break the line + append("def"); + appendSpace(); inVariableDeclaration = true; visit(node.getLeftExpression()); inVariableDeclaration = false; var source = node.getRightExpression(); if( !(source instanceof EmptyExpression) ) { - append(" = "); + append("="); + appendSpace(); var cre = currentRootExpr; currentRootExpr = source; visit(source); @@ -401,15 +616,14 @@ public void visitBinaryExpression(BinaryExpression node) { visit(node.getLeftExpression()); if( inWrappedPipeChain ) { - appendNewLine(); incIndent(); - appendIndent(); + appendNewLine(); } else { - append(' '); + appendSpace(); } append(node.getOperation().getText()); - append(' '); + appendSpace(); var iwpc = inWrappedPipeChain; inWrappedPipeChain = false; @@ -434,65 +648,77 @@ public void visitBinaryExpression(BinaryExpression node) { @Override public void visitTernaryExpression(TernaryExpression node) { + appendLeadingCommentsExpression(node); if( shouldWrapExpression(node) ) { visit(node.getBooleanExpression()); incIndent(); appendNewLine(); - appendIndent(); - append("? "); + append("?"); + appendSpace(); visit(node.getTrueExpression()); appendNewLine(); - appendIndent(); - append(": "); + append(":"); + appendSpace(); visit(node.getFalseExpression()); decIndent(); } else { visit(node.getBooleanExpression()); - append(" ? "); + appendSpace(); + append("?"); + appendSpace(); visit(node.getTrueExpression()); - append(" : "); + appendSpace(); + append(":"); + appendSpace(); visit(node.getFalseExpression()); } } @Override public void visitShortTernaryExpression(ElvisOperatorExpression node) { + appendLeadingCommentsExpression(node); visit(node.getTrueExpression()); - append(" ?: "); + appendSpace(); + append("?:"); + appendSpace(); visit(node.getFalseExpression()); } @Override public void visitNotExpression(NotExpression node) { + appendLeadingCommentsExpression(node); append('!'); visit(node.getExpression()); } @Override public void visitClosureExpression(ClosureExpression node) { + appendLeadingCommentsExpression(node); append('{'); if( node.getParameters() != null && node.getParameters().length > 0 ) { - append(' '); + appendSpace(); visitParameters(node.getParameters()); - append(" ->"); + appendSpace(); + append("->"); } var code = (BlockStatement) node.getCode(); if( code.getStatements().size() == 0 ) { - append(" }"); + appendSpace(); + append("}"); } else if( code.getStatements().size() == 1 && code.getStatements().get(0) instanceof ExpressionStatement es && !shouldWrapExpression(node) ) { - append(' '); + appendSpace(); visitStatementLabels(es); visit(es.getExpression()); - append(" }"); + appendSpace(); + append("}"); } else { - appendNewLine(); incIndent(); visit(code); decIndent(); - appendIndent(); + appendNewLine(); append('}'); } } @@ -502,35 +728,40 @@ public void visitParameters(Parameter[] parameters) { var param = parameters[i]; if( isLegacyType(param.getType()) ) { visitTypeAnnotation(param.getType()); - append(' '); + appendSpace(); } append(param.getName()); if( param.hasInitialExpression() ) { - append(" = "); + appendSpace(); + append("="); + appendSpace(); visit(param.getInitialExpression()); } - if( i + 1 < parameters.length ) - append(", "); + if( i + 1 < parameters.length ) { + append(","); + appendSpace(); + } } } @Override public void visitTupleExpression(TupleExpression node) { + appendLeadingCommentsExpression(node); var wrap = shouldWrapExpression(node); append('('); if( wrap ) incIndent(); visitPositionalArgs(node.getExpressions(), wrap); if( wrap ) { - appendNewLine(); decIndent(); - appendIndent(); + appendNewLine(); } append(')'); } @Override public void visitListExpression(ListExpression node) { + appendLeadingCommentsExpression(node); var wrap = hasTrailingComma(node) || shouldWrapExpression(node); append('['); if( wrap ) @@ -550,7 +781,6 @@ protected void visitPositionalArgs(List args, boolean wrap) { for( int i = 0; i < args.size(); i++ ) { if( wrap ) { appendNewLine(); - appendIndent(); } visit(args.get(i)); if( trailingComma || i + 1 < args.size() ) @@ -560,6 +790,7 @@ protected void visitPositionalArgs(List args, boolean wrap) { @Override public void visitMapExpression(MapExpression node) { + appendLeadingCommentsExpression(node); if( node.getMapEntryExpressions().isEmpty() ) { append("[:]"); return; @@ -570,9 +801,8 @@ public void visitMapExpression(MapExpression node) { incIndent(); visitNamedArgs(node.getMapEntryExpressions(), wrap); if( wrap ) { - appendNewLine(); decIndent(); - appendIndent(); + appendNewLine(); } append(']'); } @@ -583,7 +813,6 @@ protected void visitNamedArgs(List args, boolean wrap) { for( int i = 0; i < args.size(); i++ ) { if( wrap ) { appendNewLine(); - appendIndent(); } visit(args.get(i)); if( trailingComma || i + 1 < args.size() ) @@ -593,13 +822,16 @@ protected void visitNamedArgs(List args, boolean wrap) { @Override public void visitMapEntryExpression(MapEntryExpression node) { + appendLeadingCommentsExpression(node); visit(node.getKeyExpression()); - append(": "); + append(":"); + appendSpace(); visit(node.getValueExpression()); } @Override public void visitRangeExpression(RangeExpression node) { + appendLeadingCommentsExpression(node); visit(node.getFrom()); if( node.isExclusiveLeft() ) append('<'); @@ -611,44 +843,54 @@ public void visitRangeExpression(RangeExpression node) { @Override public void visitUnaryMinusExpression(UnaryMinusExpression node) { + appendLeadingCommentsExpression(node); append('-'); visit(node.getExpression()); } @Override public void visitUnaryPlusExpression(UnaryPlusExpression node) { + appendLeadingCommentsExpression(node); append('+'); visit(node.getExpression()); } @Override public void visitBitwiseNegationExpression(BitwiseNegationExpression node) { + appendLeadingCommentsExpression(node); append('~'); visit(node.getExpression()); } @Override public void visitCastExpression(CastExpression node) { + appendLeadingCommentsExpression(node); visit(node.getExpression()); - append(" as "); + appendSpace(); + append("as"); + appendSpace(); visitTypeAnnotation(node.getType()); } @Override public void visitConstantExpression(ConstantExpression node) { + appendLeadingCommentsExpression(node); var text = (String) node.getNodeMetaData(ASTNodeMarker.VERBATIM_TEXT); if( text != null ) append(text); else append(node.getText()); + appendTrailingCommentsExpression(node); } @Override public void visitClassExpression(ClassExpression node) { + appendLeadingCommentsExpression(node); visitTypeAnnotation(node.getType()); } public void visitTypeAnnotation(ClassNode type) { + appendLeadingCommentsExpression(type); if( isLegacyType(type) ) { append(type.getNodeMetaData(ASTNodeMarker.LEGACY_TYPE)); return; @@ -659,11 +901,13 @@ public void visitTypeAnnotation(ClassNode type) { @Override public void visitVariableExpression(VariableExpression node) { + appendLeadingCommentsExpression(node); if( inVariableDeclaration && isLegacyType(node.getType()) ) { visitTypeAnnotation(node.getType()); - append(' '); + appendSpace(); } append(node.getText()); + appendTrailingCommentsExpression(node); } @Override @@ -679,6 +923,7 @@ else if( node.isSafe() ) @Override public void visitGStringExpression(GStringExpression node) { + appendLeadingCommentsExpression(node); // see also: GStringUtil.writeToImpl() var quoteChar = (String) node.getNodeMetaData(ASTNodeMarker.QUOTE_CHAR, k -> DQ_STR); append(quoteChar); diff --git a/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java index 5ed37052d0..1c85016a7f 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java +++ b/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java @@ -30,7 +30,10 @@ import nextflow.script.ast.ScriptNode; import nextflow.script.ast.ScriptVisitorSupport; import nextflow.script.ast.WorkflowNode; +import nextflow.script.parser.CommentWriter; + import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.expr.EmptyExpression; import org.codehaus.groovy.ast.expr.PropertyExpression; import org.codehaus.groovy.ast.expr.VariableExpression; @@ -40,6 +43,8 @@ import org.codehaus.groovy.ast.stmt.Statement; import org.codehaus.groovy.control.SourceUnit; +import groovy.lang.Tuple2; + import static nextflow.script.ast.ASTUtils.*; /** @@ -53,6 +58,8 @@ public class ScriptFormattingVisitor extends ScriptVisitorSupport { private FormattingOptions options; + private CommentWriter commentWriter; + private Formatter fmt; private int maxIncludeWidth = 0; @@ -62,7 +69,17 @@ public class ScriptFormattingVisitor extends ScriptVisitorSupport { public ScriptFormattingVisitor(SourceUnit sourceUnit, FormattingOptions options) { this.sourceUnit = sourceUnit; this.options = options; - this.fmt = new Formatter(options); + this.commentWriter = getCommentWriter(); + this.fmt = new Formatter(options, this.commentWriter); + } + + public CommentWriter getCommentWriter() { + var moduleNode = sourceUnit.getAST(); + if( moduleNode instanceof ScriptNode ) { + var scriptNode = (ScriptNode) moduleNode; + return (CommentWriter)scriptNode.getNodeMetaData("commentWriter"); + } + return null; } @Override @@ -70,6 +87,8 @@ protected SourceUnit getSourceUnit() { return sourceUnit; } + + public void visit() { var moduleNode = sourceUnit.getAST(); if( !(moduleNode instanceof ScriptNode) ) @@ -124,6 +143,8 @@ else if( decl instanceof ProcessNode pn ) else if( decl instanceof WorkflowNode wn ) visitWorkflow(wn); } + fmt.appendBlanks(1); + fmt.exit(); } public String toString() { @@ -136,7 +157,9 @@ public String toString() { public void visitFeatureFlag(FeatureFlagNode node) { fmt.appendLeadingComments(node); fmt.append(node.name); - fmt.append(" = "); + fmt.appendSpace(); + fmt.append("="); + fmt.appendSpace(); fmt.visit(node.value); fmt.appendNewLine(); } @@ -145,7 +168,9 @@ public void visitFeatureFlag(FeatureFlagNode node) { public void visitInclude(IncludeNode node) { var wrap = node.getLineNumber() < node.getLastLineNumber(); fmt.appendLeadingComments(node); - fmt.append("include {"); + fmt.append("include"); + fmt.appendSpace(); + fmt.append('{'); if( wrap ) fmt.incIndent(); for( int i = 0; i < node.entries.size(); i++ ) { @@ -154,29 +179,35 @@ public void visitInclude(IncludeNode node) { fmt.appendIndent(); } else { - fmt.append(' '); + fmt.appendSpace(); } var entry = node.entries.get(i); fmt.append(entry.name); if( entry.alias != null ) { - fmt.append(" as "); + fmt.appendSpace(); + fmt.append("as"); + fmt.appendSpace(); fmt.append(entry.alias); } if( !wrap && node.entries.size() == 1 && options.harshilAlignment() ) { var padding = maxIncludeWidth - getIncludeWidth(entry); - fmt.append(" ".repeat(padding)); + fmt.appendPadding(padding); } if( i + 1 < node.entries.size() ) - fmt.append(" ;"); + fmt.appendSpace(); + fmt.append(";"); } if( wrap ) { fmt.appendNewLine(); fmt.decIndent(); } else { - fmt.append(' '); + fmt.appendSpace(); } - fmt.append("} from "); + fmt.append('}'); + fmt.appendSpace(); + fmt.append("from"); + fmt.appendSpace(); fmt.visit(node.source); fmt.appendNewLine(); } @@ -194,9 +225,11 @@ public void visitParam(ParamNode node) { fmt.visit(node.target); if( maxParamWidth > 0 ) { var padding = maxParamWidth - getParamWidth(node); - fmt.append(" ".repeat(padding)); + fmt.appendPadding(padding); } - fmt.append(" = "); + fmt.appendSpace(); + fmt.append("="); + fmt.appendSpace(); fmt.visit(node.value); fmt.appendNewLine(); } @@ -210,40 +243,46 @@ protected int getParamWidth(ParamNode node) { @Override public void visitWorkflow(WorkflowNode node) { fmt.appendLeadingComments(node); + var withinComments = commentWriter.getWithinComments(node); + fmt.appendNewLine(); fmt.append("workflow"); + fmt.appendSpace(); + fmt.writeComments(withinComments, "KEYWORD", false, true, false, false, true); if( !node.isEntry() ) { - fmt.append(' '); fmt.append(node.getName()); + fmt.appendSpace(); + fmt.writeComments(withinComments, "NAME", false, true, false, false, true); } - fmt.append(" {\n"); + fmt.append("{"); fmt.incIndent(); if( node.takes instanceof BlockStatement ) { - fmt.appendIndent(); - fmt.append("take:\n"); + appendColonStatement("take", "TAKE", node.takes); visitWorkflowTakes(asBlockStatements(node.takes)); fmt.appendNewLine(); } if( node.main instanceof BlockStatement ) { if( node.takes instanceof BlockStatement || node.emits instanceof BlockStatement || node.publishers instanceof BlockStatement ) { fmt.appendIndent(); - fmt.append("main:\n"); + appendColonStatement("main", "MAIN", node.main); } fmt.visit(node.main); } if( node.emits instanceof BlockStatement ) { fmt.appendNewLine(); fmt.appendIndent(); - fmt.append("emit:\n"); + appendColonStatement("emit", "EMIT", node.takes); visitWorkflowEmits(asBlockStatements(node.emits)); } if( node.publishers instanceof BlockStatement ) { fmt.appendNewLine(); fmt.appendIndent(); - fmt.append("publish:\n"); + appendColonStatement("publish", "PUBLISH", node.publishers); fmt.visit(node.publishers); } fmt.decIndent(); - fmt.append("}\n"); + fmt.appendNewLine(); + fmt.append("}"); + fmt.appendTrailingComments(node); } protected void visitWorkflowTakes(List takes) { @@ -255,13 +294,13 @@ protected void visitWorkflowTakes(List takes) { var ve = asVarX(stmt); fmt.appendIndent(); fmt.visit(ve); - if( fmt.hasTrailingComment(stmt) ) { - if( alignmentWidth > 0 ) { - var padding = alignmentWidth - ve.getName().length(); - fmt.append(" ".repeat(padding)); - } - fmt.appendTrailingComment(stmt); - } + // if( fmt.hasTrailingComment(stmt) ) { + // if( alignmentWidth > 0 ) { + // var padding = alignmentWidth - ve.getName().length(); + // fmt.append(" ".repeat(padding)); + // } + // } + fmt.appendTrailingComments(stmt); fmt.appendNewLine(); } } @@ -280,23 +319,25 @@ protected void visitWorkflowEmits(List emits) { fmt.visit(ve); if( alignmentWidth > 0 ) { var padding = alignmentWidth - ve.getName().length(); - fmt.append(" ".repeat(padding)); + fmt.appendPadding(padding); } - fmt.append(" = "); + fmt.appendSpace(); + fmt.append("="); + fmt.appendSpace(); fmt.visit(assign.getRightExpression()); - fmt.appendTrailingComment(stmt); + fmt.appendTrailingComments(stmt); fmt.appendNewLine(); } else if( emit instanceof VariableExpression ve ) { fmt.appendIndent(); fmt.visit(ve); - if( fmt.hasTrailingComment(stmt) ) { - if( alignmentWidth > 0 ) { - var padding = alignmentWidth - ve.getName().length(); - fmt.append(" ".repeat(padding)); - } - fmt.appendTrailingComment(stmt); - } + // if( fmt.hasTrailingComment(stmt) ) { + // if( alignmentWidth > 0 ) { + // var padding = alignmentWidth - ve.getName().length(); + // fmt.append(" ".repeat(padding)); + // } + // } + fmt.appendTrailingComments(stmt); fmt.appendNewLine(); } else { @@ -330,112 +371,150 @@ else if( emit instanceof AssignmentExpression assign ) { @Override public void visitProcess(ProcessNode node) { + var withinComments = commentWriter.getWithinComments(node); + var trailingComments = commentWriter.getTrailingComments(node); + System.err.println(trailingComments); fmt.appendLeadingComments(node); - fmt.append("process "); + fmt.appendNewLine(); + fmt.append("process"); + fmt.appendSpace(); + fmt.writeComments(withinComments, "KEYWORD", false, false, true, false, true); fmt.append(node.getName()); - fmt.append(" {\n"); + fmt.writeComments(withinComments, "NAME", false, true, false, false, true); + fmt.appendSpace(); + fmt.append("{"); + fmt.writeComments(trailingComments, "LBRACE", false, true, true, false, false); fmt.incIndent(); + if( node.directives instanceof BlockStatement ) { visitDirectives(node.directives); - fmt.appendNewLine(); + fmt.appendBlanks(1); } + if( node.inputs instanceof BlockStatement ) { - fmt.appendIndent(); - fmt.append("input:\n"); + appendColonStatement("input", "INPUT", node.inputs); visitDirectives(node.inputs); - fmt.appendNewLine(); + fmt.appendBlanks(1); } + if( !options.maheshForm() && node.outputs instanceof BlockStatement ) { - visitProcessOutputs(node.outputs); - fmt.appendNewLine(); + appendColonStatement("output", "OUTPUT", node.outputs); + visitDirectives(node.outputs); + fmt.appendBlanks(1); } + if( !(node.when instanceof EmptyExpression) ) { - fmt.appendIndent(); - fmt.append("when:\n"); - fmt.appendIndent(); + appendColonStatement("when", "WHEN", node.when); + fmt.incIndent(); + fmt.appendNewLine(); fmt.visit(node.when); - fmt.append("\n\n"); + fmt.appendBlanks(1); } - fmt.appendIndent(); - fmt.append(node.type); - fmt.append(":\n"); + + appendColonStatement(node.type, "EXEC", node.exec); fmt.visit(node.exec); + fmt.appendBlanks(1); + if( !(node.stub instanceof EmptyStatement) ) { - fmt.appendNewLine(); - fmt.appendIndent(); - fmt.append("stub:\n"); + appendColonStatement("stub", "STUB", node.stub); fmt.visit(node.stub); + fmt.appendBlanks(1); } + if( options.maheshForm() && node.outputs instanceof BlockStatement ) { - fmt.appendNewLine(); - visitProcessOutputs(node.outputs); + appendColonStatement("output", "OUTPUT", node.outputs); + visitDirectives(node.outputs); + fmt.appendBlanks(1); } + fmt.decIndent(); - fmt.append("}\n"); + fmt.appendNewLine(); + fmt.append("}"); + fmt.writeComments(trailingComments, "RBRACE", false, true, true, false, false); } - private void visitProcessOutputs(Statement outputs) { - fmt.appendIndent(); - fmt.append("output:\n"); - visitDirectives(outputs); + private void appendColonStatement(String name, String commentKey, ASTNode stmt) { + fmt.appendLeadingComments(stmt); + fmt.appendNewLine(); + var colonWithinComments = commentWriter.getWithinComments(stmt); + var colonTrailingComments = commentWriter.getTrailingComments(stmt); + // Write the name of the statement + fmt.append(name); + // Write commend inbetween the name and the colon + fmt.appendWithinComments(colonWithinComments, commentKey); + fmt.append(":"); + // Write trailing comments after the colon + fmt.appendTrailingInside(colonTrailingComments, "COLON"); } @Override public void visitFunction(FunctionNode node) { fmt.appendLeadingComments(node); - fmt.append("def "); + fmt.appendNewLine(); + fmt.append("def"); + fmt.appendSpace(); if( Formatter.isLegacyType(node.getReturnType()) ) { fmt.visitTypeAnnotation(node.getReturnType()); - fmt.append(' '); + fmt.appendSpace(); } fmt.append(node.getName()); fmt.append('('); fmt.visitParameters(node.getParameters()); - fmt.append(") {\n"); + fmt.append(')'); + fmt.appendSpace(); + fmt.append('{'); fmt.incIndent(); fmt.visit(node.getCode()); fmt.decIndent(); - fmt.append("}\n"); + fmt.appendNewLine(); + fmt.append("}"); } @Override public void visitEnum(ClassNode node) { fmt.appendLeadingComments(node); - fmt.append("enum "); + fmt.appendNewLine(); + fmt.append("enum"); + fmt.appendSpace(); fmt.append(node.getName()); - fmt.append(" {\n"); fmt.incIndent(); + fmt.appendSpace(); + fmt.append("{"); for( var fn : node.getFields() ) { - fmt.appendIndent(); + fmt.appendNewLine(); fmt.append(fn.getName()); fmt.append(','); - fmt.appendNewLine(); } fmt.decIndent(); - fmt.append("}\n"); + fmt.appendNewLine(); + fmt.append("}"); } @Override public void visitOutputs(OutputBlockNode node) { fmt.appendLeadingComments(node); - fmt.append("output {\n"); + fmt.appendNewLine(); + fmt.append("output"); + fmt.appendSpace(); + fmt.append('{'); fmt.incIndent(); super.visitOutputs(node); fmt.decIndent(); - fmt.append("}\n"); + fmt.appendNewLine(); + fmt.append("}"); } @Override public void visitOutput(OutputNode node) { fmt.appendLeadingComments(node); - fmt.appendIndent(); + fmt.appendNewLine(); fmt.append(node.name); - fmt.append(" {\n"); - fmt.incIndent(); + fmt.appendSpace(); + fmt.append("{"); visitOutputBody((BlockStatement) node.body); fmt.decIndent(); - fmt.appendIndent(); - fmt.append("}\n"); + fmt.appendNewLine(); + fmt.append("}"); } protected void visitOutputBody(BlockStatement block) { @@ -452,12 +531,15 @@ protected void visitOutputBody(BlockStatement block) { fmt.appendLeadingComments(stmt); fmt.appendIndent(); fmt.append(name); - fmt.append(" {\n"); + fmt.appendSpace(); + fmt.append("{"); fmt.incIndent(); + fmt.appendNewLine(); visitDirectives(code); fmt.decIndent(); fmt.appendIndent(); - fmt.append("}\n"); + fmt.append("}"); + fmt.appendNewLine(); return; } } diff --git a/modules/nf-lang/src/main/java/nextflow/script/parser/CommentWriter.java b/modules/nf-lang/src/main/java/nextflow/script/parser/CommentWriter.java new file mode 100644 index 0000000000..4f162da108 --- /dev/null +++ b/modules/nf-lang/src/main/java/nextflow/script/parser/CommentWriter.java @@ -0,0 +1,523 @@ +package nextflow.script.parser; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.antlr.v4.runtime.Token; +import org.codehaus.groovy.ast.ASTNode; +import groovy.lang.Tuple2; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Collections; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class CommentWriter { + + public final List commentTokens; + private final List allTokens; + + private final List unattachedComments; + private final List tokensWithInfo; + private final Set pendingComments = new HashSet<>(); + private final Map> attachedComments = new HashMap<>(); + private final Map> writtenComments = new HashMap<>(); + + private final Map> leadingComments = new HashMap<>(); + private final Map>> withinComments = new HashMap<>(); + private final Map>> trailingComments = new HashMap<>(); + + public CommentWriter(List allTokens) { + this.commentTokens = allTokens.stream().filter(t -> t.getChannel() == ScriptLexer.COMMENT).collect(Collectors.toList()); + this.allTokens = allTokens; + + this.tokensWithInfo = allTokens.stream().map(t -> new TokenInfo(t)).collect(Collectors.toList()); + this.unattachedComments = tokensWithInfo.stream() + .filter(t -> t.isComment()) + .map(t -> t.getComment()) + .collect(Collectors.toList()); + } + + public class TokenInfo { + private final Token token; + private final Comment comment; + + public TokenInfo(Token token) { + this.token = token; + if (token.getChannel() == ScriptLexer.COMMENT) { + this.comment = new Comment(token); + } else { + this.comment = null; + } + } + + public Token getToken() { + return token; + } + + public Comment getComment() { + return comment; + } + + public boolean isComment() { + return comment != null; + } + + @Override + public String toString() { + return "TokenInfo:" + (isComment() ? "COMMENT" : "SEMANTIC") + ": '" + token.getText().replace("\n", "\\n") + "'"; + } + } + + String indent = " "; + + public String writeUnattached() { + var unattachedHeader = "Unattached comments:"; + var unattachedText = unattachedComments.stream() + .map(t -> { + var line = t.getToken().getLine(); + var column = t.getToken().getCharPositionInLine(); + var text = t.getToken().getText().replaceAll("\n", "\\n"); + var isLeading = t.isLeading(); + var isTrailing = t.isTrailing(); + var attachedTo = t.getAttachedTo(); + var attachedToText = attachedTo != null ? attachedTo.getText() : "null"; + return ( + indent + line + ":" + column + "{\n" + + indent + indent + "content: " + text + "\n" + + indent + indent + "attach: " + attachedToText + "\n" + + indent + "}\n" + ); + }) + .collect(Collectors.joining("\n")); + return unattachedHeader + unattachedText; + } + + public String writePending() { + var pendingHeader = "Pending comments:"; + var pendingText = pendingComments.stream() + .map(t -> { + var line = t.getToken().getLine(); + var column = t.getToken().getCharPositionInLine(); + var text = t.getToken().getText().replaceAll("\n", "\\n"); + return ( + indent + line + ":" + column + "{\n" + + indent + indent + "content: " + text + "\n" + + indent + "}\n" + ); + }) + .collect(Collectors.joining("\n")); + return pendingHeader + pendingText; + } + + public String writeAttached() { + var attachedHeader = "Attached comments:"; + var attachedText = attachedComments.values() + .stream() + .flatMap(List::stream) + .map(t -> { + var line = t.getToken().getLine(); + var column = t.getToken().getCharPositionInLine(); + var text = t.getToken().getText().replaceAll("\n", "\\n"); + var isLeading = t.isLeading(); + var isTrailing = t.isTrailing(); + ASTNode attachedTo = t.getAttachedTo(); + var simpleName = attachedTo != null ? attachedTo.getClass().getSimpleName() : null; + var maybeText = attachedTo != null ? attachedTo.getText(): null; + var attachToCode = maybeText != null ? maybeText.replaceAll("\n", "\\n") : "null"; + var attachedToText = attachedTo != null ? simpleName + " " + attachToCode : "null"; + var relPos = ( + (t.isWithin() ? "within " : "")+ + (t.isLeading() ? "leading " : "") + + (t.isTrailing() ? "trailing " : "") + ); + return ( + indent + line + ":" + column + "{\n" + + indent + indent + "content : " + text + "\n" + + indent + indent + "attached to: " + attachedToText + "\n" + + indent + indent + "token : " + t.getPositionInfo() + "\n" + + indent + indent + "written : " + t.isWritten() + "\n" + + indent + indent + "rel. pos. : " + relPos + "\n" + + indent + "}\n" + ); + }) + .collect(Collectors.joining("\n")); + return attachedHeader + attachedText; + } + + public String writeWritten() { + var writtenHeader = "Written comments:"; + var writtenText = writtenComments.values() + .stream() + .flatMap(Set::stream) + .map(t -> { + var line = t.getToken().getLine(); + var column = t.getToken().getCharPositionInLine(); + var text = t.getToken().getText().replaceAll("\n", "\\n"); + var isLeading = t.isLeading(); + var isTrailing = t.isTrailing(); + ASTNode attachedTo = t.getAttachedTo(); + var simpleName = attachedTo != null ? attachedTo.getClass().getSimpleName() : null; + var maybeText = attachedTo != null ? attachedTo.getText(): null; + var attachToCode = maybeText != null ? maybeText.replaceAll("\n", "\\n") : "null"; + var attachedToText = attachedTo != null ? simpleName + " " + attachToCode : "null"; + var relPos = ( + (t.isWithin() ? "within " : "")+ + (t.isLeading() ? "leading " : "") + + (t.isTrailing() ? "trailing " : "") + ); + return ( + indent + line + ":" + column + "{\n" + + indent + indent + "content : " + text + "\n" + + indent + indent + "attached to: " + attachedToText + "\n" + + indent + indent + "token : " + t.getPositionInfo() + "\n" + + indent + indent + "written : " + t.isWritten() + "\n" + + indent + indent + "rel. pos. : " + relPos + "\n" + + indent + "}\n" + ); + }) + .collect(Collectors.joining("\n")); + return writtenHeader + writtenText; + } + + + public String writeUnWritten() { + var header = "CommentWriter with " + commentTokens.size() + " comment tokens"; + return header + "\n" + writeUnattached() + "\n" + writePending() + "\n"+ writeAttached() + "\n"; + } + + public String writeAll() { + var header = "CommentWriter with " + commentTokens.size() + " comment tokens"; + return header + "\n" + writeUnattached() + "\n" + writePending() + "\n"+ writeUnattached() + "\n" + writeWritten(); + } + + public Map> getAttachedComments(ASTNode node) { + var within = new ArrayList(); + var leading = new ArrayList(); + var trailing = new ArrayList(); + attachedComments + .getOrDefault(node, new ArrayList<>()) + .forEach(c -> { + if( c.isWithin() ) { + within.add(c); + } else if( c.isLeading() ) { + leading.add(c); + } else if( c.isTrailing() ) { + trailing.add(c); + } + }); + return Map.of( + "leading", leading, + "within", within, + "trailing", trailing + ); + } + + public List processLeadingComments(ParserRuleContext ctx) { + if (unattachedComments.isEmpty()) { + return new ArrayList<>(); + } + + List comments = new ArrayList<>(); + int contextStartIndex = ctx.getStart().getTokenIndex(); + int firstCommentIndex = unattachedComments.get(0).getToken().getTokenIndex(); + + // Get all comment tokens that are before the context and only separated by newlines + for (int i = contextStartIndex - 1; i >= firstCommentIndex; i--) { + TokenInfo token = tokensWithInfo.get(i); + if( token.isComment() && !token.getComment().isConsumed() ) { + comments.add(0, token.getComment()); + } else if( token.getToken().getType() == ScriptLexer.NL ){ + continue; + } else { + break; + } + } + + comments.forEach(c -> { + c.makePending(); + c.markAsLeading(ctx.getStart()); + }); + + return comments; + } + + public List processInbetweenComments( + Token start, Token end, String positionInfo, boolean isLeading, boolean isTrailing + ) { + List comments = new ArrayList<>(); + Iterator it = unattachedComments.iterator(); + int startIdx = start.getStopIndex(); + int endIdx = end.getStartIndex(); + while( it.hasNext() ) { + Comment comment = it.next(); + if (comment.getStartIndex() >= startIdx && comment.getStopIndex() <= endIdx) { + comments.add(comment); + } + } + comments.forEach(c -> { + c.markAsWithin(positionInfo, start, end, isLeading, isTrailing); + c.makePending(); + + }); + return comments; + } + + public List processInbetweenComments(TerminalNode start, TerminalNode end, String positionInfo, boolean isLeading, boolean isTrailing) { + return processInbetweenComments(start.getSymbol(), end.getSymbol(), positionInfo, isLeading, isTrailing); + } + + + public List processTrailingComments(Token lastCtxToken, String positionInfo, boolean allowNewlines) { + // A trailing comment is either on the same line as the last token of the context, + // or on the next line line after the last token of the context, but in this case it must + // be separated by at least one blank line from the next context + List comments = new ArrayList<>(); + int contextStopIndex = lastCtxToken.getTokenIndex(); + ListIterator it = tokensWithInfo.listIterator(contextStopIndex + 1); + TokenInfo token = null; + int nlsBefore = 0; + // First get tokens that are directly after the context, without any newlines + while( it.hasNext() ) { + token = it.next(); + if (token.isComment() && !token.getComment().isConsumed()) { + comments.add(token.getComment()); + } else { + if (token.getToken().getType() == ScriptLexer.NL) { + nlsBefore++; + } + break; + } + } + + if (allowNewlines) { + // Get comments that are on the lines following the context + // but only if they seem to be trailing rather than leading + // for the next context + List candidateComments = new ArrayList<>(); + boolean isLeading = false; + while( it.hasNext() && nlsBefore < 2 ) { + token = it.next(); + if (token.isComment() && !token.getComment().isConsumed()) { + candidateComments.add(token.getComment()); + } else if (token.getToken().getType() == ScriptLexer.NL) { + nlsBefore++; + } else { + // We encountered a non newline token before there were a blank line + // this means that the comment should be leading to the next statement instead + isLeading = true; + } + } + if (!isLeading) { + comments.addAll(candidateComments); + } + } + + comments.forEach(c -> { + c.makePending(); + c.markAsTrailing(lastCtxToken, positionInfo); + }); + + return comments; + } + + public List processTrailingComments(TerminalNode term, String positionInfo, boolean allowNewlines) { + return processTrailingComments(term.getSymbol(), positionInfo, allowNewlines); + } + + public List processTrailingComments(TerminalNode term, boolean allowNewlines) { + return processTrailingComments(term.getSymbol(), "STANDARD", allowNewlines); + } + + public List processTrailingComments(Token token, boolean allowNewlines) { + return processTrailingComments(token, "STANDARD", allowNewlines); + } + + public void attachComments(ASTNode node, List comments) { + comments.forEach(c -> c.attachTo(node)); + } + + public List getLeadingComments(ASTNode node) { + return leadingComments.getOrDefault(node, new ArrayList<>()); + } + + public Map> getWithinComments(ASTNode node) { + return withinComments.getOrDefault(node, new HashMap<>()); + } + + public Map> getTrailingComments(ASTNode node) { + return trailingComments.getOrDefault(node, new HashMap<>()); + } + + public boolean verifyAllWritten() { + if ( + !( + unattachedComments.size() == 0 && + pendingComments.size() == 0 && + attachedComments.size() == 0 + ) + ) { + // Likely replace this error message with an exception + System.err.println("The following comments are left to be written"); + System.err.println(writeUnWritten()); + return false; + } else { + return true; + } + } + + public class Comment { + private final Token token; + private boolean isLeading = false; + private boolean isTrailing = false; + private boolean isWithin = false; + + private ASTNode attachedTo = null; + private boolean consumed = false; + + // If the comment is within a node, we need to keep track of where it was originally + // We use the field below to keep track of this + private String positionInfo = null; + private Token precedingToken = null; + private Token followingToken = null; + + private boolean isWritten = false; + + public Comment(Token token) { + this.token = token; + } + + public void attachTo(ASTNode node) { + if (attachedTo != null) { + throw new IllegalStateException(this + " already attached to " + attachedTo); + } + this.attachedTo = node; + pendingComments.remove(this); + attachedComments.computeIfAbsent(node, k -> new ArrayList<>()).add(this); + if( isWithin ) { + withinComments.computeIfAbsent(node, k -> new HashMap<>()).computeIfAbsent(positionInfo, k -> new ArrayList<>()).add(this); + } else if( isLeading ) { + leadingComments.computeIfAbsent(node, k -> new ArrayList<>()).add(this); + } else if( isTrailing ) { + trailingComments.computeIfAbsent(node, k -> new HashMap<>()).computeIfAbsent(positionInfo, k -> new ArrayList<>()).add(this); + } + } + + public void makePending() { + if (consumed) { + throw new IllegalStateException(this + " is already consumed"); + } + unattachedComments.remove(this); + pendingComments.add(this); + consumed = true; + } + + public Token getToken() { + return token; + } + + public ASTNode getAttachedTo() { + return attachedTo; + } + + public void markAsLeading(Token followingToken) { + this.isLeading = true; + this.followingToken = followingToken; + } + + public void markAsTrailing(Token precedingToken, String positionInfo) { + this.isTrailing = true; + this.precedingToken = precedingToken; + this.positionInfo = positionInfo; + } + + public void markAsWithin( + String positionInfo, + Token start, + Token end, + boolean isLeading, + boolean isTrailing + ) { + this.isWithin = true; + this.positionInfo = positionInfo; + this.precedingToken = start; + this.followingToken = end; + this.isLeading = isLeading; + this.isTrailing = isTrailing; + } + + public boolean isConsumed() { + return consumed; + } + + public boolean isLeading() { + return isLeading; + } + + public boolean isTrailing() { + return isTrailing; + } + + public boolean isWithin() { + return isWithin; + } + + public String getPositionInfo() { + return positionInfo; + } + + public int getStartIndex() { + return token.getStartIndex(); + } + + public int getStopIndex() { + return token.getStopIndex(); + } + + public boolean isWritten() { + return isWritten; + } + + public Tuple2 write() { + if( isWritten ) { + throw new IllegalStateException(this + " already written"); + } + isWritten = true; + + writtenComments.computeIfAbsent(attachedTo, k -> new HashSet<>()).add(this); + var attachedNodeComments = attachedComments.get(attachedTo); + if (attachedNodeComments.contains(this)) { + if (attachedNodeComments.size() == 1) { + attachedComments.remove(attachedTo); + } else { + attachedNodeComments.remove(this); + } + } else { + throw new IllegalStateException(this + " was not attached to " + attachedTo); + } + + return new Tuple2<>(token.getText(), token.getType() == ScriptLexer.SL_COMMENT); + } + + @Override + public String toString() { + return String.format( + "Comment:%d:%d:'%s'", + token.getLine(), + token.getCharPositionInLine(), + token.getText().replaceAll("\n", "\\n") + ); + } + @Override + public int hashCode() { + return token.getTokenIndex(); + } + } + + +} diff --git a/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java b/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java index e66fe36734..c1363b5d71 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java +++ b/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; @@ -104,6 +105,7 @@ import static nextflow.script.parser.PositionConfigureUtils.ast; import static nextflow.script.parser.PositionConfigureUtils.tokenPosition; import static nextflow.script.parser.ScriptParser.*; +import static nextflow.script.parser.CommentWriter.*; import static org.codehaus.groovy.ast.expr.VariableExpression.THIS_EXPRESSION; import static org.codehaus.groovy.ast.tools.GeneralUtils.*; @@ -118,6 +120,8 @@ public class ScriptAstBuilder { private ScriptNode moduleNode; private ScriptLexer lexer; private ScriptParser parser; + private CommentWriter commentWriter; + private final GroovydocManager groovydocManager; private Tuple2 numberFormatError; @@ -131,6 +135,30 @@ public ScriptAstBuilder(SourceUnit sourceUnit) { this.parser = new ScriptParser(new CommonTokenStream(lexer)); parser.setErrorHandler(new DescriptiveErrorStrategy(charStream)); + // // Collect all the comments from a separate lexer instance + var commentCharStream = createCharStream(sourceUnit); + var commentLexer = new ScriptLexer(commentCharStream); + var commentTokenStream = new CommonTokenStream(commentLexer); + + // Consume all tokens first + commentTokenStream.fill(); + + // Filter for only COMMENT channel tokens + var allTokens = commentTokenStream.getTokens(); + var commentTokensList = allTokens.stream() + .filter(token -> token.getChannel() == ScriptLexer.COMMENT) + .toList(); + var semanticTokensList = allTokens.stream() + .filter(token -> token.getChannel() == ScriptLexer.DEFAULT_TOKEN_CHANNEL) + .toList(); + + for (int i = 0; i < commentTokensList.size(); i++) { + var token = commentTokensList.get(i); + } + + this.commentWriter = new CommentWriter(allTokens); + moduleNode.putNodeMetaData("commentWriter", commentWriter); + var groovydocEnabled = sourceUnit.getConfiguration().isGroovydocEnabled(); this.groovydocManager = new GroovydocManager(groovydocEnabled); } @@ -248,21 +276,19 @@ private ModuleNode compilationUnit(CompilationUnitContext ctx) { } private boolean scriptDeclaration(ScriptDeclarationContext ctx) { + if( ctx instanceof FeatureFlagDeclAltContext ffac ) { var node = featureFlagDeclaration(ffac.featureFlagDeclaration()); - saveLeadingComments(node, ctx); moduleNode.addFeatureFlag(node); } else if( ctx instanceof EnumDefAltContext edac ) { var node = enumDef(edac.enumDef()); - saveLeadingComments(node, ctx); moduleNode.addClass(node); } else if( ctx instanceof FunctionDefAltContext fdac ) { var node = functionDef(fdac.functionDef()); - saveLeadingComments(node, ctx); moduleNode.addFunction(node); } @@ -274,13 +300,11 @@ else if( ctx instanceof ImportDeclAltContext iac ) { else if( ctx instanceof IncludeDeclAltContext iac ) { var node = includeDeclaration(iac.includeDeclaration()); - saveLeadingComments(node, ctx); moduleNode.addInclude(node); } else if( ctx instanceof OutputDefAltContext odac ) { var node = outputDef(odac.outputDef()); - saveLeadingComments(node, ctx); if( moduleNode.getOutputs() != null ) collectSyntaxError(new SyntaxException("Output block defined more than once", node)); moduleNode.setOutputs(node); @@ -288,19 +312,16 @@ else if( ctx instanceof OutputDefAltContext odac ) { else if( ctx instanceof ParamDeclAltContext pac ) { var node = paramDeclaration(pac.paramDeclaration()); - saveLeadingComments(node, ctx); moduleNode.addParam(node); } else if( ctx instanceof ProcessDefAltContext pdac ) { var node = processDef(pdac.processDef()); - saveLeadingComments(node, ctx); moduleNode.addProcess(node); } else if( ctx instanceof WorkflowDefAltContext wdac ) { var node = workflowDef(wdac.workflowDef()); - saveLeadingComments(node, ctx); if( node.isEntry() ) { if( moduleNode.getEntry() != null ) collectSyntaxError(new SyntaxException("Entry workflow defined more than once", node)); @@ -370,11 +391,20 @@ private ClassNode enumDef(EnumDefContext ctx) { } private ProcessNode processDef(ProcessDefContext ctx) { + var comments = commentWriter.processLeadingComments(ctx); var name = ctx.name.getText(); + // Comments between keyword and name + comments.addAll(commentWriter.processInbetweenComments(ctx.getStart(), ctx.name.getStop(), "KEYWORD", false, true)); + // Comments between name and '{' + comments.addAll(commentWriter.processInbetweenComments(ctx.name.getStop(), ctx.LBRACE().getSymbol(), "NAME", false, true)); + // Look for trailing comments after the '{' + comments.addAll(commentWriter.processTrailingComments(ctx.LBRACE().getSymbol(), "LBRACE", false)); + if( ctx.body == null ) { var empty = EmptyStatement.INSTANCE; var result = ast( new ProcessNode(name, empty, empty, empty, EmptyExpression.INSTANCE, null, empty, empty), ctx ); collectSyntaxError(new SyntaxException("Missing process script body", result)); + commentWriter.attachComments(result, comments); return result; } @@ -382,10 +412,18 @@ private ProcessNode processDef(ProcessDefContext ctx) { var inputs = processInputs(ctx.body.processInputs()); var outputs = processOutputs(ctx.body.processOutputs()); var when = processWhen(ctx.body.processWhen()); - var type = processType(ctx.body.processExec()); - var exec = ctx.body.blockStatements() != null - ? blockStatements(ctx.body.blockStatements()) - : blockStatements(ctx.body.processExec().blockStatements()); + + String type; + BlockStatement exec; + if (ctx.body.processExec() == null) { + type = "script"; + exec = blockStatements(ctx.body.blockStatements()); + } else { + Tuple2 t = processExec(ctx.body.processExec()); + type = t.getV1(); + exec = t.getV2(); + } + var stub = processStub(ctx.body.processStub()); if( ctx.body.blockStatements() != null ) { @@ -393,7 +431,11 @@ private ProcessNode processDef(ProcessDefContext ctx) { collectSyntaxError(new SyntaxException("The `script:` or `exec:` label is required when other sections are present", exec)); } + // Look for trailing comments after the '}' + comments.addAll(commentWriter.processTrailingComments(ctx.RBRACE().getSymbol(), "RBRACE", true)); + var result = ast( new ProcessNode(name, directives, inputs, outputs, when, type, exec, stub), ctx ); + commentWriter.attachComments(result, comments); groovydocManager.handle(result, ctx); return result; } @@ -411,21 +453,48 @@ private Statement processDirectives(ProcessDirectivesContext ctx) { private Statement processInputs(ProcessInputsContext ctx) { if( ctx == null ) return EmptyStatement.INSTANCE; + var comments = commentWriter.processLeadingComments(ctx); + comments.addAll(commentWriter.processInbetweenComments( + ctx.INPUT(), ctx.COLON(), "INPUT", false, true + )); + // Capture any trailing comment that occurs after the colon + comments.addAll( + commentWriter.processTrailingComments( + ctx.COLON(), "COLON", false + ) + ); var statements = ctx.statement().stream() .map(this::statement) .map(stmt -> checkDirective(stmt, "Invalid process input")) .toList(); - return ast( block(null, statements), ctx ); + var result = ast( block(null, statements), ctx ); + commentWriter.attachComments(result, comments); + return result; } private Statement processOutputs(ProcessOutputsContext ctx) { if( ctx == null ) return EmptyStatement.INSTANCE; + + var comments = commentWriter.processLeadingComments(ctx); + comments.addAll(commentWriter.processInbetweenComments( + ctx.OUTPUT(), ctx.COLON(), "OUTPUT", false, true + )); + + // Capture any trailing comment that occurs after the colon + comments.addAll( + commentWriter.processTrailingComments( + ctx.COLON(), false + ) + ); + var statements = ctx.statement().stream() .map(this::statement) .map(stmt -> checkDirective(stmt, "Invalid process output")) .toList(); - return ast( block(null, statements), ctx ); + var result = ast( block(null, statements), ctx ); + commentWriter.attachComments(result, comments); + return result; } private Statement checkDirective(Statement stmt, String errorMessage) { @@ -480,60 +549,106 @@ private boolean isDirectiveWithNegativeValue(Expression expression) { private Expression processWhen(ProcessWhenContext ctx) { if( ctx == null ) return EmptyExpression.INSTANCE; - return ast( expression(ctx.expression()), ctx ); + var comments = commentWriter.processLeadingComments(ctx); + comments.addAll(commentWriter.processInbetweenComments( + ctx.WHEN(), ctx.COLON(), "when", false, true + )); + // Capture any trailing comment that occurs after the colon + comments.addAll( + commentWriter.processTrailingComments( + ctx.COLON(), false + ) + ); + + var result = ast( expression(ctx.expression()), ctx ); + commentWriter.attachComments(result, comments); + return result; } - private String processType(ProcessExecContext ctx) { - if( ctx == null ) - return "script"; - - if( ctx.EXEC() != null ) { - return "exec"; - } - if( ctx.SHELL() != null ) { + private Tuple2 processExec(ProcessExecContext ctx) { + + var execTypeToken = ctx.execType; + + // Collect comments + var comments = commentWriter.processLeadingComments(ctx); + comments.addAll(commentWriter.processInbetweenComments( + execTypeToken, ctx.COLON().getSymbol(), "EXEC", false, true + )); + // Capture any trailing comment that occurs after the colon + comments.addAll( + commentWriter.processTrailingComments( + ctx.COLON(), false + ) + ); + + if (execTypeToken.getType() == ScriptLexer.SHELL) collectWarning("The `shell` block is deprecated, use `script` instead", ctx.SHELL().getText(), ast( new EmptyExpression(), ctx.SHELL() )); - return "shell"; - } - return "script"; + + var result = blockStatements(ctx.blockStatements()); + commentWriter.attachComments(result, comments); + return new Tuple2<>(execTypeToken.getText(), result); } private Statement processStub(ProcessStubContext ctx) { if( ctx == null ) return EmptyStatement.INSTANCE; - return ast( blockStatements(ctx.blockStatements()), ctx ); + var comments = commentWriter.processLeadingComments(ctx); + comments.addAll(commentWriter.processInbetweenComments( + ctx.STUB(), ctx.COLON(), "stub", false, true + )); + // Capture any trailing comment that occurs after the colon + comments.addAll( + commentWriter.processTrailingComments( + ctx.COLON(), false + ) + ); + + var result = ast( blockStatements(ctx.blockStatements()), ctx ); + commentWriter.attachComments(result, comments); + return result; } private WorkflowNode workflowDef(WorkflowDefContext ctx) { var name = ctx.name != null ? ctx.name.getText() : null; + WorkflowNode result; + var comments = commentWriter.processLeadingComments(ctx); + if( name != null ) { + // Look for comments between the name and the body + comments.addAll(commentWriter.processInbetweenComments(ctx.getStart(), ctx.name.getStop(), "KEYWORD", false, true)); + comments.addAll(commentWriter.processInbetweenComments(ctx.name.getStop(), ctx.LBRACE().getSymbol(), "NAME", false, true)); + } else { + comments.addAll(commentWriter.processInbetweenComments(ctx.getStart(), ctx.LBRACE().getSymbol(), "KEYWORD", false, true)); + } if( ctx.body == null ) { - var result = ast( new WorkflowNode(name, null, null, null, null), ctx ); + result = ast( new WorkflowNode(name, null, null, null, null), ctx ); groovydocManager.handle(result, ctx); return result; - } - - var takes = workflowTakes(ctx.body.workflowTakes()); - var emits = workflowEmits(ctx.body.workflowEmits()); - var publishers = workflowPublishers(ctx.body.workflowPublishers()); - var main = blockStatements( - ctx.body.workflowMain() != null - ? ctx.body.workflowMain().blockStatements() - : null - ); + } else { + var takes = workflowTakes(ctx.body.workflowTakes()); + var emits = workflowEmits(ctx.body.workflowEmits()); + var publishers = workflowPublishers(ctx.body.workflowPublishers()); + var main = blockStatements( + ctx.body.workflowMain() != null + ? ctx.body.workflowMain().blockStatements() + : null + ); + + if( name == null ) { + if( takes instanceof BlockStatement ) + collectSyntaxError(new SyntaxException("Entry workflow cannot have a take section", takes)); + if( emits instanceof BlockStatement ) + collectSyntaxError(new SyntaxException("Entry workflow cannot have an emit section", emits)); + } + if( name != null ) { + if( publishers instanceof BlockStatement ) + collectSyntaxError(new SyntaxException("Named workflow cannot have a publish section", publishers)); + } - if( name == null ) { - if( takes instanceof BlockStatement ) - collectSyntaxError(new SyntaxException("Entry workflow cannot have a take section", takes)); - if( emits instanceof BlockStatement ) - collectSyntaxError(new SyntaxException("Entry workflow cannot have an emit section", emits)); - } - if( name != null ) { - if( publishers instanceof BlockStatement ) - collectSyntaxError(new SyntaxException("Named workflow cannot have a publish section", publishers)); + result = ast( new WorkflowNode(name, takes, main, emits, publishers), ctx ); + groovydocManager.handle(result, ctx); } - - var result = ast( new WorkflowNode(name, takes, main, emits, publishers), ctx ); - groovydocManager.handle(result, ctx); + commentWriter.attachComments(result, comments); return result; } @@ -564,6 +679,7 @@ private Statement workflowEmits(WorkflowEmitsContext ctx) { if( ctx == null ) return EmptyStatement.INSTANCE; + var comments = commentWriter.processLeadingComments(ctx); var statements = ctx.statement().stream() .map(this::workflowEmit) .filter(stmt -> stmt != null) @@ -572,6 +688,7 @@ private Statement workflowEmits(WorkflowEmitsContext ctx) { var hasEmitExpression = statements.stream().anyMatch(this::isEmitExpression); if( hasEmitExpression && statements.size() > 1 ) collectSyntaxError(new SyntaxException("Every emit must be assigned to a name when there are multiple emits", result)); + commentWriter.attachComments(result, comments); return result; } @@ -665,6 +782,10 @@ private Statement incompleteScriptDeclaration(IncompleteScriptDeclarationContext private Statement statement(StatementContext ctx) { Statement result; + if( ctx instanceof EmptyStmtAltContext ) + return EmptyStatement.INSTANCE; + + List comments = commentWriter.processLeadingComments(ctx); if( ctx instanceof IfElseStmtAltContext ieac ) result = ast( ifElseStatement(ieac.ifElseStatement()), ieac ); @@ -693,13 +814,12 @@ else if( ctx instanceof AssignmentStmtAltContext aac ) else if( ctx instanceof ExpressionStmtAltContext eac ) result = ast( expressionStatement(eac.expressionStatement()), eac ); - else if( ctx instanceof EmptyStmtAltContext ) - return EmptyStatement.INSTANCE; - else throw createParsingFailedException("Invalid statement: " + ctx.getText(), ctx); + + comments.addAll(commentWriter.processTrailingComments(ctx.getStop(), true)); + commentWriter.attachComments(result, comments); - saveLeadingComments(result, ctx); return result; } @@ -812,9 +932,12 @@ private Expression variableNames(VariableNamesContext ctx) { } private Expression variableName(IdentifierContext ctx) { + var comments = commentWriter.processLeadingComments(ctx); var name = identifier(ctx); var result = ast( varX(name), ctx ); checkInvalidVarName(name, result); + comments.addAll(commentWriter.processTrailingComments(ctx.getStop(), false)); + commentWriter.attachComments(result, comments); return result; } @@ -1161,22 +1284,28 @@ private String keywords(KeywordsContext ctx) { } private Expression literal(LiteralContext ctx) { + Expression result; + var comments = commentWriter.processLeadingComments(ctx); if( ctx instanceof IntegerLiteralAltContext iac ) - return ast( integerLiteral(iac), iac ); + result = ast( integerLiteral(iac), iac ); - if( ctx instanceof FloatingPointLiteralAltContext fac ) - return ast( floatingPointLiteral(fac), fac ); + else if( ctx instanceof FloatingPointLiteralAltContext fac ) + result = ast( floatingPointLiteral(fac), fac ); - if( ctx instanceof StringLiteralAltContext sac ) - return ast( string(sac.stringLiteral()), sac ); + else if( ctx instanceof StringLiteralAltContext sac ) + result = ast( string(sac.stringLiteral()), sac ); - if( ctx instanceof BooleanLiteralAltContext bac ) - return ast( constX("true".equals(bac.getText())), bac ); + else if( ctx instanceof BooleanLiteralAltContext bac ) + result = ast( constX("true".equals(bac.getText())), bac ); - if( ctx instanceof NullLiteralAltContext nac ) - return ast( constX(null), nac ); + else if( ctx instanceof NullLiteralAltContext nac ) + result = ast( constX(null), nac ); - throw createParsingFailedException("Invalid expression: " + ctx.getText(), ctx); + else + throw createParsingFailedException("Invalid expression: " + ctx.getText(), ctx); + comments.addAll(commentWriter.processTrailingComments(ctx.getStop(), false)); + commentWriter.attachComments(result, comments); + return result; } private Expression integerLiteral(IntegerLiteralAltContext ctx) { @@ -1659,17 +1788,29 @@ private ClassNode legacyType(ParserRuleContext ctx) { /// COMMENTS - private void saveLeadingComments(ASTNode node, ParserRuleContext ctx) { - var comments = new ArrayList(); - var child = ctx; - while( saveLeadingComments0(child, comments) ) - child = child.getParent(); - - if( !comments.isEmpty() ) - node.putNodeMetaData(ASTNodeMarker.LEADING_COMMENTS, comments); - } + // private List getLeadingComments(ParserRuleContext ctx) { + // // Get all the unattached comments up until this point + // var leadingComments = new ArrayList(unattachedComments.stream().map(Token::getText).toList()); + // // int stopIndex = ctx.getStart().getTokenIndex(); + // // Iterator it = unattachedComments.iterator(); + // // while( it.hasNext() ) { + // // var comment = it.next(); + // // if( comment.getTokenIndex() >= stopIndex ) + // // break; + // // leadingComments.add(comment.getText()); + // // it.remove(); // The comment has been attached to this node, so remove it from the unattached comments list + // // } + + // return leadingComments; + // } + + // private void saveLeadingComments(ASTNode node, List comments) { + // if( !comments.isEmpty() ) + // node.putNodeMetaData(ASTNodeMarker.LEADING_COMMENTS, comments.stream().map(CommentWriter.Comment::getToken).map(Token::getText).toList()); + // } private boolean saveLeadingComments0(ParserRuleContext ctx, List comments) { + var parent = ctx.getParent(); if( parent == null ) return false; @@ -1812,7 +1953,7 @@ else if( t instanceof GroovySyntaxError gse ) else if( t instanceof Exception e ) collectException(e); - + return new CompilationFailedException( CompilePhase.PARSING.getPhaseNumber(), sourceUnit, @@ -1820,10 +1961,12 @@ else if( t instanceof Exception e ) } private void collectSyntaxError(SyntaxException e) { + e.printStackTrace(); sourceUnit.getErrorCollector().addFatalError(new SyntaxErrorMessage(e, sourceUnit)); } private void collectException(Exception e) { + e.printStackTrace(); sourceUnit.getErrorCollector().addException(e, sourceUnit); }