diff --git a/src/Lua/CodeAnalysis/Compilation/Scanner.cs b/src/Lua/CodeAnalysis/Compilation/Scanner.cs index da5e880b..ecd7e1fe 100644 --- a/src/Lua/CodeAnalysis/Compilation/Scanner.cs +++ b/src/Lua/CodeAnalysis/Compilation/Scanner.cs @@ -116,7 +116,7 @@ public void SyntaxError(string message) ScanError(R.Position, message, Token.T); } - public void ErrorExpected(int position, char t) + public void ErrorExpected(int position, int t) { SyntaxError(position, TokenToString(t) + " expected"); } @@ -187,13 +187,12 @@ public static string TokenToString(Token t) }; } - public string TokenToString(int t) + public static string TokenToString(int t) { return t switch { - TkName or TkString => Token.S, - TkNumber => $"{Token.N}", - < FirstReserved => $"{(char)t}", // TODO check for printable rune + < FirstReserved when t is >= ' ' and <= '~' => $"'{(char)t}'", + < FirstReserved => CharTokenToLiteral(t), < TkEos => $"'{tokens[t - FirstReserved]}'", _ => tokens[t - FirstReserved], }; @@ -938,7 +937,8 @@ public void Check(int t) { if (Token.T != t) { - ErrorExpected(R.Position, (char)t); + // Reserved tokens exceed char.MaxValue; narrowing them loses the token identity. + ErrorExpected(R.Position, t); } } @@ -951,7 +951,7 @@ public void CheckMatch(int what, int who, int where) if (where == LineNumber) { - ErrorExpected(R.Position, (char)what); + ErrorExpected(R.Position, what); } else { diff --git a/src/Lua/CodeAnalysis/Syntax/Lexer.cs b/src/Lua/CodeAnalysis/Syntax/Lexer.cs index 3dd2985e..5ded48a6 100644 --- a/src/Lua/CodeAnalysis/Syntax/Lexer.cs +++ b/src/Lua/CodeAnalysis/Syntax/Lexer.cs @@ -16,6 +16,8 @@ public Lexer() { } public readonly SyntaxToken Current => current; + internal readonly SourcePosition Position => position; + [MethodImpl(MethodImplOptions.AggressiveInlining)] void Advance(int count) { diff --git a/src/Lua/CodeAnalysis/Syntax/LuaSyntaxTree.cs b/src/Lua/CodeAnalysis/Syntax/LuaSyntaxTree.cs index 820ad053..780845dd 100644 --- a/src/Lua/CodeAnalysis/Syntax/LuaSyntaxTree.cs +++ b/src/Lua/CodeAnalysis/Syntax/LuaSyntaxTree.cs @@ -21,6 +21,7 @@ public static LuaSyntaxTree Parse(string source, string? chunkName = null) parser.Add(lexer.Current); } + parser.EndPosition = lexer.Position; return parser.Parse(); } } diff --git a/src/Lua/CodeAnalysis/Syntax/Parser.cs b/src/Lua/CodeAnalysis/Syntax/Parser.cs index 65e1e8a3..2fd644d3 100644 --- a/src/Lua/CodeAnalysis/Syntax/Parser.cs +++ b/src/Lua/CodeAnalysis/Syntax/Parser.cs @@ -10,6 +10,8 @@ public ref struct Parser { public string? ChunkName { get; init; } + internal SourcePosition EndPosition { get; set; } + PooledList tokens; [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -213,7 +215,7 @@ DoStatementNode ParseDoStatement(ref SyntaxTokenEnumerator enumerator) var doToken = enumerator.Current; // parse statements - var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, out _); + var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, doToken, out _); return new(statements, doToken.Position); } @@ -317,11 +319,7 @@ IfStatementNode ParseIfStatement(ref SyntaxTokenEnumerator enumerator) { if (!enumerator.MoveNext()) { - LuaParseException.ExpectedToken( - ChunkName, - enumerator.Current.Position, - SyntaxTokenType.End - ); + MissingClosingToken(ref enumerator, SyntaxTokenType.End, ifToken, null); } var tokenType = enumerator.Current.Type; @@ -395,6 +393,16 @@ IfStatementNode ParseIfStatement(ref SyntaxTokenEnumerator enumerator) } } + if (tokenType == SyntaxTokenType.Until) + { + MissingClosingToken( + ref enumerator, + SyntaxTokenType.End, + ifToken, + enumerator.Current + ); + } + var node = ParseStatement(ref enumerator); builder.Add(node); } @@ -418,7 +426,7 @@ WhileStatementNode ParseWhileStatement(ref SyntaxTokenEnumerator enumerator) CheckCurrent(ref enumerator, SyntaxTokenType.Do); // parse statements - var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, out _); + var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, whileToken, out _); return new(condition, statements, whileToken.Position); } @@ -430,7 +438,12 @@ RepeatStatementNode ParseRepeatStatement(ref SyntaxTokenEnumerator enumerator) var repeatToken = enumerator.Current; // parse statements - var statements = ParseStatementList(ref enumerator, SyntaxTokenType.Until, out _); + var statements = ParseStatementList( + ref enumerator, + SyntaxTokenType.Until, + repeatToken, + out _ + ); // skip 'until keyword' CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Until, out _); @@ -489,7 +502,7 @@ SyntaxToken forToken var doToken = enumerator.Current; // parse statements - var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, out _); + var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, forToken, out _); return new( varName, @@ -522,7 +535,12 @@ SyntaxToken forToken CheckCurrent(ref enumerator, SyntaxTokenType.Do); var doToken = enumerator.Current; // parse statements - var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, out var endToken); + var statements = ParseStatementList( + ref enumerator, + SyntaxTokenType.End, + forToken, + out var endToken + ); return new( identifiers, @@ -540,7 +558,7 @@ SyntaxToken functionToken ) { var (Name, Identifiers, Statements, HasVariableArgments, LineDefined, EndPosition) = - ParseFunctionDeclarationCore(ref enumerator, false); + ParseFunctionDeclarationCore(ref enumerator, false, functionToken); return new( Name, Identifiers, @@ -558,7 +576,7 @@ SyntaxToken functionToken ) { var (Name, Identifiers, Statements, HasVariableArgments, LineDefined, EndPosition) = - ParseFunctionDeclarationCore(ref enumerator, false); + ParseFunctionDeclarationCore(ref enumerator, false, functionToken); return new( Name, Identifiers, @@ -577,7 +595,11 @@ SyntaxToken functionToken bool HasVariableArgments, int LineDefined, SourcePosition EndPosition - ) ParseFunctionDeclarationCore(ref SyntaxTokenEnumerator enumerator, bool isAnonymous) + ) ParseFunctionDeclarationCore( + ref SyntaxTokenEnumerator enumerator, + bool isAnonymous, + SyntaxToken functionToken + ) { ReadOnlyMemory name; @@ -616,7 +638,12 @@ enumerator.Current.Type is SyntaxTokenType.Identifier CheckCurrent(ref enumerator, SyntaxTokenType.RParen); // parse statements - var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, out var endToken); + var statements = ParseStatementList( + ref enumerator, + SyntaxTokenType.End, + functionToken, + out var endToken + ); return ( name, @@ -686,7 +713,12 @@ enumerator.Current.Type is SyntaxTokenType.Identifier CheckCurrent(ref enumerator, SyntaxTokenType.RParen); // parse statements - var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, out var endToken); + var statements = ParseStatementList( + ref enumerator, + SyntaxTokenType.End, + functionToken, + out var endToken + ); return new( names.AsSpan().ToArray(), @@ -905,6 +937,18 @@ ref SyntaxTokenEnumerator enumerator while (enumerator.MoveNext()) { var currentToken = enumerator.Current; + if ( + IsBlockEndToken(currentToken.Type) + || currentToken.Type is SyntaxTokenType.RParen or SyntaxTokenType.RSquare + ) + { + MissingClosingToken( + ref enumerator, + SyntaxTokenType.RCurly, + startToken, + currentToken + ); + } switch (currentToken.Type) { case SyntaxTokenType.RCurly: @@ -915,16 +959,19 @@ ref SyntaxTokenEnumerator enumerator continue; case SyntaxTokenType.LSquare: // general style ([key] = value) - enumerator.MoveNext(); + MoveNextWithValidation(ref enumerator); + enumerator.SkipEoL(); var keyExpression = ParseExpression( ref enumerator, OperatorPrecedence.NonOperator ); - enumerator.MoveNext(); + MoveNextWithValidation(ref enumerator); + enumerator.SkipEoL(); // skip '] =' CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.RSquare, out _); + enumerator.SkipEoL(); CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Assignment, out _); var valueExpression = ParseExpression( @@ -975,6 +1022,8 @@ when enumerator.GetNext(true).Type is SyntaxTokenType.Assignment: } } + MissingClosingToken(ref enumerator, SyntaxTokenType.RCurly, startToken, null); + RETURN: return new(items.AsSpan().ToArray(), startToken.Position); } @@ -1075,10 +1124,7 @@ GroupedExpressionNode ParseGroupedExpression(ref SyntaxTokenEnumerator enumerato enumerator.SkipEoL(); var expression = ParseExpression(ref enumerator, GetPrecedence(enumerator.Current.Type)); - MoveNextWithValidation(ref enumerator); - - // check ')' - CheckCurrent(ref enumerator, SyntaxTokenType.RParen); + MoveNextAndCheckClosingToken(ref enumerator, SyntaxTokenType.RParen, lParen); return new(expression, lParen.Position); } @@ -1112,7 +1158,7 @@ ref SyntaxTokenEnumerator enumerator enumerator.SkipEoL(); var (_, Identifiers, Statements, HasVariableArgments, LineDefined, LastLineDefined) = - ParseFunctionDeclarationCore(ref enumerator, true); + ParseFunctionDeclarationCore(ref enumerator, true, functionToken); return new( Identifiers, Statements, @@ -1145,7 +1191,7 @@ ExpressionNode[] ParseCallFunctionArguments(ref SyntaxTokenEnumerator enumerator } // check and skip '(' - CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.LParen, out _); + CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.LParen, out var lParen); ExpressionNode[] arguments; if (enumerator.Current.Type is SyntaxTokenType.RParen) @@ -1159,11 +1205,7 @@ ExpressionNode[] ParseCallFunctionArguments(ref SyntaxTokenEnumerator enumerator arguments = ParseExpressionList(ref enumerator); enumerator.SkipEoL(); - MoveNextWithValidation(ref enumerator); - enumerator.SkipEoL(); - - // check ')' - CheckCurrent(ref enumerator, SyntaxTokenType.RParen); + MoveNextAndCheckClosingToken(ref enumerator, SyntaxTokenType.RParen, lParen); } return arguments; @@ -1241,6 +1283,7 @@ IdentifierNode[] ParseIdentifierList(ref SyntaxTokenEnumerator enumerator) StatementNode[] ParseStatementList( ref SyntaxTokenEnumerator enumerator, SyntaxTokenType endTokenType, + SyntaxToken openingToken, out SyntaxToken endToken ) { @@ -1251,7 +1294,12 @@ out SyntaxToken endToken { if (enumerator.Current.Type == endTokenType) { - break; + goto RETURN; + } + + if (IsBlockEndToken(enumerator.Current.Type)) + { + MissingClosingToken(ref enumerator, endTokenType, openingToken, enumerator.Current); } if (enumerator.Current.Type is SyntaxTokenType.EndOfLine or SyntaxTokenType.SemiColon) @@ -1263,11 +1311,74 @@ out SyntaxToken endToken statements.Add(node); } + MissingClosingToken(ref enumerator, endTokenType, openingToken, null); + + RETURN: endToken = enumerator.Current; return statements.AsSpan().ToArray(); } + static bool IsBlockEndToken(SyntaxTokenType type) + { + return type + is SyntaxTokenType.End + or SyntaxTokenType.Else + or SyntaxTokenType.ElseIf + or SyntaxTokenType.Until; + } + + SourcePosition GetEndPosition(ref SyntaxTokenEnumerator enumerator) + { + // Callers feeding tokens directly may not have a source EOF position. + return EndPosition.Line > 0 ? EndPosition : enumerator.Current.Position; + } + + void MissingClosingToken( + ref SyntaxTokenEnumerator enumerator, + SyntaxTokenType expectedToken, + SyntaxToken openingToken, + SyntaxToken? nearToken + ) + { + LuaParseException.ExpectedClosingToken( + ChunkName, + nearToken?.Position ?? GetEndPosition(ref enumerator), + expectedToken, + openingToken, + nearToken + ); + } + + void MoveNextAndCheckClosingToken( + ref SyntaxTokenEnumerator enumerator, + SyntaxTokenType expectedToken, + SyntaxToken openingToken + ) + { + if (!enumerator.MoveNext()) + { + MissingClosingToken(ref enumerator, expectedToken, openingToken, null); + } + enumerator.SkipEoL(); + if (enumerator.Current.Type != expectedToken) + { + MissingClosingToken( + ref enumerator, + expectedToken, + openingToken, + GetNearToken(ref enumerator) + ); + } + } + + static SyntaxToken? GetNearToken(ref SyntaxTokenEnumerator enumerator) + { + return enumerator.Current.Type == SyntaxTokenType.EndOfLine && enumerator.IsCompleted + ? null + : enumerator.Current; + } + void CheckCurrentAndSkip( ref SyntaxTokenEnumerator enumerator, SyntaxTokenType expectedToken, @@ -1284,7 +1395,13 @@ void CheckCurrent(ref SyntaxTokenEnumerator enumerator, SyntaxTokenType expected { if (enumerator.Current.Type != expectedToken) { - LuaParseException.ExpectedToken(ChunkName, enumerator.Current.Position, expectedToken); + var nearToken = GetNearToken(ref enumerator); + LuaParseException.ExpectedToken( + ChunkName, + nearToken?.Position ?? GetEndPosition(ref enumerator), + expectedToken, + nearToken + ); } } @@ -1293,11 +1410,8 @@ void MoveNextWithValidation(ref SyntaxTokenEnumerator enumerator) { if (!enumerator.MoveNext()) { - LuaParseException.SyntaxError( - ChunkName, - enumerator.Current.Position, - enumerator.Current - ); + // A failed MoveNext retains the last token, which is not the token at EOF. + LuaParseException.SyntaxError(ChunkName, GetEndPosition(ref enumerator), null); } } diff --git a/src/Lua/CodeAnalysis/Syntax/SyntaxToken.cs b/src/Lua/CodeAnalysis/Syntax/SyntaxToken.cs index 64cea4f6..2be326d2 100644 --- a/src/Lua/CodeAnalysis/Syntax/SyntaxToken.cs +++ b/src/Lua/CodeAnalysis/Syntax/SyntaxToken.cs @@ -304,6 +304,18 @@ public override string ToString() public string ToDisplayString() { return Type switch + { + SyntaxTokenType.Number => Text.ToString(), + SyntaxTokenType.String => $"\"{Text}\"", + SyntaxTokenType.RawString => $"[[{Text}]]", + SyntaxTokenType.Identifier => Text.ToString(), + _ => GetFixedText(Type) ?? "", + }; + } + + internal static string? GetFixedText(SyntaxTokenType type) + { + return type switch { SyntaxTokenType.EndOfLine => Keywords.LF, SyntaxTokenType.LParen => Keywords.LParen, @@ -313,14 +325,13 @@ public string ToDisplayString() SyntaxTokenType.LSquare => Keywords.LSquare, SyntaxTokenType.RSquare => Keywords.RSquare, SyntaxTokenType.SemiColon => ";", + SyntaxTokenType.Colon => ":", SyntaxTokenType.Comma => ",", - SyntaxTokenType.Number => Text.ToString(), - SyntaxTokenType.String => $"\"{Text}\"", - SyntaxTokenType.RawString => $"[[{Text}]]", + SyntaxTokenType.Dot => ".", + SyntaxTokenType.VarArg => "...", SyntaxTokenType.Nil => Keywords.Nil, SyntaxTokenType.True => Keywords.True, SyntaxTokenType.False => Keywords.False, - SyntaxTokenType.Identifier => Text.ToString(), SyntaxTokenType.Addition => Keywords.Addition, SyntaxTokenType.Subtraction => Keywords.Subtraction, SyntaxTokenType.Multiplication => Keywords.Multiplication, @@ -355,7 +366,7 @@ public string ToDisplayString() SyntaxTokenType.Return => Keywords.Return, SyntaxTokenType.Until => Keywords.Until, SyntaxTokenType.While => Keywords.While, - _ => "", + _ => null, }; } diff --git a/src/Lua/Exceptions.cs b/src/Lua/Exceptions.cs index 4e9cffa5..a95a2b79 100644 --- a/src/Lua/Exceptions.cs +++ b/src/Lua/Exceptions.cs @@ -21,7 +21,7 @@ SyntaxToken token throw new LuaParseException( chunkName, position, - $"unexpected symbol <{token.Type}> near '{token.Text}'" + $"unexpected symbol near {GetNearTokenText(token)}" ); } @@ -31,7 +31,71 @@ public static void ExpectedToken( SyntaxTokenType token ) { - throw new LuaParseException(chunkName, position, $"'{token}' expected"); + throw new LuaParseException(chunkName, position, $"{GetTokenTypeText(token)} expected"); + } + + internal static void ExpectedToken( + string? chunkName, + SourcePosition position, + SyntaxTokenType token, + SyntaxToken? nearToken + ) + { + throw new LuaParseException( + chunkName, + position, + $"{GetTokenTypeText(token)} expected near {GetNearTokenText(nearToken)}" + ); + } + + static string GetTokenTypeText(SyntaxTokenType token) + { + // Lua's token2str quotes fixed syntax, but uses unquoted categories for literals/names. + return token switch + { + SyntaxTokenType.Identifier => "", + SyntaxTokenType.Number => "", + SyntaxTokenType.String or SyntaxTokenType.RawString => "", + SyntaxTokenType.EndOfLine => "", + _ => SyntaxToken.GetFixedText(token) is { } text + ? $"'{text}'" + : $"<{token.ToString().ToLowerInvariant()}>", + }; + } + + internal static void ExpectedClosingToken( + string? chunkName, + SourcePosition position, + SyntaxTokenType token, + SyntaxToken openingToken, + SyntaxToken? nearToken + ) + { + // Like Lua's check_match, a different line needs the opening construct's context. + var context = + openingToken.Position.Line == position.Line + ? "" + : $" (to close {GetTokenTypeText(openingToken.Type)} at line {openingToken.Position.Line})"; + throw new LuaParseException( + chunkName, + position, + $"{GetTokenTypeText(token)} expected{context} near {GetNearTokenText(nearToken)}" + ); + } + + static string GetNearTokenText(SyntaxToken? token) + { + if (token is not { } value) + { + return ""; + } + + return value.Type switch + { + SyntaxTokenType.Identifier or SyntaxTokenType.Number => $"'{value.Text}'", + SyntaxTokenType.String or SyntaxTokenType.RawString => $"'{value.ToDisplayString()}'", + _ => GetTokenTypeText(value.Type), + }; } public static void UnfinishedLongComment(string? chunkName, SourcePosition position) @@ -48,7 +112,7 @@ public static void SyntaxError(string? chunkName, SourcePosition position, Synta throw new LuaParseException( chunkName, position, - $"syntax error {(token == null ? "" : $"near '{token.Value.Text}'")}" + $"syntax error near {GetNearTokenText(token)}" ); } diff --git a/tests/Lua.Tests/ParserTests.cs b/tests/Lua.Tests/ParserTests.cs index dba9f670..f208fbf8 100644 --- a/tests/Lua.Tests/ParserTests.cs +++ b/tests/Lua.Tests/ParserTests.cs @@ -7,6 +7,180 @@ namespace Lua.Tests; public class ParserTests { + [TestCase("do\nlocal x = 1", "'end' expected (to close 'do' at line 1) near ")] + [TestCase( + "while true do\nlocal x = 1", + "'end' expected (to close 'while' at line 1) near " + )] + [TestCase( + "for i = 1, 2 do\nlocal x = 1", + "'end' expected (to close 'for' at line 1) near " + )] + [TestCase( + "for k, v in pairs({}) do\nlocal x = 1", + "'end' expected (to close 'for' at line 1) near " + )] + [TestCase("repeat\nlocal x = 1", "'until' expected (to close 'repeat' at line 1) near ")] + [TestCase("if true then\nlocal x = 1", "'end' expected (to close 'if' at line 1) near ")] + [TestCase( + "function f()\nlocal x = 1", + "'end' expected (to close 'function' at line 1) near " + )] + [TestCase( + "local function f()\nlocal x = 1", + "'end' expected (to close 'function' at line 1) near " + )] + [TestCase( + "local f = function()\nlocal x = 1", + "'end' expected (to close 'function' at line 1) near " + )] + [TestCase( + "function t:f()\nlocal x = 1", + "'end' expected (to close 'function' at line 1) near " + )] + [TestCase( + "function f\n()\nlocal x = 1", + "'end' expected (to close 'function' at line 1) near " + )] + [TestCase("local t = {\n1", "'}' expected (to close '{' at line 1) near ")] + [TestCase("local t = {\n{}\n", "'}' expected (to close '{' at line 1) near ")] + [TestCase("return (\n1", "')' expected (to close '(' at line 1) near ")] + [TestCase("f(\n1", "')' expected (to close '(' at line 1) near ")] + [TestCase("do\nuntil true", "'end' expected (to close 'do' at line 1) near 'until'")] + [TestCase("repeat\nend", "'until' expected (to close 'repeat' at line 1) near 'end'")] + [TestCase("if true then\nuntil true", "'end' expected (to close 'if' at line 1) near 'until'")] + [TestCase("local t = {\n1\n)", "'}' expected (to close '{' at line 1) near ')'")] + [TestCase("return (\n1\n}", "')' expected (to close '(' at line 1) near '}'")] + [TestCase("f(\n1\n]", "')' expected (to close '(' at line 1) near ']'")] + [TestCase( + "do\nif true then\nlocal x = 1", + "'end' expected (to close 'if' at line 2) near " + )] + [TestCase("do\nif true then\nend", "'end' expected (to close 'do' at line 1) near ")] + [TestCase("do", "'end' expected near ")] + [TestCase("function f()", "'end' expected near ")] + [TestCase("repeat local x = 1", "'until' expected near ")] + [TestCase("local t = {1", "'}' expected near ")] + [TestCase("return (1", "')' expected near ")] + [TestCase("f(1", "')' expected near ")] + [TestCase("do\n", "'end' expected (to close 'do' at line 1) near ")] + [TestCase("do --[[\ncomment\n]]", "'end' expected (to close 'do' at line 1) near ")] + [TestCase("\ndo\nuntil true", "'end' expected (to close 'do' at line 2) near 'until'")] + public void MissingCloser_MatchesCompilerContext(string source, string expectedMessage) + { + using var state = LuaState.Create(); + var compilerException = Assert.Throws(() => + state.Load(source, "diagnostic.lua") + )!; + Assert.That(compilerException.MessageWithNearToken, Is.EqualTo(expectedMessage)); + + var exception = Assert.Throws(() => + LuaSyntaxTree.Parse(source, "diagnostic.lua") + )!; + Assert.That(exception.Message, Does.EndWith(expectedMessage)); + Assert.That(exception.Position.Line, Is.EqualTo(compilerException.Position.Line)); + Assert.That(exception.ChunkName, Is.EqualTo("diagnostic.lua")); + } + + [TestCase("do\nif true then\nend\nend")] + [TestCase("while false do\nend; for i = 1, 2 do\nend; for k in pairs({}) do\nend")] + [TestCase("repeat\nlocal x = 1\nuntil true")] + [TestCase( + "function f\n()\nend; local function g()\nend; local h = function()\nend; function t:f()\nend" + )] + [TestCase("return (\n1\n)")] + [TestCase("f(\n1\n)")] + [TestCase("return {\n[1\n] = 2, value = 3, 4, {5}\n}")] + public void MatchedClosers_RemainValid(string source) + { + using var state = LuaState.Create(); + Assert.DoesNotThrow(() => state.Load(source, "valid.lua")); + Assert.DoesNotThrow(() => LuaSyntaxTree.Parse(source, "valid.lua")); + } + + [TestCase("if true print() end", "'then' expected near 'print'")] + [TestCase("while true print() end", "'do' expected near 'print'")] + [TestCase("for k, v pairs(t) do end", "'in' expected near 'pairs'")] + [TestCase("function f a) end", "'(' expected near 'a'")] + [TestCase("function f(a end", "')' expected near 'end'")] + [TestCase("local t = {[1] 2}", "'=' expected near '2'")] + [TestCase("local t = {[1 = 2}", "']' expected near '='")] + [TestCase("local = 1", " expected near '='")] + [TestCase("local 123 = 1", " expected near '123'")] + [TestCase("local \"text\" = 1", " expected near '\"text\"'")] + [TestCase("local [[text]] = 1", " expected near '[[text]]'")] + [TestCase("if true then", "'end' expected near ")] + [TestCase("if true\n", "'then' expected near ")] + public void MissingToken_UsesLuaSpelling(string source, string expectedMessage) + { + using var state = LuaState.Create(); + var compilerException = Assert.Throws(() => + state.Load(source, "diagnostic.lua") + )!; + Assert.That(compilerException.MessageWithNearToken, Is.EqualTo(expectedMessage)); + + var exception = Assert.Throws(() => + LuaSyntaxTree.Parse(source, "diagnostic.lua") + )!; + + Assert.That(exception.Message, Does.EndWith(expectedMessage)); + Assert.That(exception.ChunkName, Is.EqualTo("diagnostic.lua")); + Assert.That(exception.Position.Line, Is.EqualTo(source.Count(c => c == '\n') + 1)); + } + + [TestCase(SyntaxTokenType.Then, "'then'")] + [TestCase(SyntaxTokenType.ElseIf, "'elseif'")] + [TestCase(SyntaxTokenType.GreaterThanOrEqual, "'>='")] + [TestCase(SyntaxTokenType.Inequality, "'~='")] + [TestCase(SyntaxTokenType.VarArg, "'...'")] + [TestCase(SyntaxTokenType.Dot, "'.'")] + [TestCase(SyntaxTokenType.Colon, "':'")] + [TestCase(SyntaxTokenType.Identifier, "")] + [TestCase(SyntaxTokenType.Number, "")] + [TestCase(SyntaxTokenType.String, "")] + [TestCase(SyntaxTokenType.RawString, "")] + [TestCase(SyntaxTokenType.EndOfLine, "")] + [SetCulture("tr-TR")] + public void ExpectedToken_UsesSyntaxAndCultureIndependentCategoryNames( + SyntaxTokenType token, + string expectedText + ) + { + var position = new Lua.CodeAnalysis.SourcePosition(3, 4); + var exception = Assert.Throws(() => + LuaParseException.ExpectedToken("diagnostic.lua", position, token) + )!; + + Assert.That(exception.Message, Is.EqualTo($"diagnostic.lua:3: {expectedText} expected")); + Assert.That(exception.Position, Is.EqualTo(position)); + Assert.That(exception.ChunkName, Is.EqualTo("diagnostic.lua")); + } + + [TestCase("local", "syntax error near ")] + [TestCase("return t:", "syntax error near ")] + [TestCase("*a = 123", "unexpected symbol near '*'")] + public void SyntaxErrors_ShowActualTokenOrEndOfInput(string source, string expectedMessage) + { + var exception = Assert.Throws(() => + LuaSyntaxTree.Parse(source, "diagnostic.lua") + )!; + Assert.That(exception.Message, Does.EndWith(expectedMessage)); + } + + [TestCase(SyntaxTokenType.Identifier, "myValue", "myValue")] + [TestCase(SyntaxTokenType.Number, "1.5", "1.5")] + [TestCase(SyntaxTokenType.String, "text", "\"text\"")] + [TestCase(SyntaxTokenType.RawString, "text", "[[text]]")] + public void TokenDisplay_PreservesLiteralText( + SyntaxTokenType type, + string text, + string expectedText + ) + { + var token = new SyntaxToken(type, text.AsMemory(), default); + Assert.That(token.ToDisplayString(), Is.EqualTo(expectedText)); + } + [Test] public void Test_If_ElseIf_Else_Empty() {