Get rid of RazorCompiler

Adds a little more use of Razor extensibility.

Razor is a plugin model, so we can't be the 'first mover' for initiating
compilation in the build tools and IDE.

Reorganizes tests and fills out more reusable test infrastructure for
Razor-driven testing.

Adds tests for declaration-only configuration.
This commit is contained in:
Ryan Nowak 2018-02-28 20:16:15 -08:00 committed by Steve Sanderson
parent daf6a404f9
commit 6182e8448d
30 changed files with 1892 additions and 1409 deletions

View File

@ -1,13 +1,13 @@
// 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 Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation;
using Microsoft.AspNetCore.Blazor.Razor;
using Microsoft.Extensions.CommandLineUtils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Blazor.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands
{
@ -47,24 +47,35 @@ namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands
return 1;
}
var inputRazorFilePaths = FindRazorFiles(sourceDirPathValue).ToList();
var fileSystem = RazorProjectFileSystem.Create(sourceDirPathValue);
var engine = RazorProjectEngine.Create(BlazorExtensionInitializer.DefaultConfiguration, fileSystem, b =>
{
BlazorExtensionInitializer.Register(b);
});
var diagnostics = new List<RazorDiagnostic>();
var sourceFiles = FindRazorFiles(sourceDirPathValue).ToList();
using (var outputWriter = new StreamWriter(outputFilePath.Value()))
{
var diagnostics = new RazorCompiler().CompileFiles(
sourceDirPathValue,
inputRazorFilePaths,
baseNamespace.Value(),
outputWriter,
verboseFlag.HasValue() ? Console.Out : null);
foreach (var diagnostic in diagnostics)
foreach (var sourceFile in sourceFiles)
{
Console.WriteLine(diagnostic.FormatForConsole());
}
var item = fileSystem.GetItem(sourceFile);
var hasError = diagnostics.Any(item => item.Type == RazorCompilerDiagnostic.DiagnosticType.Error);
return hasError ? 1 : 0;
var codeDocument = engine.Process(item);
var cSharpDocument = codeDocument.GetCSharpDocument();
outputWriter.WriteLine(cSharpDocument.GeneratedCode);
diagnostics.AddRange(cSharpDocument.Diagnostics);
}
}
foreach (var diagnostic in diagnostics)
{
Console.WriteLine(diagnostic.ToString());
}
var hasError = diagnostics.Any(item => item.Severity == RazorDiagnosticSeverity.Error);
return hasError ? 1 : 0;
});
}

View File

@ -1,35 +0,0 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using System.IO;
namespace Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation
{
internal class BlazorProjectItem : RazorProjectItem
{
private readonly string _projectBasePath;
private readonly string _itemFullPhysicalPath;
private readonly Stream _itemContents;
public BlazorProjectItem(
string projectBasePath,
string itemFullPhysicalPath,
Stream itemFileContents)
{
_projectBasePath = projectBasePath;
_itemFullPhysicalPath = itemFullPhysicalPath;
_itemContents = itemFileContents;
}
public override string BasePath => _projectBasePath;
public override string FilePath => _itemFullPhysicalPath;
public override string PhysicalPath => _itemFullPhysicalPath;
public override bool Exists => true;
public override Stream Read() => _itemContents;
}
}

View File

@ -1,167 +0,0 @@
// 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.Linq;
using System.CodeDom.Compiler;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Razor;
using Microsoft.AspNetCore.Blazor.RenderTree;
namespace Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation
{
/// <summary>
/// Provides facilities for transforming Razor files into Blazor component classes.
/// </summary>
public class RazorCompiler
{
private static CodeDomProvider _csharpCodeDomProvider = CodeDomProvider.CreateProvider("c#");
/// <summary>
/// Writes C# source code representing Blazor components defined by Razor files.
/// </summary>
/// <param name="inputRootPath">Path to a directory containing input files.</param>
/// <param name="inputPaths">Paths to the input files relative to <paramref name="inputRootPath"/>. The generated namespaces will be based on these relative paths.</param>
/// <param name="baseNamespace">The base namespace for the generated classes.</param>
/// <param name="resultOutput">A <see cref="TextWriter"/> to which C# source code will be written.</param>
/// <param name="verboseOutput">If not null, additional information will be written to this <see cref="TextWriter"/>.</param>
/// <returns>A collection of <see cref="RazorCompilerDiagnostic"/> instances representing any warnings or errors that were encountered.</returns>
public ICollection<RazorCompilerDiagnostic> CompileFiles(
string inputRootPath,
IEnumerable<string> inputPaths,
string baseNamespace,
TextWriter resultOutput,
TextWriter verboseOutput)
=> inputPaths.SelectMany(path =>
{
using (var reader = File.OpenRead(path))
{
return CompileSingleFile(inputRootPath, path, reader, baseNamespace, resultOutput, verboseOutput);
}
}).ToList();
/// <summary>
/// Writes C# source code representing a Blazor component defined by a Razor file.
/// </summary>
/// <param name="inputRootPath">Path to a directory containing input files.</param>
/// <param name="inputPaths">Paths to the input files relative to <paramref name="inputRootPath"/>. The generated namespaces will be based on these relative paths.</param>
/// <param name="baseNamespace">The base namespace for the generated class.</param>
/// <param name="resultOutput">A <see cref="TextWriter"/> to which C# source code will be written.</param>
/// <param name="verboseOutput">If not null, additional information will be written to this <see cref="TextWriter"/>.</param>
/// <returns>An enumerable of <see cref="RazorCompilerDiagnostic"/> instances representing any warnings or errors that were encountered.</returns>
public IEnumerable<RazorCompilerDiagnostic> CompileSingleFile(
string inputRootPath,
string inputFilePath,
Stream inputFileContents,
string baseNamespace,
TextWriter resultOutput,
TextWriter verboseOutput)
{
if (inputFileContents == null)
{
throw new ArgumentNullException(nameof(inputFileContents));
}
if (resultOutput == null)
{
throw new ArgumentNullException(nameof(resultOutput));
}
if (string.IsNullOrEmpty(inputRootPath))
{
throw new ArgumentException("Cannot be null or empty.", nameof(inputRootPath));
}
if (string.IsNullOrEmpty(baseNamespace))
{
throw new ArgumentException("Cannot be null or empty.", nameof(baseNamespace));
}
try
{
verboseOutput?.WriteLine($"Compiling {inputFilePath}...");
var (itemNamespace, itemClassName) = GetNamespaceAndClassName(inputRootPath, inputFilePath);
var combinedNamespace = string.IsNullOrEmpty(itemNamespace)
? baseNamespace
: $"{baseNamespace}.{itemNamespace}";
// TODO: Pass through info about whether this is a design-time build, and if so,
// just emit enough of a stub class that intellisense will show the correct type
// name and any public members. Don't need to actually emit all the RenderTreeBuilder
// invocations.
var engine = RazorEngine.Create(b => BlazorExtensionInitializer.Register(b));
var blazorTemplateEngine = new BlazorTemplateEngine(
engine,
RazorProjectFileSystem.Create(inputRootPath));
var codeDoc = blazorTemplateEngine.CreateCodeDocument(
new BlazorProjectItem(inputRootPath, inputFilePath, inputFileContents));
codeDoc.Items[BlazorCodeDocItems.Namespace] = combinedNamespace;
codeDoc.Items[BlazorCodeDocItems.ClassName] = itemClassName;
var csharpDocument = blazorTemplateEngine.GenerateCode(codeDoc);
var generatedCode = csharpDocument.GeneratedCode;
// Add parameters to the primary method via string manipulation because
// DefaultDocumentWriter's VisitMethodDeclaration can't emit parameters
var primaryMethodSource = $"protected override void {BlazorComponent.BuildRenderTreeMethodName}";
generatedCode = generatedCode.Replace(
$"{primaryMethodSource}()",
$"{primaryMethodSource}({typeof(RenderTreeBuilder).FullName} builder)");
resultOutput.WriteLine(generatedCode);
return Enumerable.Empty<RazorCompilerDiagnostic>();
}
catch (RazorCompilerException ex)
{
return new[] { ex.ToDiagnostic(inputFilePath) };
}
catch (Exception ex)
{
return new[]
{
new RazorCompilerDiagnostic(
RazorCompilerDiagnostic.DiagnosticType.Error,
inputFilePath,
1,
1,
$"Unexpected exception: {ex.Message}{Environment.NewLine}{ex.StackTrace}")
};
}
}
private static (string, string) GetNamespaceAndClassName(string inputRootPath, string inputFilePath)
{
// First represent inputFilePath as a path relative to inputRootPath. Not using Path.GetRelativePath
// because it doesn't handle cases like inputFilePath="\\something.cs".
var inputFilePathAbsolute = Path.GetFullPath(Path.Combine(inputRootPath, inputFilePath));
var inputRootPathWithTrailingSeparator = inputRootPath.EndsWith(Path.DirectorySeparatorChar)
? inputRootPath
: (inputRootPath + Path.DirectorySeparatorChar);
var inputFilePathRelative = inputFilePathAbsolute.StartsWith(inputRootPathWithTrailingSeparator)
? inputFilePathAbsolute.Substring(inputRootPathWithTrailingSeparator.Length)
: throw new RazorCompilerException($"File is not within source root directory: '{inputFilePath}'");
// Use the set of directory names in the relative path as namespace
var inputDirname = Path.GetDirectoryName(inputFilePathRelative);
var resultNamespace = inputDirname
.Replace(Path.DirectorySeparatorChar, '.')
.Replace(" ", string.Empty);
// Use the filename as class name
var inputBasename = Path.GetFileNameWithoutExtension(inputFilePathRelative);
if (!IsValidClassName(inputBasename))
{
throw new RazorCompilerException($"Invalid name '{inputBasename}'. The name must be valid for a C# class name.");
}
return (resultNamespace, inputBasename);
}
private static bool IsValidClassName(string name)
=> _csharpCodeDomProvider.IsValidIdentifier(name);
}
}

