From 6df6573d746f4a3b12cea51af9a15bfab9e0743a Mon Sep 17 00:00:00 2001 From: James Newton-King <james@newtonking.com> Date: Wed, 27 Jul 2022 14:17:05 +0800 Subject: [PATCH] Revert "Route tooling (#42597)" (#42943) --- NuGet.config | 3 - eng/Dependencies.props | 1 - eng/Versions.props | 7 +- .../src/DiagnosticProject.cs | 34 +- .../src/Analyzers/DiagnosticDescriptors.cs | 9 - .../Microsoft.AspNetCore.App.Analyzers.csproj | 2 +- .../src/Analyzers/Resources.resx | 186 --- .../IRoutePatternNodeVisitor.cs | 24 - .../EmbeddedSyntax/EmbeddedDiagnostic.cs | 50 - .../EmbeddedSeparatedSyntaxNodeList.cs | 104 -- .../EmbeddedSyntax/EmbeddedSyntaxHelpers.cs | 34 - .../EmbeddedSyntax/EmbeddedSyntaxNode.cs | 210 --- .../EmbeddedSyntaxNodeOrToken.cs | 50 - .../EmbeddedSyntax/EmbeddedSyntaxToken.cs | 88 - .../EmbeddedSyntax/EmbeddedSyntaxTree.cs | 27 - .../EmbeddedSyntax/EmbeddedSyntaxTrivia.cs | 38 - .../Infrastructure/EmbeddedSyntax/README.md | 5 - .../Infrastructure/MvcDetector.cs | 107 -- .../RoutePatternParametersDetector.cs | 94 -- .../RoutePatternUsageDetector.cs | 218 --- .../RouteStringSyntaxDetector.cs | 431 ----- .../RouteStringSyntaxDetectorDocument.cs | 39 - .../Infrastructure/SymbolExtensions.cs | 60 - .../Infrastructure/SyntaxTokenExtensions.cs | 79 - .../AbstractVirtualCharService.ITextInfo.cs | 31 - .../AbstractVirtualCharService.cs | 245 --- .../VirtualChars/CSharpVirtualCharService.cs | 544 ------- .../VirtualChars/IVirtualCharService.cs | 72 - .../Infrastructure/VirtualChars/README.md | 5 - .../VirtualChars/RuneExtensions.cs | 44 - .../VirtualChars/System.Text/Rune.cs | 1439 ----------------- .../VirtualChars/System.Text/UnicodeDebug.cs | 77 - .../System.Text/UnicodeUtility.cs | 196 --- .../VirtualChars/System.Text/Utf16Utility.cs | 225 --- .../VirtualChars/TextLineExtensions.cs | 26 - .../VirtualChars/VirtualChar.cs | 195 --- .../VirtualCharSequence.Chunks.cs | 163 -- .../VirtualCharSequence.Enumerator.cs | 32 - .../VirtualChars/VirtualCharSequence.cs | 231 --- .../Analyzers/RouteEmbeddedLanguage/README.md | 46 - .../RoutePattern/RoutePatternHelpers.cs | 19 - .../RoutePattern/RoutePatternKind.cs | 63 - .../RoutePattern/RoutePatternLexer.cs | 506 ------ .../RoutePattern/RoutePatternNode.cs | 16 - .../RoutePattern/RoutePatternNodes.cs | 418 ----- .../RoutePattern/RoutePatternParser.cs | 617 ------- .../RoutePattern/RoutePatternTree.cs | 48 - .../RoutePatternAnalyzer.cs | 90 -- .../RoutePatternBraceMatcher.cs | 120 -- .../RoutePatternClassifier.cs | 163 -- .../RoutePatternCompletionProvider.cs | 385 ----- .../RoutePatternHighlighter.cs | 142 -- .../RouteEmbeddedLanguage/WellKnownTypes.cs | 192 --- ...tectMismatchedParameterOptionalityFixer.cs | 6 +- ...osoft.AspNetCore.App.Analyzers.Test.csproj | 8 - .../Experiment/Controllers/TodoController.cs | 34 - .../Experiment/DbContext.cs | 28 - .../Experiment/Program.cs | 33 - .../EmbeddedLanguagesTestConstants.cs | 35 - .../ExportProviderExtensions.cs | 92 -- .../Infrastructure/FormattedClassification.cs | 109 -- .../FormattedClassifications.Regex.cs | 42 - .../FormattedClassifications.cs | 208 --- .../Infrastructure/MarkupTestFile.cs | 263 --- .../RoutePatternAnalyzerTests.cs | 262 --- .../RoutePatternBraceMatcherTests.cs | 205 --- .../RoutePatternClassifierTests.cs | 135 -- .../RoutePatternCompletionProviderTests.cs | 390 ----- .../RoutePatternHighlighterTests.cs | 406 ----- .../RoutePatternParserTests.cs | 393 ----- .../RoutePatternParserTests_BasicTests.cs | 1377 ---------------- ...outePatternParserTests_ConformanceTests.cs | 818 ---------- ...outePatternParserTests_ReplacementTests.cs | 503 ------ .../test/TestDiagnosticAnalyzer.cs | 93 +- src/Framework/Framework.slnf | 7 +- .../ApplicationModels/AttributeRouteModel.cs | 2 +- 76 files changed, 14 insertions(+), 13685 deletions(-) delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/IRoutePatternNodeVisitor.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedDiagnostic.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSeparatedSyntaxNodeList.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxHelpers.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxNode.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxNodeOrToken.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxToken.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxTree.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxTrivia.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/README.md delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/MvcDetector.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternUsageDetector.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteStringSyntaxDetector.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteStringSyntaxDetectorDocument.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SymbolExtensions.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SyntaxTokenExtensions.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/AbstractVirtualCharService.ITextInfo.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/AbstractVirtualCharService.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/CSharpVirtualCharService.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/IVirtualCharService.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/README.md delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/RuneExtensions.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/Rune.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/UnicodeDebug.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/UnicodeUtility.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/Utf16Utility.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/TextLineExtensions.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualChar.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualCharSequence.Chunks.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualCharSequence.Enumerator.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualCharSequence.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/README.md delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternHelpers.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternKind.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternLexer.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternNode.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternNodes.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternParser.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternTree.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternAnalyzer.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternBraceMatcher.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternClassifier.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternCompletionProvider.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternHighlighter.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/WellKnownTypes.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Experiment/Controllers/TodoController.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Experiment/DbContext.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Experiment/Program.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/EmbeddedLanguagesTestConstants.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/ExportProviderExtensions.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/FormattedClassification.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/FormattedClassifications.Regex.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/FormattedClassifications.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/MarkupTestFile.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternAnalyzerTests.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternBraceMatcherTests.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternClassifierTests.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternCompletionProviderTests.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternHighlighterTests.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests_BasicTests.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests_ConformanceTests.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests_ReplacementTests.cs diff --git a/NuGet.config b/NuGet.config index 6b3cb80f80cd..d27ff9ced1e4 100644 --- a/NuGet.config +++ b/NuGet.config @@ -16,9 +16,6 @@ <add key="dotnet31-transport" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet3.1-transport/nuget/v3/index.json" /> <!-- Used for the Rich Navigation indexing task --> <add key="richnav" value="https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-buildservices/nuget/v3/index.json" /> - <!-- Preview VS bits for route tooling. Will be removed in future when packages referenced by Roslyn move out of preview. --> - <add key="vssdk" value="https://pkgs.dev.azure.com/azure-public/vside/_packaging/vssdk/nuget/v3/index.json" /> - <add key="vs-impl" value="https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json" /> </packageSources> <disabledPackageSources> <clear /> diff --git a/eng/Dependencies.props b/eng/Dependencies.props index aaf3b072d63f..c1b35e485328 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -21,7 +21,6 @@ and are generated based on the last package release. <LatestPackageReference Include="Microsoft.CodeAnalysis.Common" /> <LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" /> <LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp" /> - <LatestPackageReference Include="Microsoft.CodeAnalysis.ExternalAccess.AspNetCore" /> <LatestPackageReference Include="Microsoft.CodeAnalysis.Razor" /> <LatestPackageReference Include="Microsoft.CSharp" /> <LatestPackageReference Include="Microsoft.Extensions.Caching.Abstractions" /> diff --git a/eng/Versions.props b/eng/Versions.props index afd4bb3a5153..fdb5b8d0550f 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -208,10 +208,9 @@ --> <Analyzer_MicrosoftCodeAnalysisCSharpVersion>3.3.1</Analyzer_MicrosoftCodeAnalysisCSharpVersion> <Analyzer_MicrosoftCodeAnalysisCSharpWorkspacesVersion>3.3.1</Analyzer_MicrosoftCodeAnalysisCSharpWorkspacesVersion> - <MicrosoftCodeAnalysisExternalAccessAspNetCoreVersion>4.3.0-3.22329.30</MicrosoftCodeAnalysisExternalAccessAspNetCoreVersion> - <MicrosoftCodeAnalysisCommonVersion>4.3.0-3.22329.30</MicrosoftCodeAnalysisCommonVersion> - <MicrosoftCodeAnalysisCSharpVersion>4.3.0-3.22329.30</MicrosoftCodeAnalysisCSharpVersion> - <MicrosoftCodeAnalysisCSharpWorkspacesVersion>4.3.0-3.22329.30</MicrosoftCodeAnalysisCSharpWorkspacesVersion> + <MicrosoftCodeAnalysisCommonVersion>4.2.0-2.22128.1</MicrosoftCodeAnalysisCommonVersion> + <MicrosoftCodeAnalysisCSharpVersion>4.2.0-2.22128.1</MicrosoftCodeAnalysisCSharpVersion> + <MicrosoftCodeAnalysisCSharpWorkspacesVersion>4.2.0-2.22128.1</MicrosoftCodeAnalysisCSharpWorkspacesVersion> <MicrosoftCodeAnalysisPublicApiAnalyzersVersion>3.3.3</MicrosoftCodeAnalysisPublicApiAnalyzersVersion> <MicrosoftCodeAnalysisCSharpAnalyzerTestingXUnitVersion>1.1.2-beta1.22276.1</MicrosoftCodeAnalysisCSharpAnalyzerTestingXUnitVersion> <MicrosoftCodeAnalysisCSharpCodeFixTestingXUnitVersion>1.1.2-beta1.22276.1</MicrosoftCodeAnalysisCSharpCodeFixTestingXUnitVersion> diff --git a/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticProject.cs b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticProject.cs index 381e38557ccc..5c431fc5deac 100644 --- a/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticProject.cs +++ b/src/Analyzers/Microsoft.AspNetCore.Analyzer.Testing/src/DiagnosticProject.cs @@ -1,12 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.DependencyModel; using Microsoft.Extensions.DependencyModel.Resolution; @@ -28,17 +26,15 @@ public class DiagnosticProject private static readonly ICompilationAssemblyResolver _assemblyResolver = new AppBaseCompilationAssemblyResolver(); private static readonly Dictionary<Assembly, Solution> _solutionCache = new Dictionary<Assembly, Solution>(); - public static Project Create(Assembly testAssembly, string[] sources, Func<Workspace> workspaceFactory = null, Type analyzerReference = null) + public static Project Create(Assembly testAssembly, string[] sources) { Solution solution; lock (_solutionCache) { if (!_solutionCache.TryGetValue(testAssembly, out solution)) { - workspaceFactory ??= CreateWorkspace; - var projectId = ProjectId.CreateNewId(debugName: TestProjectName); - solution = workspaceFactory() + solution = new AdhocWorkspace() .CurrentSolution .AddProject(projectId, TestProjectName, TestProjectName, LanguageNames.CSharp); @@ -50,13 +46,6 @@ public static Project Create(Assembly testAssembly, string[] sources, Func<Works } } - if (analyzerReference != null) - { - solution = solution.AddAnalyzerReference( - projectId, - new AnalyzerFileReference(analyzerReference.Assembly.Location, AssemblyLoader.Instance)); - } - _solutionCache.Add(testAssembly, solution); } } @@ -79,23 +68,4 @@ public static Project Create(Assembly testAssembly, string[] sources, Func<Works return solution.GetProject(testProject); } - - private static Workspace CreateWorkspace() - { - return new AdhocWorkspace(); - } - - internal sealed class AssemblyLoader : IAnalyzerAssemblyLoader - { - public static AssemblyLoader Instance = new AssemblyLoader(); - - public void AddDependencyLocation(string fullPath) - { - } - - public Assembly LoadFromPath(string fullPath) - { - return Assembly.LoadFrom(fullPath); - } - } } diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs index 5fe68533312c..08f0860ad7be 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs @@ -106,13 +106,4 @@ internal static class DiagnosticDescriptors DiagnosticSeverity.Warning, isEnabledByDefault: true, helpLinkUri: "https://aka.ms/aspnet/analyzers"); - - internal static readonly DiagnosticDescriptor RoutePatternIssue = new( - "RP0001", - new LocalizableResourceString(nameof(Resources.Invalid_Route_pattern), Resources.ResourceManager, typeof(Resources)), - new LocalizableResourceString(nameof(Resources.Route_issue_0), Resources.ResourceManager, typeof(Resources)), - "Style", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - helpLinkUri: "https://aka.ms/aspnet/analyzers"); } diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj index 1e450bc91e7b..12662dd9bc7d 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj @@ -5,12 +5,12 @@ <AddPublicApiAnalyzers>false</AddPublicApiAnalyzers> <TargetFramework>netstandard2.0</TargetFramework> <IncludeBuildOutput>false</IncludeBuildOutput> + <Nullable>Enable</Nullable> <RootNamespace>Microsoft.AspNetCore.Analyzers</RootNamespace> </PropertyGroup> <ItemGroup> <Reference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="All" /> - <Reference Include="Microsoft.CodeAnalysis.ExternalAccess.AspNetCore" /> <InternalsVisibleTo Include="Microsoft.AspNetCore.App.Analyzers.Test" /> <InternalsVisibleTo Include="Microsoft.AspNetCore.App.CodeFixes" /> diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx deleted file mode 100644 index 355978355922..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx +++ /dev/null @@ -1,186 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<root> - <!-- - Microsoft ResX Schema - - Version 2.0 - - The primary goals of this format is to allow a simple XML format - that is mostly human readable. The generation and parsing of the - various data types are done through the TypeConverter classes - associated with the data types. - - Example: - - ... ado.net/XML headers & schema ... - <resheader name="resmimetype">text/microsoft-resx</resheader> - <resheader name="version">2.0</resheader> - <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> - <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> - <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> - <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> - <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> - <value>[base64 mime encoded serialized .NET Framework object]</value> - </data> - <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> - <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> - <comment>This is a comment</comment> - </data> - - There are any number of "resheader" rows that contain simple - name/value pairs. - - Each data row contains a name, and value. The row also contains a - type or mimetype. Type corresponds to a .NET class that support - text/value conversion through the TypeConverter architecture. - Classes that don't support this are serialized and stored with the - mimetype set. - - The mimetype is used for serialized objects, and tells the - ResXResourceReader how to depersist the object. This is currently not - extensible. For a given mimetype the value must be set accordingly: - - Note - application/x-microsoft.net.object.binary.base64 is the format - that the ResXResourceWriter will generate, however the reader can - read any of the formats listed below. - - mimetype: application/x-microsoft.net.object.binary.base64 - value : The object must be serialized with - : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter - : and then encoded with base64 encoding. - - mimetype: application/x-microsoft.net.object.soap.base64 - value : The object must be serialized with - : System.Runtime.Serialization.Formatters.Soap.SoapFormatter - : and then encoded with base64 encoding. - - mimetype: application/x-microsoft.net.object.bytearray.base64 - value : The object must be serialized into a byte array - : using a System.ComponentModel.TypeConverter - : and then encoded with base64 encoding. - --> - <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> - <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> - <xsd:element name="root" msdata:IsDataSet="true"> - <xsd:complexType> - <xsd:choice maxOccurs="unbounded"> - <xsd:element name="metadata"> - <xsd:complexType> - <xsd:sequence> - <xsd:element name="value" type="xsd:string" minOccurs="0" /> - </xsd:sequence> - <xsd:attribute name="name" use="required" type="xsd:string" /> - <xsd:attribute name="type" type="xsd:string" /> - <xsd:attribute name="mimetype" type="xsd:string" /> - <xsd:attribute ref="xml:space" /> - </xsd:complexType> - </xsd:element> - <xsd:element name="assembly"> - <xsd:complexType> - <xsd:attribute name="alias" type="xsd:string" /> - <xsd:attribute name="name" type="xsd:string" /> - </xsd:complexType> - </xsd:element> - <xsd:element name="data"> - <xsd:complexType> - <xsd:sequence> - <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> - <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> - </xsd:sequence> - <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> - <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> - <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> - <xsd:attribute ref="xml:space" /> - </xsd:complexType> - </xsd:element> - <xsd:element name="resheader"> - <xsd:complexType> - <xsd:sequence> - <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> - </xsd:sequence> - <xsd:attribute name="name" type="xsd:string" use="required" /> - </xsd:complexType> - </xsd:element> - </xsd:choice> - </xsd:complexType> - </xsd:element> - </xsd:schema> - <resheader name="resmimetype"> - <value>text/microsoft-resx</value> - </resheader> - <resheader name="version"> - <value>2.0</value> - </resheader> - <resheader name="reader"> - <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> - </resheader> - <resheader name="writer"> - <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> - </resheader> - <data name="TemplateRoute_CannotHaveCatchAllInMultiSegment" xml:space="preserve"> - <value>A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.</value> - </data> - <data name="TemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly" xml:space="preserve"> - <value>The route parameter '{0}' has both an inline default value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them.</value> - </data> - <data name="TemplateRoute_CannotHaveConsecutiveParameters" xml:space="preserve"> - <value>A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.</value> - </data> - <data name="TemplateRoute_CannotHaveConsecutiveSeparators" xml:space="preserve"> - <value>The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.</value> - </data> - <data name="TemplateRoute_CatchAllCannotBeOptional" xml:space="preserve"> - <value>A catch-all parameter cannot be marked optional.</value> - </data> - <data name="TemplateRoute_OptionalCannotHaveDefaultValue" xml:space="preserve"> - <value>An optional parameter cannot have default value.</value> - </data> - <data name="TemplateRoute_CatchAllMustBeLast" xml:space="preserve"> - <value>A catch-all parameter can only appear as the last segment of the route template.</value> - </data> - <data name="TemplateRoute_InvalidLiteral" xml:space="preserve"> - <value>The literal section '{0}' is invalid. Literal sections cannot contain the '?' character.</value> - </data> - <data name="TemplateRoute_InvalidParameterName" xml:space="preserve"> - <value>The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{{', '}}', '/'. The '?' character marks a parameter as optional, and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all, and can occur only at the start of the parameter.</value> - </data> - <data name="TemplateRoute_InvalidRouteTemplate" xml:space="preserve"> - <value>The route template cannot start with a '~' character unless followed by a '/'.</value> - </data> - <data name="TemplateRoute_MismatchedParameter" xml:space="preserve"> - <value>There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.</value> - </data> - <data name="TemplateRoute_RepeatedParameter" xml:space="preserve"> - <value>The route parameter name '{0}' appears more than one time in the route template.</value> - </data> - <data name="TemplateRoute_UnescapedBrace" xml:space="preserve"> - <value>In a route parameter, '{' and '}' must be escaped with '{{' and '}}'.</value> - </data> - <data name="TemplateRoute_OptionalParameterCanbBePrecededByPeriod" xml:space="preserve"> - <value>In the segment '{0}', the optional parameter '{1}' is preceded by an invalid segment '{2}'. Only a period (.) can precede an optional parameter.</value> - </data> - <data name="TemplateRoute_OptionalParameterHasTobeTheLast" xml:space="preserve"> - <value>An optional parameter must be at the end of the segment. In the segment '{0}', optional parameter '{1}' is followed by '{2}'.</value> - </data> - <data name="TemplateRoute_Exception" xml:space="preserve"> - <value>An error occurred while creating the route with name '{0}' and template '{1}'.</value> - </data> - <data name="Invalid_Route_pattern" xml:space="preserve"> - <value>Invalid route pattern</value> - </data> - <data name="Route_issue_0" xml:space="preserve"> - <value>Route issue: {0}</value> - </data> - <data name="AttributeRoute_TokenReplacement_EmptyTokenNotAllowed" xml:space="preserve"> - <value>An empty replacement token ('[]') is not allowed.</value> - </data> - <data name="AttributeRoute_TokenReplacement_ImbalancedSquareBrackets" xml:space="preserve"> - <value>Token delimiters ('[', ']') are imbalanced.</value> - </data> - <data name="AttributeRoute_TokenReplacement_UnclosedToken" xml:space="preserve"> - <value>A replacement token is not closed.</value> - </data> - <data name="AttributeRoute_TokenReplacement_UnescapedBraceInToken" xml:space="preserve"> - <value>An unescaped '[' token is not allowed inside of a replacement token. Use '[[' to escape.</value> - </data> -</root> diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/IRoutePatternNodeVisitor.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/IRoutePatternNodeVisitor.cs deleted file mode 100644 index 17002540a076..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/IRoutePatternNodeVisitor.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -internal interface IRoutePatternNodeVisitor -{ - void Visit(RoutePatternCompilationUnit node); - void Visit(RoutePatternSegmentNode node); - void Visit(RoutePatternReplacementNode node); - void Visit(RoutePatternParameterNode node); - void Visit(RoutePatternLiteralNode node); - void Visit(RoutePatternSegmentSeperatorNode node); - void Visit(RoutePatternOptionalSeperatorNode node); - void Visit(RoutePatternCatchAllParameterPartNode node); - void Visit(RoutePatternNameParameterPartNode node); - void Visit(RoutePatternPolicyParameterPartNode node); - void Visit(RoutePatternPolicyFragmentEscapedNode node); - void Visit(RoutePatternPolicyFragment node); - void Visit(RoutePatternOptionalParameterPartNode node); - void Visit(RoutePatternDefaultValueParameterPartNode node); -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedDiagnostic.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedDiagnostic.cs deleted file mode 100644 index e72f85dd1c5c..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedDiagnostic.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; - -internal struct EmbeddedDiagnostic : IEquatable<EmbeddedDiagnostic> -{ - public readonly string Message; - public readonly TextSpan Span; - - public EmbeddedDiagnostic(string message, TextSpan span) - { - Debug.Assert(message != null); - Message = message; - Span = span; - } - - public override bool Equals(object? obj) - => obj is EmbeddedDiagnostic diagnostic && Equals(diagnostic); - - public bool Equals(EmbeddedDiagnostic other) - => Message == other.Message && - Span.Equals(other.Span); - - public override string ToString() - => Message; - - public override int GetHashCode() - { - unchecked - { - var hashCode = -954867195; - hashCode = hashCode * -1521134295 + base.GetHashCode(); - hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Message); - hashCode = hashCode * -1521134295 + EqualityComparer<TextSpan>.Default.GetHashCode(Span); - return hashCode; - } - } - - public static bool operator ==(EmbeddedDiagnostic diagnostic1, EmbeddedDiagnostic diagnostic2) - => diagnostic1.Equals(diagnostic2); - - public static bool operator !=(EmbeddedDiagnostic diagnostic1, EmbeddedDiagnostic diagnostic2) - => !(diagnostic1 == diagnostic2); -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSeparatedSyntaxNodeList.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSeparatedSyntaxNodeList.cs deleted file mode 100644 index 8bab913ba347..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSeparatedSyntaxNodeList.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Immutable; -using System.Diagnostics; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; - -internal readonly struct EmbeddedSeparatedSyntaxNodeList<TSyntaxKind, TSyntaxNode, TDerivedNode> - where TSyntaxKind : struct - where TSyntaxNode : EmbeddedSyntaxNode<TSyntaxKind, TSyntaxNode> - where TDerivedNode : TSyntaxNode -{ - public ImmutableArray<EmbeddedSyntaxNodeOrToken<TSyntaxKind, TSyntaxNode>> NodesAndTokens { get; } - public int Length { get; } - public int SeparatorLength { get; } - - public static readonly EmbeddedSeparatedSyntaxNodeList<TSyntaxKind, TSyntaxNode, TDerivedNode> Empty - = new(ImmutableArray<EmbeddedSyntaxNodeOrToken<TSyntaxKind, TSyntaxNode>>.Empty); - - public EmbeddedSeparatedSyntaxNodeList( - ImmutableArray<EmbeddedSyntaxNodeOrToken<TSyntaxKind, TSyntaxNode>> nodesAndTokens) - { - Debug.Assert(!nodesAndTokens.IsDefault); - NodesAndTokens = nodesAndTokens; - - var allLength = NodesAndTokens.Length; - Length = (allLength + 1) / 2; - SeparatorLength = allLength / 2; - - Verify(); - } - - [Conditional("DEBUG")] - private void Verify() - { - for (var i = 0; i < NodesAndTokens.Length; i++) - { - if ((i & 1) == 0) - { - // All even values should be TNode - Debug.Assert(NodesAndTokens[i].IsNode); - Debug.Assert(NodesAndTokens[i].Node is EmbeddedSyntaxNode<TSyntaxKind, TSyntaxNode>); - } - else - { - // All odd values should be separator tokens - Debug.Assert(!NodesAndTokens[i].IsNode); - } - } - } - - /// <summary> - /// Retrieves only nodes, skipping the separator tokens - /// </summary> - public TDerivedNode this[int index] - { - get - { - if (index < Length && index >= 0) - { - // x2 here to get only even indexed numbers. Follows same logic - // as SeparatedSyntaxList in that the separator tokens are not returned - var nodeOrToken = NodesAndTokens[index * 2]; - Debug.Assert(nodeOrToken.IsNode); - Debug.Assert(nodeOrToken.Node != null); - return (TDerivedNode)nodeOrToken.Node; - } - - throw new ArgumentOutOfRangeException(nameof(index)); - } - } - - public Enumerator GetEnumerator() => new(this); - - public struct Enumerator - { - private readonly EmbeddedSeparatedSyntaxNodeList<TSyntaxKind, TSyntaxNode, TDerivedNode> _list; - private int _currentIndex; - - public Enumerator(EmbeddedSeparatedSyntaxNodeList<TSyntaxKind, TSyntaxNode, TDerivedNode> list) - { - _list = list; - _currentIndex = -1; - Current = null!; - } - - public TDerivedNode Current { get; private set; } - - public bool MoveNext() - { - _currentIndex++; - if (_currentIndex >= _list.Length) - { - Current = null!; - return false; - } - - Current = _list[_currentIndex]; - return true; - } - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxHelpers.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxHelpers.cs deleted file mode 100644 index 6abf33e19853..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxHelpers.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; -using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; - -internal static class EmbeddedSyntaxHelpers -{ - public static TextSpan GetSpan<TSyntaxKind>(EmbeddedSyntaxToken<TSyntaxKind> token1, EmbeddedSyntaxToken<TSyntaxKind> token2) where TSyntaxKind : struct - => GetSpan(token1.VirtualChars[0], token2.VirtualChars.Last()); - - public static TextSpan GetSpan(VirtualCharSequence virtualChars) - => GetSpan(virtualChars[0], virtualChars.Last()); - - public static TextSpan GetSpan(VirtualChar firstChar, VirtualChar lastChar) - => TextSpan.FromBounds(firstChar.Span.Start, lastChar.Span.End); - - public static RoutePatternNode GetChildNode(this RoutePatternNode node, RoutePatternKind kind) - { - foreach (var child in node) - { - if (child.IsNode && child.Kind == kind) - { - return child.Node; - } - } - - return null; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxNode.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxNode.cs deleted file mode 100644 index ed573e46701e..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxNode.cs +++ /dev/null @@ -1,210 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Diagnostics; -using System.Text; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; - -/// <summary> -/// Root of the embedded language syntax hierarchy. EmbeddedSyntaxNodes are very similar to -/// Roslyn Red-Nodes in concept, though there are differences for ease of implementation. -/// </summary> -internal abstract class EmbeddedSyntaxNode<TSyntaxKind, TSyntaxNode> - where TSyntaxKind : struct - where TSyntaxNode : EmbeddedSyntaxNode<TSyntaxKind, TSyntaxNode> -{ - public readonly TSyntaxKind Kind; - private TextSpan? _fullSpan; - - protected EmbeddedSyntaxNode(TSyntaxKind kind) - { - Debug.Assert((int)(object)kind != 0); - Kind = kind; - } - - internal abstract int ChildCount { get; } - internal abstract EmbeddedSyntaxNodeOrToken<TSyntaxKind, TSyntaxNode> ChildAt(int index); - - public EmbeddedSyntaxNodeOrToken<TSyntaxKind, TSyntaxNode> this[int index] => ChildAt(index); - - public TextSpan GetSpan() - { - var start = int.MaxValue; - var end = 0; - - GetSpan(ref start, ref end); - - return TextSpan.FromBounds(start, end); - } - - public TextSpan? GetFullSpan() - => _fullSpan ??= ComputeFullSpan(); - - private TextSpan? ComputeFullSpan() - { - var start = ComputeStart(); - var end = ComputeEnd(); - if (start == null || end == null) - { - return null; - } - - return TextSpan.FromBounds(start.Value, end.Value); - - int? ComputeStart() - { - for (int i = 0, n = ChildCount; i < n; i++) - { - var child = ChildAt(i); - var span = child.GetFullSpan(); - if (span != null) - { - return span.Value.Start; - } - } - - return null; - } - - int? ComputeEnd() - { - for (var i = ChildCount - 1; i >= 0; i--) - { - var child = ChildAt(i); - var span = child.GetFullSpan(); - if (span != null) - { - return span.Value.End; - } - } - - return null; - } - } - - private void GetSpan(ref int start, ref int end) - { - foreach (var child in this) - { - if (child.IsNode) - { - child.Node.GetSpan(ref start, ref end); - } - else - { - var token = child.Token; - if (!token.IsMissing) - { - start = Math.Min(token.VirtualChars[0].Span.Start, start); - end = Math.Max(token.VirtualChars.Last().Span.End, end); - } - } - } - } - - public bool Contains(VirtualChar virtualChar) - { - foreach (var child in this) - { - if (child.IsNode) - { - if (child.Node.Contains(virtualChar)) - { - return true; - } - } - else - { - if (child.Token.VirtualChars.Contains(virtualChar)) - { - return true; - } - } - } - - return false; - } - - /// <summary> - /// Returns the string representation of this node, not including its leading and trailing trivia. - /// </summary> - /// <returns>The string representation of this node, not including its leading and trailing trivia.</returns> - /// <remarks>The length of the returned string is always the same as Span.Length</remarks> - public override string ToString() - { - var sb = new StringBuilder(); - WriteTo(sb); - return sb.ToString(); - } - - /// <summary> - /// Returns full string representation of this node including its leading and trailing trivia. - /// </summary> - /// <returns>The full string representation of this node including its leading and trailing trivia.</returns> - /// <remarks>The length of the returned string is always the same as FullSpan.Length</remarks> - public string ToFullString() - { - var sb = new StringBuilder(); - WriteTo(sb); - return sb.ToString(); - } - - /// <summary> - /// Writes the node to a stringbuilder. - /// </summary> - /// <param name="leading">If false, leading trivia will not be added</param> - /// <param name="trailing">If false, trailing trivia will not be added</param> - public void WriteTo(StringBuilder sb) - { - for (var i = 0; i < ChildCount; i++) - { - var child = this[i]; - - if (child.IsNode) - { - child.Node.WriteTo(sb); - } - else - { - child.Token.WriteTo(sb); - } - } - } - - public Enumerator GetEnumerator() - => new(this); - - public struct Enumerator - { - private readonly EmbeddedSyntaxNode<TSyntaxKind, TSyntaxNode> _node; - private readonly int _childCount; - private int _currentIndex; - - public Enumerator(EmbeddedSyntaxNode<TSyntaxKind, TSyntaxNode> node) - { - _node = node; - _childCount = _node.ChildCount; - _currentIndex = -1; - Current = default; - } - - public EmbeddedSyntaxNodeOrToken<TSyntaxKind, TSyntaxNode> Current { get; private set; } - - public bool MoveNext() - { - _currentIndex++; - if (_currentIndex >= _childCount) - { - Current = default; - return false; - } - - Current = _node.ChildAt(_currentIndex); - return true; - } - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxNodeOrToken.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxNodeOrToken.cs deleted file mode 100644 index 582d196e72f9..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxNodeOrToken.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; - -internal struct EmbeddedSyntaxNodeOrToken<TSyntaxKind, TSyntaxNode> - where TSyntaxKind : struct - where TSyntaxNode : EmbeddedSyntaxNode<TSyntaxKind, TSyntaxNode> -{ - private readonly EmbeddedSyntaxToken<TSyntaxKind> _token; - - public readonly TSyntaxNode? Node; - - private EmbeddedSyntaxNodeOrToken(TSyntaxNode? node) : this() - { - Node = node; - } - - private EmbeddedSyntaxNodeOrToken(EmbeddedSyntaxToken<TSyntaxKind> token) : this() - { - Debug.Assert((int)(object)token.Kind != 0); - _token = token; - } - - public readonly EmbeddedSyntaxToken<TSyntaxKind> Token - { - get - { - Debug.Assert(Node == null); - return _token; - } - } - public TSyntaxKind Kind => Node?.Kind ?? Token.Kind; - - [MemberNotNullWhen(true, nameof(Node))] - public bool IsNode => Node != null; - - public TextSpan? GetFullSpan() - => IsNode ? Node.GetFullSpan() : _token.GetFullSpan(); - - public static implicit operator EmbeddedSyntaxNodeOrToken<TSyntaxKind, TSyntaxNode>(TSyntaxNode? node) - => new(node); - - public static implicit operator EmbeddedSyntaxNodeOrToken<TSyntaxKind, TSyntaxNode>(EmbeddedSyntaxToken<TSyntaxKind> token) - => new(token); -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxToken.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxToken.cs deleted file mode 100644 index cc235187b938..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxToken.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Immutable; -using System.Diagnostics; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; - -internal struct EmbeddedSyntaxToken<TSyntaxKind> where TSyntaxKind : struct -{ - public readonly TSyntaxKind Kind; - public readonly VirtualCharSequence VirtualChars; - internal readonly ImmutableArray<EmbeddedDiagnostic> Diagnostics; - - /// <summary> - /// Returns the value of the token. For example, if the token represents an integer capture, - /// then this property would return the actual integer. - /// </summary> - public readonly object Value; - - public EmbeddedSyntaxToken( - TSyntaxKind kind, - VirtualCharSequence virtualChars, - ImmutableArray<EmbeddedDiagnostic> diagnostics, object value) - { - Debug.Assert(!diagnostics.IsDefault); - Kind = kind; - VirtualChars = virtualChars; - Diagnostics = diagnostics; - Value = value; - } - - public bool IsMissing => VirtualChars.Length == 0; - - public EmbeddedSyntaxToken<TSyntaxKind> AddDiagnosticIfNone(EmbeddedDiagnostic diagnostic) - => Diagnostics.Length > 0 ? this : WithDiagnostics(ImmutableArray.Create(diagnostic)); - - public EmbeddedSyntaxToken<TSyntaxKind> WithDiagnostics(ImmutableArray<EmbeddedDiagnostic> diagnostics) - => With(diagnostics: diagnostics); - - public EmbeddedSyntaxToken<TSyntaxKind> With( - Optional<TSyntaxKind> kind = default, - Optional<VirtualCharSequence> virtualChars = default, - Optional<ImmutableArray<EmbeddedDiagnostic>> diagnostics = default, - Optional<object> value = default) - { - return new EmbeddedSyntaxToken<TSyntaxKind>( - kind.HasValue ? kind.Value : Kind, - virtualChars.HasValue ? virtualChars.Value : VirtualChars, - diagnostics.HasValue ? diagnostics.Value : Diagnostics, - value.HasValue ? value.Value : Value); - } - - public TextSpan GetSpan() - => EmbeddedSyntaxHelpers.GetSpan(VirtualChars); - - public TextSpan? GetFullSpan() - { - if (VirtualChars.Length == 0) - { - return null; - } - - var start = VirtualChars.Length == 0 ? int.MaxValue : VirtualChars[0].Span.Start; - var end = VirtualChars.Length == 0 ? int.MinValue : VirtualChars[VirtualChars.Length - 1].Span.End; - - return TextSpan.FromBounds(start, end); - } - - public override string ToString() - { - var sb = new StringBuilder(); - WriteTo(sb); - return sb.ToString(); - } - - /// <summary> - /// Writes the token to a stringbuilder. - /// </summary> - public void WriteTo(StringBuilder sb) - { - sb.Append(VirtualChars.CreateString()); - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxTree.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxTree.cs deleted file mode 100644 index 82445f11240b..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxTree.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Immutable; -using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; - -internal abstract class EmbeddedSyntaxTree<TSyntaxKind, TSyntaxNode, TCompilationUnitSyntax> - where TSyntaxKind : struct - where TSyntaxNode : EmbeddedSyntaxNode<TSyntaxKind, TSyntaxNode> - where TCompilationUnitSyntax : TSyntaxNode -{ - public readonly VirtualCharSequence Text; - public readonly TCompilationUnitSyntax Root; - public readonly ImmutableArray<EmbeddedDiagnostic> Diagnostics; - - protected EmbeddedSyntaxTree( - VirtualCharSequence text, - TCompilationUnitSyntax root, - ImmutableArray<EmbeddedDiagnostic> diagnostics) - { - Text = text; - Root = root; - Diagnostics = diagnostics; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxTrivia.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxTrivia.cs deleted file mode 100644 index 7a9d06fe094e..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/EmbeddedSyntaxTrivia.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Immutable; -using System.Diagnostics; -using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; - -/// <summary> -/// Trivia on an <see cref="EmbeddedSyntaxToken{TSyntaxKind}"/>. -/// </summary> -internal struct EmbeddedSyntaxTrivia<TSyntaxKind> where TSyntaxKind : struct -{ - public readonly TSyntaxKind Kind; - public readonly VirtualCharSequence VirtualChars; - - /// <summary> - /// A place for diagnostics to be stored during parsing. Not intended to be accessed - /// directly. These will be collected and aggregated into <see cref="EmbeddedSyntaxTree{TNode, TRoot, TSyntaxKind}.Diagnostics"/> - /// </summary> - internal readonly ImmutableArray<EmbeddedDiagnostic> Diagnostics; - - public EmbeddedSyntaxTrivia(TSyntaxKind kind, VirtualCharSequence virtualChars, ImmutableArray<EmbeddedDiagnostic> diagnostics) - { - Debug.Assert(virtualChars.Length > 0); - Kind = kind; - VirtualChars = virtualChars; - Diagnostics = diagnostics; - } - - public TextSpan GetSpan() - => EmbeddedSyntaxHelpers.GetSpan(VirtualChars); - - public override string ToString() - => VirtualChars.CreateString(); -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/README.md b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/README.md deleted file mode 100644 index 9ea30e3d3cf0..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/EmbeddedSyntax/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# About - -Embedded syntax code is copied from Roslyn: - -https://github.com/dotnet/roslyn/tree/b0de0c8e00ebf85db3c3884f2d81dfc3cb2d5a9d/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EmbeddedLanguages/Common \ No newline at end of file diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/MvcDetector.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/MvcDetector.cs deleted file mode 100644 index 201dc270fd25..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/MvcDetector.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; - -internal static class MvcDetector -{ - private const string ControllerTypeNameSuffix = "Controller"; - - // Replicates logic from ControllerFeatureProvider.IsController. - // https://github.com/dotnet/aspnetcore/blob/785cf9bd845a8d28dce3a079c4fedf4a4c2afe57/src/Mvc/Mvc.Core/src/Controllers/ControllerFeatureProvider.cs#L39 - public static bool IsController(INamedTypeSymbol typeSymbol, WellKnownTypes wellKnownTypes) - { - if (!typeSymbol.IsReferenceType) - { - return false; - } - - if (typeSymbol.IsAbstract) - { - return false; - } - - // We only consider public top-level classes as controllers. - if (typeSymbol.DeclaredAccessibility != Accessibility.Public) - { - return false; - } - if (typeSymbol.ContainingType != null) - { - return false; - } - - // Has generic arguments - if (typeSymbol.IsGenericType) - { - return false; - } - - // Check name before attribute's for performance. - if (!typeSymbol.Name.EndsWith(ControllerTypeNameSuffix, StringComparison.OrdinalIgnoreCase) && - !typeSymbol.HasAttribute(wellKnownTypes.ControllerAttribute)) - { - return false; - } - - if (typeSymbol.HasAttribute(wellKnownTypes.NonControllerAttribute)) - { - return false; - } - - return true; - } - - // Replicates logic from DefaultApplicationModelProvider.IsAction. - // https://github.com/dotnet/aspnetcore/blob/785cf9bd845a8d28dce3a079c4fedf4a4c2afe57/src/Mvc/Mvc.Core/src/ApplicationModels/DefaultApplicationModelProvider.cs#L393 - public static bool IsAction(IMethodSymbol methodSymbol, WellKnownTypes wellKnownTypes) - { - if (methodSymbol == null) - { - throw new ArgumentNullException(nameof(methodSymbol)); - } - - // The SpecialName bit is set to flag members that are treated in a special way by some compilers - // (such as property accessors and operator overloading methods). - if (methodSymbol.MethodKind is not (MethodKind.Ordinary or MethodKind.DeclareMethod)) - { - return false; - } - - // Overridden methods from Object class, e.g. Equals(Object), GetHashCode(), etc., are not valid. - if (methodSymbol.ContainingType.SpecialType is SpecialType.System_Object) - { - return false; - } - - if (methodSymbol.IsStatic) - { - return false; - } - - if (methodSymbol.IsAbstract) - { - return false; - } - - if (methodSymbol.IsGenericMethod) - { - return false; - } - - if (methodSymbol.DeclaredAccessibility != Accessibility.Public) - { - return false; - } - - if (methodSymbol.HasAttribute(wellKnownTypes.NonActionAttribute)) - { - return false; - } - - return true; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs deleted file mode 100644 index acaad4824d1b..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; - -internal static class RoutePatternParametersDetector -{ - public static ImmutableArray<ISymbol> ResolvedParameters(ISymbol symbol, WellKnownTypes wellKnownTypes) - { - var resolvedParameterSymbols = ImmutableArray.CreateBuilder<ISymbol>(); - var childSymbols = symbol switch - { - ITypeSymbol typeSymbol => typeSymbol.GetMembers().OfType<IPropertySymbol>().ToImmutableArray().As<ISymbol>(), - IMethodSymbol methodSymbol => methodSymbol.Parameters.As<ISymbol>(), - _ => throw new InvalidOperationException("Unexpected symbol type: " + symbol) - }; - - var allNoneRouteMetadataTypes = new[] - { - wellKnownTypes.IFromBodyMetadata, - wellKnownTypes.IFromFormMetadata, - wellKnownTypes.IFromHeaderMetadata, - wellKnownTypes.IFromQueryMetadata, - wellKnownTypes.IFromServiceMetadata - }; - var specialTypes = new[] - { - wellKnownTypes.CancellationToken, - wellKnownTypes.HttpContext, - wellKnownTypes.HttpRequest, - wellKnownTypes.HttpResponse, - wellKnownTypes.ClaimsPrincipal, - wellKnownTypes.IFormFileCollection, - wellKnownTypes.IFormFile, - wellKnownTypes.Stream, - wellKnownTypes.PipeReader - }; - - foreach (var child in childSymbols) - { - if (HasSpecialType(child, specialTypes) || HasExplicitNonRouteAttribute(child, allNoneRouteMetadataTypes)) - { - continue; - } - else if (child.HasAttribute(wellKnownTypes.AsParametersAttribute)) - { - resolvedParameterSymbols.AddRange(ResolvedParameters(child.GetParameterType(), wellKnownTypes)); - } - else - { - resolvedParameterSymbols.Add(child); - } - } - return resolvedParameterSymbols.ToImmutable(); - } - - private static bool HasSpecialType(ISymbol child, INamedTypeSymbol[] specialTypes) - { - if (child.GetParameterType() is not INamedTypeSymbol type) - { - return false; - } - - foreach (var specialType in specialTypes) - { - if (type.IsType(specialType)) - { - return true; - } - } - - return false; - } - - private static bool HasExplicitNonRouteAttribute(ISymbol child, INamedTypeSymbol[] allNoneRouteMetadataTypes) - { - foreach (var attributeData in child.GetAttributes()) - { - foreach (var nonRouteMetadata in allNoneRouteMetadataTypes) - { - if (attributeData.AttributeClass.Implements(nonRouteMetadata)) - { - return true; - } - } - } - - return false; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternUsageDetector.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternUsageDetector.cs deleted file mode 100644 index 6667828e1bbe..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternUsageDetector.cs +++ /dev/null @@ -1,218 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; - -internal record struct RoutePatternUsageContext( - IMethodSymbol? MethodSymbol, - bool IsMinimal, - bool IsMvcAttribute); - -internal static class RoutePatternUsageDetector -{ - public static RoutePatternUsageContext BuildContext(SyntaxToken token, SemanticModel semanticModel, WellKnownTypes wellKnownTypes, CancellationToken cancellationToken) - { - if (token.Parent is not LiteralExpressionSyntax) - { - return default; - } - - var container = token.TryFindContainer(); - if (container is null) - { - return default; - } - - if (container.Parent.IsKind(SyntaxKind.Argument)) - { - // We're an argument in a method call. See if we're a MapXXX method. - var mapMethodSymbol = FindMapMethod(semanticModel, wellKnownTypes, container, cancellationToken); - if (mapMethodSymbol == null) - { - return default; - } - return new(MethodSymbol: mapMethodSymbol, IsMinimal: true, IsMvcAttribute: false); - } - else if (container.Parent.IsKind(SyntaxKind.AttributeArgument)) - { - // We're an argument in an attribute. See if attribute is on a controller method. - var attributeParent = FindAttributeParent(container); - if (attributeParent is MethodDeclarationSyntax methodDeclarationSyntax) - { - var methodSymbol = semanticModel.GetDeclaredSymbol(methodDeclarationSyntax, cancellationToken); - - var actionMethodSymbol = FindMvcMethod(wellKnownTypes, methodSymbol); - if (actionMethodSymbol == null) - { - return default; - } - return new(MethodSymbol: actionMethodSymbol, IsMinimal: false, IsMvcAttribute: true); - } - else if (attributeParent is ClassDeclarationSyntax classDeclarationSyntax) - { - var classSymbol = semanticModel.GetDeclaredSymbol(classDeclarationSyntax, cancellationToken); - - return new(MethodSymbol: null, IsMinimal: false, IsMvcAttribute: MvcDetector.IsController(classSymbol, wellKnownTypes)); - } - } - - return default; - } - - private static SyntaxNode? FindAttributeParent(SyntaxNode container) - { - var argument = container.Parent; - if (argument.Parent is not AttributeArgumentListSyntax argumentList) - { - return null; - } - - if (argumentList.Parent is not AttributeSyntax attribute) - { - return null; - } - - if (attribute.Parent is not AttributeListSyntax attributeList) - { - return null; - } - - return attributeList.Parent; - } - - private static IMethodSymbol? FindMvcMethod(WellKnownTypes wellKnownTypes, IMethodSymbol methodSymbol) - { - if (methodSymbol.ContainingType is not INamedTypeSymbol typeSymbol) - { - return null; - } - - if (!MvcDetector.IsController(typeSymbol, wellKnownTypes)) - { - return null; - } - - if (!MvcDetector.IsAction(methodSymbol, wellKnownTypes)) - { - return null; - } - - return methodSymbol; - } - - private static IMethodSymbol? FindMapMethod(SemanticModel semanticModel, WellKnownTypes wellKnownTypes, SyntaxNode container, CancellationToken cancellationToken) - { - var argument = container.Parent; - if (argument.Parent is not BaseArgumentListSyntax argumentList || - argumentList.Parent is null) - { - return null; - } - - // Multiple overloads could be resolved, e.g. MapGet(string, RequestDelegate) and MapGet(string, Delegate) - // Check each overload result to see whether it matches and return the first valid result. - var symbols = GetBestOrAllSymbols(semanticModel.GetSymbolInfo(argumentList.Parent, cancellationToken)); - - foreach (var symbol in symbols) - { - if (symbol is IMethodSymbol methodSymbol) - { - var matchingMapSymbol = FindValidMapMethod(semanticModel, wellKnownTypes, argumentList, methodSymbol, cancellationToken); - if (matchingMapSymbol != null) - { - return matchingMapSymbol; - } - } - } - - return null; - } - - private static IMethodSymbol? FindValidMapMethod(SemanticModel semanticModel, WellKnownTypes wellKnownTypes, BaseArgumentListSyntax argumentList, IMethodSymbol method, CancellationToken cancellationToken) - { - if (!method.Name.StartsWith("Map", StringComparison.Ordinal)) - { - return null; - } - - var delegateSymbol = semanticModel.Compilation.GetSpecialType(SpecialType.System_Delegate); - - var delegateArgument = method.Parameters.FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(delegateSymbol, a.Type)); - if (delegateArgument == null) - { - return null; - } - - // IEndpointRouteBuilder may be removed from symbol because the method is called as an extension method. - // ReducedFrom includes the original IEndpointRouteBuilder parameter. - if (!(method.ReducedFrom ?? method).Parameters.Any( - a => SymbolEqualityComparer.Default.Equals(a.Type, wellKnownTypes.IEndpointRouteBuilder) || - a.Type.Implements(wellKnownTypes.IEndpointRouteBuilder))) - { - return null; - } - - var delegateIndex = method.Parameters.IndexOf(delegateArgument); - if (delegateIndex >= argumentList.Arguments.Count) - { - return null; - } - - ArgumentSyntax? item = null; - foreach (var argument in argumentList.Arguments) - { - // Handle named argument - if (argument.NameColon != null && !argument.NameColon.IsMissing) - { - var name = argument.NameColon.Name.Identifier.ValueText; - if (name == delegateArgument.Name) - { - item = argument; - break; - } - } - } - - if (item == null) - { - // Handle positional argument - item = argumentList.Arguments[delegateIndex]; - } - - return GetMethodInfo(semanticModel, item.Expression, cancellationToken); - } - - private static IMethodSymbol? GetMethodInfo(SemanticModel semanticModel, SyntaxNode syntaxNode, CancellationToken cancellationToken) - { - var delegateSymbolInfo = semanticModel.GetSymbolInfo(syntaxNode, cancellationToken); - var delegateSymbol = delegateSymbolInfo.Symbol; - if (delegateSymbol == null && delegateSymbolInfo.CandidateSymbols.Length == 1) - { - delegateSymbol = delegateSymbolInfo.CandidateSymbols[0]; - } - - return delegateSymbol as IMethodSymbol; - } - - private static ImmutableArray<ISymbol> GetBestOrAllSymbols(SymbolInfo info) - { - if (info.Symbol != null) - { - return ImmutableArray.Create(info.Symbol); - } - else if (info.CandidateSymbols.Length > 0) - { - return info.CandidateSymbols; - } - - return ImmutableArray<ISymbol>.Empty; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteStringSyntaxDetector.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteStringSyntaxDetector.cs deleted file mode 100644 index 16a75c1ad443..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteStringSyntaxDetector.cs +++ /dev/null @@ -1,431 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; - -internal static class RouteStringSyntaxDetector -{ - public static bool IsRouteStringSyntaxToken(SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken) - { - if (!IsAnyStringLiteral(token.RawKind)) - { - return false; - } - - if (!TryGetStringFormat(token, semanticModel, cancellationToken, out var identifier)) - { - return false; - } - - if (identifier != "Route") - { - return false; - } - - return true; - } - - private static bool IsAnyStringLiteral(int rawKind) - { - return rawKind == (int)SyntaxKind.StringLiteralToken || - rawKind == (int)SyntaxKind.SingleLineRawStringLiteralToken || - rawKind == (int)SyntaxKind.MultiLineRawStringLiteralToken || - rawKind == (int)SyntaxKind.Utf8StringLiteralToken || - rawKind == (int)SyntaxKind.Utf8SingleLineRawStringLiteralToken || - rawKind == (int)SyntaxKind.Utf8MultiLineRawStringLiteralToken; - } - - private static bool TryGetStringFormat(SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken, [NotNullWhen(true)] out string identifier) - { - if (token.Parent is not LiteralExpressionSyntax) - { - identifier = null; - return false; - } - - var container = token.TryFindContainer(); - if (container is null) - { - identifier = null; - return false; - } - - if (container.Parent.IsKind(SyntaxKind.Argument)) - { - if (IsArgumentWithMatchingStringSyntaxAttribute(semanticModel, container.Parent, cancellationToken, out identifier)) - { - return true; - } - } - else if (container.Parent.IsKind(SyntaxKind.AttributeArgument)) - { - if (IsArgumentToAttributeParameterWithMatchingStringSyntaxAttribute(semanticModel, container.Parent, cancellationToken, out identifier)) - { - return true; - } - } - else - { - var statement = container.FirstAncestorOrSelf<SyntaxNode>(n => n is StatementSyntax); - if (statement.IsSimpleAssignmentStatement()) - { - GetPartsOfAssignmentStatement(statement, out var left, out var right); - if (container == right && - IsFieldOrPropertyWithMatchingStringSyntaxAttribute( - semanticModel, left, cancellationToken, out identifier)) - { - return true; - } - } - - if (container.Parent?.IsKind(SyntaxKind.EqualsValueClause) ?? false) - { - if (container.Parent.Parent?.IsKind(SyntaxKind.VariableDeclarator) ?? false) - { - var variableDeclarator = container.Parent.Parent; - var symbol = - semanticModel.GetDeclaredSymbol(variableDeclarator, cancellationToken) ?? - semanticModel.GetDeclaredSymbol(GetIdentifierOfVariableDeclarator(variableDeclarator).GetRequiredParent(), cancellationToken); - - if (IsFieldOrPropertyWithMatchingStringSyntaxAttribute(symbol, out identifier)) - { - return true; - } - } - else if (IsEqualsValueOfPropertyDeclaration(container.Parent)) - { - var property = container.Parent.GetRequiredParent(); - var symbol = semanticModel.GetDeclaredSymbol(property, cancellationToken); - - if (IsFieldOrPropertyWithMatchingStringSyntaxAttribute(symbol, out identifier)) - { - return true; - } - } - } - } - - identifier = null; - return false; - } - - public static bool IsEqualsValueOfPropertyDeclaration(SyntaxNode? node) - => node?.Parent is PropertyDeclarationSyntax propertyDeclaration && propertyDeclaration.Initializer == node; - - private static SyntaxToken GetIdentifierOfVariableDeclarator(SyntaxNode node) - => ((VariableDeclaratorSyntax)node).Identifier; - - private static bool IsFieldOrPropertyWithMatchingStringSyntaxAttribute( - SemanticModel semanticModel, - SyntaxNode left, - CancellationToken cancellationToken, - [NotNullWhen(true)] out string? identifier) - { - var symbol = semanticModel.GetSymbolInfo(left, cancellationToken).Symbol; - return IsFieldOrPropertyWithMatchingStringSyntaxAttribute(symbol, out identifier); - } - - public static void GetPartsOfAssignmentStatement( - SyntaxNode statement, out SyntaxNode left, out SyntaxNode right) - { - GetPartsOfAssignmentExpressionOrStatement( - ((ExpressionStatementSyntax)statement).Expression, out left, out _, out right); - } - - public static void GetPartsOfAssignmentExpressionOrStatement( - SyntaxNode statement, out SyntaxNode left, out SyntaxToken operatorToken, out SyntaxNode right) - { - var expression = statement; - if (statement is ExpressionStatementSyntax expressionStatement) - { - expression = expressionStatement.Expression; - } - - var assignment = (AssignmentExpressionSyntax)expression; - left = assignment.Left; - operatorToken = assignment.OperatorToken; - right = assignment.Right; - } - - private static bool IsArgumentWithMatchingStringSyntaxAttribute( - SemanticModel semanticModel, - SyntaxNode argument, - CancellationToken cancellationToken, - [NotNullWhen(true)] out string? identifier) - { - var parameter = FindParameterForArgument(semanticModel, argument, allowUncertainCandidates: true, cancellationToken); - return HasMatchingStringSyntaxAttribute(parameter, out identifier); - } - - private static bool IsArgumentToAttributeParameterWithMatchingStringSyntaxAttribute( - SemanticModel semanticModel, - SyntaxNode argument, - CancellationToken cancellationToken, - [NotNullWhen(true)] out string? identifier) - { - // First, see if this is an `X = "..."` argument that is binding to a field/prop on the attribute. - var fieldOrProperty = FindFieldOrPropertyForAttributeArgument(semanticModel, argument, cancellationToken); - if (fieldOrProperty != null) - { - return HasMatchingStringSyntaxAttribute(fieldOrProperty, out identifier); - } - - // Otherwise, see if it's a normal named/position argument to the attribute. - var parameter = FindParameterForAttributeArgument(semanticModel, argument, allowUncertainCandidates: true, cancellationToken); - return HasMatchingStringSyntaxAttribute(parameter, out identifier); - } - - public static bool IsFieldOrPropertyWithMatchingStringSyntaxAttribute( - ISymbol? symbol, [NotNullWhen(true)] out string? identifier) - { - identifier = null; - return symbol is IFieldSymbol or IPropertySymbol && - HasMatchingStringSyntaxAttribute(symbol, out identifier); - } - - private static bool HasMatchingStringSyntaxAttribute( - [NotNullWhen(true)] ISymbol? symbol, - [NotNullWhen(true)] out string? identifier) - { - if (symbol != null) - { - foreach (var attribute in symbol.GetAttributes()) - { - if (IsMatchingStringSyntaxAttribute(attribute, out identifier)) - { - return true; - } - } - } - - identifier = null; - return false; - } - - private static bool IsMatchingStringSyntaxAttribute( - AttributeData attribute, - [NotNullWhen(true)] out string? identifier) - { - identifier = null; - if (attribute.ConstructorArguments.Length == 0) - { - return false; - } - - if (attribute.AttributeClass is not - { - Name: "StringSyntaxAttribute", - ContainingNamespace: - { - Name: nameof(CodeAnalysis), - ContainingNamespace: - { - Name: nameof(System.Diagnostics), - ContainingNamespace: - { - Name: nameof(System), - ContainingNamespace.IsGlobalNamespace: true, - } - } - } - }) - { - return false; - } - - var argument = attribute.ConstructorArguments[0]; - if (argument.Kind != TypedConstantKind.Primitive || argument.Value is not string argString) - { - return false; - } - - identifier = argString; - return true; - } - - private static ISymbol FindFieldOrPropertyForAttributeArgument(SemanticModel semanticModel, SyntaxNode argument, CancellationToken cancellationToken) - => argument is AttributeArgumentSyntax { NameEquals.Name: var name } - ? GetAnySymbol(semanticModel.GetSymbolInfo(name, cancellationToken)) - : null; - - private static IParameterSymbol FindParameterForArgument(SemanticModel semanticModel, SyntaxNode argument, bool allowUncertainCandidates, CancellationToken cancellationToken) - => ((ArgumentSyntax)argument).DetermineParameter(semanticModel, allowUncertainCandidates, allowParams: false, cancellationToken); - - private static IParameterSymbol FindParameterForAttributeArgument(SemanticModel semanticModel, SyntaxNode argument, bool allowUncertainCandidates, CancellationToken cancellationToken) - => ((AttributeArgumentSyntax)argument).DetermineParameter(semanticModel, allowUncertainCandidates, allowParams: false, cancellationToken); - - public static ISymbol? GetAnySymbol(SymbolInfo info) - => info.Symbol ?? info.CandidateSymbols.FirstOrDefault(); - - /// <summary> - /// Returns the parameter to which this argument is passed. If <paramref name="allowParams"/> - /// is true, the last parameter will be returned if it is params parameter and the index of - /// the specified argument is greater than the number of parameters. - /// </summary> - public static IParameterSymbol? DetermineParameter( - this ArgumentSyntax argument, - SemanticModel semanticModel, - bool allowUncertainCandidates = false, - bool allowParams = false, - CancellationToken cancellationToken = default) - { - if (argument.Parent is not BaseArgumentListSyntax argumentList || - argumentList.Parent is null) - { - return null; - } - - // Get the symbol as long if it's not null or if there is only one candidate symbol - var symbolInfo = semanticModel.GetSymbolInfo(argumentList.Parent, cancellationToken); - var symbols = GetBestOrAllSymbols(symbolInfo); - - if (symbols.Length >= 2 && !allowUncertainCandidates) - { - return null; - } - - foreach (var symbol in symbols) - { - var parameters = symbol.GetParameters(); - - // Handle named argument - if (argument.NameColon != null && !argument.NameColon.IsMissing) - { - var name = argument.NameColon.Name.Identifier.ValueText; - var parameter = parameters.FirstOrDefault(p => p.Name == name); - if (parameter != null) - { - return parameter; - } - - continue; - } - - // Handle positional argument - var index = argumentList.Arguments.IndexOf(argument); - if (index < 0) - { - continue; - } - - if (index < parameters.Length) - { - return parameters[index]; - } - - if (allowParams) - { - var lastParameter = parameters.LastOrDefault(); - if (lastParameter == null) - { - continue; - } - - if (lastParameter.IsParams) - { - return lastParameter; - } - } - } - - return null; - } - - /// <summary> - /// Returns the parameter to which this argument is passed. If <paramref name="allowParams"/> - /// is true, the last parameter will be returned if it is params parameter and the index of - /// the specified argument is greater than the number of parameters. - /// </summary> - /// <remarks> - /// Returns null if the <paramref name="argument"/> is a named argument. - /// </remarks> - public static IParameterSymbol? DetermineParameter( - this AttributeArgumentSyntax argument, - SemanticModel semanticModel, - bool allowUncertainCandidates = false, - bool allowParams = false, - CancellationToken cancellationToken = default) - { - // if argument is a named argument it can't map to a parameter. - if (argument.NameEquals != null) - { - return null; - } - if (argument.Parent is not AttributeArgumentListSyntax argumentList) - { - return null; - } - if (argumentList.Parent is not AttributeSyntax invocableExpression) - { - return null; - } - var symbols = GetBestOrAllSymbols(semanticModel.GetSymbolInfo(invocableExpression, cancellationToken)); - if (symbols.Length >= 2 && !allowUncertainCandidates) - { - return null; - } - foreach (var symbol in symbols) - { - var parameters = symbol.GetParameters(); - - // Handle named argument - if (argument.NameColon != null && !argument.NameColon.IsMissing) - { - var name = argument.NameColon.Name.Identifier.ValueText; - var parameter = parameters.FirstOrDefault(p => p.Name == name); - if (parameter != null) - { - return parameter; - } - continue; - } - - // Handle positional argument - var index = argumentList.Arguments.IndexOf(argument); - if (index < 0) - { - continue; - } - if (index < parameters.Length) - { - return parameters[index]; - } - if (allowParams) - { - var lastParameter = parameters.LastOrDefault(); - if (lastParameter == null) - { - continue; - } - if (lastParameter.IsParams) - { - return lastParameter; - } - } - } - - return null; - } - - public static ImmutableArray<ISymbol> GetBestOrAllSymbols(SymbolInfo info) - { - if (info.Symbol != null) - { - return ImmutableArray.Create(info.Symbol); - } - else if (info.CandidateSymbols.Length > 0) - { - return info.CandidateSymbols; - } - - return ImmutableArray<ISymbol>.Empty; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteStringSyntaxDetectorDocument.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteStringSyntaxDetectorDocument.cs deleted file mode 100644 index bca8c0bf0026..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteStringSyntaxDetectorDocument.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; - -/// <summary> -/// This type is seperate from <see cref="RouteStringSyntaxDetector"/> to avoid RS1022 warning in analyzer. -/// It doesn't like analyzers referencing types that might use Document. -/// </summary> -internal static class RouteStringSyntaxDetectorDocument -{ - internal static async ValueTask<(bool success, SyntaxToken token, SemanticModel? model)> TryGetStringSyntaxTokenAtPositionAsync( - Document document, int position, CancellationToken cancellationToken) - { - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (root == null) - { - return default; - } - var token = root.FindToken(position); - - var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (semanticModel == null) - { - return default; - } - - if (!RouteStringSyntaxDetector.IsRouteStringSyntaxToken(token, semanticModel, cancellationToken)) - { - return default; - } - - return (true, token, semanticModel); - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SymbolExtensions.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SymbolExtensions.cs deleted file mode 100644 index 499ea6b112ad..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SymbolExtensions.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; - -internal static class SymbolExtensions -{ - public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeType) - { - foreach (var attributeData in symbol.GetAttributes()) - { - if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, attributeType)) - { - return true; - } - } - - return false; - } - - public static bool Implements(this ITypeSymbol type, ITypeSymbol interfaceType) - { - foreach (var t in type.AllInterfaces) - { - if (SymbolEqualityComparer.Default.Equals(t, interfaceType)) - { - return true; - } - } - return false; - } - - public static bool IsType(this INamedTypeSymbol type, string typeName, SemanticModel semanticModel) - => SymbolEqualityComparer.Default.Equals(type, semanticModel.Compilation.GetTypeByMetadataName(typeName)); - - public static bool IsType(this INamedTypeSymbol type, INamedTypeSymbol otherType) - => SymbolEqualityComparer.Default.Equals(type, otherType); - - public static ITypeSymbol GetParameterType(this ISymbol symbol) - { - return symbol switch - { - IParameterSymbol parameterSymbol => parameterSymbol.Type, - IPropertySymbol propertySymbol => propertySymbol.Type, - _ => throw new InvalidOperationException("Unexpected symbol type: " + symbol) - }; - } - - public static ImmutableArray<IParameterSymbol> GetParameters(this ISymbol? symbol) - => symbol switch - { - IMethodSymbol methodSymbol => methodSymbol.Parameters, - IPropertySymbol parameterSymbol => parameterSymbol.Parameters, - _ => ImmutableArray<IParameterSymbol>.Empty, - }; -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SyntaxTokenExtensions.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SyntaxTokenExtensions.cs deleted file mode 100644 index aefc90ef8725..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SyntaxTokenExtensions.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; - -internal static class SyntaxTokenExtensions -{ - public static SyntaxNode? TryFindContainer(this SyntaxToken token) - { - var node = WalkUpParentheses(GetRequiredParent(token)); - - // if we're inside some collection-like initializer, find the instance actually being created. - if (IsAnyInitializerExpression(node.Parent, out var instance)) - { - node = WalkUpParentheses(instance); - } - - return node; - } - - public static SyntaxNode GetRequiredParent(this SyntaxToken token) - => token.Parent ?? throw new InvalidOperationException("Token's parent was null"); - - public static SyntaxNode GetRequiredParent(this SyntaxNode node) - => node.Parent ?? throw new InvalidOperationException("Node's parent was null"); - - public static SyntaxNode? GetParent(this SyntaxNode node, bool ascendOutOfTrivia) - { - var parent = node.Parent; - if (parent == null && ascendOutOfTrivia) - { - if (node is IStructuredTriviaSyntax structuredTrivia) - { - parent = structuredTrivia.ParentTrivia.Token.Parent; - } - } - - return parent; - } - - [return: NotNullIfNotNull("node")] - private static SyntaxNode? WalkUpParentheses(SyntaxNode? node) - { - while (IsParenthesizedExpression(node?.Parent)) - { - node = node.Parent; - } - - return node; - } - - private static bool IsAnyInitializerExpression([NotNullWhen(true)] SyntaxNode? node, [NotNullWhen(true)] out SyntaxNode? creationExpression) - { - if (node is InitializerExpressionSyntax - { - Parent: BaseObjectCreationExpressionSyntax or ArrayCreationExpressionSyntax or ImplicitArrayCreationExpressionSyntax - }) - { - creationExpression = node.Parent; - return true; - } - - creationExpression = null; - return false; - } - - private static bool IsParenthesizedExpression([NotNullWhen(true)] SyntaxNode? node) - => node?.RawKind == (int)SyntaxKind.ParenthesizedExpression; - - public static bool IsSimpleAssignmentStatement([NotNullWhen(true)] this SyntaxNode? statement) - => statement is ExpressionStatementSyntax exprStatement && - exprStatement.Expression.IsKind(SyntaxKind.SimpleAssignmentExpression); -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/AbstractVirtualCharService.ITextInfo.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/AbstractVirtualCharService.ITextInfo.cs deleted file mode 100644 index 804a09f1f965..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/AbstractVirtualCharService.ITextInfo.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; - -internal abstract partial class AbstractVirtualCharService -{ - /// <summary> - /// Abstraction to allow generic algorithms to run over a string or <see cref="SourceText"/> without any - /// overhead. - /// </summary> - private interface ITextInfo<T> - { - char Get(T text, int index); - int Length(T text); - } - - private struct SourceTextTextInfo : ITextInfo<SourceText> - { - public char Get(SourceText text, int index) => text[index]; - public int Length(SourceText text) => text.Length; - } - - private struct StringTextInfo : ITextInfo<string> - { - public char Get(string text, int index) => text[index]; - public int Length(string text) => text.Length; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/AbstractVirtualCharService.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/AbstractVirtualCharService.cs deleted file mode 100644 index 05c6a7005590..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/AbstractVirtualCharService.cs +++ /dev/null @@ -1,245 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Text; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; - -internal abstract partial class AbstractVirtualCharService : IVirtualCharService -{ - public abstract bool TryGetEscapeCharacter(VirtualChar ch, out char escapedChar); - - protected abstract VirtualCharSequence TryConvertToVirtualCharsWorker(SyntaxToken token); - protected abstract bool IsMultiLineRawStringToken(SyntaxToken token); - - /// <summary> - /// Returns <see langword="true"/> if the next two characters at <c>tokenText[index]</c> are <c>{{</c> or - /// <c>}}</c>. If so, <paramref name="span"/> will contain the span of those two characters (based on <paramref - /// name="tokenText"/> starting at <paramref name="offset"/>). - /// </summary> - protected static bool IsLegalBraceEscape( - string tokenText, int index, int offset, out TextSpan span) - { - if (index + 1 < tokenText.Length) - { - var ch = tokenText[index]; - var next = tokenText[index + 1]; - if ((ch == '{' && next == '{') || - (ch == '}' && next == '}')) - { - span = new TextSpan(offset + index, 2); - return true; - } - } - - span = default; - return false; - } - - public VirtualCharSequence TryConvertToVirtualChars(SyntaxToken token) - { - // We don't process any strings that contain diagnostics in it. That means that we can - // trust that all the string's contents (most importantly, the escape sequences) are well - // formed. - if (token.ContainsDiagnostics) - { - return default; - } - - var result = TryConvertToVirtualCharsWorker(token); - CheckInvariants(token, result); - - return result; - } - - [Conditional("DEBUG")] - private void CheckInvariants(SyntaxToken token, VirtualCharSequence result) - { - // Do some invariant checking to make sure we processed the string token the same - // way the C# and VB compilers did. - if (!result.IsDefault) - { - // Ensure that we properly broke up the token into a sequence of characters that - // matches what the compiler did. - if (token.RawKind == (int)SyntaxKind.StringLiteralToken || - token.RawKind == (int)SyntaxKind.Utf8StringLiteralToken || - token.RawKind == (int)SyntaxKind.CharacterLiteralToken) - { - var expectedValueText = token.ValueText; - var actualValueText = result.CreateString(); - Debug.Assert(expectedValueText == actualValueText); - } - - if (result.Length > 0) - { - var currentVC = result[0]; - Debug.Assert(currentVC.Span.Start >= token.SpanStart, "First span has to start after the start of the string token"); - if (token.RawKind == (int)SyntaxKind.StringLiteralToken || - token.RawKind == (int)SyntaxKind.CharacterLiteralToken) - { - Debug.Assert(currentVC.Span.Start == token.SpanStart + 1 || - currentVC.Span.Start == token.SpanStart + 2, "First span should start on the second or third char of the string."); - } - - if (IsMultiLineRawStringToken(token)) - { - for (var i = 1; i < result.Length; i++) - { - var nextVC = result[i]; - Debug.Assert(currentVC.Span.End <= nextVC.Span.Start, "Virtual character spans have to be ordered."); - currentVC = nextVC; - } - } - else - { - for (var i = 1; i < result.Length; i++) - { - var nextVC = result[i]; - Debug.Assert(currentVC.Span.End == nextVC.Span.Start, "Virtual character spans have to be touching."); - currentVC = nextVC; - } - } - - var lastVC = result.Last(); - - if (token.RawKind == (int)SyntaxKind.StringLiteralToken || - token.RawKind == (int)SyntaxKind.CharacterLiteralToken) - { - Debug.Assert(lastVC.Span.End == token.Span.End - "\"".Length, "Last span has to end right before the end of the string token."); - } - else if (token.RawKind == (int)SyntaxKind.Utf8StringLiteralToken) - { - Debug.Assert(lastVC.Span.End == token.Span.End - "\"u8".Length, "Last span has to end right before the end of the string token."); - } - } - } - } - - /// <summary> - /// Helper to convert simple string literals that escape quotes by doubling them. This is - /// how normal VB literals and c# verbatim string literals work. - /// </summary> - /// <param name="startDelimiter">The start characters string. " in VB and @" in C#</param> - protected static VirtualCharSequence TryConvertSimpleDoubleQuoteString( - SyntaxToken token, string startDelimiter, string endDelimiter, bool escapeBraces) - { - Debug.Assert(!token.ContainsDiagnostics); - - if (escapeBraces) - { - Debug.Assert(startDelimiter == ""); - Debug.Assert(endDelimiter == ""); - } - - var tokenText = token.Text; - - if (startDelimiter.Length > 0 && !tokenText.StartsWith(startDelimiter, StringComparison.Ordinal)) - { - Debug.Assert(false, "This should not be reachable as long as the compiler added no diagnostics."); - return default; - } - - if (endDelimiter.Length > 0 && !tokenText.EndsWith(endDelimiter, StringComparison.Ordinal)) - { - Debug.Assert(false, "This should not be reachable as long as the compiler added no diagnostics."); - return default; - } - - var startIndexInclusive = startDelimiter.Length; - var endIndexExclusive = tokenText.Length - endDelimiter.Length; - - var result = ImmutableList.CreateBuilder<VirtualChar>(); - var offset = token.SpanStart; - - for (var index = startIndexInclusive; index < endIndexExclusive;) - { - if (tokenText[index] == '"' && tokenText[index + 1] == '"') - { - result.Add(VirtualChar.Create(new Rune('"'), new TextSpan(offset + index, 2))); - index += 2; - continue; - } - else if (escapeBraces && IsOpenOrCloseBrace(tokenText[index])) - { - if (!IsLegalBraceEscape(tokenText, index, offset, out var span)) - { - return default; - } - - result.Add(VirtualChar.Create(new Rune(tokenText[index]), span)); - index += result[result.Count - 1].Span.Length; - continue; - } - - index += ConvertTextAtIndexToRune(tokenText, index, result, offset); - } - - return CreateVirtualCharSequence( - tokenText, offset, startIndexInclusive, endIndexExclusive, result); - } - - /// <summary> - /// Returns the number of characters to jump forward (either 1 or 2); - /// </summary> - protected static int ConvertTextAtIndexToRune(string tokenText, int index, ImmutableList<VirtualChar>.Builder result, int offset) - => ConvertTextAtIndexToRune(tokenText, index, new StringTextInfo(), result, offset); - - protected static int ConvertTextAtIndexToRune(SourceText tokenText, int index, ImmutableList<VirtualChar>.Builder result, int offset) - => ConvertTextAtIndexToRune(tokenText, index, new SourceTextTextInfo(), result, offset); - - private static int ConvertTextAtIndexToRune<T, TTextInfo>( - T tokenText, int index, TTextInfo info, ImmutableList<VirtualChar>.Builder result, int offset) - where TTextInfo : struct, ITextInfo<T> - { - var ch = info.Get(tokenText, index); - - if (Rune.TryCreate(ch, out var rune)) - { - // First, see if this was a single char that can become a rune (the common case). - result.Add(VirtualChar.Create(rune, new TextSpan(offset + index, 1))); - return 1; - } - else if (index + 1 < info.Length(tokenText) && - Rune.TryCreate(ch, info.Get(tokenText, index + 1), out rune)) - { - // Otherwise, see if we have a surrogate pair (less common, but possible). - result.Add(VirtualChar.Create(rune, new TextSpan(offset + index, 2))); - return 2; - } - else - { - // Something that couldn't be encoded as runes. - Debug.Assert(char.IsSurrogate(ch)); - result.Add(VirtualChar.Create(ch, new TextSpan(offset + index, 1))); - return 1; - } - } - - protected static bool IsOpenOrCloseBrace(char ch) - => ch is '{' or '}'; - - protected static VirtualCharSequence CreateVirtualCharSequence( - string tokenText, int offset, - int startIndexInclusive, int endIndexExclusive, - ImmutableList<VirtualChar>.Builder result) - { - // Check if we actually needed to create any special virtual chars. - // if not, we can avoid the entire array allocation and just wrap - // the text of the token and pass that back. - - var textLength = endIndexExclusive - startIndexInclusive; - if (textLength == result.Count) - { - var sequence = VirtualCharSequence.Create(offset, tokenText); - return sequence.GetSubSequence(TextSpan.FromBounds(startIndexInclusive, endIndexExclusive)); - } - - return VirtualCharSequence.Create(result.ToImmutable()); - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/CSharpVirtualCharService.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/CSharpVirtualCharService.cs deleted file mode 100644 index cff00efeb8f8..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/CSharpVirtualCharService.cs +++ /dev/null @@ -1,544 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; -using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.CodeAnalysis.Text; -using Microsoft.CodeAnalysis.Utilities; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; - -internal class CSharpVirtualCharService : AbstractVirtualCharService -{ - public static readonly IVirtualCharService Instance = new CSharpVirtualCharService(); - - protected CSharpVirtualCharService() - { - } - - protected override bool IsMultiLineRawStringToken(SyntaxToken token) - { - if (token.Kind() is SyntaxKind.MultiLineRawStringLiteralToken or SyntaxKind.Utf8MultiLineRawStringLiteralToken) - { - return true; - } - if (token.Parent?.Parent is InterpolatedStringExpressionSyntax { StringStartToken.RawKind: (int)SyntaxKind.InterpolatedMultiLineRawStringStartToken }) - { - return true; - } - return false; - } - - protected override VirtualCharSequence TryConvertToVirtualCharsWorker(SyntaxToken token) - { - // C# preprocessor directives can contain string literals. However, these string literals do not behave - // like normal literals. Because they are used for paths (i.e. in a #line directive), the language does not - // do any escaping within them. i.e. if you have a \ it's just a \ Note that this is not a verbatim - // string. You can't put a double quote in it either, and you cannot have newlines and whatnot. - // - // We technically could convert this trivially to an array of virtual chars. After all, there would just be - // a 1:1 correspondence with the literal contents and the chars returned. However, we don't even both - // returning anything here. That's because there's no useful features we can offer here. Because there are - // no escape characters we won't classify any escape characters. And there is no way that these strings - // would be Regex/Json snippets. So it's easier to just bail out and return nothing. - if (IsInDirective(token.Parent)) - { - return default; - } - - Debug.Assert(!token.ContainsDiagnostics); - - switch (token.Kind()) - { - case SyntaxKind.CharacterLiteralToken: - return TryConvertStringToVirtualChars(token, "'", "'", escapeBraces: false); - - case SyntaxKind.StringLiteralToken: - return token.IsVerbatimStringLiteral() - ? TryConvertVerbatimStringToVirtualChars(token, "@\"", "\"", escapeBraces: false) - : TryConvertStringToVirtualChars(token, "\"", "\"", escapeBraces: false); - - case SyntaxKind.Utf8StringLiteralToken: - return token.IsVerbatimStringLiteral() - ? TryConvertVerbatimStringToVirtualChars(token, "@\"", "\"u8", escapeBraces: false) - : TryConvertStringToVirtualChars(token, "\"", "\"u8", escapeBraces: false); - - case SyntaxKind.SingleLineRawStringLiteralToken: - case SyntaxKind.Utf8SingleLineRawStringLiteralToken: - return TryConvertSingleLineRawStringToVirtualChars(token); - - case SyntaxKind.MultiLineRawStringLiteralToken: - case SyntaxKind.Utf8MultiLineRawStringLiteralToken: - return token.GetRequiredParent() is LiteralExpressionSyntax literalExpression - ? TryConvertMultiLineRawStringToVirtualChars(token, literalExpression, tokenIncludeDelimiters: true) - : default; - - case SyntaxKind.InterpolatedStringTextToken: - { - var parent = token.GetRequiredParent(); - var isFormatClause = parent is InterpolationFormatClauseSyntax; - if (isFormatClause) - { - parent = parent.GetRequiredParent(); - } - - var interpolatedString = (InterpolatedStringExpressionSyntax)parent.GetRequiredParent(); - - return interpolatedString.StringStartToken.Kind() switch - { - SyntaxKind.InterpolatedStringStartToken => TryConvertStringToVirtualChars(token, "", "", escapeBraces: true), - SyntaxKind.InterpolatedVerbatimStringStartToken => TryConvertVerbatimStringToVirtualChars(token, "", "", escapeBraces: true), - SyntaxKind.InterpolatedSingleLineRawStringStartToken => TryConvertSingleLineRawStringToVirtualChars(token), - SyntaxKind.InterpolatedMultiLineRawStringStartToken - // Format clauses must be single line, even when in a multi-line interpolation. - => isFormatClause - ? TryConvertSingleLineRawStringToVirtualChars(token) - : TryConvertMultiLineRawStringToVirtualChars(token, interpolatedString, tokenIncludeDelimiters: false), - _ => default, - }; - } - } - - return default; - } - - private static bool IsInDirective(SyntaxNode? node) - { - while (node != null) - { - if (node is DirectiveTriviaSyntax) - { - return true; - } - - node = node.GetParent(ascendOutOfTrivia: true); - } - - return false; - } - - private static VirtualCharSequence TryConvertVerbatimStringToVirtualChars(SyntaxToken token, string startDelimiter, string endDelimiter, bool escapeBraces) - => TryConvertSimpleDoubleQuoteString(token, startDelimiter, endDelimiter, escapeBraces); - - private static VirtualCharSequence TryConvertSingleLineRawStringToVirtualChars(SyntaxToken token) - { - var tokenText = token.Text; - var offset = token.SpanStart; - - var result = ImmutableList.CreateBuilder<VirtualChar>(); - - var startIndexInclusive = 0; - var endIndexExclusive = tokenText.Length; - - if (token.Kind() is SyntaxKind.Utf8SingleLineRawStringLiteralToken) - { - endIndexExclusive -= "u8".Length; - } - - if (token.Kind() is SyntaxKind.SingleLineRawStringLiteralToken or SyntaxKind.Utf8SingleLineRawStringLiteralToken) - { - if (!(tokenText[0] == '"')) - { - throw new InvalidOperationException("String should start with quote."); - } - - while (tokenText[startIndexInclusive] == '"') - { - // All quotes should be paired at the end - if (!(tokenText[endIndexExclusive - 1] == '"')) - { - throw new InvalidOperationException("String should end with quote."); - } - startIndexInclusive++; - endIndexExclusive--; - } - } - - for (var index = startIndexInclusive; index < endIndexExclusive;) - { - index += ConvertTextAtIndexToRune(tokenText, index, result, offset); - } - - return CreateVirtualCharSequence(tokenText, offset, startIndexInclusive, endIndexExclusive, result); - } - - /// <summary> - /// Creates the sequence for the <b>content</b> characters in this <paramref name="token"/>. This will not - /// include indentation whitespace that the language specifies is not part of the content. - /// </summary> - /// <param name="parentExpression">The containing expression for this token. This is needed so that we can - /// determine the indentation whitespace based on the last line of the containing multiline literal.</param> - /// <param name="tokenIncludeDelimiters">If this token includes the quote (<c>"</c>) characters for the - /// delimiters inside of it or not. If so, then those quotes will need to be skipped when determining the - /// content</param> - private static VirtualCharSequence TryConvertMultiLineRawStringToVirtualChars( - SyntaxToken token, ExpressionSyntax parentExpression, bool tokenIncludeDelimiters) - { - // if this is the first text content chunk of the multi-line literal. The first chunk contains the leading - // indentation of the line it's on (which thus must be trimmed), while all subsequent chunks do not (because - // they start right after some `{...}` interpolation - var isFirstChunk = - parentExpression is LiteralExpressionSyntax || - parentExpression is InterpolatedStringExpressionSyntax { Contents: var contents } && contents.First() == token.GetRequiredParent(); - - if (parentExpression.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error)) - { - return default; - } - - // Use the parent multi-line expression to determine what whitespace to remove from the start of each line. - var parentSourceText = parentExpression.SyntaxTree.GetText(); - var indentationLength = parentSourceText.Lines.GetLineFromPosition(parentExpression.Span.End).GetFirstNonWhitespaceOffset() ?? 0; - - // Create a source-text view over the token. This makes it very easy to treat the token as a set of lines - // that can be processed sensibly. - var tokenSourceText = SourceText.From(token.Text); - - // If we're on the very first chunk of the multi-line raw string literal, then we want to start on line 1 so - // we skip the space and newline that follow the initial `"""`. - var startLineInclusive = tokenIncludeDelimiters ? 1 : 0; - - // Similarly, if we're on the very last chunk of hte multi-line raw string literal, then we don't want to - // include the line contents for the line that has the final ` """` on it. - var lastLineExclusive = tokenIncludeDelimiters ? tokenSourceText.Lines.Count - 1 : tokenSourceText.Lines.Count; - - var result = ImmutableList.CreateBuilder<VirtualChar>(); - for (var lineNumber = startLineInclusive; lineNumber < lastLineExclusive; lineNumber++) - { - var currentLine = tokenSourceText.Lines[lineNumber]; - var lineSpan = currentLine.Span; - var lineStart = lineSpan.Start; - - // If we're on the second line onwards, we want to trim the indentation if we have it. We also always - // do this for the first line of the first chunk as that will contain the initial leading whitespace. - if (isFirstChunk || lineNumber > startLineInclusive) - { - lineStart = lineSpan.Length > indentationLength - ? lineSpan.Start + indentationLength - : lineSpan.End; - } - - // The last line of the last chunk does not include the final newline on the line. - var lineEnd = lineNumber == lastLineExclusive - 1 ? currentLine.End : currentLine.EndIncludingLineBreak; - - // Now that we've found the start and end portions of that line, convert all the characters within to - // virtual chars and return. - for (var i = lineStart; i < lineEnd;) - { - i += ConvertTextAtIndexToRune(tokenSourceText, i, result, token.SpanStart); - } - } - - return VirtualCharSequence.Create(result.ToImmutable()); - } - - private static VirtualCharSequence TryConvertStringToVirtualChars( - SyntaxToken token, string startDelimiter, string endDelimiter, bool escapeBraces) - { - var tokenText = token.Text; - if (startDelimiter.Length > 0 && !tokenText.StartsWith(startDelimiter, StringComparison.Ordinal)) - { - Debug.Fail("This should not be reachable as long as the compiler added no diagnostics."); - return default; - } - - if (endDelimiter.Length > 0 && !tokenText.EndsWith(endDelimiter, StringComparison.OrdinalIgnoreCase)) - { - Debug.Fail("This should not be reachable as long as the compiler added no diagnostics."); - return default; - } - - var startIndexInclusive = startDelimiter.Length; - var endIndexExclusive = tokenText.Length - endDelimiter.Length; - - // Do things in two passes. First, convert everything in the string to a 16-bit-char+span. Then walk - // again, trying to create Runes from the 16-bit-chars. We do this to simplify complex cases where we may - // have escapes and non-escapes mixed together. - - var charResults = new List<(char ch, TextSpan span)>(); - - // First pass, just convert everything in the string (i.e. escapes) to plain 16-bit characters. - var offset = token.SpanStart; - for (var index = startIndexInclusive; index < endIndexExclusive;) - { - var ch = tokenText[index]; - if (ch == '\\') - { - if (!TryAddEscape(charResults, tokenText, offset, index)) - { - return default; - } - - index += charResults.Last().span.Length; - } - else if (escapeBraces && IsOpenOrCloseBrace(ch)) - { - if (!IsLegalBraceEscape(tokenText, index, offset, out var braceSpan)) - { - return default; - } - - charResults.Add((ch, braceSpan)); - index += charResults.Last().span.Length; - } - else - { - charResults.Add((ch, new TextSpan(offset + index, 1))); - index++; - } - } - - return CreateVirtualCharSequence(tokenText, offset, startIndexInclusive, endIndexExclusive, charResults); - } - - private static VirtualCharSequence CreateVirtualCharSequence( - string tokenText, int offset, int startIndexInclusive, int endIndexExclusive, List<(char ch, TextSpan span)> charResults) - { - // Second pass. Convert those characters to Runes. - var runeResults = ImmutableList.CreateBuilder<VirtualChar>(); - - ConvertCharactersToRunes(charResults, runeResults); - - return CreateVirtualCharSequence(tokenText, offset, startIndexInclusive, endIndexExclusive, runeResults); - } - - private static void ConvertCharactersToRunes(List<(char ch, TextSpan span)> charResults, ImmutableList<VirtualChar>.Builder runeResults) - { - for (var i = 0; i < charResults.Count;) - { - var (ch, span) = charResults[i]; - - // First, see if this was a valid single char that can become a Rune. - if (Rune.TryCreate(ch, out var rune)) - { - runeResults.Add(VirtualChar.Create(rune, span)); - i++; - continue; - } - - // Next, see if we got at least a surrogate pair that can be converted into a Rune. - if (i + 1 < charResults.Count) - { - var (nextCh, nextSpan) = charResults[i + 1]; - if (Rune.TryCreate(ch, nextCh, out rune)) - { - runeResults.Add(VirtualChar.Create(rune, TextSpan.FromBounds(span.Start, nextSpan.End))); - i += 2; - continue; - } - } - - // Had an unpaired surrogate. - Debug.Assert(char.IsSurrogate(ch)); - runeResults.Add(VirtualChar.Create(ch, span)); - i++; - } - } - - private static bool TryAddEscape( - List<(char ch, TextSpan span)> result, string tokenText, int offset, int index) - { - // Copied from Lexer.ScanEscapeSequence. - Debug.Assert(tokenText[index] == '\\'); - - return TryAddSingleCharacterEscape(result, tokenText, offset, index) || - TryAddMultiCharacterEscape(result, tokenText, offset, index); - } - - public override bool TryGetEscapeCharacter(VirtualChar ch, out char escapedChar) - => ch.TryGetEscapeCharacter(out escapedChar); - - private static bool TryAddSingleCharacterEscape( - List<(char ch, TextSpan span)> result, string tokenText, int offset, int index) - { - // Copied from Lexer.ScanEscapeSequence. - Debug.Assert(tokenText[index] == '\\'); - - var ch = tokenText[index + 1]; - - // Keep in sync with EscapeForRegularString - switch (ch) - { - // escaped characters that translate to themselves - case '\'': - case '"': - case '\\': - break; - // translate escapes as per C# spec 2.4.4.4 - case '0': ch = '\0'; break; - case 'a': ch = '\a'; break; - case 'b': ch = '\b'; break; - case 'f': ch = '\f'; break; - case 'n': ch = '\n'; break; - case 'r': ch = '\r'; break; - case 't': ch = '\t'; break; - case 'v': ch = '\v'; break; - default: - return false; - } - - result.Add((ch, new TextSpan(offset + index, 2))); - return true; - } - - private static bool TryAddMultiCharacterEscape( - List<(char ch, TextSpan span)> result, string tokenText, int offset, int index) - { - // Copied from Lexer.ScanEscapeSequence. - Debug.Assert(tokenText[index] == '\\'); - - var ch = tokenText[index + 1]; - switch (ch) - { - case 'x': - case 'u': - case 'U': - return TryAddMultiCharacterEscape(result, tokenText, offset, index, ch); - default: - Debug.Fail("This should not be reachable as long as the compiler added no diagnostics."); - return false; - } - } - - private static bool TryAddMultiCharacterEscape( - List<(char ch, TextSpan span)> result, string tokenText, int offset, int index, char character) - { - var startIndex = index; - Debug.Assert(tokenText[index] == '\\'); - - // skip past the / and the escape type. - index += 2; - if (character == 'U') - { - // 8 character escape. May represent 1 or 2 actual chars. - uint uintChar = 0; - - if (!IsHexDigit(tokenText[index])) - { - Debug.Fail("This should not be reachable as long as the compiler added no diagnostics."); - return false; - } - - for (var i = 0; i < 8; i++) - { - character = tokenText[index + i]; - if (!IsHexDigit(character)) - { - Debug.Fail("This should not be reachable as long as the compiler added no diagnostics."); - return false; - } - - uintChar = (uint)((uintChar << 4) + HexValue(character)); - } - - // Copied from Lexer.cs and SlidingTextWindow.cs - - if (uintChar > 0x0010FFFF) - { - Debug.Fail("This should not be reachable as long as the compiler added no diagnostics."); - return false; - } - - if (uintChar < 0x00010000) - { - // something like \U0000000A - // - // Represents a single char value. - result.Add(((char)uintChar, new TextSpan(startIndex + offset, 2 + 8))); - return true; - } - else - { - Debug.Assert(uintChar is > 0x0000FFFF and <= 0x0010FFFF); - var lowSurrogate = (uintChar - 0x00010000) % 0x0400 + 0xDC00; - var highSurrogate = (uintChar - 0x00010000) / 0x0400 + 0xD800; - - // Encode this as a surrogate pair. - var pos = startIndex + offset; - result.Add(((char)highSurrogate, new TextSpan(pos, 0))); - result.Add(((char)lowSurrogate, new TextSpan(pos, 2 + 8))); - return true; - } - } - else if (character == 'u') - { - // 4 character escape representing one char. - - var intChar = 0; - if (!IsHexDigit(tokenText[index])) - { - Debug.Fail("This should not be reachable as long as the compiler added no diagnostics."); - return false; - } - - for (var i = 0; i < 4; i++) - { - var ch2 = tokenText[index + i]; - if (!IsHexDigit(ch2)) - { - Debug.Fail("This should not be reachable as long as the compiler added no diagnostics."); - return false; - } - - intChar = (intChar << 4) + HexValue(ch2); - } - - character = (char)intChar; - result.Add((character, new TextSpan(startIndex + offset, 2 + 4))); - return true; - } - else - { - Debug.Assert(character == 'x'); - // Variable length (up to 4 chars) hexadecimal escape. - - var intChar = 0; - if (!IsHexDigit(tokenText[index])) - { - Debug.Fail("This should not be reachable as long as the compiler added no diagnostics."); - return false; - } - - var endIndex = index; - for (var i = 0; i < 4 && endIndex < tokenText.Length; i++) - { - var ch2 = tokenText[index + i]; - if (!IsHexDigit(ch2)) - { - // This is possible. These escape sequences are variable length. - break; - } - - intChar = (intChar << 4) + HexValue(ch2); - endIndex++; - } - - character = (char)intChar; - result.Add((character, TextSpan.FromBounds(startIndex + offset, endIndex + offset))); - return true; - } - } - - private static int HexValue(char c) - { - Debug.Assert(IsHexDigit(c)); - return c is >= '0' and <= '9' ? c - '0' : (c & 0xdf) - 'A' + 10; - } - - private static bool IsHexDigit(char c) - { - return c is >= '0' and <= '9' or - >= 'A' and <= 'F' or - >= 'a' and <= 'f'; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/IVirtualCharService.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/IVirtualCharService.cs deleted file mode 100644 index 3ecbf18d841a..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/IVirtualCharService.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; - -/// <summary> -/// Helper service that takes the raw text of a string token and produces the individual -/// characters that raw string token represents (i.e. with escapes collapsed). The difference -/// between this and the result from token.ValueText is that for each collapsed character -/// returned the original span of text in the original token can be found. i.e. if you had the -/// following in C#: -/// -/// "G\u006fo" -/// -/// Then you'd get back: -/// -/// 'G' -> [0, 1) 'o' -> [1, 7) 'o' -> [7, 1) -/// -/// This allows for embedded language processing that can refer back to the users' original code -/// instead of the escaped value we're processing. -/// </summary> -internal interface IVirtualCharService -{ - /// <summary> - /// <para> - /// Takes in a string token and return the <see cref="VirtualChar"/>s corresponding to each - /// char of the tokens <see cref="SyntaxToken.ValueText"/>. In other words, for each char - /// in ValueText there will be a VirtualChar in the resultant array. Each VirtualChar will - /// specify what char the language considers them to represent, as well as the span of text - /// in the original <see cref="SourceText"/> that the language created that char from. - /// </para> - /// <para> - /// For most chars this will be a single character span. i.e. 'c' -> 'c'. However, for - /// escapes this may be a multi character span. i.e. 'c' -> '\u0063' - /// </para> - /// <para> - /// If the token is not a string literal token, or the string literal has any diagnostics on - /// it, then <see langword="default"/> will be returned. Additionally, because a - /// VirtualChar can only represent a single char, while some escape sequences represent - /// multiple chars, <see langword="default"/> will also be returned in those cases. All - /// these cases could be relaxed in the future. But they greatly simplify the - /// implementation. - /// </para> - /// <para> - /// If this function succeeds, certain invariants will hold. First, each character in the - /// sequence of characters in <paramref name="token"/>.ValueText will become a single - /// VirtualChar in the result array with a matching <see cref="VirtualChar.Rune"/> property. - /// Similarly, each VirtualChar's <see cref="VirtualChar.Span"/> will abut each other, and - /// the union of all of them will cover the span of the token's <see - /// cref="SyntaxToken.Text"/> - /// *not* including the start and quotes. - /// </para> - /// <para> - /// In essence the VirtualChar array acts as the information explaining how the <see - /// cref="SyntaxToken.Text"/> of the token between the quotes maps to each character in the - /// token's <see cref="SyntaxToken.ValueText"/>. - /// </para> - /// </summary> - VirtualCharSequence TryConvertToVirtualChars(SyntaxToken token); - - /// <summary> - /// Produces the appropriate escape version of <paramref name="ch"/> to be placed in a - /// normal string literal. For example if <paramref name="ch"/> is the <c>tab</c> - /// character, then this would produce <c>t</c> as <c>\t</c> is what would go into a string - /// literal. - /// </summary> - bool TryGetEscapeCharacter(VirtualChar ch, out char escapeChar); -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/README.md b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/README.md deleted file mode 100644 index dd089a2e0bde..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# About - -Virtual chars code is copied from Roslyn: - -https://github.com/dotnet/roslyn/tree/b0de0c8e00ebf85db3c3884f2d81dfc3cb2d5a9d/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EmbeddedLanguages/VirtualChars \ No newline at end of file diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/RuneExtensions.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/RuneExtensions.cs deleted file mode 100644 index 7bf18176456a..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/RuneExtensions.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; - -internal static class Extensions -{ - public static bool TryGetEscapeCharacter(this VirtualChar ch, out char escapedChar) - => TryGetEscapeCharacter(ch.Value, out escapedChar); - - public static bool TryGetEscapeCharacter(Rune rune, out char escapedChar) - => TryGetEscapeCharacter(rune.Value, out escapedChar); - - private static bool TryGetEscapeCharacter(int value, out char escapedChar) - { - // Keep in sync with CSharpVirtualCharService.TryAddSingleCharacterEscape - switch (value) - { - // Note: we don't care about single quote as that doesn't need to be escaped when - // producing a normal C# string literal. - - // case '\'': - - // escaped characters that translate to themselves. - case '"': escapedChar = '"'; return true; - case '\\': escapedChar = '\\'; return true; - - // translate escapes as per C# spec 2.4.4.4 - case '\0': escapedChar = '0'; return true; - case '\a': escapedChar = 'a'; return true; - case '\b': escapedChar = 'b'; return true; - case '\f': escapedChar = 'f'; return true; - case '\n': escapedChar = 'n'; return true; - case '\r': escapedChar = 'r'; return true; - case '\t': escapedChar = 't'; return true; - case '\v': escapedChar = 'v'; return true; - } - - escapedChar = default; - return false; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/Rune.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/Rune.cs deleted file mode 100644 index f8e47c1be127..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/Rune.cs +++ /dev/null @@ -1,1439 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// <auto-generated/> - -#nullable disable - -// Copied from https://github.com/dotnet/runtime/blob/c73774b53944a6007ee85f138e3ff3d3297846ea/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs#L1 -// So that we can use Runes in netstandard 2.0 - -#if !NETCOREAPP - -using System.Buffers; -using System.Diagnostics; -using System.Globalization; -using System.Runtime.CompilerServices; -using System.Text.Unicode; - -namespace System.Text -{ - /// <summary> - /// Represents a Unicode scalar value ([ U+0000..U+D7FF ], inclusive; or [ U+E000..U+10FFFF ], inclusive). - /// </summary> - /// <remarks> - /// This type's constructors and conversion operators validate the input, so consumers can call the APIs - /// assuming that the underlying <see cref="Rune"/> instance is well-formed. - /// </remarks> - [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal readonly struct Rune : IComparable<Rune>, IEquatable<Rune> - { - private const char HighSurrogateStart = '\ud800'; - private const char LowSurrogateStart = '\udc00'; - private const int HighSurrogateRange = 0x3FF; - - private const byte IsWhiteSpaceFlag = 0x80; - private const byte IsLetterOrDigitFlag = 0x40; - private const byte UnicodeCategoryMask = 0x1F; - - // Contains information about the ASCII character range [ U+0000..U+007F ], with: - // - 0x80 bit if set means 'is whitespace' - // - 0x40 bit if set means 'is letter or digit' - // - 0x20 bit is reserved for future use - // - bottom 5 bits are the UnicodeCategory of the character - private static ReadOnlySpan<byte> AsciiCharInfo => new byte[] - { - 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x8E, 0x8E, 0x8E, 0x8E, 0x8E, 0x0E, 0x0E, // U+0000..U+000F - 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, // U+0010..U+001F - 0x8B, 0x18, 0x18, 0x18, 0x1A, 0x18, 0x18, 0x18, 0x14, 0x15, 0x18, 0x19, 0x18, 0x13, 0x18, 0x18, // U+0020..U+002F - 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48, 0x18, 0x18, 0x19, 0x19, 0x19, 0x18, // U+0030..U+003F - 0x18, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, // U+0040..U+004F - 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x14, 0x18, 0x15, 0x1B, 0x12, // U+0050..U+005F - 0x1B, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, // U+0060..U+006F - 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x14, 0x19, 0x15, 0x19, 0x0E, // U+0070..U+007F - }; - - private readonly uint _value; - - /// <summary> - /// Creates a <see cref="Rune"/> from the provided UTF-16 code unit. - /// </summary> - /// <exception cref="ArgumentOutOfRangeException"> - /// If <paramref name="ch"/> represents a UTF-16 surrogate code point - /// U+D800..U+DFFF, inclusive. - /// </exception> - public Rune(char ch) - { - uint expanded = ch; - if (UnicodeUtility.IsSurrogateCodePoint(expanded)) - { - throw new ArgumentOutOfRangeException(nameof(ch)); - } - _value = expanded; - } - - /// <summary> - /// Creates a <see cref="Rune"/> from the provided UTF-16 surrogate pair. - /// </summary> - /// <exception cref="ArgumentOutOfRangeException"> - /// If <paramref name="highSurrogate"/> does not represent a UTF-16 high surrogate code point - /// or <paramref name="lowSurrogate"/> does not represent a UTF-16 low surrogate code point. - /// </exception> - public Rune(char highSurrogate, char lowSurrogate) - : this((uint)char.ConvertToUtf32(highSurrogate, lowSurrogate), false) - { - } - - /// <summary> - /// Creates a <see cref="Rune"/> from the provided Unicode scalar value. - /// </summary> - /// <exception cref="ArgumentOutOfRangeException"> - /// If <paramref name="value"/> does not represent a value Unicode scalar value. - /// </exception> - public Rune(int value) - : this((uint)value) - { - } - - /// <summary> - /// Creates a <see cref="Rune"/> from the provided Unicode scalar value. - /// </summary> - /// <exception cref="ArgumentOutOfRangeException"> - /// If <paramref name="value"/> does not represent a value Unicode scalar value. - /// </exception> - public Rune(uint value) - { - if (!UnicodeUtility.IsValidUnicodeScalar(value)) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - _value = value; - } - - // non-validating ctor - private Rune(uint scalarValue, bool unused) - { - UnicodeDebug.AssertIsValidScalar(scalarValue); - _value = scalarValue; - } - - public static bool operator ==(Rune left, Rune right) => left._value == right._value; - - public static bool operator !=(Rune left, Rune right) => left._value != right._value; - - public static bool operator <(Rune left, Rune right) => left._value < right._value; - - public static bool operator <=(Rune left, Rune right) => left._value <= right._value; - - public static bool operator >(Rune left, Rune right) => left._value > right._value; - - public static bool operator >=(Rune left, Rune right) => left._value >= right._value; - - // Operators below are explicit because they may throw. - - public static explicit operator Rune(char ch) => new Rune(ch); - - public static explicit operator Rune(uint value) => new Rune(value); - - public static explicit operator Rune(int value) => new Rune(value); - - // Displayed as "'<char>' (U+XXXX)"; e.g., "'e' (U+0065)" - private string DebuggerDisplay => FormattableString.Invariant($"U+{_value:X4} '{(IsValid(_value) ? ToString() : "\uFFFD")}'"); - - /// <summary> - /// Returns true if and only if this scalar value is ASCII ([ U+0000..U+007F ]) - /// and therefore representable by a single UTF-8 code unit. - /// </summary> - public bool IsAscii => UnicodeUtility.IsAsciiCodePoint(_value); - - /// <summary> - /// Returns true if and only if this scalar value is within the BMP ([ U+0000..U+FFFF ]) - /// and therefore representable by a single UTF-16 code unit. - /// </summary> - public bool IsBmp => UnicodeUtility.IsBmpCodePoint(_value); - - /// <summary> - /// Returns the Unicode plane (0 to 16, inclusive) which contains this scalar. - /// </summary> - public int Plane => UnicodeUtility.GetPlane(_value); - - /// <summary> - /// A <see cref="Rune"/> instance that represents the Unicode replacement character U+FFFD. - /// </summary> - public static Rune ReplacementChar => UnsafeCreate(UnicodeUtility.ReplacementChar); - - /// <summary> - /// Returns the length in code units (<see cref="char"/>) of the - /// UTF-16 sequence required to represent this scalar value. - /// </summary> - /// <remarks> - /// The return value will be 1 or 2. - /// </remarks> - public int Utf16SequenceLength => UnicodeUtility.GetUtf16SequenceLength(_value); - - /// <summary> - /// Returns the length in code units of the - /// UTF-8 sequence required to represent this scalar value. - /// </summary> - /// <remarks> - /// The return value will be 1 through 4, inclusive. - /// </remarks> - public int Utf8SequenceLength => UnicodeUtility.GetUtf8SequenceLength(_value); - - /// <summary> - /// Returns the Unicode scalar value as an integer. - /// </summary> - public int Value => (int)_value; - -#if SYSTEM_PRIVATE_CORELIB - private static Rune ChangeCaseCultureAware(Rune rune, TextInfo textInfo, bool toUpper) - { - Debug.Assert(!GlobalizationMode.Invariant, "This should've been checked by the caller."); - Debug.Assert(textInfo != null, "This should've been checked by the caller."); - - Span<char> original = stackalloc char[2]; // worst case scenario = 2 code units (for a surrogate pair) - Span<char> modified = stackalloc char[2]; // case change should preserve UTF-16 code unit count - - int charCount = rune.EncodeToUtf16(original); - original = original.Slice(0, charCount); - modified = modified.Slice(0, charCount); - - if (toUpper) - { - textInfo.ChangeCaseToUpper(original, modified); - } - else - { - textInfo.ChangeCaseToLower(original, modified); - } - - // We use simple case folding rules, which disallows moving between the BMP and supplementary - // planes when performing a case conversion. The helper methods which reconstruct a Rune - // contain debug asserts for this condition. - - if (rune.IsBmp) - { - return UnsafeCreate(modified[0]); - } - else - { - return UnsafeCreate(UnicodeUtility.GetScalarFromUtf16SurrogatePair(modified[0], modified[1])); - } - } -#else - private static Rune ChangeCaseCultureAware(Rune rune, CultureInfo culture, bool toUpper) - { -#if false - Debug.Assert(!GlobalizationMode.Invariant, "This should've been checked by the caller."); -#endif - Debug.Assert(culture != null, "This should've been checked by the caller."); - - Span<char> original = stackalloc char[2]; // worst case scenario = 2 code units (for a surrogate pair) - Span<char> modified = stackalloc char[2]; // case change should preserve UTF-16 code unit count - - int charCount = rune.EncodeToUtf16(original); - original = original.Slice(0, charCount); - modified = modified.Slice(0, charCount); - - if (toUpper) - { - MemoryExtensions.ToUpper(original, modified, culture); - } - else - { - MemoryExtensions.ToLower(original, modified, culture); - } - - // We use simple case folding rules, which disallows moving between the BMP and supplementary - // planes when performing a case conversion. The helper methods which reconstruct a Rune - // contain debug asserts for this condition. - - if (rune.IsBmp) - { - return UnsafeCreate(modified[0]); - } - else - { - return UnsafeCreate(UnicodeUtility.GetScalarFromUtf16SurrogatePair(modified[0], modified[1])); - } - } -#endif - - public int CompareTo(Rune other) => _value.CompareTo(other._value); - - /// <summary> - /// Decodes the <see cref="Rune"/> at the beginning of the provided UTF-16 source buffer. - /// </summary> - /// <returns> - /// <para> - /// If the source buffer begins with a valid UTF-16 encoded scalar value, returns <see cref="OperationStatus.Done"/>, - /// and outs via <paramref name="result"/> the decoded <see cref="Rune"/> and via <paramref name="charsConsumed"/> the - /// number of <see langword="char"/>s used in the input buffer to encode the <see cref="Rune"/>. - /// </para> - /// <para> - /// If the source buffer is empty or contains only a standalone UTF-16 high surrogate character, returns <see cref="OperationStatus.NeedMoreData"/>, - /// and outs via <paramref name="result"/> <see cref="ReplacementChar"/> and via <paramref name="charsConsumed"/> the length of the input buffer. - /// </para> - /// <para> - /// If the source buffer begins with an ill-formed UTF-16 encoded scalar value, returns <see cref="OperationStatus.InvalidData"/>, - /// and outs via <paramref name="result"/> <see cref="ReplacementChar"/> and via <paramref name="charsConsumed"/> the number of - /// <see langword="char"/>s used in the input buffer to encode the ill-formed sequence. - /// </para> - /// </returns> - /// <remarks> - /// The general calling convention is to call this method in a loop, slicing the <paramref name="source"/> buffer by - /// <paramref name="charsConsumed"/> elements on each iteration of the loop. On each iteration of the loop <paramref name="result"/> - /// will contain the real scalar value if successfully decoded, or it will contain <see cref="ReplacementChar"/> if - /// the data could not be successfully decoded. This pattern provides convenient automatic U+FFFD substitution of - /// invalid sequences while iterating through the loop. - /// </remarks> - public static OperationStatus DecodeFromUtf16(ReadOnlySpan<char> source, out Rune result, out int charsConsumed) - { - if (!source.IsEmpty) - { - // First, check for the common case of a BMP scalar value. - // If this is correct, return immediately. - - char firstChar = source[0]; - if (TryCreate(firstChar, out result)) - { - charsConsumed = 1; - return OperationStatus.Done; - } - - // First thing we saw was a UTF-16 surrogate code point. - // Let's optimistically assume for now it's a high surrogate and hope - // that combining it with the next char yields useful results. - - if (1 < (uint)source.Length) - { - char secondChar = source[1]; - if (TryCreate(firstChar, secondChar, out result)) - { - // Success! Formed a supplementary scalar value. - charsConsumed = 2; - return OperationStatus.Done; - } - else - { - // Either the first character was a low surrogate, or the second - // character was not a low surrogate. This is an error. - goto InvalidData; - } - } - else if (!char.IsHighSurrogate(firstChar)) - { - // Quick check to make sure we're not going to report NeedMoreData for - // a single-element buffer where the data is a standalone low surrogate - // character. Since no additional data will ever make this valid, we'll - // report an error immediately. - goto InvalidData; - } - } - - // If we got to this point, the input buffer was empty, or the buffer - // was a single element in length and that element was a high surrogate char. - - charsConsumed = source.Length; - result = ReplacementChar; - return OperationStatus.NeedMoreData; - -InvalidData: - - charsConsumed = 1; // maximal invalid subsequence for UTF-16 is always a single code unit in length - result = ReplacementChar; - return OperationStatus.InvalidData; - } - - /// <summary> - /// Decodes the <see cref="Rune"/> at the beginning of the provided UTF-8 source buffer. - /// </summary> - /// <returns> - /// <para> - /// If the source buffer begins with a valid UTF-8 encoded scalar value, returns <see cref="OperationStatus.Done"/>, - /// and outs via <paramref name="result"/> the decoded <see cref="Rune"/> and via <paramref name="bytesConsumed"/> the - /// number of <see langword="byte"/>s used in the input buffer to encode the <see cref="Rune"/>. - /// </para> - /// <para> - /// If the source buffer is empty or contains only a partial UTF-8 subsequence, returns <see cref="OperationStatus.NeedMoreData"/>, - /// and outs via <paramref name="result"/> <see cref="ReplacementChar"/> and via <paramref name="bytesConsumed"/> the length of the input buffer. - /// </para> - /// <para> - /// If the source buffer begins with an ill-formed UTF-8 encoded scalar value, returns <see cref="OperationStatus.InvalidData"/>, - /// and outs via <paramref name="result"/> <see cref="ReplacementChar"/> and via <paramref name="bytesConsumed"/> the number of - /// <see langword="char"/>s used in the input buffer to encode the ill-formed sequence. - /// </para> - /// </returns> - /// <remarks> - /// The general calling convention is to call this method in a loop, slicing the <paramref name="source"/> buffer by - /// <paramref name="bytesConsumed"/> elements on each iteration of the loop. On each iteration of the loop <paramref name="result"/> - /// will contain the real scalar value if successfully decoded, or it will contain <see cref="ReplacementChar"/> if - /// the data could not be successfully decoded. This pattern provides convenient automatic U+FFFD substitution of - /// invalid sequences while iterating through the loop. - /// </remarks> - public static OperationStatus DecodeFromUtf8(ReadOnlySpan<byte> source, out Rune result, out int bytesConsumed) - { - // This method follows the Unicode Standard's recommendation for detecting - // the maximal subpart of an ill-formed subsequence. See The Unicode Standard, - // Ch. 3.9 for more details. In summary, when reporting an invalid subsequence, - // it tries to consume as many code units as possible as long as those code - // units constitute the beginning of a longer well-formed subsequence per Table 3-7. - - int index = 0; - - // Try reading input[0]. - - if ((uint)index >= (uint)source.Length) - { - goto NeedsMoreData; - } - - uint tempValue = source[index]; - if (!UnicodeUtility.IsAsciiCodePoint(tempValue)) - { - goto NotAscii; - } - -Finish: - - bytesConsumed = index + 1; - Debug.Assert(1 <= bytesConsumed && bytesConsumed <= 4); // Valid subsequences are always length [1..4] - result = UnsafeCreate(tempValue); - return OperationStatus.Done; - -NotAscii: - -// Per Table 3-7, the beginning of a multibyte sequence must be a code unit in -// the range [C2..F4]. If it's outside of that range, it's either a standalone -// continuation byte, or it's an overlong two-byte sequence, or it's an out-of-range -// four-byte sequence. - - if (!UnicodeUtility.IsInRangeInclusive(tempValue, 0xC2, 0xF4)) - { - goto FirstByteInvalid; - } - - tempValue = (tempValue - 0xC2) << 6; - - // Try reading input[1]. - - index++; - if ((uint)index >= (uint)source.Length) - { - goto NeedsMoreData; - } - - // Continuation bytes are of the form [10xxxxxx], which means that their two's - // complement representation is in the range [-65..-128]. This allows us to - // perform a single comparison to see if a byte is a continuation byte. - - int thisByteSignExtended = (sbyte)source[index]; - if (thisByteSignExtended >= -64) - { - goto Invalid; - } - - tempValue += (uint)thisByteSignExtended; - tempValue += 0x80; // remove the continuation byte marker - tempValue += (0xC2 - 0xC0) << 6; // remove the leading byte marker - - if (tempValue < 0x0800) - { - Debug.Assert(UnicodeUtility.IsInRangeInclusive(tempValue, 0x0080, 0x07FF)); - goto Finish; // this is a valid 2-byte sequence - } - - // This appears to be a 3- or 4-byte sequence. Since per Table 3-7 we now have - // enough information (from just two code units) to detect overlong or surrogate - // sequences, we need to perform these checks now. - - if (!UnicodeUtility.IsInRangeInclusive(tempValue, ((0xE0 - 0xC0) << 6) + (0xA0 - 0x80), ((0xF4 - 0xC0) << 6) + (0x8F - 0x80))) - { - // The first two bytes were not in the range [[E0 A0]..[F4 8F]]. - // This is an overlong 3-byte sequence or an out-of-range 4-byte sequence. - goto Invalid; - } - - if (UnicodeUtility.IsInRangeInclusive(tempValue, ((0xED - 0xC0) << 6) + (0xA0 - 0x80), ((0xED - 0xC0) << 6) + (0xBF - 0x80))) - { - // This is a UTF-16 surrogate code point, which is invalid in UTF-8. - goto Invalid; - } - - if (UnicodeUtility.IsInRangeInclusive(tempValue, ((0xF0 - 0xC0) << 6) + (0x80 - 0x80), ((0xF0 - 0xC0) << 6) + (0x8F - 0x80))) - { - // This is an overlong 4-byte sequence. - goto Invalid; - } - - // The first two bytes were just fine. We don't need to perform any other checks - // on the remaining bytes other than to see that they're valid continuation bytes. - - // Try reading input[2]. - - index++; - if ((uint)index >= (uint)source.Length) - { - goto NeedsMoreData; - } - - thisByteSignExtended = (sbyte)source[index]; - if (thisByteSignExtended >= -64) - { - goto Invalid; // this byte is not a UTF-8 continuation byte - } - - tempValue <<= 6; - tempValue += (uint)thisByteSignExtended; - tempValue += 0x80; // remove the continuation byte marker - tempValue -= (0xE0 - 0xC0) << 12; // remove the leading byte marker - - if (tempValue <= 0xFFFF) - { - Debug.Assert(UnicodeUtility.IsInRangeInclusive(tempValue, 0x0800, 0xFFFF)); - goto Finish; // this is a valid 3-byte sequence - } - - // Try reading input[3]. - - index++; - if ((uint)index >= (uint)source.Length) - { - goto NeedsMoreData; - } - - thisByteSignExtended = (sbyte)source[index]; - if (thisByteSignExtended >= -64) - { - goto Invalid; // this byte is not a UTF-8 continuation byte - } - - tempValue <<= 6; - tempValue += (uint)thisByteSignExtended; - tempValue += 0x80; // remove the continuation byte marker - tempValue -= (0xF0 - 0xE0) << 18; // remove the leading byte marker - - UnicodeDebug.AssertIsValidSupplementaryPlaneScalar(tempValue); - goto Finish; // this is a valid 4-byte sequence - -FirstByteInvalid: - - index = 1; // Invalid subsequences are always at least length 1. - -Invalid: - - Debug.Assert(1 <= index && index <= 3); // Invalid subsequences are always length 1..3 - bytesConsumed = index; - result = ReplacementChar; - return OperationStatus.InvalidData; - -NeedsMoreData: - - Debug.Assert(0 <= index && index <= 3); // Incomplete subsequences are always length 0..3 - bytesConsumed = index; - result = ReplacementChar; - return OperationStatus.NeedMoreData; - } - - /// <summary> - /// Decodes the <see cref="Rune"/> at the end of the provided UTF-16 source buffer. - /// </summary> - /// <remarks> - /// This method is very similar to <see cref="DecodeFromUtf16(ReadOnlySpan{char}, out Rune, out int)"/>, but it allows - /// the caller to loop backward instead of forward. The typical calling convention is that on each iteration - /// of the loop, the caller should slice off the final <paramref name="charsConsumed"/> elements of - /// the <paramref name="source"/> buffer. - /// </remarks> - public static OperationStatus DecodeLastFromUtf16(ReadOnlySpan<char> source, out Rune result, out int charsConsumed) - { - int index = source.Length - 1; - if ((uint)index < (uint)source.Length) - { - // First, check for the common case of a BMP scalar value. - // If this is correct, return immediately. - - char finalChar = source[index]; - if (TryCreate(finalChar, out result)) - { - charsConsumed = 1; - return OperationStatus.Done; - } - - if (char.IsLowSurrogate(finalChar)) - { - // The final character was a UTF-16 low surrogate code point. - // This must be preceded by a UTF-16 high surrogate code point, otherwise - // we have a standalone low surrogate, which is always invalid. - - index--; - if ((uint)index < (uint)source.Length) - { - char penultimateChar = source[index]; - if (TryCreate(penultimateChar, finalChar, out result)) - { - // Success! Formed a supplementary scalar value. - charsConsumed = 2; - return OperationStatus.Done; - } - } - - // If we got to this point, we saw a standalone low surrogate - // and must report an error. - - charsConsumed = 1; // standalone surrogate - result = ReplacementChar; - return OperationStatus.InvalidData; - } - } - - // If we got this far, the source buffer was empty, or the source buffer ended - // with a UTF-16 high surrogate code point. These aren't errors since they could - // be valid given more input data. - - charsConsumed = (int)((uint)(-source.Length) >> 31); // 0 -> 0, all other lengths -> 1 - result = ReplacementChar; - return OperationStatus.NeedMoreData; - } - - /// <summary> - /// Decodes the <see cref="Rune"/> at the end of the provided UTF-8 source buffer. - /// </summary> - /// <remarks> - /// This method is very similar to <see cref="DecodeFromUtf8(ReadOnlySpan{byte}, out Rune, out int)"/>, but it allows - /// the caller to loop backward instead of forward. The typical calling convention is that on each iteration - /// of the loop, the caller should slice off the final <paramref name="bytesConsumed"/> elements of - /// the <paramref name="source"/> buffer. - /// </remarks> - public static OperationStatus DecodeLastFromUtf8(ReadOnlySpan<byte> source, out Rune value, out int bytesConsumed) - { - int index = source.Length - 1; - if ((uint)index < (uint)source.Length) - { - // The buffer contains at least one byte. Let's check the fast case where the - // buffer ends with an ASCII byte. - - uint tempValue = source[index]; - if (UnicodeUtility.IsAsciiCodePoint(tempValue)) - { - bytesConsumed = 1; - value = UnsafeCreate(tempValue); - return OperationStatus.Done; - } - - // If the final byte is not an ASCII byte, we may be beginning or in the middle of - // a UTF-8 multi-code unit sequence. We need to back up until we see the start of - // the multi-code unit sequence; we can detect the leading byte because all multi-byte - // sequences begin with a byte whose 0x40 bit is set. Since all multi-byte sequences - // are no greater than 4 code units in length, we only need to search back a maximum - // of four bytes. - - if (((byte)tempValue & 0x40) != 0) - { - // This is a UTF-8 leading byte. We'll do a forward read from here. - // It'll return invalid (if given C0, F5, etc.) or incomplete. Both are fine. - - return DecodeFromUtf8(source.Slice(index), out value, out bytesConsumed); - } - - // If we got to this point, the final byte was a UTF-8 continuation byte. - // Let's check the three bytes immediately preceding this, looking for the starting byte. - - for (int i = 3; i > 0; i--) - { - index--; - if ((uint)index >= (uint)source.Length) - { - goto Invalid; // out of data - } - - // The check below will get hit for ASCII (values 00..7F) and for UTF-8 starting bytes - // (bits 0xC0 set, values C0..FF). In two's complement this is the range [-64..127]. - // It's just a fast way for us to terminate the search. - - if ((sbyte)source[index] >= -64) - { - goto ForwardDecode; - } - } - -Invalid: - -// If we got to this point, either: -// - the last 4 bytes of the input buffer are continuation bytes; -// - the entire input buffer (if fewer than 4 bytes) consists only of continuation bytes; or -// - there's no UTF-8 leading byte between the final continuation byte of the buffer and -// the previous well-formed subsequence or maximal invalid subsequence. -// -// In all of these cases, the final byte must be a maximal invalid subsequence of length 1. -// See comment near the end of this method for more information. - - value = ReplacementChar; - bytesConsumed = 1; - return OperationStatus.InvalidData; - -ForwardDecode: - -// If we got to this point, we found an ASCII byte or a UTF-8 starting byte at position source[index]. -// Technically this could also mean we found an invalid byte like C0 or F5 at this position, but that's -// fine since it'll be handled by the forward read. From this position, we'll perform a forward read -// and see if we consumed the entirety of the buffer. - - source = source.Slice(index); - Debug.Assert(!source.IsEmpty, "Shouldn't reach this for empty inputs."); - - OperationStatus operationStatus = DecodeFromUtf8(source, out Rune tempRune, out int tempBytesConsumed); - if (tempBytesConsumed == source.Length) - { - // If this forward read consumed the entirety of the end of the input buffer, we can return it - // as the result of this function. It could be well-formed, incomplete, or invalid. If it's - // invalid and we consumed the remainder of the buffer, we know we've found the maximal invalid - // subsequence, which is what we wanted anyway. - - bytesConsumed = tempBytesConsumed; - value = tempRune; - return operationStatus; - } - - // If we got to this point, we know that the final continuation byte wasn't consumed by the forward - // read that we just performed above. This means that the continuation byte has to be part of an - // invalid subsequence since there's no UTF-8 leading byte between what we just consumed and the - // continuation byte at the end of the input. Furthermore, since any maximal invalid subsequence - // of length > 1 must have a UTF-8 leading byte as its first code unit, this implies that the - // continuation byte at the end of the buffer is itself a maximal invalid subsequence of length 1. - - goto Invalid; - } - else - { - // Source buffer was empty. - value = ReplacementChar; - bytesConsumed = 0; - return OperationStatus.NeedMoreData; - } - } - - /// <summary> - /// Encodes this <see cref="Rune"/> to a UTF-16 destination buffer. - /// </summary> - /// <param name="destination">The buffer to which to write this value as UTF-16.</param> - /// <returns>The number of <see cref="char"/>s written to <paramref name="destination"/>.</returns> - /// <exception cref="ArgumentException"> - /// If <paramref name="destination"/> is not large enough to hold the output. - /// </exception> - public int EncodeToUtf16(Span<char> destination) - { - if (!TryEncodeToUtf16(destination, out int charsWritten)) - { - throw new ArgumentException("Destination too short"); - } - - return charsWritten; - } - - /// <summary> - /// Encodes this <see cref="Rune"/> to a UTF-8 destination buffer. - /// </summary> - /// <param name="destination">The buffer to which to write this value as UTF-8.</param> - /// <returns>The number of <see cref="byte"/>s written to <paramref name="destination"/>.</returns> - /// <exception cref="ArgumentException"> - /// If <paramref name="destination"/> is not large enough to hold the output. - /// </exception> - public int EncodeToUtf8(Span<byte> destination) - { - if (!TryEncodeToUtf8(destination, out int bytesWritten)) - { - throw new ArgumentException("Destination too short"); - } - - return bytesWritten; - } - - public override bool Equals(object obj) => (obj is Rune other) && Equals(other); - - public bool Equals(Rune other) => this == other; - - public override int GetHashCode() => Value; - - /// <summary> - /// Gets the <see cref="Rune"/> which begins at index <paramref name="index"/> in - /// string <paramref name="input"/>. - /// </summary> - /// <remarks> - /// Throws if <paramref name="input"/> is null, if <paramref name="index"/> is out of range, or - /// if <paramref name="index"/> does not reference the start of a valid scalar value within <paramref name="input"/>. - /// </remarks> - public static Rune GetRuneAt(string input, int index) - { - int runeValue = ReadRuneFromString(input, index); - if (runeValue < 0) - { - throw new ArgumentException("Cannot extract scaler", nameof(index)); - } - - return UnsafeCreate((uint)runeValue); - } - - /// <summary> - /// Returns <see langword="true"/> if and only if <paramref name="value"/> is a valid Unicode scalar - /// value, i.e., is in [ U+0000..U+D7FF ], inclusive; or [ U+E000..U+10FFFF ], inclusive. - /// </summary> - public static bool IsValid(int value) => IsValid((uint)value); - - /// <summary> - /// Returns <see langword="true"/> if and only if <paramref name="value"/> is a valid Unicode scalar - /// value, i.e., is in [ U+0000..U+D7FF ], inclusive; or [ U+E000..U+10FFFF ], inclusive. - /// </summary> - public static bool IsValid(uint value) => UnicodeUtility.IsValidUnicodeScalar(value); - - // returns a negative number on failure - internal static int ReadFirstRuneFromUtf16Buffer(ReadOnlySpan<char> input) - { - if (input.IsEmpty) - { - return -1; - } - - // Optimistically assume input is within BMP. - - uint returnValue = input[0]; - if (UnicodeUtility.IsSurrogateCodePoint(returnValue)) - { - if (!UnicodeUtility.IsHighSurrogateCodePoint(returnValue)) - { - return -1; - } - - // Treat 'returnValue' as the high surrogate. - - if (1 >= (uint)input.Length) - { - return -1; // not an argument exception - just a "bad data" failure - } - - uint potentialLowSurrogate = input[1]; - if (!UnicodeUtility.IsLowSurrogateCodePoint(potentialLowSurrogate)) - { - return -1; - } - - returnValue = UnicodeUtility.GetScalarFromUtf16SurrogatePair(returnValue, potentialLowSurrogate); - } - - return (int)returnValue; - } - - // returns a negative number on failure - private static int ReadRuneFromString(string input, int index) - { - if (input is null) - { - throw new ArgumentNullException(nameof(input)); - } - - if ((uint)index >= (uint)input!.Length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - // Optimistically assume input is within BMP. - - uint returnValue = input[index]; - if (UnicodeUtility.IsSurrogateCodePoint(returnValue)) - { - if (!UnicodeUtility.IsHighSurrogateCodePoint(returnValue)) - { - return -1; - } - - // Treat 'returnValue' as the high surrogate. - // - // If this becomes a hot code path, we can skip the below bounds check by reading - // off the end of the string using unsafe code. Since strings are null-terminated, - // we're guaranteed not to read a valid low surrogate, so we'll fail correctly if - // the string terminates unexpectedly. - - index++; - if ((uint)index >= (uint)input.Length) - { - return -1; // not an argument exception - just a "bad data" failure - } - - uint potentialLowSurrogate = input[index]; - if (!UnicodeUtility.IsLowSurrogateCodePoint(potentialLowSurrogate)) - { - return -1; - } - - returnValue = UnicodeUtility.GetScalarFromUtf16SurrogatePair(returnValue, potentialLowSurrogate); - } - - return (int)returnValue; - } - - /// <summary> - /// Returns a <see cref="string"/> representation of this <see cref="Rune"/> instance. - /// </summary> - public override string ToString() - { -#if SYSTEM_PRIVATE_CORELIB - if (IsBmp) - { - return string.CreateFromChar((char)_value); - } - else - { - UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar(_value, out char high, out char low); - return string.CreateFromChar(high, low); - } -#else - if (IsBmp) - { - return ((char)_value).ToString(); - } - else - { - Span<char> buffer = stackalloc char[2]; - UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar(_value, out buffer[0], out buffer[1]); - return buffer.ToString(); - } -#endif - } - - /// <summary> - /// Attempts to create a <see cref="Rune"/> from the provided input value. - /// </summary> - public static bool TryCreate(char ch, out Rune result) - { - uint extendedValue = ch; - if (!UnicodeUtility.IsSurrogateCodePoint(extendedValue)) - { - result = UnsafeCreate(extendedValue); - return true; - } - else - { - result = default; - return false; - } - } - - /// <summary> - /// Attempts to create a <see cref="Rune"/> from the provided UTF-16 surrogate pair. - /// Returns <see langword="false"/> if the input values don't represent a well-formed UTF-16surrogate pair. - /// </summary> - public static bool TryCreate(char highSurrogate, char lowSurrogate, out Rune result) - { - // First, extend both to 32 bits, then calculate the offset of - // each candidate surrogate char from the start of its range. - - uint highSurrogateOffset = (uint)highSurrogate - HighSurrogateStart; - uint lowSurrogateOffset = (uint)lowSurrogate - LowSurrogateStart; - - // This is a single comparison which allows us to check both for validity at once since - // both the high surrogate range and the low surrogate range are the same length. - // If the comparison fails, we call to a helper method to throw the correct exception message. - - if ((highSurrogateOffset | lowSurrogateOffset) <= HighSurrogateRange) - { - // The 0x40u << 10 below is to account for uuuuu = wwww + 1 in the surrogate encoding. - result = UnsafeCreate((highSurrogateOffset << 10) + ((uint)lowSurrogate - LowSurrogateStart) + (0x40u << 10)); - return true; - } - else - { - // Didn't have a high surrogate followed by a low surrogate. - result = default; - return false; - } - } - - /// <summary> - /// Attempts to create a <see cref="Rune"/> from the provided input value. - /// </summary> - public static bool TryCreate(int value, out Rune result) => TryCreate((uint)value, out result); - - /// <summary> - /// Attempts to create a <see cref="Rune"/> from the provided input value. - /// </summary> - public static bool TryCreate(uint value, out Rune result) - { - if (UnicodeUtility.IsValidUnicodeScalar(value)) - { - result = UnsafeCreate(value); - return true; - } - else - { - result = default; - return false; - } - } - - /// <summary> - /// Encodes this <see cref="Rune"/> to a UTF-16 destination buffer. - /// </summary> - /// <param name="destination">The buffer to which to write this value as UTF-16.</param> - /// <param name="charsWritten"> - /// The number of <see cref="char"/>s written to <paramref name="destination"/>, - /// or 0 if the destination buffer is not large enough to contain the output.</param> - /// <returns>True if the value was written to the buffer; otherwise, false.</returns> - /// <remarks> - /// The <see cref="Utf16SequenceLength"/> property can be queried ahead of time to determine - /// the required size of the <paramref name="destination"/> buffer. - /// </remarks> - public bool TryEncodeToUtf16(Span<char> destination, out int charsWritten) - { - if (destination.Length >= 1) - { - if (IsBmp) - { - destination[0] = (char)_value; - charsWritten = 1; - return true; - } - else if (destination.Length >= 2) - { - UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar(_value, out destination[0], out destination[1]); - charsWritten = 2; - return true; - } - } - - // Destination buffer not large enough - - charsWritten = default; - return false; - } - - /// <summary> - /// Encodes this <see cref="Rune"/> to a destination buffer as UTF-8 bytes. - /// </summary> - /// <param name="destination">The buffer to which to write this value as UTF-8.</param> - /// <param name="bytesWritten"> - /// The number of <see cref="byte"/>s written to <paramref name="destination"/>, - /// or 0 if the destination buffer is not large enough to contain the output.</param> - /// <returns>True if the value was written to the buffer; otherwise, false.</returns> - /// <remarks> - /// The <see cref="Utf8SequenceLength"/> property can be queried ahead of time to determine - /// the required size of the <paramref name="destination"/> buffer. - /// </remarks> - public bool TryEncodeToUtf8(Span<byte> destination, out int bytesWritten) - { - // The bit patterns below come from the Unicode Standard, Table 3-6. - - if (destination.Length >= 1) - { - if (IsAscii) - { - destination[0] = (byte)_value; - bytesWritten = 1; - return true; - } - - if (destination.Length >= 2) - { - if (_value <= 0x7FFu) - { - // Scalar 00000yyy yyxxxxxx -> bytes [ 110yyyyy 10xxxxxx ] - destination[0] = (byte)((_value + (0b110u << 11)) >> 6); - destination[1] = (byte)((_value & 0x3Fu) + 0x80u); - bytesWritten = 2; - return true; - } - - if (destination.Length >= 3) - { - if (_value <= 0xFFFFu) - { - // Scalar zzzzyyyy yyxxxxxx -> bytes [ 1110zzzz 10yyyyyy 10xxxxxx ] - destination[0] = (byte)((_value + (0b1110 << 16)) >> 12); - destination[1] = (byte)(((_value & (0x3Fu << 6)) >> 6) + 0x80u); - destination[2] = (byte)((_value & 0x3Fu) + 0x80u); - bytesWritten = 3; - return true; - } - - if (destination.Length >= 4) - { - // Scalar 000uuuuu zzzzyyyy yyxxxxxx -> bytes [ 11110uuu 10uuzzzz 10yyyyyy 10xxxxxx ] - destination[0] = (byte)((_value + (0b11110 << 21)) >> 18); - destination[1] = (byte)(((_value & (0x3Fu << 12)) >> 12) + 0x80u); - destination[2] = (byte)(((_value & (0x3Fu << 6)) >> 6) + 0x80u); - destination[3] = (byte)((_value & 0x3Fu) + 0x80u); - bytesWritten = 4; - return true; - } - } - } - } - - // Destination buffer not large enough - - bytesWritten = default; - return false; - } - - /// <summary> - /// Attempts to get the <see cref="Rune"/> which begins at index <paramref name="index"/> in - /// string <paramref name="input"/>. - /// </summary> - /// <returns><see langword="true"/> if a scalar value was successfully extracted from the specified index, - /// <see langword="false"/> if a value could not be extracted due to invalid data.</returns> - /// <remarks> - /// Throws only if <paramref name="input"/> is null or <paramref name="index"/> is out of range. - /// </remarks> - public static bool TryGetRuneAt(string input, int index, out Rune value) - { - int runeValue = ReadRuneFromString(input, index); - if (runeValue >= 0) - { - value = UnsafeCreate((uint)runeValue); - return true; - } - else - { - value = default; - return false; - } - } - - // Allows constructing a Unicode scalar value from an arbitrary 32-bit integer without - // validation. It is the caller's responsibility to have performed manual validation - // before calling this method. If a Rune instance is forcibly constructed - // from invalid input, the APIs on this type have undefined behavior, potentially including - // introducing a security hole in the consuming application. - // - // An example of a security hole resulting from an invalid Rune value, which could result - // in a stack overflow. - // - // public int GetMarvin32HashCode(Rune r) { - // Span<char> buffer = stackalloc char[r.Utf16SequenceLength]; - // r.TryEncode(buffer, ...); - // return Marvin32.ComputeHash(buffer.AsBytes()); - // } - - /// <summary> - /// Creates a <see cref="Rune"/> without performing validation on the input. - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Rune UnsafeCreate(uint scalarValue) => new Rune(scalarValue, false); - - // These are analogs of APIs on System.Char - - public static double GetNumericValue(Rune value) - { - if (value.IsAscii) - { - uint baseNum = value._value - '0'; - return (baseNum <= 9) ? (double)baseNum : -1; - } - else - { - // not an ASCII char; fall back to globalization table -#if SYSTEM_PRIVATE_CORELIB - return CharUnicodeInfo.GetNumericValue(value.Value); -#else - if (value.IsBmp) - { - return CharUnicodeInfo.GetNumericValue((char)value._value); - } - return CharUnicodeInfo.GetNumericValue(value.ToString(), 0); -#endif - } - } - - public static UnicodeCategory GetUnicodeCategory(Rune value) - { - if (value.IsAscii) - { - return (UnicodeCategory)(AsciiCharInfo[value.Value] & UnicodeCategoryMask); - } - else - { - return GetUnicodeCategoryNonAscii(value); - } - } - - private static UnicodeCategory GetUnicodeCategoryNonAscii(Rune value) - { - Debug.Assert(!value.IsAscii, "Shouldn't use this non-optimized code path for ASCII characters."); -#if !NETSTANDARD2_0 - return CharUnicodeInfo.GetUnicodeCategory(value.Value); -#else - if (value.IsBmp) - { - return CharUnicodeInfo.GetUnicodeCategory((char)value._value); - } - return CharUnicodeInfo.GetUnicodeCategory(value.ToString(), 0); -#endif - } - - // Returns true iff this Unicode category represents a letter - private static bool IsCategoryLetter(UnicodeCategory category) - { - return UnicodeUtility.IsInRangeInclusive((uint)category, (uint)UnicodeCategory.UppercaseLetter, (uint)UnicodeCategory.OtherLetter); - } - - // Returns true iff this Unicode category represents a letter or a decimal digit - private static bool IsCategoryLetterOrDecimalDigit(UnicodeCategory category) - { - return UnicodeUtility.IsInRangeInclusive((uint)category, (uint)UnicodeCategory.UppercaseLetter, (uint)UnicodeCategory.OtherLetter) - || (category == UnicodeCategory.DecimalDigitNumber); - } - - // Returns true iff this Unicode category represents a number - private static bool IsCategoryNumber(UnicodeCategory category) - { - return UnicodeUtility.IsInRangeInclusive((uint)category, (uint)UnicodeCategory.DecimalDigitNumber, (uint)UnicodeCategory.OtherNumber); - } - - // Returns true iff this Unicode category represents a punctuation mark - private static bool IsCategoryPunctuation(UnicodeCategory category) - { - return UnicodeUtility.IsInRangeInclusive((uint)category, (uint)UnicodeCategory.ConnectorPunctuation, (uint)UnicodeCategory.OtherPunctuation); - } - - // Returns true iff this Unicode category represents a separator - private static bool IsCategorySeparator(UnicodeCategory category) - { - return UnicodeUtility.IsInRangeInclusive((uint)category, (uint)UnicodeCategory.SpaceSeparator, (uint)UnicodeCategory.ParagraphSeparator); - } - - // Returns true iff this Unicode category represents a symbol - private static bool IsCategorySymbol(UnicodeCategory category) - { - return UnicodeUtility.IsInRangeInclusive((uint)category, (uint)UnicodeCategory.MathSymbol, (uint)UnicodeCategory.OtherSymbol); - } - - public static bool IsControl(Rune value) - { - // Per the Unicode stability policy, the set of control characters - // is forever fixed at [ U+0000..U+001F ], [ U+007F..U+009F ]. No - // characters will ever be added to or removed from the "control characters" - // group. See https://www.unicode.org/policies/stability_policy.html. - - // Logic below depends on Rune.Value never being -1 (since Rune is a validating type) - // 00..1F (+1) => 01..20 (&~80) => 01..20 - // 7F..9F (+1) => 80..A0 (&~80) => 00..20 - - return ((value._value + 1) & ~0x80u) <= 0x20u; - } - - public static bool IsDigit(Rune value) - { - if (value.IsAscii) - { - return UnicodeUtility.IsInRangeInclusive(value._value, '0', '9'); - } - else - { - return GetUnicodeCategoryNonAscii(value) == UnicodeCategory.DecimalDigitNumber; - } - } - - public static bool IsLetter(Rune value) - { - if (value.IsAscii) - { - return ((value._value - 'A') & ~0x20u) <= (uint)('Z' - 'A'); // [A-Za-z] - } - else - { - return IsCategoryLetter(GetUnicodeCategoryNonAscii(value)); - } - } - - public static bool IsLetterOrDigit(Rune value) - { - if (value.IsAscii) - { - return (AsciiCharInfo[value.Value] & IsLetterOrDigitFlag) != 0; - } - else - { - return IsCategoryLetterOrDecimalDigit(GetUnicodeCategoryNonAscii(value)); - } - } - - public static bool IsLower(Rune value) - { - if (value.IsAscii) - { - return UnicodeUtility.IsInRangeInclusive(value._value, 'a', 'z'); - } - else - { - return GetUnicodeCategoryNonAscii(value) == UnicodeCategory.LowercaseLetter; - } - } - - public static bool IsNumber(Rune value) - { - if (value.IsAscii) - { - return UnicodeUtility.IsInRangeInclusive(value._value, '0', '9'); - } - else - { - return IsCategoryNumber(GetUnicodeCategoryNonAscii(value)); - } - } - - public static bool IsPunctuation(Rune value) - { - return IsCategoryPunctuation(GetUnicodeCategory(value)); - } - - public static bool IsSeparator(Rune value) - { - return IsCategorySeparator(GetUnicodeCategory(value)); - } - - public static bool IsSymbol(Rune value) - { - return IsCategorySymbol(GetUnicodeCategory(value)); - } - - public static bool IsUpper(Rune value) - { - if (value.IsAscii) - { - return UnicodeUtility.IsInRangeInclusive(value._value, 'A', 'Z'); - } - else - { - return GetUnicodeCategoryNonAscii(value) == UnicodeCategory.UppercaseLetter; - } - } - - public static bool IsWhiteSpace(Rune value) - { - if (value.IsAscii) - { - return (AsciiCharInfo[value.Value] & IsWhiteSpaceFlag) != 0; - } - - // Only BMP code points can be white space, so only call into CharUnicodeInfo - // if the incoming value is within the BMP. - - return value.IsBmp && -#if SYSTEM_PRIVATE_CORELIB - CharUnicodeInfo.GetIsWhiteSpace((char)value._value); -#else - char.IsWhiteSpace((char)value._value); -#endif - } - - public static Rune ToLower(Rune value, CultureInfo culture) - { - if (culture is null) - { - throw new ArgumentNullException(nameof(culture)); - } - - // We don't want to special-case ASCII here since the specified culture might handle - // ASCII characters differently than the invariant culture (e.g., Turkish I). Instead - // we'll just jump straight to the globalization tables if they're available. - -#if false - if (GlobalizationMode.Invariant) - { - return ToLowerInvariant(value); - } -#endif - -#if SYSTEM_PRIVATE_CORELIB - return ChangeCaseCultureAware(value, culture.TextInfo, toUpper: false); -#else - return ChangeCaseCultureAware(value, culture, toUpper: false); -#endif - } - - public static Rune ToLowerInvariant(Rune value) - { - // Handle the most common case (ASCII data) first. Within the common case, we expect - // that there'll be a mix of lowercase & uppercase chars, so make the conversion branchless. - - if (value.IsAscii) - { - // It's ok for us to use the UTF-16 conversion utility for this since the high - // 16 bits of the value will never be set so will be left unchanged. - return UnsafeCreate(Utf16Utility.ConvertAllAsciiCharsInUInt32ToLowercase(value._value)); - } - -#if false - if (GlobalizationMode.Invariant) - { - // If the value isn't ASCII and if the globalization tables aren't available, - // case changing has no effect. - return value; - } -#endif - - // Non-ASCII data requires going through the case folding tables. - -#if SYSTEM_PRIVATE_CORELIB - return ChangeCaseCultureAware(value, TextInfo.Invariant, toUpper: false); -#else - return ChangeCaseCultureAware(value, CultureInfo.InvariantCulture, toUpper: false); -#endif - } - - public static Rune ToUpper(Rune value, CultureInfo culture) - { - if (culture is null) - { - throw new ArgumentNullException(nameof(culture)); - } - - // We don't want to special-case ASCII here since the specified culture might handle - // ASCII characters differently than the invariant culture (e.g., Turkish I). Instead - // we'll just jump straight to the globalization tables if they're available. - -#if false - if (GlobalizationMode.Invariant) - { - return ToUpperInvariant(value); - } -#endif - -#if SYSTEM_PRIVATE_CORELIB - return ChangeCaseCultureAware(value, culture.TextInfo, toUpper: true); -#else - return ChangeCaseCultureAware(value, culture, toUpper: true); -#endif - } - - public static Rune ToUpperInvariant(Rune value) - { - // Handle the most common case (ASCII data) first. Within the common case, we expect - // that there'll be a mix of lowercase & uppercase chars, so make the conversion branchless. - - if (value.IsAscii) - { - // It's ok for us to use the UTF-16 conversion utility for this since the high - // 16 bits of the value will never be set so will be left unchanged. - return UnsafeCreate(Utf16Utility.ConvertAllAsciiCharsInUInt32ToUppercase(value._value)); - } - -#if false - if (GlobalizationMode.Invariant) - { - // If the value isn't ASCII and if the globalization tables aren't available, - // case changing has no effect. - return value; - } -#endif - - // Non-ASCII data requires going through the case folding tables. - -#if SYSTEM_PRIVATE_CORELIB - return ChangeCaseCultureAware(value, TextInfo.Invariant, toUpper: true); -#else - return ChangeCaseCultureAware(value, CultureInfo.InvariantCulture, toUpper: true); -#endif - } - } -} - -#endif diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/UnicodeDebug.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/UnicodeDebug.cs deleted file mode 100644 index 4f5d18091719..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/UnicodeDebug.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// <auto-generated/> - -#nullable disable - -// Copied from https://github.com/dotnet/runtime/blob/c73774b53944a6007ee85f138e3ff3d3297846ea/src/libraries/System.Private.CoreLib/src/System/Text/UnicodeDebug.cs#L1 -// So that we can use Runes in netstandard 2.0 - -#if !NETCOREAPP - -using System.Diagnostics; - -namespace System.Text -{ - internal static class UnicodeDebug - { - [Conditional("DEBUG")] - internal static void AssertIsHighSurrogateCodePoint(uint codePoint) - { - if (!UnicodeUtility.IsHighSurrogateCodePoint(codePoint)) - { - Debug.Fail($"The value {ToHexString(codePoint)} is not a valid UTF-16 high surrogate code point."); - } - } - - [Conditional("DEBUG")] - internal static void AssertIsLowSurrogateCodePoint(uint codePoint) - { - if (!UnicodeUtility.IsLowSurrogateCodePoint(codePoint)) - { - Debug.Fail($"The value {ToHexString(codePoint)} is not a valid UTF-16 low surrogate code point."); - } - } - - [Conditional("DEBUG")] - internal static void AssertIsValidCodePoint(uint codePoint) - { - if (!UnicodeUtility.IsValidCodePoint(codePoint)) - { - Debug.Fail($"The value {ToHexString(codePoint)} is not a valid Unicode code point."); - } - } - - [Conditional("DEBUG")] - internal static void AssertIsValidScalar(uint scalarValue) - { - if (!UnicodeUtility.IsValidUnicodeScalar(scalarValue)) - { - Debug.Fail($"The value {ToHexString(scalarValue)} is not a valid Unicode scalar value."); - } - } - - [Conditional("DEBUG")] - internal static void AssertIsValidSupplementaryPlaneScalar(uint scalarValue) - { - if (!UnicodeUtility.IsValidUnicodeScalar(scalarValue) || UnicodeUtility.IsBmpCodePoint(scalarValue)) - { - Debug.Fail($"The value {ToHexString(scalarValue)} is not a valid supplementary plane Unicode scalar value."); - } - } - - /// <summary> - /// Formats a code point as the hex string "U+XXXX". - /// </summary> - /// <remarks> - /// The input value doesn't have to be a real code point in the Unicode codespace. It can be any integer. - /// </remarks> - private static string ToHexString(uint codePoint) - { - return FormattableString.Invariant($"U+{codePoint:X4}"); - } - } -} - -#endif diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/UnicodeUtility.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/UnicodeUtility.cs deleted file mode 100644 index 232692988db6..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/UnicodeUtility.cs +++ /dev/null @@ -1,196 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// <auto-generated/> - -#nullable disable - -// Copied from https://github.com/dotnet/runtime/blob/c73774b53944a6007ee85f138e3ff3d3297846ea/src/libraries/System.Private.CoreLib/src/System/Text/UnicodeUtility.cs#L1 -// So that we can use Runes in netstandard 2.0 - -#if !NETCOREAPP - -using System.Runtime.CompilerServices; - -namespace System.Text -{ - internal static class UnicodeUtility - { - /// <summary> - /// The Unicode replacement character U+FFFD. - /// </summary> - public const uint ReplacementChar = 0xFFFD; - - /// <summary> - /// Returns the Unicode plane (0 through 16, inclusive) which contains this code point. - /// </summary> - public static int GetPlane(uint codePoint) - { - UnicodeDebug.AssertIsValidCodePoint(codePoint); - - return (int)(codePoint >> 16); - } - - /// <summary> - /// Returns a Unicode scalar value from two code points representing a UTF-16 surrogate pair. - /// </summary> - public static uint GetScalarFromUtf16SurrogatePair(uint highSurrogateCodePoint, uint lowSurrogateCodePoint) - { - UnicodeDebug.AssertIsHighSurrogateCodePoint(highSurrogateCodePoint); - UnicodeDebug.AssertIsLowSurrogateCodePoint(lowSurrogateCodePoint); - - // This calculation comes from the Unicode specification, Table 3-5. - // Need to remove the D800 marker from the high surrogate and the DC00 marker from the low surrogate, - // then fix up the "wwww = uuuuu - 1" section of the bit distribution. The code is written as below - // to become just two instructions: shl, lea. - - return (highSurrogateCodePoint << 10) + lowSurrogateCodePoint - ((0xD800U << 10) + 0xDC00U - (1 << 16)); - } - - /// <summary> - /// Given a Unicode scalar value, gets the number of UTF-16 code units required to represent this value. - /// </summary> - public static int GetUtf16SequenceLength(uint value) - { - UnicodeDebug.AssertIsValidScalar(value); - - value -= 0x10000; // if value < 0x10000, high byte = 0xFF; else high byte = 0x00 - value += (2 << 24); // if value < 0x10000, high byte = 0x01; else high byte = 0x02 - value >>= 24; // shift high byte down - return (int)value; // and return it - } - - /// <summary> - /// Decomposes an astral Unicode scalar into UTF-16 high and low surrogate code units. - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetUtf16SurrogatesFromSupplementaryPlaneScalar(uint value, out char highSurrogateCodePoint, out char lowSurrogateCodePoint) - { - UnicodeDebug.AssertIsValidSupplementaryPlaneScalar(value); - - // This calculation comes from the Unicode specification, Table 3-5. - - highSurrogateCodePoint = (char)((value + ((0xD800u - 0x40u) << 10)) >> 10); - lowSurrogateCodePoint = (char)((value & 0x3FFu) + 0xDC00u); - } - - /// <summary> - /// Given a Unicode scalar value, gets the number of UTF-8 code units required to represent this value. - /// </summary> - public static int GetUtf8SequenceLength(uint value) - { - UnicodeDebug.AssertIsValidScalar(value); - - // The logic below can handle all valid scalar values branchlessly. - // It gives generally good performance across all inputs, and on x86 - // it's only six instructions: lea, sar, xor, add, shr, lea. - - // 'a' will be -1 if input is < 0x800; else 'a' will be 0 - // => 'a' will be -1 if input is 1 or 2 UTF-8 code units; else 'a' will be 0 - - int a = ((int)value - 0x0800) >> 31; - - // The number of UTF-8 code units for a given scalar is as follows: - // - U+0000..U+007F => 1 code unit - // - U+0080..U+07FF => 2 code units - // - U+0800..U+FFFF => 3 code units - // - U+10000+ => 4 code units - // - // If we XOR the incoming scalar with 0xF800, the chart mutates: - // - U+0000..U+F7FF => 3 code units - // - U+F800..U+F87F => 1 code unit - // - U+F880..U+FFFF => 2 code units - // - U+10000+ => 4 code units - // - // Since the 1- and 3-code unit cases are now clustered, they can - // both be checked together very cheaply. - - value ^= 0xF800u; - value -= 0xF880u; // if scalar is 1 or 3 code units, high byte = 0xFF; else high byte = 0x00 - value += (4 << 24); // if scalar is 1 or 3 code units, high byte = 0x03; else high byte = 0x04 - value >>= 24; // shift high byte down - - // Final return value: - // - U+0000..U+007F => 3 + (-1) * 2 = 1 - // - U+0080..U+07FF => 4 + (-1) * 2 = 2 - // - U+0800..U+FFFF => 3 + ( 0) * 2 = 3 - // - U+10000+ => 4 + ( 0) * 2 = 4 - return (int)value + (a * 2); - } - - /// <summary> - /// Returns <see langword="true"/> if and only if <paramref name="value"/> is an ASCII - /// character ([ U+0000..U+007F ]). - /// </summary> - /// <remarks> - /// Per http://www.unicode.org/glossary/#ASCII, ASCII is only U+0000..U+007F. - /// </remarks> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsAsciiCodePoint(uint value) => value <= 0x7Fu; - - /// <summary> - /// Returns <see langword="true"/> if and only if <paramref name="value"/> is in the - /// Basic Multilingual Plane (BMP). - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsBmpCodePoint(uint value) => value <= 0xFFFFu; - - /// <summary> - /// Returns <see langword="true"/> if and only if <paramref name="value"/> is a UTF-16 high surrogate code point, - /// i.e., is in [ U+D800..U+DBFF ], inclusive. - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsHighSurrogateCodePoint(uint value) => IsInRangeInclusive(value, 0xD800U, 0xDBFFU); - - /// <summary> - /// Returns <see langword="true"/> if and only if <paramref name="value"/> is between - /// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive. - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound) => (value - lowerBound) <= (upperBound - lowerBound); - - /// <summary> - /// Returns <see langword="true"/> if and only if <paramref name="value"/> is a UTF-16 low surrogate code point, - /// i.e., is in [ U+DC00..U+DFFF ], inclusive. - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsLowSurrogateCodePoint(uint value) => IsInRangeInclusive(value, 0xDC00U, 0xDFFFU); - - /// <summary> - /// Returns <see langword="true"/> if and only if <paramref name="value"/> is a UTF-16 surrogate code point, - /// i.e., is in [ U+D800..U+DFFF ], inclusive. - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsSurrogateCodePoint(uint value) => IsInRangeInclusive(value, 0xD800U, 0xDFFFU); - - /// <summary> - /// Returns <see langword="true"/> if and only if <paramref name="codePoint"/> is a valid Unicode code - /// point, i.e., is in [ U+0000..U+10FFFF ], inclusive. - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsValidCodePoint(uint codePoint) => codePoint <= 0x10FFFFU; - - /// <summary> - /// Returns <see langword="true"/> if and only if <paramref name="value"/> is a valid Unicode scalar - /// value, i.e., is in [ U+0000..U+D7FF ], inclusive; or [ U+E000..U+10FFFF ], inclusive. - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsValidUnicodeScalar(uint value) - { - // This is an optimized check that on x86 is just three instructions: lea, xor, cmp. - // - // After the subtraction operation, the input value is modified as such: - // [ 00000000..0010FFFF ] -> [ FFEF0000..FFFFFFFF ] - // - // We now want to _exclude_ the range [ FFEFD800..FFEFDFFF ] (surrogates) from being valid. - // After the xor, this particular exclusion range becomes [ FFEF0000..FFEF07FF ]. - // - // So now the range [ FFEF0800..FFFFFFFF ] contains all valid code points, - // excluding surrogates. This allows us to perform a single comparison. - - return ((value - 0x110000u) ^ 0xD800u) >= 0xFFEF0800u; - } - } -} - -#endif diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/Utf16Utility.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/Utf16Utility.cs deleted file mode 100644 index 0c762bb0db33..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/System.Text/Utf16Utility.cs +++ /dev/null @@ -1,225 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. -// <auto-generated/> - -#nullable disable - -// Copied from https://github.com/dotnet/runtime/blob/c73774b53944a6007ee85f138e3ff3d3297846ea/src/libraries/System.Private.CoreLib/src/System/Text/Unicode/Utf16Utility.cs#L1 -// So that we can use Runes in netstandard 2.0 - -#if !NETCOREAPP - -using System.Runtime.CompilerServices; -using System.Diagnostics; - -namespace System.Text.Unicode -{ - internal static partial class Utf16Utility - { - /// <summary> - /// Returns true iff the UInt32 represents two ASCII UTF-16 characters in machine endianness. - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool AllCharsInUInt32AreAscii(uint value) - { - return (value & ~0x007F_007Fu) == 0; - } - - /// <summary> - /// Returns true iff the UInt64 represents four ASCII UTF-16 characters in machine endianness. - /// </summary> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool AllCharsInUInt64AreAscii(ulong value) - { - return (value & ~0x007F_007F_007F_007Ful) == 0; - } - - /// <summary> - /// Given a UInt32 that represents two ASCII UTF-16 characters, returns the invariant - /// lowercase representation of those characters. Requires the input value to contain - /// two ASCII UTF-16 characters in machine endianness. - /// </summary> - /// <remarks> - /// This is a branchless implementation. - /// </remarks> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static uint ConvertAllAsciiCharsInUInt32ToLowercase(uint value) - { - // ASSUMPTION: Caller has validated that input value is ASCII. - Debug.Assert(AllCharsInUInt32AreAscii(value)); - - // the 0x80 bit of each word of 'lowerIndicator' will be set iff the word has value >= 'A' - uint lowerIndicator = value + 0x0080_0080u - 0x0041_0041u; - - // the 0x80 bit of each word of 'upperIndicator' will be set iff the word has value > 'Z' - uint upperIndicator = value + 0x0080_0080u - 0x005B_005Bu; - - // the 0x80 bit of each word of 'combinedIndicator' will be set iff the word has value >= 'A' and <= 'Z' - uint combinedIndicator = (lowerIndicator ^ upperIndicator); - - // the 0x20 bit of each word of 'mask' will be set iff the word has value >= 'A' and <= 'Z' - uint mask = (combinedIndicator & 0x0080_0080u) >> 2; - - return value ^ mask; // bit flip uppercase letters [A-Z] => [a-z] - } - - /// <summary> - /// Given a UInt32 that represents two ASCII UTF-16 characters, returns the invariant - /// uppercase representation of those characters. Requires the input value to contain - /// two ASCII UTF-16 characters in machine endianness. - /// </summary> - /// <remarks> - /// This is a branchless implementation. - /// </remarks> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static uint ConvertAllAsciiCharsInUInt32ToUppercase(uint value) - { - // ASSUMPTION: Caller has validated that input value is ASCII. - Debug.Assert(AllCharsInUInt32AreAscii(value)); - - // the 0x80 bit of each word of 'lowerIndicator' will be set iff the word has value >= 'a' - uint lowerIndicator = value + 0x0080_0080u - 0x0061_0061u; - - // the 0x80 bit of each word of 'upperIndicator' will be set iff the word has value > 'z' - uint upperIndicator = value + 0x0080_0080u - 0x007B_007Bu; - - // the 0x80 bit of each word of 'combinedIndicator' will be set iff the word has value >= 'a' and <= 'z' - uint combinedIndicator = (lowerIndicator ^ upperIndicator); - - // the 0x20 bit of each word of 'mask' will be set iff the word has value >= 'a' and <= 'z' - uint mask = (combinedIndicator & 0x0080_0080u) >> 2; - - return value ^ mask; // bit flip lowercase letters [a-z] => [A-Z] - } - - /// <summary> - /// Given a UInt32 that represents two ASCII UTF-16 characters, returns true iff - /// the input contains one or more lowercase ASCII characters. - /// </summary> - /// <remarks> - /// This is a branchless implementation. - /// </remarks> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool UInt32ContainsAnyLowercaseAsciiChar(uint value) - { - // ASSUMPTION: Caller has validated that input value is ASCII. - Debug.Assert(AllCharsInUInt32AreAscii(value)); - - // the 0x80 bit of each word of 'lowerIndicator' will be set iff the word has value >= 'a' - uint lowerIndicator = value + 0x0080_0080u - 0x0061_0061u; - - // the 0x80 bit of each word of 'upperIndicator' will be set iff the word has value > 'z' - uint upperIndicator = value + 0x0080_0080u - 0x007B_007Bu; - - // the 0x80 bit of each word of 'combinedIndicator' will be set iff the word has value >= 'a' and <= 'z' - uint combinedIndicator = (lowerIndicator ^ upperIndicator); - - return (combinedIndicator & 0x0080_0080u) != 0; - } - - /// <summary> - /// Given a UInt32 that represents two ASCII UTF-16 characters, returns true iff - /// the input contains one or more uppercase ASCII characters. - /// </summary> - /// <remarks> - /// This is a branchless implementation. - /// </remarks> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool UInt32ContainsAnyUppercaseAsciiChar(uint value) - { - // ASSUMPTION: Caller has validated that input value is ASCII. - Debug.Assert(AllCharsInUInt32AreAscii(value)); - - // the 0x80 bit of each word of 'lowerIndicator' will be set iff the word has value >= 'A' - uint lowerIndicator = value + 0x0080_0080u - 0x0041_0041u; - - // the 0x80 bit of each word of 'upperIndicator' will be set iff the word has value > 'Z' - uint upperIndicator = value + 0x0080_0080u - 0x005B_005Bu; - - // the 0x80 bit of each word of 'combinedIndicator' will be set iff the word has value >= 'A' and <= 'Z' - uint combinedIndicator = (lowerIndicator ^ upperIndicator); - - return (combinedIndicator & 0x0080_0080u) != 0; - } - - /// <summary> - /// Given two UInt32s that represent two ASCII UTF-16 characters each, returns true iff - /// the two inputs are equal using an ordinal case-insensitive comparison. - /// </summary> - /// <remarks> - /// This is a branchless implementation. - /// </remarks> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool UInt32OrdinalIgnoreCaseAscii(uint valueA, uint valueB) - { - // ASSUMPTION: Caller has validated that input values are ASCII. - Debug.Assert(AllCharsInUInt32AreAscii(valueA)); - Debug.Assert(AllCharsInUInt32AreAscii(valueB)); - - // a mask of all bits which are different between A and B - uint differentBits = valueA ^ valueB; - - // the 0x80 bit of each word of 'lowerIndicator' will be set iff the word has value < 'A' - uint lowerIndicator = valueA + 0x0100_0100u - 0x0041_0041u; - - // the 0x80 bit of each word of 'upperIndicator' will be set iff (word | 0x20) has value > 'z' - uint upperIndicator = (valueA | 0x0020_0020u) + 0x0080_0080u - 0x007B_007Bu; - - // the 0x80 bit of each word of 'combinedIndicator' will be set iff the word is *not* [A-Za-z] - uint combinedIndicator = lowerIndicator | upperIndicator; - - // Shift all the 0x80 bits of 'combinedIndicator' into the 0x20 positions, then set all bits - // aside from 0x20. This creates a mask where all bits are set *except* for the 0x20 bits - // which correspond to alpha chars (either lower or upper). For these alpha chars only, the - // 0x20 bit is allowed to differ between the two input values. Every other char must be an - // exact bitwise match between the two input values. In other words, (valueA & mask) will - // convert valueA to uppercase, so (valueA & mask) == (valueB & mask) answers "is the uppercase - // form of valueA equal to the uppercase form of valueB?" (Technically if valueA has an alpha - // char in the same position as a non-alpha char in valueB, or vice versa, this operation will - // result in nonsense, but it'll still compute as inequal regardless, which is what we want ultimately.) - // The line below is a more efficient way of doing the same check taking advantage of the XOR - // computation we performed at the beginning of the method. - - return (((combinedIndicator >> 2) | ~0x0020_0020u) & differentBits) == 0; - } - - /// <summary> - /// Given two UInt64s that represent four ASCII UTF-16 characters each, returns true iff - /// the two inputs are equal using an ordinal case-insensitive comparison. - /// </summary> - /// <remarks> - /// This is a branchless implementation. - /// </remarks> - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool UInt64OrdinalIgnoreCaseAscii(ulong valueA, ulong valueB) - { - // ASSUMPTION: Caller has validated that input values are ASCII. - Debug.Assert(AllCharsInUInt64AreAscii(valueA)); - Debug.Assert(AllCharsInUInt64AreAscii(valueB)); - - // the 0x80 bit of each word of 'lowerIndicator' will be set iff the word has value >= 'A' - ulong lowerIndicator = valueA + 0x0080_0080_0080_0080ul - 0x0041_0041_0041_0041ul; - - // the 0x80 bit of each word of 'upperIndicator' will be set iff (word | 0x20) has value <= 'z' - ulong upperIndicator = (valueA | 0x0020_0020_0020_0020ul) + 0x0100_0100_0100_0100ul - 0x007B_007B_007B_007Bul; - - // the 0x20 bit of each word of 'combinedIndicator' will be set iff the word is [A-Za-z] - ulong combinedIndicator = (0x0080_0080_0080_0080ul & lowerIndicator & upperIndicator) >> 2; - - // Convert both values to lowercase (using the combined indicator from the first value) - // and compare for equality. It's possible that the first value will contain an alpha character - // where the second value doesn't (or vice versa), and applying the combined indicator will - // create nonsensical data, but the comparison would have failed anyway in this case so it's - // a safe operation to perform. - // - // This 64-bit method is similar to the 32-bit method, but it performs the equivalent of convert-to- - // lowercase-then-compare rather than convert-to-uppercase-and-compare. This particular operation - // happens to be faster on x64. - - return (valueA | combinedIndicator) == (valueB | combinedIndicator); - } - } -} - -#endif diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/TextLineExtensions.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/TextLineExtensions.cs deleted file mode 100644 index 90af51f3491f..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/TextLineExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; - -internal static class TextLineExtensions -{ - public static int? GetFirstNonWhitespaceOffset(this TextLine line) - { - var text = line.Text; - if (text != null) - { - for (var i = line.Start; i < line.End; i++) - { - if (!char.IsWhiteSpace(text[i])) - { - return i - line.Start; - } - } - } - - return null; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualChar.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualChar.cs deleted file mode 100644 index 7701676afff1..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualChar.cs +++ /dev/null @@ -1,195 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Text; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; - -/// <summary> -/// <see cref="VirtualChar"/> provides a uniform view of a language's string token characters regardless if they -/// were written raw in source, or are the production of a language escape sequence. For example, in C#, in a -/// normal <c>""</c> string a <c>Tab</c> character can be written either as the raw tab character (value <c>9</c> in -/// ASCII), or as <c>\t</c>. The format is a single character in the source, while the latter is two characters -/// (<c>\</c> and <c>t</c>). <see cref="VirtualChar"/> will represent both, providing the raw <see cref="char"/> -/// value of <c>9</c> as well as what <see cref="TextSpan"/> in the original <see cref="SourceText"/> they occupied. -/// </summary> -/// <remarks> -/// A core consumer of this system is the Regex parser. That parser wants to work over an array of characters, -/// however this array of characters is not the same as the array of characters a user types into a string in C# or -/// VB. For example In C# someone may write: @"\z". This should appear to the user the same as if they wrote "\\z" -/// and the same as "\\\u007a". However, as these all have wildly different presentations for the user, there needs -/// to be a way to map back the characters it sees ( '\' and 'z' ) back to the ranges of characters the user wrote. -/// </remarks> -internal readonly struct VirtualChar : IEquatable<VirtualChar>, IComparable<VirtualChar>, IComparable<char> -{ - /// <summary> - /// The value of this <see cref="VirtualChar"/> as a <see cref="Rune"/> if such a representation is possible. - /// <see cref="Rune"/>s can represent Unicode codepoints that can appear in a <see cref="string"/> except for - /// unpaired surrogates. If an unpaired high or low surrogate character is present, this value will be <see - /// cref="Rune.ReplacementChar"/>. The value of this character can be retrieved from - /// <see cref="SurrogateChar"/>. - /// </summary> - public readonly Rune Rune; - - /// <summary> - /// The unpaired high or low surrogate character that was encountered that could not be represented in <see - /// cref="Rune"/>. If <see cref="Rune"/> is not <see cref="Rune.ReplacementChar"/>, this will be <c>0</c>. - /// </summary> - public readonly char SurrogateChar; - - /// <summary> - /// The span of characters in the original <see cref="SourceText"/> that represent this <see - /// cref="VirtualChar"/>. - /// </summary> - public readonly TextSpan Span; - - /// <summary> - /// Creates a new <see cref="VirtualChar"/> from the provided <paramref name="rune"/>. This operation cannot - /// fail. - /// </summary> - public static VirtualChar Create(Rune rune, TextSpan span) - => new(rune, surrogateChar: default, span); - - /// <summary> - /// Creates a new <see cref="VirtualChar"/> from an unpaired high or low surrogate character. This will throw - /// if <paramref name="surrogateChar"/> is not actually a surrogate character. The resultant <see cref="Rune"/> - /// value will be <see cref="Rune.ReplacementChar"/>. - /// </summary> - public static VirtualChar Create(char surrogateChar, TextSpan span) - { - if (!char.IsSurrogate(surrogateChar)) - { - throw new ArgumentException("Must be a surrogate char.", nameof(surrogateChar)); - } - - return new VirtualChar(rune: Rune.ReplacementChar, surrogateChar, span); - } - - private VirtualChar(Rune rune, char surrogateChar, TextSpan span) - { - if (!(surrogateChar == 0 || rune == Rune.ReplacementChar)) - { - throw new InvalidOperationException("If surrogateChar is provided then rune must be Rune.ReplacementChar"); - } - - if (span.IsEmpty) - { - throw new ArgumentException("Span should not be empty.", nameof(span)); - } - - Rune = rune; - SurrogateChar = surrogateChar; - Span = span; - } - - /// <summary> - /// Retrieves the scaler value of this character as an <see cref="int"/>. If this is an unpaired surrogate - /// character, this will be the value of that surrogate. Otherwise, this will be the value of our <see - /// cref="Rune"/>. - /// </summary> - public int Value => SurrogateChar != 0 ? SurrogateChar : Rune.Value; - - public bool IsDigit - => SurrogateChar != 0 ? char.IsDigit(SurrogateChar) : Rune.IsDigit(Rune); - - public bool IsLetterOrDigit - => SurrogateChar != 0 ? char.IsLetterOrDigit(SurrogateChar) : Rune.IsLetterOrDigit(Rune); - - public bool IsWhiteSpace - => SurrogateChar != 0 ? char.IsWhiteSpace(SurrogateChar) : Rune.IsWhiteSpace(Rune); - - #region equality - - public static bool operator ==(VirtualChar char1, VirtualChar char2) - => char1.Equals(char2); - - public static bool operator !=(VirtualChar char1, VirtualChar char2) - => !(char1 == char2); - - public static bool operator ==(VirtualChar ch1, char ch2) - => ch1.Value == ch2; - - public static bool operator !=(VirtualChar ch1, char ch2) - => !(ch1 == ch2); - - public override bool Equals(object? obj) - => obj is VirtualChar vc && Equals(vc); - - public bool Equals(VirtualChar other) - => Rune == other.Rune && - SurrogateChar == other.SurrogateChar && - Span == other.Span; - - public override int GetHashCode() - { - var hashCode = 1985253839; - hashCode = hashCode * -1521134295 + Rune.GetHashCode(); - hashCode = hashCode * -1521134295 + SurrogateChar.GetHashCode(); - hashCode = hashCode * -1521134295 + Span.GetHashCode(); - return hashCode; - } - - #endregion - - #region string operations - - /// <inheritdoc/> - public override string ToString() - => SurrogateChar != 0 ? SurrogateChar.ToString() : Rune.ToString(); - - public void AppendTo(StringBuilder builder) - { - if (SurrogateChar != 0) - { - builder.Append(SurrogateChar); - return; - } - - Span<char> chars = stackalloc char[2]; - - var length = Rune.EncodeToUtf16(chars); - builder.Append(chars[0]); - if (length == 2) - { - builder.Append(chars[1]); - } - } - - #endregion - - #region comparable - - public int CompareTo(VirtualChar other) - => Value - other.Value; - - public static bool operator <(VirtualChar ch1, VirtualChar ch2) - => ch1.Value < ch2.Value; - - public static bool operator <=(VirtualChar ch1, VirtualChar ch2) - => ch1.Value <= ch2.Value; - - public static bool operator >(VirtualChar ch1, VirtualChar ch2) - => ch1.Value > ch2.Value; - - public static bool operator >=(VirtualChar ch1, VirtualChar ch2) - => ch1.Value >= ch2.Value; - - public int CompareTo(char other) - => Value - other; - - public static bool operator <(VirtualChar ch1, char ch2) - => ch1.Value < ch2; - - public static bool operator <=(VirtualChar ch1, char ch2) - => ch1.Value <= ch2; - - public static bool operator >(VirtualChar ch1, char ch2) - => ch1.Value > ch2; - - public static bool operator >=(VirtualChar ch1, char ch2) - => ch1.Value >= ch2; - - #endregion -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualCharSequence.Chunks.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualCharSequence.Chunks.cs deleted file mode 100644 index ea57a01f4951..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualCharSequence.Chunks.cs +++ /dev/null @@ -1,163 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Text; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; -using Microsoft.CodeAnalysis.Collections; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; - -internal partial struct VirtualCharSequence -{ - /// <summary> - /// Abstraction over a contiguous chunk of <see cref="VirtualChar"/>s. This - /// is used so we can expose <see cref="VirtualChar"/>s over an <see cref="ImmutableArray{VirtualChar}"/> - /// or over a <see cref="string"/>. The latter is especially useful for reducing - /// memory usage in common cases of string tokens without escapes. - /// </summary> - private abstract partial class Chunk - { - protected Chunk() - { - } - - public abstract int Length { get; } - public abstract VirtualChar this[int index] { get; } - public abstract VirtualChar? Find(int position); - } - - /// <summary> - /// Thin wrapper over an actual <see cref="ImmutableSegmentedList{T}"/>. - /// This will be the common construct we generate when getting the - /// <see cref="Chunk"/> for a string token that has escapes in it. - /// </summary> - private class ImmutableSegmentedListChunk : Chunk - { - private readonly ImmutableList<VirtualChar> _array; - - public ImmutableSegmentedListChunk(ImmutableList<VirtualChar> array) - => _array = array; - - public override int Length => _array.Count; - public override VirtualChar this[int index] => _array[index]; - - public override VirtualChar? Find(int position) - { - if (_array.IsEmpty) - { - return null; - } - if (position < _array[0].Span.Start || position >= _array[_array.Count - 1].Span.End) - { - return null; - } - var index = BinarySearch(_array, position, static (ch, position) => - { - if (position < ch.Span.Start) - { - return 1; - } - - if (position >= ch.Span.End) - { - return -1; - } - - return 0; - }); - Debug.Assert(index >= 0); - return _array[index]; - } - } - - internal static int BinarySearch<TElement, TValue>(ImmutableList<TElement> array, TValue value, Func<TElement, TValue, int> comparer) - { - int low = 0; - int high = array.Count - 1; - - while (low <= high) - { - int middle = low + ((high - low) >> 1); - int comparison = comparer(array[middle], value); - - if (comparison == 0) - { - return middle; - } - - if (comparison > 0) - { - high = middle - 1; - } - else - { - low = middle + 1; - } - } - - return ~low; - } - - /// <summary> - /// Represents a <see cref="Chunk"/> on top of a normal - /// string. This is the common case of the type of the sequence we would - /// create for a normal string token without any escapes in it. - /// </summary> - private class StringChunk : Chunk - { - private readonly int _firstVirtualCharPosition; - - /// <summary> - /// The underlying string that we're returning virtual chars from. Note: - /// this will commonly include things like quote characters. Clients who - /// do not want that should then ask for an appropriate <see cref="VirtualCharSequence.GetSubSequence"/> - /// back that does not include those characters. - /// </summary> - private readonly string _underlyingData; - - public StringChunk(int firstVirtualCharPosition, string data) - { - _firstVirtualCharPosition = firstVirtualCharPosition; - _underlyingData = data; - } - - public override int Length => _underlyingData.Length; - - public override VirtualChar? Find(int position) - { - var stringIndex = position - _firstVirtualCharPosition; - if (stringIndex < 0 || stringIndex >= _underlyingData.Length) - { - return null; - } - - return this[stringIndex]; - } - - public override VirtualChar this[int index] - { - get - { -#if DEBUG - // We should never have a properly paired high/low surrogate in a StringChunk. We are only created - // when the string has the same number of chars as there are VirtualChars. - if (char.IsHighSurrogate(_underlyingData[index])) - { - Debug.Assert(index + 1 >= _underlyingData.Length || - !char.IsLowSurrogate(_underlyingData[index + 1])); - } -#endif - - var span = new TextSpan(_firstVirtualCharPosition + index, length: 1); - var ch = _underlyingData[index]; - return char.IsSurrogate(ch) - ? VirtualChar.Create(ch, span) - : VirtualChar.Create(new Rune(ch), span); - } - } - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualCharSequence.Enumerator.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualCharSequence.Enumerator.cs deleted file mode 100644 index 8f2b70a23ddd..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualCharSequence.Enumerator.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections; -using System.Collections.Generic; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; - -namespace Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; - -internal partial struct VirtualCharSequence -{ - public struct Enumerator : IEnumerator<VirtualChar> - { - private readonly VirtualCharSequence _virtualCharSequence; - private int _position; - - public Enumerator(VirtualCharSequence virtualCharSequence) - { - _virtualCharSequence = virtualCharSequence; - _position = -1; - } - - public bool MoveNext() => ++_position < _virtualCharSequence.Length; - public VirtualChar Current => _virtualCharSequence[_position]; - - public void Reset() - => _position = -1; - - object? IEnumerator.Current => this.Current; - public void Dispose() { } - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualCharSequence.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualCharSequence.cs deleted file mode 100644 index 9a94f8a47bf2..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/VirtualChars/VirtualCharSequence.cs +++ /dev/null @@ -1,231 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Text; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; - -/// <summary> -/// Represents the individual characters that raw string token represents (i.e. with escapes collapsed). -/// The difference between this and the result from token.ValueText is that for each collapsed character -/// returned the original span of text in the original token can be found. i.e. if you had the -/// following in C#: -/// <para/> -/// <c>"G\u006fo"</c> -/// <para/> -/// Then you'd get back: -/// <para/> -/// <c>'G' -> [0, 1) 'o' -> [1, 7) 'o' -> [7, 1)</c> -/// <para/> -/// This allows for embedded language processing that can refer back to the user's original code -/// instead of the escaped value we're processing. -/// </summary> -internal partial struct VirtualCharSequence -{ - public static readonly VirtualCharSequence Empty = Create(ImmutableList<VirtualChar>.Empty); - - public static VirtualCharSequence Create(ImmutableList<VirtualChar> virtualChars) - => new(new ImmutableSegmentedListChunk(virtualChars)); - - public static VirtualCharSequence Create(int firstVirtualCharPosition, string underlyingData) - => new(new StringChunk(firstVirtualCharPosition, underlyingData)); - - /// <summary> - /// The actual characters that this <see cref="VirtualCharSequence"/> is a portion of. - /// </summary> - private readonly Chunk _leafCharacters; - - /// <summary> - /// The portion of <see cref="_leafCharacters"/> that is being exposed. This span - /// is `[inclusive, exclusive)`. - /// </summary> - private readonly TextSpan _span; - - private VirtualCharSequence(Chunk sequence) - : this(sequence, new TextSpan(0, sequence.Length)) - { - } - - private VirtualCharSequence(Chunk sequence, TextSpan span) - { - if (span.Start > sequence.Length) - { - throw new ArgumentException("Span start out of range."); - } - - if (span.End > sequence.Length) - { - throw new ArgumentException("Span end out of range."); - } - - _leafCharacters = sequence; - _span = span; - } - - /// <summary> - /// Gets the number of elements contained in the <see cref="VirtualCharSequence"/>. - /// </summary> - public int Length => _span.Length; - - /// <summary> - /// Gets the <see cref="VirtualChar"/> at the specified index. - /// </summary> - public VirtualChar this[int index] => _leafCharacters[_span.Start + index]; - - /// <summary> - /// Gets a value indicating whether the <see cref="VirtualCharSequence"/> was declared but not initialized. - /// </summary> - public bool IsDefault => _leafCharacters == null; - public bool IsEmpty => Length == 0; - public bool IsDefaultOrEmpty => IsDefault || IsEmpty; - - /// <summary> - /// Retreives a sub-sequence from this <see cref="VirtualCharSequence"/>. - /// </summary> - public VirtualCharSequence GetSubSequence(TextSpan span) - => new(_leafCharacters, new TextSpan(_span.Start + span.Start, span.Length)); - - public Enumerator GetEnumerator() - => new(this); - - public VirtualChar First() => this[0]; - public VirtualChar Last() => this[Length - 1]; - - /// <summary> - /// Finds the virtual char in this sequence that contains the position. Will return null if this position is not - /// in the span of this sequence. - /// </summary> - public VirtualChar? Find(int position) - => _leafCharacters?.Find(position); - - public bool Contains(VirtualChar @char) - => IndexOf(@char) >= 0; - - public int IndexOf(VirtualChar @char) - { - var index = 0; - foreach (var ch in this) - { - if (ch == @char) - { - return index; - } - - index++; - } - - return -1; - } - - public VirtualChar? FirstOrNull(Func<VirtualChar, bool> predicate) - { - foreach (var ch in this) - { - if (predicate(ch)) - { - return ch; - } - } - - return null; - } - - public VirtualChar? LastOrNull(Func<VirtualChar, bool> predicate) - { - for (var i = this.Length - 1; i >= 0; i--) - { - var ch = this[i]; - if (predicate(ch)) - { - return ch; - } - } - - return null; - } - - public bool Any(Func<VirtualChar, bool> predicate) - { - foreach (var ch in this) - { - if (predicate(ch)) - { - return true; - } - } - - return false; - } - - public bool All(Func<VirtualChar, bool> predicate) - { - foreach (var ch in this) - { - if (!predicate(ch)) - { - return false; - } - } - - return true; - } - - public VirtualCharSequence Skip(int count) - => this.GetSubSequence(TextSpan.FromBounds(count, this.Length)); - - public VirtualCharSequence SkipWhile(Func<VirtualChar, bool> predicate) - { - var start = 0; - foreach (var ch in this) - { - if (!predicate(ch)) - { - break; - } - - start++; - } - - return this.GetSubSequence(TextSpan.FromBounds(start, this.Length)); - } - - /// <summary> - /// Create a <see cref="string"/> from the <see cref="VirtualCharSequence"/>. - /// </summary> - public string CreateString() - { - var sb = new StringBuilder(); - foreach (var ch in this) - { - ch.AppendTo(sb); - } - - return sb.ToString(); - } - - [Conditional("DEBUG")] - public void AssertAdjacentTo(VirtualCharSequence virtualChars) - { - Debug.Assert(_leafCharacters == virtualChars._leafCharacters); - Debug.Assert(_span.End == virtualChars._span.Start); - } - - /// <summary> - /// Combines two <see cref="VirtualCharSequence"/>s, producing a final - /// sequence that points at the same underlying data, but spans from the - /// start of <paramref name="chars1"/> to the end of <paramref name="chars2"/>. - /// </summary> - public static VirtualCharSequence FromBounds( - VirtualCharSequence chars1, VirtualCharSequence chars2) - { - Debug.Assert(chars1._leafCharacters == chars2._leafCharacters); - return new VirtualCharSequence( - chars1._leafCharacters, - TextSpan.FromBounds(chars1._span.Start, chars2._span.End)); - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/README.md b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/README.md deleted file mode 100644 index 2e6948a02d77..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Route embedded language tooling - -Route tooling is applied to strings used on APIs annotated with `[StringSyntax("Route")]`. The tooling consists of 5 components: - -1. An analyzer that reports syntax problems in routes. -2. A classifier that colorizes route parts. -3. A brace matcher that highlights matching braces inside routes. -4. A highlighter that highlights route parameter names inside routes and matching arguments the route is used with. -5. A completion provider provides completion items for route constraints and parameter names. - -## Roslyn integration - -Route tooling uses public Roslyn APIs where possible and internal Roslyn APIs where necessary. Internal APIs are accessed via `Microsoft.CodeAnalysis.ExternalAccess.AspNetCore`, which follows Roslyn's standard external access pattern. - -The classifier, brace matcher, and highlighter currently use internal APIs. The analyzer is a standard Roslyn analyzer, and the completion provider is a standard Roslyn completion provider. - -The analyzer can be run from an editor or from the SDK command-line. Because of this, the analyzer avoids any dependency on `Microsoft.CodeAnalysis.ExternalAccess.AspNetCore`. - -## Route pattern tree - -A route pattern tree is shared between all components. It is parsed from the route string and has nodes and tokens for the various parts of a route. Additionally, the route pattern tree has a list containing any route syntax errors encountered while parsing the route. - -Route parsing uses `IVirtualCharService`. This service provides a uniform view of a language's string token characters. It is easy to handle language string features, such as escaped chars. - -## Future improvements - -### Dependencies - -Route tooling pushes Roslyn boundaries by being the first external project to use string syntax features. Ideally `Microsoft.CodeAnalysis.ExternalAccess.AspNetCore` is a temporary workaround. Making these features part of Roslyn's public API would allow ASP.NET Core to remove the external access dependency and source code copying. - -- Reduce the amount of source copied from Roslyn. - - String syntax detector - - Virtual char service - - Embedded syntax model -- Remove external access requirements by adding public features to Roslyn. - - Classifier - - Brace matcher - - Highlighter - -### Splitting projects - -Splitting editor specific features out into a different assembly which references `Microsoft.CodeAnalysis.ExternalAccess.AspNetCore` would prevent analyzers accidentally using editor APIs. - -### Completion provider - -The completion provider doesn't support the user explicitly requesting completions. It isn't supported because CompletionProvider's loaded from projects don't support overriding description or customizing how text is added. This is needed to support completions when the user explicitly asks for completion. [It's fixed in VS 17.4](https://github.com/dotnet/roslyn/pull/61976). diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternHelpers.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternHelpers.cs deleted file mode 100644 index 584c33867189..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternHelpers.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Immutable; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; -using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; - -using RoutePatternToken = EmbeddedSyntaxToken<RoutePatternKind>; - -internal static class RoutePatternHelpers -{ - public static RoutePatternToken CreateToken(RoutePatternKind kind, VirtualCharSequence virtualChars) - => new(kind, virtualChars, ImmutableArray<EmbeddedDiagnostic>.Empty, value: null); - - public static RoutePatternToken CreateMissingToken(RoutePatternKind kind) - => CreateToken(kind, VirtualCharSequence.Empty); -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternKind.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternKind.cs deleted file mode 100644 index edd88e5ffdb7..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternKind.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; - -internal enum RoutePatternKind -{ - None = 0, - EndOfFile, - Segment, - CompilationUnit, - Seperator, - Literal, - Replacement, - Parameter, - CatchAll, - ParameterName, - Optional, - DefaultValue, - PolicyFragment, - PolicyFragmentEscaped, - ParameterPolicy, - - // Tokens - TextToken, - SlashToken, - TildeToken, - /// <summary> - /// { - /// </summary> - OpenBraceToken, - /// <summary> - /// } - /// </summary> - CloseBraceToken, - /// <summary> - /// [ - /// </summary> - OpenBracketToken, - /// <summary> - /// ] - /// </summary> - CloseBracketToken, - DotToken, - EqualsToken, - ColonToken, - ReplacementToken, - AsteriskToken, - /// <summary> - /// ( - /// </summary> - OpenParenToken, - /// <summary> - /// ) - /// </summary> - CloseParenToken, - QuestionMarkToken, - CommaToken, - ParameterNameToken, - DefaultValueToken, - PolicyNameToken, - PolicyFragmentToken, -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternLexer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternLexer.cs deleted file mode 100644 index 8d754999ce9b..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternLexer.cs +++ /dev/null @@ -1,506 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; -using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; - -using static RoutePatternHelpers; - -using RoutePatternToken = EmbeddedSyntaxToken<RoutePatternKind>; - -internal struct RoutePatternLexer -{ - public readonly VirtualCharSequence Text; - public readonly bool SupportTokenReplacement; - public int Position; - - public RoutePatternLexer(VirtualCharSequence text, bool supportTokenReplacement) : this() - { - Text = text; - SupportTokenReplacement = supportTokenReplacement; - } - - public VirtualChar CurrentChar => Position < Text.Length ? Text[Position] : default; - - public VirtualCharSequence GetSubPatternToCurrentPos(int start) - => GetSubPattern(start, Position); - - public VirtualCharSequence GetSubPattern(int start, int end) - => Text.GetSubSequence(TextSpan.FromBounds(start, end)); - - public RoutePatternToken ScanNextToken() - { - if (Position == Text.Length) - { - return CreateToken(RoutePatternKind.EndOfFile, VirtualCharSequence.Empty); - } - - var ch = CurrentChar; - Position++; - - return CreateToken(GetKind(ch), Text.GetSubSequence(new TextSpan(Position - 1, 1))); - } - - private static RoutePatternKind GetKind(VirtualChar ch) - => ch.Value switch - { - '/' => RoutePatternKind.SlashToken, - '~' => RoutePatternKind.TildeToken, - '{' => RoutePatternKind.OpenBraceToken, - '}' => RoutePatternKind.CloseBraceToken, - '[' => RoutePatternKind.OpenBracketToken, - ']' => RoutePatternKind.CloseBracketToken, - '.' => RoutePatternKind.DotToken, - '=' => RoutePatternKind.EqualsToken, - ':' => RoutePatternKind.ColonToken, - '*' => RoutePatternKind.AsteriskToken, - '(' => RoutePatternKind.OpenParenToken, - ')' => RoutePatternKind.CloseParenToken, - '?' => RoutePatternKind.QuestionMarkToken, - ',' => RoutePatternKind.CommaToken, - _ => RoutePatternKind.TextToken, - }; - - public TextSpan GetTextSpan(int startInclusive, int endExclusive) - => TextSpan.FromBounds(Text[startInclusive].Span.Start, Text[endExclusive - 1].Span.End); - - public bool IsAt(string val) - => TextAt(Position, val); - - private bool TextAt(int position, string val) - { - for (var i = 0; i < val.Length; i++) - { - if (position + i >= Text.Length || - Text[position + i].Value != val[i]) - { - return false; - } - } - - return true; - } - - internal RoutePatternToken? TryScanLiteral() - { - if (Position == Text.Length) - { - return null; - } - - var start = Position; - - int? mismatchBracePosition = null; - int? mismatchBracketPosition = null; - int? questionMarkPosition = null; - while (Position < Text.Length) - { - var ch = CurrentChar; - - if (ch.Value == '/') - { - // Literal ends at a seperator or start of a parameter. - break; - } - else if (ch.Value == '{' && IsUnescapedChar(ref Position, '{')) - { - // Literal ends at brace start. - break; - } - else if (ch.Value == '}' && IsUnescapedChar(ref Position, '}')) - { - // An unescaped brace is invalid. - mismatchBracePosition = Position; - } - else if (ch.Value == '?') - { - questionMarkPosition = Position; - } - else if (ch.Value == '[' && IsUnescapedChar(ref Position, '[') && SupportTokenReplacement) - { - // Literal ends at bracket start if token replacement is supported. - break; - } - else if (IsUnescapedChar(ref Position, ']') && SupportTokenReplacement) - { - mismatchBracketPosition = Position; - } - - Position++; - } - - if (Position == start) - { - return null; - } - - var token = CreateToken(RoutePatternKind.Literal, GetSubPatternToCurrentPos(start)); - token = token.With(value: token.VirtualChars.CreateString()); - - // It's fine that this only warns about the first invalid close brace. - if (mismatchBracePosition != null) - { - token = token.AddDiagnosticIfNone(new EmbeddedDiagnostic( - Resources.TemplateRoute_MismatchedParameter, - token.GetSpan())); - } - if (mismatchBracketPosition != null) - { - token = token.AddDiagnosticIfNone(new EmbeddedDiagnostic( - Resources.AttributeRoute_TokenReplacement_ImbalancedSquareBrackets, - token.GetSpan())); - } - if (questionMarkPosition != null) - { - token = token.AddDiagnosticIfNone(new EmbeddedDiagnostic( - Resources.FormatTemplateRoute_InvalidLiteral(token.Value), - token.GetSpan())); - } - - return token; - } - - private const char Separator = '/'; - private const char OpenBrace = '{'; - private const char CloseBrace = '}'; - private const char QuestionMark = '?'; - private const char Asterisk = '*'; - - internal static readonly char[] InvalidParameterNameChars = new char[] - { - Separator, - OpenBrace, - CloseBrace, - QuestionMark, - Asterisk - }; - - internal RoutePatternToken? TryScanParameterName() - { - if (Position == Text.Length) - { - return null; - } - - var start = Position; - var hasInvalidChar = false; - var hasUnescapedOpenBrace = false; - while (Position < Text.Length) - { - var ch = CurrentChar; - if (ch.Value is ':' or '=' && start != Position) - { - // Colon and equals ends a parameter name unless they're the first character. - // I think this is a bug in RoutePatternParser but follow it for compatibility. - break; - } - else if (IsTrailingQuestionMark(ch)) - { - // Parameter name ends before question mark (optional) if at the end of the parameter name. - // e.g., {id?} - break; - } - else if (ch.Value == '}' && IsUnescapedChar(ref Position, '}')) - { - break; - } - else if (ch.Value == '{' && IsUnescapedChar(ref Position, '{')) - { - hasUnescapedOpenBrace = true; - } - else if (IsInvalidNameChar(ch)) - { - hasInvalidChar = true; - } - - Position++; - } - - if (Position == start) - { - return null; - } - - var token = CreateToken(RoutePatternKind.ParameterNameToken, GetSubPatternToCurrentPos(start)); - token = token.With(value: token.VirtualChars.CreateString()); - if (hasUnescapedOpenBrace) - { - token = token.AddDiagnosticIfNone( - new EmbeddedDiagnostic(Resources.TemplateRoute_UnescapedBrace, token.GetSpan())); - } - if (hasInvalidChar) - { - token = token.AddDiagnosticIfNone( - new EmbeddedDiagnostic(Resources.FormatTemplateRoute_InvalidParameterName(token.Value.ToString().Replace("{{", "{").Replace("}}", "}")), token.GetSpan())); - } - - return token; - - static bool IsInvalidNameChar(VirtualChar ch) => - ch.Value switch - { - Separator => true, - OpenBrace => true, - CloseBrace => true, - QuestionMark => true, - Asterisk => true, - _ => false - }; - } - - private bool IsTrailingQuestionMark(VirtualChar ch) - { - return ch.Value == '?' && IsAt("?}") && !IsAt("?}}"); - } - - internal RoutePatternToken? TryScanUnescapedPolicyFragment() - { - if (Position == Text.Length) - { - return null; - } - - var start = Position; - var hasUnescapedOpenBrace = false; - while (Position < Text.Length) - { - var ch = Text[Position]; - if (ch.Value is ':' or '=' or '?') - { - break; - } - else if (ch.Value == '{' && IsUnescapedChar(ref Position, '{')) - { - hasUnescapedOpenBrace = true; - } - else if (IsUnescapedChar(ref Position, '}')) - { - break; - } - - // Only start escaped fragment if there is an open and close. - if (ch.Value == '(') - { - if (HasPolicyParenClose()) - { - break; - } - } - Position++; - } - - if (Position == start) - { - return null; - } - - var token = CreateToken(RoutePatternKind.PolicyFragmentToken, GetSubPatternToCurrentPos(start)); - token = token.With(value: token.VirtualChars.CreateString()); - if (hasUnescapedOpenBrace) - { - token = token.AddDiagnosticIfNone( - new EmbeddedDiagnostic(Resources.TemplateRoute_UnescapedBrace, token.GetSpan())); - } - return token; - } - - internal bool IsUnescapedChar(ref int position, char c) - { - if (Text[position].Value != c) - { - return false; - } - - if (position + 1 >= Text.Length || Text[position + 1].Value != c) - { - return true; - } - - position++; - return false; - } - - internal bool IsUnescapedCharLookahead(ref int position, char c) - { - var currentPosition = position; - while (currentPosition < Text.Length && Text[currentPosition].Value == c) - { - currentPosition++; - } - - // The char is unescaped if there is an odd number, e.g. - // [ == unescaped - // [[ == escaped - // [[[ = unescaped, etc - if ((currentPosition - position) % 2 == 1) - { - return true; - } - // If escaped chars encountered then skip to the end. - if (currentPosition > position) - { - position = currentPosition - 1; - } - return false; - } - - internal RoutePatternToken? TryScanEscapedPolicyFragment() - { - if (Position == Text.Length) - { - return null; - } - - var start = Position; - var parameterEndedWithoutCloseParen = false; - var hasUnescapedOpenBrace = false; - while (Position < Text.Length) - { - var ch = Text[Position]; - - if (IsUnescapedChar(ref Position, '}')) - { - parameterEndedWithoutCloseParen = true; - break; - } - else if (ch.Value == '{' && IsUnescapedChar(ref Position, '{')) - { - hasUnescapedOpenBrace = true; - } - else if (ch.Value == ')') - { - break; - } - - Position++; - } - - if (parameterEndedWithoutCloseParen) - { - // Couldn't find close paren before end of parameter. - // Reset position to start so content can be parsed as unescaped. - Position = start; - return null; - } - - // This token could end with an unclosed parameter. - var token = CreateToken(RoutePatternKind.PolicyFragmentToken, GetSubPatternToCurrentPos(start)); - token = token.With(value: token.VirtualChars.CreateString()); - if (hasUnescapedOpenBrace) - { - token = token.AddDiagnosticIfNone( - new EmbeddedDiagnostic(Resources.TemplateRoute_UnescapedBrace, token.GetSpan())); - } - return token; - } - - internal RoutePatternToken? TryScanReplacementToken() - { - if (Position == Text.Length) - { - return null; - } - - var start = Position; - var hasUnescapedOpenBracket = false; - while (Position < Text.Length) - { - var ch = Text[Position]; - - if (ch.Value == '[' && IsUnescapedChar(ref Position, '[')) - { - hasUnescapedOpenBracket = true; - } - else if (IsUnescapedCharLookahead(ref Position, ']')) - { - // Note that a replacement token ends at the start of a sequence of escapes. - // ends here -> ]]] - break; - } - - Position++; - } - - if (Position == start) - { - return null; - } - - // This token could end with an unclosed parameter. - var token = CreateToken(RoutePatternKind.ReplacementToken, GetSubPatternToCurrentPos(start)); - token = token.With(value: token.VirtualChars.CreateString()); - if (hasUnescapedOpenBracket) - { - token = token.AddDiagnosticIfNone( - new EmbeddedDiagnostic(Resources.AttributeRoute_TokenReplacement_UnescapedBraceInToken, token.GetSpan())); - } - return token; - } - - internal RoutePatternToken? TryScanDefaultValue() - { - if (Position == Text.Length) - { - return null; - } - - var start = Position; - while (Position < Text.Length) - { - var ch = Text[Position]; - - if (ch.Value is '}') - { - break; - } - else if (IsTrailingQuestionMark(ch)) - { - // Parameter name ends before question mark (optional) if at the end of the parameter name. - // e.g., {id?} - break; - } - - Position++; - } - - if (Position == start) - { - return null; - } - - var token = CreateToken(RoutePatternKind.DefaultValueToken, GetSubPatternToCurrentPos(start)); - token = token.With(value: token.VirtualChars.CreateString()); - return token; - } - - internal bool HasPolicyParenClose() - { - if (Position == Text.Length) - { - return false; - } - - var current = Position; - while (current < Text.Length) - { - var ch = Text[current]; - - if (ch.Value == ')') - { - return true; - } - if (IsUnescapedChar(ref current, '}')) - { - return false; - } - current++; - } - - return false; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternNode.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternNode.cs deleted file mode 100644 index fa817cb750f7..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternNode.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; - -internal abstract class RoutePatternNode : EmbeddedSyntaxNode<RoutePatternKind, RoutePatternNode> -{ - protected RoutePatternNode(RoutePatternKind kind) : base(kind) - { - } - - public abstract void Accept(IRoutePatternNodeVisitor visitor); -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternNodes.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternNodes.cs deleted file mode 100644 index d5f5e7ab7884..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternNodes.cs +++ /dev/null @@ -1,418 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Immutable; -using System.Diagnostics; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; - -using RoutePatternNodeOrToken = EmbeddedSyntaxNodeOrToken<RoutePatternKind, RoutePatternNode>; -using RoutePatternToken = EmbeddedSyntaxToken<RoutePatternKind>; - -internal sealed class RoutePatternCompilationUnit : RoutePatternNode -{ - public RoutePatternCompilationUnit(ImmutableArray<RoutePatternRootPartNode> parts, RoutePatternToken endOfFileToken) - : base(RoutePatternKind.CompilationUnit) - { - Debug.Assert(parts != null); - Debug.Assert(endOfFileToken.Kind == RoutePatternKind.EndOfFile); - Parts = parts; - EndOfFileToken = endOfFileToken; - } - - public ImmutableArray<RoutePatternRootPartNode> Parts { get; } - public RoutePatternToken EndOfFileToken { get; } - - internal override int ChildCount => Parts.Length + 1; - - internal override RoutePatternNodeOrToken ChildAt(int index) - { - if (index == Parts.Length) - { - return EndOfFileToken; - } - - return Parts[index]; - } - - public override void Accept(IRoutePatternNodeVisitor visitor) - => visitor.Visit(this); -} - -internal sealed class RoutePatternSegmentNode : RoutePatternRootPartNode -{ - public ImmutableArray<RoutePatternNode> Children { get; } - - internal override int ChildCount => Children.Length; - - public RoutePatternSegmentNode(ImmutableArray<RoutePatternNode> children) - : base(RoutePatternKind.Segment) - { - Children = children; - } - - internal override RoutePatternNodeOrToken ChildAt(int index) - => Children[index]; - - public override void Accept(IRoutePatternNodeVisitor visitor) - => visitor.Visit(this); -} - -/// <summary> -/// [controller] -/// </summary> -internal sealed class RoutePatternReplacementNode : RoutePatternSegmentPartNode -{ - public RoutePatternReplacementNode( - RoutePatternToken openBracketToken, RoutePatternToken textToken, RoutePatternToken closeBracketToken) - : base(RoutePatternKind.Replacement) - { - Debug.Assert(openBracketToken.Kind == RoutePatternKind.OpenBracketToken); - Debug.Assert(textToken.Kind == RoutePatternKind.ReplacementToken); - Debug.Assert(closeBracketToken.Kind == RoutePatternKind.CloseBracketToken); - OpenBracketToken = openBracketToken; - TextToken = textToken; - CloseBracketToken = closeBracketToken; - } - - public RoutePatternToken OpenBracketToken { get; } - public RoutePatternToken TextToken { get; } - public RoutePatternToken CloseBracketToken { get; } - - internal override int ChildCount => 3; - - internal override RoutePatternNodeOrToken ChildAt(int index) - => index switch - { - 0 => OpenBracketToken, - 1 => TextToken, - 2 => CloseBracketToken, - _ => throw new InvalidOperationException(), - }; - - public override void Accept(IRoutePatternNodeVisitor visitor) - => visitor.Visit(this); -} - -/// <summary> -/// {controller} -/// </summary> -internal sealed class RoutePatternParameterNode : RoutePatternSegmentPartNode -{ - public RoutePatternParameterNode( - RoutePatternToken openBraceToken, ImmutableArray<RoutePatternParameterPartNode> parameterPartNodes, RoutePatternToken closeBraceToken) - : base(RoutePatternKind.Parameter) - { - Debug.Assert(openBraceToken.Kind == RoutePatternKind.OpenBraceToken); - Debug.Assert(closeBraceToken.Kind == RoutePatternKind.CloseBraceToken); - OpenBraceToken = openBraceToken; - ParameterParts = parameterPartNodes; - CloseBraceToken = closeBraceToken; - } - - public RoutePatternToken OpenBraceToken { get; } - public ImmutableArray<RoutePatternParameterPartNode> ParameterParts { get; } - public RoutePatternToken CloseBraceToken { get; } - - internal override int ChildCount => ParameterParts.Length + 2; - - internal override RoutePatternNodeOrToken ChildAt(int index) - { - if (index == 0) - { - return OpenBraceToken; - } - else if (index == ParameterParts.Length + 1) - { - return CloseBraceToken; - } - else - { - return ParameterParts[index - 1]; - } - } - - public override void Accept(IRoutePatternNodeVisitor visitor) - => visitor.Visit(this); -} - -internal sealed class RoutePatternLiteralNode : RoutePatternSegmentPartNode -{ - public RoutePatternLiteralNode(RoutePatternToken literalToken) - : base(RoutePatternKind.Literal) - { - Debug.Assert(literalToken.Kind == RoutePatternKind.Literal); - LiteralToken = literalToken; - } - - public RoutePatternToken LiteralToken { get; } - - internal override int ChildCount => 1; - - internal override RoutePatternNodeOrToken ChildAt(int index) - => index switch - { - 0 => LiteralToken, - _ => throw new InvalidOperationException(), - }; - - public override void Accept(IRoutePatternNodeVisitor visitor) - => visitor.Visit(this); -} - -internal sealed class RoutePatternOptionalSeperatorNode : RoutePatternSegmentPartNode -{ - public RoutePatternOptionalSeperatorNode(RoutePatternToken seperatorToken) - : base(RoutePatternKind.Seperator) - { - Debug.Assert(seperatorToken.Kind == RoutePatternKind.DotToken); - SeperatorToken = seperatorToken; - } - - public RoutePatternToken SeperatorToken { get; } - - internal override int ChildCount => 1; - - internal override RoutePatternNodeOrToken ChildAt(int index) - => index switch - { - 0 => SeperatorToken, - _ => throw new InvalidOperationException(), - }; - - public override void Accept(IRoutePatternNodeVisitor visitor) - => visitor.Visit(this); -} - -internal sealed class RoutePatternSegmentSeperatorNode : RoutePatternRootPartNode -{ - public RoutePatternSegmentSeperatorNode(RoutePatternToken seperatorToken) - : base(RoutePatternKind.Seperator) - { - Debug.Assert(seperatorToken.Kind == RoutePatternKind.SlashToken); - SeperatorToken = seperatorToken; - } - - public RoutePatternToken SeperatorToken { get; } - - internal override int ChildCount => 1; - - internal override RoutePatternNodeOrToken ChildAt(int index) - => index switch - { - 0 => SeperatorToken, - _ => throw new InvalidOperationException(), - }; - - public override void Accept(IRoutePatternNodeVisitor visitor) - => visitor.Visit(this); -} - -internal sealed class RoutePatternCatchAllParameterPartNode : RoutePatternParameterPartNode -{ - public RoutePatternCatchAllParameterPartNode(RoutePatternToken asteriskToken) - : base(RoutePatternKind.CatchAll) - { - Debug.Assert(asteriskToken.Kind == RoutePatternKind.AsteriskToken); - AsteriskToken = asteriskToken; - } - - public RoutePatternToken AsteriskToken { get; } - - internal override int ChildCount => 1; - - internal override RoutePatternNodeOrToken ChildAt(int index) - => index switch - { - 0 => AsteriskToken, - _ => throw new InvalidOperationException(), - }; - - public override void Accept(IRoutePatternNodeVisitor visitor) - => visitor.Visit(this); -} - -internal sealed class RoutePatternOptionalParameterPartNode : RoutePatternParameterPartNode -{ - public RoutePatternOptionalParameterPartNode(RoutePatternToken questionMarkToken) - : base(RoutePatternKind.Optional) - { - Debug.Assert(questionMarkToken.Kind == RoutePatternKind.QuestionMarkToken); - QuestionMarkToken = questionMarkToken; - } - - public RoutePatternToken QuestionMarkToken { get; } - - internal override int ChildCount => 1; - - internal override RoutePatternNodeOrToken ChildAt(int index) - => index switch - { - 0 => QuestionMarkToken, - _ => throw new InvalidOperationException(), - }; - - public override void Accept(IRoutePatternNodeVisitor visitor) - => visitor.Visit(this); -} - -internal sealed class RoutePatternDefaultValueParameterPartNode : RoutePatternParameterPartNode -{ - public RoutePatternDefaultValueParameterPartNode(RoutePatternToken equalsToken, RoutePatternToken defaultValueToken) - : base(RoutePatternKind.DefaultValue) - { - Debug.Assert(equalsToken.Kind == RoutePatternKind.EqualsToken); - Debug.Assert(defaultValueToken.Kind == RoutePatternKind.DefaultValueToken); - EqualsToken = equalsToken; - DefaultValueToken = defaultValueToken; - } - - public RoutePatternToken EqualsToken { get; } - public RoutePatternToken DefaultValueToken { get; } - - internal override int ChildCount => 2; - - internal override RoutePatternNodeOrToken ChildAt(int index) - => index switch - { - 0 => EqualsToken, - 1 => DefaultValueToken, - _ => throw new InvalidOperationException(), - }; - - public override void Accept(IRoutePatternNodeVisitor visitor) - => visitor.Visit(this); -} - -internal sealed class RoutePatternNameParameterPartNode : RoutePatternParameterPartNode -{ - public RoutePatternNameParameterPartNode(RoutePatternToken parameterNameToken) - : base(RoutePatternKind.ParameterName) - { - Debug.Assert(parameterNameToken.Kind == RoutePatternKind.ParameterNameToken); - ParameterNameToken = parameterNameToken; - } - - public RoutePatternToken ParameterNameToken { get; } - - internal override int ChildCount => 1; - - internal override RoutePatternNodeOrToken ChildAt(int index) - => index switch - { - 0 => ParameterNameToken, - _ => throw new InvalidOperationException(), - }; - - public override void Accept(IRoutePatternNodeVisitor visitor) - => visitor.Visit(this); -} - -internal sealed class RoutePatternPolicyParameterPartNode : RoutePatternParameterPartNode -{ - public RoutePatternPolicyParameterPartNode(RoutePatternToken colonToken, ImmutableArray<RoutePatternNode> policyFragments) - : base(RoutePatternKind.ParameterPolicy) - { - Debug.Assert(colonToken.Kind == RoutePatternKind.ColonToken); - ColonToken = colonToken; - PolicyFragments = policyFragments; - } - - public RoutePatternToken ColonToken { get; } - public ImmutableArray<RoutePatternNode> PolicyFragments { get; } - - internal override int ChildCount => PolicyFragments.Length + 1; - - internal override RoutePatternNodeOrToken ChildAt(int index) - => index switch - { - 0 => ColonToken, - _ => PolicyFragments[index - 1], - }; - - public override void Accept(IRoutePatternNodeVisitor visitor) - => visitor.Visit(this); -} - -internal sealed class RoutePatternPolicyFragmentEscapedNode : RoutePatternNode -{ - public RoutePatternPolicyFragmentEscapedNode( - RoutePatternToken openParenToken, RoutePatternToken argumentToken, RoutePatternToken closeParenToken) - : base(RoutePatternKind.PolicyFragmentEscaped) - { - Debug.Assert(openParenToken.Kind == RoutePatternKind.OpenParenToken); - Debug.Assert(argumentToken.Kind == RoutePatternKind.PolicyFragmentToken); - Debug.Assert(closeParenToken.Kind == RoutePatternKind.CloseParenToken); - OpenParenToken = openParenToken; - CloseParenToken = closeParenToken; - ArgumentToken = argumentToken; - } - - public RoutePatternToken OpenParenToken { get; } - public RoutePatternToken ArgumentToken { get; } - public RoutePatternToken CloseParenToken { get; } - - internal override int ChildCount => 3; - - internal override RoutePatternNodeOrToken ChildAt(int index) - => index switch - { - 0 => OpenParenToken, - 1 => ArgumentToken, - 2 => CloseParenToken, - _ => throw new InvalidOperationException(), - }; - - public override void Accept(IRoutePatternNodeVisitor visitor) - => visitor.Visit(this); -} - -internal sealed class RoutePatternPolicyFragment : RoutePatternNode -{ - public RoutePatternPolicyFragment(RoutePatternToken argumentToken) - : base(RoutePatternKind.PolicyFragment) - { - Debug.Assert(argumentToken.Kind == RoutePatternKind.PolicyFragmentToken); - ArgumentToken = argumentToken; - } - - public RoutePatternToken ArgumentToken { get; } - - internal override int ChildCount => 1; - - internal override RoutePatternNodeOrToken ChildAt(int index) - => index switch - { - 0 => ArgumentToken, - _ => throw new InvalidOperationException(), - }; - - public override void Accept(IRoutePatternNodeVisitor visitor) - => visitor.Visit(this); -} - -internal abstract class RoutePatternRootPartNode : RoutePatternNode -{ - protected RoutePatternRootPartNode(RoutePatternKind kind) - : base(kind) - { - } -} - -internal abstract class RoutePatternSegmentPartNode : RoutePatternNode -{ - protected RoutePatternSegmentPartNode(RoutePatternKind kind) - : base(kind) - { - } -} - -internal abstract class RoutePatternParameterPartNode : RoutePatternNode -{ - protected RoutePatternParameterPartNode(RoutePatternKind kind) - : base(kind) - { - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternParser.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternParser.cs deleted file mode 100644 index 6e49e9afc031..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternParser.cs +++ /dev/null @@ -1,617 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; -using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; - -using static RoutePatternHelpers; -using RoutePatternToken = EmbeddedSyntaxToken<RoutePatternKind>; - -internal partial struct RoutePatternParser -{ - private RoutePatternLexer _lexer; - private RoutePatternToken _currentToken; - private readonly bool _supportTokenReplacement; - - private RoutePatternParser(VirtualCharSequence text, bool supportTokenReplacement) : this() - { - _lexer = new RoutePatternLexer(text, supportTokenReplacement); - - // Get the first token. It is allowed to have trivia on it. - ConsumeCurrentToken(); - _supportTokenReplacement = supportTokenReplacement; - } - - /// <summary> - /// Returns the latest token the lexer has produced, and then asks the lexer to - /// produce the next token after that. - /// </summary> - private RoutePatternToken ConsumeCurrentToken() - { - var previous = _currentToken; - _currentToken = _lexer.ScanNextToken(); - return previous; - } - - /// <summary> - /// Given an input text, and set of options, parses out a fully representative syntax tree - /// and list of diagnostics. Parsing should always succeed, except in the case of the stack - /// overflowing. - /// </summary> - public static RoutePatternTree? TryParse(VirtualCharSequence text, bool supportTokenReplacement) - { - if (text.IsDefault) - { - return null; - } - - var parser = new RoutePatternParser(text, supportTokenReplacement); - return parser.ParseTree(); - } - - private RoutePatternTree ParseTree() - { - var rootParts = ParseRootParts(); - - Debug.Assert(_lexer.Position == _lexer.Text.Length); - Debug.Assert(_currentToken.Kind == RoutePatternKind.EndOfFile); - - var root = new RoutePatternCompilationUnit(rootParts, _currentToken); - - var routeParameters = ImmutableDictionary.CreateBuilder<string, RouteParameter>(StringComparer.OrdinalIgnoreCase); - var seenDiagnostics = new HashSet<EmbeddedDiagnostic>(); - var diagnostics = ImmutableArray.CreateBuilder<EmbeddedDiagnostic>(); - - CollectDiagnostics(root, seenDiagnostics, diagnostics); - ValidateStart(root, diagnostics); - ValidateNoConsecutiveParameters(root, diagnostics); - ValidateNoConsecutiveSeparators(root, diagnostics); - ValidateCatchAllParameters(root, diagnostics); - ValidateParameterParts(root, diagnostics, routeParameters); - - return new RoutePatternTree(_lexer.Text, root, diagnostics.ToImmutable(), routeParameters.ToImmutable()); - } - - private static void ValidateStart(RoutePatternCompilationUnit root, IList<EmbeddedDiagnostic> diagnostics) - { - if (root.ChildCount > 1 && - root.ChildAt(0).Node is var firstNode && - firstNode.Kind == RoutePatternKind.Segment) - { - if (firstNode.ChildCount > 0 && - firstNode.ChildAt(0).Node is var segmentPart && - segmentPart.Kind == RoutePatternKind.Literal) - { - var literalNode = (RoutePatternLiteralNode)segmentPart; - var startText = literalNode.LiteralToken.Value.ToString(); - - // Route pattern starts with tilde - if (startText[0] == '~') - { - // Report problem if either: - // 1. There is more text. It can't be a slash. - // 2. There are more segment parameters. It can't be a slash. - if (startText.Length > 1 || - firstNode.ChildCount > 2) - { - diagnostics.Add(new EmbeddedDiagnostic(Resources.TemplateRoute_InvalidRouteTemplate, segmentPart.GetSpan())); - return; - } - - // No problem if tilde is followed by slash. - if (root.ChildCount > 2 && - root.ChildAt(1).Node is var secondNode && - secondNode.Kind == RoutePatternKind.Seperator) - { - return; - } - - // Tilde by itself. - diagnostics.Add(new EmbeddedDiagnostic(Resources.TemplateRoute_InvalidRouteTemplate, segmentPart.GetSpan())); - } - } - } - } - - private static void ValidateCatchAllParameters(RoutePatternCompilationUnit root, IList<EmbeddedDiagnostic> diagnostics) - { - RoutePatternParameterNode? catchAllParameterNode = null; - foreach (var part in root) - { - if (part.Kind == RoutePatternKind.Segment) - { - if (catchAllParameterNode != null) - { - // Validate that there aren't segments following catch-all. - diagnostics.Add(new EmbeddedDiagnostic(Resources.TemplateRoute_CatchAllMustBeLast, catchAllParameterNode.GetSpan())); - break; - } - - // Check that segment doesn't have catch-all in a complex segment. - foreach (var segmentPart in part.Node) - { - if (segmentPart.Kind == RoutePatternKind.Parameter) - { - var catchAllParameterPart = (RoutePatternCatchAllParameterPartNode)segmentPart.Node.GetChildNode(RoutePatternKind.CatchAll); - if (catchAllParameterPart != null) - { - catchAllParameterNode = (RoutePatternParameterNode)segmentPart.Node; - if (part.Node.ChildCount > 1) - { - diagnostics.Add(new EmbeddedDiagnostic(Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment, catchAllParameterNode.GetSpan())); - } - } - } - } - } - } - } - - private static void ValidateNoConsecutiveParameters(RoutePatternCompilationUnit root, IList<EmbeddedDiagnostic> diagnostics) - { - RoutePatternNode previousNode = null; - foreach (var part in root) - { - if (part.Kind == RoutePatternKind.Segment) - { - foreach (var segmentPart in part.Node) - { - if (previousNode != null && previousNode.Kind == RoutePatternKind.Parameter) - { - var previousParameterNode = (RoutePatternParameterNode)previousNode; - var isOptional = previousParameterNode.GetChildNode(RoutePatternKind.Optional) != null; - if (isOptional) - { - var message = Resources.FormatTemplateRoute_OptionalParameterHasTobeTheLast( - part.Node.ToString(), - previousParameterNode.GetChildNode(RoutePatternKind.ParameterName).ToString(), - segmentPart.Node.ToString()); - diagnostics.Add(new EmbeddedDiagnostic(message, part.Node.GetSpan())); - } - } - - if (segmentPart.Kind == RoutePatternKind.Parameter && previousNode != null) - { - var parameterNode = (RoutePatternParameterNode)segmentPart.Node; - var isOptional = parameterNode.GetChildNode(RoutePatternKind.Optional) != null; - if (isOptional) - { - // Optional parameter must either be in its own segment or follow a period. - // e.g. {filename}.{ext?} - if (previousNode.Kind != RoutePatternKind.Literal || ((RoutePatternLiteralNode)previousNode).LiteralToken.Value.ToString() != ".") - { - var message = Resources.FormatTemplateRoute_OptionalParameterCanbBePrecededByPeriod( - part.Node.ToString(), - parameterNode.GetChildNode(RoutePatternKind.ParameterName).ToString(), - previousNode.ToString()); - diagnostics.Add(new EmbeddedDiagnostic(message, parameterNode.GetSpan())); - } - } - else - { - if (previousNode.Kind == RoutePatternKind.Parameter) - { - diagnostics.Add(new EmbeddedDiagnostic(Resources.TemplateRoute_CannotHaveConsecutiveParameters, parameterNode.GetSpan())); - } - } - } - previousNode = segmentPart.Node; - } - previousNode = null; - } - } - } - - private static void ValidateParameterParts(RoutePatternCompilationUnit root, IList<EmbeddedDiagnostic> diagnostics, IDictionary<string, RouteParameter> routeParameters) - { - foreach (var part in root) - { - if (part.Kind == RoutePatternKind.Segment) - { - foreach (var segmentPart in part.Node) - { - if (segmentPart.Kind == RoutePatternKind.Parameter) - { - var parameterNode = (RoutePatternParameterNode)segmentPart.Node; - var hasOptional = false; - var hasCatchAll = false; - var encodeSlashes = true; - string? name = null; - string? defaultValue = null; - var policies = ImmutableArray.CreateBuilder<string>(); - foreach (var parameterPart in parameterNode) - { - switch (parameterPart.Kind) - { - case RoutePatternKind.ParameterName: - var parameterNameNode = (RoutePatternNameParameterPartNode)parameterPart.Node; - if (!parameterNameNode.ParameterNameToken.IsMissing) - { - name = parameterNameNode.ParameterNameToken.Value.ToString(); - } - break; - case RoutePatternKind.Optional: - hasOptional = true; - break; - case RoutePatternKind.DefaultValue: - var defaultValueNode = (RoutePatternDefaultValueParameterPartNode)parameterPart.Node; - if (!defaultValueNode.DefaultValueToken.IsMissing) - { - defaultValue = defaultValueNode.DefaultValueToken.Value.ToString(); - } - break; - case RoutePatternKind.CatchAll: - var catchAllNode = (RoutePatternCatchAllParameterPartNode)parameterPart.Node; - encodeSlashes = catchAllNode.AsteriskToken.VirtualChars.Length == 1; - hasCatchAll = true; - break; - case RoutePatternKind.ParameterPolicy: - policies.Add(parameterPart.Node.ToString()); - break; - } - } - - var routeParameter = new RouteParameter(name, encodeSlashes, defaultValue, hasOptional, hasCatchAll, policies.ToImmutable()); - if (routeParameter.DefaultValue != null && routeParameter.IsOptional) - { - diagnostics.Add(new EmbeddedDiagnostic(Resources.TemplateRoute_OptionalCannotHaveDefaultValue, parameterNode.GetSpan())); - } - if (routeParameter.IsCatchAll && routeParameter.IsOptional) - { - diagnostics.Add(new EmbeddedDiagnostic(Resources.TemplateRoute_CatchAllCannotBeOptional, parameterNode.GetSpan())); - } - - if (routeParameter.Name != null) - { - if (!routeParameters.ContainsKey(routeParameter.Name)) - { - routeParameters.Add(routeParameter.Name, routeParameter); - } - else - { - diagnostics.Add(new EmbeddedDiagnostic(Resources.FormatTemplateRoute_RepeatedParameter(routeParameter.Name), parameterNode.GetSpan())); - } - } - } - } - } - } - } - - private static void ValidateNoConsecutiveSeparators(RoutePatternCompilationUnit root, IList<EmbeddedDiagnostic> diagnostics) - { - RoutePatternSegmentSeperatorNode? previousNode = null; - foreach (var part in root) - { - if (part.Kind == RoutePatternKind.Seperator) - { - var currentNode = (RoutePatternSegmentSeperatorNode)part.Node; - if (previousNode != null) - { - diagnostics.Add( - new EmbeddedDiagnostic( - Resources.TemplateRoute_CannotHaveConsecutiveSeparators, - EmbeddedSyntaxHelpers.GetSpan(previousNode.SeperatorToken, currentNode.SeperatorToken))); - } - previousNode = currentNode; - } - else - { - previousNode = null; - } - } - } - - private void CollectDiagnostics(RoutePatternNode node, HashSet<EmbeddedDiagnostic> seenDiagnostics, IList<EmbeddedDiagnostic> diagnostics) - { - foreach (var child in node) - { - if (child.IsNode) - { - CollectDiagnostics(child.Node, seenDiagnostics, diagnostics); - } - else - { - var token = child.Token; - AddUniqueDiagnostics(seenDiagnostics, token.Diagnostics, diagnostics); - } - } - } - - /// <summary> - /// It's very common to have duplicated diagnostics. For example, consider "((". This will - /// have two 'missing )' diagnostics, both at the end. Reporting both isn't helpful, so we - /// filter duplicates out here. - /// </summary> - private static void AddUniqueDiagnostics( - HashSet<EmbeddedDiagnostic> seenDiagnostics, ImmutableArray<EmbeddedDiagnostic> from, IList<EmbeddedDiagnostic> to) - { - foreach (var diagnostic in from) - { - if (seenDiagnostics.Add(diagnostic)) - { - to.Add(diagnostic); - } - } - } - - private ImmutableArray<RoutePatternRootPartNode> ParseRootParts() - { - var result = ImmutableArray.CreateBuilder<RoutePatternRootPartNode>(); - - while (_currentToken.Kind != RoutePatternKind.EndOfFile) - { - result.Add(ParseRootPart()); - } - - return result.ToImmutable(); - } - - private RoutePatternRootPartNode ParseRootPart() - => _currentToken.Kind switch - { - RoutePatternKind.SlashToken => ParseSegmentSeperator(), - _ => ParseSegment(), - }; - - private RoutePatternSegmentNode ParseSegment() - { - var result = ImmutableArray.CreateBuilder<RoutePatternNode>(); - - while (_currentToken.Kind != RoutePatternKind.EndOfFile && - _currentToken.Kind != RoutePatternKind.SlashToken) - { - result.Add(ParsePart()); - } - - return new(result.ToImmutable()); - } - - private RoutePatternSegmentPartNode ParsePart() - { - if (_currentToken.Kind == RoutePatternKind.OpenBraceToken) - { - var openBraceToken = _currentToken; - - ConsumeCurrentToken(); - - if (_currentToken.Kind != RoutePatternKind.OpenBraceToken) - { - return ParseParameter(openBraceToken); - } - else - { - MoveBackBeforePreviousScan(); - } - } - else if (_currentToken.Kind == RoutePatternKind.OpenBracketToken && _supportTokenReplacement) - { - var openBracketToken = _currentToken; - - ConsumeCurrentToken(); - - if (_currentToken.Kind != RoutePatternKind.OpenBracketToken) - { - return ParseReplacement(openBracketToken); - } - else - { - MoveBackBeforePreviousScan(); - } - } - - return ParseLiteral(); - } - - private RoutePatternLiteralNode ParseLiteral() - { - MoveBackBeforePreviousScan(); - - var literal = _lexer.TryScanLiteral(); - - ConsumeCurrentToken(); - - // A token must be returned because we've already checked the first character. - return new(literal.Value); - } - - private void MoveBackBeforePreviousScan() - { - if (_currentToken.Kind != RoutePatternKind.EndOfFile) - { - // Move back to un-consume whatever we just consumed. - _lexer.Position--; - } - } - - private RoutePatternReplacementNode ParseReplacement(RoutePatternToken openBracketToken) - { - Debug.Assert(_supportTokenReplacement); - - MoveBackBeforePreviousScan(); - - var replacementToken = _lexer.TryScanReplacementToken(); - if (replacementToken != null) - { - ConsumeCurrentToken(); - } - else - { - replacementToken = CreateMissingToken(RoutePatternKind.ReplacementToken); - if (_currentToken.Kind != RoutePatternKind.EndOfFile) - { - ConsumeCurrentToken(); - - replacementToken = replacementToken.Value.AddDiagnosticIfNone( - new EmbeddedDiagnostic(Resources.AttributeRoute_TokenReplacement_EmptyTokenNotAllowed, _currentToken.GetFullSpan().Value)); - } - } - - return new RoutePatternReplacementNode( - openBracketToken, - replacementToken.Value, - ConsumeToken(RoutePatternKind.CloseBracketToken, Resources.AttributeRoute_TokenReplacement_UnclosedToken)); - } - - private RoutePatternParameterNode ParseParameter(RoutePatternToken openBraceToken) - { - var result = new RoutePatternParameterNode( - openBraceToken, - ParseParameterParts(), - ConsumeToken(RoutePatternKind.CloseBraceToken, Resources.TemplateRoute_MismatchedParameter)); - - return result; - } - - private RoutePatternToken ConsumeToken(RoutePatternKind kind, string? error) - { - if (_currentToken.Kind == kind) - { - return ConsumeCurrentToken(); - } - - var result = CreateMissingToken(kind); - if (error == null) - { - return result; - } - - return result.AddDiagnosticIfNone(new EmbeddedDiagnostic(error, GetTokenStartPositionSpan(_currentToken))); - } - - private ImmutableArray<RoutePatternParameterPartNode> ParseParameterParts() - { - var parts = ImmutableArray.CreateBuilder<RoutePatternParameterPartNode>(); - - // Catch-all, e.g. {*name} - if (_currentToken.Kind == RoutePatternKind.AsteriskToken) - { - var firstAsteriskToken = _currentToken; - ConsumeCurrentToken(); - - // Unescaped catch-all, e.g. {**name} - if (_currentToken.Kind == RoutePatternKind.AsteriskToken) - { - parts.Add(new RoutePatternCatchAllParameterPartNode( - CreateToken( - RoutePatternKind.AsteriskToken, - VirtualCharSequence.FromBounds(firstAsteriskToken.VirtualChars, _currentToken.VirtualChars)))); - ConsumeCurrentToken(); - } - else - { - parts.Add(new RoutePatternCatchAllParameterPartNode(firstAsteriskToken)); - } - } - - MoveBackBeforePreviousScan(); - - var parameterName = _lexer.TryScanParameterName(); - if (parameterName != null) - { - parts.Add(new RoutePatternNameParameterPartNode(parameterName.Value)); - } - else - { - if (_currentToken.Kind != RoutePatternKind.EndOfFile) - { - parts.Add(new RoutePatternNameParameterPartNode( - CreateMissingToken(RoutePatternKind.ParameterNameToken).AddDiagnosticIfNone( - new EmbeddedDiagnostic(Resources.FormatTemplateRoute_InvalidParameterName(""), _currentToken.GetFullSpan().Value)))); - } - } - - ConsumeCurrentToken(); - - // Parameter policy, e.g. {name:int} - while (_currentToken.Kind != RoutePatternKind.EndOfFile) - { - switch (_currentToken.Kind) - { - case RoutePatternKind.ColonToken: - parts.Add(ParsePolicy()); - break; - case RoutePatternKind.QuestionMarkToken: - parts.Add(new RoutePatternOptionalParameterPartNode(ConsumeCurrentToken())); - break; - case RoutePatternKind.EqualsToken: - parts.Add(ParseDefaultValue()); - break; - case RoutePatternKind.CloseBraceToken: - default: - return parts.ToImmutable(); - } - } - - return parts.ToImmutable(); - } - - private RoutePatternDefaultValueParameterPartNode ParseDefaultValue() - { - var equalsToken = _currentToken; - var defaultValue = _lexer.TryScanDefaultValue() ?? CreateMissingToken(RoutePatternKind.DefaultValueToken); - ConsumeCurrentToken(); - return new(equalsToken, defaultValue); - } - - private RoutePatternPolicyParameterPartNode ParsePolicy() - { - var colonToken = ConsumeCurrentToken(); - - var fragments = ImmutableArray.CreateBuilder<RoutePatternNode>(); - while (_currentToken.Kind != RoutePatternKind.EndOfFile && - _currentToken.Kind != RoutePatternKind.CloseBraceToken && - _currentToken.Kind != RoutePatternKind.ColonToken && - _currentToken.Kind != RoutePatternKind.QuestionMarkToken && - _currentToken.Kind != RoutePatternKind.EqualsToken) - { - MoveBackBeforePreviousScan(); - - if (_currentToken.Kind == RoutePatternKind.OpenParenToken) - { - var openParenPosition = ConsumeCurrentToken(); - var escapedPolicyFragment = _lexer.TryScanEscapedPolicyFragment(); - if (escapedPolicyFragment != null) - { - ConsumeCurrentToken(); - - fragments.Add(new RoutePatternPolicyFragmentEscapedNode( - openParenPosition, - escapedPolicyFragment.Value, - _currentToken.Kind == RoutePatternKind.EndOfFile - ? CreateMissingToken(RoutePatternKind.CloseParenToken) - : ConsumeCurrentToken())); - continue; - } - } - - var policyFragment = _lexer.TryScanUnescapedPolicyFragment(); - if (policyFragment == null) - { - break; - } - - fragments.Add(new RoutePatternPolicyFragment(policyFragment.Value)); - ConsumeCurrentToken(); - } - - return new(colonToken, fragments.ToImmutable()); - } - - private RoutePatternSegmentSeperatorNode ParseSegmentSeperator() - => new(ConsumeCurrentToken()); - - private TextSpan GetTokenStartPositionSpan(RoutePatternToken token) - { - return token.Kind == RoutePatternKind.EndOfFile - ? new TextSpan(_lexer.Text.Last().Span.End, 0) - : new TextSpan(token.VirtualChars[0].Span.Start, 0); - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternTree.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternTree.cs deleted file mode 100644 index 40f85a8eba54..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePattern/RoutePatternTree.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Immutable; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; -using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; - -internal sealed class RoutePatternTree : EmbeddedSyntaxTree<RoutePatternKind, RoutePatternNode, RoutePatternCompilationUnit> -{ - public readonly ImmutableDictionary<string, RouteParameter> RouteParameters; - - public RoutePatternTree( - VirtualCharSequence text, - RoutePatternCompilationUnit root, - ImmutableArray<EmbeddedDiagnostic> diagnostics, - ImmutableDictionary<string, RouteParameter> routeParameters) - : base(text, root, diagnostics) - { - RouteParameters = routeParameters; - } -} - -internal readonly struct RouteParameter -{ - public RouteParameter(string name, bool encodeSlashes, string defaultValue, bool isOptional, bool isCatchAll, ImmutableArray<string> policies) - { - Name = name; - EncodeSlashes = encodeSlashes; - DefaultValue = defaultValue; - IsOptional = isOptional; - IsCatchAll = isCatchAll; - Policies = policies; - } - - public readonly string Name; - public readonly bool EncodeSlashes; - public readonly string DefaultValue; - public readonly bool IsOptional; - public readonly bool IsCatchAll; - public readonly ImmutableArray<string> Policies; - - public override string ToString() - { - return Name; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternAnalyzer.cs deleted file mode 100644 index 15e74cf4e9c4..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternAnalyzer.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Immutable; -using System.Threading; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public class RoutePatternAnalyzer : DiagnosticAnalyzer -{ - public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(new[] - { - DiagnosticDescriptors.RoutePatternIssue - }); - - public void Analyze(SemanticModelAnalysisContext context) - { - var semanticModel = context.SemanticModel; - var syntaxTree = semanticModel.SyntaxTree; - var cancellationToken = context.CancellationToken; - - var root = syntaxTree.GetRoot(cancellationToken); - WellKnownTypes? wellKnownTypes = null; - Analyze(context, root, ref wellKnownTypes, cancellationToken); - } - - private void Analyze( - SemanticModelAnalysisContext context, - SyntaxNode node, - ref WellKnownTypes? wellKnownTypes, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - foreach (var child in node.ChildNodesAndTokens()) - { - if (child.IsNode) - { - Analyze(context, child.AsNode()!, ref wellKnownTypes, cancellationToken); - } - else - { - var token = child.AsToken(); - if (!RouteStringSyntaxDetector.IsRouteStringSyntaxToken(token, context.SemanticModel, cancellationToken)) - { - continue; - } - - if (wellKnownTypes == null && !WellKnownTypes.TryGetOrCreate(context.SemanticModel.Compilation, out wellKnownTypes)) - { - return; - } - - var usageContext = RoutePatternUsageDetector.BuildContext(token, context.SemanticModel, wellKnownTypes, cancellationToken); - - var virtualChars = CSharpVirtualCharService.Instance.TryConvertToVirtualChars(token); - var tree = RoutePatternParser.TryParse(virtualChars, supportTokenReplacement: usageContext.IsMvcAttribute); - if (tree == null) - { - continue; - } - - foreach (var diag in tree.Diagnostics) - { - context.ReportDiagnostic(Diagnostic.Create( - DiagnosticDescriptors.RoutePatternIssue, - Location.Create(context.SemanticModel.SyntaxTree, diag.Span), - DiagnosticDescriptors.RoutePatternIssue.DefaultSeverity, - additionalLocations: null, - properties: null, - diag.Message)); - } - } - } - } - - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - - context.RegisterSemanticModelAction(Analyze); - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternBraceMatcher.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternBraceMatcher.cs deleted file mode 100644 index 3f43bca6cb0f..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternBraceMatcher.cs +++ /dev/null @@ -1,120 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Reflection; -using System.Threading; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.EmbeddedLanguages; -using RoutePatternToken = Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax.EmbeddedSyntaxToken<Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern.RoutePatternKind>; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -[ExportAspNetCoreEmbeddedLanguageBraceMatcher(name: "Route", language: LanguageNames.CSharp)] -internal class RoutePatternBraceMatcher : IAspNetCoreEmbeddedLanguageBraceMatcher -{ - public AspNetCoreBraceMatchingResult? FindBraces(SemanticModel semanticModel, SyntaxToken token, int position, CancellationToken cancellationToken) - { - if (!WellKnownTypes.TryGetOrCreate(semanticModel.Compilation, out var wellKnownTypes)) - { - return null; - } - - var usageContext = RoutePatternUsageDetector.BuildContext(token, semanticModel, wellKnownTypes, cancellationToken); - - var virtualChars = CSharpVirtualCharService.Instance.TryConvertToVirtualChars(token); - var tree = RoutePatternParser.TryParse(virtualChars, supportTokenReplacement: usageContext.IsMvcAttribute); - if (tree == null) - { - return null; - } - - return GetMatchingBraces(tree, position); - } - - private static AspNetCoreBraceMatchingResult? GetMatchingBraces(RoutePatternTree tree, int position) - { - var virtualChar = tree.Text.Find(position); - if (virtualChar == null) - { - return null; - } - - var ch = virtualChar.Value; - return ch.Value switch - { - '{' or '}' => FindParameterBraces(tree, ch), - '(' or ')' => FindPolicyParens(tree, ch), - '[' or ']' => FindReplacementTokenBrackets(tree, ch), - _ => null, - }; - } - - private static AspNetCoreBraceMatchingResult? FindParameterBraces(RoutePatternTree tree, VirtualChar ch) - { - var node = FindParameterNode(tree.Root, ch); - return node == null ? null : CreateResult(node.OpenBraceToken, node.CloseBraceToken); - } - - private static AspNetCoreBraceMatchingResult? FindPolicyParens(RoutePatternTree tree, VirtualChar ch) - { - var node = FindPolicyFragmentEscapedNode(tree.Root, ch); - return node == null ? null : CreateResult(node.OpenParenToken, node.CloseParenToken); - } - - private static AspNetCoreBraceMatchingResult? FindReplacementTokenBrackets(RoutePatternTree tree, VirtualChar ch) - { - var node = FindReplacementNode(tree.Root, ch); - return node == null ? null : CreateResult(node.OpenBracketToken, node.CloseBracketToken); - } - - private static RoutePatternParameterNode? FindParameterNode(RoutePatternNode node, VirtualChar ch) - => FindNode<RoutePatternParameterNode>(node, ch, (parameter, c) => - parameter.OpenBraceToken.VirtualChars.Contains(c) || parameter.CloseBraceToken.VirtualChars.Contains(c)); - - private static RoutePatternPolicyFragmentEscapedNode? FindPolicyFragmentEscapedNode(RoutePatternNode node, VirtualChar ch) - => FindNode<RoutePatternPolicyFragmentEscapedNode>(node, ch, (fragment, c) => - fragment.OpenParenToken.VirtualChars.Contains(c) || fragment.CloseParenToken.VirtualChars.Contains(c)); - - private static RoutePatternReplacementNode? FindReplacementNode(RoutePatternNode node, VirtualChar ch) - => FindNode<RoutePatternReplacementNode>(node, ch, (fragment, c) => - fragment.OpenBracketToken.VirtualChars.Contains(c) || fragment.CloseBracketToken.VirtualChars.Contains(c)); - - private static TNode? FindNode<TNode>(RoutePatternNode node, VirtualChar ch, Func<TNode, VirtualChar, bool> predicate) - where TNode : RoutePatternNode - { - if (node is TNode nodeMatch && predicate(nodeMatch, ch)) - { - return nodeMatch; - } - - foreach (var child in node) - { - if (child.IsNode) - { - var result = FindNode(child.Node, ch, predicate); - if (result != null) - { - return result; - } - } - } - - return null; - } - - private static AspNetCoreBraceMatchingResult? CreateResult(RoutePatternToken open, RoutePatternToken close) - => open.IsMissing || close.IsMissing - ? null - : new AspNetCoreBraceMatchingResult(open.VirtualChars[0].Span, close.VirtualChars[0].Span); - - // IAspNetCoreEmbeddedLanguageBraceMatcher is internal and tests don't have access to it. Provide a way to get its assembly. - // Just for unit tests. Don't use in production code. - internal static class TestAccessor - { - public static Assembly ExternalAccessAssembly => typeof(IAspNetCoreEmbeddedLanguageBraceMatcher).Assembly; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternClassifier.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternClassifier.cs deleted file mode 100644 index d3619dda9349..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternClassifier.cs +++ /dev/null @@ -1,163 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Reflection; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.EmbeddedLanguages; -using Microsoft.CodeAnalysis.Classification; -using RoutePatternToken = Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax.EmbeddedSyntaxToken<Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern.RoutePatternKind>; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -[ExportAspNetCoreEmbeddedLanguageClassifier(name: "Route", language: LanguageNames.CSharp)] -internal class RoutePatternClassifier : IAspNetCoreEmbeddedLanguageClassifier -{ - public void RegisterClassifications(AspNetCoreEmbeddedLanguageClassificationContext context) - { - if (!WellKnownTypes.TryGetOrCreate(context.SemanticModel.Compilation, out var wellKnownTypes)) - { - return; - } - - var usageContext = RoutePatternUsageDetector.BuildContext(context.SyntaxToken, context.SemanticModel, wellKnownTypes, context.CancellationToken); - - var virtualChars = CSharpVirtualCharService.Instance.TryConvertToVirtualChars(context.SyntaxToken); - var tree = RoutePatternParser.TryParse(virtualChars, supportTokenReplacement: usageContext.IsMvcAttribute); - - if (tree != null) - { - var visitor = new Visitor(context); - AddClassifications(tree.Root, visitor); - } - } - - private static void AddClassifications(RoutePatternNode node, Visitor visitor) - { - node.Accept(visitor); - - foreach (var child in node) - { - if (child.IsNode) - { - AddClassifications(child.Node, visitor); - } - } - } - - private class Visitor : IRoutePatternNodeVisitor - { - public AspNetCoreEmbeddedLanguageClassificationContext _context; - - public Visitor(AspNetCoreEmbeddedLanguageClassificationContext context) - { - _context = context; - } - - public void Visit(RoutePatternCompilationUnit node) - { - // Nothing to highlight. - } - - public void Visit(RoutePatternSegmentNode node) - { - // Nothing to highlight. - } - - public void Visit(RoutePatternReplacementNode node) - { - AddClassification(node.OpenBracketToken, ClassificationTypeNames.RegexCharacterClass); - AddClassification(node.TextToken, ClassificationTypeNames.RegexCharacterClass); - AddClassification(node.CloseBracketToken, ClassificationTypeNames.RegexCharacterClass); - } - - public void Visit(RoutePatternParameterNode node) - { - AddClassification(node.OpenBraceToken, ClassificationTypeNames.RegexCharacterClass); - AddClassification(node.CloseBraceToken, ClassificationTypeNames.RegexCharacterClass); - } - - public void Visit(RoutePatternLiteralNode node) - { - // Nothing to highlight. - } - - public void Visit(RoutePatternSegmentSeperatorNode node) - { - // Nothing to highlight. - } - - public void Visit(RoutePatternOptionalSeperatorNode node) - { - // Nothing to highlight. - } - - public void Visit(RoutePatternCatchAllParameterPartNode node) - { - AddClassification(node.AsteriskToken, ClassificationTypeNames.RegexAnchor); - } - - public void Visit(RoutePatternNameParameterPartNode node) - { - AddClassification(node.ParameterNameToken, ClassificationTypeNames.ParameterName); - } - - public void Visit(RoutePatternPolicyParameterPartNode node) - { - AddClassification(node.ColonToken, ClassificationTypeNames.RegexCharacterClass); - } - - public void Visit(RoutePatternPolicyFragmentEscapedNode node) - { - AddClassification(node.OpenParenToken, ClassificationTypeNames.RegexCharacterClass); - AddClassification(node.CloseParenToken, ClassificationTypeNames.RegexCharacterClass); - } - - public void Visit(RoutePatternPolicyFragment node) - { - AddClassification(node.ArgumentToken, ClassificationTypeNames.RegexGrouping); - } - - public void Visit(RoutePatternOptionalParameterPartNode node) - { - AddClassification(node.QuestionMarkToken, ClassificationTypeNames.RegexAnchor); - } - - public void Visit(RoutePatternDefaultValueParameterPartNode node) - { - AddClassification(node.EqualsToken, ClassificationTypeNames.RegexCharacterClass); - } - - private void AddClassification(RoutePatternToken token, string typeName) - { - if (!token.IsMissing) - { - _context.AddClassification(typeName, token.GetSpan()); - } - } - - private void ClassifyWholeNode(RoutePatternNode node, string typeName) - { - foreach (var child in node) - { - if (child.IsNode) - { - ClassifyWholeNode(child.Node, typeName); - } - else - { - AddClassification(child.Token, typeName); - } - } - } - } - - // IAspNetCoreEmbeddedLanguageClassifier is internal and tests don't have access to it. Provide a way to get its assembly. - // Just for unit tests. Don't use in production code. - internal static class TestAccessor - { - public static Assembly ExternalAccessAssembly => typeof(IAspNetCoreEmbeddedLanguageClassifier).Assembly; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternCompletionProvider.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternCompletionProvider.cs deleted file mode 100644 index 9f94ab8f4b6f..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternCompletionProvider.cs +++ /dev/null @@ -1,385 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Composition; -using System.Diagnostics; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Completion; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.Tags; -using Microsoft.CodeAnalysis.Text; -using RoutePatternToken = Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax.EmbeddedSyntaxToken<Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern.RoutePatternKind>; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -[ExportCompletionProvider(nameof(RoutePatternCompletionProvider), LanguageNames.CSharp)] -[Shared] -public class RoutePatternCompletionProvider : CompletionProvider -{ - private const string StartKey = nameof(StartKey); - private const string LengthKey = nameof(LengthKey); - private const string NewTextKey = nameof(NewTextKey); - private const string NewPositionKey = nameof(NewPositionKey); - private const string DescriptionKey = nameof(DescriptionKey); - - // Always soft-select these completion items. Also, never filter down. - private static readonly CompletionItemRules s_rules = CompletionItemRules.Create( - selectionBehavior: CompletionItemSelectionBehavior.SoftSelection, - filterCharacterRules: ImmutableArray.Create(CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, Array.Empty<char>()))); - - public ImmutableHashSet<char> TriggerCharacters { get; } = ImmutableHashSet.Create( - ':', // policy name - '{'); // parameter name - - public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options) - { - if (trigger.Kind is CompletionTriggerKind.Invoke or - CompletionTriggerKind.InvokeAndCommitIfUnique) - { - return true; - } - - if (trigger.Kind == CompletionTriggerKind.Insertion) - { - return TriggerCharacters.Contains(trigger.Character); - } - - return false; - } - - public override Task<CompletionDescription> GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken) - { - if (!item.Properties.TryGetValue(DescriptionKey, out var description)) - { - return Task.FromResult<CompletionDescription>(null); - } - - return Task.FromResult(CompletionDescription.Create( - ImmutableArray.Create(new TaggedText(TextTags.Text, description)))); - } - - public override Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken) - { - // These values have always been added by us. - var startString = item.Properties[StartKey]; - var lengthString = item.Properties[LengthKey]; - var newText = item.Properties[NewTextKey]; - - // This value is optionally added in some cases and may not always be there. - item.Properties.TryGetValue(NewPositionKey, out var newPositionString); - - return Task.FromResult(CompletionChange.Create( - new TextChange(new TextSpan(int.Parse(startString, CultureInfo.InvariantCulture), int.Parse(lengthString, CultureInfo.InvariantCulture)), newText), - newPositionString == null ? null : int.Parse(newPositionString, CultureInfo.InvariantCulture))); - } - - public override async Task ProvideCompletionsAsync(CompletionContext context) - { - if (context.Trigger.Kind is not CompletionTriggerKind.Invoke and - not CompletionTriggerKind.InvokeAndCommitIfUnique and - not CompletionTriggerKind.Insertion) - { - return; - } - - var position = context.Position; - var (success, stringToken, semanticModel) = await RouteStringSyntaxDetectorDocument.TryGetStringSyntaxTokenAtPositionAsync( - context.Document, position, context.CancellationToken).ConfigureAwait(false); - - if (!success || - position <= stringToken.SpanStart || - position >= stringToken.Span.End) - { - return; - } - - if (!WellKnownTypes.TryGetOrCreate(semanticModel.Compilation, out var wellKnownTypes)) - { - return; - } - - var (methodSymbol, isMinimal, isMvcAttribute) = RoutePatternUsageDetector.BuildContext(stringToken, semanticModel, wellKnownTypes, context.CancellationToken); - - var virtualChars = CSharpVirtualCharService.Instance.TryConvertToVirtualChars(stringToken); - var tree = RoutePatternParser.TryParse(virtualChars, supportTokenReplacement: isMvcAttribute); - if (tree == null) - { - return; - } - - var routePatternCompletionContext = new EmbeddedCompletionContext( - context, tree, stringToken, wellKnownTypes, methodSymbol, isMinimal, isMvcAttribute); - ProvideCompletions(routePatternCompletionContext); - - if (routePatternCompletionContext.Items.Count == 0) - { - return; - } - - foreach (var embeddedItem in routePatternCompletionContext.Items) - { - var change = embeddedItem.Change; - var textChange = change.TextChange; - - var properties = ImmutableDictionary.CreateBuilder<string, string>(); - properties.Add(StartKey, textChange.Span.Start.ToString(CultureInfo.InvariantCulture)); - properties.Add(LengthKey, textChange.Span.Length.ToString(CultureInfo.InvariantCulture)); - properties.Add(NewTextKey, textChange.NewText); - properties.Add(DescriptionKey, embeddedItem.FullDescription); - - if (change.NewPosition != null) - { - properties.Add(NewPositionKey, change.NewPosition.ToString()); - } - - // Keep everything sorted in the order we just produced the items in. - var sortText = routePatternCompletionContext.Items.Count.ToString("0000", CultureInfo.InvariantCulture); - context.AddItem(CompletionItem.Create( - displayText: embeddedItem.DisplayText, - inlineDescription: "", - sortText: sortText, - properties: properties.ToImmutable(), - rules: s_rules, - tags: ImmutableArray.Create(embeddedItem.Glyph))); - } - - context.IsExclusive = true; - } - - private void ProvideCompletions(EmbeddedCompletionContext context) - { - // First, act as if the user just inserted the previous character. This will cause us - // to complete down to the set of relevant items based on that character. If we get - // anything, we're done and can just show the user those items. If we have no items to - // add *and* the user was explicitly invoking completion, then just add the entire set - // of suggestions to help the user out. - ProvideCompletionsBasedOffOfPrecedingCharacter(context); - - if (context.Items.Count > 0) - { - // We added items. Nothing else to do here. - return; - } - - if (context.Trigger.Kind == CompletionTriggerKind.Insertion) - { - // The user was typing a character, and we had nothing to add for them. Just bail - // out immediately as we cannot help in this circumstance. - return; - } - - // We added no items, but the user explicitly asked for completion. Add all the - // items we can to help them out. - _ = context.Tree.Text.Find(context.Position); - - // TODO: CompletionProvider's loaded from projects currently don't support overriding - // description or customizing how text is added. This is needed to properly support - // completions when the user explicitly asked for completion. - // Will be fixed in VS 17.4 - https://github.com/dotnet/roslyn/pull/61976 - } - - private void ProvideCompletionsBasedOffOfPrecedingCharacter(EmbeddedCompletionContext context) - { - var previousVirtualCharOpt = context.Tree.Text.Find(context.Position - 1); - if (previousVirtualCharOpt == null) - { - // We didn't have a previous character. Can't determine the set of - // regex items to show. - return; - } - - var previousVirtualChar = previousVirtualCharOpt.Value; - var result = FindToken(context.Tree.Root, previousVirtualChar); - if (result == null) - { - return; - } - - var (_, token) = result.Value; - switch (token.Kind) - { - case RoutePatternKind.ColonToken: - ProvidePolicyNameCompletions(context); - return; - case RoutePatternKind.OpenBraceToken: - ProvideParameterCompletions(context); - return; - } - } - - private static void ProvideParameterCompletions(EmbeddedCompletionContext context) - { - if (context.MethodSymbol != null) - { - var resolvedParameterSymbols = RoutePatternParametersDetector.ResolvedParameters(context.MethodSymbol, context.WellKnownTypes); - foreach (var parameterSymbol in resolvedParameterSymbols) - { - context.AddIfMissing(parameterSymbol.Name, suffix: null, description: null, WellKnownTags.Parameter, parentOpt: null); - } - } - } - - private static void ProvidePolicyNameCompletions(EmbeddedCompletionContext context) - { - context.AddIfMissing("int", suffix: null, "Matches any 32-bit integer.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("bool", suffix: null, "Matches true or false. Case-insensitive.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("datetime", suffix: null, "Matches a valid DateTime value in the invariant culture.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("decimal", suffix: null, "Matches a valid decimal value in the invariant culture.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("double", suffix: null, "Matches a valid double value in the invariant culture.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("float", suffix: null, "Matches a valid float value in the invariant culture.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("guid", suffix: null, "Matches a valid Guid value.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("long", suffix: null, "Matches any 64-bit integer.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("minlength", suffix: null, "Matches a string with a length greater than, or equal to, the constraint argument.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("maxlength", suffix: null, "Matches a string with a length less than, or equal to, the constraint argument.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("length", suffix: null, @"The string length constraint supports one or two constraint arguments. - -If there is one argument the string length must equal the argument. For example, length(10) matches a string with exactly 10 characters. - -If there are two arguments then the string length must be greater than, or equal to, the first argument and less than, or equal to, the second argument. For example, length(8,16) matches a string at least 8 and no more than 16 characters long.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("min", suffix: null, "Matches an integer with a value greater than, or equal to, the constraint argument.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("max", suffix: null, "Matches an integer with a value less than, or equal to, the constraint argument.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("range", suffix: null, "Matches an integer with a value greater than, or equal to, the first constraint argument and less than, or equal to, the second constraint argument.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("alpha", suffix: null, "Matches a string that contains only lowercase or uppercase letters A through Z in the English alphabet.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("regex", suffix: null, "Matches a string to the regular expression constraint argument.", WellKnownTags.Keyword, parentOpt: null); - context.AddIfMissing("required", suffix: null, "Used to enforce that a non-parameter value is present during URL generation.", WellKnownTags.Keyword, parentOpt: null); - } - - private (RoutePatternNode parent, RoutePatternToken Token)? FindToken(RoutePatternNode parent, VirtualChar ch) - { - foreach (var child in parent) - { - if (child.IsNode) - { - var result = FindToken(child.Node, ch); - if (result != null) - { - return result; - } - } - else - { - if (child.Token.VirtualChars.Contains(ch)) - { - return (parent, child.Token); - } - } - } - - return null; - } - - private readonly struct RoutePatternItem - { - public readonly string DisplayText; - public readonly string InlineDescription; - public readonly string FullDescription; - public readonly string Glyph; - public readonly CompletionChange Change; - - public RoutePatternItem( - string displayText, string inlineDescription, string fullDescription, string glyph, CompletionChange change) - { - DisplayText = displayText; - InlineDescription = inlineDescription; - FullDescription = fullDescription; - Glyph = glyph; - Change = change; - } - } - - private readonly struct EmbeddedCompletionContext - { - private readonly CompletionContext _context; - private readonly HashSet<string> _names = new(); - - public readonly RoutePatternTree Tree; - public readonly SyntaxToken StringToken; - public readonly WellKnownTypes WellKnownTypes; - public readonly IMethodSymbol? MethodSymbol; - public readonly bool IsMinimal; - public readonly bool IsMvcAttribute; - public readonly CancellationToken CancellationToken; - public readonly int Position; - public readonly CompletionTrigger Trigger; - public readonly List<RoutePatternItem> Items = new(); - - public EmbeddedCompletionContext( - CompletionContext context, - RoutePatternTree tree, - SyntaxToken stringToken, - WellKnownTypes wellKnownTypes, - IMethodSymbol? methodSymbol, - bool isMinimal, - bool isMvcAttribute) - { - _context = context; - Tree = tree; - StringToken = stringToken; - WellKnownTypes = wellKnownTypes; - MethodSymbol = methodSymbol; - IsMinimal = isMinimal; - IsMvcAttribute = isMvcAttribute; - Position = _context.Position; - Trigger = _context.Trigger; - CancellationToken = _context.CancellationToken; - } - - public void AddIfMissing( - string displayText, string suffix, string description, string glyph, - RoutePatternNode parentOpt, int? positionOffset = null, string insertionText = null) - { - var replacementStart = parentOpt != null - ? parentOpt.GetSpan().Start - : Position; - - var replacementSpan = TextSpan.FromBounds(replacementStart, Position); - var newPosition = replacementStart + positionOffset; - - insertionText ??= displayText; - var escapedInsertionText = EscapeText(insertionText, StringToken); - - if (escapedInsertionText != insertionText) - { - newPosition += escapedInsertionText.Length - insertionText.Length; - } - - AddIfMissing(new RoutePatternItem( - displayText, suffix, description, glyph, - CompletionChange.Create( - new TextChange(replacementSpan, escapedInsertionText), - newPosition))); - } - - public void AddIfMissing(RoutePatternItem item) - { - if (_names.Add(item.DisplayText)) - { - Items.Add(item); - } - } - - public static string EscapeText(string text, SyntaxToken token) - { - // This function is called when Completion needs to escape something its going to - // insert into the user's string token. This means that we only have to escape - // things that completion could insert. In this case, the only regex character - // that is relevant is the \ character, and it's only relevant if we insert into - // a normal string and not a verbatim string. There are no other regex characters - // that completion will produce that need any escaping. - Debug.Assert(token.IsKind(SyntaxKind.StringLiteralToken)); - return token.IsVerbatimStringLiteral() - ? text - : text.Replace(@"\", @"\\"); - } - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternHighlighter.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternHighlighter.cs deleted file mode 100644 index c683b3d23ad2..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/RoutePatternHighlighter.cs +++ /dev/null @@ -1,142 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.EmbeddedLanguages; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -[ExportAspNetCoreEmbeddedLanguageDocumentHighlighter(name: "Route", language: LanguageNames.CSharp)] -internal class RoutePatternHighlighter : IAspNetCoreEmbeddedLanguageDocumentHighlighter -{ - public ImmutableArray<AspNetCoreDocumentHighlights> GetDocumentHighlights( - SemanticModel semanticModel, SyntaxToken token, int position, CancellationToken cancellationToken) - { - if (!WellKnownTypes.TryGetOrCreate(semanticModel.Compilation, out var wellKnownTypes)) - { - return ImmutableArray<AspNetCoreDocumentHighlights>.Empty; - } - - var usageContext = RoutePatternUsageDetector.BuildContext(token, semanticModel, wellKnownTypes, cancellationToken); - - var virtualChars = CSharpVirtualCharService.Instance.TryConvertToVirtualChars(token); - var tree = RoutePatternParser.TryParse(virtualChars, supportTokenReplacement: usageContext.IsMvcAttribute); - if (tree == null) - { - return ImmutableArray<AspNetCoreDocumentHighlights>.Empty; - } - - return GetHighlights(tree, semanticModel, wellKnownTypes, position, usageContext.MethodSymbol, cancellationToken); - } - - private static ImmutableArray<AspNetCoreDocumentHighlights> GetHighlights( - RoutePatternTree tree, SemanticModel semanticModel, WellKnownTypes wellKnownTypes, int position, IMethodSymbol methodSymbol, CancellationToken cancellationToken) - { - var virtualChar = tree.Text.Find(position); - if (virtualChar == null) - { - return ImmutableArray<AspNetCoreDocumentHighlights>.Empty; - } - - var node = FindParameterNode(tree.Root, virtualChar.Value); - if (node == null) - { - return ImmutableArray<AspNetCoreDocumentHighlights>.Empty; - } - - var highlightSpans = ImmutableArray.CreateBuilder<AspNetCoreHighlightSpan>(); - - // Highlight the parameter in the route string, e.g. "{id}" highlights "id". - highlightSpans.Add(new AspNetCoreHighlightSpan(node.GetSpan(), AspNetCoreHighlightSpanKind.Reference)); - - if (methodSymbol != null) - { - // Resolve possible parameter symbols. Includes properties from AsParametersAttribute. - var parameters = RoutePatternParametersDetector.ResolvedParameters(methodSymbol, wellKnownTypes); - - // Match route parameter to method parameter. Parameters in a route aren't case sensitive. - // First attempt an exact match, then a case insensitive match. - var parameterName = node.ParameterNameToken.Value.ToString(); - var matchingParameter = parameters.FirstOrDefault(s => s.Name == parameterName) - ?? parameters.FirstOrDefault(s => string.Equals(s.Name, parameterName, StringComparison.OrdinalIgnoreCase)); - - if (matchingParameter != null) - { - HighlightSymbol(semanticModel, methodSymbol, highlightSpans, matchingParameter, cancellationToken); - } - } - - return ImmutableArray.Create(new AspNetCoreDocumentHighlights(highlightSpans.ToImmutable())); - } - - private static void HighlightSymbol(SemanticModel semanticModel, IMethodSymbol methodSymbol, IList<AspNetCoreHighlightSpan> highlightSpans, ISymbol? matchingParameter, CancellationToken cancellationToken) - { - // Highlight parameter in method signature. - // e.g. "{id}" in route highlights id in "void Foo(string id) {}" - foreach (var item in matchingParameter.DeclaringSyntaxReferences) - { - var syntaxNode = item.GetSyntax(cancellationToken); - if (syntaxNode is ParameterSyntax parameterSyntax) - { - highlightSpans.Add(new AspNetCoreHighlightSpan(parameterSyntax.Identifier.Span, AspNetCoreHighlightSpanKind.Definition)); - } - } - - // Highlight parameter references inside method. - // e.g. "{id}" in route highlights id in "_repository.GetBy(id)" - foreach (var item in methodSymbol.DeclaringSyntaxReferences) - { - var methodSyntax = item.GetSyntax(cancellationToken); - - // Have to call GetSymbolInfo because it's easy to have identifiers with the same name - // that reference a different API. For example, a type with the same name as parameter. - // GetSymbolInfo can be slow. To reduce calls to it we only get IdentifierNameSyntax - // nodes, filter them by name first, then check GetSymbolInfo. - var parameterReferences = methodSyntax - .DescendantNodes() - .OfType<IdentifierNameSyntax>() - .Where(i => i.Identifier.Text == matchingParameter.Name) - .Where(i => semanticModel.GetSymbolInfo(i) is var symbolInfo && SymbolEqualityComparer.Default.Equals(symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.FirstOrDefault(), matchingParameter)); - - foreach (var reference in parameterReferences) - { - highlightSpans.Add(new AspNetCoreHighlightSpan(reference.Identifier.Span, AspNetCoreHighlightSpanKind.Reference)); - } - } - } - - private static RoutePatternNameParameterPartNode? FindParameterNode(RoutePatternNode node, VirtualChar ch) - => FindNode<RoutePatternNameParameterPartNode>(node, ch, (parameter, c) => parameter.ParameterNameToken.VirtualChars.Contains(c)); - - private static TNode? FindNode<TNode>(RoutePatternNode node, VirtualChar ch, Func<TNode, VirtualChar, bool> predicate) - where TNode : RoutePatternNode - { - if (node is TNode nodeMatch && predicate(nodeMatch, ch)) - { - return nodeMatch; - } - - foreach (var child in node) - { - if (child.IsNode) - { - var result = FindNode(child.Node, ch, predicate); - if (result != null) - { - return result; - } - } - } - - return null; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/WellKnownTypes.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/WellKnownTypes.cs deleted file mode 100644 index 9c15b9e2fca8..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/WellKnownTypes.cs +++ /dev/null @@ -1,192 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -internal sealed class WellKnownTypes -{ - /// <summary> - /// Cache so that we can reuse the same <see cref="WellKnownTypes"/> when analyzing a particular compilation model. - /// </summary> - private static readonly ConditionalWeakTable<Compilation, WellKnownTypes?> _compilationToTypes = new(); - - public static bool TryGetOrCreate(Compilation compilation, [NotNullWhen(true)] out WellKnownTypes? wellKnownTypes) - { - wellKnownTypes = _compilationToTypes.GetValue(compilation, static c => - { - TryCreate(c, out var wellKnownTypes); - return wellKnownTypes; - }); - - // The cache could return null if well known types couldn't be resolved. - return wellKnownTypes != null; - } - - private static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out WellKnownTypes? wellKnownTypes) - { - wellKnownTypes = default; - - const string IFromBodyMetadata = "Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata"; - if (compilation.GetTypeByMetadataName(IFromBodyMetadata) is not { } iFromBodyMetadata) - { - return false; - } - - const string IFromFormMetadata = "Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata"; - if (compilation.GetTypeByMetadataName(IFromFormMetadata) is not { } iFromFormMetadata) - { - return false; - } - - const string IFromHeaderMetadata = "Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata"; - if (compilation.GetTypeByMetadataName(IFromHeaderMetadata) is not { } iFromHeaderMetadata) - { - return false; - } - - const string IFromQueryMetadata = "Microsoft.AspNetCore.Http.Metadata.IFromQueryMetadata"; - if (compilation.GetTypeByMetadataName(IFromQueryMetadata) is not { } iFromQueryMetadata) - { - return false; - } - - const string IFromServiceMetadata = "Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata"; - if (compilation.GetTypeByMetadataName(IFromServiceMetadata) is not { } iFromServiceMetadata) - { - return false; - } - - const string IEndpointRouteBuilder = "Microsoft.AspNetCore.Routing.IEndpointRouteBuilder"; - if (compilation.GetTypeByMetadataName(IEndpointRouteBuilder) is not { } iEndpointRouteBuilder) - { - return false; - } - - const string ControllerAttribute = "Microsoft.AspNetCore.Mvc.ControllerAttribute"; - if (compilation.GetTypeByMetadataName(ControllerAttribute) is not { } controllerAttribute) - { - return false; - } - - const string NonControllerAttribute = "Microsoft.AspNetCore.Mvc.NonControllerAttribute"; - if (compilation.GetTypeByMetadataName(NonControllerAttribute) is not { } nonControllerAttribute) - { - return false; - } - - const string NonActionAttribute = "Microsoft.AspNetCore.Mvc.NonActionAttribute"; - if (compilation.GetTypeByMetadataName(NonActionAttribute) is not { } nonActionAttribute) - { - return false; - } - - const string AsParametersAttribute = "Microsoft.AspNetCore.Http.AsParametersAttribute"; - if (compilation.GetTypeByMetadataName(AsParametersAttribute) is not { } asParametersAttribute) - { - return false; - } - - const string CancellationToken = "System.Threading.CancellationToken"; - if (compilation.GetTypeByMetadataName(CancellationToken) is not { } cancellationToken) - { - return false; - } - - const string HttpContext = "Microsoft.AspNetCore.Http.HttpContext"; - if (compilation.GetTypeByMetadataName(HttpContext) is not { } httpContext) - { - return false; - } - - const string HttpRequest = "Microsoft.AspNetCore.Http.HttpRequest"; - if (compilation.GetTypeByMetadataName(HttpRequest) is not { } httpRequest) - { - return false; - } - - const string HttpResponse = "Microsoft.AspNetCore.Http.HttpResponse"; - if (compilation.GetTypeByMetadataName(HttpResponse) is not { } httpResponse) - { - return false; - } - - const string ClaimsPrincipal = "System.Security.Claims.ClaimsPrincipal"; - if (compilation.GetTypeByMetadataName(ClaimsPrincipal) is not { } claimsPrincipal) - { - return false; - } - - const string IFormFileCollection = "Microsoft.AspNetCore.Http.IFormFileCollection"; - if (compilation.GetTypeByMetadataName(IFormFileCollection) is not { } iFormFileCollection) - { - return false; - } - - const string IFormFile = "Microsoft.AspNetCore.Http.IFormFile"; - if (compilation.GetTypeByMetadataName(IFormFile) is not { } iFormFile) - { - return false; - } - - const string Stream = "System.IO.Stream"; - if (compilation.GetTypeByMetadataName(Stream) is not { } stream) - { - return false; - } - - const string PipeReader = "System.IO.Pipelines.PipeReader"; - if (compilation.GetTypeByMetadataName(PipeReader) is not { } pipeReader) - { - return false; - } - - wellKnownTypes = new() - { - IFromBodyMetadata = iFromBodyMetadata, - IFromFormMetadata = iFromFormMetadata, - IFromHeaderMetadata = iFromHeaderMetadata, - IFromQueryMetadata = iFromQueryMetadata, - IFromServiceMetadata = iFromServiceMetadata, - IEndpointRouteBuilder = iEndpointRouteBuilder, - ControllerAttribute = controllerAttribute, - NonControllerAttribute = nonControllerAttribute, - NonActionAttribute = nonActionAttribute, - AsParametersAttribute = asParametersAttribute, - CancellationToken = cancellationToken, - HttpContext = httpContext, - HttpRequest = httpRequest, - HttpResponse = httpResponse, - ClaimsPrincipal = claimsPrincipal, - IFormFileCollection = iFormFileCollection, - IFormFile = iFormFile, - Stream = stream, - PipeReader = pipeReader, - }; - - return true; - } - - public INamedTypeSymbol IFromBodyMetadata { get; private init; } - public INamedTypeSymbol IFromFormMetadata { get; private init; } - public INamedTypeSymbol IFromHeaderMetadata { get; private init; } - public INamedTypeSymbol IFromQueryMetadata { get; private init; } - public INamedTypeSymbol IFromServiceMetadata { get; private init; } - public INamedTypeSymbol IEndpointRouteBuilder { get; private init; } - public INamedTypeSymbol ControllerAttribute { get; private init; } - public INamedTypeSymbol NonControllerAttribute { get; private init; } - public INamedTypeSymbol NonActionAttribute { get; private init; } - public INamedTypeSymbol AsParametersAttribute { get; private init; } - public INamedTypeSymbol CancellationToken { get; private init; } - public INamedTypeSymbol HttpContext { get; private init; } - public INamedTypeSymbol HttpRequest { get; private init; } - public INamedTypeSymbol HttpResponse { get; private init; } - public INamedTypeSymbol ClaimsPrincipal { get; private init; } - public INamedTypeSymbol IFormFileCollection { get; private init; } - public INamedTypeSymbol Stream { get; private init; } - public INamedTypeSymbol PipeReader { get; private init; } - public INamedTypeSymbol IFormFile { get; private set; } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/DetectMismatchedParameterOptionalityFixer.cs b/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/DetectMismatchedParameterOptionalityFixer.cs index c64ae7681a9b..f4aeb9eadd95 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/DetectMismatchedParameterOptionalityFixer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/DetectMismatchedParameterOptionalityFixer.cs @@ -25,7 +25,7 @@ public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) { context.RegisterCodeFix( CodeAction.Create("Fix mismatched route parameter and argument optionality", - cancellationToken => FixMismatchedParameterOptionalityAsync(diagnostic, context.Document, cancellationToken), + cancellationToken => FixMismatchedParameterOptionality(diagnostic, context.Document, cancellationToken), equivalenceKey: DiagnosticDescriptors.DetectMismatchedParameterOptionality.Id), diagnostic); } @@ -33,9 +33,9 @@ public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) return Task.CompletedTask; } - private static async Task<Document> FixMismatchedParameterOptionalityAsync(Diagnostic diagnostic, Document document, CancellationToken cancellationToken) + private static async Task<Document> FixMismatchedParameterOptionality(Diagnostic diagnostic, Document document, CancellationToken cancellationToken) { - var editor = await DocumentEditor.CreateAsync(document, cancellationToken); + DocumentEditor editor = await DocumentEditor.CreateAsync(document, cancellationToken); var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (root == null) diff --git a/src/Framework/AspNetCoreAnalyzers/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj b/src/Framework/AspNetCoreAnalyzers/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj index 1139242a4d19..cc172fb87ad7 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj +++ b/src/Framework/AspNetCoreAnalyzers/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj @@ -14,15 +14,7 @@ <!-- Also bring in Microsoft.AspNetCore.App.Analyzers. --> <ProjectReference Include="..\src\CodeFixes\Microsoft.AspNetCore.App.CodeFixes.csproj" /> - <!-- Load analyzers into test project. Allows for testing inside Visual Studio. --> - <!-- Note that VS must be restarted after making changes to analyzer project. Required for VS to pick up the changes. --> - <!-- - <ProjectReference Include="..\src\Analyzers\Microsoft.AspNetCore.App.Analyzers.csproj" - OutputItemType="Analyzer" ReferenceOutputAssembly="false" SetTargetFramework="TargetFramework=netstandard2.0" /> - --> - <ProjectReference Include="$(RepoRoot)src\Analyzers\Microsoft.AspNetCore.Analyzer.Testing\src\Microsoft.AspNetCore.Analyzer.Testing.csproj" /> - <Reference Include="Microsoft.AspNetCore" /> <Reference Include="Microsoft.AspNetCore.Mvc" /> <Reference Include="Microsoft.AspNetCore.Http.Results" /> diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Experiment/Controllers/TodoController.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Experiment/Controllers/TodoController.cs deleted file mode 100644 index 99fbdd258f48..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Experiment/Controllers/TodoController.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#region Namespaces -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Experiment; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -#endregion - -[Route("api/[controller]")] -public class TodoController -{ - private readonly DbContext _dbContext; - - public TodoController(DbContext dbContext) => _dbContext = dbContext; - - [HttpGet("{id}")] - public Todo Get(int id) => _dbContext.Todos.Find(id); - - [HttpPut] - public void Create([FromBody] Todo todo) => _dbContext.Todos.Add(todo); - - [HttpGet("[action]/{page?}")] - public IEnumerable<Todo> Search(int? page, [FromQuery] string text) - { - return _dbContext.Todos - .Where(t => t.Text.Contains(text)) - .Skip((page ?? 0) * 10) - .Take(10); - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Experiment/DbContext.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Experiment/DbContext.cs deleted file mode 100644 index c34bdf840def..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Experiment/DbContext.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Experiment; - -public class DbContext -{ - public IList<Todo> Todos { get; } = new List<Todo>(); - public IList<Book> Books { get; } = new List<Book>(); -} - -public static class ListExtensions -{ - public static T Find<T>(this IList<T> list, int id) - { - return default!; - } -} - -public class Todo -{ - public string Text { get; set; } -} - -public class Book -{ - public string Text { get; set; } -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Experiment/Program.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Experiment/Program.cs deleted file mode 100644 index 2eb9631c6547..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Experiment/Program.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#region Namespaces -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Experiment; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing.Patterns; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -#endregion - -var builder = WebApplication.CreateBuilder(args); -builder.Services.AddControllers(); - -var app = builder.Build(); -var db = new DbContext(); - -app.MapGet("/users/{userId}/books/{bookId?}", IResult (int userId, int? bookId) => -{ - return bookId != null - ? Results.Ok(db.Books.Find(bookId.Value)) - : Results.Ok(db.Books); -}); - -app.MapGet("/posts/{**rest}", (string rest) => $"Routing to {rest}"); -app.MapGet("/todos/{id}", (int id) => db.Todos.Find(id)); -app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text))); -app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}"); - -app.MapControllerRoute("Default", "{controller=Home}/{action=Index}/{id?}"); - -app.Run(); diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/EmbeddedLanguagesTestConstants.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/EmbeddedLanguagesTestConstants.cs deleted file mode 100644 index 97fcf0e16116..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/EmbeddedLanguagesTestConstants.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; - -internal static class EmbeddedLanguagesTestConstants -{ - public static readonly string StringSyntaxAttributeCodeCSharp = @" -namespace System.Diagnostics.CodeAnalysis -{ - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] - public sealed class StringSyntaxAttribute : Attribute - { - public StringSyntaxAttribute(string syntax) - { - Syntax = syntax; - Arguments = Array.Empty<object?>(); - } - - public StringSyntaxAttribute(string syntax, params object?[] arguments) - { - Syntax = syntax; - Arguments = arguments; - } - - public string Syntax { get; } - public object?[] Arguments { get; } - - public const string DateTimeFormat = nameof(DateTimeFormat); - public const string Json = nameof(Json); - public const string Regex = nameof(Regex); - } -} -"; -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/ExportProviderExtensions.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/ExportProviderExtensions.cs deleted file mode 100644 index e186da03294b..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/ExportProviderExtensions.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Composition; -using System.Composition.Hosting.Core; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using Microsoft.CodeAnalysis; -using Microsoft.VisualStudio.Composition; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; - -internal static class ExportProviderExtensions -{ - public static CompositionContext AsCompositionContext(this ExportProvider exportProvider) - { - return new CompositionContextShim(exportProvider); - } - - private class CompositionContextShim : CompositionContext - { - private readonly ExportProvider _exportProvider; - - public CompositionContextShim(ExportProvider exportProvider) - { - _exportProvider = exportProvider; - } - - public override bool TryGetExport(CompositionContract contract, out object export) - { - var importMany = contract.MetadataConstraints.Contains(new KeyValuePair<string, object>("IsImportMany", true)); - var (contractType, metadataType) = GetContractType(contract.ContractType, importMany); - - if (metadataType != null) - { - var methodInfo = (from method in _exportProvider.GetType().GetTypeInfo().GetMethods() - where method.Name == nameof(ExportProvider.GetExports) - where method.IsGenericMethod && method.GetGenericArguments().Length == 2 - where method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == typeof(string) - select method).Single(); - var parameterizedMethod = methodInfo.MakeGenericMethod(contractType, metadataType); - export = parameterizedMethod.Invoke(_exportProvider, new[] { contract.ContractName }); - } - else - { - var methodInfo = (from method in _exportProvider.GetType().GetTypeInfo().GetMethods() - where method.Name == nameof(ExportProvider.GetExports) - where method.IsGenericMethod && method.GetGenericArguments().Length == 1 - where method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == typeof(string) - select method).Single(); - var parameterizedMethod = methodInfo.MakeGenericMethod(contractType); - export = parameterizedMethod.Invoke(_exportProvider, new[] { contract.ContractName }); - } - - return true; - } - - private (Type exportType, Type metadataType) GetContractType(Type contractType, bool importMany) - { - if (importMany && contractType.IsConstructedGenericType) - { - if (contractType.GetGenericTypeDefinition() == typeof(IList<>) - || contractType.GetGenericTypeDefinition() == typeof(ICollection<>) - || contractType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - { - contractType = contractType.GenericTypeArguments[0]; - } - } - - if (contractType.IsConstructedGenericType) - { - if (contractType.GetGenericTypeDefinition() == typeof(Lazy<>)) - { - return (contractType.GenericTypeArguments[0], null); - } - else if (contractType.GetGenericTypeDefinition() == typeof(Lazy<,>)) - { - return (contractType.GenericTypeArguments[0], contractType.GenericTypeArguments[1]); - } - else - { - throw new NotSupportedException(); - } - } - - throw new NotSupportedException(); - } - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/FormattedClassification.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/FormattedClassification.cs deleted file mode 100644 index 8c8eba33985c..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/FormattedClassification.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -using System; -using System.Linq; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; - -public class FormattedClassification -{ - public string ClassificationName { get; } - public string Text { get; } - - private FormattedClassification() { } - - public FormattedClassification(string text, string classificationName) - { - Text = text ?? throw new ArgumentNullException(nameof(text)); - ClassificationName = classificationName ?? throw new ArgumentNullException(nameof(classificationName)); - } - - public override bool Equals(object obj) - { - if (obj is FormattedClassification other) - { - return ClassificationName == other.ClassificationName - && Text == other.Text; - } - - return false; - } - - public override int GetHashCode() - => ClassificationName.GetHashCode() ^ Text.GetHashCode(); - - public override string ToString() - { - if (ClassificationName.StartsWith("regex", StringComparison.Ordinal)) - { - var remainder = ClassificationName.Substring("regex - ".Length); - var parts = remainder.Split(' '); - var type = string.Join("", parts.Select(Capitalize)); - return "Regex." + $"{type}(\"{Text}\")"; - } - - if (ClassificationName.StartsWith("json", StringComparison.Ordinal)) - { - var remainder = ClassificationName.Substring("json - ".Length); - var parts = remainder.Split(' '); - var type = string.Join("", parts.Select(Capitalize)); - return "Json." + $"{type}(\"{Text}\")"; - } - - switch (ClassificationName) - { - case "punctuation": - switch (Text) - { - case "(": - return "Punctuation.OpenParen"; - case ")": - return "Punctuation.CloseParen"; - case "[": - return "Punctuation.OpenBracket"; - case "]": - return "Punctuation.CloseBracket"; - case "{": - return "Punctuation.OpenCurly"; - case "}": - return "Punctuation.CloseCurly"; - case ";": - return "Punctuation.Semicolon"; - case ":": - return "Punctuation.Colon"; - case ",": - return "Punctuation.Comma"; - case "..": - return "Punctuation.DotDot"; - } - - goto default; - - case "operator": - switch (Text) - { - case "=": - return "Operators.Equals"; - case "++": - return "Operators.PlusPlus"; - case "=>": - return "Operators.EqualsGreaterThan"; - } - - goto default; - - case "keyword - control": - return $"ControlKeyword(\"{Text}\")"; - - default: - return $"{Capitalize(ClassificationName)}(\"{Text}\")"; - } - } - - private static string Capitalize(string val) - => char.ToUpperInvariant(val[0]) + val.Substring(1); -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/FormattedClassifications.Regex.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/FormattedClassifications.Regex.cs deleted file mode 100644 index 4fce98466e85..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/FormattedClassifications.Regex.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.CodeAnalysis.Classification; - -namespace Microsoft.CodeAnalysis.Editor.UnitTests.Classification; - -public static partial class FormattedClassifications -{ - public static class Regex - { - [DebuggerStepThrough] - public static FormattedClassification Anchor(string value) => New(value, ClassificationTypeNames.RegexAnchor); - - [DebuggerStepThrough] - public static FormattedClassification Grouping(string value) => New(value, ClassificationTypeNames.RegexGrouping); - - [DebuggerStepThrough] - public static FormattedClassification OtherEscape(string value) => New(value, ClassificationTypeNames.RegexOtherEscape); - - [DebuggerStepThrough] - public static FormattedClassification SelfEscapedCharacter(string value) => New(value, ClassificationTypeNames.RegexSelfEscapedCharacter); - - [DebuggerStepThrough] - public static FormattedClassification Alternation(string value) => New(value, ClassificationTypeNames.RegexAlternation); - - [DebuggerStepThrough] - public static FormattedClassification CharacterClass(string value) => New(value, ClassificationTypeNames.RegexCharacterClass); - - [DebuggerStepThrough] - public static FormattedClassification Text(string value) => New(value, ClassificationTypeNames.RegexText); - - [DebuggerStepThrough] - public static FormattedClassification Quantifier(string value) => New(value, ClassificationTypeNames.RegexQuantifier); - - [DebuggerStepThrough] - public static FormattedClassification Comment(string value) => New(value, ClassificationTypeNames.RegexComment); - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/FormattedClassifications.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/FormattedClassifications.cs deleted file mode 100644 index 4141bbaf0b00..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/FormattedClassifications.cs +++ /dev/null @@ -1,208 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -using System.Diagnostics; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.CodeAnalysis.Classification; - -namespace Microsoft.CodeAnalysis.Editor.UnitTests.Classification; - -public static partial class FormattedClassifications -{ - private static FormattedClassification New(string text, string typeName) - => new FormattedClassification(text, typeName); - - [DebuggerStepThrough] - public static FormattedClassification Struct(string text) - => New(text, ClassificationTypeNames.StructName); - - [DebuggerStepThrough] - public static FormattedClassification Enum(string text) - => New(text, ClassificationTypeNames.EnumName); - - [DebuggerStepThrough] - public static FormattedClassification Interface(string text) - => New(text, ClassificationTypeNames.InterfaceName); - - [DebuggerStepThrough] - public static FormattedClassification Class(string text) - => New(text, ClassificationTypeNames.ClassName); - - [DebuggerStepThrough] - public static FormattedClassification Record(string text) - => New(text, ClassificationTypeNames.RecordClassName); - - [DebuggerStepThrough] - public static FormattedClassification RecordStruct(string text) - => New(text, ClassificationTypeNames.RecordStructName); - - [DebuggerStepThrough] - public static FormattedClassification Delegate(string text) - => New(text, ClassificationTypeNames.DelegateName); - - [DebuggerStepThrough] - public static FormattedClassification TypeParameter(string text) - => New(text, ClassificationTypeNames.TypeParameterName); - - [DebuggerStepThrough] - public static FormattedClassification Namespace(string text) - => New(text, ClassificationTypeNames.NamespaceName); - - [DebuggerStepThrough] - public static FormattedClassification Label(string text) - => New(text, ClassificationTypeNames.LabelName); - - [DebuggerStepThrough] - public static FormattedClassification Field(string text) - => New(text, ClassificationTypeNames.FieldName); - - [DebuggerStepThrough] - public static FormattedClassification EnumMember(string text) - => New(text, ClassificationTypeNames.EnumMemberName); - - [DebuggerStepThrough] - public static FormattedClassification Constant(string text) - => New(text, ClassificationTypeNames.ConstantName); - - [DebuggerStepThrough] - public static FormattedClassification Local(string text) - => New(text, ClassificationTypeNames.LocalName); - - [DebuggerStepThrough] - public static FormattedClassification Parameter(string text) - => New(text, ClassificationTypeNames.ParameterName); - - [DebuggerStepThrough] - public static FormattedClassification Method(string text) - => New(text, ClassificationTypeNames.MethodName); - - [DebuggerStepThrough] - public static FormattedClassification ExtensionMethod(string text) - => New(text, ClassificationTypeNames.ExtensionMethodName); - - [DebuggerStepThrough] - public static FormattedClassification Property(string text) - => New(text, ClassificationTypeNames.PropertyName); - - [DebuggerStepThrough] - public static FormattedClassification Event(string text) - => New(text, ClassificationTypeNames.EventName); - - [DebuggerStepThrough] - public static FormattedClassification Static(string text) - => New(text, ClassificationTypeNames.StaticSymbol); - - [DebuggerStepThrough] - public static FormattedClassification String(string text) - => New(text, ClassificationTypeNames.StringLiteral); - - [DebuggerStepThrough] - public static FormattedClassification Verbatim(string text) - => New(text, ClassificationTypeNames.VerbatimStringLiteral); - - [DebuggerStepThrough] - public static FormattedClassification Escape(string text) - => New(text, ClassificationTypeNames.StringEscapeCharacter); - - [DebuggerStepThrough] - public static FormattedClassification Keyword(string text) - => New(text, ClassificationTypeNames.Keyword); - - [DebuggerStepThrough] - public static FormattedClassification PunctuationText(string text) - => New(text, ClassificationTypeNames.Punctuation); - - [DebuggerStepThrough] - public static FormattedClassification ControlKeyword(string text) - => New(text, ClassificationTypeNames.ControlKeyword); - - [DebuggerStepThrough] - public static FormattedClassification WhiteSpace(string text) - => New(text, ClassificationTypeNames.WhiteSpace); - - [DebuggerStepThrough] - public static FormattedClassification Text(string text) - => New(text, ClassificationTypeNames.Text); - - [DebuggerStepThrough] - public static FormattedClassification NumericLiteral(string text) - => New(text, ClassificationTypeNames.NumericLiteral); - - [DebuggerStepThrough] - public static FormattedClassification PPKeyword(string text) - => New(text, ClassificationTypeNames.PreprocessorKeyword); - - [DebuggerStepThrough] - public static FormattedClassification PPText(string text) - => New(text, ClassificationTypeNames.PreprocessorText); - - [DebuggerStepThrough] - public static FormattedClassification Identifier(string text) - => New(text, ClassificationTypeNames.Identifier); - - [DebuggerStepThrough] - public static FormattedClassification Inactive(string text) - => New(text, ClassificationTypeNames.ExcludedCode); - - [DebuggerStepThrough] - public static FormattedClassification Comment(string text) - => New(text, ClassificationTypeNames.Comment); - - [DebuggerStepThrough] - public static FormattedClassification Number(string text) - => New(text, ClassificationTypeNames.NumericLiteral); - - public static FormattedClassification LineContinuation { get; } - = New("_", ClassificationTypeNames.Punctuation); - - [DebuggerStepThrough] - public static FormattedClassification Module(string text) - => New(text, ClassificationTypeNames.ModuleName); - - [DebuggerStepThrough] - public static FormattedClassification VBXmlName(string text) - => New(text, ClassificationTypeNames.XmlLiteralName); - - [DebuggerStepThrough] - public static FormattedClassification VBXmlText(string text) - => New(text, ClassificationTypeNames.XmlLiteralText); - - [DebuggerStepThrough] - public static FormattedClassification VBXmlProcessingInstruction(string text) - => New(text, ClassificationTypeNames.XmlLiteralProcessingInstruction); - - [DebuggerStepThrough] - public static FormattedClassification VBXmlEmbeddedExpression(string text) - => New(text, ClassificationTypeNames.XmlLiteralEmbeddedExpression); - - [DebuggerStepThrough] - public static FormattedClassification VBXmlDelimiter(string text) - => New(text, ClassificationTypeNames.XmlLiteralDelimiter); - - [DebuggerStepThrough] - public static FormattedClassification VBXmlComment(string text) - => New(text, ClassificationTypeNames.XmlLiteralComment); - - [DebuggerStepThrough] - public static FormattedClassification VBXmlCDataSection(string text) - => New(text, ClassificationTypeNames.XmlLiteralCDataSection); - - [DebuggerStepThrough] - public static FormattedClassification VBXmlAttributeValue(string text) - => New(text, ClassificationTypeNames.XmlLiteralAttributeValue); - - [DebuggerStepThrough] - public static FormattedClassification VBXmlAttributeQuotes(string text) - => New(text, ClassificationTypeNames.XmlLiteralAttributeQuotes); - - [DebuggerStepThrough] - public static FormattedClassification VBXmlAttributeName(string text) - => New(text, ClassificationTypeNames.XmlLiteralAttributeName); - - [DebuggerStepThrough] - public static FormattedClassification VBXmlEntityReference(string text) - => New(text, ClassificationTypeNames.XmlLiteralEntityReference); -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/MarkupTestFile.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/MarkupTestFile.cs deleted file mode 100644 index 170efdc8247a..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/Infrastructure/MarkupTestFile.cs +++ /dev/null @@ -1,263 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; -using System.Globalization; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; - -/// <summary> -/// To aid with testing, we define a special type of text file that can encode additional -/// information in it. This prevents a test writer from having to carry around multiple sources -/// of information that must be reconstituted. For example, instead of having to keep around the -/// contents of a file *and* and the location of the cursor, the tester can just provide a -/// string with the "$" character in it. This allows for easy creation of "FIT" tests where all -/// that needs to be provided are strings that encode every bit of state necessary in the string -/// itself. -/// -/// The current set of encoded features we support are: -/// -/// $$ - The position in the file. There can be at most one of these. -/// -/// [| ... |] - A span of text in the file. There can be many of these and they can be nested -/// and/or overlap the $ position. -/// -/// {|Name: ... |} A span of text in the file annotated with an identifier. There can be many of -/// these, including ones with the same name. -/// -/// Additional encoded features can be added on a case by case basis. -/// </summary> -public static class MarkupTestFile -{ - private const string PositionString = "$$"; - private const string SpanStartString = "[|"; - private const string SpanEndString = "|]"; - private const string NamedSpanStartString = "{|"; - private const string NamedSpanEndString = "|}"; - - private static readonly Regex s_namedSpanStartRegex = new Regex(@"\{\| ([-_.A-Za-z0-9\+]+) \:", - RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace); - - private static void Parse( - string input, out string output, out int? position, out IDictionary<string, List<TextSpan>> spans) - { - position = null; - var tempSpans = new Dictionary<string, List<TextSpan>>(); - - var outputBuilder = new StringBuilder(); - - var currentIndexInInput = 0; - var inputOutputOffset = 0; - - // A stack of span starts along with their associated annotation name. [||] spans simply - // have empty string for their annotation name. - var spanStartStack = new Stack<(int matchIndex, string name)>(); - var namedSpanStartStack = new Stack<(int matchIndex, string name)>(); - - while (true) - { - var matches = new List<(int matchIndex, string name)>(); - AddMatch(input, PositionString, currentIndexInInput, matches); - AddMatch(input, SpanStartString, currentIndexInInput, matches); - AddMatch(input, SpanEndString, currentIndexInInput, matches); - AddMatch(input, NamedSpanEndString, currentIndexInInput, matches); - - var namedSpanStartMatch = s_namedSpanStartRegex.Match(input, currentIndexInInput); - if (namedSpanStartMatch.Success) - { - matches.Add((namedSpanStartMatch.Index, namedSpanStartMatch.Value)); - } - - if (matches.Count == 0) - { - // No more markup to process. - break; - } - - var orderedMatches = matches.OrderBy(t => t, Comparer<(int matchIndex, string name)>.Create((t1, t2) => t1.matchIndex - t2.matchIndex)).ToList(); - if (orderedMatches.Count >= 2 && - (spanStartStack.Count > 0 || namedSpanStartStack.Count > 0) && - matches[0].matchIndex == matches[1].matchIndex - 1) - { - // We have a slight ambiguity with cases like these: - // - // [|] [|} - // - // Is it starting a new match, or ending an existing match. As a workaround, we - // special case these and consider it ending a match if we have something on the - // stack already. - if (matches[0].name == SpanStartString && matches[1].name == SpanEndString && spanStartStack.Count > 0 || - matches[0].name == SpanStartString && matches[1].name == NamedSpanEndString && namedSpanStartStack.Count > 0) - { - orderedMatches.RemoveAt(0); - } - } - - // Order the matches by their index - var firstMatch = orderedMatches.First(); - - var matchIndexInInput = firstMatch.matchIndex; - var matchString = firstMatch.name; - - var matchIndexInOutput = matchIndexInInput - inputOutputOffset; - outputBuilder.Append(input.Substring(currentIndexInInput, matchIndexInInput - currentIndexInInput)); - - currentIndexInInput = matchIndexInInput + matchString.Length; - inputOutputOffset += matchString.Length; - - switch (matchString.Substring(0, 2)) - { - case PositionString: - if (position.HasValue) - { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Saw multiple occurrences of {0}", PositionString)); - } - - position = matchIndexInOutput; - break; - - case SpanStartString: - spanStartStack.Push((matchIndexInOutput, string.Empty)); - break; - - case SpanEndString: - if (spanStartStack.Count == 0) - { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Saw {0} without matching {1}", SpanEndString, SpanStartString)); - } - - PopSpan(spanStartStack, tempSpans, matchIndexInOutput); - break; - - case NamedSpanStartString: - var name = namedSpanStartMatch.Groups[1].Value; - namedSpanStartStack.Push((matchIndexInOutput, name)); - break; - - case NamedSpanEndString: - if (namedSpanStartStack.Count == 0) - { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Saw {0} without matching {1}", NamedSpanEndString, NamedSpanStartString)); - } - - PopSpan(namedSpanStartStack, tempSpans, matchIndexInOutput); - break; - - default: - throw new InvalidOperationException(); - } - } - - if (spanStartStack.Count > 0) - { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Saw {0} without matching {1}", SpanStartString, SpanEndString)); - } - - if (namedSpanStartStack.Count > 0) - { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Saw {0} without matching {1}", NamedSpanEndString, NamedSpanEndString)); - } - - // Append the remainder of the string. - outputBuilder.Append(input.Substring(currentIndexInInput)); - output = outputBuilder.ToString(); - spans = tempSpans.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - } - - private static V GetOrAdd<K, V>(IDictionary<K, V> dictionary, K key, Func<K, V> function) - { - if (!dictionary.TryGetValue(key, out var value)) - { - value = function(key); - dictionary.Add(key, value); - } - - return value; - } - - private static void PopSpan( - Stack<(int matchIndex, string name)> spanStartStack, - IDictionary<string, List<TextSpan>> spans, - int finalIndex) - { - var (matchIndex, name) = spanStartStack.Pop(); - - var span = TextSpan.FromBounds(matchIndex, finalIndex); - GetOrAdd(spans, name, _ => new List<TextSpan>()).Add(span); - } - - private static void AddMatch(string input, string value, int currentIndex, List<(int, string)> matches) - { - var index = input.IndexOf(value, currentIndex, StringComparison.Ordinal); - if (index >= 0) - { - matches.Add((index, value)); - } - } - - private static void GetPositionAndSpans( - string input, out string output, out int? cursorPositionOpt, out ImmutableArray<TextSpan> spans) - { - Parse(input, out output, out cursorPositionOpt, out var dictionary); - - var builder = GetOrAdd(dictionary, string.Empty, _ => new List<TextSpan>()); - builder.Sort((left, right) => left.Start - right.Start); - spans = builder.ToImmutableArray(); - } - - public static void GetPositionAndSpans( - string input, out string output, out int? cursorPositionOpt, out IDictionary<string, ImmutableArray<TextSpan>> spans) - { - Parse(input, out output, out cursorPositionOpt, out var dictionary); - spans = dictionary.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableArray()); - } - - public static void GetSpans(string input, out string output, out IDictionary<string, ImmutableArray<TextSpan>> spans) - => GetPositionAndSpans(input, out output, out var cursorPositionOpt, out spans); - - public static void GetPositionAndSpans(string input, out string output, out int cursorPosition, out ImmutableArray<TextSpan> spans) - { - GetPositionAndSpans(input, out output, out int? pos, out spans); - cursorPosition = pos.Value; - } - - public static void GetPosition(string input, out string output, out int? cursorPosition) - => GetPositionAndSpans(input, out output, out cursorPosition, out ImmutableArray<TextSpan> spans); - - public static void GetPosition(string input, out string output, out int cursorPosition) - => GetPositionAndSpans(input, out output, out cursorPosition, out var spans); - - public static void GetPositionAndSpan(string input, out string output, out int? cursorPosition, out TextSpan? textSpan) - { - GetPositionAndSpans(input, out output, out cursorPosition, out ImmutableArray<TextSpan> spans); - textSpan = spans.Length == 0 ? null : spans.Single(); - } - - public static void GetPositionAndSpan(string input, out string output, out int cursorPosition, out TextSpan textSpan) - { - GetPositionAndSpans(input, out output, out cursorPosition, out var spans); - textSpan = spans.Single(); - } - - public static void GetSpans(string input, out string output, out ImmutableArray<TextSpan> spans) - { - GetPositionAndSpans(input, out output, out int? pos, out spans); - } - - public static void GetSpan(string input, out string output, out TextSpan textSpan) - { - GetSpans(input, out output, out ImmutableArray<TextSpan> spans); - textSpan = spans.Single(); - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternAnalyzerTests.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternAnalyzerTests.cs deleted file mode 100644 index a4da17cb9eee..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternAnalyzerTests.cs +++ /dev/null @@ -1,262 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Globalization; -using Microsoft.AspNetCore.Analyzer.Testing; -using Microsoft.AspNetCore.Analyzers.RenderTreeBuilder; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -public partial class RoutePatternAnalyzerTests -{ - private TestDiagnosticAnalyzerRunner Runner { get; } = new(new RoutePatternAnalyzer()); - - [Fact] - public async Task StringSyntax_AttributeProperty_ReportResults() - { - // Arrange - var source = TestSource.Read(@" -using System; -using System.Diagnostics.CodeAnalysis; - -class Program -{ - [HttpGet(Pattern = @""/*MM*/~hi"")] - static void Main() - { - } -} - -class HttpGet : Attribute -{ - [StringSyntax(""Route"")] - public string Pattern { get; set; } -} -"); - // Act - var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); - - // Assert - var diagnostic = Assert.Single(diagnostics); - Assert.Same(DiagnosticDescriptors.RoutePatternIssue, diagnostic.Descriptor); - AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); - Assert.Equal($"Route issue: {Resources.TemplateRoute_InvalidRouteTemplate}", diagnostic.GetMessage(CultureInfo.InvariantCulture)); - } - - [Fact] - public async Task StringSyntax_AttributeCtorArgument_ReportResults() - { - // Arrange - var source = TestSource.Read(@" -using System; -using System.Diagnostics.CodeAnalysis; - -class Program -{ - [HttpGet(@""/*MM*/~hi"")] - static void Main() - { - } -} - -class HttpGet : Attribute -{ - public HttpGet([StringSyntax(""Route"")] string pattern) - { - } -} -"); - // Act - var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); - - // Assert - var diagnostic = Assert.Single(diagnostics); - Assert.Same(DiagnosticDescriptors.RoutePatternIssue, diagnostic.Descriptor); - AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); - Assert.Equal($"Route issue: {Resources.TemplateRoute_InvalidRouteTemplate}", diagnostic.GetMessage(CultureInfo.InvariantCulture)); - } - - [Fact] - public async Task StringSyntax_FieldSet_ReportResults() - { - // Arrange - var source = TestSource.Read(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - field = @""/*MM*/~hi""; - } - - [StringSyntax(""Route"")] - private static string field; -} -"); - // Act - var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); - - // Assert - var diagnostic = Assert.Single(diagnostics); - Assert.Same(DiagnosticDescriptors.RoutePatternIssue, diagnostic.Descriptor); - AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); - Assert.Equal($"Route issue: {Resources.TemplateRoute_InvalidRouteTemplate}", diagnostic.GetMessage(CultureInfo.InvariantCulture)); - } - - [Fact] - public async Task StringSyntax_PropertySet_ReportResults() - { - // Arrange - var source = TestSource.Read(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - prop = @""/*MM*/~hi""; - } - - [StringSyntax(""Route"")] - private static string prop { get; set; } -} -"); - // Act - var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); - - // Assert - var diagnostic = Assert.Single(diagnostics); - Assert.Same(DiagnosticDescriptors.RoutePatternIssue, diagnostic.Descriptor); - AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); - Assert.Equal($"Route issue: {Resources.TemplateRoute_InvalidRouteTemplate}", diagnostic.GetMessage(CultureInfo.InvariantCulture)); - } - - [Fact] - public async Task StringSyntax_MethodArgument_ReportResults() - { - // Arrange - var source = TestSource.Read(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""/*MM*/~hi""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - // Act - var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); - - // Assert - var diagnostic = Assert.Single(diagnostics); - Assert.Same(DiagnosticDescriptors.RoutePatternIssue, diagnostic.Descriptor); - AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); - Assert.Equal($"Route issue: {Resources.TemplateRoute_InvalidRouteTemplate}", diagnostic.GetMessage(CultureInfo.InvariantCulture)); - } - - [Fact] - public async Task StringSyntax_MethodArgument_MultipleResults() - { - // Arrange - var source = TestSource.Read(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""~hi?""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - // Act - var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); - - // Assert - Assert.Collection( - diagnostics, - d => - { - Assert.Same(DiagnosticDescriptors.RoutePatternIssue, d.Descriptor); - Assert.Equal($"Route issue: {Resources.FormatTemplateRoute_InvalidLiteral("~hi?")}", d.GetMessage(CultureInfo.InvariantCulture)); - }, - d => - { - Assert.Same(DiagnosticDescriptors.RoutePatternIssue, d.Descriptor); - Assert.Equal($"Route issue: {Resources.TemplateRoute_InvalidRouteTemplate}", d.GetMessage(CultureInfo.InvariantCulture)); - }); - } - - [Fact] - public async Task BadTokenReplacement_MethodArgument_MultipleResults() - { - // Arrange - var source = TestSource.Read(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""[hi""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - // Act - var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); - - // Assert - Assert.Empty(diagnostics); - } - - [Fact] - public async Task BadTokenReplacement_MvcAction_TokenReplacementDiagnostics() - { - // Arrange - var source = TestSource.Read(@" -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Mvc; - -class Program -{ - static void Main() - { - } -} - -[Route(@""[hi"")] -public class TestController -{ - public void TestAction() - { - } -} -"); - // Act - var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); - - // Assert - Assert.Collection( - diagnostics, - d => - { - Assert.Same(DiagnosticDescriptors.RoutePatternIssue, d.Descriptor); - Assert.Equal($"Route issue: {Resources.AttributeRoute_TokenReplacement_UnclosedToken}", d.GetMessage(CultureInfo.InvariantCulture)); - }); - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternBraceMatcherTests.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternBraceMatcherTests.cs deleted file mode 100644 index d294648c0ac5..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternBraceMatcherTests.cs +++ /dev/null @@ -1,205 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.EmbeddedLanguages; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -public partial class RoutePatternBraceMatcherTests -{ - private TestDiagnosticAnalyzerRunner Runner { get; } = new(new RoutePatternAnalyzer()); - - [Fact] - public async Task AfterLiteral_NoHighlight() - { - // Arrange & Act & Assert - await TestBraceMatchesAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""hi$$""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - } - - [Fact] - public async Task BeforeParameterStart_CompleteParameter_HighlightBraces() - { - // Arrange & Act & Assert - await TestBraceMatchesAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""$$[|{|]hi[|}|]""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - } - - [Fact] - public async Task BeforeParameterStart_IncompleteParameter_NoHighlight() - { - // Arrange & Act & Assert - await TestBraceMatchesAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""$${hi""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - } - - [Fact] - public async Task BeforeArgumentStart_CompleteParenAndParameter_HighlightParens() - { - // Arrange & Act & Assert - await TestBraceMatchesAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""{hi:regex$$[|(|]aaa[|)|]}""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - } - - [Fact] - public async Task BeforeArgumentStart_CompleteParenIncompleteParameter_HighlightParens() - { - // Arrange & Act & Assert - await TestBraceMatchesAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""{hi:regex$$[|(|]aaa[|)|]""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - } - - [Fact] - public async Task AfterParameterStart_CompleteParameter_NoHighlight() - { - // Arrange & Act & Assert - await TestBraceMatchesAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""{$$hi}""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - } - - [Fact] - public async Task BeforeReplacementTokenStart_NotUsedWithMvc_NoHighlight() - { - // Arrange & Act & Assert - await TestBraceMatchesAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""$$[aaa]""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - } - - [Fact] - public async Task BeforeReplacementTokenStart_MvcAction_HighlightReplacementTokenBrackets() - { - // Arrange & Act & Assert - await TestBraceMatchesAsync(@" -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Mvc; - -class Program -{ - static void Main() - { - } -} - -[Route(@""$$[|[|]aaa[|]|]"")] -public class TestController -{ - public void TestAction() - { - } -} -"); - } - - private async Task TestBraceMatchesAsync(string source) - { - MarkupTestFile.GetPositionAndSpans(source, out var output, out int cursorPosition, out var spans); - - var result = await Runner.GetBraceMatchesAsync(cursorPosition, output); - if (result == null) - { - if (spans.IsDefaultOrEmpty) - { - return; - } - - throw new Exception("No result doesn't match spans."); - } - - if (!spans.Contains(result.Value.LeftSpan) || !spans.Contains(result.Value.RightSpan)) - { - throw new Exception("Not found."); - } - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternClassifierTests.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternClassifierTests.cs deleted file mode 100644 index 51961466aa68..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternClassifierTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Immutable; -using System.Globalization; -using Microsoft.AspNetCore.Analyzer.Testing; -using Microsoft.AspNetCore.Analyzers.RenderTreeBuilder; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Classification; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Text; -using Xunit.Abstractions; -using static Microsoft.CodeAnalysis.Editor.UnitTests.Classification.FormattedClassifications; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -public class RoutePatternClassifierTests -{ - private readonly ITestOutputHelper _output; - - private TestDiagnosticAnalyzerRunner Runner { get; } = new(new RenderTreeBuilderAnalyzer()); - - protected async Task TestAsync( - string code, - params FormattedClassification[] expected) - { - MarkupTestFile.GetSpans(code, out var rewrittenCode, out ImmutableArray<TextSpan> spans); - Assert.True(spans.Length == 1); - - var actual = await Runner.GetClassificationSpansAsync(spans.Single(), rewrittenCode); - var actualOrdered = actual.OrderBy(t1 => t1.TextSpan.Start).ToList(); - var actualFormatted = actualOrdered.Select(a => new FormattedClassification(rewrittenCode.Substring(a.TextSpan.Start, a.TextSpan.Length), a.ClassificationType)).ToArray(); - - Assert.Equal(expected, actualFormatted); - } - - public RoutePatternClassifierTests(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public async Task AttributeOnField_Classified() - { - await TestAsync( -@" -using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; - -class Program -{ - [StringSyntax(""Route"")] - private string field; - - void Goo() - { - this.field = [|@""{id?}""|]; - } -}" + EmbeddedLanguagesTestConstants.StringSyntaxAttributeCodeCSharp, -Verbatim(@"@""{id?}"""), -Regex.CharacterClass("{"), -Parameter("id"), -Regex.Anchor("?"), -Regex.CharacterClass("}")); - } - - [Fact] - public async Task AttributeOnField_TokenReplacementText_TokenReplacementNotClassified() - { - await TestAsync( -@" -using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; - -class Program -{ - [StringSyntax(""Route"")] - private string field; - - void Goo() - { - this.field = [|@""[one]/{id}""|]; - } -}", -Verbatim(@"@""[one]/{id}"""), -Regex.CharacterClass("{"), -Parameter("id"), -Regex.CharacterClass("}")); - } - - [Fact] - public async Task AttributeOnAction_TokenReplacementText_TokenReplacementClassified() - { - await TestAsync( -@" -using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; -using Microsoft.AspNetCore.Mvc; - -public class TestController -{ - [HttpGet([|@""[one]""|])] - public void TestAction() - { - } -}", -Verbatim(@"@""[one]"""), -Regex.CharacterClass("["), -Regex.CharacterClass("one"), -Regex.CharacterClass("]")); - } - - [Fact] - public async Task AttributeOnController_TokenReplacementText_TokenReplacementClassified() - { - await TestAsync( -@" -using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; -using Microsoft.AspNetCore.Mvc; - -[Route([|@""[one]""|])] -public class TestController -{ - public void TestAction() - { - } -}", -Verbatim(@"@""[one]"""), -Regex.CharacterClass("["), -Regex.CharacterClass("one"), -Regex.CharacterClass("]")); - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternCompletionProviderTests.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternCompletionProviderTests.cs deleted file mode 100644 index 3dee96d2891c..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternCompletionProviderTests.cs +++ /dev/null @@ -1,390 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -public partial class RoutePatternCompletionProviderTests -{ - private TestDiagnosticAnalyzerRunner Runner { get; } = new(new RoutePatternAnalyzer()); - - [Fact] - public async Task Insertion_Literal_NoItems() - { - // Arrange & Act - var result = await GetCompletionsAndServiceAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""hi$$""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - - // Assert - Assert.Empty(result.Completions.Items); - } - - [Fact] - public async Task Insertion_PolicyColon_ReturnPolicies() - { - // Arrange & Act - var result = await GetCompletionsAndServiceAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""{hi:$$""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - - // Assert - Assert.NotEmpty(result.Completions.Items); - Assert.Equal("alpha", result.Completions.Items[0].DisplayText); - - // Getting description is currently broken in Roslyn. - //var description = await result.Service.GetDescriptionAsync(result.Document, result.Completions.Items[0]); - //Assert.Equal("int", description.Text); - } - - [Fact] - public async Task Insertion_PolicyColon_MultipleOverloads_ReturnPolicies() - { - // Arrange & Act - var result = await GetCompletionsAndServiceAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""{hi:$$""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } - static void M([StringSyntax(""Route"")] string p, int i) - { - } -} -"); - - // Assert - Assert.NotEmpty(result.Completions.Items); - Assert.Equal("alpha", result.Completions.Items[0].DisplayText); - } - - [Fact] - public async Task Insertion_ParameterOpenBrace_UnsupportedMethod_NoItems() - { - // Arrange & Act - var result = await GetCompletionsAndServiceAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""{$$""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - - // Assert - Assert.Empty(result.Completions.Items); - } - - [Fact] - public async Task Insertion_ParameterOpenBrace_EndpointMapGet_HasDelegate_ReturnDelegateParameterItem() - { - // Arrange & Act - var result = await GetCompletionsAndServiceAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; - -class Program -{ - static void Main() - { - EndpointRouteBuilderExtensions.MapGet(null, @""{$$"", (string id) => ""); - } -} -"); - - // Assert - Assert.Collection( - result.Completions.Items, - i => Assert.Equal("id", i.DisplayText)); - } - - [Fact] - public async Task Insertion_ParameterOpenBrace_EndpointMapGet_HasMethod_ReturnDelegateParameterItem() - { - // Arrange & Act - var result = await GetCompletionsAndServiceAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; - -class Program -{ - static void Main() - { - EndpointRouteBuilderExtensions.MapGet(null, @""{$$"", ExecuteGet); - } - - static string ExecuteGet(string id) - { - return """"; - } -} -"); - - // Assert - Assert.Collection( - result.Completions.Items, - i => Assert.Equal("id", i.DisplayText)); - } - - [Fact] - public async Task Insertion_ParameterOpenBrace_EndpointMapGet_HasMethod_NamedParameters_ReturnDelegateParameterItem() - { - // Arrange & Act - var result = await GetCompletionsAndServiceAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; - -class Program -{ - static void Main() - { - EndpointRouteBuilderExtensions.MapGet(handler: ExecuteGet, pattern: @""{$$"", endpoints: null); - } - - static string ExecuteGet(string id) - { - return """"; - } -} -"); - - // Assert - Assert.Collection( - result.Completions.Items, - i => Assert.Equal("id", i.DisplayText)); - } - - [Fact] - public async Task Insertion_ParameterOpenBrace_EndpointMapGet_HasSpecialTypes_ExcludeSpecialTypes() - { - // Arrange & Act - var result = await GetCompletionsAndServiceAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.IO.Pipelines; -using System.Security.Claims; -using System.Threading; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; - -class Program -{ - static void Main() - { - EndpointRouteBuilderExtensions.MapGet(null, @""{$$"", ExecuteGet); - } - - static string ExecuteGet(string id, CancellationToken cancellationToken, HttpContext context, - HttpRequest request, HttpResponse response, ClaimsPrincipal claimsPrincipal, - IFormFileCollection formFiles, IFormFile formFile, Stream stream, PipeReader pipeReader) - { - return """"; - } -} -"); - - // Assert - Assert.Collection( - result.Completions.Items, - i => Assert.Equal("id", i.DisplayText)); - } - - [Fact] - public async Task Insertion_ParameterOpenBrace_EndpointMapGet_AsParameters_ReturnObjectParameterItem() - { - // Arrange & Act - var result = await GetCompletionsAndServiceAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -class Program -{ - static void Main() - { - EndpointRouteBuilderExtensions.MapGet(null, @""{$$"", ExecuteGet); - } - - static string ExecuteGet([AsParameters] PageData id) - { - return """"; - } - - class PageData - { - public int PageNumber { get; set; } - [FromRoute] - public int PageIndex { get; set; } - [FromServices] - public object Service { get; set; } - } -} -"); - - // Assert - Assert.Collection( - result.Completions.Items, - i => Assert.Equal("PageIndex", i.DisplayText), - i => Assert.Equal("PageNumber", i.DisplayText)); - } - - [Fact] - public async Task Insertion_ParameterOpenBrace_EndpointMapGet_NullDelegate_NoResults() - { - // Arrange & Act - var result = await GetCompletionsAndServiceAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; - -class Program -{ - static void Main() - { - EndpointRouteBuilderExtensions.MapGet(null, @""{$$"", null); - } -} -"); - - // Assert - Assert.Empty(result.Completions.Items); - } - - [Fact] - public async Task Insertion_ParameterOpenBrace_EndpointMapGet_Incomplete_NoResults() - { - // Arrange & Act - var result = await GetCompletionsAndServiceAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; - -class Program -{ - static void Main() - { - EndpointRouteBuilderExtensions.MapGet(null, @""{$$""; - } -} -"); - - // Assert - Assert.Empty(result.Completions.Items); - } - - [Fact] - public async Task Insertion_ParameterOpenBrace_CustomMapGet_ReturnDelegateParameterItem() - { - // Arrange & Act - var result = await GetCompletionsAndServiceAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Routing; - -class Program -{ - static void Main() - { - MapCustomThing(null, @""{$$"", (string id) => ""); - } - - static void MapCustomThing(IEndpointRouteBuilder endpoints, [StringSyntax(""Route"")] string pattern, Delegate delegate) - { - } -} -"); - - // Assert - Assert.Collection( - result.Completions.Items, - i => Assert.Equal("id", i.DisplayText)); - } - - [Fact] - public async Task Insertion_ParameterOpenBrace_ControllerAction_HasParameter_ReturnActionParameterItem() - { - // Arrange & Act - var result = await GetCompletionsAndServiceAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc; - -class Program -{ - static void Main() - { - } -} - -public class TestController -{ - [HttpGet(@""{$$"")] - public object TestAction(int id) - { - return null; - } -} -"); - - // Assert - Assert.Collection( - result.Completions.Items, - i => Assert.Equal("id", i.DisplayText)); - } - - private async Task<CompletionResult> GetCompletionsAndServiceAsync(string source) - { - MarkupTestFile.GetPosition(source, out var output, out int cursorPosition); - - var completions = await Runner.GetCompletionsAndServiceAsync(cursorPosition, output); - - return completions; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternHighlighterTests.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternHighlighterTests.cs deleted file mode 100644 index c2a2b3354817..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternHighlighterTests.cs +++ /dev/null @@ -1,406 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Routing; -using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.EmbeddedLanguages; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -public partial class RoutePatternHighlighterTests -{ - private TestDiagnosticAnalyzerRunner Runner { get; } = new(new RoutePatternAnalyzer()); - - [Fact] - public async Task AfterLiteral_NoHighlight() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""hi$$""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - } - - [Fact] - public async Task AfterParameterStart_NoHighlight() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""{$$""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - } - - [Fact] - public async Task BeforeParameterName_CompleteParameter_HighlightName() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""{$$[|hi|]}""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - } - - [Fact] - public async Task BeforeParameterName_ParameterWithConstraint_HighlightName() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""{$$[|hi|]:int""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - } - - [Fact] - public async Task MiddleParameterName_CompleteParameter_HighlightName() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""{[|h$$i|]}""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - } - - [Fact] - public async Task MiddleConstraint_ParameterWithConstraint_NoHighlight() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System.Diagnostics.CodeAnalysis; - -class Program -{ - static void Main() - { - M(@""{hi:i$$nt""); - } - - static void M([StringSyntax(""Route"")] string p) - { - } -} -"); - } - - [Fact] - public async Task InParameterName_ExtensionMethod_MatchingDelegate_HighlightParameter() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Routing; - -class Program -{ - static void Main() - { - IEndpointRouteBuilder builder = null; - builder.MapGet(@""{$$[|id|]}"", (string [|id|]) => $""{[|id|]}""); - } -} -"); - } - - [Fact] - public async Task InParameterName_MatchingDelegate_HighlightParameter() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; - -class Program -{ - static void Main() - { - EndpointRouteBuilderExtensions.MapGet(null, @""{$$[|id|]}"", (string [|id|]) => $""{[|id|]}""); - } -} -"); - } - - [Fact] - public async Task InParameterName_MatchingDelegate_AsParameters_HighlightProperty() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; - -class Program -{ - static void Main() - { - EndpointRouteBuilderExtensions.MapGet(null, @""{$$[|pageIndex|]}"", ([AsParameters] PageData pageData) => $""{pageData.[|PageIndex|]}""); - } - - int OtherMethod(PageData pageData) - { - return pageData.PageIndex; - } -} - -public class PageData -{ - public int PageNumber { get; set; } - public int PageIndex { get; set; } -} -"); - } - - [Fact] - public async Task InParameterName_MatchingDelegate_AsParameters_DontHighlightArgument() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; - -class Program -{ - static void Main() - { - EndpointRouteBuilderExtensions.MapGet(null, @""{$$[|pageData|]}"", ([AsParameters] PageData pageData) => $""{pageData.PageIndex}""); - } - - int OtherMethod(PageData pageData) - { - return pageData.PageIndex; - } -} - -public class PageData -{ - public int PageNumber { get; set; } - public int PageIndex { get; set; } -} -"); - } - - [Fact] - public async Task InParameterName_MatchingMethod_HighlightParameter() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; - -class Program -{ - static void Main() - { - EndpointRouteBuilderExtensions.MapGet(null, @""{$$[|id|]}"", ExecuteGet); - } - - static string ExecuteGet(string [|id|]) - { - return $""{[|id|]}""; - } -} -"); - } - - [Fact] - public async Task InParameterName_MatchingAction_HighlightParameter() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc; - -class Program -{ - static void Main() - { - } -} - -public class TestController -{ - [HttpGet(@""{$$[|id|]}"")] - public object TestAction(int [|id|]) - { - return null; - } -} -"); - } - - [Fact] - public async Task InParameterName_MatchingActionWithNamespace_HighlightParameter() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc; - -class Program -{ - static void Main() - { - } -} - -namespace Test -{ - public class TestController - { - [HttpGet(@""{$$[|id|]}"")] - public object TestAction(int [|id|]) - { - return null; - } - } -} -"); - } - - [Fact] - public async Task InParameterName_NestedControllerMatchingAction_NoHighlight() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Mvc; - -class Program -{ - static void Main() - { - } -} - -public class OuterClass -{ - public class TestController - { - [HttpGet(@""{$$[|id|]}"")] - public object TestAction(int id) - { - return null; - } - } -} -"); - } - - [Fact] - public async Task InParameterName_MatchingDelegateWithConflictingIdentifer_DontHighlightConflict() - { - // Arrange & Act & Assert - await TestHighlightingAsync(@" -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Builder; - -class Program -{ - static void Main() - { - EndpointRouteBuilderExtensions.MapGet(null, @""{$$[|id|]}"", ExecuteGet); - } - - static string ExecuteGet(string [|id|]) - { - [|id|] = TestEnum.id.ToString(); - return $""{[|id|]}""; - } - - enum TestEnum - { - id; - } -} -"); - } - - private async Task TestHighlightingAsync(string source) - { - MarkupTestFile.GetPositionAndSpans(source, out var output, out int cursorPosition, out var spans); - - var tempSpans = spans.ToList(); - var highlightSpans = await Runner.GetHighlightingAsync(cursorPosition, output); - foreach (var span in highlightSpans) - { - if (!tempSpans.Remove(span.TextSpan)) - { - throw new Exception($"Couldn't find {span.TextSpan} in highlight results."); - } - } - - if (tempSpans.Count > 0) - { - throw new Exception($"Unmatched highlight spans in document: {string.Join(", ", tempSpans)}"); - } - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests.cs deleted file mode 100644 index 3bbd8a8a8dd7..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests.cs +++ /dev/null @@ -1,393 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -using System; -using System.Collections.Immutable; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text.RegularExpressions; -using System.Threading; -using System.Xml.Linq; -using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.EmbeddedLanguages; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.EmbeddedLanguages.VirtualChars; -using Microsoft.CodeAnalysis.EmbeddedLanguages.Common; -using Microsoft.CodeAnalysis.EmbeddedLanguages.RegularExpressions; -using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; -using Microsoft.CodeAnalysis.Text; -using Xunit; -using System.Reflection; -using Xunit.Abstractions; -using Microsoft.AspNetCore.Routing.Patterns; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.RoutePattern; -using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.VirtualChars; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure.EmbeddedSyntax; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -using RoutePatternToken = EmbeddedSyntaxToken<RoutePatternKind>; - -public partial class RoutePatternParserTests -{ - private const string _statmentPrefix = "var v = "; - private readonly ITestOutputHelper _outputHelper; - - public RoutePatternParserTests(ITestOutputHelper outputHelper) - { - _outputHelper = outputHelper; - } - - private static SyntaxToken GetStringToken(string text) - { - var statement = _statmentPrefix + text; - var parsedStatement = SyntaxFactory.ParseStatement(statement); - var token = parsedStatement.DescendantTokens().ToArray()[3]; - Assert.True(token.IsKind(SyntaxKind.StringLiteralToken)); - - return token; - } - - private RoutePatternTree Test( - string stringText, - string expected = null, - bool runSubTreeTests = true, - bool allowDiagnosticsMismatch = false, - bool runReplaceTokens = false) - { - var (tree, sourceText) = TryParseTree( - stringText, - conversionFailureOk: false, - allowDiagnosticsMismatch, - runReplaceTokens); - - // Tests are allowed to not run the subtree tests. This is because some - // subtrees can cause the native regex parser to exhibit very bad behavior - // (like not ever actually finishing compiling). - if (runSubTreeTests) - { - TryParseSubTrees(stringText, allowDiagnosticsMismatch, runReplaceTokens); - } - - const string DoubleQuoteEscaping = "\"\""; - var actual = TreeToText(sourceText, tree) - .Replace("\"", DoubleQuoteEscaping) - .Replace(""", DoubleQuoteEscaping); - - _outputHelper.WriteLine(actual); - if (expected != null) - { - Assert.Equal(expected.Replace("\"", DoubleQuoteEscaping), actual); - } - - return tree; - } - - private void TryParseSubTrees( - string stringText, - bool allowDiagnosticsMismatch, - bool runReplaceTokens = false) - { - // Trim the input from the right and make sure tree invariants hold - var current = stringText; - while (current is not "@\"\"" and not "\"\"") - { - current = current.Substring(0, current.Length - 2) + "\""; - TryParseTree(current, conversionFailureOk: true, allowDiagnosticsMismatch, runReplaceTokens); - } - - // Trim the input from the left and make sure tree invariants hold - current = stringText; - while (current is not "@\"\"" and not "\"\"") - { - if (current[0] == '@') - { - current = "@\"" + current.Substring(3); - } - else - { - current = "\"" + current.Substring(2); - } - - TryParseTree(current, conversionFailureOk: true, allowDiagnosticsMismatch, runReplaceTokens); - } - - for (var start = stringText[0] == '@' ? 2 : 1; start < stringText.Length - 1; start++) - { - TryParseTree( - stringText.Substring(0, start) + - stringText.Substring(start + 1, stringText.Length - (start + 1)), - conversionFailureOk: true, - allowDiagnosticsMismatch, - runReplaceTokens); - } - } - - private (SyntaxToken, RoutePatternTree, VirtualCharSequence) JustParseTree( - string stringText, bool conversionFailureOk, bool runReplaceTokens) - { - var token = GetStringToken(stringText); - var allChars = CSharpVirtualCharService.Instance.TryConvertToVirtualChars(token); - if (allChars.IsDefault) - { - Assert.True(conversionFailureOk, "Failed to convert text to token."); - return (token, null, allChars); - } - - var tree = RoutePatternParser.TryParse(allChars, supportTokenReplacement: runReplaceTokens); - return (token, tree, allChars); - } - - private (RoutePatternTree, SourceText) TryParseTree( - string stringText, - bool conversionFailureOk, - bool allowDiagnosticsMismatch = false, - bool runReplaceTokens = false) - { - var (token, tree, allChars) = JustParseTree(stringText, conversionFailureOk, runReplaceTokens); - if (tree == null) - { - Assert.True(allChars.IsDefault); - return default; - } - - CheckInvariants(tree, allChars); - var sourceText = token.SyntaxTree.GetText(); - var treeAndText = (tree, sourceText); - - Routing.Patterns.RoutePattern routePattern = null; - IReadOnlyList<RoutePatternParameterPart> parsedRoutePatterns = null; - try - { - routePattern = RoutePatternFactory.Parse(token.ValueText); - parsedRoutePatterns = routePattern.Parameters; - - if (runReplaceTokens) - { - AttributeRouteModel.ReplaceTokens(token.ValueText, new Dictionary<string, string> - { - ["controller"] = "TestController", - ["action"] = "TestAction" - }); - } - } - catch (Exception ex) - { - if (!allowDiagnosticsMismatch && - !Regex.IsMatch(ex.Message, "^While processing template '(.*?)', a replacement value for the token '(.*?)' could not be found.")) - { - if (tree.Diagnostics.Length == 0) - { - throw new Exception($"Parsing '{token.ValueText}' throws RoutePattern error '{ex.Message}'. No diagnostics."); - } - - // Ensure the diagnostic we emit is the same as the .NET one. Note: we can only - // do this in en as that's the only culture where we control the text exactly - // and can ensure it exactly matches RoutePattern. We depend on localization to do a - // good enough job here for other languages. - if (Thread.CurrentThread.CurrentCulture.Parent.Name == "en") - { - if (!tree.Diagnostics.Any(d => ex.Message.Contains(d.Message))) - { - throw new Exception( - $"Parsing '{token.ValueText}' throws RoutePattern error '{ex.Message}'. Error not found in diagnostics: " + Environment.NewLine + - string.Join(Environment.NewLine, tree.Diagnostics.Select(d => d.Message))); - } - } - } - - return treeAndText; - } - - if (!tree.Diagnostics.IsEmpty && !allowDiagnosticsMismatch) - { - var expectedDiagnostics = CreateDiagnosticsElement(sourceText, tree); - Assert.False(true, $"Parsing '{token.ValueText}' didn't throw an error for expected diagnostics: \r\n" + expectedDiagnostics.ToString().Replace(@"""", @"""""")); - } - - if (parsedRoutePatterns != null) - { - foreach (var parsedRoutePattern in parsedRoutePatterns) - { - try - { - if (tree.RouteParameters.TryGetValue(parsedRoutePattern.Name, out var routeParameter)) - { - Assert.True(routeParameter.IsOptional == parsedRoutePattern.IsOptional, "IsOptional"); - Assert.True(routeParameter.IsCatchAll == parsedRoutePattern.IsCatchAll, "IsCatchAll"); - Assert.True(routeParameter.EncodeSlashes == parsedRoutePattern.EncodeSlashes, "EncodeSlashes"); - Assert.True(Equals(routeParameter.DefaultValue, parsedRoutePattern.Default), "DefaultValue"); - Assert.True(routeParameter.Policies.Length == parsedRoutePattern.ParameterPolicies.Count, "ParameterPolicies"); - for (var i = 0; i < parsedRoutePattern.ParameterPolicies.Count; i++) - { - var expected = parsedRoutePattern.ParameterPolicies[i].Content; - var actual = routeParameter.Policies[i].Substring(1).Replace("{{", "{").Replace("}}", "}"); - Assert.True(expected == actual, $"Policy {i}. Expected: '{expected}', Actual: '{actual}'."); - } - } - else - { - throw new Exception($"Couldn't find parameter '{parsedRoutePattern.Name}'."); - } - } - catch (Exception ex) - { - throw new Exception($"Parsing '{token.ValueText}' has route parameter '{parsedRoutePattern.Name}' mismatch.", ex); - } - - } - - Assert.True( - parsedRoutePatterns.Count == tree.RouteParameters.Count, - $"Parsing '{token.ValueText}' has mismatched parameter counts."); - } - - //Assert.True(regex.GetGroupNumbers().OrderBy(v => v).SequenceEqual( - // tree.CaptureNumbersToSpan.Keys.OrderBy(v => v))); - - //Assert.True(regex.GetGroupNames().Where(v => !int.TryParse(v, out _)).OrderBy(v => v).SequenceEqual( - // tree.CaptureNamesToSpan.Keys.OrderBy(v => v))); - - return treeAndText; - } - - private static string TreeToText(SourceText text, RoutePatternTree tree) - { - var element = new XElement("Tree", - NodeToElement(tree.Root)); - - if (tree.Diagnostics.Length > 0) - { - element.Add(CreateDiagnosticsElement(text, tree)); - } - - element.Add(new XElement("Parameters", - tree.RouteParameters.OrderBy(kvp => kvp.Key).Select(kvp => CreateParameter(kvp.Value)))); - - return element.ToString(); - } - - private static XElement CreateParameter(RouteParameter parameter) - { - var parameterElement = new XElement("Parameter", - new XAttribute("Name", parameter.Name), - new XAttribute("IsCatchAll", parameter.IsCatchAll), - new XAttribute("IsOptional", parameter.IsOptional), - new XAttribute("EncodeSlashes", parameter.EncodeSlashes)); - if (parameter.DefaultValue != null) - { - parameterElement.Add(new XAttribute("DefaultValue", parameter.DefaultValue)); - } - foreach (var policy in parameter.Policies) - { - parameterElement.Add(new XElement("Policy", policy)); - } - - return parameterElement; - } - - private static XElement CreateDiagnosticsElement(SourceText text, RoutePatternTree tree) - => new XElement("Diagnostics", - tree.Diagnostics.Select(d => - new XElement("Diagnostic", - new XAttribute("Message", d.Message), - new XAttribute("Span", d.Span), - GetTextAttribute(text, d.Span)))); - - private static XAttribute GetTextAttribute(SourceText text, TextSpan span) - => new("Text", text.ToString(span)); - - private static XElement NodeToElement(RoutePatternNode node) - { - var element = new XElement(node.Kind.ToString()); - foreach (var child in node) - { - element.Add(child.IsNode ? NodeToElement(child.Node) : TokenToElement(child.Token)); - } - - return element; - } - - private static XElement TokenToElement(RoutePatternToken token) - { - var element = new XElement(token.Kind.ToString()); - - if (token.Value != null) - { - element.Add(new XAttribute("value", token.Value)); - } - - if (token.VirtualChars.Length > 0) - { - element.Add(token.VirtualChars.CreateString()); - } - - return element; - } - - private static void CheckInvariants(RoutePatternTree tree, VirtualCharSequence allChars) - { - var root = tree.Root; - var position = 0; - CheckInvariants(root, ref position, allChars); - Assert.Equal(allChars.Length, position); - } - - private static void CheckInvariants(RoutePatternNode node, ref int position, VirtualCharSequence allChars) - { - foreach (var child in node) - { - if (child.IsNode) - { - CheckInvariants(child.Node, ref position, allChars); - } - else - { - CheckInvariants(child.Token, ref position, allChars); - } - } - } - - private static void CheckInvariants(RoutePatternToken token, ref int position, VirtualCharSequence allChars) - { - CheckCharacters(token.VirtualChars, ref position, allChars); - } - - private static void CheckCharacters(VirtualCharSequence virtualChars, ref int position, VirtualCharSequence allChars) - { - for (var i = 0; i < virtualChars.Length; i++) - { - var expected = allChars[position + i]; - var actual = virtualChars[i]; - try - { - Assert.Equal(expected, actual); - } - catch (Exception ex) - { - var allCharsString = allChars.GetSubSequence(new TextSpan(position, virtualChars.Length)).CreateString(); - var virtualCharsString = virtualChars.CreateString(); - - throw new Exception($"When checking '{allChars.CreateString()}' there was a mismatch between '{allCharsString}' and '{virtualCharsString}' at index {i}.", ex); - } - } - - position += virtualChars.Length; - } - - private static string And(params string[] regexes) - { - var conj = $"({regexes[regexes.Length - 1]})"; - for (var i = regexes.Length - 2; i >= 0; i--) - { - conj = $"(?({regexes[i]}){conj}|[0-[0]])"; - } - return conj; - } - - private static string Not(string regex) - => $"(?({regex})[0-[0]]|.*)"; -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests_BasicTests.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests_BasicTests.cs deleted file mode 100644 index 67800bd79095..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests_BasicTests.cs +++ /dev/null @@ -1,1377 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -using System.Text.RegularExpressions; -using Xunit; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -// These tests were created by trying to enumerate all codepaths in the lexer/parser. -public partial class RoutePatternParserTests -{ - [Fact] - public void TestEmpty() - { - Test(@"""""", @"<Tree> - <CompilationUnit> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>"); - } - - [Fact] - public void TestSingleLiteral() - { - Test(@"""hello""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""hello"">hello</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>"); - } - - [Fact] - public void TestSingleLiteralWithQuestionMark() - { - Test(@"""hel?lo""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""hel?lo"">hel?lo</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""The literal section 'hel?lo' is invalid. Literal sections cannot contain the '?' character."" Span=""[9..15)"" Text=""hel?lo"" /> - </Diagnostics> - <Parameters /> -</Tree>"); - } - - [Fact] - public void TestSlashSeperatedLiterals() - { - Test(@"""hello/world""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""hello"">hello</Literal> - </Literal> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Literal> - <Literal value=""world"">world</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>"); - } - - [Fact] - public void TestDuplicateParameterNames() - { - Test(@"""{a}/{a}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""a"">a</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""a"">a</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""The route parameter name 'a' appears more than one time in the route template."" Span=""[13..16)"" Text=""{a}"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""a"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestSlashSeperatedSegments() - { - Test(@"""{a}/{b}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""a"">a</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""b"">b</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""a"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - <Parameter Name=""b"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestCatchAllParameterFollowedBySlash() - { - Test(@"""{*a}/""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <CatchAll> - <AsteriskToken>*</AsteriskToken> - </CatchAll> - <ParameterName> - <ParameterNameToken value=""a"">a</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""a"" IsCatchAll=""true"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestCatchAllParameterNotLast() - { - Test(@"""{*a}/{b}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <CatchAll> - <AsteriskToken>*</AsteriskToken> - </CatchAll> - <ParameterName> - <ParameterNameToken value=""a"">a</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""b"">b</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""A catch-all parameter can only appear as the last segment of the route template."" Span=""[9..13)"" Text=""{*a}"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""a"" IsCatchAll=""true"" IsOptional=""false"" EncodeSlashes=""true"" /> - <Parameter Name=""b"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestCatchAllAndOptional() - { - Test(@"""{*a?}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <CatchAll> - <AsteriskToken>*</AsteriskToken> - </CatchAll> - <ParameterName> - <ParameterNameToken value=""a"">a</ParameterNameToken> - </ParameterName> - <Optional> - <QuestionMarkToken>?</QuestionMarkToken> - </Optional> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""A catch-all parameter cannot be marked optional."" Span=""[9..14)"" Text=""{*a?}"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""a"" IsCatchAll=""true"" IsOptional=""true"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestCatchAllParameterComplexSegment() - { - Test(@"""a{*a}""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""a"">a</Literal> - </Literal> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <CatchAll> - <AsteriskToken>*</AsteriskToken> - </CatchAll> - <ParameterName> - <ParameterNameToken value=""a"">a</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter."" Span=""[10..14)"" Text=""{*a}"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""a"" IsCatchAll=""true"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestPeriodSeperatedLiterals() - { - Test(@"""hello.world""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""hello.world"">hello.world</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>"); - } - - [Fact] - public void TestSimpleParameter() - { - Test(@"""{id}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterWithPolicy() - { - Test(@"""{id:foo}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <ParameterPolicy> - <ColonToken>:</ColonToken> - <PolicyFragment> - <PolicyFragmentToken value=""foo"">foo</PolicyFragmentToken> - </PolicyFragment> - </ParameterPolicy> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true""> - <Policy>:foo</Policy> - </Parameter> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterWithDefault() - { - Test(@"""{id=Home}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <DefaultValue> - <EqualsToken>=</EqualsToken> - <DefaultValueToken value=""Home"">Home</DefaultValueToken> - </DefaultValue> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" DefaultValue=""Home"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterWithDefaultContainingPolicyChars() - { - Test(@"""{id=Home=Controller:int()}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <DefaultValue> - <EqualsToken>=</EqualsToken> - <DefaultValueToken value=""Home=Controller:int()"">Home=Controller:int()</DefaultValueToken> - </DefaultValue> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" DefaultValue=""Home=Controller:int()"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterWithPolicyArgument() - { - Test(@"""{id:foo(wee)}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <ParameterPolicy> - <ColonToken>:</ColonToken> - <PolicyFragment> - <PolicyFragmentToken value=""foo"">foo</PolicyFragmentToken> - </PolicyFragment> - <PolicyFragmentEscaped> - <OpenParenToken>(</OpenParenToken> - <PolicyFragmentToken value=""wee"">wee</PolicyFragmentToken> - <CloseParenToken>)</CloseParenToken> - </PolicyFragmentEscaped> - </ParameterPolicy> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true""> - <Policy>:foo(wee)</Policy> - </Parameter> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterWithPolicyArgumentEmpty() - { - Test(@"""{id:foo()}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <ParameterPolicy> - <ColonToken>:</ColonToken> - <PolicyFragment> - <PolicyFragmentToken value=""foo"">foo</PolicyFragmentToken> - </PolicyFragment> - <PolicyFragmentEscaped> - <OpenParenToken>(</OpenParenToken> - <PolicyFragmentToken value="""" /> - <CloseParenToken>)</CloseParenToken> - </PolicyFragmentEscaped> - </ParameterPolicy> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true""> - <Policy>:foo()</Policy> - </Parameter> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterOptional() - { - Test(@"""{id?}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <Optional> - <QuestionMarkToken>?</QuestionMarkToken> - </Optional> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""true"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterDefaultValue() - { - Test(@"""{id=Home}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <DefaultValue> - <EqualsToken>=</EqualsToken> - <DefaultValueToken value=""Home"">Home</DefaultValueToken> - </DefaultValue> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" DefaultValue=""Home"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterDefaultValueAndOptional() - { - Test(@"""{id=Home?}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <DefaultValue> - <EqualsToken>=</EqualsToken> - <DefaultValueToken value=""Home"">Home</DefaultValueToken> - </DefaultValue> - <Optional> - <QuestionMarkToken>?</QuestionMarkToken> - </Optional> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""An optional parameter cannot have default value."" Span=""[9..19)"" Text=""{id=Home?}"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""true"" EncodeSlashes=""true"" DefaultValue=""Home"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterQuestionMarkBeforeEscapedClose() - { - Test(@"""{id?}}}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id?}}"">id?}}</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""The route parameter name 'id?}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all, and can occur only at the start of the parameter."" Span=""[10..15)"" Text=""id?}}"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""id?}}"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestUnbalancedBracesInComplexSegment() - { - Test(@"""a{foob{bar}c""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""a"">a</Literal> - </Literal> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""foob{bar"">foob{bar</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - <Literal> - <Literal value=""c"">c</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""In a route parameter, '{' and '}' must be escaped with '{{' and '}}'."" Span=""[11..19)"" Text=""foob{bar"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""foob{bar"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestComplexSegment() - { - Test(@"""a{foo}b{bar}c""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""a"">a</Literal> - </Literal> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""foo"">foo</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - <Literal> - <Literal value=""b"">b</Literal> - </Literal> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""bar"">bar</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - <Literal> - <Literal value=""c"">c</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""bar"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - <Parameter Name=""foo"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestConsecutiveParameters() - { - Test(@"""{a}{b}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""a"">a</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""b"">b</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string."" Span=""[12..15)"" Text=""{b}"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""a"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - <Parameter Name=""b"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestUnescapedOpenBrace() - { - Test(@"""{a{b}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""a{b"">a{b</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""In a route parameter, '{' and '}' must be escaped with '{{' and '}}'."" Span=""[10..13)"" Text=""a{b"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""a{b"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestInvalidCharsAndUnescapedOpenBrace() - { - Test(@"""{a/{b}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""a/{b"">a/{b</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""In a route parameter, '{' and '}' must be escaped with '{{' and '}}'."" Span=""[10..14)"" Text=""a/{b"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""a/{b"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterWithPolicyAndOptional() - { - Test(@"""{id:foo?}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <ParameterPolicy> - <ColonToken>:</ColonToken> - <PolicyFragment> - <PolicyFragmentToken value=""foo"">foo</PolicyFragmentToken> - </PolicyFragment> - </ParameterPolicy> - <Optional> - <QuestionMarkToken>?</QuestionMarkToken> - </Optional> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""true"" EncodeSlashes=""true""> - <Policy>:foo</Policy> - </Parameter> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterWithMultiplePolicies() - { - Test(@"""{id:foo:bar}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <ParameterPolicy> - <ColonToken>:</ColonToken> - <PolicyFragment> - <PolicyFragmentToken value=""foo"">foo</PolicyFragmentToken> - </PolicyFragment> - </ParameterPolicy> - <ParameterPolicy> - <ColonToken>:</ColonToken> - <PolicyFragment> - <PolicyFragmentToken value=""bar"">bar</PolicyFragmentToken> - </PolicyFragment> - </ParameterPolicy> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true""> - <Policy>:foo</Policy> - <Policy>:bar</Policy> - </Parameter> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestPolicyWithEscapedFragmentParameterIncomplete() - { - Test(@"""{id:foo(hi""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <ParameterPolicy> - <ColonToken>:</ColonToken> - <PolicyFragment> - <PolicyFragmentToken value=""foo(hi"">foo(hi</PolicyFragmentToken> - </PolicyFragment> - </ParameterPolicy> - <CloseBraceToken /> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character."" Span=""[19..19)"" Text="""" /> - </Diagnostics> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true""> - <Policy>:foo(hi</Policy> - </Parameter> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestPolicyWithEscapedFragmentIncomplete() - { - Test(@"""{id:foo(hi}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <ParameterPolicy> - <ColonToken>:</ColonToken> - <PolicyFragment> - <PolicyFragmentToken value=""foo(hi"">foo(hi</PolicyFragmentToken> - </PolicyFragment> - </ParameterPolicy> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true""> - <Policy>:foo(hi</Policy> - </Parameter> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestPolicyWithMultipleFragments() - { - Test(@"""{id:foo(hi)bar}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <ParameterPolicy> - <ColonToken>:</ColonToken> - <PolicyFragment> - <PolicyFragmentToken value=""foo"">foo</PolicyFragmentToken> - </PolicyFragment> - <PolicyFragmentEscaped> - <OpenParenToken>(</OpenParenToken> - <PolicyFragmentToken value=""hi"">hi</PolicyFragmentToken> - <CloseParenToken>)</CloseParenToken> - </PolicyFragmentEscaped> - <PolicyFragment> - <PolicyFragmentToken value=""bar"">bar</PolicyFragmentToken> - </PolicyFragment> - </ParameterPolicy> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true""> - <Policy>:foo(hi)bar</Policy> - </Parameter> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestCatchAllParameter() - { - Test(@"""{*id}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <CatchAll> - <AsteriskToken>*</AsteriskToken> - </CatchAll> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""true"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestCatchAllUnescapedParameter() - { - Test(@"""{**id}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <CatchAll> - <AsteriskToken>**</AsteriskToken> - </CatchAll> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""true"" IsOptional=""false"" EncodeSlashes=""false"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestEmptyParameter() - { - Test(@"""{}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken /> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all, and can occur only at the start of the parameter."" Span=""[10..11)"" Text=""}"" /> - </Diagnostics> - <Parameters /> -</Tree>"); - } - - [Fact] - public void TestParameterWithEscapedPolicyArgument() - { - Test(@"""{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""ssn"">ssn</ParameterNameToken> - </ParameterName> - <ParameterPolicy> - <ColonToken>:</ColonToken> - <PolicyFragment> - <PolicyFragmentToken value=""regex"">regex</PolicyFragmentToken> - </PolicyFragment> - <PolicyFragmentEscaped> - <OpenParenToken>(</OpenParenToken> - <PolicyFragmentToken value=""^\d{{3}}-\d{{2}}-\d{{4}}$"">^\d{{3}}-\d{{2}}-\d{{4}}$</PolicyFragmentToken> - <CloseParenToken>)</CloseParenToken> - </PolicyFragmentEscaped> - </ParameterPolicy> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""ssn"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true""> - <Policy>:regex(^\d{{3}}-\d{{2}}-\d{{4}}$)</Policy> - </Parameter> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterWithEscapedPolicyArgumentIncomplete() - { - Test(@"""{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""ssn"">ssn</ParameterNameToken> - </ParameterName> - <ParameterPolicy> - <ColonToken>:</ColonToken> - <PolicyFragment> - <PolicyFragmentToken value=""regex(^\d{{3}}-\d{{2}}-\d{{4"">regex(^\d{{3}}-\d{{2}}-\d{{4</PolicyFragmentToken> - </PolicyFragment> - </ParameterPolicy> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""ssn"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true""> - <Policy>:regex(^\d{{3}}-\d{{2}}-\d{{4</Policy> - </Parameter> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterWithOpenBraceInEscapedPolicyArgument() - { - Test(@"""{ssn:regex(^\\d{3}})}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""ssn"">ssn</ParameterNameToken> - </ParameterName> - <ParameterPolicy> - <ColonToken>:</ColonToken> - <PolicyFragment> - <PolicyFragmentToken value=""regex"">regex</PolicyFragmentToken> - </PolicyFragment> - <PolicyFragmentEscaped> - <OpenParenToken>(</OpenParenToken> - <PolicyFragmentToken value=""^\d{3}}"">^\d{3}}</PolicyFragmentToken> - <CloseParenToken>)</CloseParenToken> - </PolicyFragmentEscaped> - </ParameterPolicy> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""In a route parameter, '{' and '}' must be escaped with '{{' and '}}'."" Span=""[20..28)"" Text=""^\\d{3}}"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""ssn"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true""> - <Policy>:regex(^\d{3}})</Policy> - </Parameter> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterWithInvalidName() - { - Test(@"""{3}}-\\d{{2}}-\\d{{4}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""3}}-\d{{2}}-\d{{4"">3}}-\d{{2}}-\d{{4</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""The route parameter name '3}-\d{2}-\d{4' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all, and can occur only at the start of the parameter."" Span=""[10..29)"" Text=""3}}-\\d{{2}}-\\d{{4"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""3}}-\d{{2}}-\d{{4"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestInvalidCloseBrace() - { - Test(@"""-\\d{{2}}-\\d{{4}""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""-\d{{2}}-\d{{4}"">-\d{{2}}-\d{{4}</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character."" Span=""[9..26)"" Text=""-\\d{{2}}-\\d{{4}"" /> - </Diagnostics> - <Parameters /> -</Tree>"); - } - - [Fact] - public void TestEscapedBraces() - { - Test(@"""{{2}}""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""{{2}}"">{{2}}</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>"); - } - - [Fact] - public void TestInvalidCloseBrace2() - { - Test(@"""{2}}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""2}}"">2}}</ParameterNameToken> - </ParameterName> - <CloseBraceToken /> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""The route parameter name '2}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all, and can occur only at the start of the parameter."" Span=""[10..13)"" Text=""2}}"" /> - <Diagnostic Message=""There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character."" Span=""[13..13)"" Text="""" /> - </Diagnostics> - <Parameters> - <Parameter Name=""2}}"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestOptionalParameterPrecededByParameter() - { - Test(@"""{p1}{p2?}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""p1"">p1</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""p2"">p2</ParameterNameToken> - </ParameterName> - <Optional> - <QuestionMarkToken>?</QuestionMarkToken> - </Optional> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""In the segment '{p1}{p2?}', the optional parameter 'p2' is preceded by an invalid segment '{p1}'. Only a period (.) can precede an optional parameter."" Span=""[13..18)"" Text=""{p2?}"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""p1"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - <Parameter Name=""p2"" IsCatchAll=""false"" IsOptional=""true"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestOptionalParameterPrecededByLiteral() - { - 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> - <Diagnostics> - <Diagnostic Message=""In the segment '{p1}-{p2?}', the optional parameter 'p2' is preceded by an invalid segment '-'. Only a period (.) can precede an optional parameter."" Span=""[14..19)"" Text=""{p2?}"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""p1"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - <Parameter Name=""p2"" IsCatchAll=""false"" IsOptional=""true"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterColonStart() - { - Test(@"""{:hi}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value="":hi"">:hi</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name="":hi"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestParameterCatchAllColonStart() - { - Test(@"""{**:hi}""", @"<Tree> - <CompilationUnit> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <CatchAll> - <AsteriskToken>**</AsteriskToken> - </CatchAll> - <ParameterName> - <ParameterNameToken value="":hi"">:hi</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name="":hi"" IsCatchAll=""true"" IsOptional=""false"" EncodeSlashes=""false"" /> - </Parameters> -</Tree>"); - } - - [Fact] - public void TestTilde() - { - Test(@"""~""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""~"">~</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""The route template cannot start with a '~' character unless followed by a '/'."" Span=""[9..10)"" Text=""~"" /> - </Diagnostics> - <Parameters /> -</Tree>"); - } - - [Fact] - public void TestTwoTildes() - { - Test(@"""~~""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""~~"">~~</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""The route template cannot start with a '~' character unless followed by a '/'."" Span=""[9..11)"" Text=""~~"" /> - </Diagnostics> - <Parameters /> -</Tree>"); - } - - [Fact] - public void TestTildeSlash() - { - Test(@"""~/""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""~"">~</Literal> - </Literal> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>"); - } - - [Fact] - public void TestTildeParameter() - { - Test(@"""~{id}""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""~"">~</Literal> - </Literal> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""The route template cannot start with a '~' character unless followed by a '/'."" Span=""[9..10)"" Text=""~"" /> - </Diagnostics> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>"); - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests_ConformanceTests.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests_ConformanceTests.cs deleted file mode 100644 index cf6494b9198c..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests_ConformanceTests.cs +++ /dev/null @@ -1,818 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -using System.Text.RegularExpressions; -using Xunit; - -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> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Literal> - <Literal value=""awesome"">awesome</Literal> - </Literal> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <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> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""p2"">p2</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <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> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <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> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <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> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <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.RouteParameters["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)); - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests_ReplacementTests.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests_ReplacementTests.cs deleted file mode 100644 index 9325ee7c8270..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternParserTests_ReplacementTests.cs +++ /dev/null @@ -1,503 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -using System.Text.RegularExpressions; -using Xunit; - -namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; - -// These tests are mirrored from routing's AttributeRouteModelTests.cs -public partial class RoutePatternParserTests -{ - [Fact] - public void TestReplacement() - { - Test(@"""[controller]""", @"<Tree> - <CompilationUnit> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""controller"">controller</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>", runReplaceTokens: true); - } - - [Fact] - public void TestEscapedReplacement() - { - Test(@"""[[controller]]""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""[[controller]]"">[[controller]]</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>", runReplaceTokens: true); - } - - [Fact] - public void TestIncompleteReplacement() - { - Test(@"""[controller""", @"<Tree> - <CompilationUnit> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""controller"">controller</ReplacementToken> - <CloseBracketToken /> - </Replacement> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""A replacement token is not closed."" Span=""[20..20)"" Text="""" /> - </Diagnostics> - <Parameters /> -</Tree>", runReplaceTokens: true); - } - - [Fact] - public void TestOpenBracketInReplacement() - { - Test(@"""[cont[controller]""", @"<Tree> - <CompilationUnit> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""cont[controller"">cont[controller</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""An unescaped '[' token is not allowed inside of a replacement token. Use '[[' to escape."" Span=""[10..25)"" Text=""cont[controller"" /> - </Diagnostics> - <Parameters /> -</Tree>", runReplaceTokens: true); - } - - [Fact] - public void TestEmptyReplacement() - { - Test(@"""[]""", @"<Tree> - <CompilationUnit> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken /> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""An empty replacement token ('[]') is not allowed."" Span=""[10..11)"" Text=""]"" /> - </Diagnostics> - <Parameters /> -</Tree>", runReplaceTokens: true); - } - - [Fact] - public void TestEndReplacement() - { - Test(@"""]""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""]"">]</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""Token delimiters ('[', ']') are imbalanced."" Span=""[9..10)"" Text=""]"" /> - </Diagnostics> - <Parameters /> -</Tree>", runReplaceTokens: true); - } - - [Fact] - public void TestRepeatedReplacement() - { - Test(@"""[one][two]""", @"<Tree> - <CompilationUnit> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""one"">one</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""two"">two</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>", runReplaceTokens: true); - } - - [Fact] - public void TestMultipleReplacements() - { - Test(@"""[controller]/[action]""", @"<Tree> - <CompilationUnit> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""controller"">controller</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""action"">action</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>", runReplaceTokens: true); - } - - [Fact] - public void TestReplacementThenEscapedBracket() - { - Test(@"""[controller][[""", @"<Tree> - <CompilationUnit> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""controller"">controller</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - <Literal> - <Literal value=""[["">[[</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>", runReplaceTokens: true); - } - - [Fact] - public void TestLiteralThenReplacement() - { - Test(@"""thisisSomeText[action]""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""thisisSomeText"">thisisSomeText</Literal> - </Literal> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""action"">action</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>", runReplaceTokens: true); - } - - [Fact] - public void TestMultipleTokenEscapes() - { - Test(@"""[[-]][[/[[controller]]""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""[[-]][["">[[-]][[</Literal> - </Literal> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Literal> - <Literal value=""[[controller]]"">[[controller]]</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>", runReplaceTokens: true, allowDiagnosticsMismatch: true); - } - - [Fact] - public void TestReplacementContainingEscapedBackets() - { - Test(@"""[contr[[oller]/[act]]ion]""", @"<Tree> - <CompilationUnit> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""contr[[oller"">contr[[oller</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""act]]ion"">act]]ion</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>", runReplaceTokens: true, allowDiagnosticsMismatch: true); - } - - [Fact] - public void TestReplacementContainingBraces() - { - Test(@"""[contr}oller]/[act{ion]/{id}""", @"<Tree> - <CompilationUnit> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""contr}oller"">contr}oller</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""act{ion"">act{ion</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Parameter> - <OpenBraceToken>{</OpenBraceToken> - <ParameterName> - <ParameterNameToken value=""id"">id</ParameterNameToken> - </ParameterName> - <CloseBraceToken>}</CloseBraceToken> - </Parameter> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters> - <Parameter Name=""id"" IsCatchAll=""false"" IsOptional=""false"" EncodeSlashes=""true"" /> - </Parameters> -</Tree>", runReplaceTokens: true, allowDiagnosticsMismatch: true); - } - - [Fact] - public void TestReplacementInEscapedBrackets() - { - Test(@"""[controller]/[[[action]]]/id""", @"<Tree> - <CompilationUnit> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""controller"">controller</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Literal> - <Literal value=""[["">[[</Literal> - </Literal> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""action"">action</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - <Literal> - <Literal value=""]]"">]]</Literal> - </Literal> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Literal> - <Literal value=""id"">id</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>", runReplaceTokens: true, allowDiagnosticsMismatch: true); - } - - [Fact] - public void TestReplacementInEscapedBrackets2() - { - Test(@"""[controller]/[[[[[action]]]]]/id""", @"<Tree> - <CompilationUnit> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""controller"">controller</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Literal> - <Literal value=""[[[["">[[[[</Literal> - </Literal> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""action"">action</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - <Literal> - <Literal value=""]]]]"">]]]]</Literal> - </Literal> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Literal> - <Literal value=""id"">id</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>", runReplaceTokens: true, allowDiagnosticsMismatch: true); - } - - [Fact] - public void TestReplacementInEscapedBrackets3() - { - Test(@"""[controller]/[[[[[[[action]]]]]]]/id""", @"<Tree> - <CompilationUnit> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""controller"">controller</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Literal> - <Literal value=""[[[[[["">[[[[[[</Literal> - </Literal> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""action"">action</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - <Literal> - <Literal value=""]]]]]]"">]]]]]]</Literal> - </Literal> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Literal> - <Literal value=""id"">id</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>", runReplaceTokens: true, allowDiagnosticsMismatch: true); - } - - [Fact] - public void TestReplacementInEscapedBrackets4() - { - Test(@"""[controller]/[[[[[action]]]]]]]/id""", @"<Tree> - <CompilationUnit> - <Segment> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""controller"">controller</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Literal> - <Literal value=""[[[["">[[[[</Literal> - </Literal> - <Replacement> - <OpenBracketToken>[</OpenBracketToken> - <ReplacementToken value=""action"">action</ReplacementToken> - <CloseBracketToken>]</CloseBracketToken> - </Replacement> - <Literal> - <Literal value=""]]]]]]"">]]]]]]</Literal> - </Literal> - </Segment> - <Seperator> - <SlashToken>/</SlashToken> - </Seperator> - <Segment> - <Literal> - <Literal value=""id"">id</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Parameters /> -</Tree>", runReplaceTokens: true, allowDiagnosticsMismatch: true); - } - - [Fact] - public void TestOpenBracketInLiteral() - { - Test(@"""controller]""", @"<Tree> - <CompilationUnit> - <Segment> - <Literal> - <Literal value=""controller]"">controller]</Literal> - </Literal> - </Segment> - <EndOfFile /> - </CompilationUnit> - <Diagnostics> - <Diagnostic Message=""Token delimiters ('[', ']') are imbalanced."" Span=""[9..20)"" Text=""controller]"" /> - </Diagnostics> - <Parameters /> -</Tree>", runReplaceTokens: true); - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/test/TestDiagnosticAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/test/TestDiagnosticAnalyzer.cs index e52439e9a41a..bc8d2f3f5426 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/TestDiagnosticAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/test/TestDiagnosticAnalyzer.cs @@ -3,21 +3,12 @@ using System.Reflection; using Microsoft.AspNetCore.Analyzer.Testing; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Classification; -using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.EmbeddedLanguages; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.Composition; namespace Microsoft.AspNetCore.Analyzers; -internal class TestDiagnosticAnalyzerRunner : DiagnosticAnalyzerRunner +public class TestDiagnosticAnalyzerRunner : DiagnosticAnalyzerRunner { public TestDiagnosticAnalyzerRunner(DiagnosticAnalyzer analyzer) { @@ -26,50 +17,6 @@ public TestDiagnosticAnalyzerRunner(DiagnosticAnalyzer analyzer) public DiagnosticAnalyzer Analyzer { get; } - public async Task<ClassifiedSpan[]> GetClassificationSpansAsync(TextSpan textSpan, params string[] sources) - { - var project = CreateProjectWithReferencesInBinDir(GetType().Assembly, sources); - var doc = project.Solution.GetDocument(project.Documents.First().Id); - - var result = await Classifier.GetClassifiedSpansAsync(doc, textSpan, CancellationToken.None); - - return result.ToArray(); - } - - public async Task<CompletionResult> GetCompletionsAndServiceAsync(int caretPosition, params string[] sources) - { - var project = CreateProjectWithReferencesInBinDir(GetType().Assembly, sources); - var doc = project.Solution.GetDocument(project.Documents.First().Id); - - var completionService = CompletionService.GetService(doc); - var result = await completionService.GetCompletionsAsync(doc, caretPosition, CompletionTrigger.Invoke); - - return new(doc, completionService, result); - } - - public async Task<AspNetCoreBraceMatchingResult?> GetBraceMatchesAsync(int caretPosition, params string[] sources) - { - var project = CreateProjectWithReferencesInBinDir(GetType().Assembly, sources); - var doc = project.Solution.GetDocument(project.Documents.First().Id); - - var (_, token, model) = await RouteStringSyntaxDetectorDocument.TryGetStringSyntaxTokenAtPositionAsync(doc, caretPosition, CancellationToken.None); - var braceMatcher = new RoutePatternBraceMatcher(); - - return braceMatcher.FindBraces(model, token, caretPosition, CancellationToken.None); - } - - public async Task<List<AspNetCoreHighlightSpan>> GetHighlightingAsync(int caretPosition, params string[] sources) - { - var project = CreateProjectWithReferencesInBinDir(GetType().Assembly, sources); - var doc = project.Solution.GetDocument(project.Documents.First().Id); - - var (_, token, model) = await RouteStringSyntaxDetectorDocument.TryGetStringSyntaxTokenAtPositionAsync(doc, caretPosition, CancellationToken.None); - var highlighter = new RoutePatternHighlighter(); - - var highlights = highlighter.GetDocumentHighlights(model, token, caretPosition, CancellationToken.None); - return highlights.SelectMany(h => h.HighlightSpans).ToList(); - } - public Task<Diagnostic[]> GetDiagnosticsAsync(params string[] sources) { var project = CreateProjectWithReferencesInBinDir(GetType().Assembly, sources); @@ -77,47 +24,13 @@ public Task<Diagnostic[]> GetDiagnosticsAsync(params string[] sources) return GetDiagnosticsAsync(project); } - private static readonly Lazy<IExportProviderFactory> ExportProviderFactory; - - static TestDiagnosticAnalyzerRunner() - { - ExportProviderFactory = new Lazy<IExportProviderFactory>( - () => - { -#pragma warning disable VSTHRD011 // Use AsyncLazy<T> -#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - var assemblies = MefHostServices.DefaultAssemblies.ToList(); - assemblies.Add(RoutePatternClassifier.TestAccessor.ExternalAccessAssembly); - - var discovery = new AttributedPartDiscovery(Resolver.DefaultInstance, isNonPublicSupported: true); - var parts = Task.Run(() => discovery.CreatePartsAsync(assemblies)).GetAwaiter().GetResult(); - var catalog = ComposableCatalog.Create(Resolver.DefaultInstance).AddParts(parts); //.WithDocumentTextDifferencingService(); - - var configuration = CompositionConfiguration.Create(catalog); - var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration); - return runtimeComposition.CreateExportProviderFactory(); -#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits -#pragma warning restore VSTHRD011 // Use AsyncLazy<T> - }, - LazyThreadSafetyMode.ExecutionAndPublication); - } - - private static AdhocWorkspace CreateWorkspace() - { - var exportProvider = ExportProviderFactory.Value.CreateExportProvider(); - var host = MefHostServices.Create(exportProvider.AsCompositionContext()); - return new AdhocWorkspace(host); - } - public static Project CreateProjectWithReferencesInBinDir(Assembly testAssembly, params string[] source) { // The deps file in the project is incorrect and does not contain "compile" nodes for some references. // However these binaries are always present in the bin output. As a "temporary" workaround, we'll add // every dll file that's present in the test's build output as a metadatareference. - Func<Workspace> createWorkspace = CreateWorkspace; - - var project = DiagnosticProject.Create(testAssembly, source, createWorkspace, typeof(RoutePatternClassifier)); + var project = DiagnosticProject.Create(testAssembly, source); foreach (var assembly in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.dll")) { if (!project.MetadataReferences.Any(c => string.Equals(Path.GetFileNameWithoutExtension(c.Display), Path.GetFileNameWithoutExtension(assembly), StringComparison.OrdinalIgnoreCase))) @@ -139,5 +52,3 @@ protected override CompilationOptions ConfigureCompilationOptions(CompilationOpt return options.WithOutputKind(OutputKind.ConsoleApplication); } } - -public record CompletionResult(Document Document, CompletionService Service, CompletionList Completions); diff --git a/src/Framework/Framework.slnf b/src/Framework/Framework.slnf index 5b476b5bf07a..d3f3abdc9f19 100644 --- a/src/Framework/Framework.slnf +++ b/src/Framework/Framework.slnf @@ -19,11 +19,11 @@ "src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj", "src\\FileProviders\\Embedded\\src\\Microsoft.Extensions.FileProviders.Embedded.csproj", "src\\FileProviders\\Manifest.MSBuildTask\\src\\Microsoft.Extensions.FileProviders.Embedded.Manifest.Task.csproj", - "src\\Framework\\App.Ref\\src\\Microsoft.AspNetCore.App.Ref.csproj", - "src\\Framework\\App.Runtime\\src\\Microsoft.AspNetCore.App.Runtime.csproj", "src\\Framework\\AspNetCoreAnalyzers\\src\\Analyzers\\Microsoft.AspNetCore.App.Analyzers.csproj", "src\\Framework\\AspNetCoreAnalyzers\\src\\CodeFixes\\Microsoft.AspNetCore.App.CodeFixes.csproj", "src\\Framework\\AspNetCoreAnalyzers\\test\\Microsoft.AspNetCore.App.Analyzers.Test.csproj", + "src\\Framework\\App.Ref\\src\\Microsoft.AspNetCore.App.Ref.csproj", + "src\\Framework\\App.Runtime\\src\\Microsoft.AspNetCore.App.Runtime.csproj", "src\\Framework\\test\\Microsoft.AspNetCore.App.UnitTests.csproj", "src\\HealthChecks\\Abstractions\\src\\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj", "src\\HealthChecks\\HealthChecks\\src\\Microsoft.Extensions.Diagnostics.HealthChecks.csproj", @@ -47,8 +47,6 @@ "src\\Identity\\Extensions.Core\\src\\Microsoft.Extensions.Identity.Core.csproj", "src\\Identity\\Extensions.Stores\\src\\Microsoft.Extensions.Identity.Stores.csproj", "src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj", - "src\\Localization\\Abstractions\\src\\Microsoft.Extensions.Localization.Abstractions.csproj", - "src\\Localization\\Localization\\src\\Microsoft.Extensions.Localization.csproj", "src\\Middleware\\CORS\\src\\Microsoft.AspNetCore.Cors.csproj", "src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj", "src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj", @@ -94,7 +92,6 @@ "src\\Servers\\IIS\\IIS\\src\\Microsoft.AspNetCore.Server.IIS.csproj", "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj", "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj", - "src\\Servers\\Kestrel\\Transport.Quic\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.csproj", "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj", "src\\SignalR\\common\\Http.Connections.Common\\src\\Microsoft.AspNetCore.Http.Connections.Common.csproj", "src\\SignalR\\common\\Http.Connections\\src\\Microsoft.AspNetCore.Http.Connections.csproj", diff --git a/src/Mvc/Mvc.Core/src/ApplicationModels/AttributeRouteModel.cs b/src/Mvc/Mvc.Core/src/ApplicationModels/AttributeRouteModel.cs index bb6feb68a7d5..b07f5fe381fd 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationModels/AttributeRouteModel.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationModels/AttributeRouteModel.cs @@ -255,7 +255,7 @@ private static bool IsEmptyLeftSegment(string? template) /// <param name="template">The template.</param> /// <param name="values">The token values to use.</param> /// <returns>A new string with the replaced values.</returns> - public static string ReplaceTokens([StringSyntax("Route")] string template, IDictionary<string, string?> values) + public static string ReplaceTokens(string template, IDictionary<string, string?> values) { return ReplaceTokens(template, values, routeTokenTransformer: null); }