Handle trailing semicolon after @inject.

- Made @inject handle trailing semicolons identical to @using; essentially ignores it.
- Added parser, runtime/designtime codegen and functional tests.
- Added Microsoft.AspNet.Mvc.Common.Test.
- Transitioned pre-existing Microsoft.AspNet.Mvc.Common tests to the new test project.
- Updated transitioned tests to also work in CoreCLR (except ones with moq).

#1857
This commit is contained in:
N. Taylor Mullen 2015-01-22 12:30:10 -08:00
parent 09928a2818
commit 14bd7dcd5e
16 changed files with 417 additions and 11 deletions

17
Mvc.sln
View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.22303.1
VisualStudioVersion = 14.0.22512.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject
@ -122,6 +122,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CustomRouteWebSite", "test\
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResponseCacheWebSite", "test\WebSites\ResponseCacheWebSite\ResponseCacheWebSite.kproj", "{BDEEBE09-C0C4-433C-B0B8-8478C9776996}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Common.Test", "test\Microsoft.AspNet.Mvc.Common.Test\Microsoft.AspNet.Mvc.Common.Test.kproj", "{0449D6D2-BE1B-4E29-8E1B-444420802C03}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -682,6 +684,18 @@ Global
{BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Release|x86.ActiveCfg = Release|Any CPU
{BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Release|x86.Build.0 = Release|Any CPU
{0449D6D2-BE1B-4E29-8E1B-444420802C03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0449D6D2-BE1B-4E29-8E1B-444420802C03}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0449D6D2-BE1B-4E29-8E1B-444420802C03}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{0449D6D2-BE1B-4E29-8E1B-444420802C03}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{0449D6D2-BE1B-4E29-8E1B-444420802C03}.Debug|x86.ActiveCfg = Debug|Any CPU
{0449D6D2-BE1B-4E29-8E1B-444420802C03}.Debug|x86.Build.0 = Debug|Any CPU
{0449D6D2-BE1B-4E29-8E1B-444420802C03}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0449D6D2-BE1B-4E29-8E1B-444420802C03}.Release|Any CPU.Build.0 = Release|Any CPU
{0449D6D2-BE1B-4E29-8E1B-444420802C03}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{0449D6D2-BE1B-4E29-8E1B-444420802C03}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{0449D6D2-BE1B-4E29-8E1B-444420802C03}.Release|x86.ActiveCfg = Release|Any CPU
{0449D6D2-BE1B-4E29-8E1B-444420802C03}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -741,5 +755,6 @@ Global
{AF210F69-9D31-43AF-AC3A-CD366E252218} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{BDEEBE09-C0C4-433C-B0B8-8478C9776996} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{0449D6D2-BE1B-4E29-8E1B-444420802C03} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,7 @@
// 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.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.Common.Test")]
[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.Razor.Host")]

View File

@ -0,0 +1,58 @@
// 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.Linq;
namespace Microsoft.AspNet.Mvc
{
internal static class StringHelper
{
public static string TrimSpacesAndChars(string value, params char[] chars)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
if (chars == null || chars.Length == 0)
{
return value.Trim();
}
var firstIndex = 0;
for (; firstIndex < value.Length; firstIndex++)
{
var currentChar = value[firstIndex];
if (!char.IsWhiteSpace(currentChar) && !chars.Any(compareChar => compareChar == currentChar))
{
break;
}
}
// We trimmed all the way
if (firstIndex == value.Length)
{
return string.Empty;
}
var lastIndex = value.Length - 1;
for (; lastIndex > firstIndex; lastIndex--)
{
var currentChar = value[lastIndex];
if (!char.IsWhiteSpace(currentChar) && !chars.Any(compareChar => compareChar == currentChar))
{
break;
}
}
if (firstIndex == 0 && lastIndex == value.Length - 1)
{
return value;
}
else
{
return value.Substring(firstIndex, lastIndex - firstIndex + 1);
}
}
}
}

View File

@ -127,8 +127,9 @@ namespace Microsoft.AspNet.Mvc.Razor
.Value
.Substring(typeName.Length);
Span.CodeGenerator = new InjectParameterGenerator(typeName.Trim(),
propertyName.Trim());
// ';' is optional
propertyName = StringHelper.TrimSpacesAndChars(propertyName, ';');
Span.CodeGenerator = new InjectParameterGenerator(typeName.Trim(), propertyName);
// Output the span and finish the block
CompleteBlock();

View File

