From 50e012e02dfebcc6fd1a00e382f745ee28a06870 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Sun, 21 Apr 2024 00:33:08 -0500 Subject: [PATCH] More parsing fixes and tons of error message improvements --- src/main/antlr/BoxScriptGrammar.g4 | 8 ++- src/main/antlr/CFScriptGrammar.g4 | 35 +++++------ src/main/antlr/CFTemplateGrammar.g4 | 3 +- src/main/antlr/CFTemplateLexer.g4 | 3 +- src/main/antlr/DocGrammar.g4 | 21 +------ src/main/antlr/DocLexer.g4 | 10 +-- .../compiler/parser/BoxScriptLexerCustom.java | 25 ++++++++ .../compiler/parser/BoxScriptParser.java | 34 +++++++--- .../compiler/parser/CFScriptLexerCustom.java | 25 ++++++++ .../compiler/parser/CFScriptParser.java | 39 +++++++++--- .../parser/CFTemplateLexerCustom.java | 28 +++++++++ .../compiler/parser/CFTemplateParser.java | 62 +++++++++++++++---- .../ortus/boxlang/compiler/parser/Parser.java | 3 +- .../components/system/ObjectComponent.java | 2 +- 14 files changed, 216 insertions(+), 82 deletions(-) diff --git a/src/main/antlr/BoxScriptGrammar.g4 b/src/main/antlr/BoxScriptGrammar.g4 index 8fa63c18c..8f7702ce6 100644 --- a/src/main/antlr/BoxScriptGrammar.g4 +++ b/src/main/antlr/BoxScriptGrammar.g4 @@ -49,7 +49,7 @@ function: eos*; // Declared arguments for a function -functionParamList: functionParam (COMMA functionParam)*; +functionParamList: functionParam (COMMA functionParam)* COMMA?; // required String param1="default" inject="something" functionParam: (REQUIRED)? (type)? identifier ( @@ -94,10 +94,12 @@ functionOrStatement: function | statement; // import java:foo.bar.Baz as myAlias; importStatement: - IMPORT (prefix = identifier COLON)? fqn (DOT STAR)? ( + IMPORT (prefix = identifier COLON)? importFQN ( AS alias = identifier )? eos?; +importFQN: fqn (DOT STAR)?; + // property name="foo" type="string" default="bar" inject="something"; property: javadoc? (preannotation)* PROPERTY postannotation* eos; @@ -208,7 +210,7 @@ assignmentRight: expression; argumentList: (namedArgument | positionalArgument) ( COMMA (namedArgument | positionalArgument) - )*; + )* COMMA?; /* func( foo = bar, baz = qux ) diff --git a/src/main/antlr/CFScriptGrammar.g4 b/src/main/antlr/CFScriptGrammar.g4 index 7f691692f..823e8fc36 100644 --- a/src/main/antlr/CFScriptGrammar.g4 +++ b/src/main/antlr/CFScriptGrammar.g4 @@ -117,31 +117,30 @@ classOrInterface: boxClass | interface; script: importStatement* functionOrStatement* | EOF; // import java:foo.bar.Baz as myAlias; -importStatement: IMPORT fqn eos?; +importStatement: IMPORT importFQN eos?; + +importFQN: fqn (DOT STAR)?; // include "myFile.bxm"; include: INCLUDE expression; // class {} boxClass: - importStatement* javadoc? (preannotation)* ABSTRACT? boxClassName postannotation* LBRACE - property* functionOrStatement* RBRACE; + importStatement* javadoc? ABSTRACT? boxClassName postannotation* LBRACE property* + functionOrStatement* RBRACE; boxClassName: CLASS_NAME; interface: - importStatement* javadoc? (preannotation)* INTERFACE postannotation* LBRACE interfaceFunction* - RBRACE; + importStatement* javadoc? INTERFACE postannotation* LBRACE interfaceFunction* RBRACE; // TODO: default method implementations -interfaceFunction: (preannotation)* functionSignature ( - postannotation - )* eos; +interfaceFunction: functionSignature ( postannotation)* eos; // public String myFunction( String foo, String bar ) functionSignature: - javadoc? (preannotation)* accessModifier? STATIC? returnType? FUNCTION identifier LPAREN - functionParamList? RPAREN; + javadoc? accessModifier? STATIC? returnType? FUNCTION identifier LPAREN functionParamList? + RPAREN; // UDF function: @@ -150,17 +149,13 @@ function: eos*; // Declared arguments for a function -functionParamList: functionParam (COMMA functionParam)*; +functionParamList: functionParam (COMMA functionParam)* COMMA?; // required String param1="default" inject="something" functionParam: (REQUIRED)? (type)? identifier ( EQUALSIGN expression )? postannotation*; -// @MyAnnotation "value". This is BL specific, so it's disabled in the CF grammar, but defined here -// in the base grammar for better rule reuse. -preannotation: AT fqn (literalExpression)*; - // foo=bar baz="bum" postannotation: key = identifier ( @@ -191,11 +186,10 @@ type: | ANY; // Allow any statement or a function. TODO: This may need to be changed if functions are allowed inside of functions -functionOrStatement: function | statement; +functionOrStatement: function | importStatement | statement; // property name="foo" type="string" default="bar" inject="something"; -property: - javadoc? (preannotation)* PROPERTY postannotation* eos; +property: javadoc? PROPERTY postannotation* eos; // /** Comment */ javadoc: JAVADOC_COMMENT; @@ -221,7 +215,8 @@ statementBlock: LBRACE (statement)* RBRACE eos?; statement: // This will "eat" random extra ; at the start of statements eos* ( - do + function + | do | for | if | switch @@ -285,7 +280,7 @@ assignmentRight: expression; argumentList: (namedArgument | positionalArgument) ( COMMA (namedArgument | positionalArgument) - )*; + )* COMMA?; /* func( foo = bar, baz = qux ) diff --git a/src/main/antlr/CFTemplateGrammar.g4 b/src/main/antlr/CFTemplateGrammar.g4 index 56860d087..ee6a2b951 100644 --- a/src/main/antlr/CFTemplateGrammar.g4 +++ b/src/main/antlr/CFTemplateGrammar.g4 @@ -57,7 +57,6 @@ attributeName: | INTERFACE | FUNCTION | ARGUMENT - | SCRIPT | RETURN | IF | ELSE @@ -133,7 +132,7 @@ component: // (zero or more) (whitespace? property)* // code in pseudo-constructor - statements + topLevelStatements // COMPONENT_OPEN SLASH_PREFIX COMPONENT COMPONENT_CLOSE; diff --git a/src/main/antlr/CFTemplateLexer.g4 b/src/main/antlr/CFTemplateLexer.g4 index 5e94523df..69319958f 100644 --- a/src/main/antlr/CFTemplateLexer.g4 +++ b/src/main/antlr/CFTemplateLexer.g4 @@ -84,8 +84,6 @@ INTERFACE: 'interface'; FUNCTION: 'function'; ARGUMENT: 'argument'; -SCRIPT: 'script' -> pushMode(XFSCRIPT); - // return may or may not have an expression, so eat any leading whitespace now so it doesn't give us an expression part that's just a space RETURN: 'return' [ \t\r\n]* -> pushMode(EXPRESSION_MODE_COMPONENT); @@ -128,6 +126,7 @@ fragment DIGIT: [0-9]; fragment COMPONENT_NameChar: COMPONENT_NameStartChar | '_' + | '-' | DIGIT | ':'; diff --git a/src/main/antlr/DocGrammar.g4 b/src/main/antlr/DocGrammar.g4 index 327dfbeff..65f6e7131 100644 --- a/src/main/antlr/DocGrammar.g4 +++ b/src/main/antlr/DocGrammar.g4 @@ -34,8 +34,6 @@ descriptionLineNoSpaceNoAt: | NAME | STAR | SLASH - | BRACE_OPEN - | BRACE_CLOSE | JAVADOC_START; descriptionNewline: NEWLINE; @@ -46,7 +44,7 @@ blockTag: SPACE? AT blockTagName SPACE? blockTagContent*; blockTagName: NAME; -blockTagContent: blockTagText | inlineTag | NEWLINE; +blockTagContent: blockTagText | NEWLINE; blockTagText: blockTagTextElement+; @@ -56,19 +54,4 @@ blockTagTextElement: | SPACE | STAR | SLASH - | BRACE_OPEN - | BRACE_CLOSE - | JAVADOC_START; - -inlineTag: - INLINE_TAG_START inlineTagName SPACE* inlineTagContent? BRACE_CLOSE; - -inlineTagName: NAME; - -inlineTagContent: braceContent+; - -braceExpression: BRACE_OPEN braceContent* BRACE_CLOSE; - -braceContent: braceExpression | braceText (NEWLINE* braceText)*; - -braceText: TEXT_CONTENT | NAME | SPACE | STAR | SLASH | NEWLINE; \ No newline at end of file + | JAVADOC_START; \ No newline at end of file diff --git a/src/main/antlr/DocLexer.g4 b/src/main/antlr/DocLexer.g4 index de8e6ae04..f282d2715 100644 --- a/src/main/antlr/DocLexer.g4 +++ b/src/main/antlr/DocLexer.g4 @@ -9,7 +9,7 @@ NEWLINE: SPACE: (' ' | '\t')+; -TEXT_CONTENT: ~[\n\r\t @*{}/a-zA-Z]+; +TEXT_CONTENT: ~[\n\r\t */a-zA-Z]+; AT: '@'; @@ -19,10 +19,4 @@ SLASH: '/'; JAVADOC_START: '/**' STAR*; -JAVADOC_END: SPACE? STAR* '*/'; - -INLINE_TAG_START: '{@'; - -BRACE_OPEN: '{'; - -BRACE_CLOSE: '}'; \ No newline at end of file +JAVADOC_END: SPACE? STAR* '*/'; \ No newline at end of file diff --git a/src/main/java/ortus/boxlang/compiler/parser/BoxScriptLexerCustom.java b/src/main/java/ortus/boxlang/compiler/parser/BoxScriptLexerCustom.java index ef5eed59a..743b935f2 100644 --- a/src/main/java/ortus/boxlang/compiler/parser/BoxScriptLexerCustom.java +++ b/src/main/java/ortus/boxlang/compiler/parser/BoxScriptLexerCustom.java @@ -91,4 +91,29 @@ public Token findPreviousToken( int type ) { } return null; } + + /** + * Back up to the closest unclosed brace + * Return null if none found + * * + * + * @return the unmatched opening brace + */ + public Token findUnclosedToken( int start, int end ) { + int count = 0; + reset(); + var tokens = getAllTokens(); + for ( int i = tokens.size() - 1; i >= 0; i-- ) { + Token t = tokens.get( i ); + if ( t.getType() == start ) { + count--; + } else if ( t.getType() == end ) { + count++; + } + if ( count < 0 ) { + return t; + } + } + return null; + } } \ No newline at end of file diff --git a/src/main/java/ortus/boxlang/compiler/parser/BoxScriptParser.java b/src/main/java/ortus/boxlang/compiler/parser/BoxScriptParser.java index 4229bd1c6..c5ca85f03 100644 --- a/src/main/java/ortus/boxlang/compiler/parser/BoxScriptParser.java +++ b/src/main/java/ortus/boxlang/compiler/parser/BoxScriptParser.java @@ -113,6 +113,7 @@ import ortus.boxlang.parser.antlr.BoxScriptGrammar.ParamContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.PreannotationContext; import ortus.boxlang.parser.antlr.BoxScriptLexer; +import ortus.boxlang.parser.antlr.CFScriptLexer; import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.components.ComponentDescriptor; import ortus.boxlang.runtime.services.ComponentService; @@ -227,10 +228,10 @@ public ParsingResult parse( String code, Boolean classOrInterface ) throws IOExc * @see BoxExpression */ public ParsingResult parseExpression( String code ) throws IOException { - InputStream inputStream = IOUtils.toInputStream( code, StandardCharsets.UTF_8 ); + InputStream inputStream = IOUtils.toInputStream( code, StandardCharsets.UTF_8 ); - BoxScriptLexer lexer = new BoxScriptLexer( CharStreams.fromStream( inputStream ) ); - BoxScriptGrammar parser = new BoxScriptGrammar( new CommonTokenStream( lexer ) ); + BoxScriptLexerCustom lexer = new BoxScriptLexerCustom( CharStreams.fromStream( inputStream ) ); + BoxScriptGrammar parser = new BoxScriptGrammar( new CommonTokenStream( lexer ) ); addErrorListeners( lexer, parser ); // var t = lexer.nextToken(); // while ( t.getType() != Token.EOF ) { @@ -243,6 +244,14 @@ public ParsingResult parseExpression( String code ) throws IOException { BoxExpression ast = toAst( null, parseTree ); return new ParsingResult( ast, issues ); } + Token unclosedParen = lexer.findUnclosedToken( CFScriptLexer.LPAREN, CFScriptLexer.RPAREN ); + if ( unclosedParen != null ) { + issues.clear(); + issues + .add( new Issue( "Unclosed parenthesis [(] on line " + ( unclosedParen.getLine() + this.startLine ), + createOffsetPosition( unclosedParen.getLine(), + unclosedParen.getCharPositionInLine(), unclosedParen.getLine(), unclosedParen.getCharPositionInLine() + 1 ) ) ); + } return new ParsingResult( null, issues ); } @@ -259,10 +268,10 @@ public ParsingResult parseExpression( String code ) throws IOException { * @see BoxStatement */ public ParsingResult parseStatement( String code ) throws IOException { - InputStream inputStream = IOUtils.toInputStream( code, StandardCharsets.UTF_8 ); + InputStream inputStream = IOUtils.toInputStream( code, StandardCharsets.UTF_8 ); - BoxScriptLexer lexer = new BoxScriptLexer( CharStreams.fromStream( inputStream ) ); - BoxScriptGrammar parser = new BoxScriptGrammar( new CommonTokenStream( lexer ) ); + BoxScriptLexerCustom lexer = new BoxScriptLexerCustom( CharStreams.fromStream( inputStream ) ); + BoxScriptGrammar parser = new BoxScriptGrammar( new CommonTokenStream( lexer ) ); addErrorListeners( lexer, parser ); BoxScriptGrammar.FunctionOrStatementContext parseTree = parser.functionOrStatement(); @@ -344,6 +353,15 @@ protected BoxNode parserFirstStage( InputStream stream, Boolean classOrInterface // Don't attempt to build AST if there are parsing issues if ( !issues.isEmpty() ) { + Token unclosedBrace = lexer.findUnclosedToken( BoxScriptLexer.LBRACE, BoxScriptLexer.RBRACE ); + if ( unclosedBrace != null ) { + issues.clear(); + issues.add( + new Issue( "Unclosed curly brace [{] on line " + ( unclosedBrace.getLine() + this.startLine ), + createOffsetPosition( unclosedBrace.getLine(), + unclosedBrace.getCharPositionInLine(), unclosedBrace.getLine(), unclosedBrace.getCharPositionInLine() + 1 ) ) ); + return null; + } return null; } @@ -428,12 +446,12 @@ private BoxNode toAst( File file, BoxClassContext component ) { private BoxImport toAst( File file, BoxScriptGrammar.ImportStatementContext rule ) { BoxExpression expr = null; BoxIdentifier alias = null; - if ( rule.fqn() != null ) { + if ( rule.importFQN() != null ) { String prefix = ""; if ( rule.prefix != null ) { prefix = rule.prefix.getText() + ":"; } - expr = new BoxFQN( prefix + rule.fqn().getText(), getPosition( rule.fqn() ), getSourceText( rule.fqn() ) ); + expr = new BoxFQN( prefix + rule.importFQN().getText(), getPosition( rule.importFQN() ), getSourceText( rule.importFQN() ) ); } if ( rule.alias != null ) { BoxExpression tmp = toAst( file, rule.alias ); diff --git a/src/main/java/ortus/boxlang/compiler/parser/CFScriptLexerCustom.java b/src/main/java/ortus/boxlang/compiler/parser/CFScriptLexerCustom.java index f11f17759..34ac89b12 100644 --- a/src/main/java/ortus/boxlang/compiler/parser/CFScriptLexerCustom.java +++ b/src/main/java/ortus/boxlang/compiler/parser/CFScriptLexerCustom.java @@ -129,4 +129,29 @@ public Token nextToken() { return nextToken; } + /** + * Back up to the closest unclosed brace + * Return null if none found + * * + * + * @return the unmatched opening brace + */ + public Token findUnclosedToken( int start, int end ) { + int count = 0; + reset(); + var tokens = getAllTokens(); + for ( int i = tokens.size() - 1; i >= 0; i-- ) { + Token t = tokens.get( i ); + if ( t.getType() == start ) { + count--; + } else if ( t.getType() == end ) { + count++; + } + if ( count < 0 ) { + return t; + } + } + return null; + } + } \ No newline at end of file diff --git a/src/main/java/ortus/boxlang/compiler/parser/CFScriptParser.java b/src/main/java/ortus/boxlang/compiler/parser/CFScriptParser.java index fe8c1140c..aa4d28fd6 100644 --- a/src/main/java/ortus/boxlang/compiler/parser/CFScriptParser.java +++ b/src/main/java/ortus/boxlang/compiler/parser/CFScriptParser.java @@ -226,10 +226,10 @@ public ParsingResult parse( String code, Boolean classOrInterface ) throws IOExc * @see BoxExpression */ public ParsingResult parseExpression( String code ) throws IOException { - InputStream inputStream = IOUtils.toInputStream( code, StandardCharsets.UTF_8 ); + InputStream inputStream = IOUtils.toInputStream( code, StandardCharsets.UTF_8 ); - CFScriptLexer lexer = new CFScriptLexer( CharStreams.fromStream( inputStream ) ); - CFScriptGrammar parser = new CFScriptGrammar( new CommonTokenStream( lexer ) ); + CFScriptLexerCustom lexer = new CFScriptLexerCustom( CharStreams.fromStream( inputStream ) ); + CFScriptGrammar parser = new CFScriptGrammar( new CommonTokenStream( lexer ) ); addErrorListeners( lexer, parser ); // var t = lexer.nextToken(); // while ( t.getType() != Token.EOF ) { @@ -242,6 +242,14 @@ public ParsingResult parseExpression( String code ) throws IOException { BoxExpression ast = toAst( null, parseTree ); return new ParsingResult( ast, issues ); } + Token unclosedParen = lexer.findUnclosedToken( CFScriptLexer.LPAREN, CFScriptLexer.RPAREN ); + if ( unclosedParen != null ) { + issues.clear(); + issues + .add( new Issue( "Unclosed parenthesis [(] on line " + ( unclosedParen.getLine() + this.startLine ), + createOffsetPosition( unclosedParen.getLine(), + unclosedParen.getCharPositionInLine(), unclosedParen.getLine(), unclosedParen.getCharPositionInLine() + 1 ) ) ); + } return new ParsingResult( null, issues ); } @@ -260,7 +268,7 @@ public ParsingResult parseExpression( String code ) throws IOException { public ParsingResult parseStatement( String code ) throws IOException { InputStream inputStream = IOUtils.toInputStream( code, StandardCharsets.UTF_8 ); - CFScriptLexer lexer = new CFScriptLexer( CharStreams.fromStream( inputStream ) ); + CFScriptLexer lexer = new CFScriptLexerCustom( CharStreams.fromStream( inputStream ) ); CFScriptGrammar parser = new CFScriptGrammar( new CommonTokenStream( lexer ) ); addErrorListeners( lexer, parser ); CFScriptGrammar.FunctionOrStatementContext parseTree = parser.functionOrStatement(); @@ -343,6 +351,15 @@ protected BoxNode parserFirstStage( InputStream stream, Boolean classOrInterface // Don't attempt to build AST if there are parsing issues if ( !issues.isEmpty() ) { + Token unclosedBrace = lexer.findUnclosedToken( CFScriptLexer.LBRACE, CFScriptLexer.RBRACE ); + if ( unclosedBrace != null ) { + issues.clear(); + issues.add( + new Issue( "Unclosed curly brace [{] on line " + ( unclosedBrace.getLine() + this.startLine ), + createOffsetPosition( unclosedBrace.getLine(), + unclosedBrace.getCharPositionInLine(), unclosedBrace.getLine(), unclosedBrace.getCharPositionInLine() + 1 ) ) ); + return null; + } return null; } @@ -418,7 +435,11 @@ private BoxNode toAst( File file, BoxClassContext component ) { property.add( toAst( file, annotation ) ); } component.functionOrStatement().forEach( stmt -> { - body.add( toAst( file, stmt ) ); + if ( stmt.importStatement() != null ) { + imports.add( toAst( file, stmt.importStatement() ) ); + } else { + body.add( toAst( file, stmt ) ); + } } ); return new BoxClass( imports, body, annotations, documentation, property, getPosition( component ), getSourceText( component ) ); @@ -438,7 +459,7 @@ private BoxImport toAst( File file, CFScriptGrammar.ImportStatementContext rule BoxExpression expr = null; BoxIdentifier alias = null; String prefix = "bx:"; - expr = new BoxFQN( prefix + rule.fqn().getText(), getPosition( rule.fqn() ), getSourceText( rule.fqn() ) ); + expr = new BoxFQN( prefix + rule.importFQN().getText(), getPosition( rule.importFQN() ), getSourceText( rule.importFQN() ) ); return new BoxImport( expr, alias, getPosition( rule ), getSourceText( rule ) ); } @@ -458,6 +479,8 @@ private BoxStatement toAst( File file, CFScriptGrammar.FunctionOrStatementContex return toAst( file, node.function() ); } else if ( node.statement() != null ) { return toAst( file, node.statement() ); + } else if ( node.importStatement() != null ) { + return toAst( file, node.importStatement() ); } else { throw new IllegalStateException( "not implemented: " + node.getClass().getSimpleName() ); } @@ -474,7 +497,9 @@ private BoxStatement toAst( File file, CFScriptGrammar.FunctionOrStatementContex * @see BoxStatement */ private BoxStatement toAst( File file, CFScriptGrammar.StatementContext node ) { - if ( node.simpleStatement() != null ) { + if ( node.function() != null ) { + return toAst( file, node.function() ); + } else if ( node.simpleStatement() != null ) { return toAst( file, node.simpleStatement() ); } else if ( node.if_() != null ) { return toAst( file, node.if_() ); diff --git a/src/main/java/ortus/boxlang/compiler/parser/CFTemplateLexerCustom.java b/src/main/java/ortus/boxlang/compiler/parser/CFTemplateLexerCustom.java index 8b46c6081..c4b52e59f 100644 --- a/src/main/java/ortus/boxlang/compiler/parser/CFTemplateLexerCustom.java +++ b/src/main/java/ortus/boxlang/compiler/parser/CFTemplateLexerCustom.java @@ -75,6 +75,7 @@ public boolean lastModeWas( int mode ) { /** * Get the last token of a specific type + * Returns null if not found * * @param type type of token to find * @@ -91,4 +92,31 @@ public Token findPreviousToken( int type ) { } return null; } + + /** + * Get the last token of a specific type and the next x siblings + * Returns empty list if not found + * + * @param type type of token to find + * @param count number of siblings to find + * + * @return the list of tokens starting from the specified type + */ + public List findPreviousTokenAndXSiblings( int type, int count ) { + reset(); + List results = new ArrayList(); + var tokens = getAllTokens(); + for ( int i = tokens.size() - 1; i >= 0; i-- ) { + Token t = tokens.get( i ); + if ( t.getType() == type ) { + results.add( t ); + for ( int j = 1; j <= count; j++ ) { + results.add( tokens.get( i + j ) ); + } + return results; + } + } + return results; + } + } \ No newline at end of file diff --git a/src/main/java/ortus/boxlang/compiler/parser/CFTemplateParser.java b/src/main/java/ortus/boxlang/compiler/parser/CFTemplateParser.java index 8bbf5c8bd..4e1ee4974 100644 --- a/src/main/java/ortus/boxlang/compiler/parser/CFTemplateParser.java +++ b/src/main/java/ortus/boxlang/compiler/parser/CFTemplateParser.java @@ -170,6 +170,23 @@ protected BoxNode parserFirstStage( InputStream inputStream, Boolean classOrInte } message += " on line " + position.getStart().getLine(); issues.add( new Issue( message, position ) ); + } else if ( lexer.lastModeWas( CFTemplateLexerCustom.COMPONENT_MODE ) ) { + String message = "Unclosed tag"; + Token startToken = lexer.findPreviousToken( CFTemplateLexerCustom.COMPONENT_OPEN ); + if ( startToken != null ) { + position = createOffsetPosition( startToken.getLine(), startToken.getCharPositionInLine(), startToken.getLine(), + startToken.getCharPositionInLine() + startToken.getText().length() ); + List nameTokens = lexer.findPreviousTokenAndXSiblings( CFTemplateLexerCustom.COMPONENT_OPEN, 2 ); + if ( !nameTokens.isEmpty() ) { + message += " ["; + for ( var t : nameTokens ) { + message += t.getText(); + } + message += "]"; + } + } + message += " starting on line " + position.getStart().getLine(); + issues.add( new Issue( message, position ) ); } else { issues.add( new Issue( "Invalid Syntax. (Unpopped modes) [" + modes.stream().collect( Collectors.joining( ", " ) ) + "]", position ) ); } @@ -248,8 +265,16 @@ private BoxNode toAst( File file, ComponentContext node ) { annotations.add( toAst( file, attr ) ); } - if ( node.statements() != null ) { - body.addAll( toAst( file, node.statements() ) ); + if ( node.topLevelStatements() != null ) { + body.addAll( toAst( file, node.topLevelStatements() ) ); + } + // loop over body and move any BoxImport statements to the imports list + for ( int i = body.size() - 1; i >= 0; i-- ) { + BoxStatement statement = body.get( i ); + if ( statement instanceof BoxImport boxImport ) { + imports.add( boxImport ); + body.remove( i ); + } } for ( CFTemplateGrammar.PropertyContext annotation : node.property() ) { properties.add( toAst( file, annotation ) ); @@ -325,16 +350,11 @@ private List statementsToAst( File file, ParserRuleContext node ) if ( child instanceof StatementContext statement ) { if ( statement.genericCloseComponent() != null ) { String componentName = statement.genericCloseComponent().componentName().getText(); - ComponentDescriptor descriptor = componentService.getComponent( componentName ); - if ( descriptor != null ) { - if ( !descriptor.allowsBody() ) { - issues.add( new Issue( "The [" + componentName + "] component does not allow a body", getPosition( node ) ) ); - } - } // see if statements list has a BoxComponent with this name - int size = statements.size(); - boolean foundStart = false; - int removeAfter = -1; + int size = statements.size(); + boolean foundStart = false; + int removeAfter = -1; + List bodyStatements = new ArrayList<>(); // loop backwards checking for a BoxComponent with this name for ( int i = size - 1; i >= 0; i-- ) { BoxStatement boxStatement = statements.get( i ); @@ -343,6 +363,7 @@ private List statementsToAst( File file, ParserRuleContext node ) foundStart = true; // slice all statements from this position to the end and set them as the body of the start component boxComponent.setBody( new ArrayList<>( statements.subList( i + 1, size ) ) ); + bodyStatements = boxComponent.getBody(); boxComponent.getPosition().setEnd( getPosition( statement.genericCloseComponent() ).getEnd() ); boxComponent.setSourceText( getSourceText( boxComponent.getSourceStartIndex(), statement.genericCloseComponent() ) ); removeAfter = i; @@ -361,6 +382,12 @@ private List statementsToAst( File file, ParserRuleContext node ) getPosition( statement.genericCloseComponent() ) ) ); } + ComponentDescriptor descriptor = componentService.getComponent( componentName ); + if ( descriptor != null ) { + if ( !descriptor.allowsBody() && ( !allStatementsAreWhitespace( bodyStatements ) ) ) { + issues.add( new Issue( "The [" + componentName + "] component does not allow a body", getPosition( node ) ) ); + } + } } else { statements.add( toAst( file, statement ) ); } @@ -392,6 +419,19 @@ private List statementsToAst( File file, ParserRuleContext node ) return statements; } + private boolean allStatementsAreWhitespace( List bodyStatements ) { + for ( BoxStatement statement : bodyStatements ) { + if ( statement instanceof BoxBufferOutput bffr ) { + if ( bffr.getExpression() instanceof BoxStringLiteral str && !str.getValue().isBlank() ) { + return false; + } + } else { + return false; + } + } + return true; + } + private BoxStatement toAst( File file, StatementContext node ) { if ( node.output() != null ) { return toAst( file, node.output() ); diff --git a/src/main/java/ortus/boxlang/compiler/parser/Parser.java b/src/main/java/ortus/boxlang/compiler/parser/Parser.java index 95f9f8bb9..da65b5bb3 100644 --- a/src/main/java/ortus/boxlang/compiler/parser/Parser.java +++ b/src/main/java/ortus/boxlang/compiler/parser/Parser.java @@ -214,7 +214,8 @@ public static BoxSourceType detectFile( File file ) { try ( BufferedReader reader = Files.newBufferedReader( file.toPath() ) ) { String line; while ( ( line = reader.readLine() ) != null ) { - line = line.toLowerCase().trim(); + line = line.replaceFirst( "^\uFEFF", "" ).replaceFirst( "^\uFFFE", "" ).replaceFirst( "^\u0000FEFF", "" ) + .replaceFirst( "^\uFFFE0000", "" ).toLowerCase().trim(); if ( line.startsWith( "component" ) || line.startsWith( "interface" ) ) { return BoxSourceType.CFSCRIPT; } diff --git a/src/main/java/ortus/boxlang/runtime/components/system/ObjectComponent.java b/src/main/java/ortus/boxlang/runtime/components/system/ObjectComponent.java index a1767444b..ab2a113e5 100644 --- a/src/main/java/ortus/boxlang/runtime/components/system/ObjectComponent.java +++ b/src/main/java/ortus/boxlang/runtime/components/system/ObjectComponent.java @@ -31,7 +31,7 @@ import ortus.boxlang.runtime.validation.Validator; // Weirdly named so it doesn't conflict with `java.lang.Object` -@BoxComponent( name = "Object", requiresBody = true ) +@BoxComponent( name = "Object", allowsBody = true ) public class ObjectComponent extends Component { private final Key createObjectKey = Key.of( "createObject" );