From 59e419ba0a592a8e9936582f0011689fda30180c Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 8 May 2014 15:21:45 -0700 Subject: [PATCH] Adding support for @Inject to Mvc --- Mvc.sln | 13 + samples/MvcSample.Web/HomeController.cs | 5 + samples/MvcSample.Web/MvcSample.Web.kproj | 2 + samples/MvcSample.Web/Services/TestService.cs | 21 + samples/MvcSample.Web/Startup.cs | 2 + .../Views/Home/InjectSample.cshtml | 7 + .../InjectChunk.cs | 26 ++ .../InjectChunkVisitor.cs | 50 +++ .../InjectParameterGenerator.cs | 48 +++ .../Microsoft.AspNet.Mvc.Razor.Host.kproj | 5 + .../MvcCSharpCodeBuilder.cs | 42 ++ .../MvcCSharpCodeVistor.cs | 32 ++ .../MvcRazorCodeParser.cs | 88 +++- .../MvcRazorHost.cs | 8 +- .../Properties/Resources.Designer.cs | 26 +- .../Resources.resx | 5 +- .../ViewEngine/VirtualPathViewFactory.cs | 13 +- .../InjectChunkVisitorTest.cs | 181 ++++++++ ...Microsoft.AspNet.Mvc.Razor.Host.Test.kproj | 41 ++ .../MvcCSharpRazorCodeParserTest.cs | 406 ++++++++++++++++++ .../SpanFactory/RawTextSymbol.cs | 72 ++++ .../SpanFactory/SpanConstructor.cs | 79 ++++ .../SpanFactory/SpanFactory.cs | 107 +++++ .../SpanFactory/SpanFactoryExtensions.cs | 124 ++++++ .../UnclassifiedSpanConstructor.cs | 79 ++++ .../StringTextBuffer.cs | 49 +++ .../TestFiles/Input/Inject.cshtml | 2 + .../TestFiles/Output/Inject.cs | 40 ++ .../project.json | 22 + 29 files changed, 1576 insertions(+), 19 deletions(-) create mode 100644 samples/MvcSample.Web/Services/TestService.cs create mode 100644 samples/MvcSample.Web/Views/Home/InjectSample.cshtml create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunk.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunkVisitor.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/InjectParameterGenerator.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeBuilder.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeVistor.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/Microsoft.AspNet.Mvc.Razor.Host.Test.kproj create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcCSharpRazorCodeParserTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/RawTextSymbol.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/SpanConstructor.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/SpanFactory.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/SpanFactoryExtensions.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/UnclassifiedSpanConstructor.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/StringTextBuffer.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Input/Inject.cshtml create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Inject.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/project.json diff --git a/Mvc.sln b/Mvc.sln index 34dc993019..5729755b0d 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -29,6 +29,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "MvcSample.Web", "samples\Mv EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Razor.Host", "src\Microsoft.AspNet.Mvc.Razor.Host\Microsoft.AspNet.Mvc.Razor.Host.kproj", "{520B3AA4-363A-497C-8C15-80423C5AFC85}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Razor.Host.Test", "test\Microsoft.AspNet.Mvc.Razor.Host.Test\Microsoft.AspNet.Mvc.Razor.Host.Test.kproj", "{7C4F5973-0491-4028-B1DC-A9BA73FF9F77}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebSites", "WebSites", "{16703B76-C9F7-4C75-AE6C-53D92E308E3C}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.FunctionalTests", "test\Microsoft.AspNet.Mvc.FunctionalTests\Microsoft.AspNet.Mvc.FunctionalTests.kproj", "{323D0C04-B518-4A8F-8A8E-3546AD153D34}" @@ -145,6 +147,16 @@ Global {520B3AA4-363A-497C-8C15-80423C5AFC85}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {520B3AA4-363A-497C-8C15-80423C5AFC85}.Release|Mixed Platforms.Build.0 = Release|Any CPU {520B3AA4-363A-497C-8C15-80423C5AFC85}.Release|x86.ActiveCfg = Release|Any CPU + {7C4F5973-0491-4028-B1DC-A9BA73FF9F77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C4F5973-0491-4028-B1DC-A9BA73FF9F77}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C4F5973-0491-4028-B1DC-A9BA73FF9F77}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {7C4F5973-0491-4028-B1DC-A9BA73FF9F77}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {7C4F5973-0491-4028-B1DC-A9BA73FF9F77}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C4F5973-0491-4028-B1DC-A9BA73FF9F77}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C4F5973-0491-4028-B1DC-A9BA73FF9F77}.Release|Any CPU.Build.0 = Release|Any CPU + {7C4F5973-0491-4028-B1DC-A9BA73FF9F77}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {7C4F5973-0491-4028-B1DC-A9BA73FF9F77}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {7C4F5973-0491-4028-B1DC-A9BA73FF9F77}.Release|x86.ActiveCfg = Release|Any CPU {323D0C04-B518-4A8F-8A8E-3546AD153D34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {323D0C04-B518-4A8F-8A8E-3546AD153D34}.Debug|Any CPU.Build.0 = Debug|Any CPU {323D0C04-B518-4A8F-8A8E-3546AD153D34}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -180,6 +192,7 @@ Global {A8AA326E-8EE8-4F11-B750-23028E0949D7} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {FBB2B86E-972B-4185-9FF2-62CAB5F8388F} = {DAAE4C74-D06F-4874-A166-33305D2643CE} {520B3AA4-363A-497C-8C15-80423C5AFC85} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} + {7C4F5973-0491-4028-B1DC-A9BA73FF9F77} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {16703B76-C9F7-4C75-AE6C-53D92E308E3C} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {323D0C04-B518-4A8F-8A8E-3546AD153D34} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {34DF1487-12C6-476C-BE0A-F31DF1939AE5} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} diff --git a/samples/MvcSample.Web/HomeController.cs b/samples/MvcSample.Web/HomeController.cs index 69c63be9fc..2298c11307 100644 --- a/samples/MvcSample.Web/HomeController.cs +++ b/samples/MvcSample.Web/HomeController.cs @@ -23,6 +23,11 @@ namespace MvcSample.Web return View("ValidationSummary"); } + public ActionResult InjectSample() + { + return View(); + } + /// /// Action that shows metadata when model is null. /// diff --git a/samples/MvcSample.Web/MvcSample.Web.kproj b/samples/MvcSample.Web/MvcSample.Web.kproj index ee7589e03e..0f9dbe9769 100644 --- a/samples/MvcSample.Web/MvcSample.Web.kproj +++ b/samples/MvcSample.Web/MvcSample.Web.kproj @@ -28,6 +28,7 @@ + @@ -60,6 +61,7 @@ + diff --git a/samples/MvcSample.Web/Services/TestService.cs b/samples/MvcSample.Web/Services/TestService.cs new file mode 100644 index 0000000000..153dfeaa75 --- /dev/null +++ b/samples/MvcSample.Web/Services/TestService.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace MvcSample.Web.Services +{ + public interface ITestService + { + string GetFoo(); + } + + + public class TestService : ITestService + { + public string GetFoo() + { + return "Hello world " + DateTime.UtcNow; + } + } +} \ No newline at end of file diff --git a/samples/MvcSample.Web/Startup.cs b/samples/MvcSample.Web/Startup.cs index e00fe14daa..40508cdc84 100644 --- a/samples/MvcSample.Web/Startup.cs +++ b/samples/MvcSample.Web/Startup.cs @@ -4,6 +4,7 @@ using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Routing; using Microsoft.Framework.DependencyInjection; using MvcSample.Web.Filters; +using MvcSample.Web.Services; namespace MvcSample.Web { @@ -16,6 +17,7 @@ namespace MvcSample.Web services.AddMvc(); services.AddSingleton(); services.AddSingleton(); + services.AddTransient(); }); app.UseMvc(routes => diff --git a/samples/MvcSample.Web/Views/Home/InjectSample.cshtml b/samples/MvcSample.Web/Views/Home/InjectSample.cshtml new file mode 100644 index 0000000000..2f3f14c8c7 --- /dev/null +++ b/samples/MvcSample.Web/Views/Home/InjectSample.cshtml @@ -0,0 +1,7 @@ +@using MvcSample.Web.Services +@inject MvcSample.Web.Services.ITestService MyService +@inject ITestService MyService2 + +@MyService.GetFoo() +@MyService2.GetFoo() + diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunk.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunk.cs new file mode 100644 index 0000000000..c58b5f7a8b --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunk.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Generator.Compiler; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class InjectChunk : Chunk + { + /// + /// Represents the chunk for an @inject statement. + /// + /// The type of object that would be injected + /// The member name the field is exposed to the page as. + public InjectChunk(string typeName, + string propertyName) + { + TypeName = typeName; + MemberName = propertyName; + } + + public string TypeName { get; private set; } + + public string MemberName { get; private set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunkVisitor.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunkVisitor.cs new file mode 100644 index 0000000000..9194926bcc --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/InjectChunkVisitor.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Globalization; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Generator.Compiler.CSharp; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class InjectChunkVisitor : MvcCSharpCodeVisitor + { + private readonly List _injectChunks = new List(); + + public InjectChunkVisitor([NotNull] CSharpCodeWriter writer, + [NotNull] CodeGeneratorContext context) + : base(writer, context) + { + } + + public List InjectChunks + { + get { return _injectChunks; } + } + + protected override void Visit([NotNull] InjectChunk chunk) + { + if (Context.Host.DesignTimeMode) + { + Writer.WriteLine("public"); + var code = string.Format(CultureInfo.InvariantCulture, + "{0} {1}", + chunk.TypeName, + chunk.MemberName); + var csharpVisitor = new CSharpCodeVisitor(Writer, Context); + csharpVisitor.CreateExpressionCodeMapping(code, chunk); + Writer.WriteLine("{ get; private set; }"); + } + else + { + Writer.Write("public ") + .Write(chunk.TypeName) + .Write(" ") + .Write(chunk.MemberName) + .WriteLine(" { get; private set; }"); + } + _injectChunks.Add(chunk); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/InjectParameterGenerator.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/InjectParameterGenerator.cs new file mode 100644 index 0000000000..fdca4ab85d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/InjectParameterGenerator.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class InjectParameterGenerator : SpanCodeGenerator + { + public InjectParameterGenerator(string typeName, string propertyName) + { + TypeName = typeName; + PropertyName = propertyName; + } + + public string TypeName { get; private set; } + + public string PropertyName { get; private set; } + + public override void GenerateCode(Span target, CodeGeneratorContext context) + { + var injectChunk = new InjectChunk(TypeName, PropertyName); + context.CodeTreeBuilder.AddChunk(injectChunk, target); + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "@inject {0} {1}", TypeName, PropertyName); + } + + public override bool Equals(object obj) + { + var other = obj as InjectParameterGenerator; + return other != null && + string.Equals(TypeName, other.TypeName, StringComparison.Ordinal) && + string.Equals(PropertyName, other.PropertyName, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + return TypeName.GetHashCode() + + (PropertyName.GetHashCode() * 13); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Microsoft.AspNet.Mvc.Razor.Host.kproj b/src/Microsoft.AspNet.Mvc.Razor.Host/Microsoft.AspNet.Mvc.Razor.Host.kproj index 3f79244f82..97bfe5d17d 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/Microsoft.AspNet.Mvc.Razor.Host.kproj +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Microsoft.AspNet.Mvc.Razor.Host.kproj @@ -22,6 +22,11 @@ + + + + + diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeBuilder.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeBuilder.cs new file mode 100644 index 0000000000..a3a9ac5c63 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeBuilder.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Generator.Compiler.CSharp; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class MvcCSharpCodeBuilder : CSharpCodeBuilder + { + public MvcCSharpCodeBuilder([NotNull] CodeGeneratorContext context) + : base(context) + { + } + + protected override void BuildConstructor([NotNull] CSharpCodeWriter writer) + { + writer.WriteLineHiddenDirective(); + + var injectVisitor = new InjectChunkVisitor(writer, Context); + injectVisitor.Accept(Context.CodeTreeBuilder.CodeTree.Chunks); + + writer.WriteLine(); + writer.WriteLineHiddenDirective(); + + var arguments = injectVisitor.InjectChunks + .Select(chunk => new KeyValuePair(chunk.TypeName, + chunk.MemberName)); + using (writer.BuildConstructor("public", Context.ClassName, arguments)) + { + foreach (var inject in injectVisitor.InjectChunks) + { + writer.WriteStartAssignment("this." + inject.MemberName) + .Write(inject.MemberName) + .WriteLine(";"); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeVistor.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeVistor.cs new file mode 100644 index 0000000000..89a843d307 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcCSharpCodeVistor.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Generator.Compiler; +using Microsoft.AspNet.Razor.Generator.Compiler.CSharp; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public abstract class MvcCSharpCodeVisitor : CodeVisitor + { + public MvcCSharpCodeVisitor([NotNull] CSharpCodeWriter writer, + [NotNull] CodeGeneratorContext context) + : base(writer, context) + { + } + + public override void Accept(Chunk chunk) + { + if (chunk is InjectChunk) + { + Visit((InjectChunk)chunk); + } + else + { + base.Accept(chunk); + } + } + + protected abstract void Visit(InjectChunk chunk); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorCodeParser.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorCodeParser.cs index 99cf008f96..6b19279dac 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorCodeParser.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorCodeParser.cs @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.AspNet.Mvc.Razor.Host; using Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Text; -using Microsoft.AspNet.Mvc.Razor.Host; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; namespace Microsoft.AspNet.Mvc.Razor { @@ -14,6 +15,7 @@ namespace Microsoft.AspNet.Mvc.Razor { private const string GenericTypeFormat = "{0}<{1}>"; private const string ModelKeyword = "model"; + private const string InjectKeyword = "inject"; private readonly string _baseType; private SourceLocation? _endInheritsLocation; private bool _modelStatementFound; @@ -22,6 +24,7 @@ namespace Microsoft.AspNet.Mvc.Razor { _baseType = baseType; MapDirectives(ModelDirective, ModelKeyword); + MapDirectives(InjectDirective, InjectKeyword); } protected override void InheritsDirective() @@ -39,7 +42,7 @@ namespace Microsoft.AspNet.Mvc.Razor { if (_modelStatementFound && _endInheritsLocation.HasValue) { - Context.OnError(_endInheritsLocation.Value, + Context.OnError(_endInheritsLocation.Value, Resources.FormatMvcRazorCodeParser_CannotHaveModelAndInheritsKeyword(ModelKeyword)); } } @@ -50,14 +53,14 @@ namespace Microsoft.AspNet.Mvc.Razor AssertDirective(ModelKeyword); AcceptAndMoveNext(); - SourceLocation endModelLocation = CurrentLocation; + var endModelLocation = CurrentLocation; - BaseTypeDirective(Resources.FormatMvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName(ModelKeyword), + BaseTypeDirective(Resources.FormatMvcRazorCodeParser_KeywordMustBeFollowedByTypeName(ModelKeyword), CreateModelCodeGenerator); if (_modelStatementFound) { - Context.OnError(endModelLocation, + Context.OnError(endModelLocation, Resources.FormatMvcRazorCodeParser_OnlyOneModelStatementIsAllowed(ModelKeyword)); } @@ -66,13 +69,80 @@ namespace Microsoft.AspNet.Mvc.Razor CheckForInheritsAndModelStatements(); } + protected virtual void InjectDirective() + { + // @inject MyApp.MyService MyServicePropertyName + AssertDirective(InjectKeyword); + AcceptAndMoveNext(); + + Context.CurrentBlock.Type = BlockType.Directive; + + // Accept whitespace + var remainingWs = AcceptSingleWhiteSpaceCharacter(); + if (Span.Symbols.Count > 1) + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + Output(SpanKind.MetaCode); + + if (remainingWs != null) + { + Accept(remainingWs); + } + + // Consume any other whitespace tokens. + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + var hasTypeError = !At(CSharpSymbolType.Identifier); + if (hasTypeError) + { + Context.OnError(CurrentLocation, + Resources.FormatMvcRazorCodeParser_KeywordMustBeFollowedByTypeName(InjectKeyword)); + } + + // Accept 'MyApp.MyService' + NamespaceOrTypeName(); + + // typeName now contains the token 'MyApp.MyService' + var typeName = Span.GetContent().Value; + + var propertyStartLocation = CurrentLocation; + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + if (!hasTypeError && At(CSharpSymbolType.NewLine)) + { + // Add an error for the property name only if we successfully read the type name + Context.OnError(propertyStartLocation, + Resources.FormatMvcRazorCodeParser_InjectDirectivePropertyNameRequired(InjectKeyword)); + } + + // Read until end of line. Span now contains 'MyApp.MyService MyServiceName'. + AcceptUntil(CSharpSymbolType.NewLine); + if (!Context.DesignTimeMode) + { + // We want the newline to be treated as code, but it causes issues at design-time. + Optional(CSharpSymbolType.NewLine); + } + + // Parse out 'MyServicePropertyName' from the Span. + var propertyName = Span.GetContent() + .Value + .Substring(typeName.Length); + + Span.CodeGenerator = new InjectParameterGenerator(typeName.Trim(), + propertyName.Trim()); + + // Output the span and finish the block + Output(SpanKind.Code); + } + private SpanCodeGenerator CreateModelCodeGenerator(string model) { // In the event we have an empty model, the name we generate does not matter since it's a parser error. // We'll use the non-generic version of the base type. - string baseType = String.IsNullOrEmpty(model) ? - _baseType : - String.Format(CultureInfo.InvariantCulture, GenericTypeFormat, _baseType, model); + var baseType = string.IsNullOrEmpty(model) ? + _baseType : + string.Format(CultureInfo.InvariantCulture, GenericTypeFormat, _baseType, model); return new SetBaseTypeCodeGenerator(baseType); } } diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs index 478d2284d1..6a08c7806a 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs @@ -5,6 +5,7 @@ using System; using System.IO; using Microsoft.AspNet.Razor; using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Generator.Compiler; using Microsoft.AspNet.Razor.Parser; namespace Microsoft.AspNet.Mvc.Razor @@ -57,7 +58,7 @@ namespace Microsoft.AspNet.Mvc.Razor public GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream) { - string className = ParserHelpers.SanitizeClassName(rootRelativePath); + var className = ParserHelpers.SanitizeClassName(rootRelativePath); using (var reader = new StreamReader(inputStream)) { var engine = new RazorTemplateEngine(this); @@ -69,5 +70,10 @@ namespace Microsoft.AspNet.Mvc.Razor { return new MvcRazorCodeParser(_baseType); } + + public override CodeBuilder DecorateCodeBuilder(CodeBuilder incomingBuilder, CodeGeneratorContext context) + { + return new MvcCSharpCodeBuilder(context); + } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs index 7d4618123b..ad6b256874 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs @@ -27,19 +27,35 @@ namespace Microsoft.AspNet.Mvc.Razor.Host } /// - /// The '{0}' keyword must be followed by a type name on the same line. + /// A property name must be specified when using the '{0}' statement. Format for a '{0}' statement is '@{0} <TypeName> <PropertyName>'. /// - internal static string MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName + internal static string MvcRazorCodeParser_InjectDirectivePropertyNameRequired { - get { return GetString("MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName"); } + get { return GetString("MvcRazorCodeParser_InjectDirectivePropertyNameRequired"); } + } + + /// + /// A property name must be specified when using the '{0}' statement. Format for a '{0}' statement is '@{0} <TypeName> <PropertyName>'. + /// + internal static string FormatMvcRazorCodeParser_InjectDirectivePropertyNameRequired(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_InjectDirectivePropertyNameRequired"), p0); } /// /// The '{0}' keyword must be followed by a type name on the same line. /// - internal static string FormatMvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName(object p0) + internal static string MvcRazorCodeParser_KeywordMustBeFollowedByTypeName { - return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName"), p0); + get { return GetString("MvcRazorCodeParser_KeywordMustBeFollowedByTypeName"); } + } + + /// + /// The '{0}' keyword must be followed by a type name on the same line. + /// + internal static string FormatMvcRazorCodeParser_KeywordMustBeFollowedByTypeName(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_KeywordMustBeFollowedByTypeName"), p0); } /// diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx index f95c023e35..f6562cd582 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx @@ -120,7 +120,10 @@ The 'inherits' keyword is not allowed when a '{0}' keyword is used. - + + A property name must be specified when using the '{0}' statement. Format for a '{0}' statement is '@{0} <Type Name> <Property Name>'. + + The '{0}' keyword must be followed by a type name on the same line. diff --git a/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/VirtualPathViewFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/VirtualPathViewFactory.cs index 632db46cea..ab6bfc4dd0 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/VirtualPathViewFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/ViewEngine/VirtualPathViewFactory.cs @@ -5,6 +5,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNet.FileSystems; using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.Runtime; namespace Microsoft.AspNet.Mvc.Razor @@ -13,22 +14,28 @@ namespace Microsoft.AspNet.Mvc.Razor { private readonly PhysicalFileSystem _fileSystem; private readonly IRazorCompilationService _compilationService; + private readonly ITypeActivator _activator; + private readonly IServiceProvider _serviceProvider; public VirtualPathViewFactory(IApplicationEnvironment env, - IRazorCompilationService compilationService) + IRazorCompilationService compilationService, + ITypeActivator typeActivator, + IServiceProvider serviceProvider) { // TODO: Continue to inject the IFileSystem but only when we get it from the host _fileSystem = new PhysicalFileSystem(env.ApplicationBasePath); _compilationService = compilationService; + _activator = typeActivator; + _serviceProvider = serviceProvider; } - public IView CreateInstance([NotNull]string virtualPath) + public IView CreateInstance([NotNull] string virtualPath) { IFileInfo fileInfo; if (_fileSystem.TryGetFileInfo(virtualPath, out fileInfo)) { CompilationResult result = _compilationService.Compile(fileInfo); - return (IView)Activator.CreateInstance(result.CompiledType); + return (IView)_activator.CreateInstance(_serviceProvider, result.CompiledType); } return null; diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs new file mode 100644 index 0000000000..a1a3b8556b --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/InjectChunkVisitorTest.cs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using Microsoft.AspNet.Razor; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Generator.Compiler; +using Microsoft.AspNet.Razor.Generator.Compiler.CSharp; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class InjectChunkVisitorTest + { + [Fact] + public void Visit_IgnoresNonInjectChunks() + { + // Arrange + var writer = new CSharpCodeWriter(); + var context = CreateContext(); + + var visitor = new InjectChunkVisitor(writer, context); + + // Act + visitor.Accept(new Chunk[] + { + new LiteralChunk(), + new CodeAttributeChunk() + }); + var code = writer.GenerateCode(); + + // Assert + Assert.Empty(code); + } + + [Fact] + public void Visit_GeneratesProperties_ForInjectChunks() + { + // Arrange + var expected = +@"public MyType1 MyPropertyName1 { get; private set; } +public MyType2 @MyPropertyName2 { get; private set; } +"; + var writer = new CSharpCodeWriter(); + var context = CreateContext(); + + var visitor = new InjectChunkVisitor(writer, context); + var factory = SpanFactory.CreateCsHtml(); + var node = (Span)factory.Code("Some code") + .As(new InjectParameterGenerator("MyType", "MyPropertyName")); + + // Act + visitor.Accept(new Chunk[] + { + new LiteralChunk(), + new InjectChunk("MyType1", "MyPropertyName1") { Association = node }, + new InjectChunk("MyType2", "@MyPropertyName2") { Association = node } + }); + var code = writer.GenerateCode(); + + // Assert + Assert.Equal(expected, code); + } + + [Fact] + public void Visit_WithDesignTimeHost_GeneratesPropertiesAndLinePragmas_ForInjectChunks() + { + // Arrange + var expected = @"public +#line 1 """" +MyType1 MyPropertyName1 + +#line default +#line hidden +{ get; private set; } +public +#line 1 """" +MyType2 @MyPropertyName2 + +#line default +#line hidden +{ get; private set; } +"; + var writer = new CSharpCodeWriter(); + var context = CreateContext(); + context.Host.DesignTimeMode = true; + + var visitor = new InjectChunkVisitor(writer, context); + var factory = SpanFactory.CreateCsHtml(); + var node = (Span)factory.Code("Some code") + .As(new InjectParameterGenerator("MyType", "MyPropertyName")); + + // Act + visitor.Accept(new Chunk[] + { + new LiteralChunk(), + new InjectChunk("MyType1", "MyPropertyName1") { Association = node }, + new InjectChunk("MyType2", "@MyPropertyName2") { Association = node } + }); + var code = writer.GenerateCode(); + + // Assert + Assert.Equal(expected, code); + } + + [Fact] + public void InjectVisitor_GeneratesCorrectLineMappings() + { + // Arrange + var host = new MvcRazorHost("RazorView") + { + DesignTimeMode = true + }; + host.NamespaceImports.Clear(); + var engine = new RazorTemplateEngine(host); + var source = ReadResource("Inject.cshtml"); + var expectedCode = ReadResource("Inject.cs"); + var expectedLineMappings = new List + { + BuildLineMapping(1, 0, 1, 32, 3, 0, 17), + BuildLineMapping(28, 1, 8, 442, 21, 8, 20) + }; + + // Act + GeneratorResults results; + using (var buffer = new StringTextBuffer(source)) + { + results = engine.GenerateCode(buffer); + } + + // Assert + Assert.True(results.Success); + Assert.Equal(expectedCode, results.GeneratedCode); + Assert.Empty(results.ParserErrors); + Assert.Equal(expectedLineMappings, results.DesignTimeLineMappings); + } + + private string ReadResource(string resourceName) + { + var assembly = typeof(InjectChunkVisitorTest).Assembly; + + using (var stream = assembly.GetManifestResourceStream(resourceName)) + using (var streamReader = new StreamReader(stream)) + { + return streamReader.ReadToEnd(); + } + } + + private static CodeGeneratorContext CreateContext() + { + return CodeGeneratorContext.Create(new MvcRazorHost("RazorView"), + "MyClass", + "MyNamespace", + string.Empty, + shouldGenerateLinePragmas: true); + } + + private static LineMapping BuildLineMapping(int documentAbsoluteIndex, + int documentLineIndex, + int documentCharacterIndex, + int generatedAbsoluteIndex, + int generatedLineIndex, + int generatedCharacterIndex, + int contentLength) + { + var documentLocation = new SourceLocation(documentAbsoluteIndex, + documentLineIndex, + documentCharacterIndex); + var generatedLocation = new SourceLocation(generatedAbsoluteIndex, + generatedLineIndex, + generatedCharacterIndex); + + return new LineMapping( + documentLocation: new MappingLocation(documentLocation, contentLength), + generatedLocation: new MappingLocation(generatedLocation, contentLength)); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Microsoft.AspNet.Mvc.Razor.Host.Test.kproj b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Microsoft.AspNet.Mvc.Razor.Host.Test.kproj new file mode 100644 index 0000000000..8b7af35e08 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Microsoft.AspNet.Mvc.Razor.Host.Test.kproj @@ -0,0 +1,41 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 7c4f5973-0491-4028-b1dc-a9ba73ff9f77 + Library + + + ConsoleDebugger + + + WebDebugger + + + + + + + 2.0 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcCSharpRazorCodeParserTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcCSharpRazorCodeParserTest.cs new file mode 100644 index 0000000000..be52271698 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcCSharpRazorCodeParserTest.cs @@ -0,0 +1,406 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Generator.Compiler; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class MvcCSharpRazorCodeParserTest + { + [Theory] + [InlineData("model")] + [InlineData("inject")] + public void Constructor_AddsMvcSpecificKeywords(string keyword) + { + // Arrange + var parser = new TestMvcCSharpRazorCodeParser(); + + // Act + var hasDirective = parser.HasDirective(keyword); + + // Assert + Assert.True(hasDirective); + } + + [Fact] + public void ParseModelKeyword_HandlesSingleInstance() + { + // Arrange + var document = "@model Foo"; + var factory = SpanFactory.CreateCsHtml(); + var errors = new List(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code(" Foo") + .As(new SetBaseTypeCodeGenerator("RazorView")) + }; + + // Act + var spans = ParseDocument(document, errors); + + // Assert + Assert.Equal(expectedSpans, spans); + Assert.Empty(errors); + } + + [Theory] + [InlineData("Foo?", "RazorView")] + [InlineData("Foo[[]][]", "RazorView")] + [InlineData("$rootnamespace$.MyModel", "RazorView<$rootnamespace$.MyModel>")] + public void ParseModelKeyword_InfersBaseType_FromModelName(string modelName, + string expectedBaseType) + { + // Arrange + var documentContent = "@model " + modelName + Environment.NewLine + "Bar"; + var factory = SpanFactory.CreateCsHtml(); + var errors = new List(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code(modelName + "\r\n") + .As(new SetBaseTypeCodeGenerator(expectedBaseType)), + factory.Markup("Bar") + .With(new MarkupCodeGenerator()) + }; + + // Act + var spans = ParseDocument(documentContent, errors); + + // Assert + Assert.Equal(expectedSpans, spans); + Assert.Empty(errors); + } + + [Fact] + public void ParseModelKeyword_ErrorOnMissingModelType() + { + // Arrange + Act + var errors = new List(); + var document = "@model "; + var spans = ParseDocument(document, errors); + + // Assert + var factory = SpanFactory.CreateCsHtml(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code(" ") + .As(new SetBaseTypeCodeGenerator("RazorView")), + }; + var expectedErrors = new[] + { + new RazorError("The 'model' keyword must be followed by a type name on the same line.", + new SourceLocation(9, 0, 9), 1) + }; + Assert.Equal(expectedSpans, spans); + Assert.Equal(expectedErrors, errors); + } + + [Fact] + public void ParseModelKeyword_ErrorOnMultipleModelStatements() + { + // Arrange + Act + var errors = new List(); + var document = + "@model Foo" + Environment.NewLine + + "@model Bar"; + + var factory = SpanFactory.CreateCsHtml(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code("Foo\r\n") + .As(new SetBaseTypeCodeGenerator("RazorView")), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code("Bar") + .As(new SetBaseTypeCodeGenerator("RazorView")) + }; + + var expectedErrors = new[] + { + new RazorError("Only one 'model' statement is allowed in a file.", + new SourceLocation(18, 1, 6), 1) + }; + + // Act + var spans = ParseDocument(document, errors); + + // Assert + Assert.Equal(expectedSpans, spans); + Assert.Equal(expectedErrors, errors); + } + + [Fact] + public void ParseModelKeyword_ErrorOnModelFollowedByInherits() + { + // Arrange + var errors = new List(); + var document = + "@model Foo" + Environment.NewLine + + "@inherits Bar"; + + var factory = SpanFactory.CreateCsHtml(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code("Foo\r\n") + .As(new SetBaseTypeCodeGenerator("RazorView")), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("inherits ") + .Accepts(AcceptedCharacters.None), + factory.Code("Bar") + .As(new SetBaseTypeCodeGenerator("Bar")) + }; + + var expectedErrors = new[] + { + new RazorError("The 'inherits' keyword is not allowed when a 'model' keyword is used.", + new SourceLocation(21, 1, 9), 1) + }; + + // Act + var spans = ParseDocument(document, errors); + + // Assert + Assert.Equal(expectedSpans, spans); + Assert.Equal(expectedErrors, errors); + } + + [Fact] + public void ParseModelKeyword_ErrorOnInheritsFollowedByModel() + { + // Arrange + var errors = new List(); + var document = + "@inherits Bar" + Environment.NewLine + + "@model Foo"; + + var factory = SpanFactory.CreateCsHtml(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("inherits ") + .Accepts(AcceptedCharacters.None), + factory.Code("Bar" + Environment.NewLine) + .As(new SetBaseTypeCodeGenerator("Bar")), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code("Foo") + .As(new SetBaseTypeCodeGenerator("RazorView")) + }; + + var expectedErrors = new[] + { + new RazorError("The 'inherits' keyword is not allowed when a 'model' keyword is used.", + new SourceLocation(9, 0, 9), 1) + }; + + // Act + var spans = ParseDocument(document, errors); + + // Assert + Assert.Equal(expectedSpans, spans.ToArray()); + Assert.Equal(expectedErrors, errors.ToArray()); + } + + [Theory] + [InlineData("IMyService Service", "IMyService", "Service")] + [InlineData(" Microsoft.AspNet.Mvc.IHtmlHelper MyHelper ", + "Microsoft.AspNet.Mvc.IHtmlHelper", "MyHelper")] + [InlineData(" TestService @class ", "TestService", "@class")] + public void ParseInjectKeyword_InfersTypeAndPropertyName(string injectStatement, + string expectedService, + string expectedPropertyName) + { + // Arrange + var documentContent = "@inject " + injectStatement; + var factory = SpanFactory.CreateCsHtml(); + var errors = new List(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("inject ") + .Accepts(AcceptedCharacters.None), + factory.Code(injectStatement) + .As(new InjectParameterGenerator(expectedService, expectedPropertyName)) + }; + + // Act + var spans = ParseDocument(documentContent, errors); + + // Assert + Assert.Equal(expectedSpans, spans); + Assert.Empty(errors); + } + + [Theory] + [InlineData("IMyService Service ", "IMyService", "Service")] + [InlineData(" TestService @namespace ", "TestService", "@namespace")] + public void ParseInjectKeyword_ParsesUpToNewLine(string injectStatement, + string expectedService, + string expectedPropertyName) + { + // Arrange + var documentContent = "@inject " + injectStatement + Environment.NewLine + "Bar"; + var factory = SpanFactory.CreateCsHtml(); + var errors = new List(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("inject ") + .Accepts(AcceptedCharacters.None), + factory.Code(injectStatement + "\r\n") + .As(new InjectParameterGenerator(expectedService, expectedPropertyName)), + factory.Markup("Bar") + .With(new MarkupCodeGenerator()) + }; + + // Act + var spans = ParseDocument(documentContent, errors); + + // Assert + Assert.Equal(expectedSpans, spans); + Assert.Empty(errors); + } + + [Fact] + public void ParseInjectKeyword_ErrorOnMissingTypeName() + { + // Arrange + var errors = new List(); + var documentContent = "@inject \r\nBar"; + var factory = SpanFactory.CreateCsHtml(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("inject ") + .Accepts(AcceptedCharacters.None), + factory.Code(" \r\n") + .As(new InjectParameterGenerator(string.Empty, string.Empty)), + factory.Markup("Bar") + .With(new MarkupCodeGenerator()) + }; + var expectedErrors = new[] + { + new RazorError("The 'inject' keyword must be followed by a type name on the same line.", + new SourceLocation(11, 0, 11), 1) + }; + + // Act + var spans = ParseDocument(documentContent, errors); + + // Assert + Assert.Equal(expectedSpans, spans); + Assert.Equal(expectedErrors, errors); + } + + [Fact] + public void ParseInjectKeyword_ErrorOnMissingPropertyName() + { + // Arrange + var errors = new List(); + var documentContent = "@inject IMyService \r\nBar"; + var factory = SpanFactory.CreateCsHtml(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("inject ") + .Accepts(AcceptedCharacters.None), + factory.Code(" IMyService \r\n") + .As(new InjectParameterGenerator("IMyService", string.Empty)), + factory.Markup("Bar") + .With(new MarkupCodeGenerator()) + }; + var expectedErrors = new[] + { + new RazorError("A property name must be specified when using the 'inject' statement. " + + "Format for a 'inject' statement is '@inject '.", + new SourceLocation(20, 0, 20), 1) + }; + + // Act + var spans = ParseDocument(documentContent, errors); + + // Assert + Assert.Equal(expectedSpans, spans); + Assert.Equal(expectedErrors, errors); + } + + private static List ParseDocument(string documentContents, + List errors = null, + List lineMappings = null) + { + errors = errors ?? new List(); + var markupParser = new HtmlMarkupParser(); + var codeParser = new TestMvcCSharpRazorCodeParser(); + var reader = new SeekableTextReader(documentContents); + var context = new ParserContext(reader, codeParser, markupParser, markupParser); + codeParser.Context = context; + markupParser.Context = context; + markupParser.ParseDocument(); + + var results = context.CompleteParse(); + errors.AddRange(results.ParserErrors); + return results.Document.Flatten().ToList(); + } + + private sealed class TestMvcCSharpRazorCodeParser : MvcRazorCodeParser + { + public TestMvcCSharpRazorCodeParser() + : base("RazorView") + { + } + + public bool HasDirective(string directive) + { + Action handler; + return TryGetDirectiveHandler(directive, out handler); + } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/RawTextSymbol.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/RawTextSymbol.cs new file mode 100644 index 0000000000..04cd180354 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/RawTextSymbol.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Mvc.Razor +{ + internal class RawTextSymbol : ISymbol + { + public SourceLocation Start { get; private set; } + public string Content { get; private set; } + + public RawTextSymbol(SourceLocation start, string content) + { + if (content == null) + { + throw new ArgumentNullException("content"); + } + + Start = start; + Content = content; + } + + public override bool Equals(object obj) + { + var other = obj as RawTextSymbol; + return Equals(Start, other.Start) && Equals(Content, other.Content); + } + + internal bool EquivalentTo(ISymbol sym) + { + return Equals(Start, sym.Start) && Equals(Content, sym.Content); + } + + public override int GetHashCode() + { + return Start.GetHashCode() + + (13 * Content.GetHashCode()); + } + + public void OffsetStart(SourceLocation documentStart) + { + Start = documentStart + Start; + } + + public void ChangeStart(SourceLocation newStart) + { + Start = newStart; + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0} RAW - [{1}]", Start, Content); + } + + internal void CalculateStart(Span prev) + { + if (prev == null) + { + Start = SourceLocation.Zero; + } + else + { + Start = new SourceLocationTracker(prev.Start).UpdateLocation(prev.Content).CurrentLocation; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/SpanConstructor.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/SpanConstructor.cs new file mode 100644 index 0000000000..12e05f455c --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/SpanConstructor.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.AspNet.Razor.Editor; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class SpanConstructor + { + public SpanBuilder Builder { get; private set; } + + internal static IEnumerable TestTokenizer(string str) + { + yield return new RawTextSymbol(SourceLocation.Zero, str); + } + + public SpanConstructor(SpanKind kind, IEnumerable symbols) + { + Builder = new SpanBuilder(); + Builder.Kind = kind; + Builder.EditHandler = SpanEditHandler.CreateDefault(TestTokenizer); + foreach (ISymbol sym in symbols) + { + Builder.Accept(sym); + } + } + + private Span Build() + { + return Builder.Build(); + } + + public SpanConstructor With(ISpanCodeGenerator generator) + { + Builder.CodeGenerator = generator; + return this; + } + + public SpanConstructor With(SpanEditHandler handler) + { + Builder.EditHandler = handler; + return this; + } + + public SpanConstructor With(Action generatorConfigurer) + { + generatorConfigurer(Builder.CodeGenerator); + return this; + } + + public SpanConstructor With(Action handlerConfigurer) + { + handlerConfigurer(Builder.EditHandler); + return this; + } + + public static implicit operator Span(SpanConstructor self) + { + return self.Build(); + } + + public SpanConstructor Hidden() + { + Builder.CodeGenerator = SpanCodeGenerator.Null; + return this; + } + + public SpanConstructor Accepts(AcceptedCharacters accepted) + { + return With(eh => eh.AcceptedCharacters = accepted); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/SpanFactory.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/SpanFactory.cs new file mode 100644 index 0000000000..019a1c9d24 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/SpanFactory.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class SpanFactory + { + public Func MarkupTokenizerFactory { get; set; } + public Func CodeTokenizerFactory { get; set; } + public SourceLocationTracker LocationTracker { get; private set; } + + public static SpanFactory CreateCsHtml() + { + return new SpanFactory() + { + MarkupTokenizerFactory = doc => new HtmlTokenizer(doc), + CodeTokenizerFactory = doc => new CSharpTokenizer(doc) + }; + } + + public SpanFactory() + { + LocationTracker = new SourceLocationTracker(); + } + + public SpanConstructor Span(SpanKind kind, string content, CSharpSymbolType type) + { + return CreateSymbolSpan(kind, content, st => new CSharpSymbol(st, content, type)); + } + + public SpanConstructor Span(SpanKind kind, string content, HtmlSymbolType type) + { + return CreateSymbolSpan(kind, content, st => new HtmlSymbol(st, content, type)); + } + + public SpanConstructor Span(SpanKind kind, string content, bool markup) + { + return new SpanConstructor(kind, Tokenize(new[] { content }, markup)); + } + + public SpanConstructor Span(SpanKind kind, string[] content, bool markup) + { + return new SpanConstructor(kind, Tokenize(content, markup)); + } + + public SpanConstructor Span(SpanKind kind, params ISymbol[] symbols) + { + return new SpanConstructor(kind, symbols); + } + + private SpanConstructor CreateSymbolSpan(SpanKind kind, string content, Func ctor) + { + var start = LocationTracker.CurrentLocation; + LocationTracker.UpdateLocation(content); + return new SpanConstructor(kind, new[] { ctor(start) }); + } + + public void Reset() + { + LocationTracker.CurrentLocation = SourceLocation.Zero; + } + + private IEnumerable Tokenize(IEnumerable contentFragments, bool markup) + { + return contentFragments.SelectMany(fragment => Tokenize(fragment, markup)); + } + + private IEnumerable Tokenize(string content, bool markup) + { + var tok = MakeTokenizer(markup, new SeekableTextReader(content)); + ISymbol sym; + ISymbol last = null; + while ((sym = tok.NextSymbol()) != null) + { + OffsetStart(sym, LocationTracker.CurrentLocation); + last = sym; + yield return sym; + } + LocationTracker.UpdateLocation(content); + } + + private ITokenizer MakeTokenizer(bool markup, SeekableTextReader seekableTextReader) + { + if (markup) + { + return MarkupTokenizerFactory(seekableTextReader); + } + else + { + return CodeTokenizerFactory(seekableTextReader); + } + } + + private void OffsetStart(ISymbol sym, SourceLocation sourceLocation) + { + sym.OffsetStart(sourceLocation); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/SpanFactoryExtensions.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/SpanFactoryExtensions.cs new file mode 100644 index 0000000000..3214b3871c --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/SpanFactoryExtensions.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public static class SpanFactoryExtensions + { + public static UnclassifiedCodeSpanConstructor EmptyCSharp(this SpanFactory self) + { + var symbol = new CSharpSymbol(self.LocationTracker.CurrentLocation, string.Empty, CSharpSymbolType.Unknown); + return new UnclassifiedCodeSpanConstructor(self.Span(SpanKind.Code, symbol)); + } + + public static SpanConstructor EmptyHtml(this SpanFactory self) + { + var symbol = new HtmlSymbol(self.LocationTracker.CurrentLocation, string.Empty, HtmlSymbolType.Unknown); + return self.Span(SpanKind.Markup, symbol) + .With(new MarkupCodeGenerator()); + } + + public static UnclassifiedCodeSpanConstructor Code(this SpanFactory self, string content) + { + return new UnclassifiedCodeSpanConstructor( + self.Span(SpanKind.Code, content, markup: false)); + } + + public static SpanConstructor CodeTransition(this SpanFactory self) + { + return self.Span(SpanKind.Transition, SyntaxConstants.TransitionString, markup: false) + .Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor CodeTransition(this SpanFactory self, string content) + { + return self.Span(SpanKind.Transition, content, markup: false).Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor CodeTransition(this SpanFactory self, CSharpSymbolType type) + { + return self.Span(SpanKind.Transition, SyntaxConstants.TransitionString, type) + .Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor CodeTransition(this SpanFactory self, string content, CSharpSymbolType type) + { + return self.Span(SpanKind.Transition, content, type).Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor MarkupTransition(this SpanFactory self) + { + return self.Span(SpanKind.Transition, SyntaxConstants.TransitionString, markup: true) + .Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor MarkupTransition(this SpanFactory self, string content) + { + return self.Span(SpanKind.Transition, content, markup: true).Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor MarkupTransition(this SpanFactory self, HtmlSymbolType type) + { + return self.Span(SpanKind.Transition, SyntaxConstants.TransitionString, type) + .Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor MarkupTransition(this SpanFactory self, string content, HtmlSymbolType type) + { + return self.Span(SpanKind.Transition, content, type).Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor MetaCode(this SpanFactory self, string content) + { + return self.Span(SpanKind.MetaCode, content, markup: false); + } + + public static SpanConstructor MetaCode(this SpanFactory self, string content, CSharpSymbolType type) + { + return self.Span(SpanKind.MetaCode, content, type); + } + + public static SpanConstructor MetaMarkup(this SpanFactory self, string content) + { + return self.Span(SpanKind.MetaCode, content, markup: true); + } + + public static SpanConstructor MetaMarkup(this SpanFactory self, string content, HtmlSymbolType type) + { + return self.Span(SpanKind.MetaCode, content, type); + } + + public static SpanConstructor Comment(this SpanFactory self, string content, CSharpSymbolType type) + { + return self.Span(SpanKind.Comment, content, type); + } + + public static SpanConstructor Comment(this SpanFactory self, string content, HtmlSymbolType type) + { + return self.Span(SpanKind.Comment, content, type); + } + + public static SpanConstructor Markup(this SpanFactory self, string content) + { + return self.Span(SpanKind.Markup, content, markup: true).With(new MarkupCodeGenerator()); + } + + public static SpanConstructor Markup(this SpanFactory self, params string[] content) + { + return self.Span(SpanKind.Markup, content, markup: true).With(new MarkupCodeGenerator()); + } + + public static SourceLocation GetLocationAndAdvance(this SourceLocationTracker self, string content) + { + var ret = self.CurrentLocation; + self.UpdateLocation(content); + return ret; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/UnclassifiedSpanConstructor.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/UnclassifiedSpanConstructor.cs new file mode 100644 index 0000000000..d95b53bd8f --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory/UnclassifiedSpanConstructor.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Editor; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class UnclassifiedCodeSpanConstructor + { + private SpanConstructor _self; + + public UnclassifiedCodeSpanConstructor(SpanConstructor self) + { + _self = self; + } + + public SpanConstructor AsMetaCode() + { + _self.Builder.Kind = SpanKind.MetaCode; + return _self; + } + + public SpanConstructor AsStatement() + { + return _self.With(new StatementCodeGenerator()); + } + + public SpanConstructor AsExpression() + { + return _self.With(new ExpressionCodeGenerator()); + } + + public SpanConstructor AsImplicitExpression(ISet keywords) + { + return AsImplicitExpression(keywords, acceptTrailingDot: false); + } + + public SpanConstructor AsImplicitExpression(ISet keywords, bool acceptTrailingDot) + { + return _self.With(new ImplicitExpressionEditHandler(SpanConstructor.TestTokenizer, + keywords, + acceptTrailingDot)) + .With(new ExpressionCodeGenerator()); + } + + public SpanConstructor AsFunctionsBody() + { + return _self.With(new TypeMemberCodeGenerator()); + } + + public SpanConstructor AsNamespaceImport(string ns, int namespaceKeywordLength) + { + return _self.With(new AddImportCodeGenerator(ns, namespaceKeywordLength)); + } + + public SpanConstructor Hidden() + { + return _self.With(SpanCodeGenerator.Null); + } + + public SpanConstructor AsBaseType(string baseType) + { + return _self.With(new SetBaseTypeCodeGenerator(baseType)); + } + + public SpanConstructor AsRazorDirectiveAttribute(string key, string value) + { + return _self.With(new RazorDirectiveAttributeCodeGenerator(key, value)); + } + + public SpanConstructor As(ISpanCodeGenerator codeGenerator) + { + return _self.With(codeGenerator); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/StringTextBuffer.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/StringTextBuffer.cs new file mode 100644 index 0000000000..fd8ed4df8b --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/StringTextBuffer.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.AspNet.Razor.Text; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class StringTextBuffer : ITextBuffer, IDisposable + { + private string _buffer; + public bool Disposed { get; set; } + + public StringTextBuffer(string buffer) + { + _buffer = buffer; + } + + public int Length + { + get { return _buffer.Length; } + } + + public int Position { get; set; } + + public int Read() + { + if (Position >= _buffer.Length) + { + return -1; + } + return _buffer[Position++]; + } + + public int Peek() + { + if (Position >= _buffer.Length) + { + return -1; + } + return _buffer[Position]; + } + + public void Dispose() + { + Disposed = true; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Input/Inject.cshtml b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Input/Inject.cshtml new file mode 100644 index 0000000000..4e90b36808 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Input/Inject.cshtml @@ -0,0 +1,2 @@ +@using MyNamespace +@inject MyApp MyPropertyName \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Inject.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Inject.cs new file mode 100644 index 0000000000..27799209ba --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFiles/Output/Inject.cs @@ -0,0 +1,40 @@ +namespace Razor +{ +#line 1 "" +using MyNamespace + +#line default +#line hidden + ; + using System.Threading.Tasks; + + public class __CompiledTemplate : RazorView + { + private static object @__o; + private void @__RazorDesignTimeHelpers__() + { + #pragma warning disable 219 + #pragma warning restore 219 + } + #line hidden + public +#line 2 "" + MyApp MyPropertyName + +#line default +#line hidden + { get; private set; } + + #line hidden + public __CompiledTemplate(MyApp MyPropertyName) + { + this.MyPropertyName = MyPropertyName; + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { + } + #pragma warning restore 1998 + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/project.json b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/project.json new file mode 100644 index 0000000000..84a3ddcda2 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/project.json @@ -0,0 +1,22 @@ +{ + "version" : "0.1-alpha-*", + "compilationOptions": { + "warningsAsErrors": false + }, + "resources": "TestFiles\\**", + "dependencies": { + "Microsoft.AspNet.Mvc.Razor.Host" : "", + "xunit.assert": "2.0.0-aspnet-*", + "Xunit.KRunner": "0.1-alpha-*" + }, + "commands": { + "test": "Xunit.KRunner" + }, + "configurations": { + "net45": { + "dependencies": { + "Moq": "4.2.1312.1622" + } + } + } +} \ No newline at end of file