aspnetcore/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationSer...

229 lines
9.0 KiB
C#

// 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.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
/// <summary>
/// A type that uses Roslyn to compile C# content.
/// </summary>
public class DefaultRoslynCompilationService : ICompilationService
{
// error CS0234: The type or namespace name 'C' does not exist in the namespace 'N' (are you missing
// an assembly reference?)
private const string CS0234 = nameof(CS0234);
// error CS0246: The type or namespace name 'T' could not be found (are you missing a using directive
// or an assembly reference?)
private const string CS0246 = nameof(CS0246);
private readonly CSharpCompiler _compiler;
private readonly ILogger _logger;
private readonly Action<RoslynCompilationContext> _compilationCallback;
/// <summary>
/// Initalizes a new instance of the <see cref="DefaultRoslynCompilationService"/> class.
/// </summary>
/// <param name="compiler">The <see cref="CSharpCompiler"/>.</param>
/// <param name="optionsAccessor">Accessor to <see cref="RazorViewEngineOptions"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public DefaultRoslynCompilationService(
CSharpCompiler compiler,
IOptions<RazorViewEngineOptions> optionsAccessor,
ILoggerFactory loggerFactory)
{
_compiler = compiler;
_compilationCallback = optionsAccessor.Value.CompilationCallback;
_logger = loggerFactory.CreateLogger<DefaultRoslynCompilationService>();
}
/// <inheritdoc />
public CompilationResult Compile(RazorCodeDocument codeDocument, RazorCSharpDocument cSharpDocument)
{
if (codeDocument == null)
{
throw new ArgumentNullException(nameof(codeDocument));
}
if (cSharpDocument == null)
{
throw new ArgumentNullException(nameof(codeDocument));
}
_logger.GeneratedCodeToAssemblyCompilationStart(codeDocument.Source.FileName);
var startTimestamp = _logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : 0;
var assemblyName = Path.GetRandomFileName();
var compilation = CreateCompilation(cSharpDocument.GeneratedCode, assemblyName);
using (var assemblyStream = new MemoryStream())
{
using (var pdbStream = new MemoryStream())
{
var result = compilation.Emit(
assemblyStream,
pdbStream,
options: _compiler.EmitOptions);
if (!result.Success)
{
return GetCompilationFailedResult(
codeDocument,
cSharpDocument.GeneratedCode,
assemblyName,
result.Diagnostics);
}
assemblyStream.Seek(0, SeekOrigin.Begin);
pdbStream.Seek(0, SeekOrigin.Begin);
var assembly = LoadAssembly(assemblyStream, pdbStream);
var type = assembly.GetExportedTypes().FirstOrDefault(a => !a.IsNested);
_logger.GeneratedCodeToAssemblyCompilationEnd(codeDocument.Source.FileName, startTimestamp);
return new CompilationResult(type);
}
}
}
private CSharpCompilation CreateCompilation(string compilationContent, string assemblyName)
{
var sourceText = SourceText.From(compilationContent, Encoding.UTF8);
var syntaxTree = _compiler.CreateSyntaxTree(sourceText).WithFilePath(assemblyName);
var compilation = _compiler
.CreateCompilation(assemblyName)
.AddSyntaxTrees(syntaxTree);
compilation = ExpressionRewriter.Rewrite(compilation);
var compilationContext = new RoslynCompilationContext(compilation);
_compilationCallback(compilationContext);
compilation = compilationContext.Compilation;
return compilation;
}
// Internal for unit testing
internal CompilationResult GetCompilationFailedResult(
RazorCodeDocument codeDocument,
string compilationContent,
string assemblyName,
IEnumerable<Diagnostic> diagnostics)
{
var diagnosticGroups = diagnostics
.Where(IsError)
.GroupBy(diagnostic => GetFilePath(codeDocument, diagnostic), StringComparer.Ordinal);
var failures = new List<CompilationFailure>();
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 = GetContent(codeDocument, sourceFilePath);
}
string additionalMessage = null;
if (group.Any(g =>
string.Equals(CS0234, g.Id, StringComparison.OrdinalIgnoreCase) ||
string.Equals(CS0246, g.Id, StringComparison.OrdinalIgnoreCase)))
{
additionalMessage = Resources.FormatCompilation_DependencyContextIsNotSpecified(
"Microsoft.NET.Sdk.Web",
"PreserveCompilationContext");
}
var compilationFailure = new CompilationFailure(
sourceFilePath,
sourceFileContent,
compilationContent,
group.Select(GetDiagnosticMessage),
additionalMessage);
failures.Add(compilationFailure);
}
return new CompilationResult(failures);
}
private static string GetFilePath(RazorCodeDocument codeDocument, Diagnostic diagnostic)
{
if (diagnostic.Location == Location.None)
{
return codeDocument.Source.FileName;
}
return diagnostic.Location.GetMappedLineSpan().Path;
}
private static bool IsError(Diagnostic diagnostic)
{
return diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error;
}
public static Assembly LoadAssembly(MemoryStream assemblyStream, MemoryStream pdbStream)
{
var assembly = AssemblyLoadContext.Default.LoadFromStream(assemblyStream, pdbStream);
return assembly;
}
private static string GetContent(RazorCodeDocument codeDocument, string filePath)
{
if (filePath == codeDocument.Source.FileName)
{
var chars = new char[codeDocument.Source.Length];
codeDocument.Source.CopyTo(0, chars, 0, chars.Length);
return new string(chars);
}
for (var i = 0; i < codeDocument.Imports.Count; i++)
{
var import = codeDocument.Imports[i];
if (filePath == import.FileName)
{
var chars = new char[codeDocument.Source.Length];
codeDocument.Source.CopyTo(0, chars, 0, chars.Length);
return new string(chars);
}
}
return null;
}
private static DiagnosticMessage GetDiagnosticMessage(Diagnostic diagnostic)
{
var mappedLineSpan = diagnostic.Location.GetMappedLineSpan();
return new DiagnosticMessage(
diagnostic.GetMessage(),
CSharpDiagnosticFormatter.Instance.Format(diagnostic),
mappedLineSpan.Path,
mappedLineSpan.StartLinePosition.Line + 1,
mappedLineSpan.StartLinePosition.Character + 1,
mappedLineSpan.EndLinePosition.Line + 1,
mappedLineSpan.EndLinePosition.Character + 1);
}
}
}