diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs index d278980cd9..18b44ee66b 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs @@ -126,22 +126,33 @@ private static Dictionary GetMergerMappings(CodeTree codeTre private static CodeTree ParseViewFile(RazorTemplateEngine engine, IFileInfo fileInfo, - string viewStartPath) + string globalImportPath) { using (var stream = fileInfo.CreateReadStream()) { using (var streamReader = new StreamReader(stream)) { - var parseResults = engine.ParseTemplate(streamReader, viewStartPath); + var parseResults = engine.ParseTemplate(streamReader, globalImportPath); var className = ParserHelpers.SanitizeClassName(fileInfo.Name); var language = engine.Host.CodeLanguage; var codeGenerator = language.CreateCodeGenerator(className, engine.Host.DefaultNamespace, - viewStartPath, + globalImportPath, engine.Host); codeGenerator.Visit(parseResults); - return codeGenerator.Context.CodeTreeBuilder.CodeTree; + var codeTree = codeGenerator.Context.CodeTreeBuilder.CodeTree; + // Rewrite the location of inherited chunks so they point to the global import file. + foreach (var chunk in codeTree.Chunks) + { + chunk.Start = new SourceLocation( + globalImportPath, + chunk.Start.AbsoluteIndex, + chunk.Start.LineIndex, + chunk.Start.CharacterIndex); + } + + return codeTree; } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorParser.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorParser.cs index dd7ecf656b..f30e5bcbee 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorParser.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorParser.cs @@ -76,7 +76,7 @@ private static IEnumerable GetTagHelperDirectiveDe { var descriptor = new TagHelperDirectiveDescriptor( addTagHelperChunk.LookupText, - SourceLocation.Undefined, + chunk.Start, TagHelperDirectiveType.AddTagHelper); descriptors.Add(descriptor); @@ -89,7 +89,7 @@ private static IEnumerable GetTagHelperDirectiveDe { var descriptor = new TagHelperDirectiveDescriptor( removeTagHelperChunk.LookupText, - SourceLocation.Undefined, + chunk.Start, TagHelperDirectiveType.RemoveTagHelper); descriptors.Add(descriptor); @@ -102,7 +102,7 @@ private static IEnumerable GetTagHelperDirectiveDe { var descriptor = new TagHelperDirectiveDescriptor( tagHelperPrefixDirectiveChunk.Prefix, - SourceLocation.Undefined, + chunk.Start, TagHelperDirectiveType.TagHelperPrefix); descriptors.Add(descriptor); diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationFailedException.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationFailedException.cs index 96d697ce33..5b68af4e3f 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationFailedException.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationFailedException.cs @@ -17,24 +17,24 @@ public class CompilationFailedException : Exception, ICompilationException /// /// Instantiates a new instance of . /// - /// The instance containing + /// s containing /// details of the compilation failure. public CompilationFailedException( - [NotNull] ICompilationFailure compilationFailure) - : base(FormatMessage(compilationFailure)) + [NotNull] IEnumerable compilationFailures) + : base(FormatMessage(compilationFailures)) { - CompilationFailures = new[] { compilationFailure }; + CompilationFailures = compilationFailures; } /// public IEnumerable CompilationFailures { get; } - private static string FormatMessage(ICompilationFailure compilationFailure) + private static string FormatMessage(IEnumerable compilationFailure) { return Resources.CompilationFailed + Environment.NewLine + string.Join( Environment.NewLine, - compilationFailure.Messages.Select(message => message.FormattedMessage)); + compilationFailure.SelectMany(f => f.Messages).Select(message => message.FormattedMessage)); } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs index a55a32fa63..3c8dc4a7ff 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs @@ -2,8 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.Framework.Runtime; +using System.Collections.Generic; using Microsoft.Framework.Internal; +using Microsoft.Framework.Runtime; namespace Microsoft.AspNet.Mvc.Razor.Compilation { @@ -31,10 +32,10 @@ protected CompilationResult() public string CompiledContent { get; protected set; } /// - /// Gets the produced from parsing or compiling the Razor file. + /// Gets the s produced from parsing or compiling the Razor file. /// /// This property is null when compilation succeeded. - public ICompilationFailure CompilationFailure { get; private set; } + public IEnumerable CompilationFailures { get; private set; } /// /// Gets the . @@ -43,9 +44,9 @@ protected CompilationResult() /// Thrown if compilation failed. public CompilationResult EnsureSuccessful() { - if (CompilationFailure != null) + if (CompilationFailures != null) { - throw new CompilationFailedException(CompilationFailure); + throw new CompilationFailedException(CompilationFailures); } return this; @@ -54,14 +55,14 @@ public CompilationResult EnsureSuccessful() /// /// Creates a for a failed compilation. /// - /// The produced from parsing or + /// s produced from parsing or /// compiling the Razor file. /// A instance for a failed compilation. - public static CompilationResult Failed([NotNull] ICompilationFailure compilationFailure) + public static CompilationResult Failed([NotNull] IEnumerable compilationFailures) { return new CompilationResult { - CompilationFailure = compilationFailure + CompilationFailures = compilationFailures }; } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RazorCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RazorCompilationService.cs index 20493ddc90..dd8485ca32 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RazorCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RazorCompilationService.cs @@ -1,11 +1,14 @@ // 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; +using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.Razor; using Microsoft.Framework.Internal; +using Microsoft.Framework.OptionsModel; namespace Microsoft.AspNet.Mvc.Razor.Compilation { @@ -16,12 +19,15 @@ public class RazorCompilationService : IRazorCompilationService { private readonly ICompilationService _compilationService; private readonly IMvcRazorHost _razorHost; + private readonly IFileProvider _fileProvider; public RazorCompilationService(ICompilationService compilationService, - IMvcRazorHost razorHost) + IMvcRazorHost razorHost, + IOptions viewEngineOptions) { _compilationService = compilationService; _razorHost = razorHost; + _fileProvider = viewEngineOptions.Options.FileProvider; } /// @@ -30,39 +36,61 @@ public CompilationResult Compile([NotNull] RelativeFileInfo file) GeneratorResults results; using (var inputStream = file.FileInfo.CreateReadStream()) { - results = _razorHost.GenerateCode( - file.RelativePath, inputStream); + results = _razorHost.GenerateCode(file.RelativePath, inputStream); } if (!results.Success) { - var messages = results.ParserErrors - .Select(parseError => new RazorCompilationMessage(parseError, file.RelativePath)); - var failure = new RazorCompilationFailure( - file.RelativePath, - ReadFileContentsSafely(file.FileInfo), - messages); - - return CompilationResult.Failed(failure); + return GetCompilationFailedResult(file, results.ParserErrors); } return _compilationService.Compile(file, results.GeneratedCode); } - private static string ReadFileContentsSafely(IFileInfo fileInfo) + // Internal for unit testing + internal CompilationResult GetCompilationFailedResult(RelativeFileInfo file, IEnumerable errors) { - try + var messageGroups = errors + .GroupBy(razorError => + // If a SourceLocation does not specify a file path, assume it is produced + // from parsing the current file. + razorError.Location.FilePath ?? file.RelativePath, + StringComparer.OrdinalIgnoreCase); + + var failures = new List(); + foreach (var group in messageGroups) { - using (var reader = new StreamReader(fileInfo.CreateReadStream())) - { - return reader.ReadToEnd(); - } + var filePath = group.Key; + var fileContent = ReadFileContentsSafely(filePath); + var compilationFailure = new RazorCompilationFailure( + filePath, + fileContent, + group.Select(parserError => new RazorCompilationMessage(parserError, filePath))); + failures.Add(compilationFailure); } - catch + + return CompilationResult.Failed(failures); + } + + private string ReadFileContentsSafely(string relativePath) + { + var fileInfo = _fileProvider.GetFileInfo(relativePath); + if (fileInfo.Exists) { - // Ignore any failures - return null; + try + { + using (var reader = new StreamReader(fileInfo.CreateReadStream())) + { + return reader.ReadToEnd(); + } + } + catch + { + // Ignore any failures + } } + + return null; } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs index b17e26b747..e08d4f47e7 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; using Microsoft.Framework.Internal; +using Microsoft.Framework.OptionsModel; using Microsoft.Framework.Runtime; using Microsoft.Framework.Runtime.Compilation; using Microsoft.Framework.Runtime.Roslyn; @@ -34,9 +35,8 @@ public class RoslynCompilationService : ICompilationService private readonly IApplicationEnvironment _environment; private readonly IAssemblyLoadContext _loader; private readonly ICompilerOptionsProvider _compilerOptionsProvider; - + private readonly IFileProvider _fileProvider; private readonly Lazy> _applicationReferences; - private readonly string _classPrefix; /// @@ -50,30 +50,28 @@ public RoslynCompilationService(IApplicationEnvironment environment, IAssemblyLoadContextAccessor loaderAccessor, ILibraryManager libraryManager, ICompilerOptionsProvider compilerOptionsProvider, - IMvcRazorHost host) + IMvcRazorHost host, + IOptions optionsAccessor) { _environment = environment; _loader = loaderAccessor.GetLoadContext(typeof(RoslynCompilationService).GetTypeInfo().Assembly); _libraryManager = libraryManager; _applicationReferences = new Lazy>(GetApplicationReferences); _compilerOptionsProvider = compilerOptionsProvider; + _fileProvider = optionsAccessor.Options.FileProvider; _classPrefix = host.MainClassNamePrefix; } /// public CompilationResult Compile([NotNull] RelativeFileInfo fileInfo, [NotNull] string compilationContent) { - // The path passed to SyntaxTreeGenerator.Generate is used by the compiler to generate symbols (pdb) that - // map to the source file. If a file does not exist on a physical file system, PhysicalPath will be null. - // This prevents files that exist in a non-physical file system from being debugged. - var path = fileInfo.FileInfo.PhysicalPath ?? fileInfo.RelativePath; + var assemblyName = Path.GetRandomFileName(); var compilationSettings = _compilerOptionsProvider.GetCompilationSettings(_environment); var syntaxTree = SyntaxTreeGenerator.Generate(compilationContent, - path, + assemblyName, compilationSettings); var references = _applicationReferences.Value; - var assemblyName = Path.GetRandomFileName(); var compilationOptions = compilationSettings.CompilationOptions .WithOutputKind(OutputKind.DynamicallyLinkedLibrary); @@ -99,15 +97,11 @@ public CompilationResult Compile([NotNull] RelativeFileInfo fileInfo, [NotNull] if (!result.Success) { - var failures = result.Diagnostics.Where(IsError); - var compilationFailure = new RoslynCompilationFailure(failures) - { - CompiledContent = compilationContent, - SourceFileContent = ReadFileContentsSafely(fileInfo.FileInfo), - SourceFilePath = fileInfo.RelativePath - }; - - return CompilationResult.Failed(compilationFailure); + return GetCompilationFailedResult( + fileInfo.RelativePath, + compilationContent, + assemblyName, + result.Diagnostics); } Assembly assembly; @@ -131,6 +125,56 @@ public CompilationResult Compile([NotNull] RelativeFileInfo fileInfo, [NotNull] } } + // Internal for unit testing + internal CompilationResult GetCompilationFailedResult( + string relativePath, + string compilationContent, + string assemblyName, + IEnumerable diagnostics) + { + var diagnosticGroups = diagnostics + .Where(IsError) + .GroupBy(diagnostic => GetFilePath(relativePath, diagnostic), StringComparer.Ordinal); + + var failures = new List(); + foreach (var group in diagnosticGroups) + { + var sourceFilePath = group.Key; + string sourceFileContent; + if (string.Equals(assemblyName, sourceFilePath, StringComparison.Ordinal)) + { + // The error is in the generated code and does not have a mapping line pragma + sourceFileContent = compilationContent; + sourceFilePath = Resources.GeneratedCodeFileName; + } + else + { + sourceFileContent = ReadFileContentsSafely(_fileProvider, sourceFilePath); + } + + var compilationFailure = new RoslynCompilationFailure(group) + { + CompiledContent = compilationContent, + SourceFileContent = sourceFileContent, + SourceFilePath = sourceFilePath + }; + + failures.Add(compilationFailure); + } + + return CompilationResult.Failed(failures); + } + + private static string GetFilePath(string relativePath, Diagnostic diagnostic) + { + if (diagnostic.Location == Location.None) + { + return relativePath; + } + + return diagnostic.Location.GetMappedLineSpan().Path; + } + private List GetApplicationReferences() { var references = new List(); @@ -222,20 +266,25 @@ private static bool IsError(Diagnostic diagnostic) return diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error; } - private static string ReadFileContentsSafely(IFileInfo fileInfo) + private static string ReadFileContentsSafely(IFileProvider fileProvider, string filePath) { - try + var fileInfo = fileProvider.GetFileInfo(filePath); + if (fileInfo.Exists) { - using (var reader = new StreamReader(fileInfo.CreateReadStream())) + try { - return reader.ReadToEnd(); + using (var reader = new StreamReader(fileInfo.CreateReadStream())) + { + return reader.ReadToEnd(); + } + } + catch + { + // Ignore any failures } } - catch - { - // Ignore any failures - return null; - } + + return null; } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorErrorExtensions.cs b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorErrorExtensions.cs index abbd19b039..a08c5b916f 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorErrorExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorErrorExtensions.cs @@ -22,7 +22,7 @@ public static Diagnostic ToDiagnostics([NotNull] this RazorError error, [NotNull isEnabledByDefault: true); var location = error.Location; - if (location == SourceLocation.Undefined) + if (location.Equals(SourceLocation.Undefined)) { location = SourceLocation.Zero; } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index 8a4691f31e..b9e7b7d264 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -394,6 +394,22 @@ internal static string FormatRazorFileInfoCollection_ResourceCouldNotBeFound(obj return string.Format(CultureInfo.CurrentCulture, GetString("RazorFileInfoCollection_ResourceCouldNotBeFound"), p0, p1); } + /// + /// Generated Code + /// + internal static string GeneratedCodeFileName + { + get { return GetString("GeneratedCodeFileName"); } + } + + /// + /// Generated Code + /// + internal static string FormatGeneratedCodeFileName() + { + return GetString("GeneratedCodeFileName"); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index 101065801c..3ae312bada 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -189,4 +189,7 @@ The resource '{0}' specified by '{1}' could not be found. + + Generated Code + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ErrorPageTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ErrorPageTests.cs index 258cc6469c..d18e826963 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ErrorPageTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ErrorPageTests.cs @@ -47,5 +47,26 @@ public async Task CompilationFailuresAreListedByErrorPageMiddleware(string actio Assert.Contains($"/Views/ErrorPageMiddleware/{action}.cshtml", content); Assert.Contains(expected, content); } + + [Fact] + public async Task CompilationFailuresFromGlobalImportAreListed() + { + // Arrange + var expectedMessage = "The type or namespace name 'NamespaceDoesNotExist' could not be found (" + + "are you missing a using directive or an assembly reference?)"; + var server = TestHelper.CreateServer(_app, SiteName, _configureServices); + var client = server.CreateClient(); + var expectedMediaType = MediaTypeHeaderValue.Parse("text/html"); + + // Act + var response = await client.GetAsync("http://localhost/ErrorFromGlobalImport"); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal(expectedMediaType, response.Content.Headers.ContentType); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(@"Views\ErrorFromGlobalImport\_GlobalImport.cshtml", content); + Assert.Contains(expectedMessage, content); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs index a89ec40d3d..a5ed4cdcfd 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs @@ -39,28 +39,38 @@ @inherits MyBaseType // Assert Assert.Equal(2, codeTrees.Count); - var viewStartChunks = codeTrees[0].Chunks; - Assert.Equal(3, viewStartChunks.Count); + var globalImportChunks = codeTrees[0].Chunks; + Assert.Equal(3, globalImportChunks.Count); - Assert.IsType(viewStartChunks[0]); - var usingChunk = Assert.IsType(viewStartChunks[1]); + var globalImportPath = @"Views\home\_GlobalImport.cshtml"; + Assert.IsType(globalImportChunks[0]); + + Assert.Equal(globalImportPath, globalImportChunks[1].Start.FilePath); + var usingChunk = Assert.IsType(globalImportChunks[1]); Assert.Equal("MyNamespace", usingChunk.Namespace); - Assert.IsType(viewStartChunks[2]); + Assert.Equal(globalImportPath, globalImportChunks[2].Start.FilePath); + Assert.IsType(globalImportChunks[2]); - viewStartChunks = codeTrees[1].Chunks; - Assert.Equal(5, viewStartChunks.Count); + globalImportChunks = codeTrees[1].Chunks; + globalImportPath = @"Views\_GlobalImport.cshtml"; + Assert.Equal(5, globalImportChunks.Count); - Assert.IsType(viewStartChunks[0]); + Assert.Equal(globalImportPath, globalImportChunks[0].Start.FilePath); + Assert.IsType(globalImportChunks[0]); - var injectChunk = Assert.IsType(viewStartChunks[1]); + Assert.Equal(globalImportPath, globalImportChunks[1].Start.FilePath); + var injectChunk = Assert.IsType(globalImportChunks[1]); Assert.Equal("MyHelper", injectChunk.TypeName); Assert.Equal("Helper", injectChunk.MemberName); - var setBaseTypeChunk = Assert.IsType(viewStartChunks[2]); + Assert.Equal(globalImportPath, globalImportChunks[2].Start.FilePath); + var setBaseTypeChunk = Assert.IsType(globalImportChunks[2]); Assert.Equal("MyBaseType", setBaseTypeChunk.TypeName); - Assert.IsType(viewStartChunks[3]); - Assert.IsType(viewStartChunks[4]); + Assert.Equal(globalImportPath, globalImportChunks[3].Start.FilePath); + Assert.IsType(globalImportChunks[3]); + Assert.Equal(globalImportPath, globalImportChunks[4].Start.FilePath); + Assert.IsType(globalImportChunks[4]); } [Fact] diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorParserTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorParserTest.cs index 19e76f1aa1..45b36b46c9 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorParserTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorParserTest.cs @@ -172,7 +172,7 @@ public void GetTagHelperDescriptors_ReturnsExpectedDirectiveDescriptors( var actual = descriptors[i]; Assert.Equal(expected.DirectiveText, actual.DirectiveText, StringComparer.Ordinal); - Assert.Equal(expected.Location, actual.Location); + Assert.Equal(SourceLocation.Zero, actual.Location); Assert.Equal(expected.DirectiveType, actual.DirectiveType); } } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilationResultTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilationResultTest.cs index 46a42549c1..962c7fa896 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilationResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilationResultTest.cs @@ -14,14 +14,15 @@ public void EnsureSuccessful_ThrowsIfCompilationFailed() { // Arrange var compilationFailure = Mock.Of(); - var result = CompilationResult.Failed(compilationFailure); + var failures = new[] { compilationFailure }; + var result = CompilationResult.Failed(failures); // Act and Assert Assert.Null(result.CompiledType); - Assert.Same(compilationFailure, result.CompilationFailure); + Assert.Same(failures, result.CompilationFailures); var exception = Assert.Throws(() => result.EnsureSuccessful()); - Assert.Collection(exception.CompilationFailures, - failure => Assert.Same(compilationFailure, failure)); + var failure = Assert.Single(exception.CompilationFailures); + Assert.Same(compilationFailure, failure); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RazorCompilationServiceTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RazorCompilationServiceTest.cs index 002eadb26a..7f92efd1ad 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RazorCompilationServiceTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RazorCompilationServiceTest.cs @@ -8,6 +8,7 @@ using Microsoft.AspNet.Razor.Generator.Compiler; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.TagHelpers; +using Microsoft.Framework.OptionsModel; using Moq; using Xunit; @@ -36,7 +37,7 @@ public void CompileCalculatesRootRelativePath(string appPath, string viewPath) compiler.Setup(c => c.Compile(relativeFileInfo, It.IsAny())) .Returns(CompilationResult.Successful(typeof(RazorCompilationServiceTest))); - var razorService = new RazorCompilationService(compiler.Object, host.Object); + var razorService = new RazorCompilationService(compiler.Object, host.Object, GetOptions()); // Act razorService.Compile(relativeFileInfo); @@ -68,15 +69,19 @@ public void Compile_ReturnsFailedResultIfParseFails() var compiler = new Mock(MockBehavior.Strict); var relativeFileInfo = new RelativeFileInfo(fileInfo.Object, @"Views\index\home.cshtml"); - var razorService = new RazorCompilationService(compiler.Object, host.Object); + var razorService = new RazorCompilationService(compiler.Object, host.Object, GetOptions()); // Act var result = razorService.Compile(relativeFileInfo); // Assert - Assert.NotNull(result.CompilationFailure); - var message = Assert.Single(result.CompilationFailure.Messages); - Assert.Equal("some message", message.Message); + Assert.NotNull(result.CompilationFailures); + Assert.Collection(result.CompilationFailures, + failure => + { + var message = Assert.Single(failure.Messages); + Assert.Equal("some message", message.Message); + }); host.Verify(); } @@ -105,7 +110,7 @@ public void Compile_ReturnsResultFromCompilationServiceIfParseSucceeds() compiler.Setup(c => c.Compile(relativeFileInfo, code)) .Returns(compilationResult) .Verifiable(); - var razorService = new RazorCompilationService(compiler.Object, host.Object); + var razorService = new RazorCompilationService(compiler.Object, host.Object, GetOptions()); // Act var result = razorService.Compile(relativeFileInfo); @@ -115,6 +120,86 @@ public void Compile_ReturnsResultFromCompilationServiceIfParseSucceeds() compiler.Verify(); } + [Fact] + public void GetCompilationFailedResult_ReturnsCompilationResult_WithGroupedMessages() + { + // Arrange + var viewPath = @"views/index.razor"; + var globalImportPath = @"views/global.import.cshtml"; + var host = Mock.Of(); + + var fileProvider = new TestFileProvider(); + var file = fileProvider.AddFile(viewPath, "View Content"); + fileProvider.AddFile(globalImportPath, "Global Import Content"); + var relativeFileInfo = new RelativeFileInfo(file, viewPath); + var razorService = new RazorCompilationService( + Mock.Of(), + Mock.Of(), + GetOptions(fileProvider)); + var errors = new[] + { + new RazorError("message-1", new SourceLocation(1, 2, 17)), + new RazorError("message-2", new SourceLocation(viewPath, 1, 4, 6), 7), + new RazorError { Message = "message-3" }, + new RazorError("message-4", new SourceLocation(globalImportPath, 1, 3, 8), 4), + }; + + // Act + var result = razorService.GetCompilationFailedResult(relativeFileInfo, errors); + + // Assert + Assert.NotNull(result.CompilationFailures); + Assert.Collection(result.CompilationFailures, + failure => + { + Assert.Equal(viewPath, failure.SourceFilePath); + Assert.Equal("View Content", failure.SourceFileContent); + Assert.Collection(failure.Messages, + message => + { + Assert.Equal(errors[0].Message, message.Message); + Assert.Equal(viewPath, message.SourceFilePath); + Assert.Equal(3, message.StartLine); + Assert.Equal(17, message.StartColumn); + Assert.Equal(3, message.EndLine); + Assert.Equal(18, message.EndColumn); + }, + message => + { + Assert.Equal(errors[1].Message, message.Message); + Assert.Equal(viewPath, message.SourceFilePath); + Assert.Equal(5, message.StartLine); + Assert.Equal(6, message.StartColumn); + Assert.Equal(5, message.EndLine); + Assert.Equal(13, message.EndColumn); + }, + message => + { + Assert.Equal(errors[2].Message, message.Message); + Assert.Equal(viewPath, message.SourceFilePath); + Assert.Equal(0, message.StartLine); + Assert.Equal(-1, message.StartColumn); + Assert.Equal(0, message.EndLine); + Assert.Equal(0, message.EndColumn); + }); + }, + failure => + { + Assert.Equal(globalImportPath, failure.SourceFilePath); + Assert.Equal("Global Import Content", failure.SourceFileContent); + Assert.Collection(failure.Messages, + message => + { + Assert.Equal(errors[3].Message, message.Message); + Assert.Equal(globalImportPath, message.SourceFilePath); + Assert.Equal(4, message.StartLine); + Assert.Equal(8, message.StartColumn); + Assert.Equal(4, message.EndLine); + Assert.Equal(12, message.EndColumn); + }); + }); + } + private static GeneratorResults GetGeneratorResult() { return new GeneratorResults( @@ -124,5 +209,18 @@ private static GeneratorResults GetGeneratorResult() new CodeBuilderResult("", new LineMapping[0]), new CodeTree()); } + + private static IOptions GetOptions(IFileProvider fileProvider = null) + { + var razorViewEngineOptions = new RazorViewEngineOptions + { + FileProvider = fileProvider ?? new TestFileProvider() + }; + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(razorViewEngineOptions); + + return options.Object; + } } } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RoslynCompilationServiceTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RoslynCompilationServiceTest.cs index b4161006ad..424399c437 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RoslynCompilationServiceTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/RoslynCompilationServiceTest.cs @@ -6,6 +6,9 @@ using System.Reflection; using System.Runtime.Versioning; using Microsoft.AspNet.FileProviders; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Framework.OptionsModel; using Microsoft.Framework.Runtime; using Microsoft.Framework.Runtime.Compilation; using Moq; @@ -38,7 +41,8 @@ public class MyTestType {}"; accessor, libraryManager, compilerOptionsProvider.Object, - mvcRazorHost.Object); + mvcRazorHost.Object, + GetOptions()); var relativeFileInfo = new RelativeFileInfo(new TestFileInfo { PhysicalPath = "SomePath" }, "some-relative-path"); @@ -52,11 +56,14 @@ public class MyTestType {}"; } [Fact] - public void Compile_ReturnsCompilationFailureWithRelativePath() + public void Compile_ReturnsCompilationFailureWithPathsFromLinePragmas() { // Arrange + var viewPath = "some-relative-path"; var fileContent = "test file content"; - var content = @"this should fail"; + var content = $@" +#line 1 ""{viewPath}"" +this should fail"; var applicationEnvironment = GetApplicationEnvironment(); var accessor = GetLoadContextAccessor(); var libraryManager = GetLibraryManager(); @@ -67,17 +74,15 @@ public void Compile_ReturnsCompilationFailureWithRelativePath() applicationEnvironment.Configuration)) .Returns(new CompilerOptions()); var mvcRazorHost = Mock.Of(); + var fileProvider = new TestFileProvider(); + var fileInfo = fileProvider.AddFile(viewPath, fileContent); var compilationService = new RoslynCompilationService(applicationEnvironment, accessor, libraryManager, compilerOptionsProvider.Object, - mvcRazorHost); - var fileInfo = new TestFileInfo - { - Content = fileContent, - PhysicalPath = "physical path" - }; + mvcRazorHost, + GetOptions(fileProvider)); var relativeFileInfo = new RelativeFileInfo(fileInfo, "some-relative-path"); // Act @@ -86,12 +91,13 @@ public void Compile_ReturnsCompilationFailureWithRelativePath() // Assert Assert.IsType(result); Assert.Null(result.CompiledType); - Assert.Equal(relativeFileInfo.RelativePath, result.CompilationFailure.SourceFilePath); - Assert.Equal(fileContent, result.CompilationFailure.SourceFileContent); + var compilationFailure = Assert.Single(result.CompilationFailures); + Assert.Equal(relativeFileInfo.RelativePath, compilationFailure.SourceFilePath); + Assert.Equal(fileContent, compilationFailure.SourceFileContent); } [Fact] - public void Compile_ReturnsApplicationRelativePath_IfPhyicalPathIsNotSpecified() + public void Compile_ReturnsGeneratedCodePath_IfLinePragmaIsNotAvailable() { // Arrange var fileContent = "file content"; @@ -111,7 +117,8 @@ public void Compile_ReturnsApplicationRelativePath_IfPhyicalPathIsNotSpecified() accessor, libraryManager, compilerOptionsProvider.Object, - mvcRazorHost); + mvcRazorHost, + GetOptions()); var relativeFileInfo = new RelativeFileInfo(new TestFileInfo { Content = fileContent }, "some-relative-path"); @@ -121,15 +128,20 @@ public void Compile_ReturnsApplicationRelativePath_IfPhyicalPathIsNotSpecified() // Assert Assert.IsType(result); Assert.Null(result.CompiledType); - Assert.Equal("some-relative-path", result.CompilationFailure.SourceFilePath); - Assert.Equal(fileContent, result.CompilationFailure.SourceFileContent); + + var compilationFailure = Assert.Single(result.CompilationFailures); + Assert.Equal("Generated Code", compilationFailure.SourceFilePath); + Assert.Equal(content, compilationFailure.SourceFileContent); } [Fact] public void Compile_DoesNotThrow_IfFileCannotBeRead() { // Arrange - var content = @"this should fail"; + var path = "some-relative-path"; + var content = $@" +#line 1 ""{path}"" +this should fail"; var applicationEnvironment = GetApplicationEnvironment(); var accessor = GetLoadContextAccessor(); var libraryManager = GetLibraryManager(); @@ -141,15 +153,20 @@ public void Compile_DoesNotThrow_IfFileCannotBeRead() .Returns(new CompilerOptions()); var mvcRazorHost = Mock.Of(); + var mockFileInfo = new Mock(); + mockFileInfo.Setup(f => f.CreateReadStream()) + .Throws(new Exception()); + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(path, mockFileInfo.Object); + var compilationService = new RoslynCompilationService(applicationEnvironment, accessor, libraryManager, compilerOptionsProvider.Object, - mvcRazorHost); - var mockFileInfo = new Mock(); - mockFileInfo.Setup(f => f.CreateReadStream()) - .Throws(new Exception()); - var relativeFileInfo = new RelativeFileInfo(mockFileInfo.Object, "some-relative-path"); + mvcRazorHost, + GetOptions(fileProvider)); + + var relativeFileInfo = new RelativeFileInfo(mockFileInfo.Object, path); // Act var result = compilationService.Compile(relativeFileInfo, content); @@ -157,8 +174,9 @@ public void Compile_DoesNotThrow_IfFileCannotBeRead() // Assert Assert.IsType(result); Assert.Null(result.CompiledType); - Assert.Equal("some-relative-path", result.CompilationFailure.SourceFilePath); - Assert.Null(result.CompilationFailure.SourceFileContent); + var compilationFailure = Assert.Single(result.CompilationFailures); + Assert.Equal(path, compilationFailure.SourceFilePath); + Assert.Null(compilationFailure.SourceFileContent); } [Fact] @@ -189,7 +207,8 @@ public class MyNonCustomDefinedClass {} accessor, libraryManager, compilerOptionsProvider.Object, - mvcRazorHost.Object); + mvcRazorHost.Object, + GetOptions()); var relativeFileInfo = new RelativeFileInfo(new TestFileInfo { PhysicalPath = "SomePath" }, "some-relative-path"); @@ -225,7 +244,8 @@ public class NotRazorPrefixType {}"; accessor, libraryManager, compilerOptionsProvider.Object, - mvcRazorHost.Object); + mvcRazorHost.Object, + GetOptions()); var relativeFileInfo = new RelativeFileInfo(new TestFileInfo { PhysicalPath = "SomePath" }, "some-relative-path"); @@ -238,6 +258,113 @@ public class NotRazorPrefixType {}"; Assert.Equal("RazorPrefixType", result.CompiledType.Name); } + [Fact] + public void GetCompilationFailedResult_ReturnsCompilationResult_WithGroupedMessages() + { + // Arrange + var viewPath = "Views/Home/Index"; + var generatedCodeFileName = "Generated Code"; + var fileProvider = new TestFileProvider(); + fileProvider.AddFile(viewPath, "view-content"); + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(new RazorViewEngineOptions + { + FileProvider = fileProvider + }); + var compilationService = new RoslynCompilationService( + GetApplicationEnvironment(), + GetLoadContextAccessor(), + GetLibraryManager(), + Mock.Of(), + Mock.Of(), + options.Object); + + var assemblyName = "random-assembly-name"; + + var diagnostics = new[] + { + Diagnostic.Create( + GetDiagnosticDescriptor("message-1"), + Location.Create( + viewPath, + new TextSpan(10, 5), + new LinePositionSpan(new LinePosition(10, 1), new LinePosition(10, 2)))), + Diagnostic.Create( + GetDiagnosticDescriptor("message-2"), + Location.Create( + assemblyName, + new TextSpan(1, 6), + new LinePositionSpan(new LinePosition(1, 2), new LinePosition(3, 4)))), + Diagnostic.Create( + GetDiagnosticDescriptor("message-3"), + Location.Create( + viewPath, + new TextSpan(40, 50), + new LinePositionSpan(new LinePosition(30, 5), new LinePosition(40, 12)))), + }; + + // Act + var compilationResult = compilationService.GetCompilationFailedResult( + viewPath, + "compilation-content", + assemblyName, + diagnostics); + + // Assert + Assert.Collection(compilationResult.CompilationFailures, + failure => + { + Assert.Equal(viewPath, failure.SourceFilePath); + Assert.Equal("view-content", failure.SourceFileContent); + Assert.Collection(failure.Messages, + message => + { + Assert.Equal("message-1", message.Message); + Assert.Equal(viewPath, message.SourceFilePath); + Assert.Equal(11, message.StartLine); + Assert.Equal(2, message.StartColumn); + Assert.Equal(11, message.EndLine); + Assert.Equal(3, message.EndColumn); + }, + message => + { + Assert.Equal("message-3", message.Message); + Assert.Equal(viewPath, message.SourceFilePath); + Assert.Equal(31, message.StartLine); + Assert.Equal(6, message.StartColumn); + Assert.Equal(41, message.EndLine); + Assert.Equal(13, message.EndColumn); + }); + }, + failure => + { + Assert.Equal(generatedCodeFileName, failure.SourceFilePath); + Assert.Equal("compilation-content", failure.SourceFileContent); + Assert.Collection(failure.Messages, + message => + { + Assert.Equal("message-2", message.Message); + Assert.Equal(assemblyName, message.SourceFilePath); + Assert.Equal(2, message.StartLine); + Assert.Equal(3, message.StartColumn); + Assert.Equal(4, message.EndLine); + Assert.Equal(5, message.EndColumn); + }); + }); + } + + private static DiagnosticDescriptor GetDiagnosticDescriptor(string messageFormat) + { + return new DiagnosticDescriptor( + id: "someid", + title: "sometitle", + messageFormat: messageFormat, + category: "some-category", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + } + private static ILibraryManager GetLibraryManager() { var fileReference = new Mock(); @@ -285,5 +412,18 @@ private IApplicationEnvironment GetApplicationEnvironment() return applicationEnvironment.Object; } + + private static IOptions GetOptions(IFileProvider fileProvider = null) + { + var razorViewEngineOptions = new RazorViewEngineOptions + { + FileProvider = fileProvider ?? new TestFileProvider() + }; + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(razorViewEngineOptions); + + return options.Object; + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.TestCommon/TestFileProvider.cs b/test/Microsoft.AspNet.Mvc.TestCommon/TestFileProvider.cs index a149075ad2..454f3af187 100644 --- a/test/Microsoft.AspNet.Mvc.TestCommon/TestFileProvider.cs +++ b/test/Microsoft.AspNet.Mvc.TestCommon/TestFileProvider.cs @@ -21,7 +21,7 @@ public virtual IDirectoryContents GetDirectoryContents(string subpath) throw new NotSupportedException(); } - public void AddFile(string path, string contents) + public TestFileInfo AddFile(string path, string contents) { var fileInfo = new TestFileInfo { @@ -32,6 +32,8 @@ public void AddFile(string path, string contents) }; AddFile(path, fileInfo); + + return fileInfo; } public void AddFile(string path, IFileInfo contents) diff --git a/test/WebSites/ErrorPageMiddlewareWebSite/ErrorPageMiddlewareController.cs b/test/WebSites/ErrorPageMiddlewareWebSite/ErrorPageMiddlewareController.cs index dda4e1f7a2..191f4aeb07 100644 --- a/test/WebSites/ErrorPageMiddlewareWebSite/ErrorPageMiddlewareController.cs +++ b/test/WebSites/ErrorPageMiddlewareWebSite/ErrorPageMiddlewareController.cs @@ -18,5 +18,11 @@ public IActionResult ParserError() { return View(); } + + [HttpGet("/ErrorFromGlobalImport")] + public IActionResult GlobalImportError() + { + return View("~/Views/ErrorFromGlobalImport/Index"); + } } } diff --git a/test/WebSites/ErrorPageMiddlewareWebSite/Views/ErrorFromGlobalImport/Index.cshtml b/test/WebSites/ErrorPageMiddlewareWebSite/Views/ErrorFromGlobalImport/Index.cshtml new file mode 100644 index 0000000000..6df88a66a1 --- /dev/null +++ b/test/WebSites/ErrorPageMiddlewareWebSite/Views/ErrorFromGlobalImport/Index.cshtml @@ -0,0 +1 @@ +

Hello world!

\ No newline at end of file diff --git a/test/WebSites/ErrorPageMiddlewareWebSite/Views/ErrorFromGlobalImport/_GlobalImport.cshtml b/test/WebSites/ErrorPageMiddlewareWebSite/Views/ErrorFromGlobalImport/_GlobalImport.cshtml new file mode 100644 index 0000000000..c7ee89fafd --- /dev/null +++ b/test/WebSites/ErrorPageMiddlewareWebSite/Views/ErrorFromGlobalImport/_GlobalImport.cshtml @@ -0,0 +1 @@ +@using NamespaceDoesNotExist \ No newline at end of file