Propogate additional compilation data from RoslynCompilationService

* Update CompilationFailedException to include path of file being compiled
* Pass in path being compiled to Rolsyn.
* Adding doc comments for compilation pieces

Fixes #869
This commit is contained in:
Pranav K 2014-07-28 12:27:41 -07:00
parent d95b85d057
commit 5e010597cd
12 changed files with 285 additions and 38 deletions

View File

@ -4,38 +4,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.FileSystems;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// An exception thrown when accessing the result of a failed compilation.
/// </summary>
public class CompilationFailedException : Exception
{
public CompilationFailedException(IEnumerable<CompilationMessage> messages, string generatedCode)
/// <summary>
/// Instantiates a new instance of <see cref="CompilationFailedException"/>.
/// </summary>
/// <param name="filePath">The path of the Razor source file that was compiled.</param>
/// <param name="fileContent">The contents of the Razor source file.</param>
/// <param name="compiledContent">The generated C# content that was compiled.</param>
/// <param name="messages">A sequence of <see cref="CompilationMessage"/> encountered
/// during compilation.</param>
public CompilationFailedException(
[NotNull] string filePath,
[NotNull] string fileContent,
[NotNull] string compiledContent,
[NotNull] IEnumerable<CompilationMessage> messages)
: base(FormatMessage(messages))
{
FilePath = filePath;
FileContent = fileContent;
CompiledContent = compiledContent;
Messages = messages.ToList();
GeneratedCode = generatedCode;
}
public string GeneratedCode { get; private set; }
/// <summary>
/// Gets the path of the Razor source file that produced the compilation failure.
/// </summary>
public string FilePath { get; private set; }
/// <summary>
/// Gets a sequence of <see cref="CompilationMessage"/> instances encountered during compilation.
/// </summary>
public IEnumerable<CompilationMessage> Messages { get; private set; }
public string CompilationSource
{
get { return GeneratedCode; }
}
/// <summary>
/// Gets the content of the Razor source file.
/// </summary>
public string FileContent { get; private set; }
/// <summary>
/// Gets the generated C# content that was compiled.
/// </summary>
public string CompiledContent { get; private set; }
/// <inheritdoc />
public override string Message
{
get
{
return "Compilation Failed:" + FormatMessage(Messages);
return Resources.FormatCompilationFailed(FilePath) +
Environment.NewLine +
FormatMessage(Messages);
}
}
private static string FormatMessage(IEnumerable<CompilationMessage> messages)
{
return String.Join(Environment.NewLine, messages);
return string.Join(Environment.NewLine, messages);
}
}
}

View File