View File

@ -0,0 +1,41 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal class BlazorRazorCSharpLoweringPhase : RazorEnginePhaseBase, IRazorCSharpLoweringPhase
{
protected override void ExecuteCore(RazorCodeDocument codeDocument)
{
var documentNode = codeDocument.GetDocumentIntermediateNode();
ThrowForMissingDocumentDependency(documentNode);
var writer = new DocumentWriterWorkaround().Create(documentNode.Target, documentNode.Options);
try
{
var cSharpDocument = writer.WriteDocument(codeDocument, documentNode);
codeDocument.SetCSharpDocument(cSharpDocument);
}
catch (RazorCompilerException ex)
{
// Currently the Blazor code generation has some 'fatal errors' that can cause code generation
// to fail completely. This class is here to make that implementation work gracefully.
var cSharpDocument = RazorCSharpDocument.Create("", documentNode.Options, new[] { ex.Diagnostic });
codeDocument.SetCSharpDocument(cSharpDocument);
}
}
private class DocumentWriterWorkaround : DocumentWriter
{
public override RazorCSharpDocument WriteDocument(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -0,0 +1,76 @@
// 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 AngleSharp;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal static class BlazorDiagnosticFactory
{
public static readonly RazorDiagnosticDescriptor InvalidComponentAttributeSyntax = new RazorDiagnosticDescriptor(
"BL9980",
() => "Wrong syntax for '{0}' on '{1}': As a temporary " +
$"limitation, component attributes must be expressed with C# syntax. For example, " +
$"SomeParam=@(\"Some value\") is allowed, but SomeParam=\"Some value\" is not.",
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic Create_InvalidComponentAttributeSynx(TextPosition position, SourceSpan? span, string attributeName, string componentName)
{
span = CalculateSourcePosition(span, position);
return RazorDiagnostic.Create(InvalidComponentAttributeSyntax, span ?? SourceSpan.Undefined, attributeName, componentName);
}
public static readonly RazorDiagnosticDescriptor UnexpectedClosingTag = new RazorDiagnosticDescriptor(
"BL9981",
() => "Unexpected closing tag '{0}' with no matching start tag.",
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic Create_UnexpectedClosingTag(SourceSpan? span, string tagName)
{
return RazorDiagnostic.Create(UnexpectedClosingTag, span ?? SourceSpan.Undefined, tagName);
}
public static readonly RazorDiagnosticDescriptor MismatchedClosingTag = new RazorDiagnosticDescriptor(
"BL9982",
() => "Mismatching closing tag. Found '{0}' but expected '{1}'.",
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic Create_MismatchedClosingTag(SourceSpan? span, string expectedTagName, string tagName)
{
return RazorDiagnostic.Create(MismatchedClosingTag, span ?? SourceSpan.Undefined, expectedTagName, tagName);
}
public static readonly RazorDiagnosticDescriptor MismatchedClosingTagKind = new RazorDiagnosticDescriptor(
"BL9984",
() => "Mismatching closing tag. Found '{0}' of type '{1}' but expected type '{2}'.",
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic Create_MismatchedClosingTagKind(SourceSpan? span, string tagName, string kind, string expectedKind)
{
return RazorDiagnostic.Create(MismatchedClosingTagKind, span ?? SourceSpan.Undefined, tagName, kind, expectedKind);
}
private static SourceSpan? CalculateSourcePosition(
SourceSpan? razorTokenPosition,
TextPosition htmlNodePosition)
{
if (razorTokenPosition.HasValue)
{
var razorPos = razorTokenPosition.Value;
return new SourceSpan(
razorPos.FilePath,
razorPos.AbsoluteIndex + htmlNodePosition.Position,
razorPos.LineIndex + htmlNodePosition.Line - 1,
htmlNodePosition.Line == 1
? razorPos.CharacterIndex + htmlNodePosition.Column - 1
: htmlNodePosition.Column - 1,
length: 1);
}
else
{
return null;
}
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Extensions;
@ -10,6 +11,38 @@ namespace Microsoft.AspNetCore.Blazor.Razor
{
public class BlazorExtensionInitializer : RazorExtensionInitializer
{
public static readonly RazorConfiguration DeclarationConfiguration;
public static readonly RazorConfiguration DefaultConfiguration;
static BlazorExtensionInitializer()
{
// RazorConfiguration is changing between 15.7 and preview2 builds of Razor, this is a reflection-based
// workaround.
DeclarationConfiguration = Create("BlazorDeclaration-0.1");
DefaultConfiguration = Create("Blazor-0.1");
RazorConfiguration Create(string configurationName)
{
var args = new object[] { RazorLanguageVersion.Version_2_1, configurationName, Array.Empty<RazorExtension>(), };
MethodInfo method;
ConstructorInfo constructor;
if ((method = typeof(RazorConfiguration).GetMethod("Create", BindingFlags.Public | BindingFlags.Static)) != null)
{
return (RazorConfiguration)method.Invoke(null, args);
}
else if ((constructor = typeof(RazorConfiguration).GetConstructors().FirstOrDefault()) != null)
{
return (RazorConfiguration)constructor.Invoke(args);
}
else
{
throw new InvalidOperationException("Can't create a configuration. This is bad.");
}
}
}
public static void Register(RazorProjectEngineBuilder builder)
{
if (builder == null)
@ -26,29 +59,21 @@ namespace Microsoft.AspNetCore.Blazor.Razor
builder.Features.Remove(builder.Features.OfType<IImportProjectFeature>().Single());
builder.Features.Add(new BlazorImportProjectFeature());
var index = builder.Phases.IndexOf(builder.Phases.OfType<IRazorCSharpLoweringPhase>().Single());
builder.Phases[index] = new BlazorRazorCSharpLoweringPhase();
builder.Features.Add(new ConfigureBlazorCodeGenerationOptions());
builder.Features.Add(new ComponentDocumentClassifierPass());
}
// This is temporarily used to initialize a RazorEngine by the build tools until we get the features
// we need into the RazorProjectEngine (namespace).
public static void Register(IRazorEngineBuilder builder)
{
if (builder == null)
builder.Features.Add(new ComponentTagHelperDescriptorProvider());
if (builder.Configuration.ConfigurationName == DeclarationConfiguration.ConfigurationName)
{
throw new ArgumentNullException(nameof(builder));
// This is for 'declaration only' processing. We don't want to try and emit any method bodies during
// the design time build because we can't do it correctly until the set of components is known.
builder.Features.Add(new EliminateMethodBodyPass());
}
FunctionsDirective.Register(builder);
ImplementsDirective.Register(builder);
InheritsDirective.Register(builder);
InjectDirective.Register(builder);
LayoutDirective.Register(builder);
builder.Features.Add(new ConfigureBlazorCodeGenerationOptions());
builder.Features.Add(new ComponentDocumentClassifierPass());
}
public override void Initialize(RazorProjectEngineBuilder builder)

View File

@ -253,7 +253,12 @@ namespace Microsoft.AspNetCore.Blazor.Razor
if (isComponent && nextTag.Attributes.Count > 0)
{
ThrowTemporaryComponentSyntaxError(node, nextTag, tagNameOriginalCase);
var diagnostic = BlazorDiagnosticFactory.Create_InvalidComponentAttributeSynx(
nextTag.Position,
node.Source,
nextTag.Attributes[0].Key,
tagNameOriginalCase);
throw new RazorCompilerException(diagnostic);
}
foreach (var attribute in nextTag.Attributes)
@ -368,13 +373,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor
}
}
private void ThrowTemporaryComponentSyntaxError(HtmlContentIntermediateNode node, HtmlTagToken tag, string componentName)
=> throw new RazorCompilerException(
$"Wrong syntax for '{tag.Attributes[0].Key}' on '{componentName}': As a temporary " +
$"limitation, component attributes must be expressed with C# syntax. For example, " +
$"SomeParam=@(\"Some value\") is allowed, but SomeParam=\"Some value\" is not.",
CalculateSourcePosition(node.Source, tag.Position));
private SourceSpan? CalculateSourcePosition(
SourceSpan? razorTokenPosition,
TextPosition htmlNodePosition)

View File

@ -1,57 +0,0 @@
// 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.IO;
using Microsoft.AspNetCore.Razor.Language;
using System.Text;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Blazor.Razor
{
/// <summary>
/// A <see cref="RazorTemplateEngine"/> for Blazor components.
/// </summary>
public class BlazorTemplateEngine : RazorTemplateEngine
{
// We need to implement and register this feature for tooling support to work. Subclassing TemplateEngine
// doesn't work inside visual studio.
private readonly BlazorImportProjectFeature _feature;
public BlazorTemplateEngine(RazorEngine engine, RazorProject project)
: base(engine, project)
{
_feature = new BlazorImportProjectFeature();
Options.DefaultImports = RazorSourceDocument.ReadFrom(_feature.DefaultImports);
}
public override IEnumerable<RazorProjectItem> GetImportItems(RazorProjectItem projectItem)
{
if (projectItem == null)
{
throw new System.ArgumentNullException(nameof(projectItem));
}
return _feature.GetHierarchicalImports(Project, projectItem);
}
private static RazorSourceDocument GetDefaultImports()
{
using (var stream = new MemoryStream())
using (var writer = new StreamWriter(stream, Encoding.UTF8))
{
// TODO: Add other commonly-used Blazor namespaces here. Can't do so yet
// because the tooling wouldn't know about it, so it would still look like
// an error if you hadn't explicitly imported them.
writer.WriteLine("@using System");
writer.WriteLine("@using System.Collections.Generic");
writer.WriteLine("@using System.Linq");
writer.WriteLine("@using System.Threading.Tasks");
writer.Flush();
stream.Position = 0;
return RazorSourceDocument.ReadFrom(stream, fileName: null, encoding: Encoding.UTF8);
}
}
}
}

View File

@ -0,0 +1,72 @@
// 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.Globalization;
using System.Text;
namespace Microsoft.AspNetCore.Blazor.Razor
{
// Copied from the Razor repo
internal static class CSharpIdentifier
{
private const string CshtmlExtension = ".cshtml";
public static string GetClassNameFromPath(string path)
{
if (string.IsNullOrEmpty(path))
{
return path;
}
if (path.EndsWith(CshtmlExtension, StringComparison.OrdinalIgnoreCase))
{
path = path.Substring(0, path.Length - CshtmlExtension.Length);
}
return SanitizeClassName(path);
}
// CSharp Spec §2.4.2
private static bool IsIdentifierStart(char character)
{
return char.IsLetter(character) ||
character == '_' ||
CharUnicodeInfo.GetUnicodeCategory(character) == UnicodeCategory.LetterNumber;
}
public static bool IsIdentifierPart(char character)
{
return char.IsDigit(character) ||
IsIdentifierStart(character) ||
IsIdentifierPartByUnicodeCategory(character);
}
private static bool IsIdentifierPartByUnicodeCategory(char character)
{
var category = CharUnicodeInfo.GetUnicodeCategory(character);
return category == UnicodeCategory.NonSpacingMark || // Mn
category == UnicodeCategory.SpacingCombiningMark || // Mc
category == UnicodeCategory.ConnectorPunctuation || // Pc
category == UnicodeCategory.Format; // Cf
}
public static string SanitizeClassName(string inputName)
{
if (!IsIdentifierStart(inputName[0]) && IsIdentifierPart(inputName[0]))
{
inputName = "_" + inputName;
}
var builder = new StringBuilder(inputName.Length);
for (var i = 0; i < inputName.Length; i++)
{
var ch = inputName[i];
builder.Append(IsIdentifierPart(ch) ? ch : '_');
}
return builder.ToString();
}
}
}

View File

@ -1,19 +1,34 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using System;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal class ComponentDocumentClassifierPass : DocumentClassifierPassBase, IRazorDocumentClassifierPass
public class ComponentDocumentClassifierPass : DocumentClassifierPassBase, IRazorDocumentClassifierPass
{
public static readonly string ComponentDocumentKind = "Blazor.Component-0.1";
private static readonly char[] PathSeparators = new char[] { '/', '\\' };
// This is a fallback value and will only be used if we can't compute
// a reasonable namespace.
public string BaseNamespace { get; set; } = "__BlazorGenerated";
// Set to true in the IDE so we can generated mangled class names. This is needed
// to avoid conflicts between generated design-time code and the code in the editor.
//
// A better workaround for this would be to create a singlefilegenerator that overrides
// the codegen process when a document is open, but this is more involved, so hacking
// it for now.
public bool MangleClassNames { get; set; } = false;
protected override string DocumentKind => ComponentDocumentKind;
protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
@ -28,24 +43,27 @@ namespace Microsoft.AspNetCore.Blazor.Razor
ClassDeclarationIntermediateNode @class,
MethodDeclarationIntermediateNode method)
{
@namespace.Content = (string)codeDocument.Items[BlazorCodeDocItems.Namespace];
if (@namespace.Content == null)
if (!TryComputeNamespaceAndClass(
codeDocument.Source.FilePath,
codeDocument.Source.RelativePath,
out var computedNamespace,
out var computedClass))
{
@namespace.Content = "Blazor";
// If we can't compute a nice namespace (no relative path) then just generate something
// mangled.
computedNamespace = BaseNamespace;
computedClass = CSharpIdentifier.GetClassNameFromPath(codeDocument.Source.FilePath) ?? "__BlazorComponent";
}
if (MangleClassNames)
{
computedClass = "__" + computedClass;
}
@namespace.Content = computedNamespace;
@class.BaseType = BlazorApi.BlazorComponent.FullTypeName;
@class.ClassName = (string)codeDocument.Items[BlazorCodeDocItems.ClassName];
if (@class.ClassName == null)
{
@class.ClassName = codeDocument.Source.FilePath == null ? null : Path.GetFileNameWithoutExtension(codeDocument.Source.FilePath);
}
if (@class.ClassName == null)
{
@class.ClassName = "__BlazorComponent";
}
@class.ClassName = computedClass;
@class.Modifiers.Clear();
@class.Modifiers.Add("public");
@ -72,6 +90,50 @@ namespace Microsoft.AspNetCore.Blazor.Razor
method.Children.Insert(0, callBase);
}
// In general documents will have a relative path (relative to the project root).
// We can only really compute a nice class/namespace when we know a relative path.
//
// However all kinds of thing are possible in tools. We shouldn't barf here if the document isn't
// set up correctly.
private bool TryComputeNamespaceAndClass(string filePath, string relativePath, out string @namespace, out string @class)
{
if (filePath == null || relativePath == null || filePath.Length <= relativePath.Length)
{
@namespace = null;
@class = null;
return false;
}
// Try and infer a namespace from the project directory. We don't yet have the ability to pass
// the namespace through from the project.
var trimLength = relativePath.Length + (relativePath.StartsWith("/") ? 0 : 1);
var baseDirectory = filePath.Substring(0, filePath.Length - trimLength);
var baseNamespace = Path.GetFileName(baseDirectory);
if (string.IsNullOrEmpty(baseNamespace))
{
@namespace = null;
@class = null;
return false;
}
var builder = new StringBuilder();
builder.Append(baseNamespace); // Don't sanitize, we expect it to contain dots.
var segments = relativePath.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries);
// Skip the last segment because it's the FileName.
for (var i = 0; i < segments.Length - 1; i++)
{
builder.Append('.');
builder.Append(CSharpIdentifier.SanitizeClassName(segments[i]));
}
@namespace = builder.ToString();
@class = CSharpIdentifier.SanitizeClassName(Path.GetFileNameWithoutExtension(relativePath));
return true;
}
#region Workaround
// This is a workaround for the fact that the base class doesn't provide good support
// for replacing the IntermediateNodeWriter when building the code target.

View File

@ -0,0 +1,31 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal class EliminateMethodBodyPass : IntermediateNodePassBase, IRazorOptimizationPass
{
// Run early in the optimization phase
public override int Order => Int32.MinValue;
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
{
if (codeDocument == null)
{
throw new ArgumentNullException(nameof(codeDocument));
}
if (documentNode == null)
{
throw new ArgumentNullException(nameof(documentNode));
}
var method = documentNode.FindPrimaryMethod();
method.Children.Clear();
}
}
}

View File

@ -28,16 +28,5 @@ namespace Microsoft.AspNetCore.Blazor.Razor
builder.AddDirective(Directive);
builder.Features.Add(new ImplementsDirectivePass());
}
public static void Register(IRazorEngineBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(Directive);
builder.Features.Add(new ImplementsDirectivePass());
}
}
}

View File

@ -35,12 +35,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor
builder.Features.Add(new Pass());
}
public static void Register(IRazorEngineBuilder builder)
{
builder.AddDirective(Directive);
builder.Features.Add(new Pass());
}
private class Pass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
{
protected override void ExecuteCore(

View File

@ -28,16 +28,5 @@ namespace Microsoft.AspNetCore.Blazor.Razor
builder.AddDirective(Directive);
builder.Features.Add(new LayoutDirectivePass());
}
public static void Register(IRazorEngineBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.AddDirective(Directive);
builder.Features.Add(new LayoutDirectivePass());
}
}
}

View File

@ -6,5 +6,7 @@ using Microsoft.AspNetCore.Blazor.Razor;
using Microsoft.AspNetCore.Razor.Language;
[assembly: ProvideRazorExtensionInitializer("Blazor-0.1", typeof(BlazorExtensionInitializer))]
[assembly: ProvideRazorExtensionInitializer("BlazorDeclaration-0.1", typeof(BlazorExtensionInitializer))]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Build.Test")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Razor.Extensions.Test")]

