aspnetcore/test/Microsoft.AspNetCore.Mvc.Ra.../MvcCSharpRazorCodeParserTes...

555 lines
22 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Chunks.Generators;
using Microsoft.AspNetCore.Razor.CodeGenerators;
using Microsoft.AspNetCore.Razor.Generator;
using Microsoft.AspNetCore.Razor.Parser;
using Microsoft.AspNetCore.Razor.Parser.Internal;
using Microsoft.AspNetCore.Razor.Parser.SyntaxTree;
using Microsoft.AspNetCore.Razor.Text;
using Xunit;
namespace Microsoft.AspNetCore.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<RazorError>();
var expectedSpans = new Span[]
{
factory.EmptyHtml(),
factory.CodeTransition(SyntaxConstants.TransitionString)
.Accepts(AcceptedCharacters.None),
factory.MetaCode("model ")
.Accepts(AcceptedCharacters.None),
factory.Code(" Foo")
.As(new ModelChunkGenerator("Foo"))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.EmptyHtml()
};
// 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<RazorError>();
var expectedSpans = new Span[]
{
factory.EmptyHtml(),
factory.CodeTransition(SyntaxConstants.TransitionString)
.Accepts(AcceptedCharacters.None),
factory.MetaCode("model ")
.Accepts(AcceptedCharacters.None),
factory.Code(modelName + Environment.NewLine)
.As(new ModelChunkGenerator(expectedModel))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.Markup("Bar")
.With(new MarkupChunkGenerator())
};
// 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<RazorError>();
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 ModelChunkGenerator(string.Empty))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.EmptyHtml(),
};
var expectedErrors = new[]
{
new RazorError("The 'model' keyword must be followed by a type name on the same line.",
new SourceLocation(1, 0, 1), 5)
};
Assert.Equal(expectedSpans, spans);
Assert.Equal(expectedErrors, errors);
}
[Fact]
public void ParseModelKeyword_ErrorOnMultipleModelStatements()
{
// Arrange + Act
var errors = new List<RazorError>();
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" + Environment.NewLine)
.As(new ModelChunkGenerator("Foo"))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.EmptyHtml(),
factory.CodeTransition(SyntaxConstants.TransitionString)
.Accepts(AcceptedCharacters.None),
factory.MetaCode("model ")
.Accepts(AcceptedCharacters.None),
factory.Code("Bar")
.As(new ModelChunkGenerator("Bar"))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.EmptyHtml()
};
var expectedErrors = new[]
{
new RazorError(
"Only one 'model' statement is allowed in a file.",
PlatformNormalizer.NormalizedSourceLocation(13, 1, 1),
5)
};
// 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<RazorError>();
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" + Environment.NewLine)
.As(new ModelChunkGenerator("Foo"))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.EmptyHtml(),
factory.CodeTransition(SyntaxConstants.TransitionString)
.Accepts(AcceptedCharacters.None),
factory.MetaCode("inherits ")
.Accepts(AcceptedCharacters.None),
factory.Code("Bar")
.As(new SetBaseTypeChunkGenerator("Bar"))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.EmptyHtml()
};
var expectedErrors = new[]
{
new RazorError(
"The 'inherits' keyword is not allowed when a 'model' keyword is used.",
PlatformNormalizer.NormalizedSourceLocation(21, 1, 9),
length: 8)
};
// 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<RazorError>();
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 SetBaseTypeChunkGenerator("Bar"))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.EmptyHtml(),
factory.CodeTransition(SyntaxConstants.TransitionString)
.Accepts(AcceptedCharacters.None),
factory.MetaCode("model ")
.Accepts(AcceptedCharacters.None),
factory.Code("Foo")
.As(new ModelChunkGenerator("Foo"))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.EmptyHtml()
};
var expectedErrors = new[]
{
new RazorError(
"The 'inherits' keyword is not allowed when a 'model' keyword is used.",
new SourceLocation(9, 0, 9),
length: 8)
};
// 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.AspNetCore.Mvc.IHtmlHelper<MyNullableModel[]?> MyHelper ",
"Microsoft.AspNetCore.Mvc.IHtmlHelper<MyNullableModel[]?>", "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<RazorError>();
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))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.EmptyHtml()
};
// Act
var spans = ParseDocument(documentContent, errors);
// Assert
Assert.Equal(expectedSpans, spans);
Assert.Empty(errors);
}
[Theory]
[InlineData("IMyService Service;", "IMyService", "Service")]
[InlineData("IMyService Service;;", "IMyService", "Service")]
[InlineData(" Microsoft.AspNetCore.Mvc.IHtmlHelper<MyNullableModel[]?> MyHelper; ",
"Microsoft.AspNetCore.Mvc.IHtmlHelper<MyNullableModel[]?>", "MyHelper")]
[InlineData(" Microsoft.AspNetCore.Mvc.IHtmlHelper<MyNullableModel[]?> MyHelper; ; ",
"Microsoft.AspNetCore.Mvc.IHtmlHelper<MyNullableModel[]?>", "MyHelper")]
[InlineData(" TestService @class; ; ", "TestService", "@class")]
[InlineData("IMyService Service ;", "IMyService", "Service")]
[InlineData("IMyService Service ; ;", "IMyService", "Service")]
[InlineData(" Microsoft.AspNetCore.Mvc.IHtmlHelper<MyNullableModel[]?> MyHelper ; ",
"Microsoft.AspNetCore.Mvc.IHtmlHelper<MyNullableModel[]?>", "MyHelper")]
[InlineData(" Microsoft.AspNetCore.Mvc.IHtmlHelper<MyNullableModel[]?> MyHelper ; ; ",
"Microsoft.AspNetCore.Mvc.IHtmlHelper<MyNullableModel[]?>", "MyHelper")]
[InlineData(" TestService @class ; ", "TestService", "@class")]
[InlineData(" TestService @class ; ; ", "TestService", "@class")]
public void ParseInjectKeyword_AllowsOptionalTrailingSemicolon(
string injectStatement,
string expectedService,
string expectedPropertyName)
{
// Arrange
var documentContent = "@inject " + injectStatement;
var factory = SpanFactory.CreateCsHtml();
var errors = new List<RazorError>();
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))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.EmptyHtml()
};
// 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<RazorError>();
var expectedSpans = new Span[]
{
factory.EmptyHtml(),
factory.CodeTransition(SyntaxConstants.TransitionString)
.Accepts(AcceptedCharacters.None),
factory.MetaCode("inject ")
.Accepts(AcceptedCharacters.None),
factory.Code(injectStatement + Environment.NewLine)
.As(new InjectParameterGenerator(expectedService, expectedPropertyName))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.Markup("Bar")
.With(new MarkupChunkGenerator())
};
// Act
var spans = ParseDocument(documentContent, errors);
// Assert
Assert.Equal(expectedSpans, spans);
Assert.Empty(errors);
}
[Fact]
public void ParseInjectKeyword_ErrorOnMissingTypeName()
{
// Arrange
var errors = new List<RazorError>();
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(" " + Environment.NewLine)
.As(new InjectParameterGenerator(string.Empty, string.Empty))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.Markup("Bar")
.With(new MarkupChunkGenerator())
};
var expectedErrors = new[]
{
new RazorError("The 'inject' keyword must be followed by a type name on the same line.",
new SourceLocation(1, 0, 1), 6)
};
// 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<RazorError>();
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))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.EmptyHtml()
};
var expectedErrors = new[]
{
new RazorError("The 'inject' keyword must be followed by a type name on the same line.",
new SourceLocation(1, 0, 1), 6)
};
// 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<RazorError>();
var documentContent = $"@inject IMyService {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(" IMyService " + Environment.NewLine)
.As(new InjectParameterGenerator("IMyService", string.Empty))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.Markup("Bar")
.With(new MarkupChunkGenerator())
};
var expectedErrors = new[]
{
new RazorError("A property name must be specified when using the 'inject' statement. " +
"Format for a 'inject' statement is '@inject <Type Name> <Property Name>'.",
new SourceLocation(1, 0, 1), 21)
};
// 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<RazorError>();
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))
.Accepts(AcceptedCharacters.AnyExceptNewline),
factory.EmptyHtml()
};
var expectedErrors = new[]
{
new RazorError("A property name must be specified when using the 'inject' statement. " +
"Format for a 'inject' statement is '@inject <Type Name> <Property Name>'.",
new SourceLocation(1, 0, 1), 21)
};
// Act
var spans = ParseDocument(documentContent, errors);
// Assert
Assert.Equal(expectedSpans, spans);
Assert.Equal(expectedErrors, errors);
}
private static List<Span> ParseDocument(
string documentContents,
List<RazorError> errors = null,
List<LineMapping> lineMappings = null)
{
errors = errors ?? new List<RazorError>();
var markupParser = new HtmlMarkupParser();
var codeParser = new TestMvcCSharpRazorCodeParser();
var reader = new SeekableTextReader(documentContents);
var context = new ParserContext(
reader,
codeParser,
markupParser,
markupParser,
new ErrorSink());
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 bool HasDirective(string directive)
{
Action handler;
return TryGetDirectiveHandler(directive, out handler);
}
}
}
}