Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Consolidated view classifier to make view types internal #30976

Merged
3 commits merged into from
Mar 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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.Mvc.Razor.Extensions
{
public sealed class ConsolidatedMvcViewDocumentClassifierPass : DocumentClassifierPassBase
{
public static readonly string MvcViewDocumentKind = "mvc.1.0.view";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know MVC is still a 1.0 :)


protected override string DocumentKind => MvcViewDocumentKind;

protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) => true;

protected override void OnDocumentStructureCreated(
RazorCodeDocument codeDocument,
NamespaceDeclarationIntermediateNode @namespace,
ClassDeclarationIntermediateNode @class,
MethodDeclarationIntermediateNode method)
{
base.OnDocumentStructureCreated(codeDocument, @namespace, @class, method);

if (!codeDocument.TryComputeNamespace(fallbackToRootNamespace: false, out var namespaceName))
{
@namespace.Content = "AspNetCoreGeneratedDocument";
}
else
{
@namespace.Content = namespaceName;
}

if (!TryComputeClassName(codeDocument, out var className))
{
// It's possible for a Razor document to not have a file path.
// Eg. When we try to generate code for an in memory document like default imports.
var checksum = Checksum.BytesToString(codeDocument.Source.GetChecksum());
@class.ClassName = $"AspNetCore_{checksum}";
}
else
{
@class.ClassName = className;
}

@class.BaseType = "global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<TModel>";
@class.Modifiers.Clear();
@class.Modifiers.Add("internal");
@class.Modifiers.Add("sealed");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍 Finally! 😁

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be interested to see if this shows up in benchmarks

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm super glad about this tooooo

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure MVC views/pages are currently running in aspnet/benchmarks 😢


method.MethodName = "ExecuteAsync";
method.Modifiers.Clear();
method.Modifiers.Add("public");
method.Modifiers.Add("async");
method.Modifiers.Add("override");
method.ReturnType = $"global::{typeof(System.Threading.Tasks.Task).FullName}";
}

private bool TryComputeClassName(RazorCodeDocument codeDocument, out string className)
{
var filePath = codeDocument.Source.RelativePath ?? codeDocument.Source.FilePath;
if (string.IsNullOrEmpty(filePath))
{
className = null;
return false;
}

className = GetClassNameFromPath(filePath);
return true;
}

private static string GetClassNameFromPath(string path)
{
const string cshtmlExtension = ".cshtml";

if (string.IsNullOrEmpty(path))
{
return path;
}

if (path.EndsWith(cshtmlExtension, StringComparison.OrdinalIgnoreCase))
{
path = path.Substring(0, path.Length - cshtmlExtension.Length);
}

return CSharpIdentifier.SanitizeIdentifier(path);
}
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
#nullable enable
Microsoft.AspNetCore.Mvc.Razor.Extensions.ConsolidatedMvcViewDocumentClassifierPass
Microsoft.AspNetCore.Mvc.Razor.Extensions.ConsolidatedMvcViewDocumentClassifierPass.ConsolidatedMvcViewDocumentClassifierPass() -> void
~static readonly Microsoft.AspNetCore.Mvc.Razor.Extensions.ConsolidatedMvcViewDocumentClassifierPass.MvcViewDocumentKind -> string
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,15 @@ public static void Register(RazorProjectEngineBuilder builder)
builder.Features.Add(new PagesPropertyInjectionPass());
builder.Features.Add(new ViewComponentTagHelperPass());
builder.Features.Add(new RazorPageDocumentClassifierPass());
builder.Features.Add(new MvcViewDocumentClassifierPass());

if (builder.Configuration.UseConsolidatedMvcViews)
{
builder.Features.Add(new ConsolidatedMvcViewDocumentClassifierPass());
}
else
{
builder.Features.Add(new MvcViewDocumentClassifierPass());
}

