From 82c9c40709283fd3c2547020c6b41c81766fa849 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Thu, 4 Aug 2016 15:08:02 -0700 Subject: [PATCH] Capture exceptions when trying to parse files and return parse errors. #808 --- .../Properties/RazorResources.Designer.cs | 16 +++++ .../RazorResources.resx | 3 + .../RazorTemplateEngine.cs | 61 ++++++++++++----- .../RazorTemplateEngineTest.cs | 66 +++++++++++++++++++ 4 files changed, 130 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.AspNetCore.Razor/Properties/RazorResources.Designer.cs b/src/Microsoft.AspNetCore.Razor/Properties/RazorResources.Designer.cs index 50f903abc0..7c92b15f09 100644 --- a/src/Microsoft.AspNetCore.Razor/Properties/RazorResources.Designer.cs +++ b/src/Microsoft.AspNetCore.Razor/Properties/RazorResources.Designer.cs @@ -1562,6 +1562,22 @@ namespace Microsoft.AspNetCore.Razor return string.Format(CultureInfo.CurrentCulture, GetString("ParseError_IncompleteQuotesAroundDirective"), p0); } + /// + /// A fatal exception occurred when trying to parse '{0}':{1}{2} + /// + internal static string FatalException + { + get { return GetString("FatalException"); } + } + + /// + /// A fatal exception occurred when trying to parse '{0}':{1}{2} + /// + internal static string FormatFatalException(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("FatalException"), p0, p1, p2); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Razor/RazorResources.resx b/src/Microsoft.AspNetCore.Razor/RazorResources.resx index 515a977fa3..06855362d7 100644 --- a/src/Microsoft.AspNetCore.Razor/RazorResources.resx +++ b/src/Microsoft.AspNetCore.Razor/RazorResources.resx @@ -428,4 +428,7 @@ Instead, wrap the contents of the block in "{{}}": Optional quote around the directive '{0}' is missing the corresponding opening or closing quote. + + A fatal exception occurred when trying to parse '{0}':{1}{2} + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor/RazorTemplateEngine.cs b/src/Microsoft.AspNetCore.Razor/RazorTemplateEngine.cs index 7344f07551..dcbe13473c 100644 --- a/src/Microsoft.AspNetCore.Razor/RazorTemplateEngine.cs +++ b/src/Microsoft.AspNetCore.Razor/RazorTemplateEngine.cs @@ -2,14 +2,19 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading; +using Microsoft.AspNetCore.Razor.Chunks; using Microsoft.AspNetCore.Razor.Chunks.Generators; using Microsoft.AspNetCore.Razor.CodeGenerators; +using Microsoft.AspNetCore.Razor.Compilation.TagHelpers; using Microsoft.AspNetCore.Razor.Parser; +using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; using Microsoft.AspNetCore.Razor.Text; namespace Microsoft.AspNetCore.Razor @@ -363,26 +368,50 @@ namespace Microsoft.AspNetCore.Razor throw new ArgumentNullException(nameof(input)); } - className = (className ?? Host.DefaultClassName) ?? DefaultClassName; - rootNamespace = (rootNamespace ?? Host.DefaultNamespace) ?? DefaultNamespace; + try + { + className = (className ?? Host.DefaultClassName) ?? DefaultClassName; + rootNamespace = (rootNamespace ?? Host.DefaultNamespace) ?? DefaultNamespace; - // Run the parser - var parser = CreateParser(sourceFileName); - Debug.Assert(parser != null); - var results = parser.Parse(input); + // Run the parser + var parser = CreateParser(sourceFileName); + Debug.Assert(parser != null); + var results = parser.Parse(input); - // Generate code - var chunkGenerator = CreateChunkGenerator(className, rootNamespace, sourceFileName); - chunkGenerator.DesignTimeMode = Host.DesignTimeMode; - chunkGenerator.Visit(results); + // Generate code + var chunkGenerator = CreateChunkGenerator(className, rootNamespace, sourceFileName); + chunkGenerator.DesignTimeMode = Host.DesignTimeMode; + chunkGenerator.Visit(results); - var codeGeneratorContext = new CodeGeneratorContext(chunkGenerator.Context, results.ErrorSink); - codeGeneratorContext.Checksum = checksum; - var codeGenerator = CreateCodeGenerator(codeGeneratorContext); - var codeGeneratorResult = codeGenerator.Generate(); + var codeGeneratorContext = new CodeGeneratorContext(chunkGenerator.Context, results.ErrorSink); + codeGeneratorContext.Checksum = checksum; + var codeGenerator = CreateCodeGenerator(codeGeneratorContext); + var codeGeneratorResult = codeGenerator.Generate(); - // Collect results and return - return new GeneratorResults(results, codeGeneratorResult, codeGeneratorContext.ChunkTreeBuilder.Root); + // Collect results and return + return new GeneratorResults(results, codeGeneratorResult, codeGeneratorContext.ChunkTreeBuilder.Root); + } + // During runtime we want code generation explosions to flow up into the calling code. At design time + // we want to capture these exceptions to prevent IDEs from crashing. + catch (Exception ex) when (Host.DesignTimeMode) + { + var errorSink = new ErrorSink(); + errorSink.OnError( + SourceLocation.Undefined, + RazorResources.FormatFatalException(sourceFileName, Environment.NewLine, ex.Message), + length: -1); + var emptyBlock = new BlockBuilder(); + emptyBlock.Type = default(BlockType); + + return new GeneratorResults( + document: emptyBlock.Build(), + tagHelperDescriptors: Enumerable.Empty(), + errorSink: errorSink, + codeGeneratorResult: new CodeGeneratorResult( + code: string.Empty, + designTimeLineMappings: new List()), + chunkTree: new ChunkTree()); + } } protected internal virtual RazorChunkGenerator CreateChunkGenerator( diff --git a/test/Microsoft.AspNetCore.Razor.Test/RazorTemplateEngineTest.cs b/test/Microsoft.AspNetCore.Razor.Test/RazorTemplateEngineTest.cs index 122f84866e..b66d873844 100644 --- a/test/Microsoft.AspNetCore.Razor.Test/RazorTemplateEngineTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Test/RazorTemplateEngineTest.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 System; using System.Collections.Generic; using System.IO; using System.Text; @@ -18,6 +19,53 @@ namespace Microsoft.AspNetCore.Razor { public class RazorTemplateEngineTest { + [Fact] + public void InvalidRazorEngineHostReturnsParseErrorsAtDesignTime() + { + // Arrange + var host = new InvalidRazorEngineHost(new CSharpRazorCodeLanguage()) + { + DesignTimeMode = true + }; + var razorEngine = new RazorTemplateEngine(host); + var input = new StringTextBuffer("
Hello @(\"World\")
"); + var exception = new InvalidOperationException("Hello World"); + var expectedError = RazorResources.FormatFatalException("test", Environment.NewLine, exception.Message); + + // Act + var result = razorEngine.GenerateCode(input, className: null, rootNamespace: null, sourceFileName: "test"); + + // Assert + Assert.Empty(result.Document.Children); + Assert.Empty(result.ChunkTree.Children); + Assert.Empty(result.DesignTimeLineMappings); + Assert.Empty(result.GeneratedCode); + + var error = Assert.Single(result.ParserErrors); + Assert.Equal(expectedError, error.Message, StringComparer.Ordinal); + Assert.Equal(SourceLocation.Undefined, error.Location); + Assert.Equal(-1, error.Length); + } + + [Fact] + public void InvalidRazorEngineHostThrowsAtRuntime() + { + // Arrange + var host = new InvalidRazorEngineHost(new CSharpRazorCodeLanguage()) + { + DesignTimeMode = false + }; + var razorEngine = new RazorTemplateEngine(host); + var input = new StringTextBuffer("
Hello @(\"World\")
"); + + // Act + var thrownException = Assert.Throws(() => + razorEngine.GenerateCode(input, className: null, rootNamespace: null, sourceFileName: "test")); + + // Assert + Assert.Equal("Hello World", thrownException.Message, StringComparer.Ordinal); + } + [Fact] public void ConstructorInitializesHost() { @@ -339,5 +387,23 @@ namespace Microsoft.AspNetCore.Razor return null; } } + + private class InvalidRazorEngineHost : RazorEngineHost + { + public InvalidRazorEngineHost(RazorCodeLanguage codeLanguage) : base(codeLanguage) + { + } + + public override string DefaultClassName + { + get + { + throw new InvalidOperationException("Hello World"); + } + set + { + } + } + } } }