View File

@ -1,40 +0,0 @@
// 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.
namespace Microsoft.AspNetCore.Blazor.Razor
{
public class RazorCompilerDiagnostic
{
public DiagnosticType Type { get; }
public string SourceFilePath { get; }
public int Line { get; }
public int Column { get; }
public string Message { get; }
public RazorCompilerDiagnostic(
DiagnosticType type,
string sourceFilePath,
int line,
int column,
string message)
{
Type = type;
SourceFilePath = sourceFilePath;
Line = line;
Column = column;
Message = message;
}
public enum DiagnosticType
{
Warning,
Error
}
public string FormatForConsole()
=> $"{SourceFilePath}({Line},{Column}): {FormatTypeAndCodeForConsole()}: {Message}";
private string FormatTypeAndCodeForConsole()
=> $"{Type.ToString().ToLowerInvariant()} Blazor";
}
}

View File

@ -12,25 +12,11 @@ namespace Microsoft.AspNetCore.Blazor.Razor
/// </summary>
public class RazorCompilerException : Exception
{
private readonly int _line;
private readonly int _column;
public RazorCompilerException(string message) : this(message, null)
public RazorCompilerException(RazorDiagnostic diagnostic)
{
Diagnostic = diagnostic;
}
public RazorCompilerException(string message, SourceSpan? source) : base(message)
{
_line = source.HasValue ? (source.Value.LineIndex + 1) : 1;
_column = source.HasValue ? (source.Value.CharacterIndex + 1) : 1;
}
public RazorCompilerDiagnostic ToDiagnostic(string sourceFilePath)
=> new RazorCompilerDiagnostic(
RazorCompilerDiagnostic.DiagnosticType.Error,
sourceFilePath,
line: _line,
column: _column,
message: Message);
public RazorDiagnostic Diagnostic { get; }
}
}