builder.Features.Add(new MvcImportProjectFeature());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// 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.Intermediate;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
{
public class ConsolidatedMvcViewDocumentClassifierPassTest : RazorProjectEngineTestBase
{
protected override RazorLanguageVersion Version => RazorLanguageVersion.Latest;

[Fact]
public void ConsolidatedMvcViewDocumentClassifierPass_SetsDifferentNamespace()
{
// Arrange
var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", "Test.cshtml"));

var projectEngine = CreateProjectEngine();
var irDocument = CreateIRDocument(projectEngine, codeDocument);
var pass = new ConsolidatedMvcViewDocumentClassifierPass
{
Engine = projectEngine.Engine
};

// Act
pass.Execute(codeDocument, irDocument);
var visitor = new Visitor();
visitor.Visit(irDocument);

// Assert
Assert.Equal("AspNetCoreGeneratedDocument", visitor.Namespace.Content);
}

[Fact]
public void ConsolidatedMvcViewDocumentClassifierPass_SetsClass()
{
// Arrange
var properties = new RazorSourceDocumentProperties(filePath: "ignored", relativePath: "Test.cshtml");
var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", properties));

var projectEngine = CreateProjectEngine();
var irDocument = CreateIRDocument(projectEngine, codeDocument);
var pass = new ConsolidatedMvcViewDocumentClassifierPass
{
Engine = projectEngine.Engine
};

// Act
pass.Execute(codeDocument, irDocument);
var visitor = new Visitor();
visitor.Visit(irDocument);

// Assert
Assert.Equal("global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<TModel>", visitor.Class.BaseType);
Assert.Equal(new[] { "internal", "sealed" }, visitor.Class.Modifiers);
Assert.Equal("Test", visitor.Class.ClassName);
}

[Fact]
public void MvcViewDocumentClassifierPass_NullFilePath_SetsClass()
{
// Arrange
var properties = new RazorSourceDocumentProperties(filePath: null, relativePath: null);
var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", properties));

var projectEngine = CreateProjectEngine();
var irDocument = CreateIRDocument(projectEngine, codeDocument);
var pass = new ConsolidatedMvcViewDocumentClassifierPass
{
Engine = projectEngine.Engine
};

// Act
pass.Execute(codeDocument, irDocument);
var visitor = new Visitor();
visitor.Visit(irDocument);

// Assert
Assert.Equal("global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<TModel>", visitor.Class.BaseType);
Assert.Equal(new[] { "internal", "sealed" }, visitor.Class.Modifiers);
Assert.Equal("AspNetCore_d9f877a857a7e9928eac04d09a59f25967624155", visitor.Class.ClassName);
}

[Theory]
[InlineData("/Views/Home/Index.cshtml", "_Views_Home_Index")]
[InlineData("/Areas/MyArea/Views/Home/About.cshtml", "_Areas_MyArea_Views_Home_About")]
public void MvcViewDocumentClassifierPass_UsesRelativePathToGenerateTypeName(string relativePath, string expected)
{
// Arrange
var properties = new RazorSourceDocumentProperties(filePath: "ignored", relativePath: relativePath);
var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", properties));

var projectEngine = CreateProjectEngine();
var irDocument = CreateIRDocument(projectEngine, codeDocument);
var pass = new ConsolidatedMvcViewDocumentClassifierPass
{
Engine = projectEngine.Engine
};

// Act
pass.Execute(codeDocument, irDocument);
var visitor = new Visitor();
visitor.Visit(irDocument);

// Assert
Assert.Equal(expected, visitor.Class.ClassName);
Assert.Equal(new[] { "internal", "sealed" }, visitor.Class.Modifiers);
}

[Fact]
public void ConsolidatedMvcViewDocumentClassifierPass_SetsUpExecuteAsyncMethod()
{
// Arrange
var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", "Test.cshtml"));

var projectEngine = CreateProjectEngine();
var irDocument = CreateIRDocument(projectEngine, codeDocument);
var pass = new ConsolidatedMvcViewDocumentClassifierPass
{
Engine = projectEngine.Engine
};

// Act
pass.Execute(codeDocument, irDocument);
var visitor = new Visitor();
visitor.Visit(irDocument);

// Assert
Assert.Equal("ExecuteAsync", visitor.Method.MethodName);
Assert.Equal("global::System.Threading.Tasks.Task", visitor.Method.ReturnType);
Assert.Equal(new[] { "public", "async", "override" }, visitor.Method.Modifiers);
}

private static DocumentIntermediateNode CreateIRDocument(RazorProjectEngine projectEngine, RazorCodeDocument codeDocument)
{
for (var i = 0; i < projectEngine.Phases.Count; i++)
{
var phase = projectEngine.Phases[i];
phase.Execute(codeDocument);

if (phase is IRazorIntermediateNodeLoweringPhase)
{
break;
}
}

return codeDocument.GetDocumentIntermediateNode();
}

private class Visitor : IntermediateNodeWalker
{
public NamespaceDeclarationIntermediateNode Namespace { get; private set; }

public ClassDeclarationIntermediateNode Class { get; private set; }

public MethodDeclarationIntermediateNode Method { get; private set; }

public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode node)
{
Method = node;
}

public override void VisitNamespaceDeclaration(NamespaceDeclarationIntermediateNode node)
{
Namespace = node;
base.VisitNamespaceDeclaration(node);
}

public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node)
{
Class = node;
base.VisitClassDeclaration(node);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ Microsoft.AspNetCore.Razor.Language.Intermediate.CascadingGenericTypeParameter.C
~Microsoft.AspNetCore.Razor.Language.Intermediate.ComponentIntermediateNode.ProvidesCascadingGenericTypes.set -> void
~Microsoft.AspNetCore.Razor.Language.Intermediate.ComponentTypeInferenceMethodIntermediateNode.ReceivesCascadingGenericTypes.get -> System.Collections.Generic.List<Microsoft.AspNetCore.Razor.Language.Intermediate.CascadingGenericTypeParameter>
~Microsoft.AspNetCore.Razor.Language.Intermediate.ComponentTypeInferenceMethodIntermediateNode.ReceivesCascadingGenericTypes.set -> void
abstract Microsoft.AspNetCore.Razor.Language.RazorConfiguration.UseConsolidatedMvcViews.get -> bool
*REMOVED*~static Microsoft.AspNetCore.Razor.Language.RazorConfiguration.Create(Microsoft.AspNetCore.Razor.Language.RazorLanguageVersion languageVersion, string configurationName, System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Razor.Language.RazorExtension> extensions) -> Microsoft.AspNetCore.Razor.Language.RazorConfiguration
~static Microsoft.AspNetCore.Razor.Language.RazorConfiguration.Create(Microsoft.AspNetCore.Razor.Language.RazorLanguageVersion languageVersion, string configurationName, System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Razor.Language.RazorExtension> extensions, bool useConsolidatedMvcViews = false) -> Microsoft.AspNetCore.Razor.Language.RazorConfiguration
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ public abstract class RazorConfiguration : IEquatable<RazorConfiguration>
public static readonly RazorConfiguration Default = new DefaultRazorConfiguration(
RazorLanguageVersion.Latest,
"unnamed",
Array.Empty<RazorExtension>());
Array.Empty<RazorExtension>(),
false);

public static RazorConfiguration Create(
RazorLanguageVersion languageVersion,
string configurationName,
IEnumerable<RazorExtension> extensions)
IEnumerable<RazorExtension> extensions,
bool useConsolidatedMvcViews = false)
{
if (languageVersion == null)
{
Expand All @@ -35,7 +37,7 @@ public static RazorConfiguration Create(
throw new ArgumentNullException(nameof(extensions));
}

return new DefaultRazorConfiguration(languageVersion, configurationName, extensions.ToArray());
return new DefaultRazorConfiguration(languageVersion, configurationName, extensions.ToArray(), useConsolidatedMvcViews);
}

public abstract string ConfigurationName { get; }
Expand All @@ -44,6 +46,8 @@ public static RazorConfiguration Create(

public abstract RazorLanguageVersion LanguageVersion { get; }

public abstract bool UseConsolidatedMvcViews { get; }

public override bool Equals(object obj)
{
return base.Equals(obj as RazorConfiguration);
Expand Down Expand Up @@ -71,6 +75,11 @@ public virtual bool Equals(RazorConfiguration other)
return false;
}

if (UseConsolidatedMvcViews != other.UseConsolidatedMvcViews)
{
return false;
}

for (var i = 0; i < Extensions.Count; i++)
{
if (Extensions[i].ExtensionName != other.Extensions[i].ExtensionName)
Expand Down Expand Up @@ -101,18 +110,22 @@ private class DefaultRazorConfiguration : RazorConfiguration
public DefaultRazorConfiguration(
RazorLanguageVersion languageVersion,
string configurationName,
RazorExtension[] extensions)
RazorExtension[] extensions,
bool useConsolidatedMvcViews = false)
{
LanguageVersion = languageVersion;
ConfigurationName = configurationName;
Extensions = extensions;
UseConsolidatedMvcViews = useConsolidatedMvcViews;
}

public override string ConfigurationName { get; }

public override IReadOnlyList<RazorExtension> Extensions { get; }

public override RazorLanguageVersion LanguageVersion { get; }

public override bool UseConsolidatedMvcViews { get; }
}
}
}