diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs index 769244aa28..99e9ad0d41 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorIntermediateNodeWriter.cs @@ -8,6 +8,7 @@ using System.Text; using AngleSharp; using AngleSharp.Html; using AngleSharp.Parser.Html; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.CodeGeneration; using Microsoft.AspNetCore.Razor.Language.Intermediate; @@ -286,7 +287,8 @@ namespace Microsoft.AspNetCore.Blazor.Razor { _scopeStack.CloseScope( tagName: isComponent ? tagNameOriginalCase : nextTag.Data, - isComponent: isComponent); + isComponent: isComponent, + source: CalculateSourcePosition(node.Source, nextToken.Position)); var closeMethodName = isComponent ? nameof(RenderTreeBuilder.CloseComponent) : nameof(RenderTreeBuilder.CloseElement); @@ -315,6 +317,28 @@ namespace Microsoft.AspNetCore.Blazor.Razor } } + private SourceSpan? CalculateSourcePosition( + SourceSpan? razorTokenPosition, + TextPosition htmlNodePosition) + { + if (razorTokenPosition.HasValue) + { + var razorPos = razorTokenPosition.Value; + return new SourceSpan( + razorPos.FilePath, + razorPos.AbsoluteIndex + htmlNodePosition.Position, + razorPos.LineIndex + htmlNodePosition.Line - 1, + htmlNodePosition.Line == 1 + ? razorPos.CharacterIndex + htmlNodePosition.Column - 1 + : htmlNodePosition.Column - 1, + length: 1); + } + else + { + return null; + } + } + private static string GetTagNameWithOriginalCase(string document, HtmlTagToken tagToken) { var offset = tagToken.Type == HtmlTokenType.EndTag ? 1 : 0; // For end tags, skip the '/' diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RazorCompilerException.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RazorCompilerException.cs index 9061ad1aed..4e5d7a95dd 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RazorCompilerException.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/RazorCompilerException.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNetCore.Razor.Language; using System; namespace Microsoft.AspNetCore.Blazor.Razor @@ -11,16 +12,25 @@ namespace Microsoft.AspNetCore.Blazor.Razor /// public class RazorCompilerException : Exception { - public RazorCompilerException(string message) : base(message) + private readonly int _line; + private readonly int _column; + + public RazorCompilerException(string message) : this(message, null) { } + public RazorCompilerException(string message, SourceSpan? source) : base(message) + { + _line = source.HasValue ? (source.Value.LineIndex + 1) : 1; + _column = source.HasValue ? (source.Value.CharacterIndex + 1) : 1; + } + public RazorCompilerDiagnostic ToDiagnostic(string sourceFilePath) => new RazorCompilerDiagnostic( RazorCompilerDiagnostic.DiagnosticType.Error, sourceFilePath, - line: 1, // Later it might be necessary to take line/col constructor args, but not needed yet - column: 1, + line: _line, + column: _column, message: Message); } } diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs index 70b1eb28b6..abc543a36c 100644 --- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs +++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ScopeStack.cs @@ -21,19 +21,19 @@ namespace Microsoft.AspNetCore.Blazor.Razor _stack.Push(new ScopeEntry(tagName, isComponent)); } - public void CloseScope(string tagName, bool isComponent) + public void CloseScope(string tagName, bool isComponent, SourceSpan? source) { if (_stack.Count == 0) { throw new RazorCompilerException( - $"Unexpected closing tag '{tagName}' with no matching start tag."); + $"Unexpected closing tag '{tagName}' with no matching start tag.", source); } var expected = _stack.Pop(); if (!tagName.Equals(expected.TagName, StringComparison.Ordinal)) { throw new RazorCompilerException( - $"Mismatching closing tag. Found '{tagName}' but expected '{expected.TagName}'."); + $"Mismatching closing tag. Found '{tagName}' but expected '{expected.TagName}'.", source); } // Note: there's no unit test to cover the following, because there's no known way of diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs index dc5934df37..9a91cb5bef 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs @@ -525,7 +525,14 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test { // Arrange/Act var result = CompileToCSharp( - "textmore text"); + $"@{{\n" + + $" var abc = 123;\n" + + $"}}\n" + + $"\n" + + $" \n" + + $" text\n" + + $" more text\n" + + $"\n"); // Assert Assert.Collection(result.Diagnostics, @@ -533,6 +540,8 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test { Assert.Equal(RazorCompilerDiagnostic.DiagnosticType.Error, item.Type); Assert.StartsWith("Mismatching closing tag. Found 'root' but expected 'child'.", item.Message); + Assert.Equal(7, item.Line); + Assert.Equal(21, item.Column); }); }