@ -3,15 +3,30 @@
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Represents a message encountered during compilation.
/// </summary>
public class CompilationMessage
{
/// <summary>
/// Initializes a <see cref="CompilationMessage"/> with the specified message.
/// </summary>
/// <param name="message">A message <see cref="string"/> produced from compilation.</param>
public CompilationMessage(string message)
{
Message = message;
}
/// <summary>
/// Gets a message produced from compilation.
/// </summary>
public string Message { get; private set; }
/// <summary>
/// Returns a <see cref="string"/> representation of this instance of <see cref="CompilationMessage"/>.
/// </summary>
/// <returns>A <see cref="string"/> representing this <see cref="CompilationMessage"/> instance.</returns>
/// <remarks>Returns same value as <see cref="Message"/>.</remarks>
public override string ToString()
{
return Message;

View File

@ -3,46 +3,145 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Microsoft.AspNet.FileSystems;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Represents the result of compilation.
/// </summary>
public class CompilationResult
{
private readonly Type _type;
private Type _type;
private CompilationResult(string generatedCode, Type type, IEnumerable<CompilationMessage> messages)
private CompilationResult()
{
_type = type;
GeneratedCode = generatedCode;
Messages = messages.ToList();
}
public IEnumerable<CompilationMessage> Messages { get; private set; }
public string GeneratedCode { get; private set; }
/// <summary>
/// Gets the path of the Razor file that was compiled.
/// </summary>
public string FilePath
{
get
{
if (File != null)
{
return File.PhysicalPath;
}
return null;
}
}
/// <summary>
/// Gets a sequence of <see cref="CompilationMessage"/> instances encountered during compilation.
/// </summary>
public IEnumerable<CompilationMessage> Messages { get; private set; }
/// <summary>
/// Gets additional information from compilation.
/// </summary>
/// <remarks>
/// In the event of a compilation failure, values from this dictionary are copied to the
/// <see cref="Exception.Data"/> property of the <see cref="Exception"/> thrown.
/// </remarks>
public IDictionary<string, object> AdditionalInfo
{
get; private set;
}
/// <summary>
/// Gets the generated C# content that was compiled.
/// </summary>
public string CompiledContent { get; private set; }
/// <summary>
/// Gets the type produced as a result of compilation.
/// </summary>
/// <exception cref="CompilationFailedException">An error occured during compilation.</exception>
public Type CompiledType
{
get
{
if (_type == null)
{
throw new CompilationFailedException(Messages, GeneratedCode);
throw CreateCompilationFailedException();
}
return _type;
}
}
public static CompilationResult Failed(string generatedCode, IEnumerable<CompilationMessage> messages)
private IFileInfo File { get; set; }
/// <summary>
/// Creates a <see cref="CompilationResult"/> that represents a failure in compilation.
/// </summary>
/// <param name="fileInfo">The <see cref="IFileInfo"/> for the Razor file that was compiled.</param>
/// <param name="compilationContent">The generated C# content to be compiled.</param>
/// <param name="messages">The sequence of failure messages encountered during compilation.</param>
/// <param name="additionalInfo">Additional info about the compilation.</param>
/// <returns>A CompilationResult instance representing a failure.</returns>
public static CompilationResult Failed([NotNull] IFileInfo file,
[NotNull] string compilationContent,
[NotNull] IEnumerable<CompilationMessage> messages,
IDictionary<string, object> additionalInfo)
{
return new CompilationResult(generatedCode, type: null, messages: messages);
return new CompilationResult
{
File = file,
CompiledContent = compilationContent,
Messages = messages,
AdditionalInfo = additionalInfo
};
}
public static CompilationResult Successful(string generatedCode, Type type)
/// <summary>
/// Creates a <see cref="CompilationResult"/> that represents a success in compilation.
/// </summary>
/// <param name="type">The compiled type.</param>
/// <returns>A CompilationResult instance representing a success.</returns>
public static CompilationResult Successful([NotNull] Type type)
{
return new CompilationResult(generatedCode, type, Enumerable.Empty<CompilationMessage>());
return new CompilationResult
{
_type = type
};
}
private CompilationFailedException CreateCompilationFailedException()
{
var fileContent = ReadContent(File);
var exception = new CompilationFailedException(FilePath, fileContent, CompiledContent, Messages);
if (AdditionalInfo != null)
{
foreach (var item in AdditionalInfo)
{
exception.Data.Add(item.Key, item.Value);
}
}
return exception;
}
private static string ReadContent(IFileInfo file)
{
try
{
using (var stream = file.CreateReadStream())
{
using (var reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}
}
catch (IOException)
{
// Don't throw if reading the file fails.
return string.Empty;
}
}
}
}

View File

@ -30,7 +30,7 @@ namespace Microsoft.AspNet.Mvc.Razor
return result;
}
return CompilationResult.Successful(generatedCode: null, type: compiledType);
return CompilationResult.Successful(compiledType);
}
}
}

View File

