Skip to content
This repository has been archived by the owner on Dec 19, 2018. It is now read-only.

Added view component tag helper updates. #823

Merged
merged 1 commit into from
Aug 24, 2016
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
Expand Up @@ -703,15 +703,17 @@ private static bool IsAccessibleProperty(PropertyInfo property)
/// Converts from pascal/camel case to lower kebab-case.
/// </summary>
/// <example>
/// <code>
/// SomeThing => some-thing
/// capsONInside => caps-on-inside
/// CAPSOnOUTSIDE => caps-on-outside
/// ALLCAPS => allcaps
/// One1Two2Three3 => one1-two2-three3
/// ONE1TWO2THREE3 => one1two2three3
/// First_Second_ThirdHi => first_second_third-hi
/// </code>
/// </example>
Copy link
Member

Choose a reason for hiding this comment

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

Need to fix the <example> since this is now public. Right now all the lines will be shown as one messy line. Easiest to surround the lines w/ <code>...</code>.

private static string ToHtmlCase(string name)
public static string ToHtmlCase(string name)
{
return HtmlCaseRegex.Replace(name, HtmlCaseRegexReplacement).ToLowerInvariant();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public CSharpCodeGenerator(CodeGeneratorContext context)
{
}

private ChunkTree Tree { get { return Context.ChunkTreeBuilder.Root; } }
protected ChunkTree Tree { get { return Context.ChunkTreeBuilder.Root; } }
public RazorEngineHost Host { get { return Context.Host; } }

/// <summary>
Expand Down Expand Up @@ -82,12 +82,23 @@ public override CodeGeneratorResult Generate()
csharpCodeVisitor.Accept(Tree.Children);
}
}

BuildAfterExecuteContent(writer, Tree.Children);
}
}

return new CodeGeneratorResult(writer.GenerateCode(), writer.LineMappingManager.Mappings);
}

/// <summary>
/// Provides an entry point to append code (after execute content) to a generated Razor class.
/// </summary>
/// <param name="writer">The <see cref="CSharpCodeWriter"/> to receive the additional content.</param>
/// <param name="chunks">The list of <see cref="Chunk"/>s for the generated program.</param>
protected virtual void BuildAfterExecuteContent(CSharpCodeWriter writer, IList<Chunk> chunks)
Copy link
Member

Choose a reason for hiding this comment

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

/// docs here and for DecorateChunks() since the next dev (e.g. in MVC repo) won't know what's up. I know this class has few doc comments but it's something we already use in other repos.

Copy link
Member

Choose a reason for hiding this comment

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

Forgot: Does IReadOnlyList<Chunk> not work for your feature?

Copy link
Member

Choose a reason for hiding this comment

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

Decided offline to leave this as is b/c Tree.Children is an IList<Chunk> and creating another List<Chunk> isn't worth it.

{
Copy link
Member

Choose a reason for hiding this comment

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

What happens if you mutate the chunks list here (add/remove)?

Copy link
Contributor Author

@cjqian cjqian Aug 23, 2016

Choose a reason for hiding this comment

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

Do you mean that we would DecorateChunks at BuildAfterExecuteContent?

For view component tag helpers, we need to mutate their chunks such that the type name includes the name of the page (ex. AspNetCore._Views_Index_Cshtml) since we'll be appending the after execute content within that class.

This needs to be done before the logic in lines 68-71, since that's when the type names of the chunks are written in the generated code. We could actually call DecorateChunks directly before that instead of at line 51?

Copy link
Member

Choose a reason for hiding this comment

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

Both here and in DecorateChunks, what happens if I add a new chunk to the list of chunks? I know the view component tag helpers story required mutating the existing chunks, but what happens if new ones are added or removed. Should this list be readonly or are there valid things you could do.

Similarly, what happens if chunks are removed. Will code that runs later blow up?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, understood. I don't think anything would blow up... It would be similar to adding/deleting parts of the .cshtml file, maybe. Worst case, you could change the content of the chunks and rewrite the view? If the chunks were invalid, perhaps things would just not render? (I'm not sure on this, what do you think?) The chunk list has to be modifiable in our current implementation so we can modify the type names. Is this valid?