View File

@ -29,15 +29,15 @@ namespace Microsoft.AspNetCore.Blazor.Razor
{
if (_stack.Count == 0)
{
throw new RazorCompilerException(
$"Unexpected closing tag '{tagName}' with no matching start tag.", source);
var diagnostic = BlazorDiagnosticFactory.Create_UnexpectedClosingTag(source ?? SourceSpan.Undefined, tagName);
throw new RazorCompilerException(diagnostic);
}
var currentScope = _stack.Pop();
if (!tagName.Equals(currentScope.TagName, StringComparison.Ordinal))
{
throw new RazorCompilerException(
$"Mismatching closing tag. Found '{tagName}' but expected '{currentScope.TagName}'.", source);
var diagnostic = BlazorDiagnosticFactory.Create_MismatchedClosingTag(source, currentScope.TagName, tagName);
throw new RazorCompilerException(diagnostic);
}
// Note: there's no unit test to cover the following, because there's no known way of
@ -45,8 +45,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor
// just in case one day it turns out there is some way of causing this error.
if (isComponent != currentScope.IsComponent)
{
throw new RazorCompilerException(
$"Mismatching closing tag. Found '{tagName}' of type '{(isComponent ? "component" : "element")}' but expected type '{(currentScope.IsComponent ? "component" : "element")}'.", source);
var kind = isComponent ? "component" : "element";
var expectedKind = currentScope.IsComponent ? "component" : "element";
var diagnostic = BlazorDiagnosticFactory.Create_MismatchedClosingTagKind(source, tagName, kind, expectedKind);
throw new RazorCompilerException(diagnostic);
}
// When closing the scope for a component with children, it's time to close the lambda

View File

@ -0,0 +1,131 @@
// 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.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Layouts;
using Microsoft.AspNetCore.Blazor.RenderTree;
using Microsoft.AspNetCore.Blazor.Test.Helpers;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
{
public class ComponentRenderingRazorIntegrationTest : RazorIntegrationTestBase
{
[Fact]
public void SupportsChildComponentsViaTemporarySyntax()
{
// Arrange/Act
var testComponentTypeName = FullTypeName<TestComponent>();
var component = CompileToComponent($"<c:{testComponentTypeName} />");
var frames = GetRenderTree(component);
// Assert
Assert.Collection(frames,
frame => AssertFrame.Component<TestComponent>(frame, 1, 0));
}
[Fact]
public void CanPassParametersToComponents()
{
// Arrange/Act
var testComponentTypeName = FullTypeName<TestComponent>();
var testObjectTypeName = FullTypeName<SomeType>();
// TODO: Once we have the improved component tooling and can allow syntax
// like StringProperty="My string" or BoolProperty=true, update this
// test to use that syntax.
var component = CompileToComponent($"<c:{testComponentTypeName}" +
$" IntProperty=@(123)" +
$" BoolProperty=@true" +
$" StringProperty=@(\"My string\")" +
$" ObjectProperty=@(new {testObjectTypeName}()) />");
var frames = GetRenderTree(component);
// Assert
Assert.Collection(frames,
frame => AssertFrame.Component<TestComponent>(frame, 5, 0),
frame => AssertFrame.Attribute(frame, "IntProperty", 123, 1),
frame => AssertFrame.Attribute(frame, "BoolProperty", true, 2),
frame => AssertFrame.Attribute(frame, "StringProperty", "My string", 3),
frame =>
{
AssertFrame.Attribute(frame, "ObjectProperty", 4);
Assert.IsType<SomeType>(frame.AttributeValue);
});
}
[Fact]
public void CanIncludeChildrenInComponents()
{
// Arrange/Act
var testComponentTypeName = FullTypeName<TestComponent>();
var component = CompileToComponent($"<c:{testComponentTypeName} MyAttr=@(\"abc\")>" +
$"Some text" +
$"<some-child a='1'>Nested text</some-child>" +
$"</c:{testComponentTypeName}>");
var frames = GetRenderTree(component);
// Assert: component frames are correct
Assert.Collection(frames,
frame => AssertFrame.Component<TestComponent>(frame, 3, 0),
frame => AssertFrame.Attribute(frame, "MyAttr", "abc", 1),
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 2));
// Assert: Captured ChildContent frames are correct
var childFrames = GetFrames((RenderFragment)frames[2].AttributeValue);
Assert.Collection(childFrames,
frame => AssertFrame.Text(frame, "Some text", 3),
frame => AssertFrame.Element(frame, "some-child", 3, 4),
frame => AssertFrame.Attribute(frame, "a", "1", 5),
frame => AssertFrame.Text(frame, "Nested text", 6));
}
[Fact]
public void CanNestComponentChildContent()
{
// Arrange/Act
var testComponentTypeName = FullTypeName<TestComponent>();
var component = CompileToComponent(
$"<c:{testComponentTypeName}>" +
$"<c:{testComponentTypeName}>" +
$"Some text" +
$"</c:{testComponentTypeName}>" +
$"</c:{testComponentTypeName}>");
var frames = GetRenderTree(component);
// Assert: outer component frames are correct
Assert.Collection(frames,
frame => AssertFrame.Component<TestComponent>(frame, 2, 0),
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 1));
// Assert: first level of ChildContent is correct
// Note that we don't really need the sequence numbers to continue on from the
// sequence numbers at the parent level. All that really matters is that they are
// correct relative to each other (i.e., incrementing) within the nesting level.
// As an implementation detail, it happens that they do follow on from the parent
// level, but we could change that part of the implementation if we wanted.
var innerFrames = GetFrames((RenderFragment)frames[1].AttributeValue).ToArray();
Assert.Collection(innerFrames,
frame => AssertFrame.Component<TestComponent>(frame, 2, 2),
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 3));
// Assert: second level of ChildContent is correct
Assert.Collection(GetFrames((RenderFragment)innerFrames[1].AttributeValue),
frame => AssertFrame.Text(frame, "Some text", 4));
}
public class SomeType { }
public class TestComponent : IComponent
{
public void Init(RenderHandle renderHandle)
{
}
public void SetParameters(ParameterCollection parameters)
{
}
}
}
}

View File