@ -1,12 +1,23 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNet.FileSystems;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Provides methods for compilation of a Razor page.
/// </summary>
public interface ICompilationService
{
CompilationResult Compile(string content);
/// <summary>
/// Compiles content and returns the result of compilation.
/// </summary>
/// <param name="fileInfo">The <see cref="IFileInfo"/> for the Razor file that was compiled.</param>
/// <param name="compilationContent">The generated C# content to be compiled.</param>
/// <returns>
/// A <see cref="CompilationResult"/> representing the result of compilation.
/// </returns>
CompilationResult Compile(IFileInfo fileInfo, string compilationContent);
}
}

View File

@ -7,15 +7,22 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.AspNet.FileSystems;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Framework.Runtime;
namespace Microsoft.AspNet.Mvc.Razor.Compilation
{
/// <summary>
/// A type that uses Roslyn to compile C# content.
/// </summary>
public class RoslynCompilationService : ICompilationService
{
public static readonly string CompilationResultDiagnosticsKey = "Diagnostics";
private static readonly ConcurrentDictionary<string, MetadataReference> _metadataFileCache =
new ConcurrentDictionary<string, MetadataReference>(StringComparer.OrdinalIgnoreCase);
@ -23,6 +30,12 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
private readonly IApplicationEnvironment _environment;
private readonly IAssemblyLoaderEngine _loader;
/// <summary>
/// Initalizes a new instance of the <see cref="RoslynCompilationService"/> class.
/// </summary>
/// <param name="environment">The environment for the executing application.</param>
/// <param name="loaderEngine">The loader used to load compiled assemblies.</param>
/// <param name="libraryManager">The library manager that provides export and reference information.</param>
public RoslynCompilationService(IApplicationEnvironment environment,
IAssemblyLoaderEngine loaderEngine,
ILibraryManager libraryManager)
@ -32,9 +45,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
_libraryManager = libraryManager;
}
public CompilationResult Compile(string content)
/// <inheritdoc />
public CompilationResult Compile(IFileInfo fileInfo, string compilationContent)
{
var syntaxTrees = new[] { CSharpSyntaxTree.ParseText(content) };
var sourceText = SourceText.From(compilationContent, Encoding.UTF8);
var syntaxTrees = new[] { CSharpSyntaxTree.ParseText(sourceText, path: fileInfo.PhysicalPath) };
var targetFramework = _environment.TargetFramework;
var references = GetApplicationReferences();
@ -70,7 +85,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
.Select(d => GetCompilationMessage(formatter, d))
.ToList();
return CompilationResult.Failed(content, messages);
var additionalInfo = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
{
{ CompilationResultDiagnosticsKey, result.Diagnostics }
};
return CompilationResult.Failed(fileInfo, compilationContent, messages, additionalInfo);
}
Assembly assembly;
@ -89,7 +108,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var type = assembly.GetExportedTypes()
.First();
return CompilationResult.Successful(string.Empty, type);
return CompilationResult.Successful(type);
}
}
}
@ -135,7 +154,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
});
}
private CompilationMessage GetCompilationMessage(DiagnosticFormatter formatter, Diagnostic diagnostic)
private static CompilationMessage GetCompilationMessage(DiagnosticFormatter formatter, Diagnostic diagnostic)
{
return new CompilationMessage(formatter.Format(diagnostic));
}

View File

@ -26,6 +26,22 @@ namespace Microsoft.AspNet.Mvc.Razor
return GetString("ArgumentCannotBeNullOrEmpty");
}
/// <summary>
/// Compilation for '{0}' failed:
/// </summary>
internal static string CompilationFailed
{
get { return GetString("CompilationFailed"); }
}
/// <summary>
/// Compilation for '{0}' failed:
/// </summary>
internal static string FormatCompilationFailed(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("CompilationFailed"), p0);
}
/// <summary>
/// The layout view '{0}' could not be located.
/// </summary>

View File