Copy link
Member

Choose a reason for hiding this comment

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

Well, DecorateChunks() is about mucking w/ the chunks collection and this method is about writing stuff out. Suggest this method use IReadOnlyList<Chunk> chunks to make things more clear.

}

protected virtual CSharpCodeVisitor CreateCSharpCodeVisitor(
CSharpCodeWriter writer,
CodeGeneratorContext context)
Expand Down
11 changes: 11 additions & 0 deletions src/Microsoft.AspNetCore.Razor/CodeGenerators/CSharpCodeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,17 @@ public CSharpCodeWriter WriteMethodInvocation(string methodName, bool endLine, p
.WriteEndMethodInvocation(endLine);
}

public CSharpCodeWriter WriteAutoPropertyDeclaration(string accessibility, string typeName, string name)
Copy link
Member

@dougbu dougbu Aug 24, 2016

Choose a reason for hiding this comment

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

I assume the view component as tag helper feature needs easy auto-property generation. Correct?

But why does this method need to exist in Razor? MVC or any external code could implement this as an extension method:

public static CSharpCodeWriter WriteAutoPropertyDeclaration(
    this CSharpCodeWriter writer,
    string accessibility,
    string typeName,
    string name)
{
    return writer
        .Write(accessibility)
        // ...
        .WriteLine();
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct. Do you recommend taking this out? We could maybe start a MvcCSharpCodeWriter class, or not have it at all since we only really use it once or twice. Just thought it might be useful for others in the future.

Copy link
Member

Choose a reason for hiding this comment

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

It's generic enough to leave in the base. Are there other similar methods?

Copy link
Member

Choose a reason for hiding this comment

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

Discussed offline. This is fine as-is.

{
return Write(accessibility)
.Write(" ")
.Write(typeName)
.Write(" ")
.Write(name)
.Write(" { get; set; }")
.WriteLine();
}

public CSharpDisableWarningScope BuildDisableWarningScope(int warning)
{
return new CSharpDisableWarningScope(this, warning);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2172,6 +2172,35 @@ public void CreateDescriptors_WithPrefixes_ReturnsExpectedAttributeDescriptors(
TagHelperAttributeDescriptorComparer.Default);
}

public static TheoryData HtmlConversionData
{
get
{
return new TheoryData<string, string>
{
{ "SomeThing", "some-thing" },
{ "someOtherThing", "some-other-thing" },
{ "capsONInside", "caps-on-inside" },
{ "CAPSOnOUTSIDE", "caps-on-outside" },
{ "ALLCAPS", "allcaps" },
{ "One1Two2Three3", "one1-two2-three3" },
{ "ONE1TWO2THREE3", "one1two2three3" },
{ "First_Second_ThirdHi", "first_second_third-hi" }
};
}
}

[Theory]
[MemberData(nameof(HtmlConversionData))]
public void ToHtmlCase_ReturnsExpectedConversions(string input, string expectedOutput)
{
// Arrange, Act
var output = TagHelperDescriptorFactory.ToHtmlCase(input);

// Assert
Assert.Equal(output, expectedOutput);
}

// TagHelperDesignTimeDescriptors are not created in CoreCLR.
#if !NETCOREAPP1_0
public static TheoryData OutputElementHintData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
#if GENERATE_BASELINES
using System;
#endif
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Chunks;
using Microsoft.AspNetCore.Razor.Chunks.Generators;
using Microsoft.AspNetCore.Razor.Parser.SyntaxTree;
using Microsoft.AspNetCore.Razor.Test.CodeGenerators;
using Microsoft.AspNetCore.Razor.Test.Utils;
Expand All @@ -14,9 +18,15 @@ namespace Microsoft.AspNetCore.Razor.CodeGenerators
{
public class CSharpCodeGeneratorTest
{
#if GENERATE_BASELINES
private static readonly string _baselinePathStart = "test/Microsoft.AspNetCore.Razor.Test";
Copy link
Contributor

Choose a reason for hiding this comment

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

If the field is private you can use const instead of static readonly.

#endif
private static readonly string _testOutputDirectory = "TestFiles/CodeGenerator/Output/CSharpCodeGenerator";
Copy link
Member

Choose a reason for hiding this comment

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

Path.Combine() the folders.

Copy link
Member

Choose a reason for hiding this comment

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

Discussed offline. This is actually used to find resources and '\\' isn't handled in that code. Fine as-is.

Copy link
Member

Choose a reason for hiding this comment

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

But do we want the CSharpCodeGenerator folder here i.e. put the files / resources in the Output folder?

Copy link
Member

Choose a reason for hiding this comment

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

On second thought, leave the final folder there. Just make sure the files are written correctly w/ GENERATE_BASELINES.


[Fact]
public void ChunkTreeWithUsings()
{
// Arrange
var syntaxTreeNode = new Mock<Span>(new SpanBuilder());
var language = new CSharpRazorCodeLanguage();
var host = new CodeGenTestHost(language);
Expand All @@ -30,7 +40,9 @@ public void ChunkTreeWithUsings()
codeGeneratorContext.ChunkTreeBuilder.AddUsingChunk("FakeNamespace1", syntaxTreeNode.Object);
codeGeneratorContext.ChunkTreeBuilder.AddUsingChunk("FakeNamespace2.SubNamespace", syntaxTreeNode.Object);
var codeGenerator = new CodeGenTestCodeGenerator(codeGeneratorContext);
var testFile = TestFile.Create("TestFiles/CodeGenerator/Output/CSharpCodeGenerator.cs");

var path = $"{_testOutputDirectory}/ChunkTreeWithUsings.cs";
var testFile = TestFile.Create(path);

string expectedOutput;
#if GENERATE_BASELINES
Expand All @@ -54,14 +66,151 @@ public void ChunkTreeWithUsings()
// Update baseline files if files do not already match.
if (!string.Equals(expectedOutput, result.Code, StringComparison.Ordinal))
{
BaselineWriter.WriteBaseline(
@"test\Microsoft.AspNetCore.Razor.Test\TestFiles\CodeGenerator\Output\CSharpCodeGenerator.cs",
result.Code);
var baselinePath = $"{_baselinePathStart}/{_testOutputDirectory}/ChunkTreeWithUsings.cs";
BaselineWriter.WriteBaseline( baselinePath, result.Code);
}
#else
Assert.Equal(expectedOutput, result.Code);
#endif
}
}
}

public static TheoryData ModifyOutputData
{
get
{
var addFileName = "AddGenerateChunkTest.cs";
var addGenerator = new AddGenerateTestCodeGenerator(CreateContext());
var addResult = CreateCodeGeneratorResult(addFileName);

var clearFileName = "ClearGenerateChunkTest.cs";
var clearGenerator = new ClearGenerateTestCodeGenerator(CreateContext());
var clearResult = CreateCodeGeneratorResult(clearFileName);

var defaultFileName = "DefaultGenerateChunkTest.cs";
var defaultGenerator = new CSharpCodeGenerator(CreateContext());
var defaultResult = CreateCodeGeneratorResult(defaultFileName);

var commentFileName = "BuildAfterExecuteContentTest.cs";
var commentGenerator = new BuildAfterExecuteContentTestCodeGenerator(CreateContext());
var commentResult = CreateCodeGeneratorResult(commentFileName);

return new TheoryData<CSharpCodeGenerator, CodeGeneratorResult, string>
{
{addGenerator, addResult, addFileName},
{clearGenerator, clearResult, clearFileName },
{defaultGenerator, defaultResult, defaultFileName },
{commentGenerator, commentResult, commentFileName }
};
}
}

[Theory]
[MemberData(nameof(ModifyOutputData))]
public void BuildAfterExecuteContent_ModifyChunks_ModifyOutput(
CSharpCodeGenerator generator,
CodeGeneratorResult expectedResult,
string fileName)
{
// Arrange, Act
var result = generator.Generate();

// Assert
#if GENERATE_BASELINES
// Update baseline files if files do not already match.
if (!string.Equals(expectedResult.Code, result.Code, StringComparison.Ordinal))
{
var baselinePath = $"{_baselinePathStart}/{_testOutputDirectory}/{fileName}";
BaselineWriter.WriteBaseline( baselinePath, result.Code);
}
#else
Assert.Equal(result.Code, expectedResult.Code);
Copy link
Member

Choose a reason for hiding this comment

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

Do the GENERATE_BASELINES stuff here too.

#endif
}


private static CodeGeneratorResult CreateCodeGeneratorResult(string fileName)
{
var path = $"{_testOutputDirectory}/{fileName}";
var file = TestFile.Create(path);
string code;

#if GENERATE_BASELINES
if (file.Exists())
{
code = file.ReadAllText();
}
else
{
code = null;
}
#else
code = file.ReadAllText();
#endif

var result = new CodeGeneratorResult(code, new List<LineMapping>());
return result;
}

// Returns a context with two literal chunks.
private static CodeGeneratorContext CreateContext()
{
var language = new CSharpRazorCodeLanguage();
var host = new CodeGenTestHost(language);

var codeGeneratorContext = new CodeGeneratorContext(
new ChunkGeneratorContext(
host,
host.DefaultClassName,
host.DefaultNamespace,
"",
shouldGenerateLinePragmas: false),
new ErrorSink());

codeGeneratorContext.ChunkTreeBuilder = new ChunkTreeBuilder();
var syntaxTreeNode = new Mock<Span>(new SpanBuilder());
codeGeneratorContext.ChunkTreeBuilder.AddLiteralChunk("hello", syntaxTreeNode.Object);
codeGeneratorContext.ChunkTreeBuilder.AddStatementChunk("// asdf", syntaxTreeNode.Object);
codeGeneratorContext.ChunkTreeBuilder.AddLiteralChunk("world", syntaxTreeNode.Object);
return codeGeneratorContext;
}

private class BuildAfterExecuteContentTestCodeGenerator : CSharpCodeGenerator
{
public BuildAfterExecuteContentTestCodeGenerator(CodeGeneratorContext context) : base(context)
{
}

protected override void BuildAfterExecuteContent(CSharpCodeWriter writer, IList<Chunk> chunks)
{
writer.WriteLine("// test add content.");
}
}

private class AddGenerateTestCodeGenerator : CSharpCodeGenerator
{
public AddGenerateTestCodeGenerator(CodeGeneratorContext context) : base(context)
{
}

public override CodeGeneratorResult Generate()
{
var firstChunk = Tree.Children.First();
Tree.Children.Add(firstChunk);
return base.Generate();
}
}

private class ClearGenerateTestCodeGenerator : CSharpCodeGenerator
{
public ClearGenerateTestCodeGenerator(CodeGeneratorContext context) : base(context)
{
}

public override CodeGeneratorResult Generate()
{
Tree.Children.Clear();
return base.Generate();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Razor
{
using System.Threading.Tasks;

public class __CompiledTemplate
{
#line hidden
public __CompiledTemplate()
{
}

#pragma warning disable 1998
public override async Task ExecuteAsync()
{
WriteLiteral("hello");
#line 1 ""
// asdf

#line default
#line hidden

WriteLiteral("world");
WriteLiteral("hello");
}
#pragma warning restore 1998
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Razor
{
using System.Threading.Tasks;

public class __CompiledTemplate
{
#line hidden
public __CompiledTemplate()
{
}

#pragma warning disable 1998
public override async Task ExecuteAsync()
{
WriteLiteral("hello");
#line 1 ""
// asdf

#line default
#line hidden

WriteLiteral("world");
}
#pragma warning restore 1998
// test add content.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Razor
{
using System.Threading.Tasks;

public class __CompiledTemplate
{
#line hidden
public __CompiledTemplate()
{
}

#pragma warning disable 1998
public override async Task ExecuteAsync()
{
}
#pragma warning restore 1998
}
}
Loading