@ -3,13 +3,16 @@
using System;
using System.Collections.Generic;
#if !ASPNETCORE50
using Moq;
#endif
using Xunit;
namespace Microsoft.AspNet.Mvc.Core
namespace Microsoft.AspNet.Mvc
{
public class CopyOnWriteDictionaryTest
{
#if !ASPNETCORE50
[Fact]
public void ReadOperation_DelegatesToSourceDictionary_IfNoMutationsArePerformed()
{
@ -49,6 +52,7 @@ namespace Microsoft.AspNet.Mvc.Core
Assert.False(copyOnWriteDictionary.TryGetValue("different-key", out value));
sourceDictionary.Verify();
}
#endif
[Fact]
public void ReadOperation_DoesNotDelegateToSourceDictionary_OnceAValueIsChanged()

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>0449d6d2-be1b-4e29-8e1b-444420802c03</ProjectGuid>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -16,10 +16,10 @@ namespace Microsoft.AspNet.Mvc
{
// Arrange
var anonymous = new { foo = "bar" };
PropertyInfo property = anonymous.GetType().GetProperties().First();
var property = PropertyHelper.GetProperties(anonymous.GetType()).First().Property;
// Act
PropertyHelper helper = new PropertyHelper(property);
var helper = new PropertyHelper(property);
// Assert
Assert.Equal("foo", property.Name);
@ -31,10 +31,10 @@ namespace Microsoft.AspNet.Mvc
{
// Arrange
var anonymous = new { bar = "baz" };
PropertyInfo property = anonymous.GetType().GetProperties().First();
var property = PropertyHelper.GetProperties(anonymous.GetType()).First().Property;
// Act
PropertyHelper helper = new PropertyHelper(property);
var helper = new PropertyHelper(property);
// Assert
Assert.Equal("bar", helper.Name);
@ -46,7 +46,7 @@ namespace Microsoft.AspNet.Mvc
{
// Arrange
var anonymous = new { foo = 32 };
var property = anonymous.GetType().GetProperties().First();
var property = PropertyHelper.GetProperties(anonymous.GetType()).First().Property;
// Act
var helper = new PropertyHelper(property);

View File

@ -0,0 +1,97 @@
// 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 Xunit;
namespace Microsoft.AspNet.Mvc
{
public class StringHelperTest
{
public static TheoryData TrimSpacesAndCharsData
{
get
{
// input, trimCharacters, expectedOutput
return new TheoryData<string, char[], string>
{
{ "abcd", new char[] { }, "abcd" },
{ " /.", new char[] { '/', '.' }, string.Empty },
{ string.Empty, new char[] { }, string.Empty },
{ " ", new char[] { }, string.Empty },
{ " ", new char[] { }, string.Empty },
{ " / ", new char[] { '/' }, string.Empty },
{ " \t ", new char[] { '/' }, string.Empty },
{ " ", new char[] { '/' }, string.Empty },
{ " ", new char[] { '/' }, string.Empty },
{ "/", new char[] { '/' }, string.Empty },
{ "//", new char[] { '/' }, string.Empty },
{ "// ", new char[] { '/' }, string.Empty },
{ "/ ", new char[] { '/' }, string.Empty },
{ " a ", new char[] { }, "a" },
{ " a", new char[] { }, "a" },
{ "a ", new char[] { }, "a" },
{ " a ", new char[] { }, "a" },
{ " a \n\r", new char[] { }, "a" },
{ "\t\r a ", new char[] { }, "a" },
{ "\ta ", new char[] { }, "a" },
{ " a a ", new char[] { }, "a a" },
{ " a ", new char[] { '/' }, "a" },
{ " a", new char[] { '/' }, "a" },
{ "a ", new char[] { '/' }, "a" },
{ " a ", new char[] { '/' }, "a" },
{ " a \n\r", new char[] { '/' }, "a" },
{ "\t\r a ", new char[] { '/' }, "a" },
{ "\ta ", new char[] { '/' }, "a" },
{ " a a ", new char[] { '/' }, "a a" },
{ " a ", new char[] { '/', ' ' }, "a" },
{ " a", new char[] { '/', ' ' }, "a" },
{ "a ", new char[] { '/', ' ' }, "a" },
{ " a ", new char[] { '/', ' ' }, "a" },
{ " a \n\r", new char[] { '/', ' ' }, "a" },
{ "\t\r a ", new char[] { '/', ' ' }, "a" },
{ "\ta ", new char[] { '/', ' ' }, "a" },
{ " a a ", new char[] { '/', ' ' }, "a a" },
{ "/ a ", new char[] { '/' }, "a" },
{ " / a", new char[] { '/' }, "a" },
{ "a / /", new char[] { '/' }, "a" },
{ " a // //", new char[] { '/' }, "a" },
{ " a \n\r//", new char[] { '/' }, "a" },
{ "////\t\r a ", new char[] { '/' }, "a" },
{ "\ta /", new char[] { '/' }, "a" },
{ " a/ a ", new char[] { '/' }, "a/ a" },
{ "/ a ", new char[] { '/', ' ' }, "a" },
{ " / a", new char[] { '/', ' ' }, "a" },
{ "a / /", new char[] { '/', ' ' }, "a" },
{ " a // //", new char[] { '/', ' ' }, "a" },
{ " a \n\r//", new char[] { '/', ' ' }, "a" },
{ "////\t\r a ", new char[] { '/', ' ' }, "a" },
{ "\ta /", new char[] { '/', ' ' }, "a" },
{ " a/ a ", new char[] { '/', ' ' }, "a/ a" },
{ " a /.", new char[] { '/', '.' }, "a" },
{ " a", new char[] { '/', '.' }, "a" },
{ "/. ./a ", new char[] { '/', '.' }, "a" },
{ " a ", new char[] { '/', '.' }, "a" },
{ " a \n\r", new char[] { '/', '.' }, "a" },
{ "\t\r a ", new char[] { '/', '.' }, "a" },
{ "\ta ", new char[] { '/', '.' }, "a" },
{ "///..a/./a /. ./....", new char[] { '/', '.' }, "a/./a" },
};
}
}
[Theory]
[MemberData(nameof(TrimSpacesAndCharsData))]
public void TrimSpacesAndChars_GeneratesExpectedOutput(
string input,
char[] trimCharacters,
string expectedOutput)
{
// Arrange & Act
var output = StringHelper.TrimSpacesAndChars(input, trimCharacters);
// Assert
Assert.Equal(expectedOutput, output, StringComparer.Ordinal);
}
}
}

View File

@ -5,7 +5,7 @@ using System;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNet.Mvc.Internal
namespace Microsoft.AspNet.Mvc
{
public class TaskHelperTest
{

View File

@ -7,7 +7,7 @@ using Xunit;
namespace Microsoft.AspNet.Mvc
{
public class TypeExtensionTests
public class TypeExtensionsTest
{
[Theory]
[InlineData(typeof(decimal))]

View File

@ -0,0 +1,21 @@
{
"compilationOptions": {
"warningsAsErrors": "true"
},
"dependencies": {
"Microsoft.AspNet.Mvc.Common": "6.0.0-*",
"xunit.runner.kre": "1.0.0-*",
"Microsoft.AspNet.Testing": "1.0.0-*"
},
"commands": {
"test": "xunit.runner.kre"
},
"frameworks": {
"aspnet50": {
"dependencies": {
"Moq": "4.2.1312.1622"
}
},
"aspnetcore50": { }
}
}

View File

@ -272,6 +272,50 @@ namespace Microsoft.AspNet.Mvc.Razor
Assert.Empty(errors);
}
[Theory]
[InlineData("IMyService Service;", "IMyService", "Service")]
[InlineData("IMyService Service;;", "IMyService", "Service")]
[InlineData(" Microsoft.AspNet.Mvc.IHtmlHelper<MyNullableModel[]?> MyHelper; ",
"Microsoft.AspNet.Mvc.IHtmlHelper<MyNullableModel[]?>", "MyHelper")]
[InlineData(" Microsoft.AspNet.Mvc.IHtmlHelper<MyNullableModel[]?> MyHelper; ; ",
"Microsoft.AspNet.Mvc.IHtmlHelper<MyNullableModel[]?>", "MyHelper")]
[InlineData(" TestService @class; ; ", "TestService", "@class")]
[InlineData("IMyService Service ;", "IMyService", "Service")]
[InlineData("IMyService Service ; ;", "IMyService", "Service")]
[InlineData(" Microsoft.AspNet.Mvc.IHtmlHelper<MyNullableModel[]?> MyHelper ; ",
"Microsoft.AspNet.Mvc.IHtmlHelper<MyNullableModel[]?>", "MyHelper")]
[InlineData(" Microsoft.AspNet.Mvc.IHtmlHelper<MyNullableModel[]?> MyHelper ; ; ",
"Microsoft.AspNet.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))
};
// 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")]

View File

@ -79,6 +79,7 @@ namespace Microsoft.AspNet.Mvc.Razor
[InlineData("Basic")]
[InlineData("Inject")]
[InlineData("InjectWithModel")]
[InlineData("InjectWithSemicolon")]
[InlineData("Model")]
[InlineData("ModelExpressionTagHelper")]
public void MvcRazorHost_ParsesAndGeneratesCodeForBasicScenarios(string scenarioName)
@ -129,6 +130,28 @@ namespace Microsoft.AspNet.Mvc.Razor
RunDesignTimeTest(host, "InjectWithModel", expectedLineMappings);
}
[Fact]
public void InjectVisitorWithSemicolon_GeneratesCorrectLineMappings()
{
// Arrange
var host = new MvcRazorHost(new TestFileProvider())
{
DesignTimeMode = true
};
host.NamespaceImports.Clear();
var expectedLineMappings = new[]
{
BuildLineMapping(7, 0, 7, 222, 6, 7, 7),
BuildLineMapping(24, 1, 8, 729, 26, 8, 20),
BuildLineMapping(58, 2, 8, 941, 34, 8, 23),
BuildLineMapping(93, 3, 8, 1156, 42, 8, 21),
BuildLineMapping(129, 4, 8, 1369, 50, 8, 24),
};
// Act and Assert
RunDesignTimeTest(host, "InjectWithSemicolon", expectedLineMappings);
}
[Fact]
public void ModelVisitor_GeneratesCorrectLineMappings()
{

View File

@ -0,0 +1,5 @@
@model MyModel
@inject MyApp MyPropertyName;
@inject MyService<TModel> Html;
@inject MyApp MyPropertyName2 ;
@inject MyService<TModel> Html2 ;

View File

@ -0,0 +1,69 @@
namespace Asp
{
using System.Threading.Tasks;
public class ASPV_TestFiles_Input_InjectWithSemicolon_cshtml : Microsoft.AspNet.Mvc.Razor.RazorPage<
#line 1 "TestFiles/Input/InjectWithSemicolon.cshtml"
MyModel
#line default
#line hidden
>
{
private static object @__o;
private void @__RazorDesignTimeHelpers__()
{
#pragma warning disable 219
#pragma warning restore 219
}
#line hidden
public ASPV_TestFiles_Input_InjectWithSemicolon_cshtml()
{
}
#line hidden
[Microsoft.AspNet.Mvc.ActivateAttribute]
public
#line 2 "TestFiles/Input/InjectWithSemicolon.cshtml"
MyApp MyPropertyName
#line default
#line hidden
{ get; private set; }
[Microsoft.AspNet.Mvc.ActivateAttribute]
public
#line 3 "TestFiles/Input/InjectWithSemicolon.cshtml"
MyService<MyModel> Html
#line default
#line hidden
{ get; private set; }
[Microsoft.AspNet.Mvc.ActivateAttribute]
public
#line 4 "TestFiles/Input/InjectWithSemicolon.cshtml"
MyApp MyPropertyName2
#line default
#line hidden
{ get; private set; }
[Microsoft.AspNet.Mvc.ActivateAttribute]
public
#line 5 "TestFiles/Input/InjectWithSemicolon.cshtml"
MyService<MyModel> Html2
#line default
#line hidden
{ get; private set; }
[Microsoft.AspNet.Mvc.ActivateAttribute]
public Microsoft.AspNet.Mvc.IViewComponentHelper Component { get; private set; }
[Microsoft.AspNet.Mvc.ActivateAttribute]
public Microsoft.AspNet.Mvc.IUrlHelper Url { get; private set; }
#line hidden
#pragma warning disable 1998
public override async Task ExecuteAsync()
{
}
#pragma warning restore 1998
}
}

View File

@ -0,0 +1,45 @@
#pragma checksum "TestFiles/Input/InjectWithSemicolon.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "b753615982659a9805e6213ceced76ba06782038"
namespace Asp
{
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Rendering;
using System.Threading.Tasks;
public class ASPV_TestFiles_Input_InjectWithSemicolon_cshtml : Microsoft.AspNet.Mvc.Razor.RazorPage<
#line 1 "TestFiles/Input/InjectWithSemicolon.cshtml"
MyModel
#line default
#line hidden
>
{
#line hidden
public ASPV_TestFiles_Input_InjectWithSemicolon_cshtml()
{
}
#line hidden
[Microsoft.AspNet.Mvc.ActivateAttribute]
public MyApp MyPropertyName { get; private set; }
[Microsoft.AspNet.Mvc.ActivateAttribute]
public MyService<MyModel> Html { get; private set; }
[Microsoft.AspNet.Mvc.ActivateAttribute]
public MyApp MyPropertyName2 { get; private set; }
[Microsoft.AspNet.Mvc.ActivateAttribute]
public MyService<MyModel> Html2 { get; private set; }
[Microsoft.AspNet.Mvc.ActivateAttribute]
public Microsoft.AspNet.Mvc.IViewComponentHelper Component { get; private set; }
[Microsoft.AspNet.Mvc.ActivateAttribute]
public Microsoft.AspNet.Mvc.IUrlHelper Url { get; private set; }
#line hidden
#pragma warning disable 1998
public override async Task ExecuteAsync()
{
}
#pragma warning restore 1998
}
}