File: RouteEmbeddedLanguage\RoutePatternParserTests_ConformanceTests.cs
Web Access
Project: src\src\Framework\AspNetCoreAnalyzers\test\Microsoft.AspNetCore.App.Analyzers.Test.csproj (Microsoft.AspNetCore.App.Analyzers.Test)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage;
 
// These tests are mirrored from routing's RoutePatternParameterParserTest.cs
public partial class RoutePatternParserTests
{
    [Fact]
    public void Parse_SingleLiteral()
    {
        Test(@"""cool""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Literal>
        <Literal value=""cool"">cool</Literal>
      </Literal>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters />
</Tree>");
    }
 
    [Fact]
    public void Parse_SingleParameter()
    {
        Test(@"""{p}""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p"">p</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters>
    <Parameter Name=""p"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
  </Parameters>
</Tree>");
    }
 
    [Fact]
    public void Parse_OptionalParameter()
    {
        Test(@"""{p?}""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p"">p</ParameterNameToken>
        </ParameterName>
        <Optional>
          <QuestionMarkToken>?</QuestionMarkToken>
        </Optional>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters>
    <Parameter Name=""p"" IsCatchAll=""false"" IsOptional=""true"" EncodeSlashes=""true"" />
  </Parameters>
</Tree>");
    }
 
    [Fact]
    public void Parse_MultipleLiterals()
    {
        Test(@"""cool/awesome/super""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Literal>
        <Literal value=""cool"">cool</Literal>
      </Literal>
    </Segment>
    <Separator>
      <SlashToken>/</SlashToken>
    </Separator>
    <Segment>
      <Literal>
        <Literal value=""awesome"">awesome</Literal>
      </Literal>
    </Segment>
    <Separator>
      <SlashToken>/</SlashToken>
    </Separator>
    <Segment>
      <Literal>
        <Literal value=""super"">super</Literal>
      </Literal>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters />
</Tree>");
    }
 
    [Fact]
    public void Parse_MultipleParameters()
    {
        Test(@"""{p1}/{p2}/{*p3}""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p1"">p1</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <Separator>
      <SlashToken>/</SlashToken>
    </Separator>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p2"">p2</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <Separator>
      <SlashToken>/</SlashToken>
    </Separator>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <CatchAll>
          <AsteriskToken>*</AsteriskToken>
        </CatchAll>
        <ParameterName>
          <ParameterNameToken value=""p3"">p3</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters>
    <Parameter Name=""p1"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
    <Parameter Name=""p2"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
    <Parameter Name=""p3"" IsCatchAll=""true"" IsOptional=""false"" EncodeSlashes=""true"" />
  </Parameters>
</Tree>");
    }
 
    [Fact]
    public void Parse_ComplexSegment_LP()
    {
        Test(@"""cool-{p1}""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Literal>
        <Literal value=""cool-"">cool-</Literal>
      </Literal>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p1"">p1</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters>
    <Parameter Name=""p1"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
  </Parameters>
</Tree>");
    }
 
    [Fact]
    public void Parse_ComplexSegment_PL()
    {
        Test(@"""{p1}-cool""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p1"">p1</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
      <Literal>
        <Literal value=""-cool"">-cool</Literal>
      </Literal>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters>
    <Parameter Name=""p1"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
  </Parameters>
</Tree>");
    }
 
    [Fact]
    public void Parse_ComplexSegment_PLP()
    {
        Test(@"""{p1}-cool-{p2}""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p1"">p1</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
      <Literal>
        <Literal value=""-cool-"">-cool-</Literal>
      </Literal>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p2"">p2</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters>
    <Parameter Name=""p1"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
    <Parameter Name=""p2"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
  </Parameters>
</Tree>");
    }
 
    [Fact]
    public void Parse_ComplexSegment_LPL()
    {
        Test(@"""cool-{p1}-awesome""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Literal>
        <Literal value=""cool-"">cool-</Literal>
      </Literal>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p1"">p1</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
      <Literal>
        <Literal value=""-awesome"">-awesome</Literal>
      </Literal>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters>
    <Parameter Name=""p1"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
  </Parameters>
</Tree>");
    }
 
    [Fact]
    public void Parse_ComplexSegment_OptionalParameterFollowingPeriod()
    {
        Test(@"""{p1}.{p2?}""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p1"">p1</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
      <Literal>
        <Literal value=""."">.</Literal>
      </Literal>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p2"">p2</ParameterNameToken>
        </ParameterName>
        <Optional>
          <QuestionMarkToken>?</QuestionMarkToken>
        </Optional>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters>
    <Parameter Name=""p1"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
    <Parameter Name=""p2"" IsCatchAll=""false"" IsOptional=""true"" EncodeSlashes=""true"" />
  </Parameters>
</Tree>");
    }
 
    [Fact]
    public void Parse_ComplexSegment_ParametersFollowingPeriod()
    {
        Test(@"""{p1}.{p2}""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p1"">p1</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
      <Literal>
        <Literal value=""."">.</Literal>
      </Literal>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p2"">p2</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters>
    <Parameter Name=""p1"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
    <Parameter Name=""p2"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
  </Parameters>
</Tree>");
    }
 
    [Fact]
    public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_ThreeParameters()
    {
        Test(@"""{p1}.{p2}.{p3?}""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p1"">p1</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
      <Literal>
        <Literal value=""."">.</Literal>
      </Literal>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p2"">p2</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
      <Literal>
        <Literal value=""."">.</Literal>
      </Literal>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p3"">p3</ParameterNameToken>
        </ParameterName>
        <Optional>
          <QuestionMarkToken>?</QuestionMarkToken>
        </Optional>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters>
    <Parameter Name=""p1"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
    <Parameter Name=""p2"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
    <Parameter Name=""p3"" IsCatchAll=""false"" IsOptional=""true"" EncodeSlashes=""true"" />
  </Parameters>
</Tree>");
    }
 
    [Fact]
    public void Parse_ComplexSegment_ThreeParametersSeparatedByPeriod()
    {
        Test(@"""{p1}.{p2}.{p3}""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p1"">p1</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
      <Literal>
        <Literal value=""."">.</Literal>
      </Literal>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p2"">p2</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
      <Literal>
        <Literal value=""."">.</Literal>
      </Literal>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p3"">p3</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters>
    <Parameter Name=""p1"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
    <Parameter Name=""p2"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
    <Parameter Name=""p3"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
  </Parameters>
</Tree>");
    }
 
    [Fact]
    public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_MiddleSegment()
    {
        Test(@"""{p1}.{p2?}/{p3}""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p1"">p1</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
      <Literal>
        <Literal value=""."">.</Literal>
      </Literal>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p2"">p2</ParameterNameToken>
        </ParameterName>
        <Optional>
          <QuestionMarkToken>?</QuestionMarkToken>
        </Optional>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <Separator>
      <SlashToken>/</SlashToken>
    </Separator>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p3"">p3</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters>
    <Parameter Name=""p1"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
    <Parameter Name=""p2"" IsCatchAll=""false"" IsOptional=""true"" EncodeSlashes=""true"" />
    <Parameter Name=""p3"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
  </Parameters>
</Tree>");
    }
 
    [Fact]
    public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_LastSegment()
    {
        Test(@"""{p1}/{p2}.{p3?}""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p1"">p1</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <Separator>
      <SlashToken>/</SlashToken>
    </Separator>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p2"">p2</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
      <Literal>
        <Literal value=""."">.</Literal>
      </Literal>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p3"">p3</ParameterNameToken>
        </ParameterName>
        <Optional>
          <QuestionMarkToken>?</QuestionMarkToken>
        </Optional>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters>
    <Parameter Name=""p1"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
    <Parameter Name=""p2"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
    <Parameter Name=""p3"" IsCatchAll=""false"" IsOptional=""true"" EncodeSlashes=""true"" />
  </Parameters>
</Tree>");
    }
 
    [Fact]
    public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_PeriodAfterSlash()
    {
        Test(@"""{p2}/.{p3?}""", @"<Tree>
  <CompilationUnit>
    <Segment>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p2"">p2</ParameterNameToken>
        </ParameterName>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <Separator>
      <SlashToken>/</SlashToken>
    </Separator>
    <Segment>
      <Literal>
        <Literal value=""."">.</Literal>
      </Literal>
      <Parameter>
        <OpenBraceToken>{</OpenBraceToken>
        <ParameterName>
          <ParameterNameToken value=""p3"">p3</ParameterNameToken>
        </ParameterName>
        <Optional>
          <QuestionMarkToken>?</QuestionMarkToken>
        </Optional>
        <CloseBraceToken>}</CloseBraceToken>
      </Parameter>
    </Segment>
    <EndOfFile />
  </CompilationUnit>
  <Parameters>
    <Parameter Name=""p2"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" />
    <Parameter Name=""p3"" IsCatchAll=""false"" IsOptional=""true"" EncodeSlashes=""true"" />
  </Parameters>
</Tree>");
    }
 
    [Theory]
    [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", @"regex(^\d{3}-\d{3}-\d{4}$)")] // ssn
    [InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)}", @"regex(^\d{1,2}\/\d{1,2}\/\d{4}$)")] // date
    [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", @"regex(^\w+\@\w+\.\w+)")] // email
    [InlineData(@"{p1:regex(([}}])\w+)}", @"regex(([}])\w+)")] // Not balanced }
    [InlineData(@"{p1:regex(([{{(])\w+)}", @"regex(([{(])\w+)")] // Not balanced {
    public void Parse_RegularExpressions(string template, string constraint)
    {
        var tree = Test(@"""" + template.Replace(@"\", @"\\") + @"""");
        var parameter = tree.GetRouteParameter("p1");
        Assert.Collection(parameter.Policies, p => Assert.Equal(":" + constraint.Replace("{", "{{").Replace("}", "}}"), p));
    }
 
    [Theory]
    [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}}$)}")] // extra }
    [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}}")] // extra } at the end
    [InlineData(@"{{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}")] // extra { at the beginning
    [InlineData(@"{p1:regex(([}])\w+}")] // Not escaped }
    [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}$)}")] // Not escaped }
    [InlineData(@"{p1:regex(abc)")]
    public void Parse_RegularExpressions_Invalid(string template)
    {
        var tree = Test(@"""" + template.Replace(@"\", @"\\") + @"""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.TemplateRoute_MismatchedParameter, p.Message));
    }
 
    [Theory]
    [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{{4}}$)}")] // extra {
    [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{4}}$)}")] // Not escaped {
    public void Parse_RegularExpressions_Unescaped(string template)
    {
        var tree = Test(@"""" + template.Replace(@"\", @"\\") + @"""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.TemplateRoute_UnescapedBrace, p.Message));
    }
 
    [Theory]
    [InlineData("{p1}.{p2?}.{p3}", "p2", ".")]
    [InlineData("{p1?}{p2}", "p1", "{p2}")]
    [InlineData("{p1?}{p2?}", "p1", "{p2?}")]
    [InlineData("{p1}.{p2?})", "p2", ")")]
    [InlineData("{foorb?}-bar-{z}", "foorb", "-bar-")]
    public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart(
        string template,
        string parameter,
        string invalid)
    {
        var tree = Test(@"""" + template.Replace(@"\", @"\\") + @"""");
        // Use contains because other diagnostics can be recorded.
        Assert.Contains(tree.Diagnostics, p => p.Message == Resources.FormatTemplateRoute_OptionalParameterHasTobeTheLast(template, parameter, invalid));
    }
 
    [Theory]
    [InlineData("{p1}-{p2?}", "-")]
    [InlineData("{p1}..{p2?}", "..")]
    [InlineData("..{p2?}", "..")]
    [InlineData("{p1}.abc.{p2?}", ".abc.")]
    [InlineData("{p1}{p2?}", "{p1}")]
    public void Parse_ComplexSegment_OptionalParametersSeparatedByPeriod_Invalid(string template, string parameter)
    {
        var tree = Test(@"""" + template.Replace(@"\", @"\\") + @"""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.FormatTemplateRoute_OptionalParameterCanbBePrecededByPeriod(template, "p2", parameter), p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_WithRepeatedParameter()
    {
        var tree = Test(@"""{Controller}.mvc/{id}/{controller}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.FormatTemplateRoute_RepeatedParameter("controller"), p.Message));
    }
 
    [Theory]
    [InlineData("123{a}abc{")]
    [InlineData("123{a}abc}")]
    [InlineData("xyz}123{a}abc}")]
    [InlineData("{{p1}")]
    [InlineData("{p1}}")]
    [InlineData("p1}}p2{")]
    public void InvalidTemplate_WithMismatchedBraces(string template)
    {
        var tree = Test(@"""" + template.Replace(@"\", @"\\") + @"""");
        // Use contains because other diagnostics can be recorded.
        Assert.Contains(tree.Diagnostics, p => p.Message == Resources.TemplateRoute_MismatchedParameter);
    }
 
    [Fact]
    public void InvalidTemplate_CannotHaveCatchAllInMultiSegment()
    {
        var tree = Test(@"""123{a}abc{*moo}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment, p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_CannotHaveMoreThanOneCatchAll()
    {
        var tree = Test(@"""{*p1}/{*p2}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.TemplateRoute_CatchAllMustBeLast, p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment()
    {
        var tree = Test(@"""{*p1}abc{*p2}""");
        Assert.Collection(
            tree.Diagnostics,
            p => Assert.Equal(Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment, p.Message),
            p => Assert.Equal(Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment, p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_CannotHaveCatchAllWithNoName()
    {
        var tree = Test(@"""foo/{*}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.FormatTemplateRoute_InvalidParameterName(""), p.Message));
    }
 
    [Theory]
    [InlineData("{a*}", "a*")]
    [InlineData("{*a*}", "a*")]
    [InlineData("{*a*:int}", "a*")]
    [InlineData("{*a*=5}", "a*")]
    [InlineData("{*a*b=5}", "a*b")]
    [InlineData("{p1?}.{p2/}/{p3}", "p2/")]
    [InlineData("{p{{}", "p{")]
    [InlineData("{p}}}", "p}")]
    [InlineData("{p/}", "p/")]
    public void ParseRouteParameter_ThrowsIf_ParameterContainsSpecialCharacters(
        string template,
        string parameterName)
    {
        var tree = Test(@"""" + template.Replace(@"\", @"\\") + @"""");
        // Use contains because other diagnostics can be recorded.
        Assert.Contains(tree.Diagnostics, p => p.Message == Resources.FormatTemplateRoute_InvalidParameterName(parameterName));
    }
 
    [Fact]
    public void InvalidTemplate_CannotHaveConsecutiveOpenBrace()
    {
        var tree = Test(@"""foo/{{p1}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.TemplateRoute_MismatchedParameter, p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_CannotHaveConsecutiveCloseBrace()
    {
        var tree = Test(@"""foo/{p1}}""");
        // Use contains because other diagnostics can be recorded.
        Assert.Contains(tree.Diagnostics, p => p.Message == Resources.TemplateRoute_MismatchedParameter);
    }
 
    [Fact]
    public void InvalidTemplate_SameParameterTwiceThrows()
    {
        var tree = Test(@"""{aaa}/{AAA}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.FormatTemplateRoute_RepeatedParameter("AAA"), p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows()
    {
        var tree = Test(@"""{aaa}/{*AAA}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.FormatTemplateRoute_RepeatedParameter("AAA"), p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows()
    {
        var tree = Test(@"""{a}/{aa}a}/{z}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.TemplateRoute_MismatchedParameter, p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows()
    {
        var tree = Test(@"""{a}/{a{aa}/{z}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.TemplateRoute_UnescapedBrace, p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows()
    {
        var tree = Test(@"""{a}/{}/{z}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.FormatTemplateRoute_InvalidParameterName(""), p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_InvalidParameterNameWithQuestionThrows()
    {
        var tree = Test(@"""{Controller}.mvc/{?}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.FormatTemplateRoute_InvalidParameterName(""), p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows()
    {
        var tree = Test(@"""{a}//{z}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.TemplateRoute_CannotHaveConsecutiveSeparators, p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_WithCatchAllNotAtTheEndThrows()
    {
        var tree = Test(@"""foo/{p1}/{*p2}/{p3}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.TemplateRoute_CatchAllMustBeLast, p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_RepeatedParametersThrows()
    {
        var tree = Test(@"""foo/aa{p1}{p2}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.TemplateRoute_CannotHaveConsecutiveParameters, p.Message));
    }
 
    [Theory]
    [InlineData("/foo")]
    [InlineData("~/foo")]
    public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routePattern)
    {
        var tree = Test(@"""" + routePattern.Replace(@"\", @"\\") + @"""");
        Assert.Empty(tree.Diagnostics);
    }
 
    [Fact]
    public void InvalidTemplate_CannotStartWithTilde()
    {
        var tree = Test(@"""~foo""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.TemplateRoute_InvalidRouteTemplate, p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_CannotContainQuestionMark()
    {
        var tree = Test(@"""foor?bar""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.FormatTemplateRoute_InvalidLiteral("foor?bar"), p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd()
    {
        var tree = Test(@"""{foor?b}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.FormatTemplateRoute_InvalidParameterName("foor?b"), p.Message));
    }
 
    [Fact]
    public void InvalidTemplate_CatchAllMarkedOptional()
    {
        var tree = Test(@"""{a}/{*b?}""");
        Assert.Collection(tree.Diagnostics, p => Assert.Equal(Resources.TemplateRoute_CatchAllCannotBeOptional, p.Message));
    }
 
    [Theory]
    [InlineData("{id}", new[] { "id" }, new[] { "" })]
    [InlineData("{category}/product/{group}", new[] { "category", "group" }, new[] { "", "" })]
    [InlineData("{category:int}/product/{group:range(10, 20)}?", new[] { "category", "group" }, new[] { ":int", ":range(10, 20)" })]
    [InlineData("{person:int}/{ssn:regex(^\\\\d{{3}}-\\\\d{{2}}-\\\\d{{4}}$)}", new[] { "person", "ssn" }, new[] { ":int", ":regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)" })]
    [InlineData("{area=Home}/{controller:required}/{id:int=0}", new[] { "area", "controller", "id" }, new[] { "=Home", ":required", ":int=0" })]
    [InlineData("{category}/product/{group?}", new[] { "category", "group" }, new[] { "", "?" })]
    [InlineData("{category}/{product}/{*sku}", new[] { "category", "product", "sku" }, new[] { "", "", "" })]
    [InlineData("{category}-product-{sku}", new[] { "category", "sku" }, new[] { "", "" })]
    [InlineData("category-{product}-sku", new[] { "product" }, new[] { "" })]
    [InlineData("{category}.{sku?}", new[] { "category", "sku" }, new[] { "", "?" })]
    [InlineData("{category}.{product?}/{sku}", new[] { "category", "product", "sku" }, new[] { "", "?", "" })]
    public void RouteTokenizer_Works_ForSimpleRouteTemplates(string template, string[] expectedNames, string[] expectedQualifiers)
    {
        var tree = Test(@"""" + template + @"""", runSubTreeTests: false);
 
        Assert.Equal(expectedNames.Length, tree.RouteParameters.Length);
        Assert.Equal(expectedQualifiers.Length, tree.RouteParameters.Length);
 
        for (var i = 0; i < expectedNames.Length; i++)
        {
            var expectedName = expectedNames[i];
            var expectedQualifier = expectedQualifiers[i];
 
            if (!tree.TryGetRouteParameter(expectedName, out var routeParameter))
            {
                throw new Exception($"Couldn't find expected route parameter: {expectedName}");
            }
 
            var qualifier = string.Join(string.Empty, routeParameter.Policies);
            if (routeParameter.DefaultValue != null)
            {
                qualifier += "=" + routeParameter.DefaultValue;
            }
            if (routeParameter.IsOptional)
            {
                qualifier += "?";
            }
 
            Assert.Equal(expectedQualifier, qualifier);
        }
    }
}