Ensure pragma checksum is prepended to generated file

Fixes #160
This commit is contained in:
Pranav K 2014-09-26 19:07:26 -07:00
parent 11ee402eec
commit 5369842384
35 changed files with 221 additions and 14 deletions

View File

@ -23,6 +23,11 @@ namespace Microsoft.AspNet.Razor.Generator
public CodeTreeBuilder CodeTreeBuilder { get; set; }
/// <summary>
/// Gets or sets the <c>SHA1</c> based checksum for the file whose location is defined by <see cref="SourceFile"/>.
/// </summary>
public string Checksum { get; set; }
public static CodeGeneratorContext Create(RazorEngineHost host, string className, string rootNamespace, string sourceFile, bool shouldGenerateLinePragmas)
{
return new CodeGeneratorContext()

View File

@ -10,6 +10,8 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
{
public class CSharpCodeBuilder : CodeBuilder
{
// See http://msdn.microsoft.com/en-us/library/system.codedom.codechecksumpragma.checksumalgorithmid.aspx
private const string Sha1AlgorithmId = "{ff1816ec-aa5e-4d10-87f7-6f4963833460}";
private const int DisableAsyncWarning = 1998;
public CSharpCodeBuilder(CodeGeneratorContext context)
@ -24,6 +26,17 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
{
var writer = new CSharpCodeWriter();
if (!Host.DesignTimeMode && !string.IsNullOrEmpty(Context.Checksum))
{
writer.Write("#pragma checksum \"")
.Write(Context.SourceFile)
.Write("\" \"")
.Write(Sha1AlgorithmId)
.Write("\" \"")
.Write(Context.Checksum)
.WriteLine("\"");
}
using (writer.BuildNamespace(Context.RootNamespace))
{
// Write out using directives
@ -102,7 +115,7 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
string taskNamespace = typeof(Task).Namespace;
// We need to add the task namespace but ONLY if it hasn't been added by the default imports or using imports yet.
if(!defaultImports.Contains(taskNamespace) && !usingVisitor.ImportedUsings.Contains(taskNamespace))
if (!defaultImports.Contains(taskNamespace) && !usingVisitor.ImportedUsings.Contains(taskNamespace))
{
writer.WriteUsing(taskNamespace);
}

View File

@ -4,7 +4,10 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Microsoft.AspNet.Razor.Generator;
using Microsoft.AspNet.Razor.Generator.Compiler;
@ -19,6 +22,7 @@ namespace Microsoft.AspNet.Razor
/// </summary>
public class RazorTemplateEngine
{
private const int BufferSize = 1024;
public static readonly string DefaultClassName = "Template";
public static readonly string DefaultNamespace = String.Empty;
@ -127,7 +131,12 @@ namespace Microsoft.AspNet.Razor
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so we don't want to dispose it")]
public GeneratorResults GenerateCode(ITextBuffer input, string className, string rootNamespace, string sourceFileName, CancellationToken? cancelToken)
{
return GenerateCodeCore(input.ToDocument(), className, rootNamespace, sourceFileName, cancelToken);
return GenerateCodeCore(input.ToDocument(),
className,
rootNamespace,
sourceFileName,
checksum: null,
cancelToken: cancelToken);
}
// See GenerateCode override which takes ITextBuffer, and BufferingTextReader for details.
@ -146,13 +155,83 @@ namespace Microsoft.AspNet.Razor
return GenerateCode(input, className, rootNamespace, sourceFileName, null);
}
/// <summary>
/// Parses the contents specified by the <paramref name="inputStream"/> and returns the generated code.
/// </summary>
/// <param name="inputStream">A <see cref="Stream"/> that represents the contents to be parsed.</param>
/// <param name="className">The name of the generated class. When <c>null</c>, defaults to
/// <see cref="Host.DefaultClassName"/>.</param>
/// <param name="rootNamespace">The namespace in which the generated class will reside. When <c>null</c>,
/// defaults to <see cref="Host.DefaultNamespace"/>.</param>
/// <param name="sourceFileName">The file name to use in line pragmas, usually the original Razor file.</param>
/// <returns>A <see cref="GeneratorResults"/> that represents the results of parsing the content.</returns>
/// <remarks>
/// This overload calculates the checksum of the contents of <paramref name="inputStream"/> prior to code
/// generation. The checksum is used for producing the <c>#pragma checksum</c> line pragma required for
/// debugging.
/// </remarks>
public GeneratorResults GenerateCode([NotNull] Stream inputStream,
string className,
string rootNamespace,
string sourceFileName)
{
MemoryStream memoryStream = null;
try
{
if (!inputStream.CanSeek)
{
memoryStream = new MemoryStream();
inputStream.CopyTo(memoryStream);
// We don't have to dispose the input stream since it is owned externally.
inputStream = memoryStream;
}
inputStream.Position = 0;
var checksum = ComputeChecksum(inputStream);
inputStream.Position = 0;
using (var reader = new StreamReader(inputStream,
Encoding.UTF8,
detectEncodingFromByteOrderMarks: true,
bufferSize: BufferSize,
leaveOpen: true))
{
var seekableStream = new SeekableTextReader(reader);
return GenerateCodeCore(seekableStream,
className,
rootNamespace,
sourceFileName,
checksum,
cancelToken: null);
}
}
finally
{
if (memoryStream != null)
{
memoryStream.Dispose();
}
}
}
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so we don't want to dispose it")]
public GeneratorResults GenerateCode(TextReader input, string className, string rootNamespace, string sourceFileName, CancellationToken? cancelToken)
{
return GenerateCodeCore(new SeekableTextReader(input), className, rootNamespace, sourceFileName, cancelToken);
return GenerateCodeCore(new SeekableTextReader(input),
className,
rootNamespace,
sourceFileName,
checksum: null,
cancelToken: cancelToken);
}
protected internal virtual GeneratorResults GenerateCodeCore(ITextDocument input, string className, string rootNamespace, string sourceFileName, CancellationToken? cancelToken)
protected internal virtual GeneratorResults GenerateCodeCore(ITextDocument input,
string className,
string rootNamespace,
string sourceFileName,
string checksum,
CancellationToken? cancelToken)
{
className = (className ?? Host.DefaultClassName) ?? DefaultClassName;
rootNamespace = (rootNamespace ?? Host.DefaultNamespace) ?? DefaultNamespace;
@ -167,7 +246,11 @@ namespace Microsoft.AspNet.Razor
generator.DesignTimeMode = Host.DesignTimeMode;
generator.Visit(results);
var builder = CreateCodeBuilder(generator.Context);
var codeGenerationContext = generator.Context;
codeGenerationContext.Checksum = checksum;
var builder = CreateCodeBuilder(codeGenerationContext);
var builderResult = builder.Build();
// Collect results and return
@ -194,8 +277,24 @@ namespace Microsoft.AspNet.Razor
protected internal virtual CodeBuilder CreateCodeBuilder(CodeGeneratorContext context)
{
return Host.DecorateCodeBuilder(Host.CodeLanguage.CreateCodeBuilder(context),
return Host.DecorateCodeBuilder(Host.CodeLanguage.CreateCodeBuilder(context),
context);
}
private static string ComputeChecksum(Stream inputStream)
{
byte[] hashedBytes;
using (var hashAlgorithm = SHA1.Create())
{
hashedBytes = hashAlgorithm.ComputeHash(inputStream);
}
var fileHashBuilder = new StringBuilder(hashedBytes.Length * 2);
foreach (var value in hashedBytes)
{
fileHashBuilder.Append(value.ToString("x2"));
}
return fileHashBuilder.ToString();
}
}
}

View File

@ -15,7 +15,8 @@
"System.Runtime.InteropServices": "4.0.20.0",
"System.Threading": "4.0.0.0",
"System.Threading.Tasks": "4.0.10.0",
"System.Threading.Thread": "4.0.0.0"
"System.Threading.Thread": "4.0.0.0",
"System.Security.Cryptography.Hashing.Algorithms": "4.0.0.0"
},
"frameworks": {
"net45": { },

View File

@ -99,8 +99,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator
baselineName = name;
}
string sourceLocation = string.Format("/CodeGenerator/{1}/Source/{0}.{2}", name, LanguageName, FileExtension);
string source = TestFile.Create(string.Format("TestFiles/CodeGenerator/CS/Source/{0}.{1}", name, FileExtension)).ReadAllText();
string sourceLocation = string.Format("TestFiles/CodeGenerator/{1}/Source/{0}.{2}", name, LanguageName, FileExtension);
string expectedOutput = TestFile.Create(string.Format("TestFiles/CodeGenerator/CS/Output/{0}.{1}", baselineName, BaselineExtension)).ReadAllText();
// Set up the host and engine
@ -136,11 +135,11 @@ namespace Microsoft.AspNet.Razor.Test.Generator
// Generate code for the file
GeneratorResults results = null;
using (StringTextBuffer buffer = new StringTextBuffer(source))
using (var source = TestFile.Create(sourceLocation).OpenRead())
{
results = engine.GenerateCode(buffer, className: name, rootNamespace: TestRootNamespaceName, sourceFileName: generatePragmas ? String.Format("{0}.{1}", name, FileExtension) : null);
var sourceFileName = generatePragmas ? String.Format("{0}.{1}", name, FileExtension) : null;
results = engine.GenerateCode(source, className: name, rootNamespace: TestRootNamespaceName, sourceFileName: sourceFileName);
}
// Only called if GENERATE_BASELINES is set, otherwise compiled out.
BaselineWriter.WriteBaseline(String.Format(@"test\Microsoft.AspNet.Razor.Test\TestFiles\CodeGenerator\{0}\Output\{1}.{2}", LanguageName, baselineName, BaselineExtension), results.GeneratedCode);
@ -179,7 +178,7 @@ namespace Microsoft.AspNet.Razor.Test.Generator
for (var i = 0; i < expectedDesignTimePragmas.Count; i++)
{
if(!expectedDesignTimePragmas[i].Equals(results.DesignTimeLineMappings[i]))
if (!expectedDesignTimePragmas[i].Equals(results.DesignTimeLineMappings[i]))
{
Assert.True(false, String.Format("Line mapping {0} is not equivalent.", i));
}

View File

@ -2,7 +2,9 @@
// 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;
using System.Threading;
using System.Web.WebPages.TestUtils;
using Microsoft.AspNet.Razor.Generator;
@ -10,6 +12,7 @@ using Microsoft.AspNet.Razor.Generator.Compiler.CSharp;
using Microsoft.AspNet.Razor.Parser;
using Microsoft.AspNet.Razor.Text;
using Moq;
using Moq.Protected;
using Xunit;
namespace Microsoft.AspNet.Razor.Test
@ -161,7 +164,7 @@ namespace Microsoft.AspNet.Razor.Test
// Assert
mockEngine.Verify(e => e.GenerateCodeCore(It.Is<SeekableTextReader>(l => l.ReadToEnd() == "foo"),
className, ns, src, source.Token));
className, ns, src, null, source.Token));
}
[Fact]
@ -212,6 +215,42 @@ namespace Microsoft.AspNet.Razor.Test
Assert.NotNull(results.DesignTimeLineMappings);
}
public static IEnumerable<object[]> GenerateCodeCalculatesLinePragma_IfStreamInputIsUsedData
{
get
{
// Seekable stream
var content = Encoding.UTF8.GetBytes("Hello world");
var stream = new MemoryStream(content);
yield return new[] { stream };
// Non seekable stream
var mockStream = new Mock<MemoryStream>(content)
{
CallBase = true
};
mockStream.Setup(m => m.CanSeek)
.Returns(false);
yield return new[] { mockStream.Object };
}
}
[Theory]
[MemberData(nameof(GenerateCodeCalculatesLinePragma_IfStreamInputIsUsedData))]
public void GenerateCodeCalculatesLinePragma_IfStreamInputIsUsed(Stream stream)
{
// Arrange
var engine = new TestableRazorTemplateEngine();
// Act
var results = engine.GenerateCode(stream, "some-class", "some-ns", "foo.cshtml");
// Assert
Assert.Equal("7b502c3a1f48c8609ae212cdfb639dee39673f5e", engine.Checksum);
}
private static RazorEngineHost CreateHost(bool designTime = false)
{
return new RazorEngineHost(new CSharpRazorCodeLanguage())
@ -219,5 +258,27 @@ namespace Microsoft.AspNet.Razor.Test
DesignTimeMode = designTime
};
}
private class TestableRazorTemplateEngine : RazorTemplateEngine
{
public TestableRazorTemplateEngine()
: base(CreateHost())
{
}
public string Checksum { get; set; }
protected internal override GeneratorResults GenerateCodeCore(ITextDocument input,
string className,
string rootNamespace,
string sourceFileName,
string checksum,
CancellationToken? cancelToken)
{
Checksum = checksum;
return null;
}
}
}
}