@ -0,0 +1,112 @@
// 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.Reflection;
using System.Text;
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Razor;
using Microsoft.AspNetCore.Blazor.Test.Helpers;
using Microsoft.AspNetCore.Razor.Language;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
{
public class DeclarationRazorIntegrationTest : RazorIntegrationTestBase
{
internal override RazorConfiguration Configuration => BlazorExtensionInitializer.DeclarationConfiguration;
[Fact]
public void DeclarationConfiguration_IncludesFunctions()
{
// Arrange & Act
var component = CompileToComponent(@"
@functions {
public string Value { get; set; }
}");
// Assert
var property = component.GetType().GetProperty("Value");
Assert.NotNull(property);
Assert.Same(typeof(string), property.PropertyType);
}
[Fact]
public void DeclarationConfiguration_IncludesInject()
{
// Arrange & Act
var component = CompileToComponent(@"
@inject string Value
");
// Assert
var property = component.GetType().GetProperty("Value", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(property);
Assert.Same(typeof(string), property.PropertyType);
}
[Fact]
public void DeclarationConfiguration_IncludesUsings()
{
// Arrange & Act
var component = CompileToComponent(@"
@using System.Text
@inject StringBuilder Value
");
// Assert
var property = component.GetType().GetProperty("Value", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(property);
Assert.Same(typeof(StringBuilder), property.PropertyType);
}
[Fact]
public void DeclarationConfiguration_IncludesInherits()
{
// Arrange & Act
var component = CompileToComponent($@"
@inherits {FullTypeName<BaseClass>()}
");
// Assert
Assert.Same(typeof(BaseClass), component.GetType().BaseType);
}
[Fact]
public void DeclarationConfiguration_IncludesImplements()
{
// Arrange & Act
var component = CompileToComponent($@"
@implements {FullTypeName<IDoCoolThings>()}
");
// Assert
var type = component.GetType();
Assert.Contains(typeof(IDoCoolThings), component.GetType().GetInterfaces());
}
[Fact]
public void DeclarationConfiguration_RenderMethodIsEmpty()
{
// Arrange & Act
var component = CompileToComponent(@"
<html>
@{ var message = ""hi""; }
<span class=""@(5 + 7)"">@message</span>
</html>
");
var frames = GetRenderTree(component);
// Assert
Assert.Empty(frames);
}
public class BaseClass : BlazorComponent
{
}
public interface IDoCoolThings
{
}
}
}

View File

@ -0,0 +1,89 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
{
public class DiagnosticRazorIntegrationTest : RazorIntegrationTestBase
{
[Fact]
public void TemporaryComponentSyntaxRejectsParametersExpressedAsPlainHtmlAttributes()
{
// This is a temporary syntax restriction. Currently you can write:
// <c:MyComponent MyParam=@("My value") />
// ... but are *not* allowed to write:
// <c:MyComponent MyParam="My value" />
// This is because until we get the improved taghelper-based tooling,
// we're using AngleSharp to parse the plain HTML attributes, and it
// suffers from limitations:
// * Loses the casing of attribute names (MyParam becomes myparam)
// * Doesn't recognize MyBool=true as an bool (becomes mybool="true"),
// plus equivalent for other primitives like enum values
// So to avoid people getting runtime errors, we're currently imposing
// the compile-time restriction that component params have to be given
// as C# expressions, e.g., MyBool=@true and MyString=@("Hello")
// Arrange/Act
var result = CompileToCSharp(
$"Line 1\n" +
$"Some text <c:MyComponent MyParam=\"My value\" />");
// Assert
Assert.Collection(
result.Diagnostics,
item =>
{
Assert.Equal("BL9980", item.Id);
Assert.Equal(
$"Wrong syntax for 'myparam' on 'c:MyComponent': As a temporary " +
$"limitation, component attributes must be expressed with C# syntax. For " +
$"example, SomeParam=@(\"Some value\") is allowed, but SomeParam=\"Some value\" " +
$"is not.", item.GetMessage());
Assert.Equal(1, item.Span.LineIndex);
Assert.Equal(10, item.Span.CharacterIndex);
});
}
[Fact]
public void RejectsEndTagWithNoStartTag()
{
// Arrange/Act
var result = CompileToCSharp(
"Line1\nLine2\nLine3</mytag>");
// Assert
Assert.Collection(result.Diagnostics,
item =>
{
Assert.Equal("BL9981", item.Id);
Assert.Equal("Unexpected closing tag 'mytag' with no matching start tag.", item.GetMessage());
});
}
[Fact]
public void RejectsEndTagWithDifferentNameToStartTag()
{
// Arrange/Act
var result = CompileToCSharp(
$"@{{\n" +
$" var abc = 123;\n" +
$"}}\n" +
$"<root>\n" +
$" <other />\n" +
$" text\n" +
$" <child>more text</root>\n" +
$"</child>\n");
// Assert
Assert.Collection(result.Diagnostics,
item =>
{
Assert.Equal("BL9982", item.Id);
Assert.Equal("Mismatching closing tag. Found 'child' but expected 'root'.", item.GetMessage());
Assert.Equal(6, item.Span.LineIndex);
Assert.Equal(20, item.Span.CharacterIndex);
});
}
}
}

View File

@ -0,0 +1,144 @@
// 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.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Layouts;
using Microsoft.AspNetCore.Blazor.Test.Helpers;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
{
// Integration tests for Blazor's directives
public class DirectiveRazorIntegrationTest : RazorIntegrationTestBase
{
[Fact]
public void ComponentsDoNotHaveLayoutAttributeByDefault()
{
// Arrange/Act
var component = CompileToComponent($"Hello");
// Assert
Assert.Null(component.GetType().GetCustomAttribute<LayoutAttribute>());
}
[Fact]
public void SupportsLayoutDeclarations()
{
// Arrange/Act
var testComponentTypeName = FullTypeName<TestLayout>();
var component = CompileToComponent(
$"@layout {testComponentTypeName}\n" +
$"Hello");
var frames = GetRenderTree(component);
// Assert
var layoutAttribute = component.GetType().GetCustomAttribute<LayoutAttribute>();
Assert.NotNull(layoutAttribute);
Assert.Equal(typeof(TestLayout), layoutAttribute.LayoutType);
Assert.Collection(frames,
frame => AssertFrame.Text(frame, "Hello"));
}
[Fact]
public void SupportsImplementsDeclarations()
{
// Arrange/Act
var testInterfaceTypeName = FullTypeName<ITestInterface>();
var component = CompileToComponent(
$"@implements {testInterfaceTypeName}\n" +
$"Hello");
var frames = GetRenderTree(component);
// Assert
Assert.IsAssignableFrom<ITestInterface>(component);
Assert.Collection(frames,
frame => AssertFrame.Text(frame, "Hello"));
}
[Fact]
public void SupportsInheritsDirective()
{
// Arrange/Act
var testBaseClassTypeName = FullTypeName<TestBaseClass>();
var component = CompileToComponent(
$"@inherits {testBaseClassTypeName}" + Environment.NewLine +
$"Hello");
var frames = GetRenderTree(component);
// Assert
Assert.IsAssignableFrom<TestBaseClass>(component);
Assert.Collection(frames,
frame => AssertFrame.Text(frame, "Hello"));
}
[Fact]
public void SupportsInjectDirective()
{
// Arrange/Act 1: Compilation
var componentType = CompileToComponent(
$"@inject {FullTypeName<IMyService1>()} MyService1\n" +
$"@inject {FullTypeName<IMyService2>()} MyService2\n" +
$"Hello from @MyService1 and @MyService2").GetType();
// Assert 1: Compiled type has correct properties
var propertyFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.NonPublic;
var injectableProperties = componentType.GetProperties(propertyFlags)
.Where(p => p.GetCustomAttribute<InjectAttribute>() != null);
Assert.Collection(injectableProperties.OrderBy(p => p.Name),
property =>
{
Assert.Equal("MyService1", property.Name);
Assert.Equal(typeof(IMyService1), property.PropertyType);
Assert.False(property.GetMethod.IsPublic);
Assert.False(property.SetMethod.IsPublic);
},
property =>
{
Assert.Equal("MyService2", property.Name);
Assert.Equal(typeof(IMyService2), property.PropertyType);
Assert.False(property.GetMethod.IsPublic);
Assert.False(property.SetMethod.IsPublic);
});
// Arrange/Act 2: DI-supplied component has correct behavior
var serviceProvider = new TestServiceProvider();
serviceProvider.AddService<IMyService1>(new MyService1Impl());
serviceProvider.AddService<IMyService2>(new MyService2Impl());
var componentFactory = new ComponentFactory(serviceProvider);
var component = componentFactory.InstantiateComponent(componentType);
var frames = GetRenderTree(component);
// Assert 2: Rendered component behaves correctly
Assert.Collection(frames,
frame => AssertFrame.Text(frame, "Hello from "),
frame => AssertFrame.Text(frame, typeof(MyService1Impl).FullName),
frame => AssertFrame.Text(frame, " and "),
frame => AssertFrame.Text(frame, typeof(MyService2Impl).FullName));
}
public class TestLayout : ILayoutComponent
{
public RenderFragment Body { get; set; }
public void Init(RenderHandle renderHandle)
{
}
public void SetParameters(ParameterCollection parameters)
{
}
}
public interface ITestInterface { }
public class TestBaseClass : BlazorComponent { }
public interface IMyService1 { }
public interface IMyService2 { }
public class MyService1Impl : IMyService1 { }
public class MyService2Impl : IMyService2 { }
}
}

View File

@ -0,0 +1,48 @@
// 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.IO;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
{
// Integration tests focused on file path handling for class/namespace names
public class FilePathRazorIntegrationTest : RazorIntegrationTestBase
{
[Fact]
public void FileNameIsInvalidClasName_SanitizesInvalidClassName()
{
// Arrange
// Act
var result = CompileToAssembly("Filename with spaces.cshtml", "");
// Assert
Assert.Empty(result.Diagnostics);
var type = Assert.Single(result.Assembly.GetTypes());
Assert.Equal(DefaultBaseNamespace, type.Namespace);
Assert.Equal("Filename_with_spaces", type.Name);
}
[Theory]
[InlineData("ItemAtRoot.cs", "Test", "ItemAtRoot")]
[InlineData("Dir1\\MyFile.cs", "Test.Dir1", "MyFile")]
[InlineData("Dir1\\Dir2\\MyFile.cs", "Test.Dir1.Dir2", "MyFile")]
public void CreatesClassWithCorrectNameAndNamespace(string relativePath, string expectedNamespace, string expectedClassName)
{
// Arrange
relativePath = relativePath.Replace('\\', Path.DirectorySeparatorChar);
// Act
var result = CompileToAssembly(relativePath, "");
// Assert
Assert.Empty(result.Diagnostics);
var type = Assert.Single(result.Assembly.GetTypes());
Assert.Equal(expectedNamespace, type.Namespace);
Assert.Equal(expectedClassName, type.Name);
}
}
}

View File

@ -0,0 +1,40 @@
// 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.IO;
namespace Microsoft.AspNetCore.Razor.Language
{
/// <summary>
/// A <see cref="RazorProjectItem"/> that does not exist.
/// </summary>
internal class NotFoundProjectItem : RazorProjectItem
{
/// <summary>
/// Initializes a new instance of <see cref="NotFoundProjectItem"/>.
/// </summary>
/// <param name="basePath">The base path.</param>
/// <param name="path">The path.</param>
public NotFoundProjectItem(string basePath, string path)
{
BasePath = basePath;
FilePath = path;
}
/// <inheritdoc />
public override string BasePath { get; }
/// <inheritdoc />
public override string FilePath { get; }
/// <inheritdoc />
public override bool Exists => false;
/// <inheritdoc />
public override string PhysicalPath => throw new NotSupportedException();
/// <inheritdoc />
public override Stream Read() => throw new NotSupportedException();
}
}

View File

@ -0,0 +1,215 @@
// 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.Linq;
namespace Microsoft.AspNetCore.Razor.Language
{
internal class VirtualRazorProjectFileSystem : RazorProjectFileSystem
{
private readonly DirectoryNode _root = new DirectoryNode("/");
public override IEnumerable<RazorProjectItem> EnumerateItems(string basePath)
{
basePath = NormalizeAndEnsureValidPath(basePath);
var directory = _root.GetDirectory(basePath);
return directory?.EnumerateItems() ?? Enumerable.Empty<RazorProjectItem>();
}
public override RazorProjectItem GetItem(string path)
{
path = NormalizeAndEnsureValidPath(path);
return _root.GetItem(path) ?? new NotFoundProjectItem(string.Empty, path);
}
public void Add(RazorProjectItem projectItem)
{
if (projectItem == null)
{
throw new ArgumentNullException(nameof(projectItem));
}
var filePath = NormalizeAndEnsureValidPath(projectItem.FilePath);
_root.AddFile(new FileNode(filePath, projectItem));
}
// Internal for testing
[DebuggerDisplay("{Path}")]
internal class DirectoryNode
{
public DirectoryNode(string path)
{
Path = path;
}
public string Path { get; }
public List<DirectoryNode> Directories { get; } = new List<DirectoryNode>();
public List<FileNode> Files { get; } = new List<FileNode>();
public void AddFile(FileNode fileNode)
{
var filePath = fileNode.Path;
if (!filePath.StartsWith(Path, StringComparison.OrdinalIgnoreCase))
{
var message = "Error";
throw new InvalidOperationException(message);
}
// Look for the first / that appears in the path after the current directory path.
var directoryPath = GetDirectoryPath(filePath);
var directory = GetOrAddDirectory(this, directoryPath, createIfNotExists: true);
Debug.Assert(directory != null);
directory.Files.Add(fileNode);
}
public DirectoryNode GetDirectory(string path)
{
if (!path.StartsWith(Path, StringComparison.OrdinalIgnoreCase))
{
var message = "Error";
throw new InvalidOperationException(message);
}
return GetOrAddDirectory(this, path);
}
public IEnumerable<RazorProjectItem> EnumerateItems()
{
foreach (var file in Files)
{
yield return file.ProjectItem;
}
foreach (var directory in Directories)
{
foreach (var file in directory.EnumerateItems())
{
yield return file;
}
}
}
public RazorProjectItem GetItem(string path)
{
if (!path.StartsWith(Path, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Error");
}
var directoryPath = GetDirectoryPath(path);
var directory = GetOrAddDirectory(this, directoryPath);
if (directory == null)
{
return null;
}
foreach (var file in directory.Files)
{
var filePath = file.Path;
var directoryLength = directory.Path.Length;
// path, filePath -> /Views/Home/Index.cshtml
// directory.Path -> /Views/Home/
// We only need to match the file name portion since we've already matched the directory segment.
if (string.Compare(path, directoryLength, filePath, directoryLength, path.Length - directoryLength, StringComparison.OrdinalIgnoreCase) == 0)
{
return file.ProjectItem;
}
}
return null;
}
private static string GetDirectoryPath(string path)
{
// /dir1/dir2/file.cshtml -> /dir1/dir2/
var fileNameIndex = path.LastIndexOf('/');
if (fileNameIndex == -1)
{
return path;
}
return path.Substring(0, fileNameIndex + 1);
}
private static DirectoryNode GetOrAddDirectory(
DirectoryNode directory,
string path,
bool createIfNotExists = false)
{
Debug.Assert(!string.IsNullOrEmpty(path));
if (path[path.Length - 1] != '/')
{
path += '/';
}
int index;
while ((index = path.IndexOf('/', directory.Path.Length)) != -1 && index != path.Length)
{
var subDirectory = FindSubDirectory(directory, path);
if (subDirectory == null)
{
if (createIfNotExists)
{
var directoryPath = path.Substring(0, index + 1); // + 1 to include trailing slash
subDirectory = new DirectoryNode(directoryPath);
directory.Directories.Add(subDirectory);
}
else
{
return null;
}
}
directory = subDirectory;
}
return directory;
}
private static DirectoryNode FindSubDirectory(DirectoryNode parentDirectory, string path)
{
for (var i = 0; i < parentDirectory.Directories.Count; i++)
{
// ParentDirectory.Path -> /Views/Home/
// CurrentDirectory.Path -> /Views/Home/SubDir/
// Path -> /Views/Home/SubDir/MorePath/File.cshtml
// Each invocation of FindSubDirectory returns the immediate subdirectory along the path to the file.
var currentDirectory = parentDirectory.Directories[i];
var directoryPath = currentDirectory.Path;
var startIndex = parentDirectory.Path.Length;
var directoryNameLength = directoryPath.Length - startIndex;
if (string.Compare(path, startIndex, directoryPath, startIndex, directoryPath.Length - startIndex, StringComparison.OrdinalIgnoreCase) == 0)
{
return currentDirectory;
}
}
return null;
}
}
// Internal for testing
[DebuggerDisplay("{Path}")]
internal struct FileNode
{
public FileNode(string path, RazorProjectItem projectItem)
{
Path = path;
ProjectItem = projectItem;
}
public string Path { get; }
public RazorProjectItem ProjectItem { get; }
}
}
}

View File

@ -0,0 +1,36 @@
// 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.IO;
namespace Microsoft.AspNetCore.Razor.Language
{
internal class VirtualProjectItem : RazorProjectItem
{
private readonly byte[] _content;
public VirtualProjectItem(string basePath, string filePath, string physicalPath, string relativePhysicalPath, byte[] content)
{
BasePath = basePath;
FilePath = filePath;
PhysicalPath = physicalPath;
RelativePhysicalPath = relativePhysicalPath;
_content = content;
}
public override string BasePath { get; }
public override string RelativePhysicalPath { get; }
public override string FilePath { get; }
public override string PhysicalPath { get; }
public override bool Exists => true;
public override Stream Read()
{
return new MemoryStream(_content);
}
}
}

View File

@ -0,0 +1,240 @@
// 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.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Razor;
using Microsoft.AspNetCore.Blazor.Rendering;
using Microsoft.AspNetCore.Blazor.RenderTree;
using Microsoft.AspNetCore.Blazor.Test.Helpers;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.Blazor.Build.Test
{
public class RazorIntegrationTestBase
{
internal const string ArbitraryWindowsPath = "x:\\dir\\subdir\\Test";
internal const string ArbitraryMacLinuxPath = "/dir/subdir/Test";
private RazorProjectEngine _projectEngine;
public RazorIntegrationTestBase()
{
Configuration = BlazorExtensionInitializer.DefaultConfiguration;
FileSystem = new VirtualRazorProjectFileSystem();
WorkingDirectory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ArbitraryWindowsPath : ArbitraryMacLinuxPath;
DefaultBaseNamespace = "Test"; // Matches the default working directory
DefaultFileName = "TestComponent.cshtml";
}
internal virtual RazorConfiguration Configuration { get; }
internal virtual string DefaultBaseNamespace { get; }
internal virtual string DefaultFileName { get; }
internal virtual VirtualRazorProjectFileSystem FileSystem { get; }
internal virtual RazorProjectEngine ProjectEngine
{
get
{
if (_projectEngine == null)
{
_projectEngine = CreateProjectEngine();
}
return _projectEngine;
}
}
internal virtual string WorkingDirectory { get; }
internal RazorProjectEngine CreateProjectEngine()
{
return RazorProjectEngine.Create(Configuration, FileSystem, b =>
{
BlazorExtensionInitializer.Register(b);
});
}
protected CompileToCSharpResult CompileToCSharp(string cshtmlContent)
{
return CompileToCSharp(WorkingDirectory, DefaultFileName, cshtmlContent);
}
protected CompileToCSharpResult CompileToCSharp(string cshtmlRelativePath, string cshtmlContent)
{
return CompileToCSharp(WorkingDirectory, cshtmlRelativePath, cshtmlContent);
}
protected CompileToCSharpResult CompileToCSharp(string cshtmlRootPath, string cshtmlRelativePath, string cshtmlContent)
{
// FilePaths in Razor are **always** are of the form '/a/b/c.cshtml'
var filePath = cshtmlRelativePath.Replace('\\', '/');
if (!filePath.StartsWith('/'))
{
filePath = '/' + filePath;
}
var projectItem = new VirtualProjectItem(
cshtmlRootPath,
filePath,
Path.Combine(cshtmlRootPath, cshtmlRelativePath),
cshtmlRelativePath,
Encoding.UTF8.GetBytes(cshtmlContent));
var codeDocument = ProjectEngine.Process(projectItem);
return new CompileToCSharpResult
{
CodeDocument = codeDocument,
Code = codeDocument.GetCSharpDocument().GeneratedCode,
Diagnostics = codeDocument.GetCSharpDocument().Diagnostics,
};
}
protected CompileToAssemblyResult CompileToAssembly(string cshtmlRelativePath, string cshtmlContent)
{
return CompileToAssembly(WorkingDirectory, cshtmlRelativePath, cshtmlContent);
}
protected CompileToAssemblyResult CompileToAssembly(string cshtmlRootDirectory, string cshtmlRelativePath, string cshtmlContent)
{
var cSharpResult = CompileToCSharp(cshtmlRootDirectory, cshtmlRelativePath, cshtmlContent);
return CompileToAssembly(cSharpResult);
}
protected CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult cSharpResult)
{
if (cSharpResult.Diagnostics.Any())
{
var diagnosticsLog = string.Join(Environment.NewLine, cSharpResult.Diagnostics.Select(d => d.ToString()).ToArray());
throw new InvalidOperationException($"Aborting compilation to assembly because RazorCompiler returned nonempty diagnostics: {diagnosticsLog}");
}
var syntaxTrees = new[]
{
CSharpSyntaxTree.ParseText(cSharpResult.Code)
};
var referenceAssembliesContainingTypes = new[]
{
typeof(System.Runtime.AssemblyTargetedPatchBandAttribute), // System.Runtime
typeof(BlazorComponent),
typeof(RazorIntegrationTestBase), // Reference this assembly, so that we can refer to test component types
};
var references = referenceAssembliesContainingTypes
.SelectMany(type => type.Assembly.GetReferencedAssemblies().Concat(new[] { type.Assembly.GetName() }))
.Distinct()
.Select(Assembly.Load)
.Select(assembly => MetadataReference.CreateFromFile(assembly.Location))
.ToList();
var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
var assemblyName = "TestAssembly" + Guid.NewGuid().ToString("N");
var compilation = CSharpCompilation.Create(assemblyName,
syntaxTrees,
references,
options);
using (var peStream = new MemoryStream())
{
compilation.Emit(peStream);
var diagnostics = compilation
.GetDiagnostics()
.Where(d => d.Severity != DiagnosticSeverity.Hidden);
return new CompileToAssemblyResult
{
Diagnostics = diagnostics,
Assembly = diagnostics.Any() ? null : Assembly.Load(peStream.ToArray())
};
}
}
protected IComponent CompileToComponent(string cshtmlSource)
{
var assemblyResult = CompileToAssembly(WorkingDirectory, DefaultFileName, cshtmlSource);
var componentFullTypeName = $"{DefaultBaseNamespace}.{Path.GetFileNameWithoutExtension(DefaultFileName)}";
return CompileToComponent(assemblyResult, componentFullTypeName);
}
protected IComponent CompileToComponent(CompileToCSharpResult cSharpResult, string fullTypeName)
{
return CompileToComponent(CompileToAssembly(cSharpResult), fullTypeName);
}
protected IComponent CompileToComponent(CompileToAssemblyResult assemblyResult, string fullTypeName)
{
Assert.Empty(assemblyResult.Diagnostics);
var componentType = assemblyResult.Assembly.GetType(fullTypeName);
if (componentType == null)
{
throw new XunitException(
$"Failed to find component type '{fullTypeName}'. Found types:" + Environment.NewLine +
string.Join(Environment.NewLine, assemblyResult.Assembly.ExportedTypes.Select(t => t.FullName)));
}
return (IComponent)Activator.CreateInstance(componentType);
}
protected static string FullTypeName<T>() => typeof(T).FullName.Replace('+', '.');
protected RenderTreeFrame[] GetRenderTree(IComponent component)
{
var renderer = new TestRenderer();
renderer.AttachComponent(component);
component.SetParameters(ParameterCollection.Empty);
return renderer.LatestBatchReferenceFrames;
}
protected ArrayRange<RenderTreeFrame> GetFrames(RenderFragment fragment)
{
var builder = new RenderTreeBuilder(new TestRenderer());
fragment(builder);
return builder.GetFrames();
}
protected class CompileToCSharpResult
{
public RazorCodeDocument CodeDocument { get; set; }
public string Code { get; set; }
public IEnumerable<RazorDiagnostic> Diagnostics { get; set; }
}
protected class CompileToAssemblyResult
{
public Assembly Assembly { get; set; }
public string VerboseLog { get; set; }
public IEnumerable<Diagnostic> Diagnostics { get; set; }
}
private class TestRenderer : Renderer
{
public TestRenderer() : base(new TestServiceProvider())
{
}
public RenderTreeFrame[] LatestBatchReferenceFrames { get; private set; }
public void AttachComponent(IComponent component)
=> AssignComponentId(component);
protected override void UpdateDisplay(RenderBatch renderBatch)
{
LatestBatchReferenceFrames = renderBatch.ReferenceFrames.ToArray();
}
}
}
}

View File

@ -0,0 +1,445 @@
// 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.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Layouts;
using Microsoft.AspNetCore.Blazor.RenderTree;
using Microsoft.AspNetCore.Blazor.Test.Helpers;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
{
// Integration tests for the end-to-end of successful Razor compilation of component definitions
// Includes running the component code to verify the output.
public class RenderingRazorIntegrationTest : RazorIntegrationTestBase
{
[Fact]
public void SupportsPlainText()
{
// Arrange/Act
var component = CompileToComponent("Some plain text");
var frames = GetRenderTree(component);
// Assert
Assert.Collection(frames,
frame => AssertFrame.Text(frame, "Some plain text", 0));
}
[Fact]
public void SupportsCSharpExpressions()
{
// Arrange/Act
var component = CompileToComponent(@"
@(""Hello"")
@((object)null)
@(123)
@(new object())
");
// Assert
var frames = GetRenderTree(component);
Assert.Collection(frames,
frame => AssertFrame.Whitespace(frame, 0),
frame => AssertFrame.Text(frame, "Hello", 1),
frame => AssertFrame.Whitespace(frame, 2),
frame => AssertFrame.Whitespace(frame, 3), // @((object)null)
frame => AssertFrame.Whitespace(frame, 4),
frame => AssertFrame.Text(frame, "123", 5),
frame => AssertFrame.Whitespace(frame, 6),
frame => AssertFrame.Text(frame, new object().ToString(), 7),
frame => AssertFrame.Whitespace(frame, 8));
}
[Fact]
public void SupportsCSharpFunctionsBlock()
{
// Arrange/Act
var component = CompileToComponent(@"
@foreach(var item in items) {
@item
}
@functions {
string[] items = new[] { ""First"", ""Second"", ""Third"" };
}
");
// Assert
var frames = GetRenderTree(component);
Assert.Collection(frames,
frame => AssertFrame.Whitespace(frame, 0),
frame => AssertFrame.Text(frame, "First", 1),
frame => AssertFrame.Text(frame, "Second", 1),
frame => AssertFrame.Text(frame, "Third", 1),
frame => AssertFrame.Whitespace(frame, 2),
frame => AssertFrame.Whitespace(frame, 3));
}
[Fact]
public void SupportsElements()
{
// Arrange/Act
var component = CompileToComponent("<myelem>Hello</myelem>");
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Element(frame, "myelem", 2, 0),
frame => AssertFrame.Text(frame, "Hello", 1));
}
[Fact]
public void SupportsSelfClosingElements()
{
// Arrange/Act
var component = CompileToComponent("Some text so elem isn't at position 0 <myelem />");
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Text(frame, "Some text so elem isn't at position 0 ", 0),
frame => AssertFrame.Element(frame, "myelem", 1, 1));
}
[Fact]
public void SupportsVoidHtmlElements()
{
// Arrange/Act
var component = CompileToComponent("Some text so elem isn't at position 0 <img>");
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Text(frame, "Some text so elem isn't at position 0 ", 0),
frame => AssertFrame.Element(frame, "img", 1, 1));
}
[Fact]
public void SupportsComments()
{
// Arrange/Act
var component = CompileToComponent("Start<!-- My comment -->End");
var frames = GetRenderTree(component);
// Assert
Assert.Collection(frames,
frame => AssertFrame.Text(frame, "Start", 0),
frame => AssertFrame.Text(frame, "End", 1));
}
[Fact]
public void SupportsAttributesWithLiteralValues()
{
// Arrange/Act
var component = CompileToComponent("<elem attrib-one=\"Value 1\" a2='v2' />");
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Element(frame, "elem", 3, 0),
frame => AssertFrame.Attribute(frame, "attrib-one", "Value 1", 1),
frame => AssertFrame.Attribute(frame, "a2", "v2", 2));
}
[Fact]
public void SupportsAttributesWithStringExpressionValues()
{
// Arrange/Act
var component = CompileToComponent(
"@{ var myValue = \"My string\"; }"
+ "<elem attr=@myValue />");
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Element(frame, "elem", 2, 0),
frame => AssertFrame.Attribute(frame, "attr", "My string", 1));
}
[Fact]
public void SupportsAttributesWithNonStringExpressionValues()
{
// Arrange/Act
var component = CompileToComponent(
"@{ var myValue = 123; }"
+ "<elem attr=@myValue />");
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Element(frame, "elem", 2, 0),
frame => AssertFrame.Attribute(frame, "attr", "123", 1));
}
[Fact]
public void SupportsAttributesWithInterpolatedStringExpressionValues()
{
// Arrange/Act
var component = CompileToComponent(
"@{ var myValue = \"world\"; var myNum=123; }"
+ "<elem attr=\"Hello, @myValue.ToUpperInvariant() with number @(myNum*2)!\" />");
// Assert
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Element(frame, "elem", 2, 0),
frame => AssertFrame.Attribute(frame, "attr", "Hello, WORLD with number 246!", 1));
}
[Fact]
public void SupportsAttributesWithEventHandlerValues()
{
// Arrange/Act
var component = CompileToComponent(
@"<elem attr=@MyHandleEvent />
@functions {
public bool HandlerWasCalled { get; set; } = false;
void MyHandleEvent(Microsoft.AspNetCore.Blazor.UIEventArgs eventArgs)
{
HandlerWasCalled = true;
}
}");
var handlerWasCalledProperty = component.GetType().GetProperty("HandlerWasCalled");
// Assert
Assert.False((bool)handlerWasCalledProperty.GetValue(component));
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Element(frame, "elem", 2, 0),
frame =>
{
Assert.Equal(RenderTreeFrameType.Attribute, frame.FrameType);
Assert.Equal(1, frame.Sequence);
Assert.NotNull(frame.AttributeValue);
((UIEventHandler)frame.AttributeValue)(null);
Assert.True((bool)handlerWasCalledProperty.GetValue(component));
},
frame => AssertFrame.Whitespace(frame, 2));
}
[Fact]
public void SupportsAttributesWithCSharpCodeBlockValues()
{
// Arrange/Act
var component = CompileToComponent(
@"<elem attr=@{ DidInvokeCode = true; } />
@functions {
public bool DidInvokeCode { get; set; } = false;
}");
var didInvokeCodeProperty = component.GetType().GetProperty("DidInvokeCode");
var frames = GetRenderTree(component);
// Assert
Assert.False((bool)didInvokeCodeProperty.GetValue(component));
Assert.Collection(frames,
frame => AssertFrame.Element(frame, "elem", 2, 0),
frame =>
{
Assert.Equal(RenderTreeFrameType.Attribute, frame.FrameType);
Assert.NotNull(frame.AttributeValue);
Assert.Equal(1, frame.Sequence);
((UIEventHandler)frame.AttributeValue)(null);
Assert.True((bool)didInvokeCodeProperty.GetValue(component));
},
frame => AssertFrame.Whitespace(frame, 2));
}
[Fact]
public void SupportsUsingStatements()
{
// Arrange/Act
var component = CompileToComponent(
@"@using System.Collections.Generic
@(typeof(List<string>).FullName)");
var frames = GetRenderTree(component);
// Assert
Assert.Collection(frames,
frame => AssertFrame.Whitespace(frame, 0),
frame => AssertFrame.Text(frame, typeof(List<string>).FullName, 1));
}
[Fact]
public void SupportsAttributeFramesEvaluatedInline()
{
// Arrange/Act
var component = CompileToComponent(
@"<elem @onclick(MyHandler) />
@functions {
public bool DidInvokeCode { get; set; } = false;
void MyHandler()
{
DidInvokeCode = true;
}
}");
var didInvokeCodeProperty = component.GetType().GetProperty("DidInvokeCode");
// Assert
Assert.False((bool)didInvokeCodeProperty.GetValue(component));
Assert.Collection(GetRenderTree(component),
frame => AssertFrame.Element(frame, "elem", 2, 0),
frame =>
{
Assert.Equal(RenderTreeFrameType.Attribute, frame.FrameType);
Assert.NotNull(frame.AttributeValue);
Assert.Equal(1, frame.Sequence);
((UIEventHandler)frame.AttributeValue)(null);
Assert.True((bool)didInvokeCodeProperty.GetValue(component));
},
frame => AssertFrame.Whitespace(frame, 2));
}
[Fact]
public void SupportsTwoWayBindingForTextboxes()
{
// Arrange/Act
var component = CompileToComponent(
@"<input @bind(MyValue) />
@functions {
public string MyValue { get; set; } = ""Initial value"";
}");
var myValueProperty = component.GetType().GetProperty("MyValue");
// Assert
var frames = GetRenderTree(component);
Assert.Collection(frames,
frame => AssertFrame.Element(frame, "input", 3, 0),
frame => AssertFrame.Attribute(frame, "value", "Initial value", 1),
frame =>
{
AssertFrame.Attribute(frame, "onchange", 2);
// Trigger the change event to show it updates the property
((UIEventHandler)frame.AttributeValue)(new UIChangeEventArgs
{
Value = "Modified value"
});
Assert.Equal("Modified value", myValueProperty.GetValue(component));
},
frame => AssertFrame.Text(frame, "\n", 3));
}
[Fact]
public void SupportsTwoWayBindingForDateValues()
{
// Arrange/Act
var component = CompileToComponent(
@"<input @bind(MyDate) />
@functions {
public DateTime MyDate { get; set; } = new DateTime(2018, 3, 4, 1, 2, 3);
}");
var myDateProperty = component.GetType().GetProperty("MyDate");
// Assert
var frames = GetRenderTree(component);
Assert.Collection(frames,
frame => AssertFrame.Element(frame, "input", 3, 0),
frame => AssertFrame.Attribute(frame, "value", new DateTime(2018, 3, 4, 1, 2, 3).ToString(), 1),
frame =>
{
AssertFrame.Attribute(frame, "onchange", 2);
// Trigger the change event to show it updates the property
var newDateValue = new DateTime(2018, 3, 5, 4, 5, 6);
((UIEventHandler)frame.AttributeValue)(new UIChangeEventArgs
{
Value = newDateValue.ToString()
});
Assert.Equal(newDateValue, myDateProperty.GetValue(component));
},
frame => AssertFrame.Text(frame, "\n", 3));
}
[Fact]
public void SupportsTwoWayBindingForDateValuesWithFormatString()
{
// Arrange/Act
var component = CompileToComponent(
@"<input @bind(MyDate, ""ddd yyyy-MM-dd"") />
@functions {
public DateTime MyDate { get; set; } = new DateTime(2018, 3, 4);
}");
var myDateProperty = component.GetType().GetProperty("MyDate");
// Assert
var frames = GetRenderTree(component);
Assert.Collection(frames,
frame => AssertFrame.Element(frame, "input", 3, 0),
frame => AssertFrame.Attribute(frame, "value", "Sun 2018-03-04", 1),
frame =>
{
AssertFrame.Attribute(frame, "onchange", 2);
// Trigger the change event to show it updates the property
((UIEventHandler)frame.AttributeValue)(new UIChangeEventArgs
{
Value = "Mon 2018-03-05"
});
Assert.Equal(new DateTime(2018, 3, 5), myDateProperty.GetValue(component));
},
frame => AssertFrame.Text(frame, "\n", 3));
}
[Fact]
public void SupportsTwoWayBindingForBoolValues()
{
// Arrange/Act
var component = CompileToComponent(
@"<input @bind(MyValue) />
@functions {
public bool MyValue { get; set; } = true;
}");
var myValueProperty = component.GetType().GetProperty("MyValue");
// Assert
var frames = GetRenderTree(component);
Assert.Collection(frames,
frame => AssertFrame.Element(frame, "input", 3, 0),
frame => AssertFrame.Attribute(frame, "value", "True", 1),
frame =>
{
AssertFrame.Attribute(frame, "onchange", 2);
// Trigger the change event to show it updates the property
((UIEventHandler)frame.AttributeValue)(new UIChangeEventArgs
{
Value = false
});
Assert.False((bool)myValueProperty.GetValue(component));
},
frame => AssertFrame.Text(frame, "\n", 3));
}
[Fact]
public void SupportsTwoWayBindingForEnumValues()
{
// Arrange/Act
var myEnumType = FullTypeName<MyEnum>();
var component = CompileToComponent(
$@"<input @bind(MyValue) />
@functions {{
public {myEnumType} MyValue {{ get; set; }} = {myEnumType}.{nameof(MyEnum.FirstValue)};
}}");
var myValueProperty = component.GetType().GetProperty("MyValue");
// Assert
var frames = GetRenderTree(component);
Assert.Collection(frames,
frame => AssertFrame.Element(frame, "input", 3, 0),
frame => AssertFrame.Attribute(frame, "value", MyEnum.FirstValue.ToString(), 1),
frame =>
{
AssertFrame.Attribute(frame, "onchange", 2);
// Trigger the change event to show it updates the property
((UIEventHandler)frame.AttributeValue)(new UIChangeEventArgs
{
Value = MyEnum.SecondValue.ToString()
});
Assert.Equal(MyEnum.SecondValue, (MyEnum)myValueProperty.GetValue(component));
},
frame => AssertFrame.Text(frame, "\n", 3));
}
public enum MyEnum { FirstValue, SecondValue }
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using Microsoft.AspNetCore.Blazor.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Razor;
@ -17,6 +18,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Blazor
{
configure?.Invoke(b);
new BlazorExtensionInitializer().Initialize(b);
var classifier = b.Features.OfType<ComponentDocumentClassifierPass>().Single();
classifier.MangleClassNames = true;
});
}
}