// 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 ModelCodeGenerator("RazorView", "Foo")) }; // Act var spans = ParseDocument(document, errors); // Assert Assert.Equal(expectedSpans, spans); Assert.Empty(errors); } [Theory] [InlineData("Foo?", "Foo?")] [InlineData("Foo[[]][]", "Foo[[]][]")] [InlineData("$rootnamespace$.MyModel", "$rootnamespace$.MyModel")] public void ParseModelKeyword_InfersBaseType_FromModelName(string modelName, string expectedModel) { // 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 ModelCodeGenerator("RazorView", expectedModel)), 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 ModelCodeGenerator("RazorView", string.Empty)), }; 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 ModelCodeGenerator("RazorView", "Foo")), factory.CodeTransition(SyntaxConstants.TransitionString) .Accepts(AcceptedCharacters.None), factory.MetaCode("model ") .Accepts(AcceptedCharacters.None), factory.Code("Bar") .As(new ModelCodeGenerator("RazorView", "Bar")) }; 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 ModelCodeGenerator("RazorView", "Foo")), 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 ModelCodeGenerator("RazorView", "Foo")) }; 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 " + Environment.NewLine + "Bar"; 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_ErrorOnMissingTypeName_WhenTypeNameEndsWithEOF() { // Arrange var errors = new List(); var documentContent = "@inject "; 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(" ") .As(new InjectParameterGenerator(string.Empty, string.Empty)), }; 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); } [Fact] public void ParseInjectKeyword_ErrorOnMissingPropertyName_WhenTypeNameEndsWithEOF() { // Arrange var errors = new List(); var documentContent = "@inject IMyServi"; 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(" IMyServi") .As(new InjectParameterGenerator("IMyServi", string.Empty)), }; 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(19, 0, 19), 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); } } } }