View File

@ -1,3 +1,4 @@
#pragma checksum "Await.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "5a02c3ec89081f6e0a8f4810e127eed40467c358"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "Blocks.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "ed55fda2b7b0b96b044fc45da658dc062479924f"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "CodeBlock.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "d570564dcb19afbfb9fcd1b8a7e339d22c0d0c97"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "ConditionalAttributes.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "f0a97e23ecfbaaaa77b528650156cb123ebdbe60"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "ExplicitExpression.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "0fa6894c606c7426da1d8cacfbacf8be971c777f"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "ExpressionsInCode.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "3ccb5b16f61b84dd82d7402e4a17870a39d09ca9"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "FunctionsBlock.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "e6a053bfeb65ba3e17885a8ae1523f28a3483258"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "FunctionsBlock_Tabs.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "93dd195557fe9c2e5a15b75d76608f2cb7082f3f"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "Helpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "228b0ea0de0f06806d10a9768bb4afd7e0ecb878"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "Helpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "228b0ea0de0f06806d10a9768bb4afd7e0ecb878"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "HelpersMissingCloseParen.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "a59fed8a1d7b5333e081339188fe2dba59c71e41"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "HelpersMissingOpenBrace.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "f81212c85e39fa08cb4c95e2339817caa725397c"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "HelpersMissingOpenParen.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "dc407d9349ea9a1595c65660d41a63970de65729"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "HtmlCommentWithQuote_Double.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "520aebbcdbdb163b131356be8f3aef0eb0809c47"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "HtmlCommentWithQuote_Single.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "2080c7f4c5de3e5e7f309c11a6be5c5e8d86e75a"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "ImplicitExpression.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "68460c577f1cb90794f25535a35a82ff2aa4c193"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "Imports.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "6e81e2fa106d657a3f2e198f0c687cb19b2f7e9e"
namespace TestOutput
{
#line 1 "Imports.cshtml"

View File

@ -1,3 +1,4 @@
#pragma checksum "Inherits.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "b9264c58289dbea68e46a818fd0c4c4d835b3a84"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "InlineBlocks.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "85fc1bd0306a5a6164d3d866bd690ff95cba0a8e"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "Instrumented.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "24ae301f33f984680e86aa6c7ae226809531ffe9"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "LayoutDirective.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "936a0ebedd8d855480541640e61dacbb8b0e56c0"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "MarkupInCodeBlock.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "539d5763b7091d29df375085f623b9086e7f8ad6"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "NestedCodeBlocks.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "d83923f86f1c84f081916ef7308513ab80a65675"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "NestedHelpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "e8232b2b30af7dadb0698abf8ba08851f401963d"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "c6fa3992fa56644768995c97941d682d90f6d8ec"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "RazorComments.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "051345e2cc0313fea445db2f6cf48fe28b0b4edf"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "ResolveUrl.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "ef2ca9797f9fbd9788685830524a0daacb2a30e8"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "Sections.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "0bd579cc5b9e6bd12b720003df47899d0a0207cf"
namespace TestOutput
{
using System;

View File

@ -1,3 +1,4 @@
#pragma checksum "Templates.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "3a6da4fcc3d9b28618d5703e4925d45491b5a013"
namespace TestOutput
{
using System;