@ -21,7 +21,7 @@ namespace Microsoft.AspNet.Mvc.Razor
private readonly string _appRoot;
public RazorCompilationService(IApplicationEnvironment environment,
ICompilationService compilationService,
ICompilationService compilationService,
IMvcRazorHost razorHost)
{
_environment = environment;
@ -49,10 +49,10 @@ namespace Microsoft.AspNet.Mvc.Razor
if (!results.Success)
{
var messages = results.ParserErrors.Select(e => new CompilationMessage(e.Message));
throw new CompilationFailedException(messages, results.GeneratedCode);
return CompilationResult.Failed(file, results.GeneratedCode, messages, additionalInfo: null);
}
return _baseCompilationService.Compile(results.GeneratedCode);
return _baseCompilationService.Compile(file, results.GeneratedCode);
}
private static string EnsureTrailingSlash([NotNull]string path)

View File

@ -120,6 +120,9 @@
<data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
<value>The value cannot be null or empty.</value>
</data>
<data name="CompilationFailed" xml:space="preserve">
<value>Compilation for '{0}' failed:</value>
</data>
<data name="LayoutCannotBeLocated" xml:space="preserve">
<value>The layout view '{0}' could not be located.</value>
</data>

View File

@ -0,0 +1,51 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.AspNet.FileSystems;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor.Test
{
public class CompilationResultTest
{
[Fact]
public void FailedResult_ThrowsWhenAccessingCompiledType()
{
// Arrange
var expected =
@"Compilation for 'myfile' failed:
hello
world";
var originalContent = "Original file content";
var fileInfo = new Mock<IFileInfo>();
fileInfo.SetupGet(f => f.PhysicalPath)
.Returns("myfile");
var contentBytes = Encoding.UTF8.GetBytes(originalContent);
fileInfo.Setup(f => f.CreateReadStream())
.Returns(new MemoryStream(contentBytes));
var messages = new[]
{
new CompilationMessage("hello"),
new CompilationMessage("world")
};
var additionalInfo = new Dictionary<string, object>
{
{ "key", "value" }
};
var result = CompilationResult.Failed(fileInfo.Object,
"<h1>hello world</h1>",
messages,
additionalInfo);
// Act and Assert
var ex = Assert.Throws<CompilationFailedException>(() => result.CompiledType);
Assert.Equal(expected, ex.Message);
Assert.Equal("value", ex.Data["key"]);
Assert.Equal(originalContent, ex.FileContent);
}
}
}

View File

@ -21,6 +21,7 @@
<Content Include="project.json" />
</ItemGroup>
<ItemGroup>
<Compile Include="Compilation\CompilationResultTest.cs" />
<Compile Include="BufferEntryCollectionTest.cs" />
<Compile Include="MvcRazorCodeParserTest.cs" />
<Compile Include="RazorCompilationServiceTest.cs" />

View File

@ -27,14 +27,14 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
host.Setup(h => h.GenerateCode(@"views\index\home.cshtml", It.IsAny<Stream>()))
.Returns(new GeneratorResults(new Block(new BlockBuilder { Type = BlockType.Comment }), new RazorError[0], new CodeBuilderResult("", new LineMapping[0])))
.Verifiable();
var compiler = new Mock<ICompilationService>();
compiler.Setup(c => c.Compile(It.IsAny<string>()))
.Returns(CompilationResult.Successful("", typeof(RazorCompilationServiceTest)));
var razorService = new RazorCompilationService(env.Object, compiler.Object, host.Object);
var fileInfo = new Mock<IFileInfo>();
fileInfo.Setup(f => f.PhysicalPath).Returns(viewPath);
fileInfo.Setup(f => f.CreateReadStream()).Returns(Stream.Null);
var compiler = new Mock<ICompilationService>();
compiler.Setup(c => c.Compile(fileInfo.Object, It.IsAny<string>()))
.Returns(CompilationResult.Successful(typeof(RazorCompilationServiceTest)));
var razorService = new RazorCompilationService(env.Object, compiler.Object, host.Object);
// Act
razorService.CompileCore(fileInfo.Object);