diff --git a/src/Razor/Directory.Build.targets b/src/Razor/Directory.Build.targets index 78626b773e..181bfc9f70 100644 --- a/src/Razor/Directory.Build.targets +++ b/src/Razor/Directory.Build.targets @@ -1,8 +1,8 @@ - $(MicrosoftNETCoreApp20PackageVersion) $(MicrosoftNETCoreApp21PackageVersion) $(MicrosoftNETCoreApp22PackageVersion) + $(MicrosoftNETCoreApp30PackageVersion) $(NETStandardLibrary20PackageVersion) 99.9 diff --git a/src/Razor/NuGetPackageVerifier.json b/src/Razor/NuGetPackageVerifier.json index b62352e8b9..ceaad8c670 100644 --- a/src/Razor/NuGetPackageVerifier.json +++ b/src/Razor/NuGetPackageVerifier.json @@ -18,54 +18,53 @@ "DefaultCompositeRule" ], "packages": { - "Microsoft.AspNetCore.Razor.Design": { - "exclusions": { + "Microsoft.NET.Sdk.Razor": { + "Exclusions": { "ASSEMBLY_DESCRIPTION": { - "tools/Microsoft.CodeAnalysis.CSharp.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/Microsoft.CodeAnalysis.dll": "This assembly is not owned by us and does not follow our conventions." - }, - "NEUTRAL_RESOURCES_LANGUAGE": { - "tools/Microsoft.CodeAnalysis.CSharp.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/Microsoft.CodeAnalysis.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/Newtonsoft.Json.dll": "This assembly is not owned by us and does not follow our conventions." + "tools/netcoreapp3.0/Microsoft.CodeAnalysis.CSharp.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/Microsoft.CodeAnalysis.dll": "This assembly is not owned by us and does not follow our conventions." }, "ASSEMBLY_PRODUCT": { - "tools/Microsoft.CodeAnalysis.CSharp.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/Microsoft.CodeAnalysis.dll": "This assembly is not owned by us and does not follow our conventions." + "tools/netcoreapp3.0/Microsoft.CodeAnalysis.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/Microsoft.CodeAnalysis.CSharp.dll": "This assembly is not owned by us and does not follow our conventions." + }, + "NEUTRAL_RESOURCES_LANGUAGE": { + "tools/netcoreapp3.0/Microsoft.CodeAnalysis.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/Microsoft.CodeAnalysis.CSharp.dll": "This assembly is not owned by us and does not follow our conventions." }, "SERVICING_ATTRIBUTE": { - "tools/Newtonsoft.Json.dll": "This assembly is not owned by us and does not follow our conventions." - }, - "VERSION_INFORMATIONALVERSION": { - "tools/Newtonsoft.Json.dll": "This assembly is not owned by us and does not follow our conventions." + "tools/netcoreapp3.0/Newtonsoft.Json.dll": "This assembly is not owned by us and does not follow our conventions." }, "WRONG_PUBLICKEYTOKEN": { - "tools/Microsoft.CodeAnalysis.CSharp.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/Microsoft.CodeAnalysis.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/Newtonsoft.Json.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/runtimes/unix/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/runtimes/win/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions." + "tools/netcoreapp3.0/Microsoft.CodeAnalysis.CSharp.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/Microsoft.CodeAnalysis.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/Newtonsoft.Json.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/runtimes/unix/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/runtimes/win/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions." }, "ASSEMBLY_INFORMATIONAL_VERSION_MISMATCH": { - "tools/Microsoft.CodeAnalysis.CSharp.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/Microsoft.CodeAnalysis.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/Newtonsoft.Json.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/runtimes/unix/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/runtimes/win/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions." + "tools/netcoreapp3.0/Microsoft.CodeAnalysis.CSharp.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/Microsoft.CodeAnalysis.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/Newtonsoft.Json.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/runtimes/unix/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/runtimes/win/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions." }, "ASSEMBLY_FILE_VERSION_MISMATCH": { - "tools/Microsoft.CodeAnalysis.CSharp.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/Microsoft.CodeAnalysis.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/Newtonsoft.Json.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/runtimes/unix/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/runtimes/win/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions." + "tools/netcoreapp3.0/Microsoft.CodeAnalysis.CSharp.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/Microsoft.CodeAnalysis.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/Newtonsoft.Json.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/runtimes/unix/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/runtimes/win/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions." }, "ASSEMBLY_VERSION_MISMATCH": { - "tools/Microsoft.CodeAnalysis.CSharp.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/Microsoft.CodeAnalysis.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/Newtonsoft.Json.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/runtimes/unix/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions.", - "tools/runtimes/win/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions." + "tools/netcoreapp3.0/Microsoft.CodeAnalysis.CSharp.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/Microsoft.CodeAnalysis.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/Newtonsoft.Json.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/runtimes/unix/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions.", + "tools/netcoreapp3.0/runtimes/win/lib/netstandard1.3/System.Text.Encoding.CodePages.dll": "This assembly is not owned by us and does not follow our conventions." + }, + "BUILD_ITEMS_FRAMEWORK": { + "*": "Razor SDK does not contain any dependencies or binaries and consequently does not have a dependency group." } } } diff --git a/src/Razor/Razor.sln b/src/Razor/Razor.sln index 5bb3137be2..8232e2316c 100644 --- a/src/Razor/Razor.sln +++ b/src/Razor/Razor.sln @@ -20,6 +20,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution build\dependencies.props = build\dependencies.props Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets + build\repo.props = build\repo.props + build\repo.targets = build\repo.targets build\sources.props = build\sources.props EndProjectSection EndProject @@ -77,9 +79,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Mac. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test", "test\Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test\Microsoft.VisualStudio.Mac.LanguageServices.Razor.Test.csproj", "{B8A3E4CA-D54A-441F-A3BF-E00F060CA042}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Design", "src\Microsoft.AspNetCore.Razor.Design\Microsoft.AspNetCore.Razor.Design.csproj", "{5257B25D-330A-4DCF-ACED-B4709CFBF916}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Design.Test", "test\Microsoft.AspNetCore.Razor.Design.Test\Microsoft.AspNetCore.Razor.Design.Test.csproj", "{1D90F276-E1CA-4FDF-A173-EB889E7D3150}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NET.Sdk.Razor.Test", "test\Microsoft.NET.Sdk.Razor.Test\Microsoft.NET.Sdk.Razor.Test.csproj", "{1D90F276-E1CA-4FDF-A173-EB889E7D3150}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Test", "test\Microsoft.AspNetCore.Razor.Test\Microsoft.AspNetCore.Razor.Test.csproj", "{323553F0-14AB-4FBD-9CF0-1CC0BE8056F8}" EndProject @@ -97,6 +97,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib", "test\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.csproj", "{72E89155-86C7-454E-BDD9-39F497F2F61B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X", "src\Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X\Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.csproj", "{0693CA32-BB75-401E-BC08-72D6DEEB4C99}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.Test", "test\Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.Test\Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.Test.csproj", "{495A006C-D2B9-4AD0-9D33-60820BA501D8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Test.MvcShim.Version2_X", "test\Microsoft.AspNetCore.Razor.Test.MvcShim.Version2_X\Microsoft.AspNetCore.Razor.Test.MvcShim.Version2_X.csproj", "{D87E5501-B832-46B6-ACD3-EC989E3D14ED}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -327,14 +333,6 @@ Global {B8A3E4CA-D54A-441F-A3BF-E00F060CA042}.Release|Any CPU.Build.0 = Release|Any CPU {B8A3E4CA-D54A-441F-A3BF-E00F060CA042}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU {B8A3E4CA-D54A-441F-A3BF-E00F060CA042}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU - {5257B25D-330A-4DCF-ACED-B4709CFBF916}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5257B25D-330A-4DCF-ACED-B4709CFBF916}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5257B25D-330A-4DCF-ACED-B4709CFBF916}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU - {5257B25D-330A-4DCF-ACED-B4709CFBF916}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU - {5257B25D-330A-4DCF-ACED-B4709CFBF916}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5257B25D-330A-4DCF-ACED-B4709CFBF916}.Release|Any CPU.Build.0 = Release|Any CPU - {5257B25D-330A-4DCF-ACED-B4709CFBF916}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU - {5257B25D-330A-4DCF-ACED-B4709CFBF916}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU {1D90F276-E1CA-4FDF-A173-EB889E7D3150}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1D90F276-E1CA-4FDF-A173-EB889E7D3150}.Debug|Any CPU.Build.0 = Debug|Any CPU {1D90F276-E1CA-4FDF-A173-EB889E7D3150}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU @@ -399,6 +397,30 @@ Global {72E89155-86C7-454E-BDD9-39F497F2F61B}.Release|Any CPU.Build.0 = Release|Any CPU {72E89155-86C7-454E-BDD9-39F497F2F61B}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU {72E89155-86C7-454E-BDD9-39F497F2F61B}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU + {0693CA32-BB75-401E-BC08-72D6DEEB4C99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0693CA32-BB75-401E-BC08-72D6DEEB4C99}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0693CA32-BB75-401E-BC08-72D6DEEB4C99}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {0693CA32-BB75-401E-BC08-72D6DEEB4C99}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU + {0693CA32-BB75-401E-BC08-72D6DEEB4C99}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0693CA32-BB75-401E-BC08-72D6DEEB4C99}.Release|Any CPU.Build.0 = Release|Any CPU + {0693CA32-BB75-401E-BC08-72D6DEEB4C99}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {0693CA32-BB75-401E-BC08-72D6DEEB4C99}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU + {495A006C-D2B9-4AD0-9D33-60820BA501D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {495A006C-D2B9-4AD0-9D33-60820BA501D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {495A006C-D2B9-4AD0-9D33-60820BA501D8}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {495A006C-D2B9-4AD0-9D33-60820BA501D8}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU + {495A006C-D2B9-4AD0-9D33-60820BA501D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {495A006C-D2B9-4AD0-9D33-60820BA501D8}.Release|Any CPU.Build.0 = Release|Any CPU + {495A006C-D2B9-4AD0-9D33-60820BA501D8}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {495A006C-D2B9-4AD0-9D33-60820BA501D8}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU + {D87E5501-B832-46B6-ACD3-EC989E3D14ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D87E5501-B832-46B6-ACD3-EC989E3D14ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D87E5501-B832-46B6-ACD3-EC989E3D14ED}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {D87E5501-B832-46B6-ACD3-EC989E3D14ED}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU + {D87E5501-B832-46B6-ACD3-EC989E3D14ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D87E5501-B832-46B6-ACD3-EC989E3D14ED}.Release|Any CPU.Build.0 = Release|Any CPU + {D87E5501-B832-46B6-ACD3-EC989E3D14ED}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {D87E5501-B832-46B6-ACD3-EC989E3D14ED}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -432,7 +454,6 @@ Global {FAF9986F-E086-4513-9D52-F7BF5FFCF31D} = {C0CC1E1F-1559-44DE-93A8-63259CEA2AAB} {95B18DEE-8B45-4CF0-B9F8-CCBAF3B5251A} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED} {B8A3E4CA-D54A-441F-A3BF-E00F060CA042} = {92463391-81BE-462B-AC3C-78C6C760741F} - {5257B25D-330A-4DCF-ACED-B4709CFBF916} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED} {1D90F276-E1CA-4FDF-A173-EB889E7D3150} = {92463391-81BE-462B-AC3C-78C6C760741F} {323553F0-14AB-4FBD-9CF0-1CC0BE8056F8} = {92463391-81BE-462B-AC3C-78C6C760741F} {6205467F-E381-4C42-AEEC-763BD62B3D5E} = {C2C98051-0F39-47F2-80B6-E72B29159F2C} @@ -441,6 +462,9 @@ Global {7D9ECCEE-71D1-4A42-ABEE-876AFA1B4FC9} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED} {6EA56B2B-89EC-4C38-A384-97D203375B06} = {92463391-81BE-462B-AC3C-78C6C760741F} {72E89155-86C7-454E-BDD9-39F497F2F61B} = {92463391-81BE-462B-AC3C-78C6C760741F} + {0693CA32-BB75-401E-BC08-72D6DEEB4C99} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED} + {495A006C-D2B9-4AD0-9D33-60820BA501D8} = {92463391-81BE-462B-AC3C-78C6C760741F} + {D87E5501-B832-46B6-ACD3-EC989E3D14ED} = {92463391-81BE-462B-AC3C-78C6C760741F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0035341D-175A-4D05-95E6-F1C2785A1E26} diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/ProjectSnapshotManagerBenchmarkBase.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/ProjectSnapshotManagerBenchmarkBase.cs index 70590a6a35..580ddd1572 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/ProjectSnapshotManagerBenchmarkBase.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Performance/ProjectSystem/ProjectSnapshotManagerBenchmarkBase.cs @@ -132,14 +132,6 @@ namespace Microsoft.AspNetCore.Razor.Performance private class StaticProjectSnapshotProjectEngineFactory : ProjectSnapshotProjectEngineFactory { - public override RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure) - { - return RazorProjectEngine.Create(project.Configuration, fileSystem, b => - { - RazorExtensions.Register(b); - }); - } - public override IProjectEngineFactory FindFactory(ProjectSnapshot project) { throw new NotImplementedException(); @@ -149,6 +141,14 @@ namespace Microsoft.AspNetCore.Razor.Performance { throw new NotImplementedException(); } + + public override RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) + { + return RazorProjectEngine.Create(configuration, fileSystem, b => + { + RazorExtensions.Register(b); + }); + } } } } diff --git a/src/Razor/build/MPack.targets b/src/Razor/build/MPack.targets index ce81b91622..e00b58a968 100644 --- a/src/Razor/build/MPack.targets +++ b/src/Razor/build/MPack.targets @@ -7,9 +7,9 @@ shipoob $(IntermediateDir)mpack\ - $(AddinDirectory)bin\$(Configuration)\net461\ + $(AddinDirectory)bin\$(Configuration)\net472\ Microsoft.VisualStudio.Mac.LanguageServices.Razor - $(RepositoryRoot)src\$(LanguageServiceName)\bin\$(Configuration)\net461\ + $(RepositoryRoot)src\$(LanguageServiceName)\bin\$(Configuration)\net472\ $(AddinName)_$(AddinVersion) $(MPackName).mpack $(BuildDir)$(MPackFileName) @@ -72,8 +72,9 @@ - - + + + diff --git a/src/Razor/build/VSIX.targets b/src/Razor/build/VSIX.targets index eda19a3bef..bc3460fd16 100644 --- a/src/Razor/build/VSIX.targets +++ b/src/Razor/build/VSIX.targets @@ -105,7 +105,7 @@ /p:Configuration=$(Configuration); /p:FeatureBranchVersionSuffix=$(FeatureBranchVersionSuffix); /p:BuildNumber=$(BuildNumber); - /p:LangVersion=7.1" /> + /p:LangVersion=7.2" /> diff --git a/src/Razor/build/dependencies.props b/src/Razor/build/dependencies.props index b76ea87d30..983109a3be 100644 --- a/src/Razor/build/dependencies.props +++ b/src/Razor/build/dependencies.props @@ -4,60 +4,63 @@ 0.10.13 - 2.2.0-preview2-20181108.4 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-35661 - 2.2.0-rtm-181106-13 - 15.6.82 - 15.6.82 - 15.6.82 + 3.0.0-alpha1-20181108.5 + 3.0.0-preview-181106-14 + 3.0.0-alpha1-10717 + 3.0.0-preview-181106-14 + 15.8.166 + 15.8.166 + 15.8.166 2.8.0 2.8.0 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.1.0 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-181106-13 - 2.2.0-rtm-35661 - 2.0.9 + 3.0.0-preview-181106-14 + 3.0.0-preview-181106-14 + 3.0.0-preview1-26907-05 + 3.0.0-preview-181106-14 + 3.0.0-preview-181106-14 + 3.0.0-alpha1-10717 2.1.3 - 2.2.0-rtm-27105-02 + 3.0.0-preview1-26907-05 + 1.0.0-alpha-004 15.6.1 - 15.0.26606 - 15.6.161-preview - 15.6.161-preview - 7.10.6070 - 15.3.224 + 15.8.525 + 15.8.28010 + 16.0.142-g25b7188c54 + 16.0.142-g25b7188c54 + 16.0.142-g25b7188c54 + 7.10.6071 + 16.0.201-pre-g7d366164d0 2.0.6142705 - 15.3.224 - 15.0.26606 - 10.0.30319 - 11.0.61030 - 12.0.30110 - 8.0.50727 - 9.0.30729 - 7.10.6071 - 15.6.161-preview + 16.0.201-pre-g7d366164d0 + 15.8.28010 + 10.0.30320 + 11.0.61031 + 12.0.30111 + 8.0.50728 + 9.0.30730 + 7.10.6072 + 16.0.142-g25b7188c54 + 15.8.168 1.3.8 1.0.1 4.10.0 2.0.3 11.0.2 - 1.1.92 - 4.5.0 + 1.3.23 + 4.6.0-preview1-26907-04 4.3.0 - 4.5.0 + 4.6.0-preview1-26829-04 9.0.1 - 2.9.0-beta4-62911-02 - 2.9.0-beta4-62911-02 - 2.9.0-beta4-62911-02 - 2.9.0-beta4-62911-02 - 2.9.0-beta4-62911-02 - 2.9.0-beta4-62911-02 - 2.9.0-beta4-62911-02 - 2.9.0-beta4-62911-02 - 2.9.0-beta4-62911-02 - 2.9.0-beta4-62911-02 + 2.11.0-beta3-63519-01 + 2.11.0-beta3-63519-01 + 2.11.0-beta3-63519-01 + 2.11.0-beta3-63519-01 + 2.11.0-beta3-63519-01 + 2.11.0-beta3-63519-01 + 2.11.0-beta3-63519-01 + 2.11.0-beta3-63519-01 + 2.11.0-beta3-63519-01 + 2.11.0-beta3-63519-01 0.10.0 2.3.1 2.4.0 diff --git a/src/Razor/build/repo.props b/src/Razor/build/repo.props index def34f0b6f..0aebc758fb 100644 --- a/src/Razor/build/repo.props +++ b/src/Razor/build/repo.props @@ -21,13 +21,11 @@ Internal.AspNetCore.Universe.Lineup - 2.2.0-* https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json - - + diff --git a/src/Razor/build/repo.targets b/src/Razor/build/repo.targets index 550069ec47..1c111a1c01 100644 --- a/src/Razor/build/repo.targets +++ b/src/Razor/build/repo.targets @@ -11,9 +11,9 @@ $(PrepareDependsOn);GenerateMSBuildLocationFile - $(RepositoryRoot)test\Microsoft.AspNetCore.Razor.Design.Test\ - $(RazorDesignTestProject)BuildVariables.cs.template - $(RazorDesignTestProject)obj\BuildVariables.generated.cs + $(RepositoryRoot)test\Microsoft.NET.Sdk.Razor.Test\ + $(RazorSdkTestProject)BuildVariables.cs.template + $(RazorSdkTestProject)obj\BuildVariables.generated.cs MSBuildLocation=$(VisualStudioMSBuildx86Path); - MicrosoftNETCoreAppPackageVersion=$(MicrosoftNETCoreApp22PackageVersion); - NETStandardLibraryPackageVersion=$(NETStandardLibrary20PackageVersion) + MicrosoftNETCoreApp30PackageVersion=$(MicrosoftNETCoreApp30PackageVersion); + NETStandardLibrary20PackageVersion=$(NETStandardLibrary20PackageVersion) diff --git a/src/Razor/build/sources.props b/src/Razor/build/sources.props index 02efac4549..2bd61d3599 100644 --- a/src/Razor/build/sources.props +++ b/src/Razor/build/sources.props @@ -10,8 +10,10 @@ https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; https://dotnet.myget.org/F/msbuild/api/v3/index.json; https://dotnet.myget.org/F/roslyn/api/v3/index.json; + https://dotnet.myget.org/F/roslyn-tools/api/v3/index.json; https://vside.myget.org/F/vssdk/api/v3/index.json; - https://vside.myget.org/F/vsmac/api/v3/index.json + https://vside.myget.org/F/vsmac/api/v3/index.json; + https://vside.myget.org/F/devcore/api/v3/index.json; $(RestoreSources); diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.csproj b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.csproj index c6b3d3e212..0d61d12b26 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.csproj +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X.csproj @@ -2,7 +2,7 @@ ASP.NET Core design time hosting infrastructure for the Razor view engine. - net46;netstandard2.0 + netstandard2.0 $(PackageTags);aspnetcoremvc false diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs index 5211072d0e..55e8929457 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X/RazorExtensions.cs @@ -20,7 +20,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X InjectDirective.Register(builder); ModelDirective.Register(builder); - FunctionsDirective.Register(builder); InheritsDirective.Register(builder); builder.Features.Add(new DefaultTagHelperDescriptorProvider()); diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/AssemblyAttributeInjectionPass.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/AssemblyAttributeInjectionPass.cs new file mode 100644 index 0000000000..021c61c368 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/AssemblyAttributeInjectionPass.cs @@ -0,0 +1,102 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public class AssemblyAttributeInjectionPass : IntermediateNodePassBase, IRazorOptimizationPass + { + private const string RazorViewAttribute = "global::Microsoft.AspNetCore.Mvc.Razor.Compilation.RazorViewAttribute"; + private const string RazorPageAttribute = "global::Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.RazorPageAttribute"; + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + if (documentNode.Options.DesignTime) + { + return; + } + + var @namespace = documentNode.FindPrimaryNamespace(); + if (@namespace == null || string.IsNullOrEmpty(@namespace.Content)) + { + // No namespace node or it's incomplete. Skip. + return; + } + + var @class = documentNode.FindPrimaryClass(); + if (@class == null || string.IsNullOrEmpty(@class.ClassName)) + { + // No class node or it's incomplete. Skip. + return; + } + + var generatedTypeName = $"{@namespace.Content}.{@class.ClassName}"; + + // The MVC attributes require a relative path to be specified so that we can make a view engine path. + // We can't use a rooted path because we don't know what the project root is. + // + // If we can't sanitize the path, we'll just set it to null and let is blow up at runtime - we don't + // want to create noise if this code has to run in some unanticipated scenario. + var escapedPath = MakeVerbatimStringLiteral(ConvertToViewEnginePath(codeDocument.Source.RelativePath)); + + string attribute; + if (documentNode.DocumentKind == MvcViewDocumentClassifierPass.MvcViewDocumentKind) + { + attribute = $"[assembly:{RazorViewAttribute}({escapedPath}, typeof({generatedTypeName}))]"; + } + else if (documentNode.DocumentKind == RazorPageDocumentClassifierPass.RazorPageDocumentKind && + PageDirective.TryGetPageDirective(documentNode, out var pageDirective)) + { + var escapedRoutePrefix = MakeVerbatimStringLiteral(pageDirective.RouteTemplate); + attribute = $"[assembly:{RazorPageAttribute}({escapedPath}, typeof({generatedTypeName}), {escapedRoutePrefix})]"; + } + else + { + return; + } + + var index = documentNode.Children.IndexOf(@namespace); + Debug.Assert(index >= 0); + + var pageAttribute = new CSharpCodeIntermediateNode(); + pageAttribute.Children.Add(new IntermediateToken() + { + Kind = TokenKind.CSharp, + Content = attribute, + }); + + documentNode.Children.Insert(index, pageAttribute); + } + + private static string MakeVerbatimStringLiteral(string value) + { + if (value == null) + { + return "null"; + } + + value = value.Replace("\"", "\"\""); + return $"@\"{value}\""; + } + + private static string ConvertToViewEnginePath(string relativePath) + { + if (string.IsNullOrEmpty(relativePath)) + { + return null; + } + + // Checking for both / and \ because a \ will become a /. + if (!relativePath.StartsWith("/") && !relativePath.StartsWith("\\")) + { + relativePath = "/" + relativePath; + } + + relativePath = relativePath.Replace('\\', '/'); + return relativePath; + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/CSharpIdentifier.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/CSharpIdentifier.cs similarity index 97% rename from src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/CSharpIdentifier.cs rename to src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/CSharpIdentifier.cs index e1ca2e343e..77a5e8f54e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/CSharpIdentifier.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/CSharpIdentifier.cs @@ -5,7 +5,7 @@ using System; using System.Globalization; using System.Text; -namespace Microsoft.AspNetCore.Mvc.Razor.Extensions +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X { internal static class CSharpIdentifier { diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ExtensionInitializer.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ExtensionInitializer.cs new file mode 100644 index 0000000000..03d05f7f9b --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ExtensionInitializer.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + internal class ExtensionInitializer : RazorExtensionInitializer + { + public override void Initialize(RazorProjectEngineBuilder builder) + { + RazorExtensions.Register(builder); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/IInjectTargetExtension.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/IInjectTargetExtension.cs new file mode 100644 index 0000000000..c334e23c2b --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/IInjectTargetExtension.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public interface IInjectTargetExtension : ICodeTargetExtension + { + void WriteInjectProperty(CodeRenderingContext context, InjectIntermediateNode node); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/IViewComponentTagHelperTargetExtension.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/IViewComponentTagHelperTargetExtension.cs new file mode 100644 index 0000000000..f3e07a2b75 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/IViewComponentTagHelperTargetExtension.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public interface IViewComponentTagHelperTargetExtension : ICodeTargetExtension + { + void WriteViewComponentTagHelper(CodeRenderingContext context, ViewComponentTagHelperIntermediateNode node); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/InjectDirective.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/InjectDirective.cs new file mode 100644 index 0000000000..9442aaf641 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/InjectDirective.cs @@ -0,0 +1,124 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public static class InjectDirective + { + public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective( + "inject", + DirectiveKind.SingleLine, + builder => + { + builder + .AddTypeToken(Resources.InjectDirective_TypeToken_Name, Resources.InjectDirective_TypeToken_Description) + .AddMemberToken(Resources.InjectDirective_MemberToken_Name, Resources.InjectDirective_MemberToken_Description); + + builder.Usage = DirectiveUsage.FileScopedMultipleOccurring; + builder.Description = Resources.InjectDirective_Description; + }); + + public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + builder.Features.Add(new Pass()); + builder.AddTargetExtension(new InjectTargetExtension()); + return builder; + } + + internal class Pass : IntermediateNodePassBase, IRazorDirectiveClassifierPass + { + // Runs after the @model and @namespace directives + public override int Order => 10; + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + var visitor = new Visitor(); + visitor.Visit(documentNode); + var modelType = ModelDirective.GetModelType(documentNode); + + var properties = new HashSet(StringComparer.Ordinal); + + for (var i = visitor.Directives.Count - 1; i >= 0; i--) + { + var directive = visitor.Directives[i]; + var tokens = directive.Tokens.ToArray(); + if (tokens.Length < 2) + { + continue; + } + + var typeName = tokens[0].Content; + var memberName = tokens[1].Content; + + if (!properties.Add(memberName)) + { + continue; + } + + typeName = typeName.Replace("", "<" + modelType + ">"); + + var injectNode = new InjectIntermediateNode() + { + TypeName = typeName, + MemberName = memberName, + }; + + visitor.Class.Children.Add(injectNode); + } + } + } + + private class Visitor : IntermediateNodeWalker + { + public ClassDeclarationIntermediateNode Class { get; private set; } + + public IList Directives { get; } = new List(); + + public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node) + { + if (Class == null) + { + Class = node; + } + + base.VisitClassDeclaration(node); + } + + public override void VisitDirective(DirectiveIntermediateNode node) + { + if (node.Directive == Directive) + { + Directives.Add(node); + } + } + } + + #region Obsolete + [Obsolete("This method is obsolete and will be removed in a future version.")] + public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + builder.Features.Add(new Pass()); + builder.AddTargetExtension(new InjectTargetExtension()); + return builder; + } + #endregion + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/InjectIntermediateNode.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/InjectIntermediateNode.cs new file mode 100644 index 0000000000..4e3603f40d --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/InjectIntermediateNode.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public class InjectIntermediateNode : ExtensionIntermediateNode + { + public string TypeName { get; set; } + + public string MemberName { get; set; } + + public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly; + + public override void Accept(IntermediateNodeVisitor visitor) + { + if (visitor == null) + { + throw new ArgumentNullException(nameof(visitor)); + } + + AcceptExtensionNode(this, visitor); + } + + public override void WriteNode(CodeTarget target, CodeRenderingContext context) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var extension = target.GetExtension(); + if (extension == null) + { + ReportMissingCodeTargetExtension(context); + return; + } + + extension.WriteInjectProperty(context, this); + } + + public override void FormatNode(IntermediateNodeFormatter formatter) + { + formatter.WriteContent(MemberName); + + formatter.WriteProperty(nameof(MemberName), MemberName); + formatter.WriteProperty(nameof(TypeName), TypeName); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/InjectTargetExtension.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/InjectTargetExtension.cs new file mode 100644 index 0000000000..1660b84d6d --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/InjectTargetExtension.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public class InjectTargetExtension : IInjectTargetExtension + { + private const string RazorInjectAttribute = "[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]"; + + public void WriteInjectProperty(CodeRenderingContext context, InjectIntermediateNode node) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + var property = $"public {node.TypeName} {node.MemberName} {{ get; private set; }}"; + + if (node.Source.HasValue) + { + using (context.CodeWriter.BuildLinePragma(node.Source.Value)) + { + context.CodeWriter + .WriteLine(RazorInjectAttribute) + .WriteLine(property); + } + } + else + { + context.CodeWriter + .WriteLine(RazorInjectAttribute) + .WriteLine(property); + } + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InstrumentationPass.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/InstrumentationPass.cs similarity index 98% rename from src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InstrumentationPass.cs rename to src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/InstrumentationPass.cs index d80c2b9b68..f82ed32e45 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InstrumentationPass.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/InstrumentationPass.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Intermediate; -namespace Microsoft.AspNetCore.Mvc.Razor.Extensions +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X { public class InstrumentationPass : IntermediateNodePassBase, IRazorOptimizationPass { diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.csproj b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.csproj new file mode 100644 index 0000000000..e77d4c3837 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.csproj @@ -0,0 +1,20 @@ + + + + ASP.NET Core design time hosting infrastructure for the Razor view engine. + netstandard2.0 + $(PackageTags);aspnetcoremvc + + + + + Shared\CodeWriterExtensions.cs + + + + + + + + + diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ModelDirective.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ModelDirective.cs new file mode 100644 index 0000000000..cf80df5f68 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ModelDirective.cs @@ -0,0 +1,152 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public static class ModelDirective + { + public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective( + "model", + DirectiveKind.SingleLine, + builder => + { + builder.AddTypeToken(Resources.ModelDirective_TypeToken_Name, Resources.ModelDirective_TypeToken_Description); + builder.Usage = DirectiveUsage.FileScopedSinglyOccurring; + builder.Description = Resources.ModelDirective_Description; + }); + + public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + builder.Features.Add(new Pass()); + return builder; + } + + public static string GetModelType(DocumentIntermediateNode document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + var visitor = new Visitor(); + return GetModelType(document, visitor); + } + + private static string GetModelType(DocumentIntermediateNode document, Visitor visitor) + { + visitor.Visit(document); + + for (var i = visitor.ModelDirectives.Count - 1; i >= 0; i--) + { + var directive = visitor.ModelDirectives[i]; + + var tokens = directive.Tokens.ToArray(); + if (tokens.Length >= 1) + { + return tokens[0].Content; + } + } + + if (document.DocumentKind == RazorPageDocumentClassifierPass.RazorPageDocumentKind) + { + return visitor.Class.ClassName; + } + else + { + return "dynamic"; + } + } + + internal class Pass : IntermediateNodePassBase, IRazorDirectiveClassifierPass + { + // Runs after the @inherits directive + public override int Order => 5; + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + var visitor = new Visitor(); + var modelType = GetModelType(documentNode, visitor); + + if (documentNode.Options.DesignTime) + { + // Alias the TModel token to a known type. + // This allows design time compilation to succeed for Razor files where the token isn't replaced. + var typeName = $"global::{typeof(object).FullName}"; + var usingNode = new UsingDirectiveIntermediateNode() + { + Content = $"TModel = {typeName}" + }; + + visitor.Namespace?.Children.Insert(0, usingNode); + } + + var baseType = visitor.Class?.BaseType?.Replace("", "<" + modelType + ">"); + visitor.Class.BaseType = baseType; + } + } + + private class Visitor : IntermediateNodeWalker + { + public NamespaceDeclarationIntermediateNode Namespace { get; private set; } + + public ClassDeclarationIntermediateNode Class { get; private set; } + + public IList ModelDirectives { get; } = new List(); + + public override void VisitNamespaceDeclaration(NamespaceDeclarationIntermediateNode node) + { + if (Namespace == null) + { + Namespace = node; + } + + base.VisitNamespaceDeclaration(node); + } + + public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node) + { + if (Class == null) + { + Class = node; + } + + base.VisitClassDeclaration(node); + } + + public override void VisitDirective(DirectiveIntermediateNode node) + { + if (node.Directive == Directive) + { + ModelDirectives.Add(node); + } + } + } + + #region Obsolete + [Obsolete("This method is obsolete and will be removed in a future version.")] + public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + builder.Features.Add(new Pass()); + return builder; + } + #endregion + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ModelExpressionPass.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ModelExpressionPass.cs new file mode 100644 index 0000000000..ef8e8fb181 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ModelExpressionPass.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public class ModelExpressionPass : IntermediateNodePassBase, IRazorOptimizationPass + { + private const string ModelExpressionTypeName = "Microsoft.AspNetCore.Mvc.ViewFeatures.ModelExpression"; + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + var visitor = new Visitor(); + visitor.Visit(documentNode); + } + + private class Visitor : IntermediateNodeWalker + { + public List TagHelpers { get; } = new List(); + + public override void VisitTagHelperProperty(TagHelperPropertyIntermediateNode node) + { + if (string.Equals(node.BoundAttribute.TypeName, ModelExpressionTypeName, StringComparison.Ordinal) || + (node.IsIndexerNameMatch && + string.Equals(node.BoundAttribute.IndexerTypeName, ModelExpressionTypeName, StringComparison.Ordinal))) + { + var expression = new CSharpExpressionIntermediateNode(); + + expression.Children.Add(new IntermediateToken() + { + Kind = TokenKind.CSharp, + Content = "ModelExpressionProvider.CreateModelExpression(ViewData, __model => ", + }); + + if (node.Children.Count == 1 && node.Children[0] is IntermediateToken token && token.IsCSharp) + { + // A 'simple' expression will look like __model => __model.Foo + + expression.Children.Add(new IntermediateToken() + { + Kind = TokenKind.CSharp, + Content = "__model." + }); + + expression.Children.Add(token); + } + else + { + for (var i = 0; i < node.Children.Count; i++) + { + if (node.Children[i] is CSharpExpressionIntermediateNode nestedExpression) + { + for (var j = 0; j < nestedExpression.Children.Count; j++) + { + if (nestedExpression.Children[j] is IntermediateToken cSharpToken && + cSharpToken.IsCSharp) + { + expression.Children.Add(cSharpToken); + } + } + + continue; + } + } + } + + expression.Children.Add(new IntermediateToken() + { + Kind = TokenKind.CSharp, + Content = ")", + }); + + node.Children.Clear(); + + node.Children.Add(expression); + } + } + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/MvcImportProjectFeature.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/MvcImportProjectFeature.cs new file mode 100644 index 0000000000..c25cb0d063 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/MvcImportProjectFeature.cs @@ -0,0 +1,91 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + internal class MvcImportProjectFeature : RazorProjectEngineFeatureBase, IImportProjectFeature + { + private const string ImportsFileName = "_ViewImports.cshtml"; + + public IReadOnlyList GetImports(RazorProjectItem projectItem) + { + if (projectItem == null) + { + throw new ArgumentNullException(nameof(projectItem)); + } + + var imports = new List(); + AddDefaultDirectivesImport(imports); + + // We add hierarchical imports second so any default directive imports can be overridden. + AddHierarchicalImports(projectItem, imports); + + return imports; + } + + // Internal for testing + internal static void AddDefaultDirectivesImport(List imports) + { + imports.Add(DefaultDirectivesProjectItem.Instance); + } + + // Internal for testing + internal void AddHierarchicalImports(RazorProjectItem projectItem, List imports) + { + // We want items in descending order. FindHierarchicalItems returns items in ascending order. + var importProjectItems = ProjectEngine.FileSystem.FindHierarchicalItems(projectItem.FilePath, ImportsFileName).Reverse(); + imports.AddRange(importProjectItems); + } + + private class DefaultDirectivesProjectItem : RazorProjectItem + { + private readonly byte[] _defaultImportBytes; + + private DefaultDirectivesProjectItem() + { + var preamble = Encoding.UTF8.GetPreamble(); + var content = @" +@using System +@using System.Collections.Generic +@using System.Linq +@using System.Threading.Tasks +@using Microsoft.AspNetCore.Mvc +@using Microsoft.AspNetCore.Mvc.Rendering +@using Microsoft.AspNetCore.Mvc.ViewFeatures +@inject global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html +@inject global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json +@inject global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component +@inject global::Microsoft.AspNetCore.Mvc.IUrlHelper Url +@inject global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider +@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.UrlResolutionTagHelper, Microsoft.AspNetCore.Mvc.Razor +@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.HeadTagHelper, Microsoft.AspNetCore.Mvc.Razor +@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.BodyTagHelper, Microsoft.AspNetCore.Mvc.Razor +"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + _defaultImportBytes = new byte[preamble.Length + contentBytes.Length]; + preamble.CopyTo(_defaultImportBytes, 0); + contentBytes.CopyTo(_defaultImportBytes, preamble.Length); + } + + public override string BasePath => null; + + public override string FilePath => null; + + public override string PhysicalPath => null; + + public override bool Exists => true; + + public static DefaultDirectivesProjectItem Instance { get; } = new DefaultDirectivesProjectItem(); + + public override Stream Read() => new MemoryStream(_defaultImportBytes); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/MvcViewDocumentClassifierPass.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/MvcViewDocumentClassifierPass.cs new file mode 100644 index 0000000000..7ec6a1360e --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/MvcViewDocumentClassifierPass.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Text; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public class MvcViewDocumentClassifierPass : DocumentClassifierPassBase + { + public static readonly string MvcViewDocumentKind = "mvc.1.0.view"; + + protected override string DocumentKind => MvcViewDocumentKind; + + protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) => true; + + protected override void OnDocumentStructureCreated( + RazorCodeDocument codeDocument, + NamespaceDeclarationIntermediateNode @namespace, + ClassDeclarationIntermediateNode @class, + MethodDeclarationIntermediateNode method) + { + base.OnDocumentStructureCreated(codeDocument, @namespace, @class, method); + + @namespace.Content = "AspNetCore"; + + var filePath = codeDocument.Source.RelativePath ?? codeDocument.Source.FilePath; + if (string.IsNullOrEmpty(filePath)) + { + // It's possible for a Razor document to not have a file path. + // Eg. When we try to generate code for an in memory document like default imports. + var checksum = BytesToString(codeDocument.Source.GetChecksum()); + @class.ClassName = $"AspNetCore_{checksum}"; + } + else + { + @class.ClassName = CSharpIdentifier.GetClassNameFromPath(filePath); + } + + @class.BaseType = "global::Microsoft.AspNetCore.Mvc.Razor.RazorPage"; + @class.Modifiers.Clear(); + @class.Modifiers.Add("public"); + + method.MethodName = "ExecuteAsync"; + method.Modifiers.Clear(); + method.Modifiers.Add("public"); + method.Modifiers.Add("async"); + method.Modifiers.Add("override"); + method.ReturnType = $"global::{typeof(System.Threading.Tasks.Task).FullName}"; + } + + private static string BytesToString(byte[] bytes) + { + if (bytes == null) + { + throw new ArgumentNullException(nameof(bytes)); + } + + var result = new StringBuilder(bytes.Length); + for (var i = 0; i < bytes.Length; i++) + { + // The x2 format means lowercase hex, where each byte is a 2-character string. + result.Append(bytes[i].ToString("x2")); + } + + return result.ToString(); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/NamespaceDirective.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/NamespaceDirective.cs new file mode 100644 index 0000000000..de05d354b1 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/NamespaceDirective.cs @@ -0,0 +1,204 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public static class NamespaceDirective + { + private static readonly char[] Separators = new char[] { '\\', '/' }; + + public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective( + "namespace", + DirectiveKind.SingleLine, + builder => + { + builder.AddNamespaceToken( + Resources.NamespaceDirective_NamespaceToken_Name, + Resources.NamespaceDirective_NamespaceToken_Description); + builder.Usage = DirectiveUsage.FileScopedSinglyOccurring; + builder.Description = Resources.NamespaceDirective_Description; + }); + + public static void Register(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(); + } + + builder.AddDirective(Directive); + builder.Features.Add(new Pass()); + } + + // internal for testing + internal class Pass : IntermediateNodePassBase, IRazorDirectiveClassifierPass + { + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + if (documentNode.DocumentKind != RazorPageDocumentClassifierPass.RazorPageDocumentKind && + documentNode.DocumentKind != MvcViewDocumentClassifierPass.MvcViewDocumentKind) + { + // Not a page. Skip. + return; + } + + var visitor = new Visitor(); + visitor.Visit(documentNode); + + var directive = visitor.LastNamespaceDirective; + if (directive == null) + { + // No namespace set. Skip. + return; + } + + var @namespace = visitor.FirstNamespace; + if (@namespace == null) + { + // No namespace node. Skip. + return; + } + + @namespace.Content = GetNamespace(codeDocument.Source.FilePath, directive); + } + } + + // internal for testing. + // + // This code does a best-effort attempt to compute a namespace 'suffix' - the path difference between + // where the @namespace directive appears and where the current document is on disk. + // + // In the event that these two source either don't have FileNames set or don't follow a coherent hierarchy, + // we will just use the namespace verbatim. + internal static string GetNamespace(string source, DirectiveIntermediateNode directive) + { + var directiveSource = NormalizeDirectory(directive.Source?.FilePath); + + var baseNamespace = directive.Tokens.FirstOrDefault()?.Content; + if (string.IsNullOrEmpty(baseNamespace)) + { + // The namespace directive was incomplete. + return string.Empty; + } + + if (string.IsNullOrEmpty(source) || directiveSource == null) + { + // No sources, can't compute a suffix. + return baseNamespace; + } + + // We're specifically using OrdinalIgnoreCase here because Razor treats all paths as case-insensitive. + if (!source.StartsWith(directiveSource, StringComparison.OrdinalIgnoreCase) || + source.Length <= directiveSource.Length) + { + // The imports are not from the directory hierarchy, can't compute a suffix. + return baseNamespace; + } + + // OK so that this point we know that the 'imports' file containing this directive is in the directory + // hierarchy of this soure file. This is the case where we can append a suffix to the baseNamespace. + // + // Everything so far has just been defensiveness on our part. + + var builder = new StringBuilder(baseNamespace); + + var segments = source.Substring(directiveSource.Length).Split(Separators); + + // Skip the last segment because it's the FileName. + for (var i = 0; i < segments.Length - 1; i++) + { + builder.Append('.'); + builder.Append(CSharpIdentifier.SanitizeClassName(segments[i])); + } + + return builder.ToString(); + } + + // We want to normalize the path of the file containing the '@namespace' directive to just the containing + // directory with a trailing separator. + // + // Not using Path.GetDirectoryName here because it doesn't meet these requirements, and we want to handle + // both 'view engine' style paths and absolute paths. + // + // We also don't normalize the separators here. We expect that all documents are using a consistent style of path. + // + // If we can't normalize the path, we just return null so it will be ignored. + private static string NormalizeDirectory(string path) + { + if (string.IsNullOrEmpty(path)) + { + return null; + } + + var lastSeparator = path.LastIndexOfAny(Separators); + if (lastSeparator == -1) + { + return null; + } + + // Includes the separator + return path.Substring(0, lastSeparator + 1); + } + + private class Visitor : IntermediateNodeWalker + { + public ClassDeclarationIntermediateNode FirstClass { get; private set; } + + public NamespaceDeclarationIntermediateNode FirstNamespace { get; private set; } + + // We want the last one, so get them all and then . + public DirectiveIntermediateNode LastNamespaceDirective { get; private set; } + + public override void VisitNamespaceDeclaration(NamespaceDeclarationIntermediateNode node) + { + if (FirstNamespace == null) + { + FirstNamespace = node; + } + + base.VisitNamespaceDeclaration(node); + } + + public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node) + { + if (FirstClass == null) + { + FirstClass = node; + } + + base.VisitClassDeclaration(node); + } + + public override void VisitDirective(DirectiveIntermediateNode node) + { + if (node.Directive == Directive) + { + LastNamespaceDirective = node; + } + + base.VisitDirective(node); + } + } + + #region Obsolete + [Obsolete("This method is obsolete and will be removed in a future version.")] + public static void Register(IRazorEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(); + } + + builder.AddDirective(Directive); + builder.Features.Add(new Pass()); + } + #endregion + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/PageDirective.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/PageDirective.cs new file mode 100644 index 0000000000..9b483f9b9f --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/PageDirective.cs @@ -0,0 +1,121 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public class PageDirective + { + public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective( + "page", + DirectiveKind.SingleLine, + builder => + { + builder.AddOptionalStringToken(Resources.PageDirective_RouteToken_Name, Resources.PageDirective_RouteToken_Description); + builder.Usage = DirectiveUsage.FileScopedSinglyOccurring; + builder.Description = Resources.PageDirective_Description; + }); + + private PageDirective(string routeTemplate, IntermediateNode directiveNode) + { + RouteTemplate = routeTemplate; + DirectiveNode = directiveNode; + } + + public string RouteTemplate { get; } + + public IntermediateNode DirectiveNode { get; } + + public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + return builder; + } + + public static bool TryGetPageDirective(DocumentIntermediateNode documentNode, out PageDirective pageDirective) + { + var visitor = new Visitor(); + for (var i = 0; i < documentNode.Children.Count; i++) + { + visitor.Visit(documentNode.Children[i]); + } + + if (visitor.DirectiveTokens == null) + { + pageDirective = null; + return false; + } + + var tokens = visitor.DirectiveTokens.ToList(); + string routeTemplate = null; + if (tokens.Count > 0) + { + routeTemplate = TrimQuotes(tokens[0].Content); + } + + pageDirective = new PageDirective(routeTemplate, visitor.DirectiveNode); + return true; + } + + private static string TrimQuotes(string content) + { + // Tokens aren't captured if they're malformed. Therefore, this method will + // always be called with a valid token content. + Debug.Assert(content.Length >= 2); + Debug.Assert(content.StartsWith("\"", StringComparison.Ordinal)); + Debug.Assert(content.EndsWith("\"", StringComparison.Ordinal)); + + return content.Substring(1, content.Length - 2); + } + + private class Visitor : IntermediateNodeWalker + { + public IntermediateNode DirectiveNode { get; private set; } + + public IEnumerable DirectiveTokens { get; private set; } + + public override void VisitDirective(DirectiveIntermediateNode node) + { + if (node.Directive == Directive) + { + DirectiveNode = node; + DirectiveTokens = node.Tokens; + } + } + + public override void VisitMalformedDirective(MalformedDirectiveIntermediateNode node) + { + if (DirectiveTokens == null && node.Directive == Directive) + { + DirectiveNode = node; + DirectiveTokens = node.Tokens; + } + } + } + + #region Obsolete + [Obsolete("This method is obsolete and will be removed in a future version.")] + public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AddDirective(Directive); + return builder; + } + #endregion + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/PagesPropertyInjectionPass.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/PagesPropertyInjectionPass.cs new file mode 100644 index 0000000000..4010c1903b --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/PagesPropertyInjectionPass.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public class PagesPropertyInjectionPass : IntermediateNodePassBase, IRazorOptimizationPass + { + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + if (documentNode.DocumentKind != RazorPageDocumentClassifierPass.RazorPageDocumentKind) + { + return; + } + + var modelType = ModelDirective.GetModelType(documentNode); + var visitor = new Visitor(); + visitor.Visit(documentNode); + + var @class = visitor.Class; + + var viewDataType = $"global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary<{modelType}>"; + var vddProperty = new CSharpCodeIntermediateNode(); + vddProperty.Children.Add(new IntermediateToken() + { + Kind = TokenKind.CSharp, + Content = $"public {viewDataType} ViewData => ({viewDataType})PageContext?.ViewData;", + }); + @class.Children.Add(vddProperty); + + var modelProperty = new CSharpCodeIntermediateNode(); + modelProperty.Children.Add(new IntermediateToken() + { + Kind = TokenKind.CSharp, + Content = $"public {modelType} Model => ViewData.Model;", + }); + @class.Children.Add(modelProperty); + } + + private class Visitor : IntermediateNodeWalker + { + public ClassDeclarationIntermediateNode Class { get; private set; } + + public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node) + { + if (Class == null) + { + Class = node; + } + + base.VisitClassDeclaration(node); + } + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Properties/AssemblyInfo.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..a7a402e09a --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Properties/AssemblyInfo.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X; +using Microsoft.AspNetCore.Razor.Language; + +[assembly: ProvideRazorExtensionInitializer("MVC-2.0", typeof(ExtensionInitializer))] +[assembly: ProvideRazorExtensionInitializer("MVC-2.1", typeof(ExtensionInitializer))] + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Properties/Resources.Designer.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..ec4883803c --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Properties/Resources.Designer.cs @@ -0,0 +1,338 @@ +// +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// Value cannot be null or empty. + /// + internal static string ArgumentCannotBeNullOrEmpty + { + get => GetString("ArgumentCannotBeNullOrEmpty"); + } + + /// + /// Value cannot be null or empty. + /// + internal static string FormatArgumentCannotBeNullOrEmpty() + => GetString("ArgumentCannotBeNullOrEmpty"); + + /// + /// Inject a service from the application's service container into a property. + /// + internal static string InjectDirective_Description + { + get => GetString("InjectDirective_Description"); + } + + /// + /// Inject a service from the application's service container into a property. + /// + internal static string FormatInjectDirective_Description() + => GetString("InjectDirective_Description"); + + /// + /// The name of the property. + /// + internal static string InjectDirective_MemberToken_Description + { + get => GetString("InjectDirective_MemberToken_Description"); + } + + /// + /// The name of the property. + /// + internal static string FormatInjectDirective_MemberToken_Description() + => GetString("InjectDirective_MemberToken_Description"); + + /// + /// PropertyName + /// + internal static string InjectDirective_MemberToken_Name + { + get => GetString("InjectDirective_MemberToken_Name"); + } + + /// + /// PropertyName + /// + internal static string FormatInjectDirective_MemberToken_Name() + => GetString("InjectDirective_MemberToken_Name"); + + /// + /// The type of the service to inject. + /// + internal static string InjectDirective_TypeToken_Description + { + get => GetString("InjectDirective_TypeToken_Description"); + } + + /// + /// The type of the service to inject. + /// + internal static string FormatInjectDirective_TypeToken_Description() + => GetString("InjectDirective_TypeToken_Description"); + + /// + /// TypeName + /// + internal static string InjectDirective_TypeToken_Name + { + get => GetString("InjectDirective_TypeToken_Name"); + } + + /// + /// TypeName + /// + internal static string FormatInjectDirective_TypeToken_Name() + => GetString("InjectDirective_TypeToken_Name"); + + /// + /// Specify the view or page model for the page. + /// + internal static string ModelDirective_Description + { + get => GetString("ModelDirective_Description"); + } + + /// + /// Specify the view or page model for the page. + /// + internal static string FormatModelDirective_Description() + => GetString("ModelDirective_Description"); + + /// + /// The model type. + /// + internal static string ModelDirective_TypeToken_Description + { + get => GetString("ModelDirective_TypeToken_Description"); + } + + /// + /// The model type. + /// + internal static string FormatModelDirective_TypeToken_Description() + => GetString("ModelDirective_TypeToken_Description"); + + /// + /// TypeName + /// + internal static string ModelDirective_TypeToken_Name + { + get => GetString("ModelDirective_TypeToken_Name"); + } + + /// + /// TypeName + /// + internal static string FormatModelDirective_TypeToken_Name() + => GetString("ModelDirective_TypeToken_Name"); + + /// + /// The 'inherits' keyword is not allowed when a '{0}' keyword is used. + /// + internal static string MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword + { + get => GetString("MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword"); + } + + /// + /// The 'inherits' keyword is not allowed when a '{0}' keyword is used. + /// + internal static string FormatMvcRazorCodeParser_CannotHaveModelAndInheritsKeyword(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword"), p0); + + /// + /// A property name must be specified when using the '{0}' statement. Format for a '{0}' statement is '@{0} <Type Name> <Property Name>'. + /// + internal static string MvcRazorCodeParser_InjectDirectivePropertyNameRequired + { + get => GetString("MvcRazorCodeParser_InjectDirectivePropertyNameRequired"); + } + + /// + /// A property name must be specified when using the '{0}' statement. Format for a '{0}' statement is '@{0} <Type Name> <Property Name>'. + /// + internal static string FormatMvcRazorCodeParser_InjectDirectivePropertyNameRequired(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_InjectDirectivePropertyNameRequired"), p0); + + /// + /// The '{0}' keyword must be followed by a type name on the same line. + /// + internal static string MvcRazorCodeParser_KeywordMustBeFollowedByTypeName + { + get => GetString("MvcRazorCodeParser_KeywordMustBeFollowedByTypeName"); + } + + /// + /// The '{0}' keyword must be followed by a type name on the same line. + /// + internal static string FormatMvcRazorCodeParser_KeywordMustBeFollowedByTypeName(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_KeywordMustBeFollowedByTypeName"), p0); + + /// + /// Only one '{0}' statement is allowed in a file. + /// + internal static string MvcRazorCodeParser_OnlyOneModelStatementIsAllowed + { + get => GetString("MvcRazorCodeParser_OnlyOneModelStatementIsAllowed"); + } + + /// + /// Only one '{0}' statement is allowed in a file. + /// + internal static string FormatMvcRazorCodeParser_OnlyOneModelStatementIsAllowed(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_OnlyOneModelStatementIsAllowed"), p0); + + /// + /// Invalid tag helper property '{0}.{1}'. Dictionary values must not be of type '{2}'. + /// + internal static string MvcRazorParser_InvalidPropertyType + { + get => GetString("MvcRazorParser_InvalidPropertyType"); + } + + /// + /// Invalid tag helper property '{0}.{1}'. Dictionary values must not be of type '{2}'. + /// + internal static string FormatMvcRazorParser_InvalidPropertyType(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorParser_InvalidPropertyType"), p0, p1, p2); + + /// + /// Specify the base namespace for the page. + /// + internal static string NamespaceDirective_Description + { + get => GetString("NamespaceDirective_Description"); + } + + /// + /// Specify the base namespace for the page. + /// + internal static string FormatNamespaceDirective_Description() + => GetString("NamespaceDirective_Description"); + + /// + /// The namespace for the page. + /// + internal static string NamespaceDirective_NamespaceToken_Description + { + get => GetString("NamespaceDirective_NamespaceToken_Description"); + } + + /// + /// The namespace for the page. + /// + internal static string FormatNamespaceDirective_NamespaceToken_Description() + => GetString("NamespaceDirective_NamespaceToken_Description"); + + /// + /// Namespace + /// + internal static string NamespaceDirective_NamespaceToken_Name + { + get => GetString("NamespaceDirective_NamespaceToken_Name"); + } + + /// + /// Namespace + /// + internal static string FormatNamespaceDirective_NamespaceToken_Name() + => GetString("NamespaceDirective_NamespaceToken_Name"); + + /// + /// The '@{0}' directive specified in {1} file will not be imported. The directive must appear at the top of each Razor cshtml file. + /// + internal static string PageDirectiveCannotBeImported + { + get => GetString("PageDirectiveCannotBeImported"); + } + + /// + /// The '@{0}' directive specified in {1} file will not be imported. The directive must appear at the top of each Razor cshtml file. + /// + internal static string FormatPageDirectiveCannotBeImported(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("PageDirectiveCannotBeImported"), p0, p1); + + /// + /// The '@{0}' directive must exist at the top of the file. Only comments and whitespace are allowed before the '@{0}' directive. + /// + internal static string PageDirectiveMustExistAtTheTopOfFile + { + get => GetString("PageDirectiveMustExistAtTheTopOfFile"); + } + + /// + /// The '@{0}' directive must exist at the top of the file. Only comments and whitespace are allowed before the '@{0}' directive. + /// + internal static string FormatPageDirectiveMustExistAtTheTopOfFile(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("PageDirectiveMustExistAtTheTopOfFile"), p0); + + /// + /// Mark the page as a Razor Page. + /// + internal static string PageDirective_Description + { + get => GetString("PageDirective_Description"); + } + + /// + /// Mark the page as a Razor Page. + /// + internal static string FormatPageDirective_Description() + => GetString("PageDirective_Description"); + + /// + /// An optional route template for the page. + /// + internal static string PageDirective_RouteToken_Description + { + get => GetString("PageDirective_RouteToken_Description"); + } + + /// + /// An optional route template for the page. + /// + internal static string FormatPageDirective_RouteToken_Description() + => GetString("PageDirective_RouteToken_Description"); + + /// + /// RouteTemplate + /// + internal static string PageDirective_RouteToken_Name + { + get => GetString("PageDirective_RouteToken_Name"); + } + + /// + /// RouteTemplate + /// + internal static string FormatPageDirective_RouteToken_Name() + => GetString("PageDirective_RouteToken_Name"); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Properties/ViewComponentResources.Designer.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Properties/ViewComponentResources.Designer.cs new file mode 100644 index 0000000000..3309adf1c7 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Properties/ViewComponentResources.Designer.cs @@ -0,0 +1,100 @@ +// +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class ViewComponentResources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ViewComponentResources", typeof(ViewComponentResources).GetTypeInfo().Assembly); + + /// + /// View component '{0}' must have exactly one public method named '{1}' or '{2}'. + /// + internal static string ViewComponent_AmbiguousMethods + { + get => GetString("ViewComponent_AmbiguousMethods"); + } + + /// + /// View component '{0}' must have exactly one public method named '{1}' or '{2}'. + /// + internal static string FormatViewComponent_AmbiguousMethods(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AmbiguousMethods"), p0, p1, p2); + + /// + /// Method '{0}' of view component '{1}' should be declared to return {2}&lt;T&gt;. + /// + internal static string ViewComponent_AsyncMethod_ShouldReturnTask + { + get => GetString("ViewComponent_AsyncMethod_ShouldReturnTask"); + } + + /// + /// Method '{0}' of view component '{1}' should be declared to return {2}&lt;T&gt;. + /// + internal static string FormatViewComponent_AsyncMethod_ShouldReturnTask(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_AsyncMethod_ShouldReturnTask"), p0, p1, p2); + + /// + /// Could not find an '{0}' or '{1}' method for the view component '{2}'. + /// + internal static string ViewComponent_CannotFindMethod + { + get => GetString("ViewComponent_CannotFindMethod"); + } + + /// + /// Could not find an '{0}' or '{1}' method for the view component '{2}'. + /// + internal static string FormatViewComponent_CannotFindMethod(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_CannotFindMethod"), p0, p1, p2); + + /// + /// Method '{0}' of view component '{1}' cannot return a {2}. + /// + internal static string ViewComponent_SyncMethod_CannotReturnTask + { + get => GetString("ViewComponent_SyncMethod_CannotReturnTask"); + } + + /// + /// Method '{0}' of view component '{1}' cannot return a {2}. + /// + internal static string FormatViewComponent_SyncMethod_CannotReturnTask(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_SyncMethod_CannotReturnTask"), p0, p1, p2); + + /// + /// Method '{0}' of view component '{1}' should be declared to return a value. + /// + internal static string ViewComponent_SyncMethod_ShouldReturnValue + { + get => GetString("ViewComponent_SyncMethod_ShouldReturnValue"); + } + + /// + /// Method '{0}' of view component '{1}' should be declared to return a value. + /// + internal static string FormatViewComponent_SyncMethod_ShouldReturnValue(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_SyncMethod_ShouldReturnValue"), p0, p1); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/RazorExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/RazorExtensions.cs new file mode 100644 index 0000000000..69cf7e2055 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/RazorExtensions.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public static class RazorExtensions + { + public static void Register(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + InjectDirective.Register(builder); + ModelDirective.Register(builder); + NamespaceDirective.Register(builder); + PageDirective.Register(builder); + + InheritsDirective.Register(builder); + SectionDirective.Register(builder); + + builder.Features.Add(new DefaultTagHelperDescriptorProvider()); + builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); + + builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); + builder.AddTargetExtension(new TemplateTargetExtension() + { + TemplateTypeName = "global::Microsoft.AspNetCore.Mvc.Razor.HelperResult", + }); + + builder.Features.Add(new ModelExpressionPass()); + builder.Features.Add(new PagesPropertyInjectionPass()); + builder.Features.Add(new ViewComponentTagHelperPass()); + builder.Features.Add(new RazorPageDocumentClassifierPass()); + builder.Features.Add(new MvcViewDocumentClassifierPass()); + builder.Features.Add(new AssemblyAttributeInjectionPass()); + builder.Features.Add(new InstrumentationPass()); + + builder.SetImportFeature(new MvcImportProjectFeature()); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/RazorExtensionsDiagnosticFactory.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/RazorExtensionsDiagnosticFactory.cs new file mode 100644 index 0000000000..05a01d38a9 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/RazorExtensionsDiagnosticFactory.cs @@ -0,0 +1,131 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + internal class RazorExtensionsDiagnosticFactory + { + private const string DiagnosticPrefix = "RZ"; + + internal static readonly RazorDiagnosticDescriptor ViewComponent_CannotFindMethod = + new RazorDiagnosticDescriptor( + $"{DiagnosticPrefix}3900", + () => ViewComponentResources.ViewComponent_CannotFindMethod, + RazorDiagnosticSeverity.Error); + + public static RazorDiagnostic CreateViewComponent_CannotFindMethod(string tagHelperType) + { + var diagnostic = RazorDiagnostic.Create( + ViewComponent_CannotFindMethod, + new SourceSpan(SourceLocation.Undefined, contentLength: 0), + ViewComponentTypes.SyncMethodName, + ViewComponentTypes.AsyncMethodName, + tagHelperType); + + return diagnostic; + } + + internal static readonly RazorDiagnosticDescriptor ViewComponent_AmbiguousMethods = + new RazorDiagnosticDescriptor( + $"{DiagnosticPrefix}3901", + () => ViewComponentResources.ViewComponent_AmbiguousMethods, + RazorDiagnosticSeverity.Error); + + public static RazorDiagnostic CreateViewComponent_AmbiguousMethods(string tagHelperType) + { + var diagnostic = RazorDiagnostic.Create( + ViewComponent_AmbiguousMethods, + new SourceSpan(SourceLocation.Undefined, contentLength: 0), + tagHelperType, + ViewComponentTypes.SyncMethodName, + ViewComponentTypes.AsyncMethodName); + + return diagnostic; + } + + internal static readonly RazorDiagnosticDescriptor ViewComponent_AsyncMethod_ShouldReturnTask = + new RazorDiagnosticDescriptor( + $"{DiagnosticPrefix}3902", + () => ViewComponentResources.ViewComponent_AsyncMethod_ShouldReturnTask, + RazorDiagnosticSeverity.Error); + + public static RazorDiagnostic CreateViewComponent_AsyncMethod_ShouldReturnTask(string tagHelperType) + { + var diagnostic = RazorDiagnostic.Create( + ViewComponent_AsyncMethod_ShouldReturnTask, + new SourceSpan(SourceLocation.Undefined, contentLength: 0), + ViewComponentTypes.AsyncMethodName, + tagHelperType, + nameof(Task)); + + return diagnostic; + } + + internal static readonly RazorDiagnosticDescriptor ViewComponent_SyncMethod_ShouldReturnValue = + new RazorDiagnosticDescriptor( + $"{DiagnosticPrefix}3903", + () => ViewComponentResources.ViewComponent_SyncMethod_ShouldReturnValue, + RazorDiagnosticSeverity.Error); + + public static RazorDiagnostic CreateViewComponent_SyncMethod_ShouldReturnValue(string tagHelperType) + { + var diagnostic = RazorDiagnostic.Create( + ViewComponent_SyncMethod_ShouldReturnValue, + new SourceSpan(SourceLocation.Undefined, contentLength: 0), + ViewComponentTypes.SyncMethodName, + tagHelperType); + + return diagnostic; + } + + internal static readonly RazorDiagnosticDescriptor ViewComponent_SyncMethod_CannotReturnTask = + new RazorDiagnosticDescriptor( + $"{DiagnosticPrefix}3904", + () => ViewComponentResources.ViewComponent_SyncMethod_CannotReturnTask, + RazorDiagnosticSeverity.Error); + + public static RazorDiagnostic CreateViewComponent_SyncMethod_CannotReturnTask(string tagHelperType) + { + var diagnostic = RazorDiagnostic.Create( + ViewComponent_SyncMethod_CannotReturnTask, + new SourceSpan(SourceLocation.Undefined, contentLength: 0), + ViewComponentTypes.SyncMethodName, + tagHelperType, + nameof(Task)); + + return diagnostic; + } + + internal static readonly RazorDiagnosticDescriptor PageDirective_CannotBeImported = + new RazorDiagnosticDescriptor( + $"{DiagnosticPrefix}3905", + () => Resources.PageDirectiveCannotBeImported, + RazorDiagnosticSeverity.Error); + + public static RazorDiagnostic CreatePageDirective_CannotBeImported(SourceSpan source) + { + var fileName = Path.GetFileName(source.FilePath); + var diagnostic = RazorDiagnostic.Create(PageDirective_CannotBeImported, source, PageDirective.Directive.Directive, fileName); + + return diagnostic; + } + + internal static readonly RazorDiagnosticDescriptor PageDirective_MustExistAtTheTopOfFile = + new RazorDiagnosticDescriptor( + $"{DiagnosticPrefix}3906", + () => Resources.PageDirectiveMustExistAtTheTopOfFile, + RazorDiagnosticSeverity.Error); + + public static RazorDiagnostic CreatePageDirective_MustExistAtTheTopOfFile(SourceSpan source) + { + var fileName = Path.GetFileName(source.FilePath); + var diagnostic = RazorDiagnostic.Create(PageDirective_MustExistAtTheTopOfFile, source, PageDirective.Directive.Directive); + + return diagnostic; + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/RazorPageDocumentClassifierPass.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/RazorPageDocumentClassifierPass.cs new file mode 100644 index 0000000000..a5de7d0783 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/RazorPageDocumentClassifierPass.cs @@ -0,0 +1,164 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Text; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public class RazorPageDocumentClassifierPass : DocumentClassifierPassBase + { + public static readonly string RazorPageDocumentKind = "mvc.1.0.razor-page"; + public static readonly string RouteTemplateKey = "RouteTemplate"; + + private static readonly RazorProjectEngine LeadingDirectiveParsingEngine = RazorProjectEngine.Create( + RazorConfiguration.Default, + RazorProjectFileSystem.Create("/"), + builder => + { + for (var i = builder.Phases.Count - 1; i >= 0; i--) + { + var phase = builder.Phases[i]; + builder.Phases.RemoveAt(i); + if (phase is IRazorDocumentClassifierPhase) + { + break; + } + } + + RazorExtensions.Register(builder); + builder.Features.Add(new LeadingDirectiveParserOptionsFeature()); + }); + + protected override string DocumentKind => RazorPageDocumentKind; + + protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + return PageDirective.TryGetPageDirective(documentNode, out var pageDirective); + } + + protected override void OnDocumentStructureCreated( + RazorCodeDocument codeDocument, + NamespaceDeclarationIntermediateNode @namespace, + ClassDeclarationIntermediateNode @class, + MethodDeclarationIntermediateNode method) + { + base.OnDocumentStructureCreated(codeDocument, @namespace, @class, method); + + @namespace.Content = "AspNetCore"; + + @class.BaseType = "global::Microsoft.AspNetCore.Mvc.RazorPages.Page"; + + var filePath = codeDocument.Source.RelativePath ?? codeDocument.Source.FilePath; + if (string.IsNullOrEmpty(filePath)) + { + // It's possible for a Razor document to not have a file path. + // Eg. When we try to generate code for an in memory document like default imports. + var checksum = BytesToString(codeDocument.Source.GetChecksum()); + @class.ClassName = $"AspNetCore_{checksum}"; + } + else + { + @class.ClassName = CSharpIdentifier.GetClassNameFromPath(filePath); + } + + @class.Modifiers.Clear(); + @class.Modifiers.Add("public"); + + method.MethodName = "ExecuteAsync"; + method.Modifiers.Clear(); + method.Modifiers.Add("public"); + method.Modifiers.Add("async"); + method.Modifiers.Add("override"); + method.ReturnType = $"global::{typeof(System.Threading.Tasks.Task).FullName}"; + + var document = codeDocument.GetDocumentIntermediateNode(); + PageDirective.TryGetPageDirective(document, out var pageDirective); + + EnsureValidPageDirective(codeDocument, pageDirective); + + AddRouteTemplateMetadataAttribute(@namespace, @class, pageDirective); + } + + private static void AddRouteTemplateMetadataAttribute(NamespaceDeclarationIntermediateNode @namespace, ClassDeclarationIntermediateNode @class, PageDirective pageDirective) + { + if (string.IsNullOrEmpty(pageDirective.RouteTemplate)) + { + return; + } + + var classIndex = @namespace.Children.IndexOf(@class); + if (classIndex == -1) + { + return; + } + + var metadataAttributeNode = new RazorCompiledItemMetadataAttributeIntermediateNode + { + Key = RouteTemplateKey, + Value = pageDirective.RouteTemplate, + }; + // Metadata attributes need to be inserted right before the class declaration. + @namespace.Children.Insert(classIndex, metadataAttributeNode); + } + + private void EnsureValidPageDirective(RazorCodeDocument codeDocument, PageDirective pageDirective) + { + Debug.Assert(pageDirective != null); + + if (pageDirective.DirectiveNode.IsImported()) + { + pageDirective.DirectiveNode.Diagnostics.Add( + RazorExtensionsDiagnosticFactory.CreatePageDirective_CannotBeImported(pageDirective.DirectiveNode.Source.Value)); + } + else + { + // The document contains a page directive and it is not imported. + // We now want to make sure this page directive exists at the top of the file. + // We are going to do that by re-parsing the document until the very first line that is not Razor comment + // or whitespace. We then make sure the page directive still exists in the re-parsed IR tree. + var leadingDirectiveCodeDocument = RazorCodeDocument.Create(codeDocument.Source); + LeadingDirectiveParsingEngine.Engine.Process(leadingDirectiveCodeDocument); + + var leadingDirectiveDocumentNode = leadingDirectiveCodeDocument.GetDocumentIntermediateNode(); + if (!PageDirective.TryGetPageDirective(leadingDirectiveDocumentNode, out var _)) + { + // The page directive is not the leading directive. Add an error. + pageDirective.DirectiveNode.Diagnostics.Add( + RazorExtensionsDiagnosticFactory.CreatePageDirective_MustExistAtTheTopOfFile(pageDirective.DirectiveNode.Source.Value)); + } + } + } + + private class LeadingDirectiveParserOptionsFeature : RazorEngineFeatureBase, IConfigureRazorParserOptionsFeature + { + public int Order { get; } + + public void Configure(RazorParserOptionsBuilder options) + { + options.ParseLeadingDirectives = true; + } + } + + private static string BytesToString(byte[] bytes) + { + if (bytes == null) + { + throw new ArgumentNullException(nameof(bytes)); + } + + var result = new StringBuilder(bytes.Length); + for (var i = 0; i < bytes.Length; i++) + { + // The x2 format means lowercase hex, where each byte is a 2-character string. + result.Append(bytes[i].ToString("x2")); + } + + return result.ToString(); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Resources.resx b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Resources.resx new file mode 100644 index 0000000000..474537197e --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/Resources.resx @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Value cannot be null or empty. + + + Inject a service from the application's service container into a property. + + + The name of the property. + + + PropertyName + + + The type of the service to inject. + + + TypeName + + + Specify the view or page model for the page. + + + The model type. + + + TypeName + + + The 'inherits' keyword is not allowed when a '{0}' keyword is used. + + + A property name must be specified when using the '{0}' statement. Format for a '{0}' statement is '@{0} <Type Name> <Property Name>'. + + + The '{0}' keyword must be followed by a type name on the same line. + + + Only one '{0}' statement is allowed in a file. + + + Invalid tag helper property '{0}.{1}'. Dictionary values must not be of type '{2}'. + + + Specify the base namespace for the page. + + + The namespace for the page. + + + Namespace + + + The '@{0}' directive specified in {1} file will not be imported. The directive must appear at the top of each Razor cshtml file. + + + The '@{0}' directive must precede all other elements defined in a Razor file. + + + Mark the page as a Razor Page. + + + An optional route template for the page. + + + RouteTemplate + + \ No newline at end of file diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/TagHelperDescriptorExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/TagHelperDescriptorExtensions.cs new file mode 100644 index 0000000000..b7bd72ec70 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/TagHelperDescriptorExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public static class TagHelperDescriptorExtensions + { + /// + /// Indicates whether a represents a view component. + /// + /// The to check. + /// Whether a represents a view component. + public static bool IsViewComponentKind(this TagHelperDescriptor tagHelper) + { + if (tagHelper == null) + { + throw new ArgumentNullException(nameof(tagHelper)); + } + + return string.Equals(ViewComponentTagHelperConventions.Kind, tagHelper.Kind, StringComparison.Ordinal); + } + + public static string GetViewComponentName(this TagHelperDescriptor tagHelper) + { + if (tagHelper == null) + { + throw new ArgumentNullException(nameof(tagHelper)); + } + + tagHelper.Metadata.TryGetValue(ViewComponentTagHelperMetadata.Name, out var result); + return result; + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentResources.resx b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentResources.resx new file mode 100644 index 0000000000..d6aad4ff3f --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentResources.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + View component '{0}' must have exactly one public method named '{1}' or '{2}'. + + + Method '{0}' of view component '{1}' should be declared to return {2}&lt;T&gt;. + + + Could not find an '{0}' or '{1}' method for the view component '{2}'. + + + Method '{0}' of view component '{1}' cannot return a {2}. + + + Method '{0}' of view component '{1}' should be declared to return a value. + + \ No newline at end of file diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperConventions.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperConventions.cs new file mode 100644 index 0000000000..30e8d1c36f --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperConventions.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public static class ViewComponentTagHelperConventions + { + public static readonly string Kind = "MVC.ViewComponent"; + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperDescriptorFactory.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperDescriptorFactory.cs new file mode 100644 index 0000000000..0b1221af72 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperDescriptorFactory.cs @@ -0,0 +1,284 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + internal class ViewComponentTagHelperDescriptorFactory + { + private readonly INamedTypeSymbol _viewComponentAttributeSymbol; + private readonly INamedTypeSymbol _genericTaskSymbol; + private readonly INamedTypeSymbol _taskSymbol; + private readonly INamedTypeSymbol _iDictionarySymbol; + + private static readonly SymbolDisplayFormat FullNameTypeDisplayFormat = + SymbolDisplayFormat.FullyQualifiedFormat + .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted) + .WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions & (~SymbolDisplayMiscellaneousOptions.UseSpecialTypes)); + + private static readonly IReadOnlyDictionary PrimitiveDisplayTypeNameLookups = new Dictionary(StringComparer.Ordinal) + { + [typeof(byte).FullName] = "byte", + [typeof(sbyte).FullName] = "sbyte", + [typeof(int).FullName] = "int", + [typeof(uint).FullName] = "uint", + [typeof(short).FullName] = "short", + [typeof(ushort).FullName] = "ushort", + [typeof(long).FullName] = "long", + [typeof(ulong).FullName] = "ulong", + [typeof(float).FullName] = "float", + [typeof(double).FullName] = "double", + [typeof(char).FullName] = "char", + [typeof(bool).FullName] = "bool", + [typeof(object).FullName] = "object", + [typeof(string).FullName] = "string", + [typeof(decimal).FullName] = "decimal", + }; + + public ViewComponentTagHelperDescriptorFactory(Compilation compilation) + { + _viewComponentAttributeSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute); + _genericTaskSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.GenericTask); + _taskSymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.Task); + _iDictionarySymbol = compilation.GetTypeByMetadataName(ViewComponentTypes.IDictionary); + } + + public virtual TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type) + { + var assemblyName = type.ContainingAssembly.Name; + var shortName = GetShortName(type); + var tagName = $"vc:{HtmlConventions.ToHtmlCase(shortName)}"; + var typeName = $"__Generated__{shortName}ViewComponentTagHelper"; + var displayName = shortName + "ViewComponentTagHelper"; + var descriptorBuilder = TagHelperDescriptorBuilder.Create(ViewComponentTagHelperConventions.Kind, typeName, assemblyName); + descriptorBuilder.SetTypeName(typeName); + descriptorBuilder.DisplayName = displayName; + + if (TryFindInvokeMethod(type, out var method, out var diagnostic)) + { + var methodParameters = method.Parameters; + descriptorBuilder.TagMatchingRule(ruleBuilder => + { + ruleBuilder.TagName = tagName; + AddRequiredAttributes(methodParameters, ruleBuilder); + }); + + AddBoundAttributes(methodParameters, displayName, descriptorBuilder); + } + else + { + descriptorBuilder.Diagnostics.Add(diagnostic); + } + + descriptorBuilder.Metadata[ViewComponentTagHelperMetadata.Name] = shortName; + + var descriptor = descriptorBuilder.Build(); + return descriptor; + } + + private bool TryFindInvokeMethod(INamedTypeSymbol type, out IMethodSymbol method, out RazorDiagnostic diagnostic) + { + var methods = GetInvokeMethods(type); + + if (methods.Count == 0) + { + diagnostic = RazorExtensionsDiagnosticFactory.CreateViewComponent_CannotFindMethod(type.ToDisplayString(FullNameTypeDisplayFormat)); + method = null; + return false; + } + else if (methods.Count > 1) + { + diagnostic = RazorExtensionsDiagnosticFactory.CreateViewComponent_AmbiguousMethods(type.ToDisplayString(FullNameTypeDisplayFormat)); + method = null; + return false; + } + + var selectedMethod = methods[0]; + var returnType = selectedMethod.ReturnType as INamedTypeSymbol; + if (string.Equals(selectedMethod.Name, ViewComponentTypes.AsyncMethodName, StringComparison.Ordinal)) + { + // Will invoke asynchronously. Method must not return Task or Task. + if (returnType == _taskSymbol) + { + // This is ok. + } + else if (returnType.IsGenericType && returnType.ConstructedFrom == _genericTaskSymbol) + { + // This is ok. + } + else + { + diagnostic = RazorExtensionsDiagnosticFactory.CreateViewComponent_AsyncMethod_ShouldReturnTask(type.ToDisplayString(FullNameTypeDisplayFormat)); + method = null; + return false; + } + } + else + { + // Will invoke synchronously. Method must not return void, Task or Task. + if (returnType.SpecialType == SpecialType.System_Void) + { + diagnostic = RazorExtensionsDiagnosticFactory.CreateViewComponent_SyncMethod_ShouldReturnValue(type.ToDisplayString(FullNameTypeDisplayFormat)); + method = null; + return false; + } + else if (returnType == _taskSymbol) + { + diagnostic = RazorExtensionsDiagnosticFactory.CreateViewComponent_SyncMethod_CannotReturnTask(type.ToDisplayString(FullNameTypeDisplayFormat)); + method = null; + return false; + } + else if (returnType.IsGenericType && returnType.ConstructedFrom == _genericTaskSymbol) + { + diagnostic = RazorExtensionsDiagnosticFactory.CreateViewComponent_SyncMethod_CannotReturnTask(type.ToDisplayString(FullNameTypeDisplayFormat)); + method = null; + return false; + } + } + + method = selectedMethod; + diagnostic = null; + return true; + } + + private static IReadOnlyList GetInvokeMethods(INamedTypeSymbol type) + { + var methods = new List(); + while (type != null) + { + var currentTypeMethods = type.GetMembers() + .OfType() + .Where(m => + m.DeclaredAccessibility == Accessibility.Public && + !m.IsStatic && + (string.Equals(m.Name, ViewComponentTypes.AsyncMethodName, StringComparison.Ordinal) || + string.Equals(m.Name, ViewComponentTypes.SyncMethodName, StringComparison.Ordinal))); + + methods.AddRange(currentTypeMethods); + + type = type.BaseType; + } + + return methods; + } + + private void AddRequiredAttributes(ImmutableArray methodParameters, TagMatchingRuleDescriptorBuilder builder) + { + foreach (var parameter in methodParameters) + { + if (GetIndexerValueTypeName(parameter) == null) + { + // Set required attributes only for non-indexer attributes. Indexer attributes can't be required attributes + // because there are two ways of setting values for the attribute. + builder.Attribute(attributeBuilder => + { + var lowerKebabName = HtmlConventions.ToHtmlCase(parameter.Name); + attributeBuilder.Name = lowerKebabName; + }); + } + } + } + + private void AddBoundAttributes(ImmutableArray methodParameters, string containingDisplayName, TagHelperDescriptorBuilder builder) + { + foreach (var parameter in methodParameters) + { + var lowerKebabName = HtmlConventions.ToHtmlCase(parameter.Name); + var typeName = parameter.Type.ToDisplayString(FullNameTypeDisplayFormat); + + if (!PrimitiveDisplayTypeNameLookups.TryGetValue(typeName, out var simpleName)) + { + simpleName = typeName; + } + + builder.BindAttribute(attributeBuilder => + { + attributeBuilder.Name = lowerKebabName; + attributeBuilder.TypeName = typeName; + attributeBuilder.DisplayName = $"{simpleName} {containingDisplayName}.{parameter.Name}"; + attributeBuilder.SetPropertyName(parameter.Name); + + if (parameter.Type.TypeKind == TypeKind.Enum) + { + attributeBuilder.IsEnum = true; + } + else + { + var dictionaryValueType = GetIndexerValueTypeName(parameter); + if (dictionaryValueType != null) + { + attributeBuilder.AsDictionary(lowerKebabName + "-", dictionaryValueType); + } + } + }); + } + } + + private string GetIndexerValueTypeName(IParameterSymbol parameter) + { + INamedTypeSymbol dictionaryType; + if ((parameter.Type as INamedTypeSymbol)?.ConstructedFrom == _iDictionarySymbol) + { + dictionaryType = (INamedTypeSymbol)parameter.Type; + } + else if (parameter.Type.AllInterfaces.Any(s => s.ConstructedFrom == _iDictionarySymbol)) + { + dictionaryType = parameter.Type.AllInterfaces.First(s => s.ConstructedFrom == _iDictionarySymbol); + } + else + { + dictionaryType = null; + } + + if (dictionaryType == null || dictionaryType.TypeArguments[0].SpecialType != SpecialType.System_String) + { + return null; + } + + var type = dictionaryType.TypeArguments[1]; + var typeName = type.ToDisplayString(FullNameTypeDisplayFormat); + + return typeName; + } + + private string GetShortName(INamedTypeSymbol componentType) + { + var viewComponentAttribute = componentType.GetAttributes().Where(a => a.AttributeClass == _viewComponentAttributeSymbol).FirstOrDefault(); + var name = viewComponentAttribute + ?.NamedArguments + .Where(namedArgument => string.Equals(namedArgument.Key, ViewComponentTypes.ViewComponent.Name, StringComparison.Ordinal)) + .FirstOrDefault() + .Value + .Value as string; + + if (!string.IsNullOrEmpty(name)) + { + var separatorIndex = name.LastIndexOf('.'); + if (separatorIndex >= 0) + { + return name.Substring(separatorIndex + 1); + } + else + { + return name; + } + } + + // Get name by convention + if (componentType.Name.EndsWith(ViewComponentTypes.ViewComponentSuffix, StringComparison.OrdinalIgnoreCase)) + { + return componentType.Name.Substring(0, componentType.Name.Length - ViewComponentTypes.ViewComponentSuffix.Length); + } + else + { + return componentType.Name; + } + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperDescriptorProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperDescriptorProvider.cs new file mode 100644 index 0000000000..4806acc806 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperDescriptorProvider.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public sealed class ViewComponentTagHelperDescriptorProvider : RazorEngineFeatureBase, ITagHelperDescriptorProvider + { + public int Order { get; set; } + + public void Execute(TagHelperDescriptorProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var compilation = context.GetCompilation(); + if (compilation == null) + { + // No compilation, nothing to do. + return; + } + + var vcAttribute = compilation.GetTypeByMetadataName(ViewComponentTypes.ViewComponentAttribute); + var nonVCAttribute = compilation.GetTypeByMetadataName(ViewComponentTypes.NonViewComponentAttribute); + if (vcAttribute == null || vcAttribute.TypeKind == TypeKind.Error) + { + // Could not find attributes we care about in the compilation. Nothing to do. + return; + } + + var types = new List(); + var visitor = new ViewComponentTypeVisitor(vcAttribute, nonVCAttribute, types); + + // We always visit the global namespace. + visitor.Visit(compilation.Assembly.GlobalNamespace); + + foreach (var reference in compilation.References) + { + if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly) + { + if (IsTagHelperAssembly(assembly)) + { + visitor.Visit(assembly.GlobalNamespace); + } + } + } + + var factory = new ViewComponentTagHelperDescriptorFactory(compilation); + for (var i = 0; i < types.Count; i++) + { + var descriptor = factory.CreateDescriptor(types[i]); + + if (descriptor != null) + { + context.Results.Add(descriptor); + } + } + } + + private bool IsTagHelperAssembly(IAssemblySymbol assembly) + { + return assembly.Name != null && !assembly.Name.StartsWith("System.", StringComparison.Ordinal); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperIntermediateNode.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperIntermediateNode.cs new file mode 100644 index 0000000000..a42b88416b --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperIntermediateNode.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public sealed class ViewComponentTagHelperIntermediateNode : ExtensionIntermediateNode + { + public override IntermediateNodeCollection Children { get; } = IntermediateNodeCollection.ReadOnly; + + public string ClassName { get; set; } + + public TagHelperDescriptor TagHelper { get; set; } + + public override void Accept(IntermediateNodeVisitor visitor) + { + if (visitor == null) + { + throw new ArgumentNullException(nameof(visitor)); + } + + AcceptExtensionNode(this, visitor); + } + + public override void WriteNode(CodeTarget target, CodeRenderingContext context) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var extension = target.GetExtension(); + if (extension == null) + { + ReportMissingCodeTargetExtension(context); + return; + } + + extension.WriteViewComponentTagHelper(context, this); + } + + public override void FormatNode(IntermediateNodeFormatter formatter) + { + formatter.WriteContent(ClassName); + + formatter.WriteProperty(nameof(ClassName), ClassName); + formatter.WriteProperty(nameof(TagHelper), TagHelper?.DisplayName); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperMetadata.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperMetadata.cs new file mode 100644 index 0000000000..ad77c7a29e --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperMetadata.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public static class ViewComponentTagHelperMetadata + { + /// + /// The key in a containing + /// the short name of a view component. + /// + public static readonly string Name = "MVC.ViewComponent.Name"; + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperPass.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperPass.cs new file mode 100644 index 0000000000..f5c37eb13b --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperPass.cs @@ -0,0 +1,203 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + public class ViewComponentTagHelperPass : IntermediateNodePassBase, IRazorOptimizationPass + { + // Run after the default taghelper pass + public override int Order => IntermediateNodePassBase.DefaultFeatureOrder + 2000; + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + var @namespace = documentNode.FindPrimaryNamespace(); + var @class = documentNode.FindPrimaryClass(); + if (@namespace == null || @class == null) + { + // Nothing to do, bail. We can't function without the standard structure. + return; + } + + var context = new Context(@namespace, @class); + + // For each VCTH *usage* we need to rewrite the tag helper node to use the tag helper runtime to construct + // and set properties on the the correct field, and using the name of the type we will generate. + var nodes = documentNode.FindDescendantNodes(); + for (var i = 0; i < nodes.Count; i++) + { + var node = nodes[i]; + foreach (var tagHelper in node.TagHelpers) + { + RewriteUsage(context, node, tagHelper); + } + } + + // Then for each VCTH *definition* that we've seen we need to generate the class that implements + // ITagHelper and the field that will hold it. + foreach (var tagHelper in context.TagHelpers) + { + AddField(context, tagHelper); + AddTagHelperClass(context, tagHelper); + } + } + + private void RewriteUsage(Context context, TagHelperIntermediateNode node, TagHelperDescriptor tagHelper) + { + if (!tagHelper.IsViewComponentKind()) + { + return; + } + + context.Add(tagHelper); + + // Now we need to insert a create node using the default tag helper runtime. This is similar to + // code in DefaultTagHelperOptimizationPass. + // + // Find the body node. + var i = 0; + while (i < node.Children.Count && node.Children[i] is TagHelperBodyIntermediateNode) + { + i++; + } + while (i < node.Children.Count && node.Children[i] is DefaultTagHelperBodyIntermediateNode) + { + i++; + } + + // Now find the last create node. + while (i < node.Children.Count && node.Children[i] is DefaultTagHelperCreateIntermediateNode) + { + i++; + } + + // Now i has the right insertion point. + node.Children.Insert(i, new DefaultTagHelperCreateIntermediateNode() + { + FieldName = context.GetFieldName(tagHelper), + TagHelper = tagHelper, + TypeName = context.GetFullyQualifiedName(tagHelper), + }); + + // Now we need to rewrite any set property nodes to use the default runtime. + for (i = 0; i < node.Children.Count; i++) + { + if (node.Children[i] is TagHelperPropertyIntermediateNode propertyNode && + propertyNode.TagHelper == tagHelper) + { + // This is a set property for this VCTH - we need to replace it with a node + // that will use our field and property name. + node.Children[i] = new DefaultTagHelperPropertyIntermediateNode(propertyNode) + { + FieldName = context.GetFieldName(tagHelper), + PropertyName = propertyNode.BoundAttribute.GetPropertyName(), + }; + } + } + } + + private void AddField(Context context, TagHelperDescriptor tagHelper) + { + // We need to insert a node for the field that will hold the tag helper. We've already generated a field name + // at this time and use it for all uses of the same tag helper type. + // + // We also want to preserve the ordering of the nodes for testability. So insert at the end of any existing + // field nodes. + var i = 0; + while (i < context.Class.Children.Count && context.Class.Children[i] is DefaultTagHelperRuntimeIntermediateNode) + { + i++; + } + + while (i < context.Class.Children.Count && context.Class.Children[i] is FieldDeclarationIntermediateNode) + { + i++; + } + + context.Class.Children.Insert(i, new FieldDeclarationIntermediateNode() + { + Annotations = + { + { CommonAnnotations.DefaultTagHelperExtension.TagHelperField, bool.TrueString }, + }, + Modifiers = + { + "private", + }, + FieldName = context.GetFieldName(tagHelper), + FieldType = "global::" + context.GetFullyQualifiedName(tagHelper), + }); + } + + private void AddTagHelperClass(Context context, TagHelperDescriptor tagHelper) + { + var node = new ViewComponentTagHelperIntermediateNode() + { + ClassName = context.GetClassName(tagHelper), + TagHelper = tagHelper + }; + + context.Class.Children.Add(node); + } + + private struct Context + { + private Dictionary _tagHelpers; + + public Context(NamespaceDeclarationIntermediateNode @namespace, ClassDeclarationIntermediateNode @class) + { + Namespace = @namespace; + Class = @class; + + _tagHelpers = new Dictionary(); + } + + public ClassDeclarationIntermediateNode Class { get; } + + public NamespaceDeclarationIntermediateNode Namespace { get; } + + + public IEnumerable TagHelpers => _tagHelpers.Keys; + + public bool Add(TagHelperDescriptor tagHelper) + { + if (_tagHelpers.ContainsKey(tagHelper)) + { + return false; + } + + var className = $"__Generated__{tagHelper.GetViewComponentName()}ViewComponentTagHelper"; + var fullyQualifiedName = $"{Namespace.Content}.{Class.ClassName}.{className}"; + var fieldName = GenerateFieldName(tagHelper); + + _tagHelpers.Add(tagHelper, (className, fullyQualifiedName, fieldName)); + + return true; + } + + public string GetClassName(TagHelperDescriptor taghelper) + { + return _tagHelpers[taghelper].className; + } + + public string GetFullyQualifiedName(TagHelperDescriptor taghelper) + { + return _tagHelpers[taghelper].fullyQualifiedName; + } + + public string GetFieldName(TagHelperDescriptor taghelper) + { + return _tagHelpers[taghelper].fieldName; + } + + private static string GenerateFieldName(TagHelperDescriptor tagHelper) + { + return $"__{tagHelper.GetViewComponentName()}ViewComponentTagHelper"; + } + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperTargetExtension.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperTargetExtension.cs new file mode 100644 index 0000000000..a56e86fdca --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTagHelperTargetExtension.cs @@ -0,0 +1,183 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + internal class ViewComponentTagHelperTargetExtension : IViewComponentTagHelperTargetExtension + { + private static readonly string[] PublicModifiers = new[] { "public" }; + + public string TagHelperTypeName { get; set; } = "Microsoft.AspNetCore.Razor.TagHelpers.TagHelper"; + + public string ViewComponentHelperTypeName { get; set; } = "global::Microsoft.AspNetCore.Mvc.IViewComponentHelper"; + + public string ViewComponentHelperVariableName { get; set; } = "_helper"; + + public string ViewComponentInvokeMethodName { get; set; } = "InvokeAsync"; + + public string HtmlAttributeNotBoundAttributeTypeName { get; set; } = "Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute"; + + public string ViewContextAttributeTypeName { get; set; } = "global::Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute"; + + public string ViewContextTypeName { get; set; } = "global::Microsoft.AspNetCore.Mvc.Rendering.ViewContext"; + + public string ViewContextPropertyName { get; set; } = "ViewContext"; + + public string HtmlTargetElementAttributeTypeName { get; set; } = "Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute"; + + public string TagHelperProcessMethodName { get; set; } = "ProcessAsync"; + + public string TagHelperContextTypeName { get; set; } = "Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext"; + + public string TagHelperContextVariableName { get; set; } = "context"; + + public string TagHelperOutputTypeName { get; set; } = "Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput"; + + public string TagHelperOutputVariableName { get; set; } = "output"; + + public string TagHelperOutputTagNamePropertyName { get; set; } = "TagName"; + + public string TagHelperOutputContentPropertyName { get; set; } = "Content"; + + public string TagHelperContentSetMethodName { get; set; } = "SetHtmlContent"; + + public string TagHelperContentVariableName { get; set; } = "content"; + + public string IViewContextAwareTypeName { get; set; } = "global::Microsoft.AspNetCore.Mvc.ViewFeatures.IViewContextAware"; + + public string IViewContextAwareContextualizeMethodName { get; set; } = "Contextualize"; + + public void WriteViewComponentTagHelper(CodeRenderingContext context, ViewComponentTagHelperIntermediateNode node) + { + // Add target element. + WriteTargetElementString(context.CodeWriter, node.TagHelper); + + // Initialize declaration. + using (context.CodeWriter.BuildClassDeclaration( + PublicModifiers, + node.ClassName, + TagHelperTypeName, + interfaces: null, + typeParameters: null)) + { + // Add view component helper. + context.CodeWriter.WriteVariableDeclaration( + $"private readonly {ViewComponentHelperTypeName}", + ViewComponentHelperVariableName, + value: null); + + // Add constructor. + WriteConstructorString(context.CodeWriter, node.ClassName); + + // Add attributes. + WriteAttributeDeclarations(context.CodeWriter, node.TagHelper); + + // Add process method. + WriteProcessMethodString(context.CodeWriter, node.TagHelper); + } + } + + private void WriteConstructorString(CodeWriter writer, string className) + { + writer.Write("public ") + .Write(className) + .Write("(") + .Write($"{ViewComponentHelperTypeName} helper") + .WriteLine(")"); + using (writer.BuildScope()) + { + writer.WriteStartAssignment(ViewComponentHelperVariableName) + .Write("helper") + .WriteLine(";"); + } + } + + private void WriteAttributeDeclarations(CodeWriter writer, TagHelperDescriptor tagHelper) + { + writer.Write("[") + .Write(HtmlAttributeNotBoundAttributeTypeName) + .WriteParameterSeparator() + .Write(ViewContextAttributeTypeName) + .WriteLine("]"); + + writer.WriteAutoPropertyDeclaration( + PublicModifiers, + ViewContextTypeName, + ViewContextPropertyName); + + foreach (var attribute in tagHelper.BoundAttributes) + { + writer.WriteAutoPropertyDeclaration( + PublicModifiers, + attribute.TypeName, + attribute.GetPropertyName()); + + if (attribute.IndexerTypeName != null) + { + writer.Write(" = ") + .WriteStartNewObject(attribute.TypeName) + .WriteEndMethodInvocation(); + } + } + } + + private void WriteProcessMethodString(CodeWriter writer, TagHelperDescriptor tagHelper) + { + using (writer.BuildMethodDeclaration( + $"public override async", + $"global::{typeof(Task).FullName}", + TagHelperProcessMethodName, + new Dictionary() + { + { TagHelperContextTypeName, TagHelperContextVariableName }, + { TagHelperOutputTypeName, TagHelperOutputVariableName } + })) + { + writer.WriteInstanceMethodInvocation( + $"({ViewComponentHelperVariableName} as {IViewContextAwareTypeName})?", + IViewContextAwareContextualizeMethodName, + new[] { ViewContextPropertyName }); + + var methodParameters = GetMethodParameters(tagHelper); + writer.Write("var ") + .WriteStartAssignment(TagHelperContentVariableName) + .WriteInstanceMethodInvocation($"await {ViewComponentHelperVariableName}", ViewComponentInvokeMethodName, methodParameters); + writer.WriteStartAssignment($"{TagHelperOutputVariableName}.{TagHelperOutputTagNamePropertyName}") + .WriteLine("null;"); + writer.WriteInstanceMethodInvocation( + $"{TagHelperOutputVariableName}.{TagHelperOutputContentPropertyName}", + TagHelperContentSetMethodName, + new[] { TagHelperContentVariableName }); + } + } + + private string[] GetMethodParameters(TagHelperDescriptor tagHelper) + { + var propertyNames = tagHelper.BoundAttributes.Select(attribute => attribute.GetPropertyName()); + var joinedPropertyNames = string.Join(", ", propertyNames); + var parametersString = $"new {{ { joinedPropertyNames } }}"; + var viewComponentName = tagHelper.GetViewComponentName(); + var methodParameters = new[] { $"\"{viewComponentName}\"", parametersString }; + return methodParameters; + } + + private void WriteTargetElementString(CodeWriter writer, TagHelperDescriptor tagHelper) + { + Debug.Assert(tagHelper.TagMatchingRules.Count() == 1); + + var rule = tagHelper.TagMatchingRules.First(); + + writer.Write("[") + .WriteStartMethodInvocation(HtmlTargetElementAttributeTypeName) + .WriteStringLiteral(rule.TagName) + .WriteLine(")]"); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTypeVisitor.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTypeVisitor.cs new file mode 100644 index 0000000000..01eb3fffaa --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTypeVisitor.cs @@ -0,0 +1,88 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + internal class ViewComponentTypeVisitor : SymbolVisitor + { + private readonly INamedTypeSymbol _viewComponentAttribute; + private readonly INamedTypeSymbol _nonViewComponentAttribute; + private readonly List _results; + + public ViewComponentTypeVisitor( + INamedTypeSymbol viewComponentAttribute, + INamedTypeSymbol nonViewComponentAttribute, + List results) + { + _viewComponentAttribute = viewComponentAttribute; + _nonViewComponentAttribute = nonViewComponentAttribute; + _results = results; + } + + public override void VisitNamedType(INamedTypeSymbol symbol) + { + if (IsViewComponent(symbol)) + { + _results.Add(symbol); + } + + if (symbol.DeclaredAccessibility != Accessibility.Public) + { + return; + } + + foreach (var member in symbol.GetTypeMembers()) + { + Visit(member); + } + } + + public override void VisitNamespace(INamespaceSymbol symbol) + { + foreach (var member in symbol.GetMembers()) + { + Visit(member); + } + } + + internal bool IsViewComponent(INamedTypeSymbol symbol) + { + if (_viewComponentAttribute == null) + { + return false; + } + + if (symbol.DeclaredAccessibility != Accessibility.Public || + symbol.IsAbstract || + symbol.IsGenericType || + AttributeIsDefined(symbol, _nonViewComponentAttribute)) + { + return false; + } + + return symbol.Name.EndsWith(ViewComponentTypes.ViewComponentSuffix) || + AttributeIsDefined(symbol, _viewComponentAttribute); + } + + private static bool AttributeIsDefined(INamedTypeSymbol type, INamedTypeSymbol queryAttribute) + { + if (type == null || queryAttribute == null) + { + return false; + } + + var attribute = type.GetAttributes().Where(a => a.AttributeClass == queryAttribute).FirstOrDefault(); + + if (attribute != null) + { + return true; + } + + return AttributeIsDefined(type.BaseType, queryAttribute); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTypes.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTypes.cs new file mode 100644 index 0000000000..cd7e2771be --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/ViewComponentTypes.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X +{ + internal static class ViewComponentTypes + { + public const string Assembly = "Microsoft.AspNetCore.Mvc.ViewFeatures"; + + public static readonly Version AssemblyVersion = new Version(1, 1, 0, 0); + + public const string ViewComponentSuffix = "ViewComponent"; + + public const string ViewComponentAttribute = "Microsoft.AspNetCore.Mvc.ViewComponentAttribute"; + + public const string NonViewComponentAttribute = "Microsoft.AspNetCore.Mvc.NonViewComponentAttribute"; + + public const string GenericTask = "System.Threading.Tasks.Task`1"; + + public const string Task = "System.Threading.Tasks.Task"; + + public const string IDictionary = "System.Collections.Generic.IDictionary`2"; + + public const string AsyncMethodName = "InvokeAsync"; + + public const string SyncMethodName = "Invoke"; + + public static class ViewComponent + { + public const string Name = "Name"; + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/baseline.netcore.json b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/baseline.netcore.json new file mode 100644 index 0000000000..87b3d074f3 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/baseline.netcore.json @@ -0,0 +1,1180 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.AssemblyAttributeInjectionPass", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.IntermediateNodePassBase", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass" + ], + "Members": [ + { + "Kind": "Method", + "Name": "ExecuteCore", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.IInjectTargetExtension", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.CodeGeneration.ICodeTargetExtension" + ], + "Members": [ + { + "Kind": "Method", + "Name": "WriteInjectProperty", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeRenderingContext" + }, + { + "Name": "node", + "Type": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.InjectIntermediateNode" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.InjectDirective", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Register", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorProjectEngineBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.RazorProjectEngineBuilder", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Register", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Directive", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.DirectiveDescriptor", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.InjectIntermediateNode", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.Intermediate.ExtensionIntermediateNode", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_TypeName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TypeName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MemberName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MemberName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Children", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.Intermediate.IntermediateNodeCollection", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Accept", + "Parameters": [ + { + "Name": "visitor", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.IntermediateNodeVisitor" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteNode", + "Parameters": [ + { + "Name": "target", + "Type": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeTarget" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeRenderingContext" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.InjectTargetExtension", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.IInjectTargetExtension" + ], + "Members": [ + { + "Kind": "Method", + "Name": "WriteInjectProperty", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeRenderingContext" + }, + { + "Name": "node", + "Type": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.InjectIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.IInjectTargetExtension", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.InstrumentationPass", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.IntermediateNodePassBase", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Order", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "ImplementedInterface": "Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ExecuteCore", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.IViewComponentTagHelperTargetExtension", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.CodeGeneration.ICodeTargetExtension" + ], + "Members": [ + { + "Kind": "Method", + "Name": "WriteViewComponentTagHelper", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeRenderingContext" + }, + { + "Name": "node", + "Type": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ViewComponentTagHelperIntermediateNode" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ModelDirective", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Register", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorProjectEngineBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.RazorProjectEngineBuilder", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetModelType", + "Parameters": [ + { + "Name": "document", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Register", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Directive", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.DirectiveDescriptor", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ModelExpressionPass", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.IntermediateNodePassBase", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass" + ], + "Members": [ + { + "Kind": "Method", + "Name": "ExecuteCore", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.MvcViewDocumentClassifierPass", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.DocumentClassifierPassBase", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_DocumentKind", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsMatch", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnDocumentStructureCreated", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "namespace", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.NamespaceDeclarationIntermediateNode" + }, + { + "Name": "class", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.ClassDeclarationIntermediateNode" + }, + { + "Name": "method", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.MethodDeclarationIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "MvcViewDocumentKind", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.NamespaceDirective", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Register", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorProjectEngineBuilder" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Register", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Directive", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.DirectiveDescriptor", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.PageDirective", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_RouteTemplate", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DirectiveNode", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.Intermediate.IntermediateNode", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Register", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorProjectEngineBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.RazorProjectEngineBuilder", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetPageDirective", + "Parameters": [ + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + }, + { + "Name": "pageDirective", + "Type": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.PageDirective", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Register", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Directive", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.DirectiveDescriptor", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.PagesPropertyInjectionPass", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.IntermediateNodePassBase", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass" + ], + "Members": [ + { + "Kind": "Method", + "Name": "ExecuteCore", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.RazorExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Register", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorProjectEngineBuilder" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.RazorPageDocumentClassifierPass", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.DocumentClassifierPassBase", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_DocumentKind", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsMatch", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnDocumentStructureCreated", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "namespace", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.NamespaceDeclarationIntermediateNode" + }, + { + "Name": "class", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.ClassDeclarationIntermediateNode" + }, + { + "Name": "method", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.MethodDeclarationIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "RazorPageDocumentKind", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "RouteTemplateKey", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.TagHelperDescriptorExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "IsViewComponentKind", + "Parameters": [ + { + "Name": "tagHelper", + "Type": "Microsoft.AspNetCore.Razor.Language.TagHelperDescriptor" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetViewComponentName", + "Parameters": [ + { + "Name": "tagHelper", + "Type": "Microsoft.AspNetCore.Razor.Language.TagHelperDescriptor" + } + ], + "ReturnType": "System.String", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ViewComponentTagHelperConventions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Kind", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ViewComponentTagHelperDescriptorProvider", + "Visibility": "Public", + "Kind": "Class", + "Sealed": true, + "BaseType": "Microsoft.AspNetCore.Razor.Language.RazorEngineFeatureBase", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.ITagHelperDescriptorProvider" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Order", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Razor.Language.ITagHelperDescriptorProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Order", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Execute", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Razor.Language.TagHelperDescriptorProviderContext" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Razor.Language.ITagHelperDescriptorProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ViewComponentTagHelperIntermediateNode", + "Visibility": "Public", + "Kind": "Class", + "Sealed": true, + "BaseType": "Microsoft.AspNetCore.Razor.Language.Intermediate.ExtensionIntermediateNode", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Children", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.Intermediate.IntermediateNodeCollection", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_ClassName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_ClassName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_TagHelper", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.TagHelperDescriptor", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TagHelper", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Razor.Language.TagHelperDescriptor" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Accept", + "Parameters": [ + { + "Name": "visitor", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.IntermediateNodeVisitor" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteNode", + "Parameters": [ + { + "Name": "target", + "Type": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeTarget" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeRenderingContext" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ViewComponentTagHelperMetadata", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Name", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ViewComponentTagHelperPass", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.IntermediateNodePassBase", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Order", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "ImplementedInterface": "Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ExecuteCore", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeWriterExtensions+CSharpCodeWritingScope", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.IDisposable" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "writer", + "Type": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeWriter" + }, + { + "Name": "tabSize", + "Type": "System.Int32", + "DefaultValue": "4" + }, + { + "Name": "autoSpace", + "Type": "System.Boolean", + "DefaultValue": "True" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ViewComponentTypes+ViewComponent", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Name", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Name\"" + } + ], + "GenericParameters": [] + } + ] +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/baseline.netframework.json b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/baseline.netframework.json new file mode 100644 index 0000000000..9386496226 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X/baseline.netframework.json @@ -0,0 +1,982 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.AssemblyAttributeInjectionPass", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.IntermediateNodePassBase", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass" + ], + "Members": [ + { + "Kind": "Method", + "Name": "ExecuteCore", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.IInjectTargetExtension", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.CodeGeneration.ICodeTargetExtension" + ], + "Members": [ + { + "Kind": "Method", + "Name": "WriteInjectProperty", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeRenderingContext" + }, + { + "Name": "node", + "Type": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.InjectIntermediateNode" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.InjectDirective", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Register", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Directive", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.DirectiveDescriptor", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.InjectIntermediateNode", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.Intermediate.ExtensionIntermediateNode", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_TypeName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_TypeName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MemberName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MemberName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Children", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.Intermediate.IntermediateNodeCollection", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Accept", + "Parameters": [ + { + "Name": "visitor", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.IntermediateNodeVisitor" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteNode", + "Parameters": [ + { + "Name": "target", + "Type": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeTarget" + }, + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeRenderingContext" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.InjectTargetExtension", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.IInjectTargetExtension" + ], + "Members": [ + { + "Kind": "Method", + "Name": "WriteInjectProperty", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeRenderingContext" + }, + { + "Name": "node", + "Type": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.InjectIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.IInjectTargetExtension", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.InstrumentationPass", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.IntermediateNodePassBase", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Order", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "ImplementedInterface": "Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ExecuteCore", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ModelDirective", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Register", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetModelType", + "Parameters": [ + { + "Name": "document", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Directive", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.DirectiveDescriptor", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ModelExpressionPass", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.IntermediateNodePassBase", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass" + ], + "Members": [ + { + "Kind": "Method", + "Name": "ExecuteCore", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.MvcViewDocumentClassifierPass", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.DocumentClassifierPassBase", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_DocumentKind", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsMatch", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnDocumentStructureCreated", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "namespace", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.NamespaceDeclarationIntermediateNode" + }, + { + "Name": "class", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.ClassDeclarationIntermediateNode" + }, + { + "Name": "method", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.MethodDeclarationIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "MvcViewDocumentKind", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.NamespaceDirective", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Register", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Directive", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.DirectiveDescriptor", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.PageDirective", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_RouteTemplate", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_DirectiveNode", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.Intermediate.IntermediateNode", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Register", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "TryGetPageDirective", + "Parameters": [ + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + }, + { + "Name": "pageDirective", + "Type": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.PageDirective", + "Direction": "Out" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Directive", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Razor.Language.DirectiveDescriptor", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.PagesPropertyInjectionPass", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.IntermediateNodePassBase", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass" + ], + "Members": [ + { + "Kind": "Method", + "Name": "ExecuteCore", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.RazorExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Register", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorProjectEngineBuilder" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.RazorPageDocumentClassifierPass", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.DocumentClassifierPassBase", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_DocumentKind", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "IsMatch", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.Boolean", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "OnDocumentStructureCreated", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "namespace", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.NamespaceDeclarationIntermediateNode" + }, + { + "Name": "class", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.ClassDeclarationIntermediateNode" + }, + { + "Name": "method", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.MethodDeclarationIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "RazorPageDocumentKind", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.TagHelperDescriptorExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "IsViewComponentKind", + "Parameters": [ + { + "Name": "tagHelper", + "Type": "Microsoft.AspNetCore.Razor.Language.TagHelperDescriptor" + } + ], + "ReturnType": "System.Boolean", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "GetViewComponentName", + "Parameters": [ + { + "Name": "tagHelper", + "Type": "Microsoft.AspNetCore.Razor.Language.TagHelperDescriptor" + } + ], + "ReturnType": "System.String", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ViewComponentTagHelperConventions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Kind", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ViewComponentTagHelperDescriptorProvider", + "Visibility": "Public", + "Kind": "Class", + "Sealed": true, + "BaseType": "Microsoft.AspNetCore.Razor.Language.RazorEngineFeatureBase", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.ITagHelperDescriptorProvider" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Order", + "Parameters": [], + "ReturnType": "System.Int32", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Razor.Language.ITagHelperDescriptorProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Order", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Execute", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Razor.Language.TagHelperDescriptorProviderContext" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Razor.Language.ITagHelperDescriptorProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ViewComponentTagHelperMetadata", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Name", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ViewComponentTagHelperPass", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Razor.Language.IntermediateNodePassBase", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Order", + "Parameters": [], + "ReturnType": "System.Int32", + "Virtual": true, + "Override": true, + "ImplementedInterface": "Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ExecuteCore", + "Parameters": [ + { + "Name": "codeDocument", + "Type": "Microsoft.AspNetCore.Razor.Language.RazorCodeDocument" + }, + { + "Name": "documentNode", + "Type": "Microsoft.AspNetCore.Razor.Language.Intermediate.DocumentIntermediateNode" + } + ], + "ReturnType": "System.Void", + "Virtual": true, + "Override": true, + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeWriterExtensions+CSharpCodeWritingScope", + "Visibility": "Public", + "Kind": "Struct", + "Sealed": true, + "ImplementedInterfaces": [ + "System.IDisposable" + ], + "Members": [ + { + "Kind": "Method", + "Name": "Dispose", + "Parameters": [], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "System.IDisposable", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "writer", + "Type": "Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeWriter" + }, + { + "Name": "tabSize", + "Type": "System.Int32", + "DefaultValue": "4" + }, + { + "Name": "autoSpace", + "Type": "System.Boolean", + "DefaultValue": "True" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X.ViewComponentTypes+ViewComponent", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Field", + "Name": "Name", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [], + "Constant": true, + "Literal": "\"Name\"" + } + ], + "GenericParameters": [] + } + ] +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ExtensionInitializer.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ExtensionInitializer.cs index eeb3246f74..8c650ebedb 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ExtensionInitializer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ExtensionInitializer.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Components; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InjectDirective.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InjectDirective.cs index fcb874dc29..4282ea559f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InjectDirective.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InjectDirective.cs @@ -44,6 +44,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) { + if (documentNode.DocumentKind != RazorPageDocumentClassifierPass.RazorPageDocumentKind && + documentNode.DocumentKind != MvcViewDocumentClassifierPass.MvcViewDocumentKind) + { + // Not a MVC file. Skip. + return; + } + var visitor = new Visitor(); visitor.Visit(documentNode); var modelType = ModelDirective.GetModelType(documentNode); diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj index 401d07c030..afa5ecd679 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj @@ -2,19 +2,14 @@ ASP.NET Core design time hosting infrastructure for the Razor view engine. - net46;netstandard2.0 + netstandard2.0 $(PackageTags);aspnetcoremvc - - Shared\CodeWriterExtensions.cs - - - - - - + + + diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/MvcViewDocumentClassifierPass.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/MvcViewDocumentClassifierPass.cs index 57e9ad23a2..975cf90666 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/MvcViewDocumentClassifierPass.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/MvcViewDocumentClassifierPass.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Text; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Intermediate; @@ -31,12 +30,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { // It's possible for a Razor document to not have a file path. // Eg. When we try to generate code for an in memory document like default imports. - var checksum = BytesToString(codeDocument.Source.GetChecksum()); + var checksum = Checksum.BytesToString(codeDocument.Source.GetChecksum()); @class.ClassName = $"AspNetCore_{checksum}"; } else { - @class.ClassName = CSharpIdentifier.GetClassNameFromPath(filePath); + @class.ClassName = GetClassNameFromPath(filePath); } @class.BaseType = "global::Microsoft.AspNetCore.Mvc.Razor.RazorPage"; @@ -51,21 +50,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions method.ReturnType = $"global::{typeof(System.Threading.Tasks.Task).FullName}"; } - private static string BytesToString(byte[] bytes) + private static string GetClassNameFromPath(string path) { - if (bytes == null) + const string cshtmlExtension = ".cshtml"; + + if (string.IsNullOrEmpty(path)) { - throw new ArgumentNullException(nameof(bytes)); + return path; } - var result = new StringBuilder(bytes.Length); - for (var i = 0; i < bytes.Length; i++) + if (path.EndsWith(cshtmlExtension, StringComparison.OrdinalIgnoreCase)) { - // The x2 format means lowercase hex, where each byte is a 2-character string. - result.Append(bytes[i].ToString("x2")); + path = path.Substring(0, path.Length - cshtmlExtension.Length); } - return result.ToString(); + return CSharpIdentifier.SanitizeIdentifier(path); } } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/NamespaceDirective.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/NamespaceDirective.cs index ea3bc40bc1..1a809c0629 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/NamespaceDirective.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/NamespaceDirective.cs @@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions for (var i = 0; i < segments.Length - 1; i++) { builder.Append('.'); - builder.Append(CSharpIdentifier.SanitizeClassName(segments[i])); + builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[i])); } return builder.ToString(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs index b5caca17dd..7d8a47bc67 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs @@ -5,8 +5,7 @@ using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; -[assembly: ProvideRazorExtensionInitializer("MVC-2.0", typeof(ExtensionInitializer))] -[assembly: ProvideRazorExtensionInitializer("MVC-2.1", typeof(ExtensionInitializer))] +[assembly: ProvideRazorExtensionInitializer("MVC-3.0", typeof(ExtensionInitializer))] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs index ff5fcaad68..04febb17d6 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs @@ -22,7 +22,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions NamespaceDirective.Register(builder); PageDirective.Register(builder); - FunctionsDirective.Register(builder); InheritsDirective.Register(builder); SectionDirective.Register(builder); @@ -41,50 +40,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions builder.Features.Add(new RazorPageDocumentClassifierPass()); builder.Features.Add(new MvcViewDocumentClassifierPass()); builder.Features.Add(new AssemblyAttributeInjectionPass()); - builder.Features.Add(new InstrumentationPass()); builder.SetImportFeature(new MvcImportProjectFeature()); } - - #region Obsolete - [Obsolete("This method is obsolete and will be removed in a future version.")] - public static void Register(IRazorEngineBuilder builder) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - InjectDirective.Register(builder); - ModelDirective.Register(builder); - NamespaceDirective.Register(builder); - PageDirective.Register(builder); - - FunctionsDirective.Register(builder); - InheritsDirective.Register(builder); - SectionDirective.Register(builder); - - builder.Features.Add(new DefaultTagHelperDescriptorProvider()); - builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); - - builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); - builder.AddTargetExtension(new TemplateTargetExtension() - { - TemplateTypeName = "global::Microsoft.AspNetCore.Mvc.Razor.HelperResult", - }); - - builder.Features.Add(new ModelExpressionPass()); - builder.Features.Add(new PagesPropertyInjectionPass()); - builder.Features.Add(new ViewComponentTagHelperPass()); - builder.Features.Add(new RazorPageDocumentClassifierPass()); - builder.Features.Add(new MvcViewDocumentClassifierPass()); - - if (!builder.DesignTime) - { - builder.Features.Add(new AssemblyAttributeInjectionPass()); - builder.Features.Add(new InstrumentationPass()); - } - } - #endregion } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorPageDocumentClassifierPass.cs b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorPageDocumentClassifierPass.cs index 9af7873874..c6c0df9345 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorPageDocumentClassifierPass.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorPageDocumentClassifierPass.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.Text; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Intermediate; @@ -58,12 +57,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { // It's possible for a Razor document to not have a file path. // Eg. When we try to generate code for an in memory document like default imports. - var checksum = BytesToString(codeDocument.Source.GetChecksum()); + var checksum = Checksum.BytesToString(codeDocument.Source.GetChecksum()); @class.ClassName = $"AspNetCore_{checksum}"; } else { - @class.ClassName = CSharpIdentifier.GetClassNameFromPath(filePath); + @class.ClassName = GetClassNameFromPath(filePath); } @class.Modifiers.Clear(); @@ -144,21 +143,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions } } - private static string BytesToString(byte[] bytes) + private static string GetClassNameFromPath(string path) { - if (bytes == null) + const string cshtmlExtension = ".cshtml"; + + if (string.IsNullOrEmpty(path)) { - throw new ArgumentNullException(nameof(bytes)); + return path; } - var result = new StringBuilder(bytes.Length); - for (var i = 0; i < bytes.Length; i++) + if (path.EndsWith(cshtmlExtension, StringComparison.OrdinalIgnoreCase)) { - // The x2 format means lowercase hex, where each byte is a 2-character string. - result.Append(bytes[i].ToString("x2")); + path = path.Substring(0, path.Length - cshtmlExtension.Length); } - return result.ToString(); + return CSharpIdentifier.SanitizeIdentifier(path); } } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/breakingchanges.netcore.json b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/breakingchanges.netcore.json new file mode 100644 index 0000000000..5f5b6444f6 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/breakingchanges.netcore.json @@ -0,0 +1,11 @@ +[ + { + "TypeId": "public static class Microsoft.AspNetCore.Mvc.Razor.Extensions.RazorExtensions", + "MemberId": "public static System.Void Register(Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder builder)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.AspNetCore.Mvc.Razor.Extensions.InstrumentationPass : Microsoft.AspNetCore.Razor.Language.IntermediateNodePassBase, Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass", + "Kind": "Removal" + } +] diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/breakingchanges.netframework.json b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/breakingchanges.netframework.json new file mode 100644 index 0000000000..5f5b6444f6 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/breakingchanges.netframework.json @@ -0,0 +1,11 @@ +[ + { + "TypeId": "public static class Microsoft.AspNetCore.Mvc.Razor.Extensions.RazorExtensions", + "MemberId": "public static System.Void Register(Microsoft.AspNetCore.Razor.Language.IRazorEngineBuilder builder)", + "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.AspNetCore.Mvc.Razor.Extensions.InstrumentationPass : Microsoft.AspNetCore.Razor.Language.IntermediateNodePassBase, Microsoft.AspNetCore.Razor.Language.IRazorOptimizationPass", + "Kind": "Removal" + } +] diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.Extensions.props b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.Extensions.props deleted file mode 100644 index c968ea7b3a..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.Extensions.props +++ /dev/null @@ -1,40 +0,0 @@ - - - - - MVC-2.1 - - - true - - - <_MvcExtensionAssemblyPath Condition="'$(_MvcExtensionAssemblyPath)'==''">$(MSBuildThisFileDirectory)..\..\lib\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll - - - - - - MVC-2.1;$(CustomRazorExtension) - - - - - - Microsoft.AspNetCore.Mvc.Razor.Extensions - $(_MvcExtensionAssemblyPath) - - - diff --git a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.Extensions.targets b/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.Extensions.targets deleted file mode 100644 index ffe642e283..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.Extensions.targets +++ /dev/null @@ -1,36 +0,0 @@ - - - - true - - - $(GenerateRazorAssemblyInfo) - - - .Views - - - Microsoft.AspNetCore.Mvc.ApplicationParts.CompiledRazorAssemblyApplicationPartFactory, Microsoft.AspNetCore.Mvc.Razor - - - - <_RazorAssemblyAttribute Include="Microsoft.AspNetCore.Mvc.ApplicationParts.RelatedAssemblyAttribute"> - <_Parameter1>$(RazorTargetName) - - - - - - <_Parameter1>$(ProvideApplicationPartFactoryAttributeTypeName) - - - diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Design/Microsoft.AspNetCore.Razor.Design.csproj b/src/Razor/src/Microsoft.AspNetCore.Razor.Design/Microsoft.AspNetCore.Razor.Design.csproj deleted file mode 100644 index 083a24e847..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Design/Microsoft.AspNetCore.Razor.Design.csproj +++ /dev/null @@ -1,84 +0,0 @@ - - - Razor is a markup syntax for adding server-side logic to web pages. This package contains MSBuild support for Razor. - netcoreapp2.0 - - - false - false - false - - $(MSBuildProjectName).nuspec - - - - - - - ..\Microsoft.AspNetCore.Razor.Tools\Microsoft.AspNetCore.Razor.Tools.csproj - - - - - - - - - - - - - - - - - - - - - - - - - true - - - unknown - - - id=$(PackageId); - version=$(PackageVersion); - authors=$(Authors); - description=$(Description); - tags=$(PackageTags.Replace(';', ' ')); - licenseUrl=$(PackageLicenseUrl); - projectUrl=$(PackageProjectUrl); - iconUrl=$(PackageIconUrl); - repositoryUrl=$(RepositoryUrl); - repositoryCommit=$(RepositoryCommit); - copyright=$(Copyright); - - - ToolFiles=$(OutputPath)tools\**\*; - - - - - - - - - - <_RazorTool Include="$(OutputPath)tools\**\*" /> - - - - - - diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Design/Microsoft.AspNetCore.Razor.Design.nuspec b/src/Razor/src/Microsoft.AspNetCore.Razor.Design/Microsoft.AspNetCore.Razor.Design.nuspec deleted file mode 100644 index 47d16cd554..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Design/Microsoft.AspNetCore.Razor.Design.nuspec +++ /dev/null @@ -1,31 +0,0 @@ - - - - $id$ - $version$ - $authors$ - $description$ - true - $tags$ - $licenseUrl$ - $projectUrl$ - $iconUrl$ - - $copyright$ - - - - - - - - - - - - - - - - - diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Design/baseline.netcore.json b/src/Razor/src/Microsoft.AspNetCore.Razor.Design/baseline.netcore.json deleted file mode 100644 index 4491a03c76..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Design/baseline.netcore.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "AssemblyIdentity": "Microsoft.AspNetCore.Razor.Design, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", - "Types": [] -} \ No newline at end of file diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Design/baseline.netframework.json b/src/Razor/src/Microsoft.AspNetCore.Razor.Design/baseline.netframework.json deleted file mode 100644 index 7a73a41bfd..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Design/baseline.netframework.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.props b/src/Razor/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.props deleted file mode 100644 index 81b2e3afe4..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.props +++ /dev/null @@ -1,45 +0,0 @@ - - - - true - - - $(MSBuildThisFileDirectory)Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets - - - 2.1 - - - - - <_RazorMSBuildRoot Condition="'$(_RazorMSBuildRoot)'==''">$(MSBuildThisFileDirectory)..\..\ - - - <_RazorToolAssembly Condition="'$(_RazorToolAssembly)'==''">$(_RazorMSBuildRoot)tools\rzc.dll - - - - - - - - diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Design/buildMultiTargeting/Microsoft.AspNetCore.Razor.Design.props b/src/Razor/src/Microsoft.AspNetCore.Razor.Design/buildMultiTargeting/Microsoft.AspNetCore.Razor.Design.props deleted file mode 100644 index 227f58dea9..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Design/buildMultiTargeting/Microsoft.AspNetCore.Razor.Design.props +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/CSharpIdentifier.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/CSharpIdentifier.cs new file mode 100644 index 0000000000..1ec6188aed --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/CSharpIdentifier.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Globalization; +using System.Text; + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal static class CSharpIdentifier + { + // CSharp Spec §2.4.2 + private static bool IsIdentifierStart(char character) + { + return char.IsLetter(character) || + character == '_' || + CharUnicodeInfo.GetUnicodeCategory(character) == UnicodeCategory.LetterNumber; + } + + public static bool IsIdentifierPart(char character) + { + return char.IsDigit(character) || + IsIdentifierStart(character) || + IsIdentifierPartByUnicodeCategory(character); + } + + private static bool IsIdentifierPartByUnicodeCategory(char character) + { + var category = CharUnicodeInfo.GetUnicodeCategory(character); + + return category == UnicodeCategory.NonSpacingMark || // Mn + category == UnicodeCategory.SpacingCombiningMark || // Mc + category == UnicodeCategory.ConnectorPunctuation || // Pc + category == UnicodeCategory.Format; // Cf + } + + public static string SanitizeIdentifier(string inputName) + { + if (!IsIdentifierStart(inputName[0]) && IsIdentifierPart(inputName[0])) + { + inputName = "_" + inputName; + } + + var builder = new StringBuilder(inputName.Length); + for (var i = 0; i < inputName.Length; i++) + { + var ch = inputName[i]; + builder.Append(IsIdentifierPart(ch) ? ch : '_'); + } + + return builder.ToString(); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/ClassifiedSpanVisitor.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/ClassifiedSpanVisitor.cs new file mode 100644 index 0000000000..ec0e7977d0 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/ClassifiedSpanVisitor.cs @@ -0,0 +1,312 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal class ClassifiedSpanVisitor : SyntaxWalker + { + private RazorSourceDocument _source; + private List _spans; + private BlockKindInternal _currentBlockKind; + private SyntaxNode _currentBlock; + + public ClassifiedSpanVisitor(RazorSourceDocument source) + { + _source = source; + _spans = new List(); + _currentBlockKind = BlockKindInternal.Markup; + } + + public IReadOnlyList ClassifiedSpans => _spans; + + public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) + { + WriteBlock(node, BlockKindInternal.Comment, razorCommentSyntax => + { + WriteSpan(razorCommentSyntax.StartCommentTransition, SpanKindInternal.Transition, AcceptedCharactersInternal.None); + WriteSpan(razorCommentSyntax.StartCommentStar, SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); + + var comment = razorCommentSyntax.Comment; + if (comment.IsMissing) + { + // We need to generate a classified span at this position. So insert a marker in its place. + comment = (SyntaxToken)SyntaxFactory.Token(SyntaxKind.Marker, string.Empty).Green.CreateRed(razorCommentSyntax, razorCommentSyntax.StartCommentStar.EndPosition); + } + WriteSpan(comment, SpanKindInternal.Comment, AcceptedCharactersInternal.Any); + + WriteSpan(razorCommentSyntax.EndCommentStar, SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); + WriteSpan(razorCommentSyntax.EndCommentTransition, SpanKindInternal.Transition, AcceptedCharactersInternal.None); + }); + } + + public override void VisitCSharpCodeBlock(CSharpCodeBlockSyntax node) + { + if (node.Parent is CSharpStatementBodySyntax || + node.Parent is CSharpExplicitExpressionBodySyntax || + node.Parent is CSharpImplicitExpressionBodySyntax || + node.Parent is RazorDirectiveBodySyntax || + (_currentBlockKind == BlockKindInternal.Directive && + node.Children.Count == 1 && + node.Children[0] is CSharpStatementLiteralSyntax)) + { + base.VisitCSharpCodeBlock(node); + return; + } + + WriteBlock(node, BlockKindInternal.Statement, base.VisitCSharpCodeBlock); + } + + public override void VisitCSharpStatement(CSharpStatementSyntax node) + { + WriteBlock(node, BlockKindInternal.Statement, base.VisitCSharpStatement); + } + + public override void VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node) + { + WriteBlock(node, BlockKindInternal.Expression, base.VisitCSharpExplicitExpression); + } + + public override void VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node) + { + WriteBlock(node, BlockKindInternal.Expression, base.VisitCSharpImplicitExpression); + } + + public override void VisitRazorDirective(RazorDirectiveSyntax node) + { + WriteBlock(node, BlockKindInternal.Directive, base.VisitRazorDirective); + } + + public override void VisitCSharpTemplateBlock(CSharpTemplateBlockSyntax node) + { + WriteBlock(node, BlockKindInternal.Template, base.VisitCSharpTemplateBlock); + } + + public override void VisitMarkupBlock(MarkupBlockSyntax node) + { + WriteBlock(node, BlockKindInternal.Markup, base.VisitMarkupBlock); + } + + public override void VisitMarkupTagHelperAttributeValue(MarkupTagHelperAttributeValueSyntax node) + { + // We don't generate a classified span when the attribute value is a simple literal value. + // This is done so we maintain the classified spans generated in 2.x which + // used ConditionalAttributeCollapser (combines markup literal attribute values into one span with no block parent). + if (node.Children.Count > 1 || + (node.Children.Count == 1 && node.Children[0] is MarkupDynamicAttributeValueSyntax)) + { + WriteBlock(node, BlockKindInternal.Markup, base.VisitMarkupTagHelperAttributeValue); + return; + } + + base.VisitMarkupTagHelperAttributeValue(node); + } + + public override void VisitMarkupTagBlock(MarkupTagBlockSyntax node) + { + WriteBlock(node, BlockKindInternal.Tag, base.VisitMarkupTagBlock); + } + + public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax node) + { + WriteBlock(node, BlockKindInternal.Tag, base.VisitMarkupTagHelperElement); + } + + public override void VisitMarkupTagHelperStartTag(MarkupTagHelperStartTagSyntax node) + { + foreach (var child in node.Children) + { + if (child is MarkupTagHelperAttributeSyntax attribute) + { + Visit(attribute); + } + } + } + + public override void VisitMarkupTagHelperEndTag(MarkupTagHelperEndTagSyntax node) + { + // We don't want to generate a classified span for a tag helper end tag. Do nothing. + } + + public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node) + { + WriteBlock(node, BlockKindInternal.Markup, n => + { + var equalsSyntax = SyntaxFactory.MarkupTextLiteral(new SyntaxList(node.EqualsToken)); + var mergedAttributePrefix = MergeTextLiteralSpans(node.NamePrefix, node.Name, node.NameSuffix, equalsSyntax, node.ValuePrefix); + Visit(mergedAttributePrefix); + Visit(node.Value); + Visit(node.ValueSuffix); + }); + } + + public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSyntax node) + { + Visit(node.Value); + } + + public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttributeBlockSyntax node) + { + WriteBlock(node, BlockKindInternal.Markup, n => + { + var mergedAttributePrefix = MergeTextLiteralSpans(node.NamePrefix, node.Name); + Visit(mergedAttributePrefix); + }); + } + + public override void VisitMarkupCommentBlock(MarkupCommentBlockSyntax node) + { + WriteBlock(node, BlockKindInternal.HtmlComment, base.VisitMarkupCommentBlock); + } + + public override void VisitMarkupDynamicAttributeValue(MarkupDynamicAttributeValueSyntax node) + { + WriteBlock(node, BlockKindInternal.Markup, base.VisitMarkupDynamicAttributeValue); + } + + public override void VisitRazorMetaCode(RazorMetaCodeSyntax node) + { + WriteSpan(node, SpanKindInternal.MetaCode); + base.VisitRazorMetaCode(node); + } + + public override void VisitCSharpTransition(CSharpTransitionSyntax node) + { + WriteSpan(node, SpanKindInternal.Transition); + base.VisitCSharpTransition(node); + } + + public override void VisitMarkupTransition(MarkupTransitionSyntax node) + { + WriteSpan(node, SpanKindInternal.Transition); + base.VisitMarkupTransition(node); + } + + public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax node) + { + WriteSpan(node, SpanKindInternal.Code); + base.VisitCSharpStatementLiteral(node); + } + + public override void VisitCSharpExpressionLiteral(CSharpExpressionLiteralSyntax node) + { + WriteSpan(node, SpanKindInternal.Code); + base.VisitCSharpExpressionLiteral(node); + } + + public override void VisitCSharpEphemeralTextLiteral(CSharpEphemeralTextLiteralSyntax node) + { + WriteSpan(node, SpanKindInternal.Code); + base.VisitCSharpEphemeralTextLiteral(node); + } + + public override void VisitUnclassifiedTextLiteral(UnclassifiedTextLiteralSyntax node) + { + WriteSpan(node, SpanKindInternal.None); + base.VisitUnclassifiedTextLiteral(node); + } + + public override void VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node) + { + WriteSpan(node, SpanKindInternal.Markup); + base.VisitMarkupLiteralAttributeValue(node); + } + + public override void VisitMarkupTextLiteral(MarkupTextLiteralSyntax node) + { + if (node.Parent is MarkupLiteralAttributeValueSyntax) + { + base.VisitMarkupTextLiteral(node); + return; + } + + WriteSpan(node, SpanKindInternal.Markup); + base.VisitMarkupTextLiteral(node); + } + + public override void VisitMarkupEphemeralTextLiteral(MarkupEphemeralTextLiteralSyntax node) + { + WriteSpan(node, SpanKindInternal.Markup); + base.VisitMarkupEphemeralTextLiteral(node); + } + + private void WriteBlock(TNode node, BlockKindInternal kind, Action handler) where TNode : SyntaxNode + { + var previousBlock = _currentBlock; + var previousKind = _currentBlockKind; + + _currentBlock = node; + _currentBlockKind = kind; + + handler(node); + + _currentBlock = previousBlock; + _currentBlockKind = previousKind; + } + + private void WriteSpan(SyntaxNode node, SpanKindInternal kind, AcceptedCharactersInternal? acceptedCharacters = null) + { + if (node.IsMissing) + { + return; + } + + var spanSource = node.GetSourceSpan(_source); + var blockSource = _currentBlock.GetSourceSpan(_source); + if (!acceptedCharacters.HasValue) + { + acceptedCharacters = AcceptedCharactersInternal.Any; + var context = node.GetSpanContext(); + if (context != null) + { + acceptedCharacters = context.EditHandler.AcceptedCharacters; + } + } + + var span = new ClassifiedSpanInternal(spanSource, blockSource, kind, _currentBlockKind, acceptedCharacters.Value); + _spans.Add(span); + } + + private MarkupTextLiteralSyntax MergeTextLiteralSpans(params MarkupTextLiteralSyntax[] literalSyntaxes) + { + if (literalSyntaxes == null || literalSyntaxes.Length == 0) + { + return null; + } + + SyntaxNode parent = null; + var position = 0; + var seenFirstLiteral = false; + var builder = Syntax.InternalSyntax.SyntaxListBuilder.Create(); + + foreach (var syntax in literalSyntaxes) + { + if (syntax == null) + { + continue; + } + else if (!seenFirstLiteral) + { + // Set the parent and position of the merged literal to the value of the first non-null literal. + parent = syntax.Parent; + position = syntax.Position; + seenFirstLiteral = true; + } + + foreach (var token in syntax.LiteralTokens) + { + builder.Add(token.Green); + } + } + + var mergedLiteralSyntax = Syntax.InternalSyntax.SyntaxFactory.MarkupTextLiteral( + builder.ToList()); + + return (MarkupTextLiteralSyntax)mergedLiteralSyntax.CreateRed(parent, position); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Components/CodeGenerationConstants.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Components/CodeGenerationConstants.cs new file mode 100644 index 0000000000..35c176a678 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Components/CodeGenerationConstants.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + // Constants for method names used in code-generation + // Keep these in sync with the actual definitions + internal static class CodeGenerationConstants + { + public static class RazorComponent + { + public const string FullTypeName = "Microsoft.AspNetCore.Components.Component"; + public const string BuildRenderTree = "BuildRenderTree"; + public const string BuildRenderTreeParameter = "builder"; + } + + public static class RenderTreeBuilder + { + public const string FullTypeName = "Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder"; + } + + public static class InjectDirective + { + public const string FullTypeName = "Microsoft.AspNetCore.Razor.Components.InjectAttribute"; + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Components/ComponentDocumentClassifierPass.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Components/ComponentDocumentClassifierPass.cs new file mode 100644 index 0000000000..29afa982af --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Components/ComponentDocumentClassifierPass.cs @@ -0,0 +1,139 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + internal class ComponentDocumentClassifierPass : DocumentClassifierPassBase + { + public static readonly string ComponentDocumentKind = "component.1.0"; + private static readonly object BuildRenderTreeBaseCallAnnotation = new object(); + private static readonly char[] PathSeparators = new char[] { '/', '\\' }; + private static readonly char[] NamespaceSeparators = new char[] { '.' }; + + protected override string DocumentKind => ComponentDocumentKind; + + // Ensure this runs before the MVC classifiers which have Order = 0 + public override int Order => -100; + + protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + return codeDocument.GetInputDocumentKind() == InputDocumentKind.Component; + } + + protected override void OnDocumentStructureCreated(RazorCodeDocument codeDocument, NamespaceDeclarationIntermediateNode @namespace, ClassDeclarationIntermediateNode @class, MethodDeclarationIntermediateNode method) + { + base.OnDocumentStructureCreated(codeDocument, @namespace, @class, method); + + if (!TryComputeNamespaceAndClass( + codeDocument.Source.FilePath, + codeDocument.Source.RelativePath, + out var computedNamespace, + out var computedClass)) + { + // If we can't compute a nice namespace (no relative path) then just generate something + // mangled. + computedNamespace = "AspNetCore"; + var checksum = Checksum.BytesToString(codeDocument.Source.GetChecksum()); + computedClass = $"AspNetCore_{checksum}"; + } + + @namespace.Content = computedNamespace; + + @class.ClassName = computedClass; + @class.BaseType = $"global::{CodeGenerationConstants.RazorComponent.FullTypeName}"; + var filePath = codeDocument.Source.RelativePath ?? codeDocument.Source.FilePath; + if (string.IsNullOrEmpty(filePath)) + { + // It's possible for a Razor document to not have a file path. + // Eg. When we try to generate code for an in memory document like default imports. + var checksum = Checksum.BytesToString(codeDocument.Source.GetChecksum()); + @class.ClassName = $"AspNetCore_{checksum}"; + } + else + { + @class.ClassName = CSharpIdentifier.SanitizeIdentifier(Path.GetFileNameWithoutExtension(filePath)); + } + + @class.Modifiers.Clear(); + @class.Modifiers.Add("public"); + @class.Modifiers.Add("sealed"); + + method.MethodName = CodeGenerationConstants.RazorComponent.BuildRenderTree; + method.ReturnType = "void"; + method.Modifiers.Clear(); + method.Modifiers.Add("public"); + method.Modifiers.Add("override"); + + method.Parameters.Clear(); + method.Parameters.Add(new MethodParameter() + { + TypeName = CodeGenerationConstants.RenderTreeBuilder.FullTypeName, + ParameterName = CodeGenerationConstants.RazorComponent.BuildRenderTreeParameter, + }); + + // We need to call the 'base' method as the first statement. + var callBase = new CSharpCodeIntermediateNode(); + callBase.Annotations.Add(BuildRenderTreeBaseCallAnnotation, true); + callBase.Children.Add(new IntermediateToken + { + Kind = TokenKind.CSharp, + Content = $"base.{CodeGenerationConstants.RazorComponent.BuildRenderTree}({CodeGenerationConstants.RazorComponent.BuildRenderTreeParameter});" + }); + method.Children.Insert(0, callBase); + } + + private bool TryComputeNamespaceAndClass(string filePath, string relativePath, out string @namespace, out string @class) + { + if (filePath == null || relativePath == null || filePath.Length <= relativePath.Length) + { + @namespace = null; + @class = null; + return false; + } + + // Try and infer a namespace from the project directory. We don't yet have the ability to pass + // the namespace through from the project. + var trimLength = relativePath.Length + (relativePath.StartsWith("/") ? 0 : 1); + var baseDirectory = filePath.Substring(0, filePath.Length - trimLength); + + var lastSlash = baseDirectory.LastIndexOfAny(PathSeparators); + var baseNamespace = lastSlash == -1 ? baseDirectory : baseDirectory.Substring(lastSlash + 1); + if (string.IsNullOrEmpty(baseNamespace)) + { + @namespace = null; + @class = null; + return false; + } + + var builder = new StringBuilder(); + + // Sanitize the base namespace, but leave the dots. + var segments = baseNamespace.Split(NamespaceSeparators, StringSplitOptions.RemoveEmptyEntries); + builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[0])); + for (var i = 1; i < segments.Length; i++) + { + builder.Append('.'); + builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[i])); + } + + segments = relativePath.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries); + + // Skip the last segment because it's the FileName. + for (var i = 0; i < segments.Length - 1; i++) + { + builder.Append('.'); + builder.Append(CSharpIdentifier.SanitizeIdentifier(segments[i])); + } + + @namespace = builder.ToString(); + @class = CSharpIdentifier.SanitizeIdentifier(Path.GetFileNameWithoutExtension(relativePath)); + + return true; + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Components/ComponentExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Components/ComponentExtensions.cs new file mode 100644 index 0000000000..6f7a2affa4 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Components/ComponentExtensions.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Razor.Language.Extensions; + +namespace Microsoft.AspNetCore.Razor.Language.Components +{ + public static class ComponentExtensions + { + public static void Register(RazorProjectEngineBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + FunctionsDirective.Register(builder); + builder.Features.Add(new ComponentDocumentClassifierPass()); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultDirectiveSyntaxTreePass.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultDirectiveSyntaxTreePass.cs index 54c733e0f1..65e578b5ce 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultDirectiveSyntaxTreePass.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultDirectiveSyntaxTreePass.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; namespace Microsoft.AspNetCore.Razor.Language { @@ -23,37 +24,42 @@ namespace Microsoft.AspNetCore.Razor.Language { throw new ArgumentNullException(nameof(syntaxTree)); } - - var sectionVerifier = new NestedSectionVerifier(); - sectionVerifier.Verify(syntaxTree); - - return syntaxTree; + + var sectionVerifier = new NestedSectionVerifier(syntaxTree); + return sectionVerifier.Verify(); } - private class NestedSectionVerifier : ParserVisitor + private class NestedSectionVerifier : SyntaxRewriter { private int _nestedLevel; + private RazorSyntaxTree _syntaxTree; - public void Verify(RazorSyntaxTree tree) + public NestedSectionVerifier(RazorSyntaxTree syntaxTree) { - tree.Root.Accept(this); + _syntaxTree = syntaxTree; } - public override void VisitDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block) + public RazorSyntaxTree Verify() + { + var root = Visit(_syntaxTree.Root); + var rewrittenTree = new DefaultRazorSyntaxTree(root, _syntaxTree.Source, _syntaxTree.Diagnostics, _syntaxTree.Options); + return rewrittenTree; + } + + public override SyntaxNode VisitRazorDirective(RazorDirectiveSyntax node) { if (_nestedLevel > 0) { - var directiveStart = block.Children.First(child => !child.IsBlock && ((Span)child).Kind == SpanKindInternal.Transition).Start; + var directiveStart = node.Transition.GetSourceLocation(_syntaxTree.Source); var errorLength = /* @ */ 1 + SectionDirective.Directive.Directive.Length; var error = RazorDiagnosticFactory.CreateParsing_SectionsCannotBeNested(new SourceSpan(directiveStart, errorLength)); - chunkGenerator.Diagnostics.Add(error); + node = node.AppendDiagnostic(error); } - _nestedLevel++; - - VisitDefault(block); - + var result = base.VisitRazorDirective(node); _nestedLevel--; + + return result; } } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptions.cs index e521141628..6b694d9382 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptions.cs @@ -10,13 +10,15 @@ namespace Microsoft.AspNetCore.Razor.Language int indentSize, bool designTime, bool suppressChecksum, - bool supressMetadataAttributes) + bool supressMetadataAttributes, + bool suppressPrimaryMethodBody) { IndentWithTabs = indentWithTabs; IndentSize = indentSize; DesignTime = designTime; SuppressChecksum = suppressChecksum; SuppressMetadataAttributes = supressMetadataAttributes; + SuppressPrimaryMethodBody = suppressPrimaryMethodBody; } public override bool DesignTime { get; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsBuilder.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsBuilder.cs index 8dd095a04e..7507ac0fee 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsBuilder.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorCodeGenerationOptionsBuilder.cs @@ -31,10 +31,16 @@ namespace Microsoft.AspNetCore.Razor.Language public override bool IndentWithTabs { get; set; } public override bool SuppressChecksum { get; set; } - + public override RazorCodeGenerationOptions Build() { - return new DefaultRazorCodeGenerationOptions(IndentWithTabs, IndentSize, DesignTime, SuppressChecksum, SuppressMetadataAttributes); + return new DefaultRazorCodeGenerationOptions( + IndentWithTabs, + IndentSize, + DesignTime, + SuppressChecksum, + SuppressMetadataAttributes, + SuppressPrimaryMethodBody); } public override void SetDesignTime(bool designTime) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs index a711567547..07e1ad44c8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs @@ -8,6 +8,7 @@ using System.Linq; using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Intermediate; using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; namespace Microsoft.AspNetCore.Razor.Language { @@ -46,8 +47,8 @@ namespace Microsoft.AspNetCore.Razor.Language { var import = imports[j]; - importsVisitor.FilePath = import.Source.FilePath; - importsVisitor.VisitBlock(import.Root); + importsVisitor.SourceDocument = import.Source; + importsVisitor.Visit(import.Root); } importedUsings = importsVisitor.Usings; @@ -56,10 +57,10 @@ namespace Microsoft.AspNetCore.Razor.Language var tagHelperPrefix = tagHelperContext?.Prefix; var visitor = new MainSourceVisitor(document, builder, tagHelperPrefix, syntaxTree.Options.FeatureFlags) { - FilePath = syntaxTree.Source.FilePath, + SourceDocument = syntaxTree.Source, }; - visitor.VisitBlock(syntaxTree.Root); + visitor.Visit(syntaxTree.Root); // 1. Prioritize non-imported usings over imported ones. // 2. Don't import usings that already exist in primary document. @@ -189,7 +190,7 @@ namespace Microsoft.AspNetCore.Razor.Language public override int GetHashCode() => Namespace.GetHashCode(); } - private class LoweringVisitor : ParserVisitor + private class LoweringVisitor : SyntaxWalker { protected readonly IntermediateNodeBuilder _builder; protected readonly DocumentIntermediateNode _document; @@ -206,195 +207,208 @@ namespace Microsoft.AspNetCore.Razor.Language public IReadOnlyList Usings => _usings; - public string FilePath { get; set; } + public RazorSourceDocument SourceDocument { get; set; } - public override void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span span) - { - _builder.Add(new DirectiveTokenIntermediateNode() - { - Content = span.Content, - DirectiveToken = chunkGenerator.Descriptor, - Source = BuildSourceSpanFromNode(span), - }); - } - - public override void VisitDirectiveBlock(DirectiveChunkGenerator chunkGenerator, Block block) + public override void VisitRazorDirective(RazorDirectiveSyntax node) { IntermediateNode directiveNode; - if (IsMalformed(chunkGenerator.Diagnostics)) + var descriptor = node.DirectiveDescriptor; + + if (descriptor != null) { - directiveNode = new MalformedDirectiveIntermediateNode() + var diagnostics = node.GetDiagnostics(); + + // This is an extensible directive. + if (IsMalformed(diagnostics)) { - DirectiveName = chunkGenerator.Descriptor.Directive, - Directive = chunkGenerator.Descriptor, - Source = BuildSourceSpanFromNode(block), - }; - } - else - { - directiveNode = new DirectiveIntermediateNode() + directiveNode = new MalformedDirectiveIntermediateNode() + { + DirectiveName = descriptor.Directive, + Directive = descriptor, + Source = BuildSourceSpanFromNode(node), + }; + } + else { - DirectiveName = chunkGenerator.Descriptor.Directive, - Directive = chunkGenerator.Descriptor, - Source = BuildSourceSpanFromNode(block), - }; + directiveNode = new DirectiveIntermediateNode() + { + DirectiveName = descriptor.Directive, + Directive = descriptor, + Source = BuildSourceSpanFromNode(node), + }; + } + + for (var i = 0; i < diagnostics.Length; i++) + { + directiveNode.Diagnostics.Add(diagnostics[i]); + } + + _builder.Push(directiveNode); } - for (var i = 0; i < chunkGenerator.Diagnostics.Count; i++) + Visit(node.Body); + + if (descriptor != null) { - directiveNode.Diagnostics.Add(chunkGenerator.Diagnostics[i]); + _builder.Pop(); } - - _builder.Push(directiveNode); - - VisitDefault(block); - - _builder.Pop(); } - public override void VisitImportSpan(AddImportChunkGenerator chunkGenerator, Span span) + public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax node) { - var namespaceImport = chunkGenerator.Namespace.Trim(); - var namespaceSpan = BuildSourceSpanFromNode(span); - _usings.Add(new UsingReference(namespaceImport, namespaceSpan)); + var context = node.GetSpanContext(); + if (context == null) + { + base.VisitCSharpStatementLiteral(node); + return; + } + else if (context.ChunkGenerator is DirectiveTokenChunkGenerator tokenChunkGenerator) + { + _builder.Add(new DirectiveTokenIntermediateNode() + { + Content = node.GetContent(), + DirectiveToken = tokenChunkGenerator.Descriptor, + Source = BuildSourceSpanFromNode(node), + }); + } + else if (context.ChunkGenerator is AddImportChunkGenerator importChunkGenerator) + { + var namespaceImport = importChunkGenerator.Namespace.Trim(); + var namespaceSpan = BuildSourceSpanFromNode(node); + _usings.Add(new UsingReference(namespaceImport, namespaceSpan)); + } + else if (context.ChunkGenerator is AddTagHelperChunkGenerator addTagHelperChunkGenerator) + { + IntermediateNode directiveNode; + if (IsMalformed(addTagHelperChunkGenerator.Diagnostics)) + { + directiveNode = new MalformedDirectiveIntermediateNode() + { + DirectiveName = CSharpCodeParser.AddTagHelperDirectiveDescriptor.Directive, + Directive = CSharpCodeParser.AddTagHelperDirectiveDescriptor, + Source = BuildSourceSpanFromNode(node), + }; + } + else + { + directiveNode = new DirectiveIntermediateNode() + { + DirectiveName = CSharpCodeParser.AddTagHelperDirectiveDescriptor.Directive, + Directive = CSharpCodeParser.AddTagHelperDirectiveDescriptor, + Source = BuildSourceSpanFromNode(node), + }; + } + + for (var i = 0; i < addTagHelperChunkGenerator.Diagnostics.Count; i++) + { + directiveNode.Diagnostics.Add(addTagHelperChunkGenerator.Diagnostics[i]); + } + + _builder.Push(directiveNode); + + _builder.Add(new DirectiveTokenIntermediateNode() + { + Content = addTagHelperChunkGenerator.LookupText, + DirectiveToken = CSharpCodeParser.AddTagHelperDirectiveDescriptor.Tokens.First(), + Source = BuildSourceSpanFromNode(node), + }); + + _builder.Pop(); + } + else if (context.ChunkGenerator is RemoveTagHelperChunkGenerator removeTagHelperChunkGenerator) + { + IntermediateNode directiveNode; + if (IsMalformed(removeTagHelperChunkGenerator.Diagnostics)) + { + directiveNode = new MalformedDirectiveIntermediateNode() + { + DirectiveName = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor.Directive, + Directive = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor, + Source = BuildSourceSpanFromNode(node), + }; + } + else + { + directiveNode = new DirectiveIntermediateNode() + { + DirectiveName = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor.Directive, + Directive = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor, + Source = BuildSourceSpanFromNode(node), + }; + } + + for (var i = 0; i < removeTagHelperChunkGenerator.Diagnostics.Count; i++) + { + directiveNode.Diagnostics.Add(removeTagHelperChunkGenerator.Diagnostics[i]); + } + + _builder.Push(directiveNode); + + _builder.Add(new DirectiveTokenIntermediateNode() + { + Content = removeTagHelperChunkGenerator.LookupText, + DirectiveToken = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor.Tokens.First(), + Source = BuildSourceSpanFromNode(node), + }); + + _builder.Pop(); + } + else if (context.ChunkGenerator is TagHelperPrefixDirectiveChunkGenerator tagHelperPrefixChunkGenerator) + { + IntermediateNode directiveNode; + if (IsMalformed(tagHelperPrefixChunkGenerator.Diagnostics)) + { + directiveNode = new MalformedDirectiveIntermediateNode() + { + DirectiveName = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor.Directive, + Directive = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor, + Source = BuildSourceSpanFromNode(node), + }; + } + else + { + directiveNode = new DirectiveIntermediateNode() + { + DirectiveName = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor.Directive, + Directive = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor, + Source = BuildSourceSpanFromNode(node), + }; + } + + for (var i = 0; i < tagHelperPrefixChunkGenerator.Diagnostics.Count; i++) + { + directiveNode.Diagnostics.Add(tagHelperPrefixChunkGenerator.Diagnostics[i]); + } + + _builder.Push(directiveNode); + + _builder.Add(new DirectiveTokenIntermediateNode() + { + Content = tagHelperPrefixChunkGenerator.Prefix, + DirectiveToken = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor.Tokens.First(), + Source = BuildSourceSpanFromNode(node), + }); + + _builder.Pop(); + } + + base.VisitCSharpStatementLiteral(node); } - public override void VisitAddTagHelperSpan(AddTagHelperChunkGenerator chunkGenerator, Span span) + protected SourceSpan? BuildSourceSpanFromNode(SyntaxNode node) { - IntermediateNode directiveNode; - if (IsMalformed(chunkGenerator.Diagnostics)) - { - directiveNode = new MalformedDirectiveIntermediateNode() - { - DirectiveName = CSharpCodeParser.AddTagHelperDirectiveDescriptor.Directive, - Directive = CSharpCodeParser.AddTagHelperDirectiveDescriptor, - Source = BuildSourceSpanFromNode(span), - }; - } - else - { - directiveNode = new DirectiveIntermediateNode() - { - DirectiveName = CSharpCodeParser.AddTagHelperDirectiveDescriptor.Directive, - Directive = CSharpCodeParser.AddTagHelperDirectiveDescriptor, - Source = BuildSourceSpanFromNode(span), - }; - } - - for (var i = 0; i < chunkGenerator.Diagnostics.Count; i++) - { - directiveNode.Diagnostics.Add(chunkGenerator.Diagnostics[i]); - } - - _builder.Push(directiveNode); - - _builder.Add(new DirectiveTokenIntermediateNode() - { - Content = chunkGenerator.LookupText, - DirectiveToken = CSharpCodeParser.AddTagHelperDirectiveDescriptor.Tokens.First(), - Source = BuildSourceSpanFromNode(span), - }); - - _builder.Pop(); - } - - public override void VisitRemoveTagHelperSpan(RemoveTagHelperChunkGenerator chunkGenerator, Span span) - { - IntermediateNode directiveNode; - if (IsMalformed(chunkGenerator.Diagnostics)) - { - directiveNode = new MalformedDirectiveIntermediateNode() - { - DirectiveName = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor.Directive, - Directive = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor, - Source = BuildSourceSpanFromNode(span), - }; - } - else - { - directiveNode = new DirectiveIntermediateNode() - { - DirectiveName = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor.Directive, - Directive = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor, - Source = BuildSourceSpanFromNode(span), - }; - } - - for (var i = 0; i < chunkGenerator.Diagnostics.Count; i++) - { - directiveNode.Diagnostics.Add(chunkGenerator.Diagnostics[i]); - } - - _builder.Push(directiveNode); - - _builder.Add(new DirectiveTokenIntermediateNode() - { - Content = chunkGenerator.LookupText, - DirectiveToken = CSharpCodeParser.RemoveTagHelperDirectiveDescriptor.Tokens.First(), - Source = BuildSourceSpanFromNode(span), - }); - - _builder.Pop(); - } - - public override void VisitTagHelperPrefixDirectiveSpan(TagHelperPrefixDirectiveChunkGenerator chunkGenerator, Span span) - { - IntermediateNode directiveNode; - if (IsMalformed(chunkGenerator.Diagnostics)) - { - directiveNode = new MalformedDirectiveIntermediateNode() - { - DirectiveName = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor.Directive, - Directive = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor, - Source = BuildSourceSpanFromNode(span), - }; - } - else - { - directiveNode = new DirectiveIntermediateNode() - { - DirectiveName = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor.Directive, - Directive = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor, - Source = BuildSourceSpanFromNode(span), - }; - } - - for (var i = 0; i < chunkGenerator.Diagnostics.Count; i++) - { - directiveNode.Diagnostics.Add(chunkGenerator.Diagnostics[i]); - } - - _builder.Push(directiveNode); - - _builder.Add(new DirectiveTokenIntermediateNode() - { - Content = chunkGenerator.Prefix, - DirectiveToken = CSharpCodeParser.TagHelperPrefixDirectiveDescriptor.Tokens.First(), - Source = BuildSourceSpanFromNode(span), - }); - - _builder.Pop(); - } - - protected SourceSpan? BuildSourceSpanFromNode(SyntaxTreeNode node) - { - if (node == null || node.Start == SourceLocation.Undefined) + if (node == null) { return null; } - var span = new SourceSpan( - node.Start.FilePath ?? FilePath, - node.Start.AbsoluteIndex, - node.Start.LineIndex, - node.Start.CharacterIndex, - node.Length); - return span; + return node.GetSourceSpan(SourceDocument); } } private class MainSourceVisitor : LoweringVisitor { + private readonly HashSet _renderedBoundAttributeNames = new HashSet(StringComparer.OrdinalIgnoreCase); private readonly string _tagHelperPrefix; public MainSourceVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, string tagHelperPrefix, RazorParserFeatureFlags featureFlags) @@ -408,86 +422,144 @@ namespace Microsoft.AspNetCore.Razor.Language // Name=checked // Prefix= checked=" // Suffix=" - public override void VisitAttributeBlock(AttributeBlockChunkGenerator chunkGenerator, Block block) + public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node) { - _builder.Push(new HtmlAttributeIntermediateNode() + var prefixTokens = MergeLiterals( + node.NamePrefix?.LiteralTokens, + node.Name.LiteralTokens, + node.NameSuffix?.LiteralTokens, + node.EqualsToken == null ? new SyntaxList() : new SyntaxList(node.EqualsToken), + node.ValuePrefix?.LiteralTokens); + var prefix = (MarkupTextLiteralSyntax)SyntaxFactory.MarkupTextLiteral(prefixTokens).Green.CreateRed(node, node.NamePrefix?.Position ?? node.Name.Position); + + var name = node.Name.GetContent(); + if (name.StartsWith("data-", StringComparison.OrdinalIgnoreCase) && + !_featureFlags.EXPERIMENTAL_AllowConditionalDataDashAttributes) { - AttributeName = chunkGenerator.Name, - Prefix = chunkGenerator.Prefix, - Suffix = chunkGenerator.Suffix, - Source = BuildSourceSpanFromNode(block), - }); + Visit(prefix); + Visit(node.Value); + Visit(node.ValueSuffix); + } + else + { + if (node.Value != null && node.Value.ChildNodes().All(c => c is MarkupLiteralAttributeValueSyntax)) + { + // We need to do what ConditionalAttributeCollapser used to do. + var literalAttributeValueNodes = node.Value.ChildNodes().Cast().ToArray(); + var valueTokens = SyntaxListBuilder.Create(); + for (var i = 0; i < literalAttributeValueNodes.Length; i++) + { + var mergedValue = MergeAttributeValue(literalAttributeValueNodes[i]); + valueTokens.AddRange(mergedValue.LiteralTokens); + } + var rewritten = SyntaxFactory.MarkupTextLiteral(valueTokens.ToList()); - VisitDefault(block); + var mergedLiterals = MergeLiterals(prefix?.LiteralTokens, rewritten.LiteralTokens, node.ValueSuffix?.LiteralTokens); + var mergedAttribute = SyntaxFactory.MarkupTextLiteral(mergedLiterals).Green.CreateRed(node.Parent, node.Position); + Visit(mergedAttribute); + } + else + { + _builder.Push(new HtmlAttributeIntermediateNode() + { + AttributeName = node.Name.GetContent(), + Prefix = prefix.GetContent(), + Suffix = node.ValueSuffix?.GetContent() ?? string.Empty, + Source = BuildSourceSpanFromNode(node), + }); - _builder.Pop(); + VisitAttributeValue(node.Value); + + _builder.Pop(); + } + } + } + + public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttributeBlockSyntax node) + { + var name = node.Name.GetContent(); + if (name.StartsWith("data-", StringComparison.OrdinalIgnoreCase) && + !_featureFlags.EXPERIMENTAL_AllowConditionalDataDashAttributes) + { + base.VisitMarkupMinimizedAttributeBlock(node); + return; + } + + // Minimized attributes are just html content. + var literals = MergeLiterals( + node.NamePrefix?.LiteralTokens, + node.Name?.LiteralTokens); + var literal = SyntaxFactory.MarkupTextLiteral(literals).Green.CreateRed(node.Parent, node.Position); + + Visit(literal); } // Example // // Prefix= (space) // Children will contain a token for @false. - public override void VisitDynamicAttributeBlock(DynamicAttributeBlockChunkGenerator chunkGenerator, Block block) + public override void VisitMarkupDynamicAttributeValue(MarkupDynamicAttributeValueSyntax node) { - var firstChild = block.Children.FirstOrDefault(c => c.IsBlock) as Block; - if (firstChild == null || firstChild.Type == BlockKindInternal.Expression) + var containsExpression = false; + var descendantNodes = node.DescendantNodes(n => + { + // Don't go into sub block. They may contain expressions but we only care about the top level. + return !(n.Parent is CSharpCodeBlockSyntax); + }); + foreach (var child in descendantNodes) + { + if (child is CSharpImplicitExpressionSyntax || child is CSharpExplicitExpressionSyntax) + { + containsExpression = true; + } + } + + if (containsExpression) { _builder.Push(new CSharpExpressionAttributeValueIntermediateNode() { - Prefix = chunkGenerator.Prefix, - Source = BuildSourceSpanFromNode(block), + Prefix = node.Prefix?.GetContent() ?? string.Empty, + Source = BuildSourceSpanFromNode(node), }); } else { _builder.Push(new CSharpCodeAttributeValueIntermediateNode() { - Prefix = chunkGenerator.Prefix, - Source = BuildSourceSpanFromNode(block), + Prefix = node.Prefix?.GetContent() ?? string.Empty, + Source = BuildSourceSpanFromNode(node), }); } - VisitDefault(block); + Visit(node.Value); _builder.Pop(); } - public override void VisitLiteralAttributeSpan(LiteralAttributeChunkGenerator chunkGenerator, Span span) + public override void VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node) { _builder.Push(new HtmlAttributeValueIntermediateNode() { - Prefix = chunkGenerator.Prefix, - Source = BuildSourceSpanFromNode(span), + Prefix = node.Prefix?.GetContent() ?? string.Empty, + Source = BuildSourceSpanFromNode(node), }); - var location = chunkGenerator.Value.Location; - SourceSpan? valueSpan = null; - if (location != SourceLocation.Undefined) - { - valueSpan = new SourceSpan( - location.FilePath ?? FilePath, - location.AbsoluteIndex, - location.LineIndex, - location.CharacterIndex, - chunkGenerator.Value.Value.Length); - } - _builder.Add(new IntermediateToken() { - Content = chunkGenerator.Value, + Content = node.Value?.GetContent() ?? string.Empty, Kind = TokenKind.Html, - Source = valueSpan + Source = BuildSourceSpanFromNode(node.Value) }); _builder.Pop(); } - public override void VisitTemplateBlock(TemplateBlockChunkGenerator chunkGenerator, Block block) + public override void VisitCSharpTemplateBlock(CSharpTemplateBlockSyntax node) { var templateNode = new TemplateIntermediateNode(); _builder.Push(templateNode); - VisitDefault(block); + base.VisitCSharpTemplateBlock(node); _builder.Pop(); @@ -503,7 +575,7 @@ namespace Microsoft.AspNetCore.Razor.Language var contentLength = templateNode.Children.Sum(child => child.Source?.Length ?? 0); templateNode.Source = new SourceSpan( - sourceRangeStart.Value.FilePath ?? FilePath, + sourceRangeStart.Value.FilePath ?? SourceDocument.FilePath, sourceRangeStart.Value.AbsoluteIndex, sourceRangeStart.Value.LineIndex, sourceRangeStart.Value.CharacterIndex, @@ -518,11 +590,11 @@ namespace Microsoft.AspNetCore.Razor.Language // @DateTime.@*This is a comment*@Now // // We need to capture this in the IR so that we can give each piece the correct source mappings - public override void VisitExpressionBlock(ExpressionChunkGenerator chunkGenerator, Block block) + public override void VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node) { if (_builder.Current is CSharpExpressionAttributeValueIntermediateNode) { - VisitDefault(block); + base.VisitCSharpExplicitExpression(node); return; } @@ -530,7 +602,7 @@ namespace Microsoft.AspNetCore.Razor.Language _builder.Push(expressionNode); - VisitDefault(block); + base.VisitCSharpExplicitExpression(node); _builder.Pop(); @@ -546,7 +618,7 @@ namespace Microsoft.AspNetCore.Razor.Language var contentLength = expressionNode.Children.Sum(child => child.Source?.Length ?? 0); expressionNode.Source = new SourceSpan( - sourceRangeStart.Value.FilePath ?? FilePath, + sourceRangeStart.Value.FilePath ?? SourceDocument.FilePath, sourceRangeStart.Value.AbsoluteIndex, sourceRangeStart.Value.LineIndex, sourceRangeStart.Value.CharacterIndex, @@ -555,57 +627,120 @@ namespace Microsoft.AspNetCore.Razor.Language } } - public override void VisitExpressionSpan(ExpressionChunkGenerator chunkGenerator, Span span) + public override void VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node) { - _builder.Add(new IntermediateToken() + if (_builder.Current is CSharpExpressionAttributeValueIntermediateNode) { - Content = span.Content, - Kind = TokenKind.CSharp, - Source = BuildSourceSpanFromNode(span), - }); - } + base.VisitCSharpImplicitExpression(node); + return; + } - public override void VisitStatementSpan(StatementChunkGenerator chunkGenerator, Span span) - { - var isAttributeValue = _builder.Current is CSharpCodeAttributeValueIntermediateNode; + var expressionNode = new CSharpExpressionIntermediateNode(); - if (!isAttributeValue) + _builder.Push(expressionNode); + + base.VisitCSharpImplicitExpression(node); + + _builder.Pop(); + + if (expressionNode.Children.Count > 0) { - var statementNode = new CSharpCodeIntermediateNode() + var sourceRangeStart = expressionNode + .Children + .FirstOrDefault(child => child.Source != null) + ?.Source; + + if (sourceRangeStart != null) { - Source = BuildSourceSpanFromNode(span) - }; - _builder.Push(statementNode); + var contentLength = expressionNode.Children.Sum(child => child.Source?.Length ?? 0); + + expressionNode.Source = new SourceSpan( + sourceRangeStart.Value.FilePath ?? SourceDocument.FilePath, + sourceRangeStart.Value.AbsoluteIndex, + sourceRangeStart.Value.LineIndex, + sourceRangeStart.Value.CharacterIndex, + contentLength); + } + } + } + + public override void VisitCSharpExpressionLiteral(CSharpExpressionLiteralSyntax node) + { + if (_builder.Current is TagHelperHtmlAttributeIntermediateNode) + { + // If we are top level in a tag helper HTML attribute, we want to be rendered as markup. + // This case happens for duplicate non-string bound attributes. They would be initially be categorized as + // CSharp but since they are duplicate, they should just be markup. + var markupLiteral = SyntaxFactory.MarkupTextLiteral(node.LiteralTokens).Green.CreateRed(node.Parent, node.Position); + Visit(markupLiteral); + return; } _builder.Add(new IntermediateToken() { - Content = span.Content, + Content = node.GetContent(), Kind = TokenKind.CSharp, - Source = BuildSourceSpanFromNode(span), + Source = BuildSourceSpanFromNode(node), }); - if (!isAttributeValue) - { - _builder.Pop(); - } + base.VisitCSharpExpressionLiteral(node); } - public override void VisitMarkupSpan(MarkupChunkGenerator chunkGenerator, Span span) + public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax node) { - if (span.Tokens.Count == 1) + var context = node.GetSpanContext(); + if (context == null || context.ChunkGenerator is StatementChunkGenerator) { - var token = span.Tokens[0] as HtmlToken; + var isAttributeValue = _builder.Current is CSharpCodeAttributeValueIntermediateNode; + + if (!isAttributeValue) + { + var statementNode = new CSharpCodeIntermediateNode() + { + Source = BuildSourceSpanFromNode(node) + }; + _builder.Push(statementNode); + } + + _builder.Add(new IntermediateToken() + { + Content = node.GetContent(), + Kind = TokenKind.CSharp, + Source = BuildSourceSpanFromNode(node), + }); + + if (!isAttributeValue) + { + _builder.Pop(); + } + } + + base.VisitCSharpStatementLiteral(node); + } + + public override void VisitMarkupTextLiteral(MarkupTextLiteralSyntax node) + { + var context = node.GetSpanContext(); + if (context != null && context.ChunkGenerator == SpanChunkGenerator.Null) + { + base.VisitMarkupTextLiteral(node); + return; + } + + if (node.LiteralTokens.Count == 1) + { + var token = node.LiteralTokens[0]; if (token != null && - token.Type == HtmlTokenType.Unknown && + token.Kind == SyntaxKind.Marker && token.Content.Length == 0) { // We don't want to create IR nodes for marker tokens. + base.VisitMarkupTextLiteral(node); return; } } - var source = BuildSourceSpanFromNode(span); + var source = BuildSourceSpanFromNode(node); var currentChildren = _builder.Current.Children; if (currentChildren.Count > 0 && currentChildren[currentChildren.Count - 1] is HtmlContentIntermediateNode) { @@ -613,7 +748,8 @@ namespace Microsoft.AspNetCore.Razor.Language if (existingHtmlContent.Source == null && source == null) { - Combine(existingHtmlContent, span); + Combine(existingHtmlContent, node); + base.VisitMarkupTextLiteral(node); return; } @@ -622,7 +758,8 @@ namespace Microsoft.AspNetCore.Razor.Language existingHtmlContent.Source.Value.FilePath == source.Value.FilePath && existingHtmlContent.Source.Value.AbsoluteIndex + existingHtmlContent.Source.Value.Length == source.Value.AbsoluteIndex) { - Combine(existingHtmlContent, span); + Combine(existingHtmlContent, node); + base.VisitMarkupTextLiteral(node); return; } } @@ -635,23 +772,20 @@ namespace Microsoft.AspNetCore.Razor.Language _builder.Add(new IntermediateToken() { - Content = span.Content, + Content = node.GetContent(), Kind = TokenKind.Html, Source = source, }); _builder.Pop(); + + base.VisitMarkupTextLiteral(node); } - public override void VisitTagHelperBlock(TagHelperChunkGenerator chunkGenerator, Block block) + public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax node) { - var tagHelperBlock = block as TagHelperBlock; - if (tagHelperBlock == null) - { - return; - } - - var tagName = tagHelperBlock.TagName; + var info = node.TagHelperInfo; + var tagName = info.TagName; if (_tagHelperPrefix != null) { tagName = tagName.Substring(_tagHelperPrefix.Length); @@ -660,11 +794,11 @@ namespace Microsoft.AspNetCore.Razor.Language var tagHelperNode = new TagHelperIntermediateNode() { TagName = tagName, - TagMode = tagHelperBlock.TagMode, - Source = BuildSourceSpanFromNode(block) + TagMode = info.TagMode, + Source = BuildSourceSpanFromNode(node) }; - foreach (var tagHelper in tagHelperBlock.Binding.Descriptors) + foreach (var tagHelper in info.BindingResult.Descriptors) { tagHelperNode.TagHelpers.Add(tagHelper); } @@ -673,22 +807,223 @@ namespace Microsoft.AspNetCore.Razor.Language _builder.Push(new TagHelperBodyIntermediateNode()); - VisitDefault(block); + foreach (var item in node.Body) + { + Visit(item); + } _builder.Pop(); // Pop InitializeTagHelperStructureIntermediateNode - AddTagHelperAttributes(tagHelperBlock.Attributes, tagHelperBlock.Binding); + Visit(node.StartTag); _builder.Pop(); // Pop TagHelperIntermediateNode + + // No need to visit the end tag because we don't write any IR for it. + + // We don't want to track attributes from a previous tag helper element. + _renderedBoundAttributeNames.Clear(); } - private void Combine(HtmlContentIntermediateNode node, Span span) + public override void VisitMarkupTagHelperStartTag(MarkupTagHelperStartTagSyntax node) + { + foreach (var child in node.Children) + { + if (child is MarkupTagHelperAttributeSyntax || child is MarkupMinimizedTagHelperAttributeSyntax) + { + Visit(child); + } + } + } + + public override void VisitMarkupMinimizedTagHelperAttribute(MarkupMinimizedTagHelperAttributeSyntax node) + { + if (!_featureFlags.AllowMinimizedBooleanTagHelperAttributes) + { + // Minimized attributes are not valid for non-boolean bound attributes. TagHelperBlockRewriter + // has already logged an error if it was a non-boolean bound attribute; so we can skip. + return; + } + + var element = node.FirstAncestorOrSelf(); + var descriptors = element.TagHelperInfo.BindingResult.Descriptors; + var attributeName = node.Name.GetContent(); + var associatedDescriptors = descriptors.Where(descriptor => + descriptor.BoundAttributes.Any(attributeDescriptor => TagHelperMatchingConventions.CanSatisfyBoundAttribute(attributeName, attributeDescriptor))); + + if (associatedDescriptors.Any() && _renderedBoundAttributeNames.Add(attributeName)) + { + foreach (var associatedDescriptor in associatedDescriptors) + { + var associatedAttributeDescriptor = associatedDescriptor.BoundAttributes.First(a => + { + return TagHelperMatchingConventions.CanSatisfyBoundAttribute(attributeName, a); + }); + + var expectsBooleanValue = associatedAttributeDescriptor.ExpectsBooleanValue(attributeName); + + if (!expectsBooleanValue) + { + // We do not allow minimized non-boolean bound attributes. + return; + } + + var setTagHelperProperty = new TagHelperPropertyIntermediateNode() + { + AttributeName = attributeName, + BoundAttribute = associatedAttributeDescriptor, + TagHelper = associatedDescriptor, + AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, + Source = null, + IsIndexerNameMatch = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(attributeName, associatedAttributeDescriptor), + }; + + _builder.Add(setTagHelperProperty); + } + } + else + { + var addHtmlAttribute = new TagHelperHtmlAttributeIntermediateNode() + { + AttributeName = attributeName, + AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure + }; + + _builder.Add(addHtmlAttribute); + } + } + + public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSyntax node) + { + var element = node.FirstAncestorOrSelf(); + var descriptors = element.TagHelperInfo.BindingResult.Descriptors; + var attributeName = node.Name.GetContent(); + var attributeValueNode = node.Value; + var associatedDescriptors = descriptors.Where(descriptor => + descriptor.BoundAttributes.Any(attributeDescriptor => TagHelperMatchingConventions.CanSatisfyBoundAttribute(attributeName, attributeDescriptor))); + + if (associatedDescriptors.Any() && _renderedBoundAttributeNames.Add(attributeName)) + { + foreach (var associatedDescriptor in associatedDescriptors) + { + var associatedAttributeDescriptor = associatedDescriptor.BoundAttributes.First(a => + { + return TagHelperMatchingConventions.CanSatisfyBoundAttribute(attributeName, a); + }); + + var setTagHelperProperty = new TagHelperPropertyIntermediateNode() + { + AttributeName = attributeName, + BoundAttribute = associatedAttributeDescriptor, + TagHelper = associatedDescriptor, + AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure, + Source = BuildSourceSpanFromNode(attributeValueNode), + IsIndexerNameMatch = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(attributeName, associatedAttributeDescriptor), + }; + + _builder.Push(setTagHelperProperty); + VisitAttributeValue(attributeValueNode); + _builder.Pop(); + } + } + else + { + var addHtmlAttribute = new TagHelperHtmlAttributeIntermediateNode() + { + AttributeName = attributeName, + AttributeStructure = node.TagHelperAttributeInfo.AttributeStructure + }; + + _builder.Push(addHtmlAttribute); + VisitAttributeValue(attributeValueNode); + _builder.Pop(); + } + } + + private void VisitAttributeValue(SyntaxNode node) + { + if (node == null) + { + return; + } + + IReadOnlyList children = node.ChildNodes(); + var position = node.Position; + if (children.First() is MarkupBlockSyntax markupBlock && + markupBlock.Children.Count == 2 && + markupBlock.Children[0] is MarkupTextLiteralSyntax && + markupBlock.Children[1] is MarkupEphemeralTextLiteralSyntax) + { + // This is a special case when we have an attribute like attr="@@foo". + // In this case, we want the foo to be written out as HtmlContent and not HtmlAttributeValue. + Visit(markupBlock); + children = children.Skip(1).ToList(); + position = children.Count > 0 ? children[0].Position : position; + } + + if (children.All(c => c is MarkupLiteralAttributeValueSyntax)) + { + var literalAttributeValueNodes = children.Cast().ToArray(); + var valueTokens = SyntaxListBuilder.Create(); + for (var i = 0; i < literalAttributeValueNodes.Length; i++) + { + var mergedValue = MergeAttributeValue(literalAttributeValueNodes[i]); + valueTokens.AddRange(mergedValue.LiteralTokens); + } + var rewritten = SyntaxFactory.MarkupTextLiteral(valueTokens.ToList()).Green.CreateRed(node.Parent, position); + Visit(rewritten); + } + else if (children.All(c => c is MarkupTextLiteralSyntax)) + { + var builder = SyntaxListBuilder.Create(); + var markupLiteralArray = children.Cast(); + foreach (var literal in markupLiteralArray) + { + builder.AddRange(literal.LiteralTokens); + } + var rewritten = SyntaxFactory.MarkupTextLiteral(builder.ToList()).Green.CreateRed(node.Parent, position); + Visit(rewritten); + } + else if (children.All(c => c is CSharpExpressionLiteralSyntax)) + { + var builder = SyntaxListBuilder.Create(); + var expressionLiteralArray = children.Cast(); + SpanContext context = null; + foreach (var literal in expressionLiteralArray) + { + context = literal.GetSpanContext(); + builder.AddRange(literal.LiteralTokens); + } + var rewritten = SyntaxFactory.CSharpExpressionLiteral(builder.ToList()).Green.CreateRed(node.Parent, position); + rewritten = context != null ? rewritten.WithSpanContext(context) : rewritten; + Visit(rewritten); + } + else + { + Visit(node); + } + } + + private MarkupTextLiteralSyntax MergeAttributeValue(MarkupLiteralAttributeValueSyntax node) + { + var valueTokens = MergeLiterals(node.Prefix?.LiteralTokens, node.Value?.LiteralTokens); + var rewritten = node.Prefix?.Update(valueTokens) ?? node.Value?.Update(valueTokens); + rewritten = (MarkupTextLiteralSyntax)rewritten?.Green.CreateRed(node, node.Position); + var originalContext = rewritten.GetSpanContext(); + if (originalContext != null) + { + rewritten = rewritten.WithSpanContext(new SpanContext(new MarkupChunkGenerator(), originalContext.EditHandler)); + } + + return rewritten; + } + + private void Combine(HtmlContentIntermediateNode node, SyntaxNode item) { node.Children.Add(new IntermediateToken() { - Content = span.Content, + Content = item.GetContent(), Kind = TokenKind.Html, - Source = BuildSourceSpanFromNode(span), + Source = BuildSourceSpanFromNode(item), }); if (node.Source != null) @@ -700,76 +1035,25 @@ namespace Microsoft.AspNetCore.Razor.Language node.Source.Value.AbsoluteIndex, node.Source.Value.LineIndex, node.Source.Value.CharacterIndex, - node.Source.Value.Length + span.Content.Length); + node.Source.Value.Length + item.FullWidth); } } - private void AddTagHelperAttributes(IList attributes, TagHelperBinding tagHelperBinding) + private SyntaxList MergeLiterals(params SyntaxList?[] literals) { - var descriptors = tagHelperBinding.Descriptors; - var renderedBoundAttributeNames = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var attribute in attributes) + var builder = SyntaxListBuilder.Create(); + for (var i = 0; i < literals.Length; i++) { - var attributeValueNode = attribute.Value; - var associatedDescriptors = descriptors.Where(descriptor => - descriptor.BoundAttributes.Any(attributeDescriptor => TagHelperMatchingConventions.CanSatisfyBoundAttribute(attribute.Name, attributeDescriptor))); - - if (associatedDescriptors.Any() && renderedBoundAttributeNames.Add(attribute.Name)) + var literal = literals[i]; + if (!literal.HasValue) { - var isMinimizedAttribute = attributeValueNode == null; - if (isMinimizedAttribute && !_featureFlags.AllowMinimizedBooleanTagHelperAttributes) - { - // Minimized attributes are not valid for non-boolean bound attributes. TagHelperBlockRewriter - // has already logged an error if it was a non-boolean bound attribute; so we can skip. - continue; - } - - foreach (var associatedDescriptor in associatedDescriptors) - { - var associatedAttributeDescriptor = associatedDescriptor.BoundAttributes.First(a => - { - return TagHelperMatchingConventions.CanSatisfyBoundAttribute(attribute.Name, a); - }); - - var expectsBooleanValue = associatedAttributeDescriptor.ExpectsBooleanValue(attribute.Name); - - if (isMinimizedAttribute && !expectsBooleanValue) - { - // We do not allow minimized non-boolean bound attributes. - continue; - } - - var setTagHelperProperty = new TagHelperPropertyIntermediateNode() - { - AttributeName = attribute.Name, - BoundAttribute = associatedAttributeDescriptor, - TagHelper = associatedDescriptor, - AttributeStructure = attribute.AttributeStructure, - Source = BuildSourceSpanFromNode(attributeValueNode), - IsIndexerNameMatch = TagHelperMatchingConventions.SatisfiesBoundAttributeIndexer(attribute.Name, associatedAttributeDescriptor), - }; - - _builder.Push(setTagHelperProperty); - attributeValueNode?.Accept(this); - _builder.Pop(); - } + continue; } - else - { - var addHtmlAttribute = new TagHelperHtmlAttributeIntermediateNode() - { - AttributeName = attribute.Name, - AttributeStructure = attribute.AttributeStructure - }; - _builder.Push(addHtmlAttribute); - if (attributeValueNode != null) - { - attributeValueNode.Accept(this); - } - _builder.Pop(); - } + builder.AddRange(literal.Value); } + + return builder.ToList(); } } @@ -827,8 +1111,8 @@ namespace Microsoft.AspNetCore.Razor.Language } } - private static bool IsMalformed(List diagnostics) - => diagnostics.Count > 0 && diagnostics.Any(diagnostic => diagnostic.Severity == RazorDiagnosticSeverity.Error); + private static bool IsMalformed(IEnumerable diagnostics) + => diagnostics.Any(diagnostic => diagnostic.Severity == RazorDiagnosticSeverity.Error); } #pragma warning restore CS0618 // Type or member is obsolete } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectFileSystem.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectFileSystem.cs index 2600ce5efe..9283088cd1 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectFileSystem.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectFileSystem.cs @@ -49,6 +49,10 @@ namespace Microsoft.AspNetCore.Razor.Language var absolutePath = NormalizeAndEnsureValidPath(path); var file = new FileInfo(absolutePath); + if (!absolutePath.StartsWith(absoluteBasePath)) + { + throw new InvalidOperationException($"The file '{file.FullName}' is not a descendent of the base path '{absoluteBasePath}'."); + } var relativePhysicalPath = file.FullName.Substring(absoluteBasePath.Length + 1); // Include leading separator var filePath = "/" + relativePhysicalPath.Replace(Path.DirectorySeparatorChar, '/'); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorSyntaxTree.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorSyntaxTree.cs index 4b6d43fe06..018dbfe5fb 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorSyntaxTree.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorSyntaxTree.cs @@ -1,15 +1,17 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; namespace Microsoft.AspNetCore.Razor.Language { internal class DefaultRazorSyntaxTree : RazorSyntaxTree { public DefaultRazorSyntaxTree( - Block root, + SyntaxNode root, RazorSourceDocument source, IReadOnlyList diagnostics, RazorParserOptions options) @@ -24,7 +26,7 @@ namespace Microsoft.AspNetCore.Razor.Language public override RazorParserOptions Options { get; } - internal override Block Root { get; } + internal override SyntaxNode Root { get; } public override RazorSourceDocument Source { get; } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs index aa0668a539..c15aaff04e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; namespace Microsoft.AspNetCore.Razor.Language { @@ -39,11 +40,11 @@ namespace Microsoft.AspNetCore.Razor.Language for (var i = 0; i < imports.Count; i++) { var import = imports[i]; - visitor.VisitBlock(import.Root); + visitor.Visit(import.Root); } } - visitor.VisitBlock(syntaxTree.Root); + visitor.Visit(syntaxTree.Root); var tagHelperPrefix = visitor.TagHelperPrefix; descriptors = visitor.Matches.ToArray(); @@ -57,21 +58,9 @@ namespace Microsoft.AspNetCore.Razor.Language return; } - var errorSink = new ErrorSink(); - var rewriter = new TagHelperParseTreeRewriter(tagHelperPrefix, descriptors, syntaxTree.Options.FeatureFlags); - - var root = syntaxTree.Root; - root = rewriter.Rewrite(root, errorSink); - - var errorList = new List(); - errorList.AddRange(errorSink.Errors); - - errorList.AddRange(descriptors.SelectMany(d => d.GetAllDiagnostics())); - - var diagnostics = CombineErrors(syntaxTree.Diagnostics, errorList); - - var newSyntaxTree = RazorSyntaxTree.Create(root, syntaxTree.Source, diagnostics, syntaxTree.Options); - codeDocument.SetSyntaxTree(newSyntaxTree); + var rewrittenSyntaxTree = TagHelperParseTreeRewriter.Rewrite(syntaxTree, tagHelperPrefix, descriptors); + + codeDocument.SetSyntaxTree(rewrittenSyntaxTree); } private static bool MatchesDirective(TagHelperDescriptor descriptor, string typePattern, string assemblyName) @@ -97,25 +86,7 @@ namespace Microsoft.AspNetCore.Razor.Language return string.Equals(descriptor.Name, typePattern, StringComparison.Ordinal); } - private static int GetErrorLength(string directiveText) - { - var nonNullLength = directiveText == null ? 1 : directiveText.Length; - var normalizeEmptyStringLength = Math.Max(nonNullLength, 1); - - return normalizeEmptyStringLength; - } - - private IReadOnlyList CombineErrors(IReadOnlyList errors1, IReadOnlyList errors2) - { - var combinedErrors = new List(errors1.Count + errors2.Count); - combinedErrors.AddRange(errors1); - combinedErrors.AddRange(errors2); - - return combinedErrors; - } - - // Internal for testing. - internal class DirectiveVisitor : ParserVisitor + internal class DirectiveVisitor : SyntaxRewriter { private IReadOnlyList _tagHelpers; @@ -128,62 +99,80 @@ namespace Microsoft.AspNetCore.Razor.Language public HashSet Matches { get; } = new HashSet(); - public override void VisitAddTagHelperSpan(AddTagHelperChunkGenerator chunkGenerator, Span span) + public override SyntaxNode VisitRazorDirective(RazorDirectiveSyntax node) { - if (chunkGenerator.AssemblyName == null) + var descendantLiterals = node.DescendantNodes(); + foreach (var child in descendantLiterals) { - // Skip this one, it's an error - return; - } - - if (!AssemblyContainsTagHelpers(chunkGenerator.AssemblyName, _tagHelpers)) - { - // No tag helpers in the assembly. - return; - } - - for (var i = 0; i < _tagHelpers.Count; i++) - { - var tagHelper = _tagHelpers[i]; - if (MatchesDirective(tagHelper, chunkGenerator.TypePattern, chunkGenerator.AssemblyName)) + if (!(child is CSharpStatementLiteralSyntax literal)) { - Matches.Add(tagHelper); + continue; + } + + var context = literal.GetSpanContext(); + if (context == null) + { + // We can't find a chunk generator. + continue; + } + else if (context.ChunkGenerator is AddTagHelperChunkGenerator addTagHelper) + { + if (addTagHelper.AssemblyName == null) + { + // Skip this one, it's an error + continue; + } + + if (!AssemblyContainsTagHelpers(addTagHelper.AssemblyName, _tagHelpers)) + { + // No tag helpers in the assembly. + continue; + } + + for (var i = 0; i < _tagHelpers.Count; i++) + { + var tagHelper = _tagHelpers[i]; + if (MatchesDirective(tagHelper, addTagHelper.TypePattern, addTagHelper.AssemblyName)) + { + Matches.Add(tagHelper); + } + } + } + else if (context.ChunkGenerator is RemoveTagHelperChunkGenerator removeTagHelper) + { + if (removeTagHelper.AssemblyName == null) + { + // Skip this one, it's an error + continue; + } + + + if (!AssemblyContainsTagHelpers(removeTagHelper.AssemblyName, _tagHelpers)) + { + // No tag helpers in the assembly. + continue; + } + + for (var i = 0; i < _tagHelpers.Count; i++) + { + var tagHelper = _tagHelpers[i]; + if (MatchesDirective(tagHelper, removeTagHelper.TypePattern, removeTagHelper.AssemblyName)) + { + Matches.Remove(tagHelper); + } + } + } + else if (context.ChunkGenerator is TagHelperPrefixDirectiveChunkGenerator tagHelperPrefix) + { + if (!string.IsNullOrEmpty(tagHelperPrefix.DirectiveText)) + { + // We only expect to see a single one of these per file, but that's enforced at another level. + TagHelperPrefix = tagHelperPrefix.DirectiveText; + } } } - } - public override void VisitRemoveTagHelperSpan(RemoveTagHelperChunkGenerator chunkGenerator, Span span) - { - if (chunkGenerator.AssemblyName == null) - { - // Skip this one, it's an error - return; - } - - - if (!AssemblyContainsTagHelpers(chunkGenerator.AssemblyName, _tagHelpers)) - { - // No tag helpers in the assembly. - return; - } - - for (var i = 0; i < _tagHelpers.Count; i++) - { - var tagHelper = _tagHelpers[i]; - if (MatchesDirective(tagHelper, chunkGenerator.TypePattern, chunkGenerator.AssemblyName)) - { - Matches.Remove(tagHelper); - } - } - } - - public override void VisitTagHelperPrefixDirectiveSpan(TagHelperPrefixDirectiveChunkGenerator chunkGenerator, Span span) - { - if (!string.IsNullOrEmpty(chunkGenerator.DirectiveText)) - { - // We only expect to see a single one of these per file, but that's enforced at another level. - TagHelperPrefix = chunkGenerator.DirectiveText; - } + return base.VisitRazorDirective(node); } private bool AssemblyContainsTagHelpers(string assemblyName, IReadOnlyList tagHelpers) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DirectiveTokenEditHandler.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DirectiveTokenEditHandler.cs index 308c274518..c84d8b0c67 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DirectiveTokenEditHandler.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/DirectiveTokenEditHandler.cs @@ -4,18 +4,19 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; namespace Microsoft.AspNetCore.Razor.Language { internal class DirectiveTokenEditHandler : SpanEditHandler { - public DirectiveTokenEditHandler(Func> tokenizer) : base(tokenizer) + public DirectiveTokenEditHandler(Func> tokenizer) : base(tokenizer) { } - protected override PartialParseResultInternal CanAcceptChange(Span target, SourceChange change) + protected override PartialParseResultInternal CanAcceptChange(SyntaxNode target, SourceChange change) { - if (AcceptedCharacters == AcceptedCharactersInternal.NonWhiteSpace) + if (AcceptedCharacters == AcceptedCharactersInternal.NonWhitespace) { var originalText = change.GetOriginalText(target); var editedContent = change.GetEditedContent(target); @@ -30,7 +31,6 @@ namespace Microsoft.AspNetCore.Razor.Language } return PartialParseResultInternal.Rejected; - } private static bool ContainsWhitespace(string content) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Extensions/DesignTimeDirectiveTargetExtension.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Extensions/DesignTimeDirectiveTargetExtension.cs index 316c255bfe..29cc438fbb 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Extensions/DesignTimeDirectiveTargetExtension.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Extensions/DesignTimeDirectiveTargetExtension.cs @@ -65,15 +65,17 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions } // {node.Content} __typeHelper = default({node.Content}); - - context.AddSourceMappingFor(node); - context.CodeWriter - .Write(node.Content) - .Write(" ") - .WriteStartAssignment(TypeHelper) - .Write("default(") - .Write(node.Content) - .WriteLine(");"); + using (context.CodeWriter.BuildLinePragma(node.Source)) + { + context.AddSourceMappingFor(node); + context.CodeWriter + .Write(node.Content) + .Write(" ") + .WriteStartAssignment(TypeHelper) + .Write("default(") + .Write(node.Content) + .WriteLine(");"); + } break; case DirectiveTokenKind.Member: @@ -86,16 +88,18 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions } // global::System.Object {node.content} = null; - - context.CodeWriter + using (context.CodeWriter.BuildLinePragma(node.Source)) + { + context.CodeWriter .Write("global::") .Write(typeof(object).FullName) .Write(" "); - context.AddSourceMappingFor(node); - context.CodeWriter - .Write(node.Content) - .WriteLine(" = null;"); + context.AddSourceMappingFor(node); + context.CodeWriter + .Write(node.Content) + .WriteLine(" = null;"); + } break; case DirectiveTokenKind.Namespace: @@ -108,46 +112,50 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions } // global::System.Object __typeHelper = nameof({node.Content}); - - context.CodeWriter + using (context.CodeWriter.BuildLinePragma(node.Source)) + { + context.CodeWriter .Write("global::") .Write(typeof(object).FullName) .Write(" ") .WriteStartAssignment(TypeHelper); - context.CodeWriter.Write("nameof("); + context.CodeWriter.Write("nameof("); - context.AddSourceMappingFor(node); - context.CodeWriter - .Write(node.Content) - .WriteLine(");"); + context.AddSourceMappingFor(node); + context.CodeWriter + .Write(node.Content) + .WriteLine(");"); + } break; case DirectiveTokenKind.String: // global::System.Object __typeHelper = "{node.Content}"; - - context.CodeWriter + using (context.CodeWriter.BuildLinePragma(node.Source)) + { + context.CodeWriter .Write("global::") .Write(typeof(object).FullName) .Write(" ") .WriteStartAssignment(TypeHelper); - if (node.Content.StartsWith("\"", StringComparison.Ordinal)) - { - context.AddSourceMappingFor(node); - context.CodeWriter.Write(node.Content); - } - else - { - context.CodeWriter.Write("\""); - context.AddSourceMappingFor(node); - context.CodeWriter - .Write(node.Content) - .Write("\""); - } + if (node.Content.StartsWith("\"", StringComparison.Ordinal)) + { + context.AddSourceMappingFor(node); + context.CodeWriter.Write(node.Content); + } + else + { + context.CodeWriter.Write("\""); + context.AddSourceMappingFor(node); + context.CodeWriter + .Write(node.Content) + .Write("\""); + } - context.CodeWriter.WriteLine(";"); + context.CodeWriter.WriteLine(";"); + } break; } context.CodeWriter.CurrentIndent = originalIndent; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Extensions/EliminateMethodBodyPass.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Extensions/EliminateMethodBodyPass.cs new file mode 100644 index 0000000000..96d336ca72 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Extensions/EliminateMethodBodyPass.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Language.Extensions +{ + internal sealed class EliminateMethodBodyPass : IntermediateNodePassBase, IRazorOptimizationPass + { + // Run early in the optimization phase + public override int Order => int.MinValue; + + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + if (codeDocument == null) + { + throw new ArgumentNullException(nameof(codeDocument)); + } + + if (documentNode == null) + { + throw new ArgumentNullException(nameof(documentNode)); + } + + var codeGenerationOptions = codeDocument.GetCodeGenerationOptions(); + if (codeGenerationOptions == null || !codeGenerationOptions.SuppressPrimaryMethodBody) + { + return; + } + + var method = documentNode.FindPrimaryMethod(); + if (method == null) + { + return; + } + + method.Children.Clear(); + + // After we clear all of the method body there might be some unused fields, which can be + // blocking if compiling with warnings as errors. Suppress this warning so that it doesn't + // get annoying in VS. + documentNode.Children.Insert(documentNode.Children.IndexOf(documentNode.FindPrimaryNamespace()), new CSharpCodeIntermediateNode() + { + Children = + { + // Field is assigned but never used + new IntermediateToken() + { + Content = "#pragma warning disable 0414" + Environment.NewLine, + Kind = TokenKind.CSharp, + }, + + // Field is never assigned + new IntermediateToken() + { + Content = "#pragma warning disable 0649" + Environment.NewLine, + Kind = TokenKind.CSharp, + }, + + // Field is never used + new IntermediateToken() + { + Content = "#pragma warning disable 0169" + Environment.NewLine, + Kind = TokenKind.CSharp, + }, + }, + }); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/HtmlNodeOptimizationPass.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/HtmlNodeOptimizationPass.cs index ea3d754554..1b327aa032 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/HtmlNodeOptimizationPass.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/HtmlNodeOptimizationPass.cs @@ -22,14 +22,11 @@ namespace Microsoft.AspNetCore.Razor.Language throw new ArgumentNullException(nameof(syntaxTree)); } - var conditionalAttributeCollapser = new ConditionalAttributeCollapser(); - var rewritten = conditionalAttributeCollapser.Rewrite(syntaxTree.Root); - - var whitespaceRewriter = new WhiteSpaceRewriter(); - rewritten = whitespaceRewriter.Rewrite(rewritten); + var whitespaceRewriter = new WhitespaceRewriter(); + var rewritten = whitespaceRewriter.Visit(syntaxTree.Root); var rewrittenSyntaxTree = RazorSyntaxTree.Create(rewritten, syntaxTree.Source, syntaxTree.Diagnostics, syntaxTree.Options); return rewrittenSyntaxTree; } } -} +} \ No newline at end of file diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Intermediate/InputDocumentKind.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Intermediate/InputDocumentKind.cs new file mode 100644 index 0000000000..7adf2bd8b5 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Intermediate/InputDocumentKind.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Language.Intermediate +{ + internal static class InputDocumentKind + { + public static readonly string Component = "component"; + + public static readonly string MvcFile = "mvc"; + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AcceptedCharactersInternal.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AcceptedCharactersInternal.cs index f6b658f769..9838a182b5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AcceptedCharactersInternal.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AcceptedCharactersInternal.cs @@ -10,13 +10,13 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { None = 0, NewLine = 1, - WhiteSpace = 2, + Whitespace = 2, - NonWhiteSpace = 4, + NonWhitespace = 4, - AllWhiteSpace = NewLine | WhiteSpace, - Any = AllWhiteSpace | NonWhiteSpace, + AllWhitespace = NewLine | Whitespace, + Any = AllWhitespace | NonWhitespace, - AnyExceptNewline = NonWhiteSpace | WhiteSpace + AnyExceptNewline = NonWhitespace | Whitespace } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AddImportChunkGenerator.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AddImportChunkGenerator.cs index 0d7b9de155..4cee1abe3e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AddImportChunkGenerator.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AddImportChunkGenerator.cs @@ -14,23 +14,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public string Namespace { get; } - public override void Accept(ParserVisitor visitor, Span span) - { - visitor.VisitImportSpan(this, span); - } - - public override void GenerateChunk(Span target, ChunkGeneratorContext context) - { - var ns = Namespace; - - if (!string.IsNullOrEmpty(ns) && char.IsWhiteSpace(ns[0])) - { - ns = ns.Substring(1); - } - - //context.ChunkTreeBuilder.AddUsingChunk(ns, target); - } - public override string ToString() { return "Import:" + Namespace + ";"; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AddTagHelperChunkGenerator.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AddTagHelperChunkGenerator.cs index f446be12c0..2df86b32de 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AddTagHelperChunkGenerator.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AddTagHelperChunkGenerator.cs @@ -35,11 +35,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public List Diagnostics { get; } - public override void Accept(ParserVisitor visitor, Span span) - { - visitor.VisitAddTagHelperSpan(this, span); - } - /// public override bool Equals(object obj) { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AttributeBlockChunkGenerator.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AttributeBlockChunkGenerator.cs deleted file mode 100644 index de77a1746d..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AttributeBlockChunkGenerator.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Globalization; -using Microsoft.Extensions.Internal; - -namespace Microsoft.AspNetCore.Razor.Language.Legacy -{ - internal class AttributeBlockChunkGenerator : ParentChunkGenerator - { - public AttributeBlockChunkGenerator(string name, LocationTagged prefix, LocationTagged suffix) - { - Name = name; - Prefix = prefix; - Suffix = suffix; - } - - public string Name { get; } - - public LocationTagged Prefix { get; } - - public LocationTagged Suffix { get; } - - public override void GenerateStartParentChunk(Block target, ChunkGeneratorContext context) - { - //var chunk = context.ChunkTreeBuilder.StartParentChunk(target); - - //chunk.Attribute = Name; - //chunk.Prefix = Prefix; - //chunk.Suffix = Suffix; - } - - public override void GenerateEndParentChunk(Block target, ChunkGeneratorContext context) - { - //context.ChunkTreeBuilder.EndParentChunk(); - } - - public override void Accept(ParserVisitor visitor, Block block) - { - visitor.VisitAttributeBlock(this, block); - } - - public override string ToString() - { - return string.Format(CultureInfo.CurrentCulture, "Attr:{0},{1:F},{2:F}", Name, Prefix, Suffix); - } - - public override bool Equals(object obj) - { - var other = obj as AttributeBlockChunkGenerator; - return other != null && - string.Equals(other.Name, Name, StringComparison.Ordinal) && - Equals(other.Prefix, Prefix) && - Equals(other.Suffix, Suffix); - } - - public override int GetHashCode() - { - var hashCodeCombiner = HashCodeCombiner.Start(); - hashCodeCombiner.Add(Name, StringComparer.Ordinal); - hashCodeCombiner.Add(Prefix); - hashCodeCombiner.Add(Suffix); - - return hashCodeCombiner; - } - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs index 3aecb29a52..2514377780 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/AutoCompleteEditHandler.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Razor.Language.Legacy @@ -11,18 +12,18 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { private static readonly int TypeHashCode = typeof(AutoCompleteEditHandler).GetHashCode(); - public AutoCompleteEditHandler(Func> tokenizer) + public AutoCompleteEditHandler(Func> tokenizer) : base(tokenizer) { } - public AutoCompleteEditHandler(Func> tokenizer, bool autoCompleteAtEndOfSpan) + public AutoCompleteEditHandler(Func> tokenizer, bool autoCompleteAtEndOfSpan) : this(tokenizer) { AutoCompleteAtEndOfSpan = autoCompleteAtEndOfSpan; } - public AutoCompleteEditHandler(Func> tokenizer, AcceptedCharactersInternal accepted) + public AutoCompleteEditHandler(Func> tokenizer, AcceptedCharactersInternal accepted) : base(tokenizer, accepted) { } @@ -31,7 +32,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public string AutoCompleteString { get; set; } - protected override PartialParseResultInternal CanAcceptChange(Span target, SourceChange change) + protected override PartialParseResultInternal CanAcceptChange(SyntaxNode target, SourceChange change) { if (((AutoCompleteAtEndOfSpan && IsAtEndOfSpan(target, change)) || IsAtEndOfFirstLine(target, change)) && change.IsInsert && diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs deleted file mode 100644 index ee7cc135f1..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Microsoft.Extensions.Internal; - -namespace Microsoft.AspNetCore.Razor.Language.Legacy -{ - internal class Block : SyntaxTreeNode - { - private int? _length; - - public Block(BlockBuilder source) - : this(source.Type, source.Children, source.ChunkGenerator) - { - source.Reset(); - } - - protected Block(BlockKindInternal? type, IReadOnlyList children, IParentChunkGenerator generator) - { - if (type == null) - { - throw new InvalidOperationException(Resources.Block_Type_Not_Specified); - } - - Type = type.Value; - Children = children; - ChunkGenerator = generator; - - // Perf: Avoid allocating an enumerator. - for (var i = 0; i < Children.Count; i++) - { - Children[i].Parent = this; - } - } - public IParentChunkGenerator ChunkGenerator { get; } - - public BlockKindInternal Type { get; } - - public IReadOnlyList Children { get; } - - public override bool IsBlock => true; - - public override SourceLocation Start - { - get - { - var child = Children.FirstOrDefault(); - if (child == null) - { - return SourceLocation.Zero; - } - else - { - return child.Start; - } - } - } - - public override int Length - { - get - { - if (_length == null) - { - var length = 0; - for (var i = 0; i < Children.Count; i++) - { - length += Children[i].Length; - } - - _length = length; - } - - return _length.Value; - } - } - - - public virtual IEnumerable Flatten() - { - // Perf: Avoid allocating an enumerator. - for (var i = 0; i < Children.Count; i++) - { - var element = Children[i]; - var span = element as Span; - if (span != null) - { - yield return span; - } - else - { - var block = element as Block; - foreach (Span childSpan in block.Flatten()) - { - yield return childSpan; - } - } - } - } - - public Span FindFirstDescendentSpan() - { - SyntaxTreeNode current = this; - while (current != null && current.IsBlock) - { - current = ((Block)current).Children.FirstOrDefault(); - } - return current as Span; - } - - public Span FindLastDescendentSpan() - { - SyntaxTreeNode current = this; - while (current != null && current.IsBlock) - { - current = ((Block)current).Children.LastOrDefault(); - } - return current as Span; - } - - public virtual Span LocateOwner(SourceChange change) => LocateOwner(change, Children); - - protected static Span LocateOwner(SourceChange change, IEnumerable elements) - { - // Ask each child recursively - Span owner = null; - foreach (var element in elements) - { - var span = element as Span; - if (span == null) - { - owner = ((Block)element).LocateOwner(change); - } - else - { - if (change.Span.AbsoluteIndex < span.Start.AbsoluteIndex) - { - // Early escape for cases where changes overlap multiple spans - // In those cases, the span will return false, and we don't want to search the whole tree - // So if the current span starts after the change, we know we've searched as far as we need to - break; - } - owner = span.EditHandler.OwnsChange(span, change) ? span : owner; - } - - if (owner != null) - { - break; - } - } - return owner; - } - - public override string ToString() - { - return string.Format( - CultureInfo.CurrentCulture, - "{0} Block at {1}::{2} (Gen:{3})", - Type, - Start, - Length, - ChunkGenerator); - } - - public override bool Equals(object obj) - { - var other = obj as Block; - return other != null && - Type == other.Type && - Equals(ChunkGenerator, other.ChunkGenerator) && - ChildrenEqual(Children, other.Children); - } - - public override int GetHashCode() - { - var hashCodeCombiner = HashCodeCombiner.Start(); - hashCodeCombiner.Add(Type); - hashCodeCombiner.Add(ChunkGenerator); - hashCodeCombiner.Add(Children); - - return hashCodeCombiner; - } - - private static bool ChildrenEqual(IEnumerable left, IEnumerable right) - { - IEnumerator leftEnum = left.GetEnumerator(); - IEnumerator rightEnum = right.GetEnumerator(); - while (leftEnum.MoveNext()) - { - if (!rightEnum.MoveNext() || // More items in left than in right - !Equals(leftEnum.Current, rightEnum.Current)) - { - // Nodes are not equal - return false; - } - } - if (rightEnum.MoveNext()) - { - // More items in right than left - return false; - } - return true; - } - - public override bool EquivalentTo(SyntaxTreeNode node) - { - var other = node as Block; - if (other == null || other.Type != Type) - { - return false; - } - - return Enumerable.SequenceEqual(Children, other.Children, EquivalenceComparer.Default); - } - - public override int GetEquivalenceHash() - { - var hashCodeCombiner = HashCodeCombiner.Start(); - hashCodeCombiner.Add(Type); - foreach (var child in Children) - { - hashCodeCombiner.Add(child.GetEquivalenceHash()); - } - - return hashCodeCombiner.CombinedHash; - } - - public override void Accept(ParserVisitor visitor) - { - visitor.VisitBlock(this); - } - - public override SyntaxTreeNode Clone() - { - var blockBuilder = new BlockBuilder(this); - - blockBuilder.Children.Clear(); - for (var i = 0; i < Children.Count; i++) - { - var clonedChild = Children[i].Clone(); - blockBuilder.Children.Add(clonedChild); - } - - return blockBuilder.Build(); - } - - internal void ChildChanged() - { - // A node in our graph has changed. We'll need to recompute our length the next time we're asked for it. - _length = null; - - Parent?.ChildChanged(); - } - - private class EquivalenceComparer : IEqualityComparer - { - public static readonly EquivalenceComparer Default = new EquivalenceComparer(); - - private EquivalenceComparer() - { - } - - public bool Equals(SyntaxTreeNode nodeX, SyntaxTreeNode nodeY) - { - if (nodeX == nodeY) - { - return true; - } - - return nodeX != null && nodeX.EquivalentTo(nodeY); - } - - public int GetHashCode(SyntaxTreeNode node) - { - if (node == null) - { - throw new ArgumentNullException(nameof(node)); - } - - return node.GetEquivalenceHash(); - } - } - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/BlockBuilder.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/BlockBuilder.cs deleted file mode 100644 index 3e3905c4a7..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/BlockBuilder.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Razor.Language.Legacy -{ - internal class BlockBuilder - { - public BlockBuilder() - { - Reset(); - } - - public BlockBuilder(Block original) - { - Type = original.Type; - Children = new List(original.Children); - ChunkGenerator = original.ChunkGenerator; - } - - public IParentChunkGenerator ChunkGenerator { get; set; } - - public BlockKindInternal? Type { get; set; } - - public List Children { get; private set; } - - public virtual Block Build() - { - return new Block(this); - } - - public virtual void Reset() - { - Type = null; - Children = new List(); - ChunkGenerator = ParentChunkGenerator.Null; - } - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/BlockExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/BlockExtensions.cs deleted file mode 100644 index 232baf7ee9..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/BlockExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Razor.Language.Legacy -{ - internal static class BlockExtensions - { - public static void LinkNodes(this Block self) - { - Span first = null; - Span previous = null; - foreach (Span span in self.Flatten()) - { - if (first == null) - { - first = span; - } - span.Previous = previous; - - if (previous != null) - { - previous.Next = span; - } - previous = span; - } - } - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs index a75c983fec..44612ee18c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs @@ -5,17 +5,18 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax; namespace Microsoft.AspNetCore.Razor.Language.Legacy { - internal class CSharpCodeParser : TokenizerBackedParser + internal class CSharpCodeParser : TokenizerBackedParser { private static HashSet InvalidNonWhitespaceNameCharacters = new HashSet(new[] { '@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'', '*' }); - private static readonly Func IsValidStatementSpacingToken = + private static readonly Func IsValidStatementSpacingToken = IsSpacingToken(includeNewLines: true, includeComments: true); internal static readonly DirectiveDescriptor AddTagHelperDirectiveDescriptor = DirectiveDescriptor.CreateDirective( @@ -69,8 +70,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy private readonly ISet CurrentKeywords = new HashSet(DefaultKeywords); - private Dictionary _directiveParsers = new Dictionary(StringComparer.Ordinal); - private Dictionary> _keywordParsers = new Dictionary>(); + private Dictionary, CSharpTransitionSyntax>> _keywordParserMap = new Dictionary, CSharpTransitionSyntax>>(); + private Dictionary, CSharpTransitionSyntax>> _directiveParserMap = new Dictionary, CSharpTransitionSyntax>>(StringComparer.Ordinal); public CSharpCodeParser(ParserContext context) : this(directives: Enumerable.Empty(), context: context) @@ -91,9 +92,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } Keywords = new HashSet(); - SetUpKeywords(); - SetupDirectives(directives); - SetUpExpressions(); + SetupKeywordParsers(); + SetupExpressionParsers(); + SetupDirectiveParsers(directives); } public HtmlMarkupParser HtmlParser { get; set; } @@ -102,262 +103,146 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public bool IsNested { get; set; } - protected override bool TokenTypeEquals(CSharpTokenType x, CSharpTokenType y) => x == y; - - protected void MapDirectives(Action handler, params string[] directives) + public CSharpCodeBlockSyntax ParseBlock() { - foreach (var directive in directives) + if (Context == null) { - _directiveParsers.Add(directive, () => - { - handler(); - Context.SeenDirectives.Add(directive); - }); - - Keywords.Add(directive); - - // These C# keywords are reserved for use in directives. It's an error to use them outside of - // a directive. This code removes the error generation if the directive *is* registered. - if (string.Equals(directive, "class", StringComparison.OrdinalIgnoreCase)) - { - _keywordParsers.Remove(CSharpKeyword.Class); - } - else if (string.Equals(directive, "namespace", StringComparison.OrdinalIgnoreCase)) - { - _keywordParsers.Remove(CSharpKeyword.Namespace); - } + throw new InvalidOperationException(Resources.Parser_Context_Not_Set); } - } - protected bool TryGetDirectiveHandler(string directive, out Action handler) - { - return _directiveParsers.TryGetValue(directive, out handler); - } - - private void MapExpressionKeyword(Action handler, CSharpKeyword keyword) - { - _keywordParsers.Add(keyword, handler); - - // Expression keywords don't belong in the regular keyword list - } - - private void MapKeywords(Action handler, params CSharpKeyword[] keywords) - { - MapKeywords(handler, topLevel: true, keywords: keywords); - } - - private void MapKeywords(Action handler, bool topLevel, params CSharpKeyword[] keywords) - { - foreach (var keyword in keywords) + if (EndOfFile) { - _keywordParsers.Add(keyword, handler); - if (topLevel) - { - Keywords.Add(CSharpLanguageCharacteristics.GetKeyword(keyword)); - } + // Nothing to parse. + return null; } - } - [Conditional("DEBUG")] - internal void Assert(CSharpKeyword expectedKeyword) - { - Debug.Assert(CurrentToken.Type == CSharpTokenType.Keyword && - CurrentToken.Keyword.HasValue && - CurrentToken.Keyword.Value == expectedKeyword); - } - - protected internal bool At(CSharpKeyword keyword) - { - return At(CSharpTokenType.Keyword) && - CurrentToken.Keyword.HasValue && - CurrentToken.Keyword.Value == keyword; - } - - protected internal bool AcceptIf(CSharpKeyword keyword) - { - if (At(keyword)) + using (var pooledResult = Pool.Allocate()) + using (PushSpanContextConfig(DefaultSpanContextConfig)) { - AcceptAndMoveNext(); - return true; - } - return false; - } - - protected static Func IsSpacingToken(bool includeNewLines, bool includeComments) - { - return token => token.Type == CSharpTokenType.WhiteSpace || - (includeNewLines && token.Type == CSharpTokenType.NewLine) || - (includeComments && token.Type == CSharpTokenType.Comment); - } - - public override void ParseBlock() - { - using (PushSpanConfig(DefaultSpanConfig)) - { - if (Context == null) - { - throw new InvalidOperationException(Resources.Parser_Context_Not_Set); - } - - Span.Start = CurrentLocation; - - // Unless changed, the block is a statement block - using (Context.Builder.StartBlock(BlockKindInternal.Statement)) + var builder = pooledResult.Builder; + try { NextToken(); + // Unless changed, the block is a statement block AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + builder.Add(OutputTokensAsStatementLiteral()); - var current = CurrentToken; - if (At(CSharpTokenType.StringLiteral) && + // We are usually called when the other parser sees a transition '@'. Look for it. + SyntaxToken transitionToken = null; + if (At(SyntaxKind.StringLiteral) && CurrentToken.Content.Length > 0 && CurrentToken.Content[0] == SyntaxConstants.TransitionCharacter) { - var split = Language.SplitToken(CurrentToken, 1, CSharpTokenType.Transition); - current = split.Item1; + var split = Language.SplitToken(CurrentToken, 1, SyntaxKind.Transition); + transitionToken = split.Item1; // Back up to the end of the transition Context.Source.Position -= split.Item2.Content.Length; NextToken(); } - else if (At(CSharpTokenType.Transition)) + else if (At(SyntaxKind.Transition)) { - NextToken(); + transitionToken = EatCurrentToken(); } - // Accept "@" if we see it, but if we don't, that's OK. We assume we were started for a good reason - if (current.Type == CSharpTokenType.Transition) + if (transitionToken == null) { - if (Span.Tokens.Count > 0) + transitionToken = SyntaxFactory.MissingToken(SyntaxKind.Transition); + } + + SpanContext.ChunkGenerator = SpanChunkGenerator.Null; + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; + var transition = GetNodeWithSpanContext(SyntaxFactory.CSharpTransition(transitionToken)); + + if (At(SyntaxKind.LeftBrace)) + { + var statementBody = ParseStatementBody(); + var statement = SyntaxFactory.CSharpStatement(transition, statementBody); + builder.Add(statement); + } + else if (At(SyntaxKind.LeftParenthesis)) + { + var expressionBody = ParseExplicitExpressionBody(); + var expression = SyntaxFactory.CSharpExplicitExpression(transition, expressionBody); + builder.Add(expression); + } + else if (At(SyntaxKind.Identifier)) + { + if (!TryParseDirective(builder, transition, CurrentToken.Content)) { - Output(SpanKindInternal.Code); + if (string.Equals( + CurrentToken.Content, + SyntaxConstants.CSharp.HelperKeyword, + StringComparison.Ordinal)) + { + var diagnostic = RazorDiagnosticFactory.CreateParsing_HelperDirectiveNotAvailable( + new SourceSpan(CurrentStart, CurrentToken.Content.Length)); + CurrentToken.SetDiagnostics(new[] { diagnostic }); + Context.ErrorSink.OnError(diagnostic); + } + + var implicitExpressionBody = ParseImplicitExpressionBody(); + var implicitExpression = SyntaxFactory.CSharpImplicitExpression(transition, implicitExpressionBody); + builder.Add(implicitExpression); } - AtTransition(current); + } + else if (At(SyntaxKind.Keyword)) + { + if (!TryParseDirective(builder, transition, CurrentToken.Content) && + !TryParseKeyword(builder, transition)) + { + // Not a directive or a special keyword. Just parse as an implicit expression. + var implicitExpressionBody = ParseImplicitExpressionBody(); + var implicitExpression = SyntaxFactory.CSharpImplicitExpression(transition, implicitExpressionBody); + builder.Add(implicitExpression); + } + + builder.Add(OutputTokensAsStatementLiteral()); } else { - // No "@" => Jump straight to AfterTransition - AfterTransition(); - } - - Output(SpanKindInternal.Code); - } - } - } - - private void DefaultSpanConfig(SpanBuilder span) - { - span.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString); - span.ChunkGenerator = new StatementChunkGenerator(); - } - - private void AtTransition(CSharpToken current) - { - Debug.Assert(current.Type == CSharpTokenType.Transition); - Accept(current); - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; - Span.ChunkGenerator = SpanChunkGenerator.Null; - - // Output the "@" span and continue here - Output(SpanKindInternal.Transition); - AfterTransition(); - } - - private void AfterTransition() - { - using (PushSpanConfig(DefaultSpanConfig)) - { - EnsureCurrent(); - try - { - // What type of block is this? - if (!EndOfFile) - { - if (CurrentToken.Type == CSharpTokenType.LeftParenthesis) + // Invalid character + SpanContext.ChunkGenerator = new ExpressionChunkGenerator(); + SpanContext.EditHandler = new ImplicitExpressionEditHandler( + Language.TokenizeString, + CurrentKeywords, + acceptTrailingDot: IsNested) { - Context.Builder.CurrentBlock.Type = BlockKindInternal.Expression; - Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); - ExplicitExpression(); - return; + AcceptedCharacters = AcceptedCharactersInternal.NonWhitespace + }; + + AcceptMarkerTokenIfNecessary(); + var expressionLiteral = SyntaxFactory.CSharpCodeBlock(OutputTokensAsExpressionLiteral()); + var expressionBody = SyntaxFactory.CSharpImplicitExpressionBody(expressionLiteral); + var expressionBlock = SyntaxFactory.CSharpImplicitExpression(transition, expressionBody); + builder.Add(expressionBlock); + + if (At(SyntaxKind.Whitespace) || At(SyntaxKind.NewLine)) + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_UnexpectedWhiteSpaceAtStartOfCodeBlock( + new SourceSpan(CurrentStart, CurrentToken.Content.Length))); } - else if (CurrentToken.Type == CSharpTokenType.Identifier) + else if (EndOfFile) { - if (TryGetDirectiveHandler(CurrentToken.Content, out var handler)) - { - Span.ChunkGenerator = SpanChunkGenerator.Null; - handler(); - return; - } - else - { - if (string.Equals( - CurrentToken.Content, - SyntaxConstants.CSharp.HelperKeyword, - StringComparison.Ordinal)) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_HelperDirectiveNotAvailable( - new SourceSpan(CurrentStart, CurrentToken.Content.Length))); - } - - Context.Builder.CurrentBlock.Type = BlockKindInternal.Expression; - Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); - ImplicitExpression(); - return; - } + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_UnexpectedEndOfFileAtStartOfCodeBlock( + new SourceSpan(CurrentStart, contentLength: 1 /* end of file */))); } - else if (CurrentToken.Type == CSharpTokenType.Keyword) + else { - if (TryGetDirectiveHandler(CurrentToken.Content, out var handler)) - { - Span.ChunkGenerator = SpanChunkGenerator.Null; - handler(); - return; - } - else - { - KeywordBlock(topLevel: true); - return; - } - } - else if (CurrentToken.Type == CSharpTokenType.LeftBrace) - { - VerbatimBlock(); - return; + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_UnexpectedCharacterAtStartOfCodeBlock( + new SourceSpan(CurrentStart, CurrentToken.Content.Length), + CurrentToken.Content)); } } - // Invalid character - Context.Builder.CurrentBlock.Type = BlockKindInternal.Expression; - Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); - AddMarkerTokenIfNecessary(); - Span.ChunkGenerator = new ExpressionChunkGenerator(); - Span.EditHandler = new ImplicitExpressionEditHandler( - Language.TokenizeString, - CurrentKeywords, - acceptTrailingDot: IsNested) - { - AcceptedCharacters = AcceptedCharactersInternal.NonWhiteSpace - }; - if (At(CSharpTokenType.WhiteSpace) || At(CSharpTokenType.NewLine)) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_UnexpectedWhiteSpaceAtStartOfCodeBlock( - new SourceSpan(CurrentStart, CurrentToken.Content.Length))); - } - else if (EndOfFile) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_UnexpectedEndOfFileAtStartOfCodeBlock( - new SourceSpan(CurrentStart, contentLength: 1 /* end of file */))); - } - else - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_UnexpectedCharacterAtStartOfCodeBlock( - new SourceSpan(CurrentStart, CurrentToken.Content.Length), - CurrentToken.Content)); - } + Debug.Assert(TokenBuilder.Count == 0, "We should not have any tokens left."); + + var codeBlock = SyntaxFactory.CSharpCodeBlock(builder.ToList()); + return codeBlock; } finally { @@ -367,75 +252,90 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } } - private void VerbatimBlock() + private CSharpExplicitExpressionBodySyntax ParseExplicitExpressionBody() { - Assert(CSharpTokenType.LeftBrace); - var block = new Block(Resources.BlockName_Code, CurrentStart); - AcceptAndMoveNext(); + var block = new Block(Resources.BlockName_ExplicitExpression, CurrentStart); + Assert(SyntaxKind.LeftParenthesis); + var leftParenToken = EatCurrentToken(); + var leftParen = OutputAsMetaCode(leftParenToken); - // Set up the "{" span and output - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; - Span.ChunkGenerator = SpanChunkGenerator.Null; - Output(SpanKindInternal.MetaCode); - - // Set up auto-complete and parse the code block - var editHandler = new AutoCompleteEditHandler(Language.TokenizeString); - Span.EditHandler = editHandler; - CodeBlock(false, block); - - Span.ChunkGenerator = new StatementChunkGenerator(); - AddMarkerTokenIfNecessary(); - if (!At(CSharpTokenType.RightBrace)) + using (var pooledResult = Pool.Allocate()) { - editHandler.AutoCompleteString = "}"; - } - Output(SpanKindInternal.Code); - - if (Optional(CSharpTokenType.RightBrace)) - { - // Set up the "}" span - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; - Span.ChunkGenerator = SpanChunkGenerator.Null; - } - - if (!IsNested) - { - EnsureCurrent(); - if (At(CSharpTokenType.NewLine) || - (At(CSharpTokenType.WhiteSpace) && NextIs(CSharpTokenType.NewLine))) + var expressionBuilder = pooledResult.Builder; + using (PushSpanContextConfig(ExplicitExpressionSpanContextConfig)) { - Context.NullGenerateWhitespaceAndNewLine = true; + var success = Balance( + expressionBuilder, + BalancingModes.BacktrackOnFailure | + BalancingModes.NoErrorOnFailure | + BalancingModes.AllowCommentsAndTemplates, + SyntaxKind.LeftParenthesis, + SyntaxKind.RightParenthesis, + block.Start); + + if (!success) + { + AcceptUntil(SyntaxKind.LessThan); + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_ExpectedEndOfBlockBeforeEOF( + new SourceSpan(block.Start, contentLength: 1 /* ( */), block.Name, ")", "(")); + } + + // If necessary, put an empty-content marker token here + AcceptMarkerTokenIfNecessary(); + expressionBuilder.Add(OutputTokensAsExpressionLiteral()); } + + var expressionBlock = SyntaxFactory.CSharpCodeBlock(expressionBuilder.ToList()); + + RazorMetaCodeSyntax rightParen = null; + if (At(SyntaxKind.RightParenthesis)) + { + rightParen = OutputAsMetaCode(EatCurrentToken()); + } + else + { + var missingToken = SyntaxFactory.MissingToken(SyntaxKind.RightParenthesis); + rightParen = OutputAsMetaCode(missingToken, SpanContext.EditHandler.AcceptedCharacters); + } + if (!EndOfFile) + { + PutCurrentBack(); + } + + return SyntaxFactory.CSharpExplicitExpressionBody(leftParen, expressionBlock, rightParen); + } + } + + private CSharpImplicitExpressionBodySyntax ParseImplicitExpressionBody(bool async = false) + { + var accepted = AcceptedCharactersInternal.NonWhitespace; + if (async) + { + // Async implicit expressions include the "await" keyword and therefore need to allow spaces to + // separate the "await" and the following code. + accepted = AcceptedCharactersInternal.AnyExceptNewline; } - Output(SpanKindInternal.MetaCode); - } - - private void ImplicitExpression() - { - ImplicitExpression(AcceptedCharactersInternal.NonWhiteSpace); - } - - // Async implicit expressions include the "await" keyword and therefore need to allow spaces to - // separate the "await" and the following code. - private void AsyncImplicitExpression() - { - ImplicitExpression(AcceptedCharactersInternal.AnyExceptNewline); - } - - private void ImplicitExpression(AcceptedCharactersInternal acceptedCharacters) - { - Context.Builder.CurrentBlock.Type = BlockKindInternal.Expression; - Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); - - using (PushSpanConfig(span => + using (var pooledResult = Pool.Allocate()) { - span.EditHandler = new ImplicitExpressionEditHandler( + var expressionBuilder = pooledResult.Builder; + ParseImplicitExpression(expressionBuilder, accepted); + var codeBlock = SyntaxFactory.CSharpCodeBlock(expressionBuilder.ToList()); + return SyntaxFactory.CSharpImplicitExpressionBody(codeBlock); + } + } + + private void ParseImplicitExpression(in SyntaxListBuilder builder, AcceptedCharactersInternal acceptedCharacters) + { + using (PushSpanContextConfig(spanContext => + { + spanContext.EditHandler = new ImplicitExpressionEditHandler( Language.TokenizeString, Keywords, acceptTrailingDot: IsNested); - span.EditHandler.AcceptedCharacters = acceptedCharacters; - span.ChunkGenerator = new ExpressionChunkGenerator(); + spanContext.EditHandler.AcceptedCharacters = acceptedCharacters; + spanContext.ChunkGenerator = new ExpressionChunkGenerator(); })) { do @@ -445,80 +345,80 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy AcceptAndMoveNext(); } } - while (MethodCallOrArrayIndex(acceptedCharacters)); + while (ParseMethodCallOrArrayIndex(builder, acceptedCharacters)); PutCurrentBack(); - Output(SpanKindInternal.Code); + builder.Add(OutputTokensAsExpressionLiteral()); } } - private bool MethodCallOrArrayIndex(AcceptedCharactersInternal acceptedCharacters) + private bool ParseMethodCallOrArrayIndex(in SyntaxListBuilder builder, AcceptedCharactersInternal acceptedCharacters) { if (!EndOfFile) { - if (CurrentToken.Type == CSharpTokenType.LeftParenthesis || - CurrentToken.Type == CSharpTokenType.LeftBracket) + if (CurrentToken.Kind == SyntaxKind.LeftParenthesis || + CurrentToken.Kind == SyntaxKind.LeftBracket) { // If we end within "(", whitespace is fine - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; - CSharpTokenType right; + SyntaxKind right; bool success; - using (PushSpanConfig((span, prev) => + using (PushSpanContextConfig((spanContext, prev) => { - prev(span); - span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; + prev(spanContext); + spanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; })) { - right = Language.FlipBracket(CurrentToken.Type); - success = Balance(BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates); + right = Language.FlipBracket(CurrentToken.Kind); + success = Balance(builder, BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates); } if (!success) { - AcceptUntil(CSharpTokenType.LessThan); + AcceptUntil(SyntaxKind.LessThan); } if (At(right)) { AcceptAndMoveNext(); // At the ending brace, restore the initial accepted characters. - Span.EditHandler.AcceptedCharacters = acceptedCharacters; + SpanContext.EditHandler.AcceptedCharacters = acceptedCharacters; } - return MethodCallOrArrayIndex(acceptedCharacters); + return ParseMethodCallOrArrayIndex(builder, acceptedCharacters); } - if (At(CSharpTokenType.QuestionMark)) + if (At(SyntaxKind.QuestionMark)) { var next = Lookahead(count: 1); if (next != null) { - if (next.Type == CSharpTokenType.Dot) + if (next.Kind == SyntaxKind.Dot) { // Accept null conditional dot operator (?.). AcceptAndMoveNext(); AcceptAndMoveNext(); // If the next piece after the ?. is a keyword or identifier then we want to continue. - return At(CSharpTokenType.Identifier) || At(CSharpTokenType.Keyword); + return At(SyntaxKind.Identifier) || At(SyntaxKind.Keyword); } - else if (next.Type == CSharpTokenType.LeftBracket) + else if (next.Kind == SyntaxKind.LeftBracket) { // We're at the ? for a null conditional bracket operator (?[). AcceptAndMoveNext(); // Accept the [ and any content inside (it will attempt to balance). - return MethodCallOrArrayIndex(acceptedCharacters); + return ParseMethodCallOrArrayIndex(builder, acceptedCharacters); } } } - else if (At(CSharpTokenType.Dot)) + else if (At(SyntaxKind.Dot)) { var dot = CurrentToken; if (NextToken()) { - if (At(CSharpTokenType.Identifier) || At(CSharpTokenType.Keyword)) + if (At(SyntaxKind.Identifier) || At(SyntaxKind.Keyword)) { // Accept the dot and return to the start Accept(dot); @@ -540,7 +440,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy Accept(dot); } } - else if (!At(CSharpTokenType.WhiteSpace) && !At(CSharpTokenType.NewLine)) + else if (!At(SyntaxKind.Whitespace) && !At(SyntaxKind.NewLine)) { PutCurrentBack(); } @@ -550,746 +450,107 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return false; } - protected void CompleteBlock() + private CSharpStatementBodySyntax ParseStatementBody(Block block = null) { - CompleteBlock(insertMarkerIfNecessary: true); - } - - protected void CompleteBlock(bool insertMarkerIfNecessary) - { - CompleteBlock(insertMarkerIfNecessary, captureWhitespaceToEndOfLine: insertMarkerIfNecessary); - } - - protected void CompleteBlock(bool insertMarkerIfNecessary, bool captureWhitespaceToEndOfLine) - { - if (insertMarkerIfNecessary && Context.Builder.LastAcceptedCharacters != AcceptedCharactersInternal.Any) + Assert(SyntaxKind.LeftBrace); + block = block ?? new Block(Resources.BlockName_Code, CurrentStart); + var leftBrace = OutputAsMetaCode(EatExpectedToken(SyntaxKind.LeftBrace)); + CSharpCodeBlockSyntax codeBlock = null; + using (var pooledResult = Pool.Allocate()) { - AddMarkerTokenIfNecessary(); - } - - EnsureCurrent(); - - // Read whitespace, but not newlines - // If we're not inserting a marker span, we don't need to capture whitespace - if (!Context.WhiteSpaceIsSignificantToAncestorBlock && - Context.Builder.CurrentBlock.Type != BlockKindInternal.Expression && - captureWhitespaceToEndOfLine && - !Context.DesignTimeMode && - !IsNested) - { - CaptureWhitespaceAtEndOfCodeOnlyLine(); - } - else - { - PutCurrentBack(); - } - } - - private void CaptureWhitespaceAtEndOfCodeOnlyLine() - { - var whitespace = ReadWhile(token => token.Type == CSharpTokenType.WhiteSpace); - if (At(CSharpTokenType.NewLine)) - { - Accept(whitespace); - AcceptAndMoveNext(); - PutCurrentBack(); - } - else - { - PutCurrentBack(); - PutBack(whitespace); - } - } - - private void ConfigureExplicitExpressionSpan(SpanBuilder sb) - { - sb.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString); - sb.ChunkGenerator = new ExpressionChunkGenerator(); - } - - private void ExplicitExpression() - { - var block = new Block(Resources.BlockName_ExplicitExpression, CurrentStart); - Assert(CSharpTokenType.LeftParenthesis); - AcceptAndMoveNext(); - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; - Span.ChunkGenerator = SpanChunkGenerator.Null; - Output(SpanKindInternal.MetaCode); - using (PushSpanConfig(ConfigureExplicitExpressionSpan)) - { - var success = Balance( - BalancingModes.BacktrackOnFailure | - BalancingModes.NoErrorOnFailure | - BalancingModes.AllowCommentsAndTemplates, - CSharpTokenType.LeftParenthesis, - CSharpTokenType.RightParenthesis, - block.Start); - - if (!success) - { - AcceptUntil(CSharpTokenType.LessThan); - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_ExpectedEndOfBlockBeforeEOF( - new SourceSpan(block.Start, contentLength: 1 /* ( */), block.Name, ")", "(")); - } - - // If necessary, put an empty-content marker token here - if (Span.Tokens.Count == 0) - { - Accept(new CSharpToken(string.Empty, CSharpTokenType.Unknown)); - } - - // Output the content span and then capture the ")" - Output(SpanKindInternal.Code); - } - Optional(CSharpTokenType.RightParenthesis); - if (!EndOfFile) - { - PutCurrentBack(); - } - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; - Span.ChunkGenerator = SpanChunkGenerator.Null; - CompleteBlock(insertMarkerIfNecessary: false); - Output(SpanKindInternal.MetaCode); - } - - private void Template() - { - if (Context.Builder.ActiveBlocks.Any(block => block.Type == BlockKindInternal.Template)) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_InlineMarkupBlocksCannotBeNested( - new SourceSpan(CurrentStart, contentLength: 1 /* @ */))); - } - Output(SpanKindInternal.Code); - using (Context.Builder.StartBlock(BlockKindInternal.Template)) - { - Context.Builder.CurrentBlock.ChunkGenerator = new TemplateBlockChunkGenerator(); - PutCurrentBack(); - OtherParserBlock(); - } - } - - private void OtherParserBlock() - { - ParseWithOtherParser(p => p.ParseBlock()); - } - - private void SectionBlock(string left, string right, bool caseSensitive) - { - ParseWithOtherParser(p => p.ParseRazorBlock(Tuple.Create(left, right), caseSensitive)); - } - - private void NestedBlock() - { - Output(SpanKindInternal.Code); - - var wasNested = IsNested; - IsNested = true; - using (PushSpanConfig()) - { - ParseBlock(); - } - - Span.Start = CurrentLocation; - Initialize(Span); - IsNested = wasNested; - NextToken(); - } - - protected override bool IsAtEmbeddedTransition(bool allowTemplatesAndComments, bool allowTransitions) - { - // No embedded transitions in C#, so ignore that param - return allowTemplatesAndComments - && ((Language.IsTransition(CurrentToken) - && NextIs(CSharpTokenType.LessThan, CSharpTokenType.Colon, CSharpTokenType.DoubleColon)) - || Language.IsCommentStart(CurrentToken)); - } - - protected override void HandleEmbeddedTransition() - { - if (Language.IsTransition(CurrentToken)) - { - PutCurrentBack(); - Template(); - } - else if (Language.IsCommentStart(CurrentToken)) - { - RazorComment(); - } - } - - private void ParseWithOtherParser(Action parseAction) - { - // When transitioning to the HTML parser we no longer want to act as if we're in a nested C# state. - // For instance, if
@hello.
is in a nested C# block we don't want the trailing '.' to be handled - // as C#; it should be handled as a period because it's wrapped in markup. - var wasNested = IsNested; - IsNested = false; - - using (PushSpanConfig()) - { - parseAction(HtmlParser); - } - - Span.Start = CurrentLocation; - Initialize(Span); - - IsNested = wasNested; - - NextToken(); - } - - private void SetUpKeywords() - { - MapKeywords( - ConditionalBlock, - CSharpKeyword.For, - CSharpKeyword.Foreach, - CSharpKeyword.While, - CSharpKeyword.Switch, - CSharpKeyword.Lock); - MapKeywords(CaseStatement, false, CSharpKeyword.Case, CSharpKeyword.Default); - MapKeywords(IfStatement, CSharpKeyword.If); - MapKeywords(TryStatement, CSharpKeyword.Try); - MapKeywords(UsingKeyword, CSharpKeyword.Using); - MapKeywords(DoStatement, CSharpKeyword.Do); - MapKeywords(ReservedDirective, CSharpKeyword.Class, CSharpKeyword.Namespace); - } - - protected virtual void ReservedDirective(bool topLevel) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_ReservedWord( - new SourceSpan(CurrentStart, CurrentToken.Content.Length), CurrentToken.Content)); - - AcceptAndMoveNext(); - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; - Span.ChunkGenerator = SpanChunkGenerator.Null; - Context.Builder.CurrentBlock.Type = BlockKindInternal.Directive; - CompleteBlock(); - Output(SpanKindInternal.MetaCode); - } - - private void KeywordBlock(bool topLevel) - { - HandleKeyword(topLevel, () => - { - Context.Builder.CurrentBlock.Type = BlockKindInternal.Expression; - Context.Builder.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); - ImplicitExpression(); - }); - } - - private void CaseStatement(bool topLevel) - { - Assert(CSharpTokenType.Keyword); - Debug.Assert(CurrentToken.Keyword != null && - (CurrentToken.Keyword.Value == CSharpKeyword.Case || - CurrentToken.Keyword.Value == CSharpKeyword.Default)); - AcceptUntil(CSharpTokenType.Colon); - Optional(CSharpTokenType.Colon); - } - - private void DoStatement(bool topLevel) - { - Assert(CSharpKeyword.Do); - UnconditionalBlock(); - WhileClause(); - if (topLevel) - { - CompleteBlock(); - } - } - - private void WhileClause() - { - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; - var whitespace = SkipToNextImportantToken(); - - if (At(CSharpKeyword.While)) - { - Accept(whitespace); - Assert(CSharpKeyword.While); - AcceptAndMoveNext(); - AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); - if (AcceptCondition() && Optional(CSharpTokenType.Semicolon)) - { - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; - } - } - else - { - PutCurrentBack(); - PutBack(whitespace); - } - } - - private void UsingKeyword(bool topLevel) - { - Assert(CSharpKeyword.Using); - var block = new Block(CurrentToken, CurrentStart); - AcceptAndMoveNext(); - AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); - - if (At(CSharpTokenType.LeftParenthesis)) - { - // using ( ==> Using Statement - UsingStatement(block); - } - else if (At(CSharpTokenType.Identifier) || At(CSharpKeyword.Static)) - { - // using Identifier ==> Using Declaration - if (!topLevel) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_NamespaceImportAndTypeAliasCannotExistWithinCodeBlock( - new SourceSpan(block.Start, block.Name.Length))); - StandardStatement(); - } - else - { - UsingDeclaration(); - } - } - - if (topLevel) - { - CompleteBlock(); - } - } - - private void UsingDeclaration() - { - // Set block type to directive - Context.Builder.CurrentBlock.Type = BlockKindInternal.Directive; - - var start = CurrentStart; - if (At(CSharpTokenType.Identifier)) - { - // non-static using - NamespaceOrTypeName(); - var whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); - if (At(CSharpTokenType.Assign)) - { - // Alias - Accept(whitespace); - Assert(CSharpTokenType.Assign); - AcceptAndMoveNext(); - - AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); - - // One more namespace or type name - NamespaceOrTypeName(); - } - else - { - PutCurrentBack(); - PutBack(whitespace); - } - } - else if (At(CSharpKeyword.Static)) - { - // static using - AcceptAndMoveNext(); - AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); - NamespaceOrTypeName(); - } - - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.AnyExceptNewline; - Span.ChunkGenerator = new AddImportChunkGenerator(new LocationTagged( - string.Concat(Span.Tokens.Skip(1).Select(s => s.Content)), - start)); - - // Optional ";" - if (EnsureCurrent()) - { - Optional(CSharpTokenType.Semicolon); - } - } - - // Used for parsing a qualified name like that which follows the `namespace` keyword. - // - // qualified-identifier: - // identifier - // qualified-identifier . identifier - protected bool QualifiedIdentifier(out int identifierLength) - { - var currentIdentifierLength = 0; - var expectingDot = false; - var tokens = ReadWhile(token => - { - var type = token.Type; - if ((expectingDot && type == CSharpTokenType.Dot) || - (!expectingDot && type == CSharpTokenType.Identifier)) - { - expectingDot = !expectingDot; - return true; - } - - if (type != CSharpTokenType.WhiteSpace && - type != CSharpTokenType.NewLine) - { - expectingDot = false; - currentIdentifierLength += token.Content.Length; - } - - return false; - }); - - identifierLength = currentIdentifierLength; - var validQualifiedIdentifier = expectingDot; - if (validQualifiedIdentifier) - { - foreach (var token in tokens) - { - identifierLength += token.Content.Length; - Accept(token); - } - - return true; - } - else - { - PutCurrentBack(); - - foreach (var token in tokens) - { - identifierLength += token.Content.Length; - PutBack(token); - } + var builder = pooledResult.Builder; + // Set up auto-complete and parse the code block + var editHandler = new AutoCompleteEditHandler(Language.TokenizeString); + SpanContext.EditHandler = editHandler; + ParseCodeBlock(builder, block, acceptTerminatingBrace: false); EnsureCurrent(); - return false; + SpanContext.ChunkGenerator = new StatementChunkGenerator(); + AcceptMarkerTokenIfNecessary(); + if (!At(SyntaxKind.RightBrace)) + { + editHandler.AutoCompleteString = "}"; + } + builder.Add(OutputTokensAsStatementLiteral()); + + codeBlock = SyntaxFactory.CSharpCodeBlock(builder.ToList()); } - } - protected bool NamespaceOrTypeName() - { - if (Optional(CSharpTokenType.LeftParenthesis)) + RazorMetaCodeSyntax rightBrace = null; + if (At(SyntaxKind.RightBrace)) { - while (!Optional(CSharpTokenType.RightParenthesis) && !EndOfFile) - { - Optional(CSharpTokenType.WhiteSpace); - - if (!NamespaceOrTypeName()) - { - return false; - } - - Optional(CSharpTokenType.WhiteSpace); - Optional(CSharpTokenType.Identifier); - Optional(CSharpTokenType.WhiteSpace); - Optional(CSharpTokenType.Comma); - } - - if (At(CSharpTokenType.WhiteSpace) && NextIs(CSharpTokenType.QuestionMark)) - { - // Only accept the whitespace if we are going to consume the next token. - AcceptAndMoveNext(); - } - - Optional(CSharpTokenType.QuestionMark); // Nullable - - return true; - } - else if (Optional(CSharpTokenType.Identifier) || Optional(CSharpTokenType.Keyword)) - { - if (Optional(CSharpTokenType.DoubleColon)) - { - if (!Optional(CSharpTokenType.Identifier)) - { - Optional(CSharpTokenType.Keyword); - } - } - if (At(CSharpTokenType.LessThan)) - { - TypeArgumentList(); - } - if (Optional(CSharpTokenType.Dot)) - { - NamespaceOrTypeName(); - } - - if (At(CSharpTokenType.WhiteSpace) && NextIs(CSharpTokenType.QuestionMark)) - { - // Only accept the whitespace if we are going to consume the next token. - AcceptAndMoveNext(); - } - - Optional(CSharpTokenType.QuestionMark); // Nullable - - if (At(CSharpTokenType.WhiteSpace) && NextIs(CSharpTokenType.LeftBracket)) - { - // Only accept the whitespace if we are going to consume the next token. - AcceptAndMoveNext(); - } - - while (At(CSharpTokenType.LeftBracket)) - { - Balance(BalancingModes.None); - Optional(CSharpTokenType.RightBracket); - } - return true; + rightBrace = OutputAsMetaCode(EatCurrentToken()); } else { - return false; + rightBrace = OutputAsMetaCode( + SyntaxFactory.MissingToken(SyntaxKind.RightBrace), + SpanContext.EditHandler.AcceptedCharacters); } - } - private void TypeArgumentList() - { - Assert(CSharpTokenType.LessThan); - Balance(BalancingModes.None); - Optional(CSharpTokenType.GreaterThan); - } - - private void UsingStatement(Block block) - { - Assert(CSharpTokenType.LeftParenthesis); - - // Parse condition - if (AcceptCondition()) + if (!IsNested) { - AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); - - // Parse code block - ExpectCodeBlock(block); - } - } - - private void TryStatement(bool topLevel) - { - Assert(CSharpKeyword.Try); - UnconditionalBlock(); - AfterTryClause(); - if (topLevel) - { - CompleteBlock(); - } - } - - private void IfStatement(bool topLevel) - { - Assert(CSharpKeyword.If); - ConditionalBlock(topLevel: false); - AfterIfClause(); - if (topLevel) - { - CompleteBlock(); - } - } - - private void AfterTryClause() - { - // Grab whitespace - var whitespace = SkipToNextImportantToken(); - - // Check for a catch or finally part - if (At(CSharpKeyword.Catch)) - { - Accept(whitespace); - Assert(CSharpKeyword.Catch); - FilterableCatchBlock(); - AfterTryClause(); - } - else if (At(CSharpKeyword.Finally)) - { - Accept(whitespace); - Assert(CSharpKeyword.Finally); - UnconditionalBlock(); - } - else - { - // Return whitespace and end the block - PutCurrentBack(); - PutBack(whitespace); - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; - } - } - - private void AfterIfClause() - { - // Grab whitespace and razor comments - var whitespace = SkipToNextImportantToken(); - - // Check for an else part - if (At(CSharpKeyword.Else)) - { - Accept(whitespace); - Assert(CSharpKeyword.Else); - ElseClause(); - } - else - { - // No else, return whitespace - PutCurrentBack(); - PutBack(whitespace); - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; - } - } - - private void ElseClause() - { - if (!At(CSharpKeyword.Else)) - { - return; - } - var block = new Block(CurrentToken, CurrentStart); - - AcceptAndMoveNext(); - AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); - if (At(CSharpKeyword.If)) - { - // ElseIf - block.Name = SyntaxConstants.CSharp.ElseIfKeyword; - ConditionalBlock(block); - AfterIfClause(); - } - else if (!EndOfFile) - { - // Else - ExpectCodeBlock(block); - } - } - - private void ExpectCodeBlock(Block block) - { - if (!EndOfFile) - { - // Check for "{" to make sure we're at a block - if (!At(CSharpTokenType.LeftBrace)) + EnsureCurrent(); + if (At(SyntaxKind.NewLine) || + (At(SyntaxKind.Whitespace) && NextIs(SyntaxKind.NewLine))) { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_SingleLineControlFlowStatementsNotAllowed( - new SourceSpan(CurrentStart, CurrentToken.Content.Length), - Language.GetSample(CSharpTokenType.LeftBrace), - CurrentToken.Content)); + Context.NullGenerateWhitespaceAndNewLine = true; } - - // Parse the statement and then we're done - Statement(block); } + + return SyntaxFactory.CSharpStatementBody(leftBrace, codeBlock, rightBrace); } - private void UnconditionalBlock() + private void ParseCodeBlock(in SyntaxListBuilder builder, Block block, bool acceptTerminatingBrace = true) { - Assert(CSharpTokenType.Keyword); - var block = new Block(CurrentToken, CurrentStart); - AcceptAndMoveNext(); - AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); - ExpectCodeBlock(block); - } - - private void FilterableCatchBlock() - { - Assert(CSharpKeyword.Catch); - - var block = new Block(CurrentToken, CurrentStart); - - // Accept "catch" - AcceptAndMoveNext(); - AcceptWhile(IsValidStatementSpacingToken); - - // Parse the catch condition if present. If not present, let the C# compiler complain. - if (AcceptCondition()) + EnsureCurrent(); + while (!EndOfFile && !At(SyntaxKind.RightBrace)) { - AcceptWhile(IsValidStatementSpacingToken); - - if (At(CSharpKeyword.When)) - { - // Accept "when". - AcceptAndMoveNext(); - AcceptWhile(IsValidStatementSpacingToken); - - // Parse the filter condition if present. If not present, let the C# compiler complain. - if (!AcceptCondition()) - { - // Incomplete condition. - return; - } - - AcceptWhile(IsValidStatementSpacingToken); - } - - ExpectCodeBlock(block); + // Parse a statement, then return here + ParseStatement(builder, block: block); + EnsureCurrent(); } - } - private void ConditionalBlock(bool topLevel) - { - Assert(CSharpTokenType.Keyword); - var block = new Block(CurrentToken, CurrentStart); - ConditionalBlock(block); - if (topLevel) + if (EndOfFile) { - CompleteBlock(); + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_ExpectedEndOfBlockBeforeEOF( + new SourceSpan(block.Start, contentLength: 1 /* { OR } */), block.Name, "}", "{")); } - } - - private void ConditionalBlock(Block block) - { - AcceptAndMoveNext(); - AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); - - // Parse the condition, if present (if not present, we'll let the C# compiler complain) - if (AcceptCondition()) + else if (acceptTerminatingBrace) { - AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); - ExpectCodeBlock(block); + Assert(SyntaxKind.RightBrace); + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; + AcceptAndMoveNext(); } } - private bool AcceptCondition() + private void ParseStatement(in SyntaxListBuilder builder, Block block) { - if (At(CSharpTokenType.LeftParenthesis)) - { - var complete = Balance(BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates); - if (!complete) - { - AcceptUntil(CSharpTokenType.NewLine); - } - else - { - Optional(CSharpTokenType.RightParenthesis); - } - return complete; - } - return true; - } - - private void Statement() - { - Statement(null); - } - - private void Statement(Block block) - { - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; - + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; // Accept whitespace but always keep the last whitespace node so we can put it back if necessary - var lastWhitespace = AcceptWhiteSpaceInLines(); - + var lastWhitespace = AcceptWhitespaceInLines(); if (EndOfFile) { if (lastWhitespace != null) { Accept(lastWhitespace); } + + builder.Add(OutputTokensAsStatementLiteral()); return; } - var type = CurrentToken.Type; - var loc = CurrentStart; + var kind = CurrentToken.Kind; + var location = CurrentStart; // Both cases @: and @:: are triggered as markup, second colon in second case will be triggered as a plain text - var isSingleLineMarkup = type == CSharpTokenType.Transition && - (NextIs(CSharpTokenType.Colon, CSharpTokenType.DoubleColon)); + var isSingleLineMarkup = kind == SyntaxKind.Transition && + (NextIs(SyntaxKind.Colon, SyntaxKind.DoubleColon)); var isMarkup = isSingleLineMarkup || - type == CSharpTokenType.LessThan || - (type == CSharpTokenType.Transition && NextIs(CSharpTokenType.LessThan)); + kind == SyntaxKind.LessThan || + (kind == SyntaxKind.Transition && NextIs(SyntaxKind.LessThan)); if (Context.DesignTimeMode || !isMarkup) { @@ -1321,90 +582,89 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy if (isMarkup) { - if (type == CSharpTokenType.Transition && !isSingleLineMarkup) + if (kind == SyntaxKind.Transition && !isSingleLineMarkup) { Context.ErrorSink.OnError( RazorDiagnosticFactory.CreateParsing_AtInCodeMustBeFollowedByColonParenOrIdentifierStart( - new SourceSpan(loc, contentLength: 1 /* @ */))); + new SourceSpan(location, contentLength: 1 /* @ */))); } // Markup block - Output(SpanKindInternal.Code); + builder.Add(OutputTokensAsStatementLiteral()); if (Context.DesignTimeMode && CurrentToken != null && - (CurrentToken.Type == CSharpTokenType.LessThan || CurrentToken.Type == CSharpTokenType.Transition)) + (CurrentToken.Kind == SyntaxKind.LessThan || CurrentToken.Kind == SyntaxKind.Transition)) { PutCurrentBack(); } - OtherParserBlock(); + OtherParserBlock(builder); } else { // What kind of statement is this? - HandleStatement(block, type); + switch (kind) + { + case SyntaxKind.RazorCommentTransition: + AcceptMarkerTokenIfNecessary(); + builder.Add(OutputTokensAsStatementLiteral()); + var comment = ParseRazorComment(); + builder.Add(comment); + ParseStatement(builder, block); + break; + case SyntaxKind.LeftBrace: + // Verbatim Block + AcceptAndMoveNext(); + ParseCodeBlock(builder, block); + break; + case SyntaxKind.Keyword: + if (!TryParseKeyword(builder, transition: null)) + { + ParseStandardStatement(builder); + } + break; + case SyntaxKind.Transition: + // Embedded Expression block + ParseEmbeddedExpression(builder); + break; + case SyntaxKind.RightBrace: + // Possible end of Code Block, just run the continuation + break; + case SyntaxKind.CSharpComment: + Accept(CurrentToken); + NextToken(); + break; + default: + // Other statement + ParseStandardStatement(builder); + break; + } } } - private void HandleStatement(Block block, CSharpTokenType type) - { - switch (type) - { - case CSharpTokenType.RazorCommentTransition: - Output(SpanKindInternal.Code); - RazorComment(); - Statement(block); - break; - case CSharpTokenType.LeftBrace: - // Verbatim Block - block = block ?? new Block(Resources.BlockName_Code, CurrentStart); - AcceptAndMoveNext(); - CodeBlock(block); - break; - case CSharpTokenType.Keyword: - // Keyword block - HandleKeyword(false, StandardStatement); - break; - case CSharpTokenType.Transition: - // Embedded Expression block - EmbeddedExpression(); - break; - case CSharpTokenType.RightBrace: - // Possible end of Code Block, just run the continuation - break; - case CSharpTokenType.Comment: - AcceptAndMoveNext(); - break; - default: - // Other statement - StandardStatement(); - break; - } - } - - private void EmbeddedExpression() + private void ParseEmbeddedExpression(in SyntaxListBuilder builder) { // First, verify the type of the block - Assert(CSharpTokenType.Transition); + Assert(SyntaxKind.Transition); var transition = CurrentToken; NextToken(); - if (At(CSharpTokenType.Transition)) + if (At(SyntaxKind.Transition)) { // Escaped "@" - Output(SpanKindInternal.Code); + builder.Add(OutputTokensAsStatementLiteral()); // Output "@" as hidden span Accept(transition); - Span.ChunkGenerator = SpanChunkGenerator.Null; - Output(SpanKindInternal.Code); + SpanContext.ChunkGenerator = SpanChunkGenerator.Null; + builder.Add(OutputTokensAsEphemeralLiteral()); - Assert(CSharpTokenType.Transition); + Assert(SyntaxKind.Transition); AcceptAndMoveNext(); - StandardStatement(); + ParseStandardStatement(builder); } else { // Throw errors as necessary, but continue parsing - if (At(CSharpTokenType.LeftBrace)) + if (At(SyntaxKind.LeftBrace)) { Context.ErrorSink.OnError( RazorDiagnosticFactory.CreateParsing_UnexpectedNestedCodeBlock( @@ -1416,60 +676,82 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy PutBack(transition); // Before exiting, add a marker span if necessary - AddMarkerTokenIfNecessary(); + AcceptMarkerTokenIfNecessary(); + builder.Add(OutputTokensAsStatementLiteral()); - NestedBlock(); + var nestedBlock = ParseNestedBlock(); + builder.Add(nestedBlock); } } - private void StandardStatement() + private RazorSyntaxNode ParseNestedBlock() + { + var wasNested = IsNested; + IsNested = true; + + RazorSyntaxNode nestedBlock; + using (PushSpanContextConfig()) + { + nestedBlock = ParseBlock(); + } + + InitializeContext(SpanContext); + IsNested = wasNested; + NextToken(); + + return nestedBlock; + } + + private void ParseStandardStatement(in SyntaxListBuilder builder) { while (!EndOfFile) { var bookmark = CurrentStart.AbsoluteIndex; var read = ReadWhile(token => - token.Type != CSharpTokenType.Semicolon && - token.Type != CSharpTokenType.RazorCommentTransition && - token.Type != CSharpTokenType.Transition && - token.Type != CSharpTokenType.LeftBrace && - token.Type != CSharpTokenType.LeftParenthesis && - token.Type != CSharpTokenType.LeftBracket && - token.Type != CSharpTokenType.RightBrace); + token.Kind != SyntaxKind.Semicolon && + token.Kind != SyntaxKind.RazorCommentTransition && + token.Kind != SyntaxKind.Transition && + token.Kind != SyntaxKind.LeftBrace && + token.Kind != SyntaxKind.LeftParenthesis && + token.Kind != SyntaxKind.LeftBracket && + token.Kind != SyntaxKind.RightBrace); - if (At(CSharpTokenType.LeftBrace) || - At(CSharpTokenType.LeftParenthesis) || - At(CSharpTokenType.LeftBracket)) + if (At(SyntaxKind.LeftBrace) || + At(SyntaxKind.LeftParenthesis) || + At(SyntaxKind.LeftBracket)) { Accept(read); - if (Balance(BalancingModes.AllowCommentsAndTemplates | BalancingModes.BacktrackOnFailure)) + if (Balance(builder, BalancingModes.AllowCommentsAndTemplates | BalancingModes.BacktrackOnFailure)) { - Optional(CSharpTokenType.RightBrace); + TryAccept(SyntaxKind.RightBrace); } else { // Recovery - AcceptUntil(CSharpTokenType.LessThan, CSharpTokenType.RightBrace); + AcceptUntil(SyntaxKind.LessThan, SyntaxKind.RightBrace); return; } } - else if (At(CSharpTokenType.Transition) && (NextIs(CSharpTokenType.LessThan, CSharpTokenType.Colon))) + else if (At(SyntaxKind.Transition) && (NextIs(SyntaxKind.LessThan, SyntaxKind.Colon))) { Accept(read); - Output(SpanKindInternal.Code); - Template(); + builder.Add(OutputTokensAsStatementLiteral()); + ParseTemplate(builder); } - else if (At(CSharpTokenType.RazorCommentTransition)) + else if (At(SyntaxKind.RazorCommentTransition)) { Accept(read); - RazorComment(); + AcceptMarkerTokenIfNecessary(); + builder.Add(OutputTokensAsStatementLiteral()); + builder.Add(ParseRazorComment()); } - else if (At(CSharpTokenType.Semicolon)) + else if (At(SyntaxKind.Semicolon)) { Accept(read); AcceptAndMoveNext(); return; } - else if (At(CSharpTokenType.RightBrace)) + else if (At(SyntaxKind.RightBrace)) { Accept(read); return; @@ -1478,110 +760,56 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { Context.Source.Position = bookmark; NextToken(); - AcceptUntil(CSharpTokenType.LessThan, CSharpTokenType.LeftBrace, CSharpTokenType.RightBrace); + AcceptUntil(SyntaxKind.LessThan, SyntaxKind.LeftBrace, SyntaxKind.RightBrace); return; } } } - private void CodeBlock(Block block) + private void ParseTemplate(in SyntaxListBuilder builder) { - CodeBlock(true, block); - } - - private void CodeBlock(bool acceptTerminatingBrace, Block block) - { - EnsureCurrent(); - while (!EndOfFile && !At(CSharpTokenType.RightBrace)) - { - // Parse a statement, then return here - Statement(); - EnsureCurrent(); - } - - if (EndOfFile) + if (Context.InTemplateContext) { Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_ExpectedEndOfBlockBeforeEOF( - new SourceSpan(block.Start, contentLength: 1 /* { OR } */), block.Name, "}", "{")); + RazorDiagnosticFactory.CreateParsing_InlineMarkupBlocksCannotBeNested( + new SourceSpan(CurrentStart, contentLength: 1 /* @ */))); } - else if (acceptTerminatingBrace) + if (SpanContext.ChunkGenerator is ExpressionChunkGenerator) { - Assert(CSharpTokenType.RightBrace); - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; - AcceptAndMoveNext(); - } - } - - private void HandleKeyword(bool topLevel, Action fallback) - { - Debug.Assert(CurrentToken.Type == CSharpTokenType.Keyword && CurrentToken.Keyword != null); - if (_keywordParsers.TryGetValue(CurrentToken.Keyword.Value, out var handler)) - { - handler(topLevel); + builder.Add(OutputTokensAsExpressionLiteral()); } else { - fallback(); + builder.Add(OutputTokensAsStatementLiteral()); } - } - private IEnumerable SkipToNextImportantToken() - { - while (!EndOfFile) + using (var pooledResult = Pool.Allocate()) { - var whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); - if (At(CSharpTokenType.RazorCommentTransition)) - { - Accept(whitespace); - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; - RazorComment(); - } - else - { - return whitespace; - } + var templateBuilder = pooledResult.Builder; + Context.InTemplateContext = true; + PutCurrentBack(); + OtherParserBlock(templateBuilder); + + var template = SyntaxFactory.CSharpTemplateBlock(templateBuilder.ToList()); + builder.Add(template); + + Context.InTemplateContext = false; } - return Enumerable.Empty(); } - // Common code for Parsers, but FxCop REALLY doesn't like it in the base class.. moving it here for now. - protected override void OutputSpanBeforeRazorComment() + protected bool TryParseDirective(in SyntaxListBuilder builder, CSharpTransitionSyntax transition, string directive) { - AddMarkerTokenIfNecessary(); - Output(SpanKindInternal.Code); - } - - private void SetUpExpressions() - { - MapExpressionKeyword(AwaitExpression, CSharpKeyword.Await); - } - - private void AwaitExpression(bool topLevel) - { - // Ensure that we're on the await statement (only runs in debug) - Assert(CSharpKeyword.Await); - - // Accept the "await" and move on - AcceptAndMoveNext(); - - // Accept 1 or more spaces between the await and the following code. - AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); - - // Top level basically indicates if we're within an expression or statement. - // Ex: topLevel true = @await Foo() | topLevel false = @{ await Foo(); } - // Note that in this case @{ @await Foo() } top level is true for await. - // Therefore, if we're top level then we want to act like an implicit expression, - // otherwise just act as whatever we're contained in. - if (topLevel) + if (_directiveParserMap.TryGetValue(directive, out var handler)) { - // Setup the Span to be an async implicit expression (an implicit expresison that allows spaces). - // Spaces are allowed because of "@await Foo()". - AsyncImplicitExpression(); + SpanContext.ChunkGenerator = SpanChunkGenerator.Null; + handler(builder, transition); + return true; } + + return false; } - private void SetupDirectives(IEnumerable directiveDescriptors) + private void SetupDirectiveParsers(IEnumerable directiveDescriptors) { var allDirectives = directiveDescriptors.Concat(DefaultDirectiveDescriptors).ToList(); @@ -1589,12 +817,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { var directiveDescriptor = allDirectives[i]; CurrentKeywords.Add(directiveDescriptor.Directive); - MapDirectives(() => HandleDirective(directiveDescriptor), directiveDescriptor.Directive); + MapDirectives((builder, transition) => ParseExtensibleDirective(builder, transition, directiveDescriptor), directiveDescriptor.Directive); } - MapDirectives(TagHelperPrefixDirective, SyntaxConstants.CSharp.TagHelperPrefixKeyword); - MapDirectives(AddTagHelperDirective, SyntaxConstants.CSharp.AddTagHelperKeyword); - MapDirectives(RemoveTagHelperDirective, SyntaxConstants.CSharp.RemoveTagHelperKeyword); + MapDirectives(ParseTagHelperPrefixDirective, SyntaxConstants.CSharp.TagHelperPrefixKeyword); + MapDirectives(ParseAddTagHelperDirective, SyntaxConstants.CSharp.AddTagHelperKeyword); + MapDirectives(ParseRemoveTagHelperDirective, SyntaxConstants.CSharp.RemoveTagHelperKeyword); } private void EnsureDirectiveIsAtStartOfLine() @@ -1620,340 +848,208 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } } - private void HandleDirective(DirectiveDescriptor descriptor) + protected void MapDirectives(Action, CSharpTransitionSyntax> handler, params string[] directives) { - AssertDirective(descriptor.Directive); - - var directiveErrorSink = new ErrorSink(); - var savedErrorSink = Context.ErrorSink; - Context.ErrorSink = directiveErrorSink; - - var directiveChunkGenerator = new DirectiveChunkGenerator(descriptor); - try + foreach (var directive in directives) { - EnsureDirectiveIsAtStartOfLine(); - - Context.Builder.CurrentBlock.Type = BlockKindInternal.Directive; - Context.Builder.CurrentBlock.ChunkGenerator = directiveChunkGenerator; - - AcceptAndMoveNext(); - Output(SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); - - // Even if an error was logged do not bail out early. If a directive was used incorrectly it doesn't mean it can't be parsed. - ValidateDirectiveUsage(descriptor); - - for (var i = 0; i < descriptor.Tokens.Count; i++) + _directiveParserMap.Add(directive, (builder, transition) => { - if (!At(CSharpTokenType.WhiteSpace) && - !At(CSharpTokenType.NewLine) && - !EndOfFile) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_DirectiveTokensMustBeSeparatedByWhitespace( - new SourceSpan(CurrentStart, CurrentToken.Content.Length), descriptor.Directive)); - return; - } + handler(builder, transition); + Context.SeenDirectives.Add(directive); + }); - var tokenDescriptor = descriptor.Tokens[i]; - AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + Keywords.Add(directive); - if (tokenDescriptor.Kind == DirectiveTokenKind.Member || - tokenDescriptor.Kind == DirectiveTokenKind.Namespace || - tokenDescriptor.Kind == DirectiveTokenKind.Type) - { - Span.ChunkGenerator = SpanChunkGenerator.Null; - Output(SpanKindInternal.Code, AcceptedCharactersInternal.WhiteSpace); - - if (EndOfFile || At(CSharpTokenType.NewLine)) - { - // Add a marker token to provide CSharp intellisense when we start typing the directive token. - AddMarkerTokenIfNecessary(); - Span.ChunkGenerator = new DirectiveTokenChunkGenerator(tokenDescriptor); - Span.EditHandler = new DirectiveTokenEditHandler(Language.TokenizeString); - Output(SpanKindInternal.Code, AcceptedCharactersInternal.NonWhiteSpace); - } - } - else - { - Span.ChunkGenerator = SpanChunkGenerator.Null; - Output(SpanKindInternal.Markup, AcceptedCharactersInternal.WhiteSpace); - } - - if (tokenDescriptor.Optional && (EndOfFile || At(CSharpTokenType.NewLine))) - { - break; - } - else if (EndOfFile) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_UnexpectedEOFAfterDirective( - new SourceSpan(CurrentStart, contentLength: 1), - descriptor.Directive, - tokenDescriptor.Kind.ToString().ToLowerInvariant())); - return; - } - - switch (tokenDescriptor.Kind) - { - case DirectiveTokenKind.Type: - if (!NamespaceOrTypeName()) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_DirectiveExpectsTypeName( - new SourceSpan(CurrentStart, CurrentToken.Content.Length), descriptor.Directive)); - - return; - } - break; - - case DirectiveTokenKind.Namespace: - if (!QualifiedIdentifier(out var identifierLength)) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_DirectiveExpectsNamespace( - new SourceSpan(CurrentStart, identifierLength), descriptor.Directive)); - - return; - } - break; - - case DirectiveTokenKind.Member: - if (At(CSharpTokenType.Identifier)) - { - AcceptAndMoveNext(); - } - else - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_DirectiveExpectsIdentifier( - new SourceSpan(CurrentStart, CurrentToken.Content.Length), descriptor.Directive)); - return; - } - break; - - case DirectiveTokenKind.String: - if (At(CSharpTokenType.StringLiteral) && CurrentToken.Errors.Count == 0) - { - AcceptAndMoveNext(); - } - else - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_DirectiveExpectsQuotedStringLiteral( - new SourceSpan(CurrentStart, CurrentToken.Content.Length), descriptor.Directive)); - return; - } - break; - } - - Span.ChunkGenerator = new DirectiveTokenChunkGenerator(tokenDescriptor); - Span.EditHandler = new DirectiveTokenEditHandler(Language.TokenizeString); - Output(SpanKindInternal.Code, AcceptedCharactersInternal.NonWhiteSpace); + // These C# keywords are reserved for use in directives. It's an error to use them outside of + // a directive. This code removes the error generation if the directive *is* registered. + if (string.Equals(directive, "class", StringComparison.OrdinalIgnoreCase)) + { + _keywordParserMap.Remove(CSharpKeyword.Class); } - - AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); - Span.ChunkGenerator = SpanChunkGenerator.Null; - - switch (descriptor.Kind) + else if (string.Equals(directive, "namespace", StringComparison.OrdinalIgnoreCase)) { - case DirectiveKind.SingleLine: - Output(SpanKindInternal.None, AcceptedCharactersInternal.WhiteSpace); - - Optional(CSharpTokenType.Semicolon); - Span.ChunkGenerator = SpanChunkGenerator.Null; - Output(SpanKindInternal.MetaCode, AcceptedCharactersInternal.WhiteSpace); - - AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); - - if (At(CSharpTokenType.NewLine)) - { - AcceptAndMoveNext(); - } - else if (!EndOfFile) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_UnexpectedDirectiveLiteral( - new SourceSpan(CurrentStart, CurrentToken.Content.Length), - descriptor.Directive, - Resources.ErrorComponent_Newline)); - } - - Span.ChunkGenerator = SpanChunkGenerator.Null; - - // This should contain the optional whitespace after the optional semicolon and the new line. - // Output as Markup as we want intellisense here. - Output(SpanKindInternal.Markup, AcceptedCharactersInternal.WhiteSpace); - break; - case DirectiveKind.RazorBlock: - AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); - Output(SpanKindInternal.Markup, AcceptedCharactersInternal.AllWhiteSpace); - - ParseDirectiveBlock(descriptor, parseChildren: (startingBraceLocation) => - { - // When transitioning to the HTML parser we no longer want to act as if we're in a nested C# state. - // For instance, if
@hello.
is in a nested C# block we don't want the trailing '.' to be handled - // as C#; it should be handled as a period because it's wrapped in markup. - var wasNested = IsNested; - IsNested = false; - - using (PushSpanConfig()) - { - HtmlParser.ParseRazorBlock(Tuple.Create("{", "}"), caseSensitive: true); - } - - Span.Start = CurrentLocation; - Initialize(Span); - - IsNested = wasNested; - - NextToken(); - }); - break; - case DirectiveKind.CodeBlock: - AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); - Output(SpanKindInternal.Markup, AcceptedCharactersInternal.AllWhiteSpace); - - ParseDirectiveBlock(descriptor, parseChildren: (startingBraceLocation) => - { - NextToken(); - Balance(BalancingModes.NoErrorOnFailure, CSharpTokenType.LeftBrace, CSharpTokenType.RightBrace, startingBraceLocation); - Span.ChunkGenerator = new StatementChunkGenerator(); - var existingEditHandler = Span.EditHandler; - Span.EditHandler = new CodeBlockEditHandler(Language.TokenizeString); - - AddMarkerTokenIfNecessary(); - - Output(SpanKindInternal.Code); - - Span.EditHandler = existingEditHandler; - }); - break; - } - } - finally - { - if (directiveErrorSink.Errors.Count > 0) - { - directiveChunkGenerator.Diagnostics.AddRange(directiveErrorSink.Errors); - } - - Context.ErrorSink = savedErrorSink; - } - } - - - private void ValidateDirectiveUsage(DirectiveDescriptor descriptor) - { - if (descriptor.Usage == DirectiveUsage.FileScopedSinglyOccurring) - { - if (Context.SeenDirectives.Contains(descriptor.Directive)) - { - // There will always be at least 1 child because of the `@` transition. - var directiveStart = Context.Builder.CurrentBlock.Children.First().Start; - var errorLength = /* @ */ 1 + descriptor.Directive.Length; - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_DuplicateDirective( - new SourceSpan(directiveStart, errorLength), descriptor.Directive)); - - return; + _keywordParserMap.Remove(CSharpKeyword.Namespace); } } } - private void ParseDirectiveBlock(DirectiveDescriptor descriptor, Action parseChildren) - { - if (EndOfFile) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_UnexpectedEOFAfterDirective( - new SourceSpan(CurrentStart, contentLength: 1 /* { */), descriptor.Directive, "{")); - } - else if (!At(CSharpTokenType.LeftBrace)) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_UnexpectedDirectiveLiteral( - new SourceSpan(CurrentStart, CurrentToken.Content.Length), descriptor.Directive, "{")); - } - else - { - var editHandler = new AutoCompleteEditHandler(Language.TokenizeString, autoCompleteAtEndOfSpan: true); - Span.EditHandler = editHandler; - var startingBraceLocation = CurrentStart; - Accept(CurrentToken); - Span.ChunkGenerator = SpanChunkGenerator.Null; - Output(SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); - - parseChildren(startingBraceLocation); - - Span.ChunkGenerator = SpanChunkGenerator.Null; - if (!Optional(CSharpTokenType.RightBrace)) - { - editHandler.AutoCompleteString = "}"; - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_ExpectedEndOfBlockBeforeEOF( - new SourceSpan(startingBraceLocation, contentLength: 1 /* } */), descriptor.Directive, "}", "{")); - } - else - { - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; - } - CompleteBlock(insertMarkerIfNecessary: false, captureWhitespaceToEndOfLine: true); - Span.ChunkGenerator = SpanChunkGenerator.Null; - Output(SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); - } - } - - protected virtual void TagHelperPrefixDirective() + private void ParseTagHelperPrefixDirective(SyntaxListBuilder builder, CSharpTransitionSyntax transition) { RazorDiagnostic duplicateDiagnostic = null; if (Context.SeenDirectives.Contains(SyntaxConstants.CSharp.TagHelperPrefixKeyword)) { - // There wil always be at least 1 child because of the `@` transition. - var directiveStart = Context.Builder.CurrentBlock.Children.First().Start; + var directiveStart = CurrentStart; + if (transition != null) + { + // Start the error from the Transition '@'. + directiveStart = new SourceLocation( + directiveStart.FilePath, + directiveStart.AbsoluteIndex - 1, + directiveStart.LineIndex, + directiveStart.CharacterIndex - 1); + } var errorLength = /* @ */ 1 + SyntaxConstants.CSharp.TagHelperPrefixKeyword.Length; duplicateDiagnostic = RazorDiagnosticFactory.CreateParsing_DuplicateDirective( new SourceSpan(directiveStart, errorLength), SyntaxConstants.CSharp.TagHelperPrefixKeyword); } - TagHelperDirective( + var directiveBody = ParseTagHelperDirective( SyntaxConstants.CSharp.TagHelperPrefixKeyword, - (prefix, errors) => + (prefix, errors, startLocation) => { if (duplicateDiagnostic != null) { errors.Add(duplicateDiagnostic); } - var parsedDirective = ParseDirective(prefix, Span.Start, TagHelperDirectiveType.TagHelperPrefix, errors); + var parsedDirective = ParseDirective(prefix, startLocation, TagHelperDirectiveType.TagHelperPrefix, errors); return new TagHelperPrefixDirectiveChunkGenerator( prefix, parsedDirective.DirectiveText, errors); }); + + var directive = SyntaxFactory.RazorDirective(transition, directiveBody); + builder.Add(directive); } - // Internal for testing. - internal void ValidateTagHelperPrefix( - string prefix, - SourceLocation directiveLocation, - List diagnostics) + private void ParseAddTagHelperDirective(SyntaxListBuilder builder, CSharpTransitionSyntax transition) { - foreach (var character in prefix) - { - // Prefixes are correlated with tag names, tag names cannot have whitespace. - if (char.IsWhiteSpace(character) || InvalidNonWhitespaceNameCharacters.Contains(character)) + var directiveBody = ParseTagHelperDirective( + SyntaxConstants.CSharp.AddTagHelperKeyword, + (lookupText, errors, startLocation) => { - diagnostics.Add( - RazorDiagnosticFactory.CreateParsing_InvalidTagHelperPrefixValue( - new SourceSpan(directiveLocation, prefix.Length), - SyntaxConstants.CSharp.TagHelperPrefixKeyword, - character, - prefix)); + var parsedDirective = ParseDirective(lookupText, startLocation, TagHelperDirectiveType.AddTagHelper, errors); - return; + return new AddTagHelperChunkGenerator( + lookupText, + parsedDirective.DirectiveText, + parsedDirective.TypePattern, + parsedDirective.AssemblyName, + errors); + }); + + var directive = SyntaxFactory.RazorDirective(transition, directiveBody); + builder.Add(directive); + } + + private void ParseRemoveTagHelperDirective(SyntaxListBuilder builder, CSharpTransitionSyntax transition) + { + var directiveBody = ParseTagHelperDirective( + SyntaxConstants.CSharp.RemoveTagHelperKeyword, + (lookupText, errors, startLocation) => + { + var parsedDirective = ParseDirective(lookupText, startLocation, TagHelperDirectiveType.RemoveTagHelper, errors); + + return new RemoveTagHelperChunkGenerator( + lookupText, + parsedDirective.DirectiveText, + parsedDirective.TypePattern, + parsedDirective.AssemblyName, + errors); + }); + + var directive = SyntaxFactory.RazorDirective(transition, directiveBody); + builder.Add(directive); + } + + [Conditional("DEBUG")] + protected void AssertDirective(string directive) + { + Debug.Assert(CurrentToken.Kind == SyntaxKind.Identifier || CurrentToken.Kind == SyntaxKind.Keyword); + Debug.Assert(string.Equals(CurrentToken.Content, directive, StringComparison.Ordinal)); + } + + private RazorDirectiveBodySyntax ParseTagHelperDirective( + string keyword, + Func, SourceLocation, ISpanChunkGenerator> chunkGeneratorFactory) + { + AssertDirective(keyword); + + var savedErrorSink = Context.ErrorSink; + var directiveErrorSink = new ErrorSink(); + RazorMetaCodeSyntax keywordBlock = null; + using (var pooledResult = Pool.Allocate()) + { + var directiveBuilder = pooledResult.Builder; + Context.ErrorSink = directiveErrorSink; + + string directiveValue = null; + SourceLocation? valueStartLocation = null; + try + { + EnsureDirectiveIsAtStartOfLine(); + + var keywordStartLocation = CurrentStart; + + // Accept the directive name + var keywordToken = EatCurrentToken(); + var keywordLength = keywordToken.FullWidth + 1 /* @ */; + + var foundWhitespace = At(SyntaxKind.Whitespace); + + // If we found whitespace then any content placed within the whitespace MAY cause a destructive change + // to the document. We can't accept it. + var acceptedCharacters = foundWhitespace ? AcceptedCharactersInternal.None : AcceptedCharactersInternal.AnyExceptNewline; + Accept(keywordToken); + keywordBlock = OutputAsMetaCode(Output(), acceptedCharacters); + + AcceptWhile(SyntaxKind.Whitespace); + SpanContext.ChunkGenerator = SpanChunkGenerator.Null; + SpanContext.EditHandler.AcceptedCharacters = acceptedCharacters; + directiveBuilder.Add(OutputAsMarkupLiteral()); + + if (EndOfFile || At(SyntaxKind.NewLine)) + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_DirectiveMustHaveValue( + new SourceSpan(keywordStartLocation, keywordLength), keyword)); + + directiveValue = string.Empty; + } + else + { + // Need to grab the current location before we accept until the end of the line. + valueStartLocation = CurrentStart; + + // Parse to the end of the line. Essentially accepts anything until end of line, comments, invalid code + // etc. + AcceptUntil(SyntaxKind.NewLine); + + // Pull out the value and remove whitespaces and optional quotes + var rawValue = string.Concat(TokenBuilder.ToList().Nodes.Select(s => s.Content)).Trim(); + + var startsWithQuote = rawValue.StartsWith("\"", StringComparison.Ordinal); + var endsWithQuote = rawValue.EndsWith("\"", StringComparison.Ordinal); + if (startsWithQuote != endsWithQuote) + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_IncompleteQuotesAroundDirective( + new SourceSpan(valueStartLocation.Value, rawValue.Length), keyword)); + } + + directiveValue = rawValue; + } } + finally + { + SpanContext.ChunkGenerator = chunkGeneratorFactory( + directiveValue, + directiveErrorSink.Errors.ToList(), + valueStartLocation ?? CurrentStart); + Context.ErrorSink = savedErrorSink; + } + + // Finish the block and output the tokens + CompleteBlock(); + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.AnyExceptNewline; + + directiveBuilder.Add(OutputTokensAsStatementLiteral()); + var directiveCodeBlock = SyntaxFactory.CSharpCodeBlock(directiveBuilder.ToList()); + + return SyntaxFactory.RazorDirectiveBody(keywordBlock, directiveCodeBlock); } } @@ -1982,9 +1078,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy // Ex: @addTagHelper "*, Microsoft.AspNetCore.CoolLibrary" // ^ ^ // Start End - if (Span.Tokens.Count == 1 && (Span.Tokens[0] as CSharpToken)?.Type == CSharpTokenType.StringLiteral) + if (TokenBuilder.Count == 1 && + TokenBuilder[0] is SyntaxToken token && + token.Kind == SyntaxKind.StringLiteral) { - offset += Span.Tokens[0].Content.IndexOf(directiveText, StringComparison.Ordinal); + offset += token.Content.IndexOf(directiveText, StringComparison.Ordinal); // This is safe because inside one of these directives all of the text needs to be on the // same line. @@ -2044,122 +1142,1294 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return directive; } - protected virtual void AddTagHelperDirective() + // Internal for testing. + internal void ValidateTagHelperPrefix( + string prefix, + SourceLocation directiveLocation, + List diagnostics) { - TagHelperDirective( - SyntaxConstants.CSharp.AddTagHelperKeyword, - (lookupText, errors) => + foreach (var character in prefix) + { + // Prefixes are correlated with tag names, tag names cannot have whitespace. + if (char.IsWhiteSpace(character) || InvalidNonWhitespaceNameCharacters.Contains(character)) { - var parsedDirective = ParseDirective(lookupText, Span.Start, TagHelperDirectiveType.AddTagHelper, errors); + diagnostics.Add( + RazorDiagnosticFactory.CreateParsing_InvalidTagHelperPrefixValue( + new SourceSpan(directiveLocation, prefix.Length), + SyntaxConstants.CSharp.TagHelperPrefixKeyword, + character, + prefix)); - return new AddTagHelperChunkGenerator( - lookupText, - parsedDirective.DirectiveText, - parsedDirective.TypePattern, - parsedDirective.AssemblyName, - errors); - }); + return; + } + } } - protected virtual void RemoveTagHelperDirective() + private void ParseExtensibleDirective(in SyntaxListBuilder builder, CSharpTransitionSyntax transition, DirectiveDescriptor descriptor) { - TagHelperDirective( - SyntaxConstants.CSharp.RemoveTagHelperKeyword, - (lookupText, errors) => - { - var parsedDirective = ParseDirective(lookupText, Span.Start, TagHelperDirectiveType.RemoveTagHelper, errors); + AssertDirective(descriptor.Directive); - return new RemoveTagHelperChunkGenerator( - lookupText, - parsedDirective.DirectiveText, - parsedDirective.TypePattern, - parsedDirective.AssemblyName, - errors); - }); - } - - [Conditional("DEBUG")] - protected void AssertDirective(string directive) - { - Debug.Assert(CurrentToken.Type == CSharpTokenType.Identifier || CurrentToken.Type == CSharpTokenType.Keyword); - Debug.Assert(string.Equals(CurrentToken.Content, directive, StringComparison.Ordinal)); - } - - private void TagHelperDirective(string keyword, Func, ISpanChunkGenerator> chunkGeneratorFactory) - { - AssertDirective(keyword); - - var savedErrorSink = Context.ErrorSink; var directiveErrorSink = new ErrorSink(); + var savedErrorSink = Context.ErrorSink; Context.ErrorSink = directiveErrorSink; - string directiveValue = null; - try + using (var pooledResult = Pool.Allocate()) { - EnsureDirectiveIsAtStartOfLine(); + var directiveBuilder = pooledResult.Builder; + RazorMetaCodeSyntax keywordBlock = null; - var keywordStartLocation = CurrentStart; - - // Accept the directive name - AcceptAndMoveNext(); - - // Set the block type - Context.Builder.CurrentBlock.Type = BlockKindInternal.Directive; - - var keywordLength = Span.End.AbsoluteIndex - Span.Start.AbsoluteIndex; - - var foundWhitespace = At(CSharpTokenType.WhiteSpace); - - // If we found whitespace then any content placed within the whitespace MAY cause a destructive change - // to the document. We can't accept it. - var acceptedCharacters = foundWhitespace ? AcceptedCharactersInternal.None : AcceptedCharactersInternal.AnyExceptNewline; - Output(SpanKindInternal.MetaCode, acceptedCharacters); - - AcceptWhile(CSharpTokenType.WhiteSpace); - Span.ChunkGenerator = SpanChunkGenerator.Null; - Output(SpanKindInternal.Markup, acceptedCharacters); - - if (EndOfFile || At(CSharpTokenType.NewLine)) + try { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_DirectiveMustHaveValue( - new SourceSpan(keywordStartLocation, keywordLength), keyword)); + EnsureDirectiveIsAtStartOfLine(); + var directiveStart = CurrentStart; + if (transition != null) + { + // Start the error from the Transition '@'. + directiveStart = new SourceLocation( + directiveStart.FilePath, + directiveStart.AbsoluteIndex - 1, + directiveStart.LineIndex, + directiveStart.CharacterIndex - 1); + } - directiveValue = string.Empty; + AcceptAndMoveNext(); + keywordBlock = OutputAsMetaCode(Output()); + + // Even if an error was logged do not bail out early. If a directive was used incorrectly it doesn't mean it can't be parsed. + ValidateDirectiveUsage(descriptor, directiveStart); + + for (var i = 0; i < descriptor.Tokens.Count; i++) + { + if (!At(SyntaxKind.Whitespace) && + !At(SyntaxKind.NewLine) && + !EndOfFile) + { + // This case should never happen in a real scenario. We're just being defensive. + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_DirectiveTokensMustBeSeparatedByWhitespace( + new SourceSpan(CurrentStart, CurrentToken.Content.Length), descriptor.Directive)); + + builder.Add(BuildDirective()); + return; + } + + var tokenDescriptor = descriptor.Tokens[i]; + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + if (tokenDescriptor.Kind == DirectiveTokenKind.Member || + tokenDescriptor.Kind == DirectiveTokenKind.Namespace || + tokenDescriptor.Kind == DirectiveTokenKind.Type) + { + SpanContext.ChunkGenerator = SpanChunkGenerator.Null; + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Whitespace; + directiveBuilder.Add(OutputTokensAsStatementLiteral()); + + if (EndOfFile || At(SyntaxKind.NewLine)) + { + // Add a marker token to provide CSharp intellisense when we start typing the directive token. + AcceptMarkerTokenIfNecessary(); + SpanContext.ChunkGenerator = new DirectiveTokenChunkGenerator(tokenDescriptor); + SpanContext.EditHandler = new DirectiveTokenEditHandler(Language.TokenizeString); + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.NonWhitespace; + directiveBuilder.Add(OutputTokensAsStatementLiteral()); + } + } + else + { + SpanContext.ChunkGenerator = SpanChunkGenerator.Null; + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Whitespace; + directiveBuilder.Add(OutputAsMarkupEphemeralLiteral()); + } + + if (tokenDescriptor.Optional && (EndOfFile || At(SyntaxKind.NewLine))) + { + break; + } + else if (EndOfFile) + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_UnexpectedEOFAfterDirective( + new SourceSpan(CurrentStart, contentLength: 1), + descriptor.Directive, + tokenDescriptor.Kind.ToString().ToLowerInvariant())); + builder.Add(BuildDirective()); + return; + } + + switch (tokenDescriptor.Kind) + { + case DirectiveTokenKind.Type: + if (!TryParseNamespaceOrTypeName(directiveBuilder)) + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_DirectiveExpectsTypeName( + new SourceSpan(CurrentStart, CurrentToken.Content.Length), descriptor.Directive)); + + builder.Add(BuildDirective()); + return; + } + break; + + case DirectiveTokenKind.Namespace: + if (!TryParseQualifiedIdentifier(out var identifierLength)) + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_DirectiveExpectsNamespace( + new SourceSpan(CurrentStart, identifierLength), descriptor.Directive)); + + builder.Add(BuildDirective()); + return; + } + break; + + case DirectiveTokenKind.Member: + if (At(SyntaxKind.Identifier)) + { + AcceptAndMoveNext(); + } + else + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_DirectiveExpectsIdentifier( + new SourceSpan(CurrentStart, CurrentToken.Content.Length), descriptor.Directive)); + builder.Add(BuildDirective()); + return; + } + break; + + case DirectiveTokenKind.String: + if (At(SyntaxKind.StringLiteral) && !CurrentToken.ContainsDiagnostics) + { + AcceptAndMoveNext(); + } + else + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_DirectiveExpectsQuotedStringLiteral( + new SourceSpan(CurrentStart, CurrentToken.Content.Length), descriptor.Directive)); + builder.Add(BuildDirective()); + return; + } + break; + } + + SpanContext.ChunkGenerator = new DirectiveTokenChunkGenerator(tokenDescriptor); + SpanContext.EditHandler = new DirectiveTokenEditHandler(Language.TokenizeString); + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.NonWhitespace; + directiveBuilder.Add(OutputTokensAsStatementLiteral()); + } + + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + SpanContext.ChunkGenerator = SpanChunkGenerator.Null; + + switch (descriptor.Kind) + { + case DirectiveKind.SingleLine: + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Whitespace; + directiveBuilder.Add(OutputTokensAsUnclassifiedLiteral()); + + TryAccept(SyntaxKind.Semicolon); + directiveBuilder.Add(OutputAsMetaCode(Output(), AcceptedCharactersInternal.Whitespace)); + + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + if (At(SyntaxKind.NewLine)) + { + AcceptAndMoveNext(); + } + else if (!EndOfFile) + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_UnexpectedDirectiveLiteral( + new SourceSpan(CurrentStart, CurrentToken.Content.Length), + descriptor.Directive, + Resources.ErrorComponent_Newline)); + } + + + // This should contain the optional whitespace after the optional semicolon and the new line. + // Output as Markup as we want intellisense here. + SpanContext.ChunkGenerator = SpanChunkGenerator.Null; + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Whitespace; + directiveBuilder.Add(OutputAsMarkupEphemeralLiteral()); + break; + case DirectiveKind.RazorBlock: + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.AllWhitespace; + directiveBuilder.Add(OutputAsMarkupLiteral()); + + ParseDirectiveBlock(directiveBuilder, descriptor, parseChildren: (childBuilder, startingBraceLocation) => + { + // When transitioning to the HTML parser we no longer want to act as if we're in a nested C# state. + // For instance, if
@hello.
is in a nested C# block we don't want the trailing '.' to be handled + // as C#; it should be handled as a period because it's wrapped in markup. + var wasNested = IsNested; + IsNested = false; + + using (PushSpanContextConfig()) + { + var razorBlock = HtmlParser.ParseRazorBlock(Tuple.Create("{", "}"), caseSensitive: true); + directiveBuilder.Add(razorBlock); + } + + InitializeContext(SpanContext); + IsNested = wasNested; + NextToken(); + }); + break; + case DirectiveKind.CodeBlock: + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.AllWhitespace; + directiveBuilder.Add(OutputAsMarkupLiteral()); + + ParseDirectiveBlock(directiveBuilder, descriptor, parseChildren: (childBuilder, startingBraceLocation) => + { + NextToken(); + Balance(childBuilder, BalancingModes.NoErrorOnFailure, SyntaxKind.LeftBrace, SyntaxKind.RightBrace, startingBraceLocation); + SpanContext.ChunkGenerator = new StatementChunkGenerator(); + var existingEditHandler = SpanContext.EditHandler; + SpanContext.EditHandler = new CodeBlockEditHandler(Language.TokenizeString); + + AcceptMarkerTokenIfNecessary(); + + childBuilder.Add(OutputTokensAsStatementLiteral()); + + SpanContext.EditHandler = existingEditHandler; + }); + break; + } + } + finally + { + Context.ErrorSink = savedErrorSink; + } + + builder.Add(BuildDirective()); + + RazorDirectiveSyntax BuildDirective() + { + directiveBuilder.Add(OutputTokensAsStatementLiteral()); + var directiveCodeBlock = SyntaxFactory.CSharpCodeBlock(directiveBuilder.ToList()); + + var directiveBody = SyntaxFactory.RazorDirectiveBody(keywordBlock, directiveCodeBlock); + var directive = SyntaxFactory.RazorDirective(transition, directiveBody); + directive = (RazorDirectiveSyntax)directive.SetDiagnostics(directiveErrorSink.Errors.ToArray()); + directive = directive.WithDirectiveDescriptor(descriptor); + return directive; + } + } + } + + private void ValidateDirectiveUsage(DirectiveDescriptor descriptor, SourceLocation directiveStart) + { + if (descriptor.Usage == DirectiveUsage.FileScopedSinglyOccurring) + { + if (Context.SeenDirectives.Contains(descriptor.Directive)) + { + // There will always be at least 1 child because of the `@` transition. + var errorLength = /* @ */ 1 + descriptor.Directive.Length; + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_DuplicateDirective( + new SourceSpan(directiveStart, errorLength), descriptor.Directive)); + + return; + } + } + } + + // Used for parsing a qualified name like that which follows the `namespace` keyword. + // + // qualified-identifier: + // identifier + // qualified-identifier . identifier + protected bool TryParseQualifiedIdentifier(out int identifierLength) + { + var currentIdentifierLength = 0; + var expectingDot = false; + var tokens = ReadWhile(token => + { + var type = token.Kind; + if ((expectingDot && type == SyntaxKind.Dot) || + (!expectingDot && type == SyntaxKind.Identifier)) + { + expectingDot = !expectingDot; + return true; + } + + if (type != SyntaxKind.Whitespace && + type != SyntaxKind.NewLine) + { + expectingDot = false; + currentIdentifierLength += token.Content.Length; + } + + return false; + }); + + identifierLength = currentIdentifierLength; + var validQualifiedIdentifier = expectingDot; + if (validQualifiedIdentifier) + { + foreach (var token in tokens) + { + identifierLength += token.Content.Length; + Accept(token); + } + + return true; + } + else + { + PutCurrentBack(); + + foreach (var token in tokens) + { + identifierLength += token.Content.Length; + PutBack(token); + } + + EnsureCurrent(); + return false; + } + } + + private void ParseDirectiveBlock(in SyntaxListBuilder builder, DirectiveDescriptor descriptor, Action, SourceLocation> parseChildren) + { + if (EndOfFile) + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_UnexpectedEOFAfterDirective( + new SourceSpan(CurrentStart, contentLength: 1 /* { */), descriptor.Directive, "{")); + } + else if (!At(SyntaxKind.LeftBrace)) + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_UnexpectedDirectiveLiteral( + new SourceSpan(CurrentStart, CurrentToken.Content.Length), descriptor.Directive, "{")); + } + else + { + var editHandler = new AutoCompleteEditHandler(Language.TokenizeString, autoCompleteAtEndOfSpan: true); + SpanContext.EditHandler = editHandler; + var startingBraceLocation = CurrentStart; + Accept(CurrentToken); + builder.Add(OutputAsMetaCode(Output())); + + using (var pooledResult = Pool.Allocate()) + { + var childBuilder = pooledResult.Builder; + parseChildren(childBuilder, startingBraceLocation); + if (childBuilder.Count > 0) + { + builder.Add(SyntaxFactory.CSharpCodeBlock(childBuilder.ToList())); + } + } + + SpanContext.ChunkGenerator = SpanChunkGenerator.Null; + if (!TryAccept(SyntaxKind.RightBrace)) + { + editHandler.AutoCompleteString = "}"; + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_ExpectedEndOfBlockBeforeEOF( + new SourceSpan(startingBraceLocation, contentLength: 1 /* } */), descriptor.Directive, "}", "{")); + + Accept(SyntaxFactory.MissingToken(SyntaxKind.RightBrace)); } else { - // Need to grab the current location before we accept until the end of the line. - var startLocation = CurrentStart; + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; + } + CompleteBlock(insertMarkerIfNecessary: false, captureWhitespaceToEndOfLine: true); + builder.Add(OutputAsMetaCode(Output(), SpanContext.EditHandler.AcceptedCharacters)); + } + } - // Parse to the end of the line. Essentially accepts anything until end of line, comments, invalid code - // etc. - AcceptUntil(CSharpTokenType.NewLine); + private bool TryParseKeyword(in SyntaxListBuilder builder, CSharpTransitionSyntax transition) + { + var result = CSharpTokenizer.GetTokenKeyword(CurrentToken); + Debug.Assert(CurrentToken.Kind == SyntaxKind.Keyword && result.HasValue); + if (_keywordParserMap.TryGetValue(result.Value, out var handler)) + { + handler(builder, transition); + return true; + } - // Pull out the value and remove whitespaces and optional quotes - var rawValue = string.Concat(Span.Tokens.Select(s => s.Content)).Trim(); + return false; + } - var startsWithQuote = rawValue.StartsWith("\"", StringComparison.Ordinal); - var endsWithQuote = rawValue.EndsWith("\"", StringComparison.Ordinal); - if (startsWithQuote != endsWithQuote) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_IncompleteQuotesAroundDirective( - new SourceSpan(startLocation, rawValue.Length), keyword)); - } + private void SetupExpressionParsers() + { + MapExpressionKeyword(ParseAwaitExpression, CSharpKeyword.Await); + } - directiveValue = rawValue; + private void SetupKeywordParsers() + { + MapKeywords( + ParseConditionalBlock, + CSharpKeyword.For, + CSharpKeyword.Foreach, + CSharpKeyword.While, + CSharpKeyword.Switch, + CSharpKeyword.Lock); + MapKeywords(ParseCaseStatement, false, CSharpKeyword.Case, CSharpKeyword.Default); + MapKeywords(ParseIfStatement, CSharpKeyword.If); + MapKeywords(ParseTryStatement, CSharpKeyword.Try); + MapKeywords(ParseDoStatement, CSharpKeyword.Do); + MapKeywords(ParseUsingKeyword, CSharpKeyword.Using); + MapKeywords(ParseReservedDirective, CSharpKeyword.Class, CSharpKeyword.Namespace); + } + + private void MapExpressionKeyword(Action, CSharpTransitionSyntax> handler, CSharpKeyword keyword) + { + _keywordParserMap.Add(keyword, handler); + + // Expression keywords don't belong in the regular keyword list + } + + private void MapKeywords(Action, CSharpTransitionSyntax> handler, params CSharpKeyword[] keywords) + { + MapKeywords(handler, topLevel: true, keywords: keywords); + } + + private void MapKeywords(Action, CSharpTransitionSyntax> handler, bool topLevel, params CSharpKeyword[] keywords) + { + foreach (var keyword in keywords) + { + _keywordParserMap.Add(keyword, handler); + if (topLevel) + { + Keywords.Add(CSharpLanguageCharacteristics.GetKeyword(keyword)); } } - finally + } + + private void ParseAwaitExpression(SyntaxListBuilder builder, CSharpTransitionSyntax transition) + { + // Ensure that we're on the await statement (only runs in debug) + Assert(CSharpKeyword.Await); + + // Accept the "await" and move on + AcceptAndMoveNext(); + + // Accept 1 or more spaces between the await and the following code. + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + // Top level basically indicates if we're within an expression or statement. + // Ex: topLevel true = @await Foo() | topLevel false = @{ await Foo(); } + // Note that in this case @{ @await Foo() } top level is true for await. + // Therefore, if we're top level then we want to act like an implicit expression, + // otherwise just act as whatever we're contained in. + var topLevel = transition != null; + if (topLevel) { - Span.ChunkGenerator = chunkGeneratorFactory(directiveValue, directiveErrorSink.Errors.ToList()); - Context.ErrorSink = savedErrorSink; + // Setup the Span to be an async implicit expression (an implicit expresison that allows spaces). + // Spaces are allowed because of "@await Foo()". + var implicitExpressionBody = ParseImplicitExpressionBody(async: true); + builder.Add(SyntaxFactory.CSharpImplicitExpression(transition, implicitExpressionBody)); + } + } + + private void ParseConditionalBlock(SyntaxListBuilder builder, CSharpTransitionSyntax transition) + { + var topLevel = transition != null; + ParseConditionalBlock(builder, transition, topLevel); + } + + private void ParseConditionalBlock(in SyntaxListBuilder builder, CSharpTransitionSyntax transition, bool topLevel) + { + Assert(SyntaxKind.Keyword); + if (transition != null) + { + builder.Add(transition); } - // Output the span and finish the block + var block = new Block(CurrentToken, CurrentStart); + ParseConditionalBlock(builder, block); + if (topLevel) + { + CompleteBlock(); + } + } + + private void ParseConditionalBlock(in SyntaxListBuilder builder, Block block) + { + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + // Parse the condition, if present (if not present, we'll let the C# compiler complain) + if (TryParseCondition(builder)) + { + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + ParseExpectedCodeBlock(builder, block); + } + } + + private bool TryParseCondition(in SyntaxListBuilder builder) + { + if (At(SyntaxKind.LeftParenthesis)) + { + var complete = Balance(builder, BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates); + if (!complete) + { + AcceptUntil(SyntaxKind.NewLine); + } + else + { + TryAccept(SyntaxKind.RightParenthesis); + } + return complete; + } + return true; + } + + private void ParseExpectedCodeBlock(in SyntaxListBuilder builder, Block block) + { + if (!EndOfFile) + { + // Check for "{" to make sure we're at a block + if (!At(SyntaxKind.LeftBrace)) + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_SingleLineControlFlowStatementsNotAllowed( + new SourceSpan(CurrentStart, CurrentToken.Content.Length), + Language.GetSample(SyntaxKind.LeftBrace), + CurrentToken.Content)); + } + + // Parse the statement and then we're done + ParseStatement(builder, block); + } + } + + private void ParseUnconditionalBlock(in SyntaxListBuilder builder) + { + Assert(SyntaxKind.Keyword); + var block = new Block(CurrentToken, CurrentStart); + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + ParseExpectedCodeBlock(builder, block); + } + + private void ParseCaseStatement(SyntaxListBuilder builder, CSharpTransitionSyntax transition) + { + Assert(SyntaxKind.Keyword); + if (transition != null) + { + // Normally, case statement won't start with a transition in a valid scenario. + // If it does, just accept it and let the compiler complain. + builder.Add(transition); + } + var result = CSharpTokenizer.GetTokenKeyword(CurrentToken); + Debug.Assert(result.HasValue && + (result.Value == CSharpKeyword.Case || + result.Value == CSharpKeyword.Default)); + AcceptUntil(SyntaxKind.Colon); + TryAccept(SyntaxKind.Colon); + } + + private void ParseIfStatement(SyntaxListBuilder builder, CSharpTransitionSyntax transition) + { + Assert(CSharpKeyword.If); + ParseConditionalBlock(builder, transition, topLevel: false); + ParseAfterIfClause(builder); + var topLevel = transition != null; + if (topLevel) + { + CompleteBlock(); + } + } + + private void ParseAfterIfClause(SyntaxListBuilder builder) + { + // Grab whitespace and razor comments + var whitespace = SkipToNextImportantToken(builder); + + // Check for an else part + if (At(CSharpKeyword.Else)) + { + Accept(whitespace); + Assert(CSharpKeyword.Else); + ParseElseClause(builder); + } + else + { + // No else, return whitespace + PutCurrentBack(); + PutBack(whitespace); + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; + } + } + + private void ParseElseClause(in SyntaxListBuilder builder) + { + if (!At(CSharpKeyword.Else)) + { + return; + } + var block = new Block(CurrentToken, CurrentStart); + + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (At(CSharpKeyword.If)) + { + // ElseIf + block.Name = SyntaxConstants.CSharp.ElseIfKeyword; + ParseConditionalBlock(builder, block); + ParseAfterIfClause(builder); + } + else if (!EndOfFile) + { + // Else + ParseExpectedCodeBlock(builder, block); + } + } + + private void ParseTryStatement(SyntaxListBuilder builder, CSharpTransitionSyntax transition) + { + Assert(CSharpKeyword.Try); + var topLevel = transition != null; + if (topLevel) + { + builder.Add(transition); + } + + ParseUnconditionalBlock(builder); + ParseAfterTryClause(builder); + if (topLevel) + { + CompleteBlock(); + } + } + + private void ParseAfterTryClause(in SyntaxListBuilder builder) + { + // Grab whitespace + var whitespace = SkipToNextImportantToken(builder); + + // Check for a catch or finally part + if (At(CSharpKeyword.Catch)) + { + Accept(whitespace); + Assert(CSharpKeyword.Catch); + ParseFilterableCatchBlock(builder); + ParseAfterTryClause(builder); + } + else if (At(CSharpKeyword.Finally)) + { + Accept(whitespace); + Assert(CSharpKeyword.Finally); + ParseUnconditionalBlock(builder); + } + else + { + // Return whitespace and end the block + PutCurrentBack(); + PutBack(whitespace); + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; + } + } + + private void ParseFilterableCatchBlock(in SyntaxListBuilder builder) + { + Assert(CSharpKeyword.Catch); + + var block = new Block(CurrentToken, CurrentStart); + + // Accept "catch" + AcceptAndMoveNext(); + AcceptWhile(IsValidStatementSpacingToken); + + // Parse the catch condition if present. If not present, let the C# compiler complain. + if (TryParseCondition(builder)) + { + AcceptWhile(IsValidStatementSpacingToken); + + if (At(CSharpKeyword.When)) + { + // Accept "when". + AcceptAndMoveNext(); + AcceptWhile(IsValidStatementSpacingToken); + + // Parse the filter condition if present. If not present, let the C# compiler complain. + if (!TryParseCondition(builder)) + { + // Incomplete condition. + return; + } + + AcceptWhile(IsValidStatementSpacingToken); + } + + ParseExpectedCodeBlock(builder, block); + } + } + + private void ParseDoStatement(SyntaxListBuilder builder, CSharpTransitionSyntax transition) + { + Assert(CSharpKeyword.Do); + if (transition != null) + { + builder.Add(transition); + } + + ParseUnconditionalBlock(builder); + ParseWhileClause(builder); + var topLevel = transition != null; + if (topLevel) + { + CompleteBlock(); + } + } + + private void ParseWhileClause(in SyntaxListBuilder builder) + { + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; + var whitespace = SkipToNextImportantToken(builder); + + if (At(CSharpKeyword.While)) + { + Accept(whitespace); + Assert(CSharpKeyword.While); + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (TryParseCondition(builder) && TryAccept(SyntaxKind.Semicolon)) + { + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; + } + } + else + { + PutCurrentBack(); + PutBack(whitespace); + } + } + + private void ParseUsingKeyword(SyntaxListBuilder builder, CSharpTransitionSyntax transition) + { + Assert(CSharpKeyword.Using); + var topLevel = transition != null; + var block = new Block(CurrentToken, CurrentStart); + var usingToken = EatCurrentToken(); + var whitespaceOrComments = ReadWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + var atLeftParen = At(SyntaxKind.LeftParenthesis); + var atIdentifier = At(SyntaxKind.Identifier); + var atStatic = At(CSharpKeyword.Static); + + // Put the read tokens back and let them be handled later. + PutCurrentBack(); + PutBack(whitespaceOrComments); + PutBack(usingToken); + EnsureCurrent(); + + if (atLeftParen) + { + // using ( ==> Using Statement + ParseUsingStatement(builder, transition, block); + } + else if (atIdentifier || atStatic) + { + // using Identifier ==> Using Declaration + if (!topLevel) + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_NamespaceImportAndTypeAliasCannotExistWithinCodeBlock( + new SourceSpan(block.Start, block.Name.Length))); + if (transition != null) + { + builder.Add(transition); + } + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + ParseStandardStatement(builder); + } + else + { + ParseUsingDeclaration(builder, transition); + return; + } + } + else + { + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + } + + if (topLevel) + { + CompleteBlock(); + } + } + + private void ParseUsingStatement(in SyntaxListBuilder builder, CSharpTransitionSyntax transition, Block block) + { + Assert(CSharpKeyword.Using); + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + Assert(SyntaxKind.LeftParenthesis); + if (transition != null) + { + builder.Add(transition); + } + + // Parse condition + if (TryParseCondition(builder)) + { + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + // Parse code block + ParseExpectedCodeBlock(builder, block); + } + } + + private void ParseUsingDeclaration(in SyntaxListBuilder builder, CSharpTransitionSyntax transition) + { + // Using declarations should always be top level. The error case is handled in a different code path. + Debug.Assert(transition != null); + using (var pooledResult = Pool.Allocate()) + { + var directiveBuilder = pooledResult.Builder; + Assert(CSharpKeyword.Using); + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + var start = CurrentStart; + if (At(SyntaxKind.Identifier)) + { + // non-static using + TryParseNamespaceOrTypeName(directiveBuilder); + var whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (At(SyntaxKind.Assign)) + { + // Alias + Accept(whitespace); + Assert(SyntaxKind.Assign); + AcceptAndMoveNext(); + + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + // One more namespace or type name + TryParseNamespaceOrTypeName(directiveBuilder); + } + else + { + PutCurrentBack(); + PutBack(whitespace); + } + } + else if (At(CSharpKeyword.Static)) + { + // static using + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + TryParseNamespaceOrTypeName(directiveBuilder); + } + + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.AnyExceptNewline; + SpanContext.ChunkGenerator = new AddImportChunkGenerator(new LocationTagged( + string.Concat(TokenBuilder.ToList().Nodes.Skip(1).Select(s => s.Content)), + start)); + + // Optional ";" + if (EnsureCurrent()) + { + TryAccept(SyntaxKind.Semicolon); + } + + CompleteBlock(); + Debug.Assert(directiveBuilder.Count == 0, "We should not have built any blocks so far."); + var keywordTokens = OutputTokensAsStatementLiteral(); + var directiveBody = SyntaxFactory.RazorDirectiveBody(keywordTokens, null); + builder.Add(SyntaxFactory.RazorDirective(transition, directiveBody)); + } + } + + private bool TryParseNamespaceOrTypeName(in SyntaxListBuilder builder) + { + if (TryAccept(SyntaxKind.LeftParenthesis)) + { + while (!TryAccept(SyntaxKind.RightParenthesis) && !EndOfFile) + { + TryAccept(SyntaxKind.Whitespace); + + if (!TryParseNamespaceOrTypeName(builder)) + { + return false; + } + + TryAccept(SyntaxKind.Whitespace); + TryAccept(SyntaxKind.Identifier); + TryAccept(SyntaxKind.Whitespace); + TryAccept(SyntaxKind.Comma); + } + + if (At(SyntaxKind.Whitespace) && NextIs(SyntaxKind.QuestionMark)) + { + // Only accept the whitespace if we are going to consume the next token. + AcceptAndMoveNext(); + } + + TryAccept(SyntaxKind.QuestionMark); // Nullable + + return true; + } + else if (TryAccept(SyntaxKind.Identifier) || TryAccept(SyntaxKind.Keyword)) + { + if (TryAccept(SyntaxKind.DoubleColon)) + { + if (!TryAccept(SyntaxKind.Identifier)) + { + TryAccept(SyntaxKind.Keyword); + } + } + if (At(SyntaxKind.LessThan)) + { + ParseTypeArgumentList(builder); + } + if (TryAccept(SyntaxKind.Dot)) + { + TryParseNamespaceOrTypeName(builder); + } + + if (At(SyntaxKind.Whitespace) && NextIs(SyntaxKind.QuestionMark)) + { + // Only accept the whitespace if we are going to consume the next token. + AcceptAndMoveNext(); + } + + TryAccept(SyntaxKind.QuestionMark); // Nullable + + if (At(SyntaxKind.Whitespace) && NextIs(SyntaxKind.LeftBracket)) + { + // Only accept the whitespace if we are going to consume the next token. + AcceptAndMoveNext(); + } + + while (At(SyntaxKind.LeftBracket)) + { + Balance(builder, BalancingModes.None); + if (!TryAccept(SyntaxKind.RightBracket)) + { + Accept(SyntaxFactory.MissingToken(SyntaxKind.RightBracket)); + } + } + return true; + } + else + { + return false; + } + } + + private void ParseTypeArgumentList(in SyntaxListBuilder builder) + { + Assert(SyntaxKind.LessThan); + Balance(builder, BalancingModes.None); + if (!TryAccept(SyntaxKind.GreaterThan)) + { + Accept(SyntaxFactory.MissingToken(SyntaxKind.GreaterThan)); + } + } + + private void ParseReservedDirective(SyntaxListBuilder builder, CSharpTransitionSyntax transition) + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_ReservedWord( + new SourceSpan(CurrentStart, CurrentToken.Content.Length), CurrentToken.Content)); + + AcceptAndMoveNext(); + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; + SpanContext.ChunkGenerator = SpanChunkGenerator.Null; CompleteBlock(); - Output(SpanKindInternal.Code, AcceptedCharactersInternal.AnyExceptNewline); + var keyword = OutputAsMetaCode(Output()); + var directiveBody = SyntaxFactory.RazorDirectiveBody(keyword, cSharpCode: null); + var directive = SyntaxFactory.RazorDirective(transition, directiveBody); + builder.Add(directive); + } + + protected void CompleteBlock() + { + CompleteBlock(insertMarkerIfNecessary: true); + } + + protected void CompleteBlock(bool insertMarkerIfNecessary) + { + CompleteBlock(insertMarkerIfNecessary, captureWhitespaceToEndOfLine: insertMarkerIfNecessary); + } + + protected void CompleteBlock(bool insertMarkerIfNecessary, bool captureWhitespaceToEndOfLine) + { + if (insertMarkerIfNecessary && Context.LastAcceptedCharacters != AcceptedCharactersInternal.Any) + { + AcceptMarkerTokenIfNecessary(); + } + + EnsureCurrent(); + + // Read whitespace, but not newlines + // If we're not inserting a marker span, we don't need to capture whitespace + if (!Context.WhiteSpaceIsSignificantToAncestorBlock && + captureWhitespaceToEndOfLine && + !Context.DesignTimeMode && + !IsNested) + { + var whitespace = ReadWhile(token => token.Kind == SyntaxKind.Whitespace); + if (At(SyntaxKind.NewLine)) + { + Accept(whitespace); + AcceptAndMoveNext(); + PutCurrentBack(); + } + else + { + PutCurrentBack(); + PutBack(whitespace); + } + } + else + { + PutCurrentBack(); + } + } + + private IEnumerable SkipToNextImportantToken(in SyntaxListBuilder builder) + { + while (!EndOfFile) + { + var whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (At(SyntaxKind.RazorCommentTransition)) + { + Accept(whitespace); + SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; + AcceptMarkerTokenIfNecessary(); + builder.Add(OutputTokensAsStatementLiteral()); + var comment = ParseRazorComment(); + builder.Add(comment); + } + else + { + return whitespace; + } + } + return Enumerable.Empty(); + } + + private void DefaultSpanContextConfig(SpanContextBuilder spanContext) + { + spanContext.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString); + spanContext.ChunkGenerator = new StatementChunkGenerator(); + } + + private void ExplicitExpressionSpanContextConfig(SpanContextBuilder spanContext) + { + spanContext.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString); + spanContext.ChunkGenerator = new ExpressionChunkGenerator(); + } + + private CSharpStatementLiteralSyntax OutputTokensAsStatementLiteral() + { + var tokens = Output(); + if (tokens.Count == 0) + { + return null; + } + + return GetNodeWithSpanContext(SyntaxFactory.CSharpStatementLiteral(tokens)); + } + + private CSharpExpressionLiteralSyntax OutputTokensAsExpressionLiteral() + { + var tokens = Output(); + if (tokens.Count == 0) + { + return null; + } + + return GetNodeWithSpanContext(SyntaxFactory.CSharpExpressionLiteral(tokens)); + } + + private CSharpEphemeralTextLiteralSyntax OutputTokensAsEphemeralLiteral() + { + var tokens = Output(); + if (tokens.Count == 0) + { + return null; + } + + return GetNodeWithSpanContext(SyntaxFactory.CSharpEphemeralTextLiteral(tokens)); + } + + private UnclassifiedTextLiteralSyntax OutputTokensAsUnclassifiedLiteral() + { + var tokens = Output(); + if (tokens.Count == 0) + { + return null; + } + + + return GetNodeWithSpanContext(SyntaxFactory.UnclassifiedTextLiteral(tokens)); + } + + private void OtherParserBlock(in SyntaxListBuilder builder) + { + // When transitioning to the HTML parser we no longer want to act as if we're in a nested C# state. + // For instance, if
@hello.
is in a nested C# block we don't want the trailing '.' to be handled + // as C#; it should be handled as a period because it's wrapped in markup. + var wasNested = IsNested; + IsNested = false; + + RazorSyntaxNode htmlBlock = null; + using (PushSpanContextConfig()) + { + htmlBlock = HtmlParser.ParseBlock(); + } + + builder.Add(htmlBlock); + InitializeContext(SpanContext); + + IsNested = wasNested; + NextToken(); + } + + private bool Balance(SyntaxListBuilder builder, BalancingModes mode) + { + var left = CurrentToken.Kind; + var right = Language.FlipBracket(left); + var start = CurrentStart; + AcceptAndMoveNext(); + if (EndOfFile && ((mode & BalancingModes.NoErrorOnFailure) != BalancingModes.NoErrorOnFailure)) + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_ExpectedCloseBracketBeforeEOF( + new SourceSpan(start, contentLength: 1 /* { OR } */), + Language.GetSample(left), + Language.GetSample(right))); + } + + return Balance(builder, mode, left, right, start); + } + + private bool Balance(SyntaxListBuilder builder, BalancingModes mode, SyntaxKind left, SyntaxKind right, SourceLocation start) + { + var startPosition = CurrentStart.AbsoluteIndex; + var nesting = 1; + if (!EndOfFile) + { + var tokens = new List(); + do + { + if (IsAtEmbeddedTransition( + (mode & BalancingModes.AllowCommentsAndTemplates) == BalancingModes.AllowCommentsAndTemplates, + (mode & BalancingModes.AllowEmbeddedTransitions) == BalancingModes.AllowEmbeddedTransitions)) + { + Accept(tokens); + tokens.Clear(); + ParseEmbeddedTransition(builder); + + // Reset backtracking since we've already outputted some spans. + startPosition = CurrentStart.AbsoluteIndex; + } + if (At(left)) + { + nesting++; + } + else if (At(right)) + { + nesting--; + } + if (nesting > 0) + { + tokens.Add(CurrentToken); + } + } + while (nesting > 0 && NextToken()); + + if (nesting > 0) + { + if ((mode & BalancingModes.NoErrorOnFailure) != BalancingModes.NoErrorOnFailure) + { + Context.ErrorSink.OnError( + RazorDiagnosticFactory.CreateParsing_ExpectedCloseBracketBeforeEOF( + new SourceSpan(start, contentLength: 1 /* { OR } */), + Language.GetSample(left), + Language.GetSample(right))); + } + if ((mode & BalancingModes.BacktrackOnFailure) == BalancingModes.BacktrackOnFailure) + { + Context.Source.Position = startPosition; + NextToken(); + } + else + { + Accept(tokens); + } + } + else + { + // Accept all the tokens we saw + Accept(tokens); + } + } + return nesting == 0; + } + + private bool IsAtEmbeddedTransition(bool allowTemplatesAndComments, bool allowTransitions) + { + // No embedded transitions in C#, so ignore that param + return allowTemplatesAndComments + && ((Language.IsTransition(CurrentToken) + && NextIs(SyntaxKind.LessThan, SyntaxKind.Colon, SyntaxKind.DoubleColon)) + || Language.IsCommentStart(CurrentToken)); + } + + private void ParseEmbeddedTransition(in SyntaxListBuilder builder) + { + if (Language.IsTransition(CurrentToken)) + { + PutCurrentBack(); + ParseTemplate(builder); + } + else if (Language.IsCommentStart(CurrentToken)) + { + // Output tokens before parsing the comment. + AcceptMarkerTokenIfNecessary(); + if (SpanContext.ChunkGenerator is ExpressionChunkGenerator) + { + builder.Add(OutputTokensAsExpressionLiteral()); + } + else + { + builder.Add(OutputTokensAsStatementLiteral()); + } + + var comment = ParseRazorComment(); + builder.Add(comment); + } + } + + [Conditional("DEBUG")] + internal void Assert(CSharpKeyword expectedKeyword) + { + var result = CSharpTokenizer.GetTokenKeyword(CurrentToken); + Debug.Assert(CurrentToken.Kind == SyntaxKind.Keyword && + result.HasValue && + result.Value == expectedKeyword); + } + + protected internal bool At(CSharpKeyword keyword) + { + var result = CSharpTokenizer.GetTokenKeyword(CurrentToken); + return At(SyntaxKind.Keyword) && + result.HasValue && + result.Value == keyword; + } + + protected static Func IsSpacingToken(bool includeNewLines, bool includeComments) + { + return token => token.Kind == SyntaxKind.Whitespace || + (includeNewLines && token.Kind == SyntaxKind.NewLine) || + (includeComments && token.Kind == SyntaxKind.CSharpComment); } protected class Block @@ -2170,7 +2440,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy Start = start; } - public Block(CSharpToken token, SourceLocation start) + public Block(SyntaxToken token, SourceLocation start) : this(GetName(token), start) { } @@ -2178,11 +2448,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public string Name { get; set; } public SourceLocation Start { get; set; } - private static string GetName(CSharpToken token) + private static string GetName(SyntaxToken token) { - if (token.Type == CSharpTokenType.Keyword) + var result = CSharpTokenizer.GetTokenKeyword(token); + if (result.HasValue && token.Kind == SyntaxKind.Keyword) { - return CSharpLanguageCharacteristics.GetKeyword(token.Keyword.Value); + return CSharpLanguageCharacteristics.GetKeyword(result.Value); } return token.Content; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpLanguageCharacteristics.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpLanguageCharacteristics.cs index 70d4bfeda1..3a017b54de 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpLanguageCharacteristics.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpLanguageCharacteristics.cs @@ -3,64 +3,65 @@ using System.Collections.Generic; using System.Diagnostics; +using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax; namespace Microsoft.AspNetCore.Razor.Language.Legacy { - internal class CSharpLanguageCharacteristics : LanguageCharacteristics + internal class CSharpLanguageCharacteristics : LanguageCharacteristics { private static readonly CSharpLanguageCharacteristics _instance = new CSharpLanguageCharacteristics(); - private static Dictionary _tokenSamples = new Dictionary() + private static Dictionary _tokenSamples = new Dictionary() { - { CSharpTokenType.Arrow, "->" }, - { CSharpTokenType.Minus, "-" }, - { CSharpTokenType.Decrement, "--" }, - { CSharpTokenType.MinusAssign, "-=" }, - { CSharpTokenType.NotEqual, "!=" }, - { CSharpTokenType.Not, "!" }, - { CSharpTokenType.Modulo, "%" }, - { CSharpTokenType.ModuloAssign, "%=" }, - { CSharpTokenType.AndAssign, "&=" }, - { CSharpTokenType.And, "&" }, - { CSharpTokenType.DoubleAnd, "&&" }, - { CSharpTokenType.LeftParenthesis, "(" }, - { CSharpTokenType.RightParenthesis, ")" }, - { CSharpTokenType.Star, "*" }, - { CSharpTokenType.MultiplyAssign, "*=" }, - { CSharpTokenType.Comma, "," }, - { CSharpTokenType.Dot, "." }, - { CSharpTokenType.Slash, "/" }, - { CSharpTokenType.DivideAssign, "/=" }, - { CSharpTokenType.DoubleColon, "::" }, - { CSharpTokenType.Colon, ":" }, - { CSharpTokenType.Semicolon, ";" }, - { CSharpTokenType.QuestionMark, "?" }, - { CSharpTokenType.NullCoalesce, "??" }, - { CSharpTokenType.RightBracket, "]" }, - { CSharpTokenType.LeftBracket, "[" }, - { CSharpTokenType.XorAssign, "^=" }, - { CSharpTokenType.Xor, "^" }, - { CSharpTokenType.LeftBrace, "{" }, - { CSharpTokenType.OrAssign, "|=" }, - { CSharpTokenType.DoubleOr, "||" }, - { CSharpTokenType.Or, "|" }, - { CSharpTokenType.RightBrace, "}" }, - { CSharpTokenType.Tilde, "~" }, - { CSharpTokenType.Plus, "+" }, - { CSharpTokenType.PlusAssign, "+=" }, - { CSharpTokenType.Increment, "++" }, - { CSharpTokenType.LessThan, "<" }, - { CSharpTokenType.LessThanEqual, "<=" }, - { CSharpTokenType.LeftShift, "<<" }, - { CSharpTokenType.LeftShiftAssign, "<<=" }, - { CSharpTokenType.Assign, "=" }, - { CSharpTokenType.Equals, "==" }, - { CSharpTokenType.GreaterThan, ">" }, - { CSharpTokenType.GreaterThanEqual, ">=" }, - { CSharpTokenType.RightShift, ">>" }, - { CSharpTokenType.RightShiftAssign, ">>=" }, - { CSharpTokenType.Hash, "#" }, - { CSharpTokenType.Transition, "@" }, + { SyntaxKind.Arrow, "->" }, + { SyntaxKind.Minus, "-" }, + { SyntaxKind.Decrement, "--" }, + { SyntaxKind.MinusAssign, "-=" }, + { SyntaxKind.NotEqual, "!=" }, + { SyntaxKind.Not, "!" }, + { SyntaxKind.Modulo, "%" }, + { SyntaxKind.ModuloAssign, "%=" }, + { SyntaxKind.AndAssign, "&=" }, + { SyntaxKind.And, "&" }, + { SyntaxKind.DoubleAnd, "&&" }, + { SyntaxKind.LeftParenthesis, "(" }, + { SyntaxKind.RightParenthesis, ")" }, + { SyntaxKind.Star, "*" }, + { SyntaxKind.MultiplyAssign, "*=" }, + { SyntaxKind.Comma, "," }, + { SyntaxKind.Dot, "." }, + { SyntaxKind.Slash, "/" }, + { SyntaxKind.DivideAssign, "/=" }, + { SyntaxKind.DoubleColon, "::" }, + { SyntaxKind.Colon, ":" }, + { SyntaxKind.Semicolon, ";" }, + { SyntaxKind.QuestionMark, "?" }, + { SyntaxKind.NullCoalesce, "??" }, + { SyntaxKind.RightBracket, "]" }, + { SyntaxKind.LeftBracket, "[" }, + { SyntaxKind.XorAssign, "^=" }, + { SyntaxKind.Xor, "^" }, + { SyntaxKind.LeftBrace, "{" }, + { SyntaxKind.OrAssign, "|=" }, + { SyntaxKind.DoubleOr, "||" }, + { SyntaxKind.Or, "|" }, + { SyntaxKind.RightBrace, "}" }, + { SyntaxKind.Tilde, "~" }, + { SyntaxKind.Plus, "+" }, + { SyntaxKind.PlusAssign, "+=" }, + { SyntaxKind.Increment, "++" }, + { SyntaxKind.LessThan, "<" }, + { SyntaxKind.LessThanEqual, "<=" }, + { SyntaxKind.LeftShift, "<<" }, + { SyntaxKind.LeftShiftAssign, "<<=" }, + { SyntaxKind.Assign, "=" }, + { SyntaxKind.Equals, "==" }, + { SyntaxKind.GreaterThan, ">" }, + { SyntaxKind.GreaterThanEqual, ">=" }, + { SyntaxKind.RightShift, ">>" }, + { SyntaxKind.RightShiftAssign, ">>=" }, + { SyntaxKind.Hash, "#" }, + { SyntaxKind.Transition, "@" }, }; protected CSharpLanguageCharacteristics() @@ -74,35 +75,35 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return new CSharpTokenizer(source); } - protected override CSharpToken CreateToken(string content, CSharpTokenType type, IReadOnlyList errors) + protected override SyntaxToken CreateToken(string content, SyntaxKind kind, IReadOnlyList errors) { - return new CSharpToken(content, type, errors); + return SyntaxFactory.Token(kind, content, errors); } - public override string GetSample(CSharpTokenType type) + public override string GetSample(SyntaxKind kind) { string sample; - if (!_tokenSamples.TryGetValue(type, out sample)) + if (!_tokenSamples.TryGetValue(kind, out sample)) { - switch (type) + switch (kind) { - case CSharpTokenType.Identifier: + case SyntaxKind.Identifier: return Resources.CSharpToken_Identifier; - case CSharpTokenType.Keyword: + case SyntaxKind.Keyword: return Resources.CSharpToken_Keyword; - case CSharpTokenType.IntegerLiteral: + case SyntaxKind.IntegerLiteral: return Resources.CSharpToken_IntegerLiteral; - case CSharpTokenType.NewLine: + case SyntaxKind.NewLine: return Resources.CSharpToken_Newline; - case CSharpTokenType.WhiteSpace: + case SyntaxKind.Whitespace: return Resources.CSharpToken_Whitespace; - case CSharpTokenType.Comment: + case SyntaxKind.CSharpComment: return Resources.CSharpToken_Comment; - case CSharpTokenType.RealLiteral: + case SyntaxKind.RealLiteral: return Resources.CSharpToken_RealLiteral; - case CSharpTokenType.CharacterLiteral: + case SyntaxKind.CharacterLiteral: return Resources.CSharpToken_CharacterLiteral; - case CSharpTokenType.StringLiteral: + case SyntaxKind.StringLiteral: return Resources.CSharpToken_StringLiteral; default: return Resources.Token_Unknown; @@ -111,59 +112,59 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return sample; } - public override CSharpToken CreateMarkerToken() + public override SyntaxToken CreateMarkerToken() { - return new CSharpToken(string.Empty, CSharpTokenType.Unknown); + return SyntaxFactory.Token(SyntaxKind.Marker, string.Empty); } - public override CSharpTokenType GetKnownTokenType(KnownTokenType type) + public override SyntaxKind GetKnownTokenType(KnownTokenType type) { switch (type) { case KnownTokenType.Identifier: - return CSharpTokenType.Identifier; + return SyntaxKind.Identifier; case KnownTokenType.Keyword: - return CSharpTokenType.Keyword; + return SyntaxKind.Keyword; case KnownTokenType.NewLine: - return CSharpTokenType.NewLine; - case KnownTokenType.WhiteSpace: - return CSharpTokenType.WhiteSpace; + return SyntaxKind.NewLine; + case KnownTokenType.Whitespace: + return SyntaxKind.Whitespace; case KnownTokenType.Transition: - return CSharpTokenType.Transition; + return SyntaxKind.Transition; case KnownTokenType.CommentStart: - return CSharpTokenType.RazorCommentTransition; + return SyntaxKind.RazorCommentTransition; case KnownTokenType.CommentStar: - return CSharpTokenType.RazorCommentStar; + return SyntaxKind.RazorCommentStar; case KnownTokenType.CommentBody: - return CSharpTokenType.RazorComment; + return SyntaxKind.RazorCommentLiteral; default: - return CSharpTokenType.Unknown; + return SyntaxKind.Marker; } } - public override CSharpTokenType FlipBracket(CSharpTokenType bracket) + public override SyntaxKind FlipBracket(SyntaxKind bracket) { switch (bracket) { - case CSharpTokenType.LeftBrace: - return CSharpTokenType.RightBrace; - case CSharpTokenType.LeftBracket: - return CSharpTokenType.RightBracket; - case CSharpTokenType.LeftParenthesis: - return CSharpTokenType.RightParenthesis; - case CSharpTokenType.LessThan: - return CSharpTokenType.GreaterThan; - case CSharpTokenType.RightBrace: - return CSharpTokenType.LeftBrace; - case CSharpTokenType.RightBracket: - return CSharpTokenType.LeftBracket; - case CSharpTokenType.RightParenthesis: - return CSharpTokenType.LeftParenthesis; - case CSharpTokenType.GreaterThan: - return CSharpTokenType.LessThan; + case SyntaxKind.LeftBrace: + return SyntaxKind.RightBrace; + case SyntaxKind.LeftBracket: + return SyntaxKind.RightBracket; + case SyntaxKind.LeftParenthesis: + return SyntaxKind.RightParenthesis; + case SyntaxKind.LessThan: + return SyntaxKind.GreaterThan; + case SyntaxKind.RightBrace: + return SyntaxKind.LeftBrace; + case SyntaxKind.RightBracket: + return SyntaxKind.LeftBracket; + case SyntaxKind.RightParenthesis: + return SyntaxKind.LeftParenthesis; + case SyntaxKind.GreaterThan: + return SyntaxKind.LessThan; default: Debug.Fail("FlipBracket must be called with a bracket character"); - return CSharpTokenType.Unknown; + return SyntaxKind.Marker; } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpToken.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpToken.cs deleted file mode 100644 index c928f0880e..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpToken.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.Razor.Language.Legacy -{ - internal class CSharpToken : TokenBase - { - public CSharpToken( - string content, - CSharpTokenType type) - : base(content, type, RazorDiagnostic.EmptyArray) - { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - } - - public CSharpToken( - string content, - CSharpTokenType type, - IReadOnlyList errors) - : base(content, type, errors) - { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - } - - public CSharpKeyword? Keyword { get; set; } - - public override bool Equals(object obj) - { - var other = obj as CSharpToken; - return base.Equals(other) && - other.Keyword == Keyword; - } - - public override int GetHashCode() - { - // Hash code should include only immutable properties. - return base.GetHashCode(); - } - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpTokenType.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpTokenType.cs deleted file mode 100644 index 10f3478d6d..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpTokenType.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Razor.Language.Legacy -{ - internal enum CSharpTokenType - { - Unknown, - Identifier, - Keyword, - IntegerLiteral, - NewLine, - WhiteSpace, - Comment, - RealLiteral, - CharacterLiteral, - StringLiteral, - - // Operators - Arrow, - Minus, - Decrement, - MinusAssign, - NotEqual, - Not, - Modulo, - ModuloAssign, - AndAssign, - And, - DoubleAnd, - LeftParenthesis, - RightParenthesis, - Star, - MultiplyAssign, - Comma, - Dot, - Slash, - DivideAssign, - DoubleColon, - Colon, - Semicolon, - QuestionMark, - NullCoalesce, - RightBracket, - LeftBracket, - XorAssign, - Xor, - LeftBrace, - OrAssign, - DoubleOr, - Or, - RightBrace, - Tilde, - Plus, - PlusAssign, - Increment, - LessThan, - LessThanEqual, - LeftShift, - LeftShiftAssign, - Assign, - Equals, - GreaterThan, - GreaterThanEqual, - RightShift, - RightShiftAssign, - Hash, - Transition, - - // Razor specific - RazorCommentTransition, - RazorCommentStar, - RazorComment - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpTokenizer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpTokenizer.cs index f819e98a3a..21cd0dc00b 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpTokenizer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpTokenizer.cs @@ -5,12 +5,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax; namespace Microsoft.AspNetCore.Razor.Language.Legacy { - internal class CSharpTokenizer : Tokenizer + internal class CSharpTokenizer : Tokenizer { - private Dictionary> _operatorHandlers; + private Dictionary> _operatorHandlers; private static readonly Dictionary _keywords = new Dictionary(StringComparer.Ordinal) { @@ -100,31 +101,31 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { base.CurrentState = StartState; - _operatorHandlers = new Dictionary>() + _operatorHandlers = new Dictionary>() { { '-', MinusOperator }, { '<', LessThanOperator }, { '>', GreaterThanOperator }, - { '&', CreateTwoCharOperatorHandler(CSharpTokenType.And, '=', CSharpTokenType.AndAssign, '&', CSharpTokenType.DoubleAnd) }, - { '|', CreateTwoCharOperatorHandler(CSharpTokenType.Or, '=', CSharpTokenType.OrAssign, '|', CSharpTokenType.DoubleOr) }, - { '+', CreateTwoCharOperatorHandler(CSharpTokenType.Plus, '=', CSharpTokenType.PlusAssign, '+', CSharpTokenType.Increment) }, - { '=', CreateTwoCharOperatorHandler(CSharpTokenType.Assign, '=', CSharpTokenType.Equals, '>', CSharpTokenType.GreaterThanEqual) }, - { '!', CreateTwoCharOperatorHandler(CSharpTokenType.Not, '=', CSharpTokenType.NotEqual) }, - { '%', CreateTwoCharOperatorHandler(CSharpTokenType.Modulo, '=', CSharpTokenType.ModuloAssign) }, - { '*', CreateTwoCharOperatorHandler(CSharpTokenType.Star, '=', CSharpTokenType.MultiplyAssign) }, - { ':', CreateTwoCharOperatorHandler(CSharpTokenType.Colon, ':', CSharpTokenType.DoubleColon) }, - { '?', CreateTwoCharOperatorHandler(CSharpTokenType.QuestionMark, '?', CSharpTokenType.NullCoalesce) }, - { '^', CreateTwoCharOperatorHandler(CSharpTokenType.Xor, '=', CSharpTokenType.XorAssign) }, - { '(', () => CSharpTokenType.LeftParenthesis }, - { ')', () => CSharpTokenType.RightParenthesis }, - { '{', () => CSharpTokenType.LeftBrace }, - { '}', () => CSharpTokenType.RightBrace }, - { '[', () => CSharpTokenType.LeftBracket }, - { ']', () => CSharpTokenType.RightBracket }, - { ',', () => CSharpTokenType.Comma }, - { ';', () => CSharpTokenType.Semicolon }, - { '~', () => CSharpTokenType.Tilde }, - { '#', () => CSharpTokenType.Hash } + { '&', CreateTwoCharOperatorHandler(SyntaxKind.And, '=', SyntaxKind.AndAssign, '&', SyntaxKind.DoubleAnd) }, + { '|', CreateTwoCharOperatorHandler(SyntaxKind.Or, '=', SyntaxKind.OrAssign, '|', SyntaxKind.DoubleOr) }, + { '+', CreateTwoCharOperatorHandler(SyntaxKind.Plus, '=', SyntaxKind.PlusAssign, '+', SyntaxKind.Increment) }, + { '=', CreateTwoCharOperatorHandler(SyntaxKind.Assign, '=', SyntaxKind.Equals, '>', SyntaxKind.GreaterThanEqual) }, + { '!', CreateTwoCharOperatorHandler(SyntaxKind.Not, '=', SyntaxKind.NotEqual) }, + { '%', CreateTwoCharOperatorHandler(SyntaxKind.Modulo, '=', SyntaxKind.ModuloAssign) }, + { '*', CreateTwoCharOperatorHandler(SyntaxKind.Star, '=', SyntaxKind.MultiplyAssign) }, + { ':', CreateTwoCharOperatorHandler(SyntaxKind.Colon, ':', SyntaxKind.DoubleColon) }, + { '?', CreateTwoCharOperatorHandler(SyntaxKind.QuestionMark, '?', SyntaxKind.NullCoalesce) }, + { '^', CreateTwoCharOperatorHandler(SyntaxKind.Xor, '=', SyntaxKind.XorAssign) }, + { '(', () => SyntaxKind.LeftParenthesis }, + { ')', () => SyntaxKind.RightParenthesis }, + { '{', () => SyntaxKind.LeftBrace }, + { '}', () => SyntaxKind.RightBrace }, + { '[', () => SyntaxKind.LeftBracket }, + { ']', () => SyntaxKind.RightBracket }, + { ',', () => SyntaxKind.Comma }, + { ';', () => SyntaxKind.Semicolon }, + { '~', () => SyntaxKind.Tilde }, + { '#', () => SyntaxKind.Hash } }; } @@ -132,11 +133,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy private new CSharpTokenizerState? CurrentState => (CSharpTokenizerState?)base.CurrentState; - public override CSharpTokenType RazorCommentType => CSharpTokenType.RazorComment; + public override SyntaxKind RazorCommentKind => SyntaxKind.RazorCommentLiteral; - public override CSharpTokenType RazorCommentTransitionType => CSharpTokenType.RazorCommentTransition; + public override SyntaxKind RazorCommentTransitionKind => SyntaxKind.RazorCommentTransition; - public override CSharpTokenType RazorCommentStarType => CSharpTokenType.RazorCommentStar; + public override SyntaxKind RazorCommentStarKind => SyntaxKind.RazorCommentStar; protected override StateResult Dispatch() { @@ -169,7 +170,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } // Optimize memory allocation by returning constants for the most frequent cases - protected override string GetTokenContent(CSharpTokenType type) + protected override string GetTokenContent(SyntaxKind type) { var tokenLength = Buffer.Length; @@ -177,7 +178,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { switch (type) { - case CSharpTokenType.IntegerLiteral: + case SyntaxKind.IntegerLiteral: switch (Buffer[0]) { case '0': @@ -202,13 +203,13 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return "9"; } break; - case CSharpTokenType.NewLine: + case SyntaxKind.NewLine: if (Buffer[0] == '\n') { return "\n"; } break; - case CSharpTokenType.WhiteSpace: + case SyntaxKind.Whitespace: if (Buffer[0] == ' ') { return " "; @@ -218,57 +219,57 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return "\t"; } break; - case CSharpTokenType.Minus: + case SyntaxKind.Minus: return "-"; - case CSharpTokenType.Not: + case SyntaxKind.Not: return "!"; - case CSharpTokenType.Modulo: + case SyntaxKind.Modulo: return "%"; - case CSharpTokenType.And: + case SyntaxKind.And: return "&"; - case CSharpTokenType.LeftParenthesis: + case SyntaxKind.LeftParenthesis: return "("; - case CSharpTokenType.RightParenthesis: + case SyntaxKind.RightParenthesis: return ")"; - case CSharpTokenType.Star: + case SyntaxKind.Star: return "*"; - case CSharpTokenType.Comma: + case SyntaxKind.Comma: return ","; - case CSharpTokenType.Dot: + case SyntaxKind.Dot: return "."; - case CSharpTokenType.Slash: + case SyntaxKind.Slash: return "/"; - case CSharpTokenType.Colon: + case SyntaxKind.Colon: return ":"; - case CSharpTokenType.Semicolon: + case SyntaxKind.Semicolon: return ";"; - case CSharpTokenType.QuestionMark: + case SyntaxKind.QuestionMark: return "?"; - case CSharpTokenType.RightBracket: + case SyntaxKind.RightBracket: return "]"; - case CSharpTokenType.LeftBracket: + case SyntaxKind.LeftBracket: return "["; - case CSharpTokenType.Xor: + case SyntaxKind.Xor: return "^"; - case CSharpTokenType.LeftBrace: + case SyntaxKind.LeftBrace: return "{"; - case CSharpTokenType.Or: + case SyntaxKind.Or: return "|"; - case CSharpTokenType.RightBrace: + case SyntaxKind.RightBrace: return "}"; - case CSharpTokenType.Tilde: + case SyntaxKind.Tilde: return "~"; - case CSharpTokenType.Plus: + case SyntaxKind.Plus: return "+"; - case CSharpTokenType.LessThan: + case SyntaxKind.LessThan: return "<"; - case CSharpTokenType.Assign: + case SyntaxKind.Assign: return "="; - case CSharpTokenType.GreaterThan: + case SyntaxKind.GreaterThan: return ">"; - case CSharpTokenType.Hash: + case SyntaxKind.Hash: return "#"; - case CSharpTokenType.Transition: + case SyntaxKind.Transition: return "@"; } @@ -277,53 +278,53 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { switch (type) { - case CSharpTokenType.NewLine: + case SyntaxKind.NewLine: return "\r\n"; - case CSharpTokenType.Arrow: + case SyntaxKind.Arrow: return "->"; - case CSharpTokenType.Decrement: + case SyntaxKind.Decrement: return "--"; - case CSharpTokenType.MinusAssign: + case SyntaxKind.MinusAssign: return "-="; - case CSharpTokenType.NotEqual: + case SyntaxKind.NotEqual: return "!="; - case CSharpTokenType.ModuloAssign: + case SyntaxKind.ModuloAssign: return "%="; - case CSharpTokenType.AndAssign: + case SyntaxKind.AndAssign: return "&="; - case CSharpTokenType.DoubleAnd: + case SyntaxKind.DoubleAnd: return "&&"; - case CSharpTokenType.MultiplyAssign: + case SyntaxKind.MultiplyAssign: return "*="; - case CSharpTokenType.DivideAssign: + case SyntaxKind.DivideAssign: return "/="; - case CSharpTokenType.DoubleColon: + case SyntaxKind.DoubleColon: return "::"; - case CSharpTokenType.NullCoalesce: + case SyntaxKind.NullCoalesce: return "??"; - case CSharpTokenType.XorAssign: + case SyntaxKind.XorAssign: return "^="; - case CSharpTokenType.OrAssign: + case SyntaxKind.OrAssign: return "|="; - case CSharpTokenType.DoubleOr: + case SyntaxKind.DoubleOr: return "||"; - case CSharpTokenType.PlusAssign: + case SyntaxKind.PlusAssign: return "+="; - case CSharpTokenType.Increment: + case SyntaxKind.Increment: return "++"; - case CSharpTokenType.LessThanEqual: + case SyntaxKind.LessThanEqual: return "<="; - case CSharpTokenType.LeftShift: + case SyntaxKind.LeftShift: return "<<"; - case CSharpTokenType.Equals: + case SyntaxKind.Equals: return "=="; - case CSharpTokenType.GreaterThanEqual: + case SyntaxKind.GreaterThanEqual: if (Buffer[0] == '=') { return "=>"; } return ">="; - case CSharpTokenType.RightShift: + case SyntaxKind.RightShift: return ">>"; @@ -333,9 +334,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { switch (type) { - case CSharpTokenType.LeftShiftAssign: + case SyntaxKind.LeftShiftAssign: return "<<="; - case CSharpTokenType.RightShiftAssign: + case SyntaxKind.RightShiftAssign: return ">>="; } } @@ -343,9 +344,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return base.GetTokenContent(type); } - protected override CSharpToken CreateToken(string content, CSharpTokenType type, IReadOnlyList errors) + protected override SyntaxToken CreateToken(string content, SyntaxKind kind, IReadOnlyList errors) { - return new CSharpToken(content, type, errors); + return SyntaxFactory.Token(kind, content, errors); } private StateResult Data() @@ -359,13 +360,13 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { TakeCurrent(); } - return Stay(EndToken(CSharpTokenType.NewLine)); + return Stay(EndToken(SyntaxKind.NewLine)); } else if (ParserHelpers.IsWhitespace(CurrentCharacter)) { // CSharp Spec §2.3.3 TakeUntil(c => !ParserHelpers.IsWhitespace(c)); - return Stay(EndToken(CSharpTokenType.WhiteSpace)); + return Stay(EndToken(SyntaxKind.Whitespace)); } else if (IsIdentifierStart(CurrentCharacter)) { @@ -390,7 +391,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { return RealLiteral(); } - return Stay(Single(CSharpTokenType.Dot)); + return Stay(Single(SyntaxKind.Dot)); case '/': TakeCurrent(); if (CurrentCharacter == '/') @@ -406,11 +407,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy else if (CurrentCharacter == '=') { TakeCurrent(); - return Stay(EndToken(CSharpTokenType.DivideAssign)); + return Stay(EndToken(SyntaxKind.DivideAssign)); } else { - return Stay(EndToken(CSharpTokenType.Slash)); + return Stay(EndToken(SyntaxKind.Slash)); } default: return Stay(EndToken(Operator())); @@ -429,78 +430,78 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { return Transition( CSharpTokenizerState.AfterRazorCommentTransition, - EndToken(CSharpTokenType.RazorCommentTransition)); + EndToken(SyntaxKind.RazorCommentTransition)); } else if (CurrentCharacter == '@') { // Could be escaped comment transition return Transition( CSharpTokenizerState.EscapedRazorCommentTransition, - EndToken(CSharpTokenType.Transition)); + EndToken(SyntaxKind.Transition)); } - return Stay(EndToken(CSharpTokenType.Transition)); + return Stay(EndToken(SyntaxKind.Transition)); } private StateResult EscapedRazorCommentTransition() { TakeCurrent(); - return Transition(CSharpTokenizerState.Data, EndToken(CSharpTokenType.Transition)); + return Transition(CSharpTokenizerState.Data, EndToken(SyntaxKind.Transition)); } - private CSharpTokenType Operator() + private SyntaxKind Operator() { var first = CurrentCharacter; TakeCurrent(); - Func handler; + Func handler; if (_operatorHandlers.TryGetValue(first, out handler)) { return handler(); } - return CSharpTokenType.Unknown; + return SyntaxKind.Marker; } - private CSharpTokenType LessThanOperator() + private SyntaxKind LessThanOperator() { if (CurrentCharacter == '=') { TakeCurrent(); - return CSharpTokenType.LessThanEqual; + return SyntaxKind.LessThanEqual; } - return CSharpTokenType.LessThan; + return SyntaxKind.LessThan; } - private CSharpTokenType GreaterThanOperator() + private SyntaxKind GreaterThanOperator() { if (CurrentCharacter == '=') { TakeCurrent(); - return CSharpTokenType.GreaterThanEqual; + return SyntaxKind.GreaterThanEqual; } - return CSharpTokenType.GreaterThan; + return SyntaxKind.GreaterThan; } - private CSharpTokenType MinusOperator() + private SyntaxKind MinusOperator() { if (CurrentCharacter == '>') { TakeCurrent(); - return CSharpTokenType.Arrow; + return SyntaxKind.Arrow; } else if (CurrentCharacter == '-') { TakeCurrent(); - return CSharpTokenType.Decrement; + return SyntaxKind.Decrement; } else if (CurrentCharacter == '=') { TakeCurrent(); - return CSharpTokenType.MinusAssign; + return SyntaxKind.MinusAssign; } - return CSharpTokenType.Minus; + return SyntaxKind.Minus; } - private Func CreateTwoCharOperatorHandler(CSharpTokenType typeIfOnlyFirst, char second, CSharpTokenType typeIfBoth) + private Func CreateTwoCharOperatorHandler(SyntaxKind typeIfOnlyFirst, char second, SyntaxKind typeIfBoth) { return () => { @@ -513,7 +514,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy }; } - private Func CreateTwoCharOperatorHandler(CSharpTokenType typeIfOnlyFirst, char option1, CSharpTokenType typeIfOption1, char option2, CSharpTokenType typeIfOption2) + private Func CreateTwoCharOperatorHandler(SyntaxKind typeIfOnlyFirst, char option1, SyntaxKind typeIfOption1, char option2, SyntaxKind typeIfOption2) { return () => { @@ -550,14 +551,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy RazorDiagnosticFactory.CreateParsing_UnterminatedStringLiteral( new SourceSpan(CurrentStart, contentLength: 1 /* end of file */))); } - return Transition(CSharpTokenizerState.Data, EndToken(CSharpTokenType.StringLiteral)); + return Transition(CSharpTokenizerState.Data, EndToken(SyntaxKind.StringLiteral)); } - private StateResult QuotedCharacterLiteral() => QuotedLiteral('\'', CSharpTokenType.CharacterLiteral); + private StateResult QuotedCharacterLiteral() => QuotedLiteral('\'', SyntaxKind.CharacterLiteral); - private StateResult QuotedStringLiteral() => QuotedLiteral('\"', CSharpTokenType.StringLiteral); + private StateResult QuotedStringLiteral() => QuotedLiteral('\"', SyntaxKind.StringLiteral); - private StateResult QuotedLiteral(char quote, CSharpTokenType literalType) + private StateResult QuotedLiteral(char quote, SyntaxKind literalType) { TakeUntil(c => c == '\\' || c == quote || ParserHelpers.IsNewLine(c)); if (CurrentCharacter == '\\') @@ -594,7 +595,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy RazorDiagnosticFactory.CreateParsing_BlockCommentNotTerminated( new SourceSpan(CurrentStart, contentLength: 1 /* end of file */))); - return Transition(CSharpTokenizerState.Data, EndToken(CSharpTokenType.Comment)); + return Transition(CSharpTokenizerState.Data, EndToken(SyntaxKind.CSharpComment)); } if (CurrentCharacter == '*') { @@ -602,7 +603,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy if (CurrentCharacter == '/') { TakeCurrent(); - return Transition(CSharpTokenizerState.Data, EndToken(CSharpTokenType.Comment)); + return Transition(CSharpTokenizerState.Data, EndToken(SyntaxKind.CSharpComment)); } } return Stay(); @@ -612,7 +613,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy private StateResult SingleLineComment() { TakeUntil(c => ParserHelpers.IsNewLine(c)); - return Stay(EndToken(CSharpTokenType.Comment)); + return Stay(EndToken(SyntaxKind.CSharpComment)); } // CSharp Spec §2.4.4 @@ -632,7 +633,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { TakeUntil(c => !IsHexDigit(c)); TakeIntegerSuffix(); - return Stay(EndToken(CSharpTokenType.IntegerLiteral)); + return Stay(EndToken(SyntaxKind.IntegerLiteral)); } private StateResult DecimalLiteral() @@ -650,7 +651,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy else { TakeIntegerSuffix(); - return Stay(EndToken(CSharpTokenType.IntegerLiteral)); + return Stay(EndToken(SyntaxKind.IntegerLiteral)); } } @@ -669,7 +670,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { TakeCurrent(); } - return Stay(EndToken(CSharpTokenType.RealLiteral)); + return Stay(EndToken(SyntaxKind.RealLiteral)); } // CSharp Spec §2.4.4.3 @@ -708,21 +709,18 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy Debug.Assert(IsIdentifierStart(CurrentCharacter)); TakeCurrent(); TakeUntil(c => !IsIdentifierPart(c)); - CSharpToken token = null; + SyntaxToken token = null; if (HaveContent) { CSharpKeyword keyword; - var type = CSharpTokenType.Identifier; + var type = SyntaxKind.Identifier; var tokenContent = Buffer.ToString(); if (_keywords.TryGetValue(tokenContent, out keyword)) { - type = CSharpTokenType.Keyword; + type = SyntaxKind.Keyword; } - - token = new CSharpToken(tokenContent, type) - { - Keyword = type == CSharpTokenType.Keyword ? (CSharpKeyword?)keyword : null, - }; + + token = SyntaxFactory.Token(type, tokenContent); Buffer.Clear(); CurrentErrors.Clear(); @@ -736,7 +734,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return Transition((int)state, result: null); } - private StateResult Transition(CSharpTokenizerState state, CSharpToken result) + private StateResult Transition(CSharpTokenizerState state, SyntaxToken result) { return Transition((int)state, result); } @@ -780,6 +778,16 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return (value >= '0' && value <= '9') || (value >= 'A' && value <= 'F') || (value >= 'a' && value <= 'f'); } + internal static CSharpKeyword? GetTokenKeyword(SyntaxToken token) + { + if (token != null && _keywords.TryGetValue(token.Content, out var keyword)) + { + return keyword; + } + + return null; + } + private enum CSharpTokenizerState { Data, diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/ChunkGeneratorContext.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/ChunkGeneratorContext.cs deleted file mode 100644 index b4a4a4782e..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/ChunkGeneratorContext.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Razor.Language.Legacy -{ - internal class ChunkGeneratorContext - { - public ChunkGeneratorContext( - string className, - string rootNamespace, - string sourceFile, - bool shouldGenerateLinePragmas) - { - SourceFile = shouldGenerateLinePragmas ? sourceFile : null; - RootNamespace = rootNamespace; - ClassName = className; - } - - public string SourceFile { get; internal set; } - - public string RootNamespace { get; } - - public string ClassName { get; } - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CodeBlockEditHandler.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CodeBlockEditHandler.cs index d5ce71c43e..8ecbd85906 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CodeBlockEditHandler.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/CodeBlockEditHandler.cs @@ -4,16 +4,17 @@ using System; using System.Collections.Generic; using System.Globalization; +using Microsoft.AspNetCore.Razor.Language.Syntax; namespace Microsoft.AspNetCore.Razor.Language.Legacy { internal class CodeBlockEditHandler : SpanEditHandler { - public CodeBlockEditHandler(Func> tokenizer) : base(tokenizer) + public CodeBlockEditHandler(Func> tokenizer) : base(tokenizer) { } - protected override PartialParseResultInternal CanAcceptChange(Span target, SourceChange change) + protected override PartialParseResultInternal CanAcceptChange(SyntaxNode target, SourceChange change) { if (IsAcceptableDeletion(target, change)) { @@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } // Internal for testing - internal static bool IsAcceptableReplacement(Span target, SourceChange change) + internal static bool IsAcceptableReplacement(SyntaxNode target, SourceChange change) { if (!change.IsReplace) { @@ -55,7 +56,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } // Internal for testing - internal static bool IsAcceptableDeletion(Span target, SourceChange change) + internal static bool IsAcceptableDeletion(SyntaxNode target, SourceChange change) { if (!change.IsDelete) { @@ -71,11 +72,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } // Internal for testing - internal static bool ModifiesInvalidContent(Span target, SourceChange change) + internal static bool ModifiesInvalidContent(SyntaxNode target, SourceChange change) { - var relativePosition = change.Span.AbsoluteIndex - target.Start.AbsoluteIndex; + var relativePosition = change.Span.AbsoluteIndex - target.Position; - if (target.Content.IndexOfAny(new[] { '{', '}' }, relativePosition, change.Span.Length) >= 0) + if (target.GetContent().IndexOfAny(new[] { '{', '}' }, relativePosition, change.Span.Length) >= 0) { return true; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/ConditionalAttributeCollapser.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/ConditionalAttributeCollapser.cs deleted file mode 100644 index 4fa89603dc..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/ConditionalAttributeCollapser.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Diagnostics; -using System.Text; - -namespace Microsoft.AspNetCore.Razor.Language.Legacy -{ - internal class ConditionalAttributeCollapser : MarkupRewriter - { - protected override bool CanRewrite(Block block) - { - var generator = block.ChunkGenerator as AttributeBlockChunkGenerator; - if (generator != null && block.Children.Count > 0) - { - // Perf: Avoid allocating an enumerator. - for (var i = 0; i < block.Children.Count; i++) - { - if (!IsLiteralAttributeValue(block.Children[i])) - { - return false; - } - } - - return true; - } - - return false; - } - - protected override SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block) - { - // Collect the content of this node - var builder = new StringBuilder(); - for (var i = 0; i < block.Children.Count; i++) - { - var childSpan = (Span)block.Children[i]; - builder.Append(childSpan.Content); - } - - // Create a new span containing this content - var span = new SpanBuilder(block.Children[0].Start); - - span.EditHandler = SpanEditHandler.CreateDefault(HtmlLanguageCharacteristics.Instance.TokenizeString); - Debug.Assert(block.Children.Count > 0); - var start = ((Span)block.Children[0]).Start; - FillSpan(span, start, builder.ToString()); - return span.Build(); - } - - private bool IsLiteralAttributeValue(SyntaxTreeNode node) - { - if (node.IsBlock) - { - return false; - } - - var span = node as Span; - Debug.Assert(span != null); - - return span != null && - (span.ChunkGenerator is LiteralAttributeChunkGenerator || - span.ChunkGenerator is MarkupChunkGenerator || - span.ChunkGenerator == SpanChunkGenerator.Null); - } - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveCSharpTokenizer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveCSharpTokenizer.cs index a9befaa302..59224e709f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveCSharpTokenizer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveCSharpTokenizer.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics; +using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax; namespace Microsoft.AspNetCore.Razor.Language.Legacy { @@ -17,11 +18,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy protected override StateResult Dispatch() { var result = base.Dispatch(); - if (result.Result != null && !_visitedFirstTokenStart && IsValidTokenType(result.Result.Type)) + if (result.Result != null && !_visitedFirstTokenStart && IsValidTokenType(result.Result.Kind)) { _visitedFirstTokenStart = true; } - else if (result.Result != null && _visitedFirstTokenStart && result.Result.Type == CSharpTokenType.NewLine) + else if (result.Result != null && _visitedFirstTokenStart && result.Result.Kind == SyntaxKind.NewLine) { _visitedFirstTokenLineEnd = true; } @@ -29,7 +30,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return result; } - public override CSharpToken NextToken() + public override SyntaxToken NextToken() { // Post-Condition: Buffer should be empty at the start of Next() Debug.Assert(Buffer.Length == 0); @@ -48,15 +49,15 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return token; } - private bool IsValidTokenType(CSharpTokenType type) + private bool IsValidTokenType(SyntaxKind kind) { - return type != CSharpTokenType.WhiteSpace && - type != CSharpTokenType.NewLine && - type != CSharpTokenType.Comment && - type != CSharpTokenType.RazorComment && - type != CSharpTokenType.RazorCommentStar && - type != CSharpTokenType.RazorCommentTransition && - type != CSharpTokenType.Transition; + return kind != SyntaxKind.Whitespace && + kind != SyntaxKind.NewLine && + kind != SyntaxKind.CSharpComment && + kind != SyntaxKind.RazorCommentLiteral && + kind != SyntaxKind.RazorCommentStar && + kind != SyntaxKind.RazorCommentTransition && + kind != SyntaxKind.Transition; } } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveChunkGenerator.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveChunkGenerator.cs deleted file mode 100644 index 19a6bad463..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveChunkGenerator.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright(c) .NET Foundation.All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.Extensions.Internal; - -namespace Microsoft.AspNetCore.Razor.Language.Legacy -{ - internal class DirectiveChunkGenerator : ParentChunkGenerator - { - private static readonly Type Type = typeof(DirectiveChunkGenerator); - private List _diagnostics; - - public DirectiveChunkGenerator(DirectiveDescriptor descriptor) - { - Descriptor = descriptor; - } - - public DirectiveDescriptor Descriptor { get; } - - public List Diagnostics - { - get - { - if (_diagnostics == null) - { - _diagnostics = new List(); - } - - return _diagnostics; - } - } - - public override void Accept(ParserVisitor visitor, Block block) - { - visitor.VisitDirectiveBlock(this, block); - } - - public override bool Equals(object obj) - { - var other = obj as DirectiveChunkGenerator; - return base.Equals(other) && - Enumerable.SequenceEqual(Diagnostics, other.Diagnostics) && - DirectiveDescriptorComparer.Default.Equals(Descriptor, other.Descriptor); - } - - public override int GetHashCode() - { - var combiner = HashCodeCombiner.Start(); - combiner.Add(base.GetHashCode()); - combiner.Add(Type); - - return combiner.CombinedHash; - } - - public override string ToString() - { - // This is used primarily at test time to show an identifiable representation of the chunk generator. - - var builder = new StringBuilder("Directive:{"); - builder.Append(Descriptor.Directive); - builder.Append(";"); - builder.Append(Descriptor.Kind); - builder.Append(";"); - builder.Append(Descriptor.Usage); - builder.Append("}"); - - if (Diagnostics.Count > 0) - { - builder.Append(" ["); - var ids = string.Join(", ", Diagnostics.Select(diagnostic => $"{diagnostic.Id}{diagnostic.Span}")); - builder.Append(ids); - builder.Append("]"); - } - - return builder.ToString(); - } - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveHtmlTokenizer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveHtmlTokenizer.cs index 034977c758..4921c8aa1e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveHtmlTokenizer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveHtmlTokenizer.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics; +using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax; namespace Microsoft.AspNetCore.Razor.Language.Legacy { @@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy protected override StateResult Dispatch() { var result = base.Dispatch(); - if (result.Result != null && IsValidTokenType(result.Result.Type)) + if (result.Result != null && IsValidTokenType(result.Result.Kind)) { _visitedFirstTokenStart = true; } @@ -24,7 +25,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return result; } - public override HtmlToken NextToken() + public override SyntaxToken NextToken() { // Post-Condition: Buffer should be empty at the start of Next() Debug.Assert(Buffer.Length == 0); @@ -43,14 +44,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return token; } - private bool IsValidTokenType(HtmlTokenType type) + private bool IsValidTokenType(SyntaxKind kind) { - return type != HtmlTokenType.WhiteSpace && - type != HtmlTokenType.NewLine && - type != HtmlTokenType.RazorComment && - type != HtmlTokenType.RazorCommentStar && - type != HtmlTokenType.RazorCommentTransition && - type != HtmlTokenType.Transition; + return kind != SyntaxKind.Whitespace && + kind != SyntaxKind.NewLine && + kind != SyntaxKind.RazorCommentLiteral && + kind != SyntaxKind.RazorCommentStar && + kind != SyntaxKind.RazorCommentTransition && + kind != SyntaxKind.Transition; } } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveTokenChunkGenerator.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveTokenChunkGenerator.cs index 69327ba6e2..ea1095bd5b 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveTokenChunkGenerator.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DirectiveTokenChunkGenerator.cs @@ -18,11 +18,6 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public DirectiveTokenDescriptor Descriptor { get; } - public override void Accept(ParserVisitor visitor, Span span) - { - visitor.VisitDirectiveToken(this, span); - } - public override bool Equals(object obj) { var other = obj as DirectiveTokenChunkGenerator; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DynamicAttributeBlockChunkGenerator.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DynamicAttributeBlockChunkGenerator.cs deleted file mode 100644 index 5b8c31efae..0000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/DynamicAttributeBlockChunkGenerator.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Globalization; - -namespace Microsoft.AspNetCore.Razor.Language.Legacy -{ - internal class DynamicAttributeBlockChunkGenerator : ParentChunkGenerator - { - public DynamicAttributeBlockChunkGenerator(LocationTagged prefix, int offset, int line, int col) - : this(prefix, new SourceLocation(offset, line, col)) - { - } - - public DynamicAttributeBlockChunkGenerator(LocationTagged prefix, SourceLocation valueStart) - { - Prefix = prefix; - ValueStart = valueStart; - } - - public LocationTagged Prefix { get; } - - public SourceLocation ValueStart { get; } - - public override void Accept(ParserVisitor visitor, Block block) - { - visitor.VisitDynamicAttributeBlock(this, block); - } - - public override void GenerateStartParentChunk(Block target, ChunkGeneratorContext context) - { - //var chunk = context.ChunkTreeBuilder.StartParentChunk(target); - //chunk.Start = ValueStart; - //chunk.Prefix = Prefix; - } - - public override void GenerateEndParentChunk(Block target, ChunkGeneratorContext context) - { - //context.ChunkTreeBuilder.EndParentChunk(); - } - - public override string ToString() - { - return string.Format(CultureInfo.CurrentCulture, "DynAttr:{0:F}", Prefix); - } - - public override bool Equals(object obj) - { - var other = obj as DynamicAttributeBlockChunkGenerator; - return other != null && - Equals(other.Prefix, Prefix); - } - - public override int GetHashCode() - { - return Prefix == null ? 0 : Prefix.GetHashCode(); - } - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/EditResult.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/EditResult.cs index 6aff8dea24..13dfb20a71 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/EditResult.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/EditResult.cs @@ -1,17 +1,19 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNetCore.Razor.Language.Syntax; + namespace Microsoft.AspNetCore.Razor.Language.Legacy { internal class EditResult { - public EditResult(PartialParseResultInternal result, SpanBuilder editedSpan) + public EditResult(PartialParseResultInternal result, SyntaxNode editedNode) { Result = result; - EditedSpan = editedSpan; + EditedNode = editedNode; } public PartialParseResultInternal Result { get; set; } - public SpanBuilder EditedSpan { get; set; } + public SyntaxNode EditedNode { get; set; } } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/ExpressionChunkGenerator.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/ExpressionChunkGenerator.cs index fd2ef3ab91..24eb488c1e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/ExpressionChunkGenerator.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/ExpressionChunkGenerator.cs @@ -5,35 +5,10 @@ using System; namespace Microsoft.AspNetCore.Razor.Language.Legacy { - internal class ExpressionChunkGenerator : ISpanChunkGenerator, IParentChunkGenerator + internal class ExpressionChunkGenerator : ISpanChunkGenerator { private static readonly int TypeHashCode = typeof(ExpressionChunkGenerator).GetHashCode(); - public void GenerateStartParentChunk(Block target, ChunkGeneratorContext context) - { - //context.ChunkTreeBuilder.StartParentChunk(target); - } - - public void GenerateChunk(Span target, ChunkGeneratorContext context) - { - //context.ChunkTreeBuilder.AddExpressionChunk(target.Content, target); - } - - public void GenerateEndParentChunk(Block target, ChunkGeneratorContext context) - { - //context.ChunkTreeBuilder.EndParentChunk(); - } - - public void Accept(ParserVisitor visitor, Span span) - { - visitor.VisitExpressionSpan(this, span); - } - - public void Accept(ParserVisitor visitor, Block block) - { - visitor.VisitExpressionBlock(this, block); - } - public override string ToString() { return "Expr"; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlLanguageCharacteristics.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlLanguageCharacteristics.cs index 096ae84dc0..5efd2b80a5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlLanguageCharacteristics.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlLanguageCharacteristics.cs @@ -3,10 +3,11 @@ using System.Collections.Generic; using System.Diagnostics; +using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax; namespace Microsoft.AspNetCore.Razor.Language.Legacy { - internal class HtmlLanguageCharacteristics : LanguageCharacteristics + internal class HtmlLanguageCharacteristics : LanguageCharacteristics { private static readonly HtmlLanguageCharacteristics _instance = new HtmlLanguageCharacteristics(); @@ -19,47 +20,47 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy get { return _instance; } } - public override string GetSample(HtmlTokenType type) + public override string GetSample(SyntaxKind type) { switch (type) { - case HtmlTokenType.Text: + case SyntaxKind.Text: return Resources.HtmlToken_Text; - case HtmlTokenType.WhiteSpace: + case SyntaxKind.Whitespace: return Resources.HtmlToken_WhiteSpace; - case HtmlTokenType.NewLine: + case SyntaxKind.NewLine: return Resources.HtmlToken_NewLine; - case HtmlTokenType.OpenAngle: + case SyntaxKind.OpenAngle: return "<"; - case HtmlTokenType.Bang: + case SyntaxKind.Bang: return "!"; - case HtmlTokenType.ForwardSlash: + case SyntaxKind.ForwardSlash: return "/"; - case HtmlTokenType.QuestionMark: + case SyntaxKind.QuestionMark: return "?"; - case HtmlTokenType.DoubleHyphen: + case SyntaxKind.DoubleHyphen: return "--"; - case HtmlTokenType.LeftBracket: + case SyntaxKind.LeftBracket: return "["; - case HtmlTokenType.CloseAngle: + case SyntaxKind.CloseAngle: return ">"; - case HtmlTokenType.RightBracket: + case SyntaxKind.RightBracket: return "]"; - case HtmlTokenType.Equals: + case SyntaxKind.Equals: return "="; - case HtmlTokenType.DoubleQuote: + case SyntaxKind.DoubleQuote: return "\""; - case HtmlTokenType.SingleQuote: + case SyntaxKind.SingleQuote: return "'"; - case HtmlTokenType.Transition: + case SyntaxKind.Transition: return "@"; - case HtmlTokenType.Colon: + case SyntaxKind.Colon: return ":"; - case HtmlTokenType.RazorComment: + case SyntaxKind.RazorCommentLiteral: return Resources.HtmlToken_RazorComment; - case HtmlTokenType.RazorCommentStar: + case SyntaxKind.RazorCommentStar: return "*"; - case HtmlTokenType.RazorCommentTransition: + case SyntaxKind.RazorCommentTransition: return "@"; default: return Resources.Token_Unknown; @@ -71,57 +72,57 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return new HtmlTokenizer(source); } - public override HtmlTokenType FlipBracket(HtmlTokenType bracket) + public override SyntaxKind FlipBracket(SyntaxKind bracket) { switch (bracket) { - case HtmlTokenType.LeftBracket: - return HtmlTokenType.RightBracket; - case HtmlTokenType.OpenAngle: - return HtmlTokenType.CloseAngle; - case HtmlTokenType.RightBracket: - return HtmlTokenType.LeftBracket; - case HtmlTokenType.CloseAngle: - return HtmlTokenType.OpenAngle; + case SyntaxKind.LeftBracket: + return SyntaxKind.RightBracket; + case SyntaxKind.OpenAngle: + return SyntaxKind.CloseAngle; + case SyntaxKind.RightBracket: + return SyntaxKind.LeftBracket; + case SyntaxKind.CloseAngle: + return SyntaxKind.OpenAngle; default: Debug.Fail("FlipBracket must be called with a bracket character"); - return HtmlTokenType.Unknown; + return SyntaxKind.Marker; } } - public override HtmlToken CreateMarkerToken() + public override SyntaxToken CreateMarkerToken() { - return new HtmlToken(string.Empty, HtmlTokenType.Unknown); + return SyntaxFactory.Token(SyntaxKind.Marker, string.Empty); } - public override HtmlTokenType GetKnownTokenType(KnownTokenType type) + public override SyntaxKind GetKnownTokenType(KnownTokenType type) { switch (type) { case KnownTokenType.CommentStart: - return HtmlTokenType.RazorCommentTransition; + return SyntaxKind.RazorCommentTransition; case KnownTokenType.CommentStar: - return HtmlTokenType.RazorCommentStar; + return SyntaxKind.RazorCommentStar; case KnownTokenType.CommentBody: - return HtmlTokenType.RazorComment; + return SyntaxKind.RazorCommentLiteral; case KnownTokenType.Identifier: - return HtmlTokenType.Text; + return SyntaxKind.Text; case KnownTokenType.Keyword: - return HtmlTokenType.Text; + return SyntaxKind.Text; case KnownTokenType.NewLine: - return HtmlTokenType.NewLine; + return SyntaxKind.NewLine; case KnownTokenType.Transition: - return HtmlTokenType.Transition; - case KnownTokenType.WhiteSpace: - return HtmlTokenType.WhiteSpace; + return SyntaxKind.Transition; + case KnownTokenType.Whitespace: + return SyntaxKind.Whitespace; default: - return HtmlTokenType.Unknown; + return SyntaxKind.Marker; } } - protected override HtmlToken CreateToken(string content, HtmlTokenType type, IReadOnlyList errors) + protected override SyntaxToken CreateToken(string content, SyntaxKind kind, IReadOnlyList errors) { - return new HtmlToken(content, type, errors); + return SyntaxFactory.Token(kind, content, errors); } } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlMarkupParser.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlMarkupParser.cs index 2408c552dd..f06c8db1bc 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlMarkupParser.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlMarkupParser.cs @@ -5,22 +5,27 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax; namespace Microsoft.AspNetCore.Razor.Language.Legacy { - internal class HtmlMarkupParser : TokenizerBackedParser + internal class HtmlMarkupParser : TokenizerBackedParser { private const string ScriptTagName = "script"; - private static readonly HtmlToken[] nonAllowedHtmlCommentEnding = new[] { HtmlToken.Hyphen, new HtmlToken("!", HtmlTokenType.Bang), new HtmlToken("<", HtmlTokenType.OpenAngle) }; - private static readonly HtmlToken[] singleHyphenArray = new[] { HtmlToken.Hyphen }; + private static readonly SyntaxToken[] nonAllowedHtmlCommentEnding = new[] + { + SyntaxFactory.Token(SyntaxKind.Text, "-"), + SyntaxFactory.Token(SyntaxKind.Bang, "!"), + SyntaxFactory.Token(SyntaxKind.OpenAngle, "<"), + }; private static readonly char[] ValidAfterTypeAttributeNameCharacters = { ' ', '\t', '\r', '\n', '\f', '=' }; private SourceLocation _lastTagStart = SourceLocation.Zero; - private HtmlToken _bufferedOpenAngle; + private SyntaxToken _bufferedOpenAngle; //From http://dev.w3.org/html5/spec/Overview.html#elements-0 - private ISet _voidElements = new HashSet(StringComparer.OrdinalIgnoreCase) + private readonly ISet _voidElements = new HashSet(StringComparer.OrdinalIgnoreCase) { "area", "base", @@ -45,7 +50,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { } - public ParserBase CodeParser { get; set; } + public CSharpCodeParser CodeParser { get; set; } public ISet VoidElements { @@ -59,44 +64,75 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy get { return CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; } } - protected override bool TokenTypeEquals(HtmlTokenType x, HtmlTokenType y) => x == y; - - public override void BuildSpan(SpanBuilder span, SourceLocation start, string content) + // Special tags include " - if (CurrentToken.Type != HtmlTokenType.DoubleHyphen) + if (CurrentToken.Kind != SyntaxKind.DoubleHyphen) { return false; } // Check condition 2.1 - if (NextIs(HtmlTokenType.CloseAngle) || NextIs(next => IsHyphen(next) && NextIs(HtmlTokenType.CloseAngle))) + if (NextIs(SyntaxKind.CloseAngle) || NextIs(next => IsHyphen(next) && NextIs(SyntaxKind.CloseAngle))) { return false; } @@ -610,15 +811,15 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy var isValidComment = false; LookaheadUntil((token, prevTokens) => { - if (token.Type == HtmlTokenType.DoubleHyphen) + if (token.Kind == SyntaxKind.DoubleHyphen) { - if (NextIs(HtmlTokenType.CloseAngle)) + if (NextIs(SyntaxKind.CloseAngle)) { // Check condition 2.3: We're at the end of a comment. Check to make sure the text ending is allowed. isValidComment = !IsCommentContentEndingInvalid(prevTokens); return true; } - else if (NextIs(ns => IsHyphen(ns) && NextIs(HtmlTokenType.CloseAngle))) + else if (NextIs(ns => IsHyphen(ns) && NextIs(SyntaxKind.CloseAngle))) { // Check condition 2.3: we're at the end of a comment, which has an extra dash. // Need to treat the dash as part of the content and check the ending. @@ -627,17 +828,17 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy isValidComment = true; return true; } - else if (NextIs(ns => ns.Type == HtmlTokenType.Bang && NextIs(HtmlTokenType.CloseAngle))) + else if (NextIs(ns => ns.Kind == SyntaxKind.Bang && NextIs(SyntaxKind.CloseAngle))) { // This is condition 2.2.3 isValidComment = false; return true; } } - else if (token.Type == HtmlTokenType.OpenAngle) + else if (token.Kind == SyntaxKind.OpenAngle) { // Checking condition 2.2.1 - if (NextIs(ns => ns.Type == HtmlTokenType.Bang && NextIs(HtmlTokenType.DoubleHyphen))) + if (NextIs(ns => ns.Kind == SyntaxKind.Bang && NextIs(SyntaxKind.DoubleHyphen))) { isValidComment = false; return true; @@ -653,13 +854,13 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy /// /// Verifies, that the sequence doesn't end with the "<!-" HtmlTokens. Note, the first token is an opening bracket token /// - internal static bool IsCommentContentEndingInvalid(IEnumerable sequence) + internal static bool IsCommentContentEndingInvalid(IEnumerable sequence) { var reversedSequence = sequence.Reverse(); var index = 0; foreach (var item in reversedSequence) { - if (!item.Equals(nonAllowedHtmlCommentEnding[index++])) + if (!item.IsEquivalentTo(nonAllowedHtmlCommentEnding[index++])) { return false; } @@ -673,15 +874,15 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return false; } - private bool CData() + private bool TryParseCData(in SyntaxListBuilder builder) { - if (CurrentToken.Type == HtmlTokenType.Text && string.Equals(CurrentToken.Content, "cdata", StringComparison.OrdinalIgnoreCase)) + if (CurrentToken.Kind == SyntaxKind.Text && string.Equals(CurrentToken.Content, "cdata", StringComparison.OrdinalIgnoreCase)) { if (AcceptAndMoveNext()) { - if (CurrentToken.Type == HtmlTokenType.LeftBracket) + if (CurrentToken.Kind == SyntaxKind.LeftBracket) { - return AcceptUntilAll(HtmlTokenType.RightBracket, HtmlTokenType.RightBracket, HtmlTokenType.CloseAngle); + return AcceptTokenUntilAll(builder, SyntaxKind.RightBracket, SyntaxKind.RightBracket, SyntaxKind.CloseAngle); } } } @@ -689,701 +890,65 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return false; } - private bool EndTag(SourceLocation tagStart, - Stack> tags, - IDisposable tagBlockWrapper) + private bool TryParseXmlPI(in SyntaxListBuilder builder) { - // Accept "/" and move next - Assert(HtmlTokenType.ForwardSlash); - var forwardSlash = CurrentToken; - if (!NextToken()) - { - Accept(_bufferedOpenAngle); - Accept(forwardSlash); - return false; - } - else - { - var tagName = string.Empty; - HtmlToken bangToken = null; - - if (At(HtmlTokenType.Bang)) - { - bangToken = CurrentToken; - - var nextToken = Lookahead(count: 1); - - if (nextToken != null && nextToken.Type == HtmlTokenType.Text) - { - tagName = "!" + nextToken.Content; - } - } - else if (At(HtmlTokenType.Text)) - { - tagName = CurrentToken.Content; - } - - var matched = RemoveTag(tags, tagName, tagStart); - - if (tags.Count == 0 && - // Note tagName may contain a '!' escape character. This ensures doesn't match here. - // tags are treated like any other escaped HTML end tag. - string.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase) && - matched) - { - return EndTextTag(forwardSlash, tagBlockWrapper); - } - Accept(_bufferedOpenAngle); - Accept(forwardSlash); - - OptionalBangEscape(); - - AcceptUntil(HtmlTokenType.CloseAngle); - - // Accept the ">" - return Optional(HtmlTokenType.CloseAngle); - } - } - - private void RecoverTextTag() - { - // We don't want to skip-to and parse because there shouldn't be anything in the body of text tags. - AcceptUntil(HtmlTokenType.CloseAngle, HtmlTokenType.NewLine); - - // Include the close angle in the text tag block if it's there, otherwise just move on - Optional(HtmlTokenType.CloseAngle); - } - - private bool EndTextTag(HtmlToken solidus, IDisposable tagBlockWrapper) - { - Accept(_bufferedOpenAngle); - Accept(solidus); - - var textLocation = CurrentStart; - Assert(HtmlTokenType.Text); + // Accept "?" + Assert(SyntaxKind.QuestionMark); AcceptAndMoveNext(); - - var seenCloseAngle = Optional(HtmlTokenType.CloseAngle); - - if (!seenCloseAngle) - { - Context.ErrorSink.OnError( - RazorDiagnosticFactory.CreateParsing_TextTagCannotContainAttributes( - new SourceSpan(textLocation, contentLength: 4 /* text */))); - - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; - RecoverTextTag(); - } - else - { - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None; - } - - Span.ChunkGenerator = SpanChunkGenerator.Null; - - CompleteTagBlockWithSpan(tagBlockWrapper, Span.EditHandler.AcceptedCharacters, SpanKindInternal.Transition); - - return seenCloseAngle; + return AcceptTokenUntilAll(builder, SyntaxKind.QuestionMark, SyntaxKind.CloseAngle); } - // Special tags include + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxAnnotation.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxAnnotation.cs new file mode 100644 index 0000000000..e639335475 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxAnnotation.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + /// + /// A SyntaxAnnotation is used to annotate syntax elements with additional information. + /// + /// Since syntax elements are immutable, annotating them requires creating new instances of them + /// with the annotations attached. + /// + [DebuggerDisplay("{GetDebuggerDisplay(), nq}")] + internal sealed class SyntaxAnnotation : IEquatable + { + // use a value identity instead of object identity so a deserialized instance matches the original instance. + private readonly long _id; + private static long s_nextId; + + // use a value identity instead of object identity so a deserialized instance matches the original instance. + public string Kind { get; } + public object Data { get; } + + public SyntaxAnnotation() + { + _id = System.Threading.Interlocked.Increment(ref s_nextId); + } + + public SyntaxAnnotation(string kind) + : this() + { + Kind = kind; + } + + public SyntaxAnnotation(string kind, object data) + : this(kind) + { + Data = data; + } + + private string GetDebuggerDisplay() + { + return string.Format("Annotation: Kind='{0}' Data='{1}'", this.Kind ?? "", this.Data ?? ""); + } + + public bool Equals(SyntaxAnnotation other) + { + return (object)other != null && _id == other._id; + } + + public static bool operator ==(SyntaxAnnotation left, SyntaxAnnotation right) + { + if ((object)left == (object)right) + { + return true; + } + + if ((object)left == null || (object)right == null) + { + return false; + } + + return left.Equals(right); + } + + public static bool operator !=(SyntaxAnnotation left, SyntaxAnnotation right) + { + if ((object)left == (object)right) + { + return false; + } + + if ((object)left == null || (object)right == null) + { + return true; + } + + return !left.Equals(right); + } + + public override bool Equals(object obj) + { + return Equals(obj as SyntaxAnnotation); + } + + public override int GetHashCode() + { + return _id.GetHashCode(); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxExtensions.cs new file mode 100644 index 0000000000..055180ca97 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxExtensions.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal partial class MarkupTextLiteralSyntax + { + protected override string GetDebuggerDisplay() + { + return string.Format("{0} [{1}]", base.GetDebuggerDisplay(), this.GetContent()); + } + } + + internal partial class MarkupEphemeralTextLiteralSyntax + { + protected override string GetDebuggerDisplay() + { + return string.Format("{0} [{1}]", base.GetDebuggerDisplay(), this.GetContent()); + } + } + + internal partial class CSharpStatementLiteralSyntax + { + protected override string GetDebuggerDisplay() + { + return string.Format("{0} [{1}]", base.GetDebuggerDisplay(), this.GetContent()); + } + } + + internal partial class CSharpExpressionLiteralSyntax + { + protected override string GetDebuggerDisplay() + { + return string.Format("{0} [{1}]", base.GetDebuggerDisplay(), this.GetContent()); + } + } + + internal partial class CSharpEphemeralTextLiteralSyntax + { + protected override string GetDebuggerDisplay() + { + return string.Format("{0} [{1}]", base.GetDebuggerDisplay(), this.GetContent()); + } + } + + internal partial class UnclassifiedTextLiteralSyntax + { + protected override string GetDebuggerDisplay() + { + return string.Format("{0} [{1}]", base.GetDebuggerDisplay(), this.GetContent()); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxFactory.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxFactory.cs new file mode 100644 index 0000000000..4e619799db --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxFactory.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal static partial class SyntaxFactory + { + public static SyntaxToken Token(SyntaxKind kind, params RazorDiagnostic[] diagnostics) + { + return Token(kind, content: string.Empty, diagnostics: diagnostics); + } + + public static SyntaxToken Token(SyntaxKind kind, string content, params RazorDiagnostic[] diagnostics) + { + return new SyntaxToken(InternalSyntax.SyntaxFactory.Token(kind, content), parent: null, position: 0); + } + + internal static SyntaxToken MissingToken(SyntaxKind kind, params RazorDiagnostic[] diagnostics) + { + return new SyntaxToken(InternalSyntax.SyntaxFactory.MissingToken(kind, diagnostics), parent: null, position: 0); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxKind.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxKind.cs new file mode 100644 index 0000000000..75dd554ab6 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxKind.cs @@ -0,0 +1,136 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal enum SyntaxKind : byte + { + #region Nodes + // Common + RazorDocument, + GenericBlock, + RazorComment, + RazorMetaCode, + RazorDirective, + RazorDirectiveBody, + UnclassifiedTextLiteral, + + // Markup + MarkupBlock, + MarkupTransition, + MarkupElement, + MarkupTagBlock, + MarkupTextLiteral, + MarkupEphemeralTextLiteral, + MarkupCommentBlock, + MarkupAttributeBlock, + MarkupMinimizedAttributeBlock, + MarkupLiteralAttributeValue, + MarkupDynamicAttributeValue, + MarkupTagHelperElement, + MarkupTagHelperStartTag, + MarkupTagHelperEndTag, + MarkupTagHelperAttribute, + MarkupMinimizedTagHelperAttribute, + MarkupTagHelperAttributeValue, + + // CSharp + CSharpStatement, + CSharpStatementBody, + CSharpExplicitExpression, + CSharpExplicitExpressionBody, + CSharpImplicitExpression, + CSharpImplicitExpressionBody, + CSharpCodeBlock, + CSharpTemplateBlock, + CSharpStatementLiteral, + CSharpExpressionLiteral, + CSharpEphemeralTextLiteral, + CSharpTransition, + #endregion + + #region Tokens + // Common + None, + Marker, + List, + Whitespace, + NewLine, + Colon, + QuestionMark, + RightBracket, + LeftBracket, + Equals, + Transition, + + // HTML + Text, + OpenAngle, + Bang, + ForwardSlash, + DoubleHyphen, + CloseAngle, + DoubleQuote, + SingleQuote, + + // CSharp literals + Identifier, + Keyword, + IntegerLiteral, + CSharpComment, + RealLiteral, + CharacterLiteral, + StringLiteral, + + // CSharp operators + Arrow, + Minus, + Decrement, + MinusAssign, + NotEqual, + Not, + Modulo, + ModuloAssign, + AndAssign, + And, + DoubleAnd, + LeftParenthesis, + RightParenthesis, + Star, + MultiplyAssign, + Comma, + Dot, + Slash, + DivideAssign, + DoubleColon, + Semicolon, + NullCoalesce, + XorAssign, + Xor, + LeftBrace, + OrAssign, + DoubleOr, + Or, + RightBrace, + Tilde, + Plus, + PlusAssign, + Increment, + LessThan, + LessThanEqual, + LeftShift, + LeftShiftAssign, + Assign, + GreaterThan, + GreaterThanEqual, + RightShift, + RightShiftAssign, + Hash, + + // Razor specific + RazorCommentLiteral, + RazorCommentStar, + RazorCommentTransition, + #endregion + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxList.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxList.cs new file mode 100644 index 0000000000..3b7d318e37 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxList.cs @@ -0,0 +1,123 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal abstract class SyntaxList : SyntaxNode + { + internal SyntaxList(InternalSyntax.SyntaxList green, SyntaxNode parent, int position) + : base(green, parent, position) + { + } + + public override TResult Accept(SyntaxVisitor visitor) + { + return visitor.Visit(this); + } + + public override void Accept(SyntaxVisitor visitor) + { + visitor.Visit(this); + } + + internal class WithTwoChildren : SyntaxList + { + private SyntaxNode _child0; + private SyntaxNode _child1; + + internal WithTwoChildren(InternalSyntax.SyntaxList green, SyntaxNode parent, int position) + : base(green, parent, position) + { + } + + internal override SyntaxNode GetNodeSlot(int index) + { + switch (index) + { + case 0: + return GetRedElement(ref _child0, 0); + case 1: + return GetRedElement(ref _child1, 1); + default: + return null; + } + } + + internal override SyntaxNode GetCachedSlot(int index) + { + switch (index) + { + case 0: + return _child0; + case 1: + return _child1; + default: + return null; + } + } + } + + internal class WithThreeChildren : SyntaxList + { + private SyntaxNode _child0; + private SyntaxNode _child1; + private SyntaxNode _child2; + + internal WithThreeChildren(InternalSyntax.SyntaxList green, SyntaxNode parent, int position) + : base(green, parent, position) + { + } + + internal override SyntaxNode GetNodeSlot(int index) + { + switch (index) + { + case 0: + return GetRedElement(ref _child0, 0); + case 1: + return GetRedElement(ref _child1, 1); + case 2: + return GetRedElement(ref _child2, 2); + default: + return null; + } + } + + internal override SyntaxNode GetCachedSlot(int index) + { + switch (index) + { + case 0: + return _child0; + case 1: + return _child1; + case 2: + return _child2; + default: + return null; + } + } + } + + internal class WithManyChildren : SyntaxList + { + private readonly ArrayElement[] _children; + + internal WithManyChildren(InternalSyntax.SyntaxList green, SyntaxNode parent, int position) + : base(green, parent, position) + { + _children = new ArrayElement[green.SlotCount]; + } + + internal override SyntaxNode GetNodeSlot(int index) + { + return this.GetRedElement(ref _children[index].Value, index); + } + + internal override SyntaxNode GetCachedSlot(int index) + { + return _children[index]; + } + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxListBuilder.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxListBuilder.cs new file mode 100644 index 0000000000..415b8352bb --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxListBuilder.cs @@ -0,0 +1,172 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal class SyntaxListBuilder + { + private ArrayElement[] _nodes; + + public int Count { get; private set; } + + public SyntaxListBuilder(int size) + { + _nodes = new ArrayElement[size]; + } + + public void Clear() + { + Count = 0; + } + + public void Add(SyntaxNode item) + { + AddInternal(item.Green); + } + + internal void AddInternal(GreenNode item) + { + if (item == null) + { + throw new ArgumentNullException(); + } + + if (_nodes == null || Count >= _nodes.Length) + { + Grow(Count == 0 ? 8 : _nodes.Length * 2); + } + + _nodes[Count++].Value = item; + } + + public void AddRange(SyntaxNode[] items) + { + AddRange(items, 0, items.Length); + } + + public void AddRange(SyntaxNode[] items, int offset, int length) + { + if (_nodes == null || Count + length > _nodes.Length) + { + Grow(Count + length); + } + + for (int i = offset, j = Count; i < offset + length; ++i, ++j) + { + _nodes[j].Value = items[i].Green; + } + + var start = Count; + Count += length; + Validate(start, Count); + } + + [Conditional("DEBUG")] + private void Validate(int start, int end) + { + for (var i = start; i < end; i++) + { + if (_nodes[i].Value == null) + { + throw new ArgumentException("Cannot add a null node."); + } + } + } + + public void AddRange(SyntaxList list) + { + AddRange(list, 0, list.Count); + } + + public void AddRange(SyntaxList list, int offset, int count) + { + if (_nodes == null || Count + count > _nodes.Length) + { + Grow(Count + count); + } + + var dst = Count; + for (int i = offset, limit = offset + count; i < limit; i++) + { + _nodes[dst].Value = list.ItemInternal(i).Green; + dst++; + } + + var start = Count; + Count += count; + Validate(start, Count); + } + + public void AddRange(SyntaxList list) where TNode : SyntaxNode + { + AddRange(list, 0, list.Count); + } + + public void AddRange(SyntaxList list, int offset, int count) where TNode : SyntaxNode + { + AddRange(new SyntaxList(list.Node), offset, count); + } + + private void Grow(int size) + { + var tmp = new ArrayElement[size]; + Array.Copy(_nodes, tmp, _nodes.Length); + _nodes = tmp; + } + + public bool Any(SyntaxKind kind) + { + for (var i = 0; i < Count; i++) + { + if (_nodes[i].Value.Kind == kind) + { + return true; + } + } + + return false; + } + + internal GreenNode ToListNode() + { + switch (Count) + { + case 0: + return null; + case 1: + return _nodes[0].Value; + case 2: + return InternalSyntax.SyntaxList.List(_nodes[0].Value, _nodes[1].Value); + case 3: + return InternalSyntax.SyntaxList.List(_nodes[0].Value, _nodes[1].Value, _nodes[2].Value); + default: + var tmp = new ArrayElement[Count]; + for (var i = 0; i < Count; i++) + { + tmp[i].Value = _nodes[i].Value; + } + + return InternalSyntax.SyntaxList.List(tmp); + } + } + + public static implicit operator SyntaxList(SyntaxListBuilder builder) + { + if (builder == null) + { + return default(SyntaxList); + } + + return builder.ToList(); + } + + internal void RemoveLast() + { + Count -= 1; + _nodes[Count] = default(ArrayElement); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxListBuilderExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxListBuilderExtensions.cs new file mode 100644 index 0000000000..5b813c05b1 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxListBuilderExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal static class SyntaxListBuilderExtensions + { + public static SyntaxList ToList(this SyntaxListBuilder builder) + { + if (builder == null || builder.Count == 0) + { + return default(SyntaxList); + } + + return new SyntaxList(builder.ToListNode().CreateRed()); + } + + public static SyntaxList ToList(this SyntaxListBuilder builder) + where TNode : SyntaxNode + { + if (builder == null || builder.Count == 0) + { + return new SyntaxList(); + } + + return new SyntaxList(builder.ToListNode().CreateRed()); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxListBuilderOfT.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxListBuilderOfT.cs new file mode 100644 index 0000000000..8fcd9f2b3a --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxListBuilderOfT.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal readonly struct SyntaxListBuilder + where TNode : SyntaxNode + { + private readonly SyntaxListBuilder _builder; + + public SyntaxListBuilder(int size) + : this(new SyntaxListBuilder(size)) + { + } + + public static SyntaxListBuilder Create() + { + return new SyntaxListBuilder(8); + } + + internal SyntaxListBuilder(SyntaxListBuilder builder) + { + _builder = builder; + } + + public bool IsNull + { + get + { + return _builder == null; + } + } + + public int Count + { + get + { + return _builder.Count; + } + } + + public void Clear() + { + _builder.Clear(); + } + + public SyntaxListBuilder Add(TNode node) + { + _builder.Add(node); + return this; + } + + public void AddRange(TNode[] items, int offset, int length) + { + _builder.AddRange(items, offset, length); + } + + public void AddRange(SyntaxList nodes) + { + _builder.AddRange(nodes); + } + + public void AddRange(SyntaxList nodes, int offset, int length) + { + _builder.AddRange(nodes, offset, length); + } + + public bool Any(SyntaxKind kind) + { + return _builder.Any(kind); + } + + public SyntaxList ToList() + { + return _builder.ToList(); + } + + public static implicit operator SyntaxListBuilder(SyntaxListBuilder builder) + { + return builder._builder; + } + + public static implicit operator SyntaxList(SyntaxListBuilder builder) + { + if (builder._builder != null) + { + return builder.ToList(); + } + + return default(SyntaxList); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxListOfT.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxListOfT.cs new file mode 100644 index 0000000000..158e82859f --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxListOfT.cs @@ -0,0 +1,606 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal readonly struct SyntaxList : IReadOnlyList, IEquatable> + where TNode : SyntaxNode + { + public SyntaxList(SyntaxNode node) + { + Node = node; + } + + /// + /// Creates a singleton list of syntax nodes. + /// + /// The single element node. + public SyntaxList(TNode node) + : this((SyntaxNode)node) + { + } + + /// + /// Creates a list of syntax nodes. + /// + /// A sequence of element nodes. + public SyntaxList(IEnumerable nodes) + : this(CreateNode(nodes)) + { + } + + private static SyntaxNode CreateNode(IEnumerable nodes) + { + if (nodes == null) + { + return null; + } + + var builder = (nodes is ICollection collection) ? new SyntaxListBuilder(collection.Count) : SyntaxListBuilder.Create(); + + foreach (var node in nodes) + { + builder.Add(node); + } + + return builder.ToList().Node; + } + + internal SyntaxNode Node { get; } + + /// + /// The number of nodes in the list. + /// + public int Count + { + get + { + return Node == null ? 0 : (Node.IsList ? Node.SlotCount : 1); + } + } + + /// + /// Gets the node at the specified index. + /// + /// The zero-based index of the node to get or set. + /// The node at the specified index. + public TNode this[int index] + { + get + { + if (Node != null) + { + if (Node.IsList) + { + if (unchecked((uint)index < (uint)Node.SlotCount)) + { + return (TNode)Node.GetNodeSlot(index); + } + } + else if (index == 0) + { + return (TNode)Node; + } + } + throw new ArgumentOutOfRangeException(); + } + } + + internal SyntaxNode ItemInternal(int index) + { + if (Node.IsList) + { + return Node.GetNodeSlot(index); + } + + Debug.Assert(index == 0); + return Node; + } + + /// + /// The absolute span of the list elements in characters, including the leading and trailing trivia of the first and last elements. + /// + public TextSpan FullSpan + { + get + { + if (Count == 0) + { + return default(TextSpan); + } + else + { + return TextSpan.FromBounds(this[0].FullSpan.Start, this[Count - 1].FullSpan.End); + } + } + } + + /// + /// The absolute span of the list elements in characters, not including the leading and trailing trivia of the first and last elements. + /// + public TextSpan Span + { + get + { + if (Count == 0) + { + return default(TextSpan); + } + else + { + return TextSpan.FromBounds(this[0].Span.Start, this[Count - 1].Span.End); + } + } + } + + /// + /// Returns the string representation of the nodes in this list, not including + /// the first node's leading trivia and the last node's trailing trivia. + /// + /// + /// The string representation of the nodes in this list, not including + /// the first node's leading trivia and the last node's trailing trivia. + /// + public override string ToString() + { + return Node != null ? Node.ToString() : string.Empty; + } + + /// + /// Returns the full string representation of the nodes in this list including + /// the first node's leading trivia and the last node's trailing trivia. + /// + /// + /// The full string representation of the nodes in this list including + /// the first node's leading trivia and the last node's trailing trivia. + /// + public string ToFullString() + { + return Node != null ? Node.ToFullString() : string.Empty; + } + + /// + /// Creates a new list with the specified node added at the end. + /// + /// The node to add. + public SyntaxList Add(TNode node) + { + return Insert(Count, node); + } + + /// + /// Creates a new list with the specified nodes added at the end. + /// + /// The nodes to add. + public SyntaxList AddRange(IEnumerable nodes) + { + return InsertRange(Count, nodes); + } + + /// + /// Creates a new list with the specified node inserted at the index. + /// + /// The index to insert at. + /// The node to insert. + public SyntaxList Insert(int index, TNode node) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + return InsertRange(index, new[] { node }); + } + + /// + /// Creates a new list with the specified nodes inserted at the index. + /// + /// The index to insert at. + /// The nodes to insert. + public SyntaxList InsertRange(int index, IEnumerable nodes) + { + if (index < 0 || index > Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (nodes == null) + { + throw new ArgumentNullException(nameof(nodes)); + } + + var list = this.ToList(); + list.InsertRange(index, nodes); + + if (list.Count == 0) + { + return this; + } + else + { + return CreateList(list[0].Green, list); + } + } + + /// + /// Creates a new list with the element at specified index removed. + /// + /// The index of the element to remove. + public SyntaxList RemoveAt(int index) + { + if (index < 0 || index > Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + return Remove(this[index]); + } + + /// + /// Creates a new list with the element removed. + /// + /// The element to remove. + public SyntaxList Remove(TNode node) + { + return CreateList(this.Where(x => x != node).ToList()); + } + + /// + /// Creates a new list with the specified element replaced with the new node. + /// + /// The element to replace. + /// The new node. + public SyntaxList Replace(TNode nodeInList, TNode newNode) + { + return ReplaceRange(nodeInList, new[] { newNode }); + } + + /// + /// Creates a new list with the specified element replaced with new nodes. + /// + /// The element to replace. + /// The new nodes. + public SyntaxList ReplaceRange(TNode nodeInList, IEnumerable newNodes) + { + if (nodeInList == null) + { + throw new ArgumentNullException(nameof(nodeInList)); + } + + if (newNodes == null) + { + throw new ArgumentNullException(nameof(newNodes)); + } + + var index = IndexOf(nodeInList); + if (index >= 0 && index < Count) + { + var list = this.ToList(); + list.RemoveAt(index); + list.InsertRange(index, newNodes); + return CreateList(list); + } + else + { + throw new ArgumentException(nameof(nodeInList)); + } + } + + static SyntaxList CreateList(List items) + { + if (items.Count == 0) + { + return default(SyntaxList); + } + else + { + return CreateList(items[0].Green, items); + } + } + + static SyntaxList CreateList(GreenNode creator, List items) + { + if (items.Count == 0) + { + return default(SyntaxList); + } + + var newGreen = creator.CreateList(items.Select(n => n.Green)); + return new SyntaxList(newGreen.CreateRed()); + } + + /// + /// The first node in the list. + /// + public TNode First() + { + return this[0]; + } + + /// + /// The first node in the list or default if the list is empty. + /// + public TNode FirstOrDefault() + { + if (this.Any()) + { + return this[0]; + } + else + { + return null; + } + } + + /// + /// The last node in the list. + /// + public TNode Last() + { + return this[Count - 1]; + } + + /// + /// The last node in the list or default if the list is empty. + /// + public TNode LastOrDefault() + { + if (Any()) + { + return this[Count - 1]; + } + else + { + return null; + } + } + + /// + /// True if the list has at least one node. + /// + public bool Any() + { + Debug.Assert(Node == null || Count != 0); + return Node != null; + } + + // for debugging + private TNode[] Nodes + { + get { return this.ToArray(); } + } + + /// + /// Get's the enumerator for this list. + /// + public Enumerator GetEnumerator() + { + return new Enumerator(in this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + if (this.Any()) + { + return new EnumeratorImpl(this); + } + + return SpecializedCollections.EmptyEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + if (this.Any()) + { + return new EnumeratorImpl(this); + } + + return SpecializedCollections.EmptyEnumerator(); + } + + public static bool operator ==(SyntaxList left, SyntaxList right) + { + return left.Node == right.Node; + } + + public static bool operator !=(SyntaxList left, SyntaxList right) + { + return left.Node != right.Node; + } + + public bool Equals(SyntaxList other) + { + return Node == other.Node; + } + + public override bool Equals(object obj) + { + return obj is SyntaxList && Equals((SyntaxList)obj); + } + + public override int GetHashCode() + { + return Node?.GetHashCode() ?? 0; + } + + public static implicit operator SyntaxList(SyntaxList nodes) + { + return new SyntaxList(nodes.Node); + } + + public static implicit operator SyntaxList(SyntaxList nodes) + { + return new SyntaxList(nodes.Node); + } + + /// + /// The index of the node in this list, or -1 if the node is not in the list. + /// + public int IndexOf(TNode node) + { + var index = 0; + foreach (var child in this) + { + if (object.Equals(child, node)) + { + return index; + } + + index++; + } + + return -1; + } + + public int IndexOf(Func predicate) + { + var index = 0; + foreach (var child in this) + { + if (predicate(child)) + { + return index; + } + + index++; + } + + return -1; + } + + internal int IndexOf(SyntaxKind kind) + { + var index = 0; + foreach (var child in this) + { + if (child.Kind == kind) + { + return index; + } + + index++; + } + + return -1; + } + + public int LastIndexOf(TNode node) + { + for (var i = Count - 1; i >= 0; i--) + { + if (object.Equals(this[i], node)) + { + return i; + } + } + + return -1; + } + + public int LastIndexOf(Func predicate) + { + for (var i = Count - 1; i >= 0; i--) + { + if (predicate(this[i])) + { + return i; + } + } + + return -1; + } + + public struct Enumerator + { + private readonly SyntaxList _list; + private int _index; + + internal Enumerator(in SyntaxList list) + { + _list = list; + _index = -1; + } + + public bool MoveNext() + { + var newIndex = _index + 1; + if (newIndex < _list.Count) + { + _index = newIndex; + return true; + } + + return false; + } + + public TNode Current + { + get + { + return (TNode)_list.ItemInternal(_index); + } + } + + public void Reset() + { + _index = -1; + } + + public override bool Equals(object obj) + { + throw new NotSupportedException(); + } + + public override int GetHashCode() + { + throw new NotSupportedException(); + } + } + + private class EnumeratorImpl : IEnumerator + { + private Enumerator _e; + + internal EnumeratorImpl(in SyntaxList list) + { + _e = new Enumerator(in list); + } + + public bool MoveNext() + { + return _e.MoveNext(); + } + + public TNode Current + { + get + { + return _e.Current; + } + } + + void IDisposable.Dispose() + { + } + + object IEnumerator.Current + { + get + { + return _e.Current; + } + } + + void IEnumerator.Reset() + { + _e.Reset(); + } + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxNode.Iterators.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxNode.Iterators.cs new file mode 100644 index 0000000000..e619a75b2c --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxNode.Iterators.cs @@ -0,0 +1,126 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal abstract partial class SyntaxNode + { + private IEnumerable DescendantNodesImpl(TextSpan span, Func descendIntoChildren, bool includeSelf) + { + if (includeSelf && IsInSpan(in span, FullSpan)) + { + yield return this; + } + + using (var stack = new ChildSyntaxListEnumeratorStack(this, descendIntoChildren)) + { + while (stack.IsNotEmpty) + { + var nodeValue = stack.TryGetNextAsNodeInSpan(in span); + if (nodeValue != null) + { + // PERF: Push before yield return so that "nodeValue" is 'dead' after the yield + // and therefore doesn't need to be stored in the iterator state machine. This + // saves a field. + stack.PushChildren(nodeValue, descendIntoChildren); + + yield return nodeValue; + } + } + } + } + + private static bool IsInSpan(in TextSpan span, TextSpan childSpan) + { + return span.OverlapsWith(childSpan) + // special case for zero-width tokens (OverlapsWith never returns true for these) + || (childSpan.Length == 0 && span.IntersectsWith(childSpan)); + } + + private struct ChildSyntaxListEnumeratorStack : IDisposable + { + private static readonly ObjectPool StackPool = new ObjectPool(() => new ChildSyntaxList.Enumerator[16]); + + private ChildSyntaxList.Enumerator[] _stack; + private int _stackPtr; + + public ChildSyntaxListEnumeratorStack(SyntaxNode startingNode, Func descendIntoChildren) + { + if (descendIntoChildren == null || descendIntoChildren(startingNode)) + { + _stack = StackPool.Allocate(); + _stackPtr = 0; + _stack[0].InitializeFrom(startingNode); + } + else + { + _stack = null; + _stackPtr = -1; + } + } + + public bool IsNotEmpty { get { return _stackPtr >= 0; } } + + public bool TryGetNextInSpan(in TextSpan span, out SyntaxNode value) + { + while (_stack[_stackPtr].TryMoveNextAndGetCurrent(out value)) + { + if (IsInSpan(in span, value.FullSpan)) + { + return true; + } + } + + _stackPtr--; + return false; + } + + public SyntaxNode TryGetNextAsNodeInSpan(in TextSpan span) + { + SyntaxNode nodeValue; + while ((nodeValue = _stack[_stackPtr].TryMoveNextAndGetCurrentAsNode()) != null) + { + if (IsInSpan(in span, nodeValue.FullSpan)) + { + return nodeValue; + } + } + + _stackPtr--; + return null; + } + + public void PushChildren(SyntaxNode node) + { + if (++_stackPtr >= _stack.Length) + { + // Geometric growth + Array.Resize(ref _stack, checked(_stackPtr * 2)); + } + + _stack[_stackPtr].InitializeFrom(node); + } + + public void PushChildren(SyntaxNode node, Func descendIntoChildren) + { + if (descendIntoChildren == null || descendIntoChildren(node)) + { + PushChildren(node); + } + } + + public void Dispose() + { + // Return only reasonably-sized stacks to the pool. + if (_stack?.Length < 256) + { + Array.Clear(_stack, 0, _stack.Length); + StackPool.Free(_stack); + } + } + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxNode.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxNode.cs new file mode 100644 index 0000000000..2f58f13fe3 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxNode.cs @@ -0,0 +1,413 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Threading; + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + [DebuggerDisplay("{GetDebuggerDisplay(), nq}")] + internal abstract partial class SyntaxNode + { + public SyntaxNode(GreenNode green, SyntaxNode parent, int position) + { + Green = green; + Parent = parent; + Position = position; + } + + internal GreenNode Green { get; } + + public SyntaxNode Parent { get; } + + public int Position { get; } + + public int EndPosition => Position + FullWidth; + + public SyntaxKind Kind => Green.Kind; + + public int Width => Green.Width; + + public int FullWidth => Green.FullWidth; + + public int SpanStart => Position + Green.GetLeadingTriviaWidth(); + + public TextSpan FullSpan => new TextSpan(Position, Green.FullWidth); + + public TextSpan Span + { + get + { + // Start with the full span. + var start = Position; + var width = Green.FullWidth; + + // adjust for preceding trivia (avoid calling this twice, do not call Green.Width) + var precedingWidth = Green.GetLeadingTriviaWidth(); + start += precedingWidth; + width -= precedingWidth; + + // adjust for following trivia width + width -= Green.GetTrailingTriviaWidth(); + + Debug.Assert(width >= 0); + return new TextSpan(start, width); + } + } + + internal int SlotCount => Green.SlotCount; + + public bool IsList => Green.IsList; + + public bool IsMissing => Green.IsMissing; + + public bool IsToken => Green.IsToken; + + public bool IsTrivia => Green.IsTrivia; + + public bool HasLeadingTrivia + { + get + { + return GetLeadingTrivia().Count > 0; + } + } + + public bool HasTrailingTrivia + { + get + { + return GetTrailingTrivia().Count > 0; + } + } + + public bool ContainsDiagnostics => Green.ContainsDiagnostics; + + public bool ContainsAnnotations => Green.ContainsAnnotations; + + internal string SerializedValue => SyntaxSerializer.Serialize(this); + + public abstract TResult Accept(SyntaxVisitor visitor); + + public abstract void Accept(SyntaxVisitor visitor); + + internal abstract SyntaxNode GetNodeSlot(int index); + + internal abstract SyntaxNode GetCachedSlot(int index); + + internal SyntaxNode GetRed(ref SyntaxNode field, int slot) + { + var result = field; + + if (result == null) + { + var green = Green.GetSlot(slot); + if (green != null) + { + Interlocked.CompareExchange(ref field, green.CreateRed(this, GetChildPosition(slot)), null); + result = field; + } + } + + return result; + } + + // Special case of above function where slot = 0, does not need GetChildPosition + internal SyntaxNode GetRedAtZero(ref SyntaxNode field) + { + var result = field; + + if (result == null) + { + var green = Green.GetSlot(0); + if (green != null) + { + Interlocked.CompareExchange(ref field, green.CreateRed(this, Position), null); + result = field; + } + } + + return result; + } + + protected T GetRed(ref T field, int slot) where T : SyntaxNode + { + var result = field; + + if (result == null) + { + var green = Green.GetSlot(slot); + if (green != null) + { + Interlocked.CompareExchange(ref field, (T)green.CreateRed(this, this.GetChildPosition(slot)), null); + result = field; + } + } + + return result; + } + + // special case of above function where slot = 0, does not need GetChildPosition + protected T GetRedAtZero(ref T field) where T : SyntaxNode + { + var result = field; + + if (result == null) + { + var green = Green.GetSlot(0); + if (green != null) + { + Interlocked.CompareExchange(ref field, (T)green.CreateRed(this, Position), null); + result = field; + } + } + + return result; + } + + internal SyntaxNode GetRedElement(ref SyntaxNode element, int slot) + { + Debug.Assert(IsList); + + var result = element; + + if (result == null) + { + var green = Green.GetSlot(slot); + // passing list's parent + Interlocked.CompareExchange(ref element, green.CreateRed(Parent, GetChildPosition(slot)), null); + result = element; + } + + return result; + } + + internal virtual int GetChildPosition(int index) + { + var offset = 0; + var green = Green; + while (index > 0) + { + index--; + var prevSibling = GetCachedSlot(index); + if (prevSibling != null) + { + return prevSibling.EndPosition + offset; + } + var greenChild = green.GetSlot(index); + if (greenChild != null) + { + offset += greenChild.FullWidth; + } + } + + return Position + offset; + } + + public virtual SyntaxTriviaList GetLeadingTrivia() + { + var firstToken = GetFirstToken(); + return firstToken != null ? firstToken.GetLeadingTrivia() : default(SyntaxTriviaList); + } + + public virtual SyntaxTriviaList GetTrailingTrivia() + { + var lastToken = GetLastToken(); + return lastToken != null ? lastToken.GetTrailingTrivia() : default(SyntaxTriviaList); + } + + internal SyntaxToken GetFirstToken() + { + return ((SyntaxToken)GetFirstTerminal()); + } + + internal SyntaxToken GetLastToken() + { + return ((SyntaxToken)GetLastTerminal()); + } + + public SyntaxNode GetFirstTerminal() + { + var node = this; + + do + { + var foundChild = false; + for (int i = 0, n = node.SlotCount; i < n; i++) + { + var child = node.GetNodeSlot(i); + if (child != null) + { + node = child; + foundChild = true; + break; + } + } + + if (!foundChild) + { + return null; + } + } + while (node.SlotCount != 0); + + return node == this ? this : node; + } + + public SyntaxNode GetLastTerminal() + { + var node = this; + + do + { + SyntaxNode lastChild = null; + for (var i = node.SlotCount - 1; i >= 0; i--) + { + var child = node.GetNodeSlot(i); + if (child != null && child.FullWidth > 0) + { + lastChild = child; + break; + } + } + node = lastChild; + } while (node?.SlotCount > 0); + + return node; + } + + /// + /// The list of child nodes of this node, where each element is a SyntaxNode instance. + /// + public ChildSyntaxList ChildNodes() + { + return new ChildSyntaxList(this); + } + + /// + /// Gets a list of ancestor nodes + /// + public IEnumerable Ancestors() + { + return Parent? + .AncestorsAndSelf() ?? + Array.Empty(); + } + + /// + /// Gets a list of ancestor nodes (including this node) + /// + public IEnumerable AncestorsAndSelf() + { + for (var node = this; node != null; node = node.Parent) + { + yield return node; + } + } + + /// + /// Gets the first node of type TNode that matches the predicate. + /// + public TNode FirstAncestorOrSelf(Func predicate = null) + where TNode : SyntaxNode + { + for (var node = this; node != null; node = node.Parent) + { + if (node is TNode tnode && (predicate == null || predicate(tnode))) + { + return tnode; + } + } + + return default; + } + + /// + /// Gets a list of descendant nodes in prefix document order. + /// + /// An optional function that determines if the search descends into the argument node's children. + public IEnumerable DescendantNodes(Func descendIntoChildren = null) + { + return DescendantNodesImpl(FullSpan, descendIntoChildren, includeSelf: false); + } + + /// + /// Gets a list of descendant nodes (including this node) in prefix document order. + /// + /// An optional function that determines if the search descends into the argument node's children. + public IEnumerable DescendantNodesAndSelf(Func descendIntoChildren = null) + { + return DescendantNodesImpl(FullSpan, descendIntoChildren, includeSelf: true); + } + + protected internal SyntaxNode ReplaceCore( + IEnumerable nodes = null, + Func computeReplacementNode = null) + where TNode : SyntaxNode + { + return SyntaxReplacer.Replace(this, nodes, computeReplacementNode); + } + + protected internal SyntaxNode ReplaceNodeInListCore(SyntaxNode originalNode, IEnumerable replacementNodes) + { + return SyntaxReplacer.ReplaceNodeInList(this, originalNode, replacementNodes); + } + + protected internal SyntaxNode InsertNodesInListCore(SyntaxNode nodeInList, IEnumerable nodesToInsert, bool insertBefore) + { + return SyntaxReplacer.InsertNodeInList(this, nodeInList, nodesToInsert, insertBefore); + } + + public RazorDiagnostic[] GetDiagnostics() + { + return Green.GetDiagnostics(); + } + + public SyntaxAnnotation[] GetAnnotations() + { + return Green.GetAnnotations(); + } + + public bool IsEquivalentTo(SyntaxNode other) + { + if (this == other) + { + return true; + } + + if (other == null) + { + return false; + } + + return Green.IsEquivalentTo(other.Green); + } + + public override string ToString() + { + var builder = new StringBuilder(); + builder.Append(Green.ToString()); + builder.AppendFormat(" at {0}::{1}", Position, FullWidth); + + return builder.ToString(); + } + + public virtual string ToFullString() + { + return Green.ToFullString(); + } + + protected virtual string GetDebuggerDisplay() + { + if (IsToken) + { + return string.Format("{0};[{1}]", Kind, ToFullString()); + } + + return string.Format("{0} [{1}..{2})", Kind, Position, EndPosition); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxNodeExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxNodeExtensions.cs new file mode 100644 index 0000000000..86b8cf4a19 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxNodeExtensions.cs @@ -0,0 +1,339 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language.Legacy; + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal static class SyntaxNodeExtensions + { + // From http://dev.w3.org/html5/spec/Overview.html#elements-0 + private static readonly HashSet VoidElements = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "area", + "base", + "br", + "col", + "command", + "embed", + "hr", + "img", + "input", + "keygen", + "link", + "meta", + "param", + "source", + "track", + "wbr" + }; + + public static TNode WithAnnotations(this TNode node, params SyntaxAnnotation[] annotations) where TNode : SyntaxNode + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + return (TNode)node.Green.SetAnnotations(annotations).CreateRed(node.Parent, node.Position); + } + + public static object GetAnnotationValue(this TNode node, string key) where TNode : SyntaxNode + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + var annotation = node.GetAnnotations().FirstOrDefault(n => n.Kind == key); + return annotation?.Data; + } + + public static TNode WithDiagnostics(this TNode node, params RazorDiagnostic[] diagnostics) where TNode : SyntaxNode + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + return (TNode)node.Green.SetDiagnostics(diagnostics).CreateRed(node.Parent, node.Position); + } + + public static TNode AppendDiagnostic(this TNode node, params RazorDiagnostic[] diagnostics) where TNode : SyntaxNode + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + var existingDiagnostics = node.GetDiagnostics(); + var allDiagnostics = existingDiagnostics.Concat(diagnostics).ToArray(); + + return (TNode)node.WithDiagnostics(allDiagnostics); + } + + public static SourceLocation GetSourceLocation(this SyntaxNode node, RazorSourceDocument source) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + try + { + if (source.Length == 0) + { + // Just a marker symbol + return new SourceLocation(source.FilePath, 0, 0, 0); + } + if (node.Position == source.Length) + { + // E.g. Marker symbol at the end of the document + var lastPosition = source.Length - 1; + var endsWithLineBreak = ParserHelpers.IsNewLine(source[lastPosition]); + var lastLocation = source.Lines.GetLocation(lastPosition); + return new SourceLocation( + source.FilePath, // GetLocation prefers RelativePath but we want FilePath. + lastLocation.AbsoluteIndex + 1, + lastLocation.LineIndex + (endsWithLineBreak ? 1 : 0), + endsWithLineBreak ? 0 : lastLocation.CharacterIndex + 1); + } + + var location = source.Lines.GetLocation(node.Position); + return new SourceLocation( + source.FilePath, // GetLocation prefers RelativePath but we want FilePath. + location.AbsoluteIndex, + location.LineIndex, + location.CharacterIndex); + } + catch (IndexOutOfRangeException) + { + Debug.Assert(false, "Node position should stay within document length."); + return new SourceLocation(source.FilePath, node.Position, 0, 0); + } + } + + public static SourceSpan GetSourceSpan(this SyntaxNode node, RazorSourceDocument source) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + var location = node.GetSourceLocation(source); + + return new SourceSpan(location, node.FullWidth); + } + + /// + /// Creates a new tree of nodes with the specified nodes, tokens and trivia replaced. + /// + /// The type of the root node. + /// The root node of the tree of nodes. + /// The nodes to be replaced. + /// A function that computes a replacement node for the + /// argument nodes. The first argument is the original node. The second argument is the same + /// node potentially rewritten with replaced descendants. + public static TRoot ReplaceSyntax( + this TRoot root, + IEnumerable nodes, + Func computeReplacementNode) + where TRoot : SyntaxNode + { + return (TRoot)root.ReplaceCore( + nodes: nodes, computeReplacementNode: computeReplacementNode); + } + + /// + /// Creates a new tree of nodes with the specified old node replaced with a new node. + /// + /// The type of the root node. + /// The type of the nodes being replaced. + /// The root node of the tree of nodes. + /// The nodes to be replaced; descendants of the root node. + /// A function that computes a replacement node for the + /// argument nodes. The first argument is the original node. The second argument is the same + /// node potentially rewritten with replaced descendants. + public static TRoot ReplaceNodes(this TRoot root, IEnumerable nodes, Func computeReplacementNode) + where TRoot : SyntaxNode + where TNode : SyntaxNode + { + return (TRoot)root.ReplaceCore(nodes: nodes, computeReplacementNode: computeReplacementNode); + } + + /// + /// Creates a new tree of nodes with the specified old node replaced with a new node. + /// + /// The type of the root node. + /// The root node of the tree of nodes. + /// The node to be replaced; a descendant of the root node. + /// The new node to use in the new tree in place of the old node. + public static TRoot ReplaceNode(this TRoot root, SyntaxNode oldNode, SyntaxNode newNode) + where TRoot : SyntaxNode + { + if (oldNode == newNode) + { + return root; + } + + return (TRoot)root.ReplaceCore(nodes: new[] { oldNode }, computeReplacementNode: (o, r) => newNode); + } + + /// + /// Creates a new tree of nodes with specified old node replaced with a new nodes. + /// + /// The type of the root node. + /// The root of the tree of nodes. + /// The node to be replaced; a descendant of the root node and an element of a list member. + /// A sequence of nodes to use in the tree in place of the old node. + public static TRoot ReplaceNode(this TRoot root, SyntaxNode oldNode, IEnumerable newNodes) + where TRoot : SyntaxNode + { + return (TRoot)root.ReplaceNodeInListCore(oldNode, newNodes); + } + + /// + /// Creates a new tree of nodes with new nodes inserted before the specified node. + /// + /// The type of the root node. + /// The root of the tree of nodes. + /// The node to insert before; a descendant of the root node an element of a list member. + /// A sequence of nodes to insert into the tree immediately before the specified node. + public static TRoot InsertNodesBefore(this TRoot root, SyntaxNode nodeInList, IEnumerable newNodes) + where TRoot : SyntaxNode + { + return (TRoot)root.InsertNodesInListCore(nodeInList, newNodes, insertBefore: true); + } + + /// + /// Creates a new tree of nodes with new nodes inserted after the specified node. + /// + /// The type of the root node. + /// The root of the tree of nodes. + /// The node to insert after; a descendant of the root node an element of a list member. + /// A sequence of nodes to insert into the tree immediately after the specified node. + public static TRoot InsertNodesAfter(this TRoot root, SyntaxNode nodeInList, IEnumerable newNodes) + where TRoot : SyntaxNode + { + return (TRoot)root.InsertNodesInListCore(nodeInList, newNodes, insertBefore: false); + } + + public static string GetContent(this TNode node) where TNode : SyntaxNode + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + var tokens = node.DescendantNodes().Where(n => n.IsToken).Cast(); + var content = string.Concat(tokens.Select(t => t.Content)); + return content; + } + + public static string GetTagName(this MarkupTagBlockSyntax tagBlock) + { + if (tagBlock == null) + { + throw new ArgumentNullException(nameof(tagBlock)); + } + + var child = tagBlock.Children[0]; + + if (tagBlock.Children.Count == 0 || !(child is MarkupTextLiteralSyntax)) + { + return null; + } + + var childLiteral = (MarkupTextLiteralSyntax)child; + SyntaxToken textToken = null; + for (var i = 0; i < childLiteral.LiteralTokens.Count; i++) + { + var token = childLiteral.LiteralTokens[i]; + + if (token != null && + (token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.Text)) + { + textToken = token; + break; + } + } + + if (textToken == null) + { + return null; + } + + return textToken.Kind == SyntaxKind.Whitespace ? null : textToken.Content; + } + + public static string GetTagName(this MarkupTagHelperStartTagSyntax tagBlock) + { + if (tagBlock == null) + { + throw new ArgumentNullException(nameof(tagBlock)); + } + + var child = tagBlock.Children[0]; + + if (tagBlock.Children.Count == 0 || !(child is MarkupTextLiteralSyntax)) + { + return null; + } + + var childLiteral = (MarkupTextLiteralSyntax)child; + SyntaxToken textToken = null; + for (var i = 0; i < childLiteral.LiteralTokens.Count; i++) + { + var token = childLiteral.LiteralTokens[i]; + + if (token != null && + (token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.Text)) + { + textToken = token; + break; + } + } + + if (textToken == null) + { + return null; + } + + return textToken.Kind == SyntaxKind.Whitespace ? null : textToken.Content; + } + + public static bool IsSelfClosing(this MarkupTagBlockSyntax tagBlock) + { + if (tagBlock == null) + { + throw new ArgumentNullException(nameof(tagBlock)); + } + + var lastChild = tagBlock.ChildNodes().LastOrDefault(); + + return lastChild?.GetContent().EndsWith("/>", StringComparison.Ordinal) ?? false; + } + + public static bool IsVoidElement(this MarkupTagBlockSyntax tagBlock) + { + if (tagBlock == null) + { + throw new ArgumentNullException(nameof(tagBlock)); + } + + return VoidElements.Contains(tagBlock.GetTagName()); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxReplacer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxReplacer.cs new file mode 100644 index 0000000000..2d192958e4 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxReplacer.cs @@ -0,0 +1,207 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal static class SyntaxReplacer + { + internal static SyntaxNode Replace( + SyntaxNode root, + IEnumerable nodes = null, + Func computeReplacementNode = null) + where TNode : SyntaxNode + { + var replacer = new Replacer(nodes, computeReplacementNode); + + if (replacer.HasWork) + { + return replacer.Visit(root); + } + else + { + return root; + } + } + + internal static SyntaxNode ReplaceNodeInList(SyntaxNode root, SyntaxNode originalNode, IEnumerable newNodes) + { + return new NodeListEditor(originalNode, newNodes, ListEditKind.Replace).Visit(root); + } + + internal static SyntaxNode InsertNodeInList(SyntaxNode root, SyntaxNode nodeInList, IEnumerable nodesToInsert, bool insertBefore) + { + return new NodeListEditor(nodeInList, nodesToInsert, insertBefore ? ListEditKind.InsertBefore : ListEditKind.InsertAfter).Visit(root); + } + + private class Replacer : SyntaxRewriter where TNode : SyntaxNode + { + private readonly Func _computeReplacementNode; + private readonly HashSet _nodeSet; + private readonly HashSet _spanSet; + private readonly TextSpan _totalSpan; + + public Replacer(IEnumerable nodes, Func computeReplacementNode) + { + _computeReplacementNode = computeReplacementNode; + _nodeSet = nodes != null ? new HashSet(nodes) : new HashSet(); + _spanSet = new HashSet(_nodeSet.Select(n => n.FullSpan)); + _totalSpan = ComputeTotalSpan(_spanSet); + } + + public bool HasWork => _nodeSet.Count > 0; + + public override SyntaxNode Visit(SyntaxNode node) + { + var rewritten = node; + + if (node != null) + { + if (ShouldVisit(node.FullSpan)) + { + rewritten = base.Visit(node); + } + + if (_nodeSet.Contains(node) && _computeReplacementNode != null) + { + rewritten = _computeReplacementNode((TNode)node, (TNode)rewritten); + } + } + + return rewritten; + } + + private static TextSpan ComputeTotalSpan(IEnumerable spans) + { + var first = true; + var start = 0; + var end = 0; + + foreach (var span in spans) + { + if (first) + { + start = span.Start; + end = span.End; + first = false; + } + else + { + start = Math.Min(start, span.Start); + end = Math.Max(end, span.End); + } + } + + return new TextSpan(start, end - start); + } + + private bool ShouldVisit(TextSpan span) + { + // first do quick check against total span + if (!span.IntersectsWith(_totalSpan)) + { + // if the node is outside the total span of the nodes to be replaced + // then we won't find any nodes to replace below it. + return false; + } + + foreach (var s in _spanSet) + { + if (span.IntersectsWith(s)) + { + // node's full span intersects with at least one node to be replaced + // so we need to visit node's children to find it. + return true; + } + } + + return false; + } + } + + private class NodeListEditor : SyntaxRewriter + { + private readonly TextSpan _elementSpan; + private readonly SyntaxNode _originalNode; + private readonly IEnumerable _newNodes; + private readonly ListEditKind _editKind; + + public NodeListEditor( + SyntaxNode originalNode, + IEnumerable replacementNodes, + ListEditKind editKind) + { + _elementSpan = originalNode.Span; + _originalNode = originalNode; + _newNodes = replacementNodes; + _editKind = editKind; + } + + private bool ShouldVisit(TextSpan span) + { + if (span.IntersectsWith(_elementSpan)) + { + // node's full span intersects with at least one node to be replaced + // so we need to visit node's children to find it. + return true; + } + + return false; + } + + public override SyntaxNode Visit(SyntaxNode node) + { + if (node == _originalNode) + { + throw new InvalidOperationException("Expecting a list"); + } + + var rewritten = node; + + if (node != null) + { + if (ShouldVisit(node.FullSpan)) + { + rewritten = base.Visit(node); + } + } + + return rewritten; + } + + public override SyntaxList VisitList(SyntaxList list) + { + if (_originalNode is TNode) + { + var index = list.IndexOf((TNode)_originalNode); + if (index >= 0 && index < list.Count) + { + switch (_editKind) + { + case ListEditKind.Replace: + return list.ReplaceRange((TNode)_originalNode, _newNodes.Cast()); + + case ListEditKind.InsertAfter: + return list.InsertRange(index + 1, _newNodes.Cast()); + + case ListEditKind.InsertBefore: + return list.InsertRange(index, _newNodes.Cast()); + } + } + } + + return base.VisitList(list); + } + } + + private enum ListEditKind + { + InsertBefore, + InsertAfter, + Replace + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxRewriter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxRewriter.cs new file mode 100644 index 0000000000..d491bd0e88 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxRewriter.cs @@ -0,0 +1,148 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal abstract partial class SyntaxRewriter : SyntaxVisitor + { + public override SyntaxNode VisitToken(SyntaxToken token) + { + // PERF: This is a hot method, so it has been written to minimize the following: + // 1. Virtual method calls + // 2. Copying of structs + // 3. Repeated null checks + + // PERF: Avoid testing node for null more than once + var node = token?.Green; + if (node == null) + { + return token; + } + + // PERF: Make one virtual method call each to get the leading and trailing trivia + var leadingTrivia = node.GetLeadingTrivia(); + var trailingTrivia = node.GetTrailingTrivia(); + + // Trivia is either null or a non-empty list (there's no such thing as an empty green list) + Debug.Assert(leadingTrivia == null || !leadingTrivia.IsList || leadingTrivia.SlotCount > 0); + Debug.Assert(trailingTrivia == null || !trailingTrivia.IsList || trailingTrivia.SlotCount > 0); + + if (leadingTrivia != null) + { + // PERF: Expand token.LeadingTrivia when node is not null. + var leading = VisitList(new SyntaxTriviaList(leadingTrivia.CreateRed(token, token.Position))); + + if (trailingTrivia != null) + { + // Both leading and trailing trivia + + // PERF: Expand token.TrailingTrivia when node is not null and leadingTrivia is not null. + // Also avoid node.Width because it makes a virtual call to GetText. Instead use node.FullWidth - trailingTrivia.FullWidth. + var index = leadingTrivia.IsList ? leadingTrivia.SlotCount : 1; + var position = token.Position + node.FullWidth - trailingTrivia.FullWidth; + var trailing = VisitList(new SyntaxTriviaList(trailingTrivia.CreateRed(token, position), position, index)); + + if (leading.Node.Green != leadingTrivia) + { + token = token.WithLeadingTrivia(leading); + } + + return trailing.Node.Green != trailingTrivia ? token.WithTrailingTrivia(trailing) : token; + } + else + { + // Leading trivia only + return leading.Node.Green != leadingTrivia ? token.WithLeadingTrivia(leading) : token; + } + } + else if (trailingTrivia != null) + { + // Trailing trivia only + // PERF: Expand token.TrailingTrivia when node is not null and leading is null. + // Also avoid node.Width because it makes a virtual call to GetText. Instead use node.FullWidth - trailingTrivia.FullWidth. + var position = token.Position + node.FullWidth - trailingTrivia.FullWidth; + var trailing = VisitList(new SyntaxTriviaList(trailingTrivia.CreateRed(token, position), position, index: 0)); + return trailing.Node.Green != trailingTrivia ? token.WithTrailingTrivia(trailing) : token; + } + else + { + // No trivia + return token; + } + } + + public virtual SyntaxList VisitList(SyntaxList list) where TNode : SyntaxNode + { + SyntaxListBuilder alternate = null; + for (int i = 0, n = list.Count; i < n; i++) + { + var item = list[i]; + var visited = VisitListElement(item); + if (item != visited && alternate == null) + { + alternate = new SyntaxListBuilder(n); + alternate.AddRange(list, 0, i); + } + + if (alternate != null && visited != null) + { + alternate.Add(visited); + } + } + + if (alternate != null) + { + return alternate.ToList(); + } + + return list; + } + + public override SyntaxNode VisitTrivia(SyntaxTrivia trivia) + { + return trivia; + } + + public virtual SyntaxTriviaList VisitList(SyntaxTriviaList list) + { + var count = list.Count; + if (count != 0) + { + SyntaxTriviaListBuilder alternate = null; + var index = -1; + + foreach (var item in list) + { + index++; + var visited = VisitListElement(item); + + //skip the null check since SyntaxTrivia is a value type + if (visited != item && alternate == null) + { + alternate = new SyntaxTriviaListBuilder(count); + alternate.Add(list, 0, index); + } + + if (alternate != null && visited != null) + { + alternate.Add(visited); + } + } + + if (alternate != null) + { + return alternate.ToList(); + } + } + + return list; + } + + public virtual TNode VisitListElement(TNode node) where TNode : SyntaxNode + { + return (TNode)(SyntaxNode)Visit(node); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxSerializer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxSerializer.cs new file mode 100644 index 0000000000..5b167b510f --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxSerializer.cs @@ -0,0 +1,304 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Razor.Language.Legacy; + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal class SyntaxSerializer + { + internal static string Serialize(SyntaxNode node) + { + using (var writer = new StringWriter()) + { + var walker = new Walker(writer); + walker.Visit(node); + + return writer.ToString(); + } + } + + private class Walker : SyntaxWalker + { + private readonly SyntaxWriter _visitor; + private readonly TextWriter _writer; + + public Walker(TextWriter writer) + { + _visitor = new SyntaxWriter(writer); + _writer = writer; + } + + public TextWriter Writer { get; } + + public override SyntaxNode Visit(SyntaxNode node) + { + if (node == null) + { + return node; + } + + if (node.IsList) + { + return base.DefaultVisit(node); + } + + _visitor.Visit(node); + _writer.WriteLine(); + + if (!node.IsToken && !node.IsTrivia) + { + _visitor.Depth++; + node = base.DefaultVisit(node); + _visitor.Depth--; + } + + return node; + } + } + + private class SyntaxWalker : SyntaxRewriter + { + private readonly List _ancestors = new List(); + + protected IReadOnlyList Ancestors => _ancestors; + + protected SyntaxNode Parent => _ancestors.Count > 0 ? _ancestors[0] : null; + + protected override SyntaxNode DefaultVisit(SyntaxNode node) + { + _ancestors.Insert(0, node); + + try + { + for (var i = 0; i < node.SlotCount; i++) + { + var child = node.GetNodeSlot(i); + Visit(child); + } + } + finally + { + _ancestors.RemoveAt(0); + } + + return node; + } + } + + private class SyntaxWriter : SyntaxRewriter + { + private readonly TextWriter _writer; + private bool _visitedRoot; + + public SyntaxWriter(TextWriter writer) + { + _writer = writer; + } + + public int Depth { get; set; } + + public override SyntaxNode Visit(SyntaxNode node) + { + if (node is SyntaxToken token) + { + return VisitToken(token); + } + + WriteNode(node); + return node; + } + + public override SyntaxNode VisitToken(SyntaxToken token) + { + WriteToken(token); + return base.VisitToken(token); + } + + public override SyntaxNode VisitTrivia(SyntaxTrivia trivia) + { + WriteTrivia(trivia); + return base.VisitTrivia(trivia); + } + + private void WriteNode(SyntaxNode node) + { + WriteIndent(); + Write(node.Kind); + WriteSeparator(); + Write($"[{node.Position}..{node.EndPosition})"); + WriteSeparator(); + Write($"FullWidth: {node.FullWidth}"); + + if (node is RazorDirectiveSyntax razorDirective) + { + WriteRazorDirective(razorDirective); + } + else if (node is MarkupTagHelperElementSyntax tagHelperElement) + { + WriteTagHelperElement(tagHelperElement); + } + else if (node is MarkupTagHelperAttributeSyntax tagHelperAttribute) + { + WriteTagHelperAttributeInfo(tagHelperAttribute.TagHelperAttributeInfo); + } + else if (node is MarkupMinimizedTagHelperAttributeSyntax minimizedTagHelperAttribute) + { + WriteTagHelperAttributeInfo(minimizedTagHelperAttribute.TagHelperAttributeInfo); + } + + if (ShouldDisplayNodeContent(node)) + { + WriteSeparator(); + Write($"[{node.GetContent()}]"); + } + + var annotation = node.GetAnnotations().FirstOrDefault(a => a.Kind == SyntaxConstants.SpanContextKind); + if (annotation != null && annotation.Data is SpanContext context) + { + WriteSpanContext(context); + } + + if (!_visitedRoot) + { + WriteSeparator(); + Write($"[{node.ToFullString()}]"); + _visitedRoot = true; + } + } + + private void WriteRazorDirective(RazorDirectiveSyntax node) + { + if (node.DirectiveDescriptor == null) + { + return; + } + + var builder = new StringBuilder("Directive:{"); + builder.Append(node.DirectiveDescriptor.Directive); + builder.Append(";"); + builder.Append(node.DirectiveDescriptor.Kind); + builder.Append(";"); + builder.Append(node.DirectiveDescriptor.Usage); + builder.Append("}"); + + var diagnostics = node.GetDiagnostics(); + if (diagnostics.Length > 0) + { + builder.Append(" ["); + var ids = string.Join(", ", diagnostics.Select(diagnostic => $"{diagnostic.Id}{diagnostic.Span}")); + builder.Append(ids); + builder.Append("]"); + } + + WriteSeparator(); + Write(builder.ToString()); + } + + private void WriteTagHelperElement(MarkupTagHelperElementSyntax node) + { + // Write tag name + WriteSeparator(); + Write($"{node.TagHelperInfo.TagName}[{node.TagHelperInfo.TagMode}]"); + + // Write descriptors + foreach (var descriptor in node.TagHelperInfo.BindingResult.Descriptors) + { + WriteSeparator(); + + // Get the type name without the namespace. + var typeName = descriptor.Name.Substring(descriptor.Name.LastIndexOf('.') + 1); + Write(typeName); + } + } + + private void WriteTagHelperAttributeInfo(TagHelperAttributeInfo info) + { + // Write attributes + WriteSeparator(); + Write(info.Name); + WriteSeparator(); + Write(info.AttributeStructure); + WriteSeparator(); + Write(info.Bound ? "Bound" : "Unbound"); + } + + private void WriteToken(SyntaxToken token) + { + WriteIndent(); + var content = token.IsMissing ? "" : token.Content; + var diagnostics = token.GetDiagnostics(); + var tokenString = $"{token.Kind};[{content}];{string.Join(", ", diagnostics.Select(diagnostic => diagnostic.Id + diagnostic.Span))}"; + Write(tokenString); + } + + private void WriteTrivia(SyntaxTrivia trivia) + { + throw new NotImplementedException(); + } + + private void WriteSpanContext(SpanContext context) + { + WriteSeparator(); + Write($"Gen<{context.ChunkGenerator}>"); + WriteSeparator(); + Write(context.EditHandler); + } + + protected void WriteIndent() + { + for (var i = 0; i < Depth; i++) + { + for (var j = 0; j < 4; j++) + { + Write(' '); + } + } + } + + protected void WriteSeparator() + { + Write(" - "); + } + + protected void WriteNewLine() + { + _writer.WriteLine(); + } + + protected void Write(object value) + { + if (value is string stringValue) + { + stringValue = stringValue.Replace(Environment.NewLine, "LF"); + _writer.Write(stringValue); + return; + } + + _writer.Write(value); + } + + private static bool ShouldDisplayNodeContent(SyntaxNode node) + { + return node.Kind == SyntaxKind.MarkupTextLiteral || + node.Kind == SyntaxKind.MarkupEphemeralTextLiteral || + node.Kind == SyntaxKind.MarkupTagBlock || + node.Kind == SyntaxKind.MarkupAttributeBlock || + node.Kind == SyntaxKind.MarkupMinimizedAttributeBlock || + node.Kind == SyntaxKind.MarkupTagHelperAttribute || + node.Kind == SyntaxKind.MarkupMinimizedTagHelperAttribute || + node.Kind == SyntaxKind.MarkupLiteralAttributeValue || + node.Kind == SyntaxKind.MarkupDynamicAttributeValue || + node.Kind == SyntaxKind.CSharpStatementLiteral || + node.Kind == SyntaxKind.CSharpExpressionLiteral || + node.Kind == SyntaxKind.CSharpEphemeralTextLiteral || + node.Kind == SyntaxKind.UnclassifiedTextLiteral; + } + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxToken.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxToken.cs new file mode 100644 index 0000000000..e3ecd35bf6 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxToken.cs @@ -0,0 +1,105 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language.Legacy; + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal class SyntaxToken : RazorSyntaxNode + { + internal SyntaxToken(GreenNode green, SyntaxNode parent, int position) + : base(green, parent, position) + { + } + + internal new InternalSyntax.SyntaxToken Green => (InternalSyntax.SyntaxToken)base.Green; + + public string Content => Green.Content; + + internal override sealed SyntaxNode GetCachedSlot(int index) + { + throw new InvalidOperationException("Tokens can't have slots."); + } + + internal override sealed SyntaxNode GetNodeSlot(int slot) + { + throw new InvalidOperationException("Tokens can't have slots."); + } + + public override TResult Accept(SyntaxVisitor visitor) + { + return visitor.VisitToken(this); + } + + public override void Accept(SyntaxVisitor visitor) + { + visitor.VisitToken(this); + } + + public SyntaxToken WithLeadingTrivia(SyntaxNode trivia) + { + return Green != null + ? new SyntaxToken(Green.WithLeadingTrivia(trivia.Green), parent: null, position: 0) + : default(SyntaxToken); + } + + public SyntaxToken WithTrailingTrivia(SyntaxNode trivia) + { + return Green != null + ? new SyntaxToken(Green.WithTrailingTrivia(trivia.Green), parent: null, position: 0) + : default(SyntaxToken); + } + + public SyntaxToken WithLeadingTrivia(IEnumerable trivia) + { + var greenList = trivia?.Select(t => t.Green); + return WithLeadingTrivia(Green.CreateList(greenList)?.CreateRed()); + } + + public SyntaxToken WithTrailingTrivia(IEnumerable trivia) + { + var greenList = trivia?.Select(t => t.Green); + return WithTrailingTrivia(Green.CreateList(greenList)?.CreateRed()); + } + + public override SyntaxTriviaList GetLeadingTrivia() + { + var leading = Green.GetLeadingTrivia(); + if (leading == null) + { + return default(SyntaxTriviaList); + } + + return new SyntaxTriviaList(leading.CreateRed(this, Position), Position); + } + + public override SyntaxTriviaList GetTrailingTrivia() + { + var trailing = Green.GetTrailingTrivia(); + if (trailing == null) + { + return default(SyntaxTriviaList); + } + + var leading = Green.GetLeadingTrivia(); + var index = 0; + if (leading != null) + { + index = leading.IsList ? leading.SlotCount : 1; + } + int trailingPosition = Position + FullWidth; + trailingPosition -= trailing.FullWidth; + + return new SyntaxTriviaList(trailing.CreateRed(this, trailingPosition), trailingPosition, index); + } + + public override string ToString() + { + return Content; + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxTrivia.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxTrivia.cs new file mode 100644 index 0000000000..d16e42f1ed --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxTrivia.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal class SyntaxTrivia : SyntaxNode + { + internal SyntaxTrivia(GreenNode green, SyntaxNode parent, int position) + : base(green, parent, position) + { + } + + internal new InternalSyntax.SyntaxTrivia Green => (InternalSyntax.SyntaxTrivia)base.Green; + + public string Text => Green.Text; + + internal override sealed SyntaxNode GetCachedSlot(int index) + { + throw new InvalidOperationException(); + } + + internal override sealed SyntaxNode GetNodeSlot(int slot) + { + throw new InvalidOperationException(); + } + + public override TResult Accept(SyntaxVisitor visitor) + { + return visitor.VisitTrivia(this); + } + + public override void Accept(SyntaxVisitor visitor) + { + visitor.VisitTrivia(this); + } + + public sealed override SyntaxTriviaList GetTrailingTrivia() + { + return default(SyntaxTriviaList); + } + + public sealed override SyntaxTriviaList GetLeadingTrivia() + { + return default(SyntaxTriviaList); + } + + public override string ToString() + { + return Text; + } + + public sealed override string ToFullString() + { + return Text; + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxTriviaList.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxTriviaList.cs new file mode 100644 index 0000000000..2097e9391c --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxTriviaList.cs @@ -0,0 +1,785 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + [StructLayout(LayoutKind.Auto)] + internal readonly struct SyntaxTriviaList : IEquatable, IReadOnlyList + { + public static SyntaxTriviaList Empty => default(SyntaxTriviaList); + + internal SyntaxTriviaList(SyntaxNode node, int position, int index = 0) + { + Node = node; + Position = position; + Index = index; + } + + internal SyntaxTriviaList(SyntaxNode node) + { + Node = node; + Position = node.Position; + Index = 0; + } + + public SyntaxTriviaList(SyntaxTrivia trivia) + { + Node = trivia; + Position = 0; + Index = 0; + } + + /// + /// Creates a list of trivia. + /// + /// An array of trivia. + public SyntaxTriviaList(params SyntaxTrivia[] trivias) + : this(CreateNode(trivias), 0, 0) + { + } + + /// + /// Creates a list of trivia. + /// + /// A sequence of trivia. + public SyntaxTriviaList(IEnumerable trivias) + : this(SyntaxTriviaListBuilder.Create(trivias).Node, 0, 0) + { + } + + private static SyntaxNode CreateNode(SyntaxTrivia[] trivias) + { + if (trivias == null) + { + return null; + } + + var builder = new SyntaxTriviaListBuilder(trivias.Length); + builder.Add(trivias); + return builder.ToList().Node; + } + + internal SyntaxNode Node { get; } + + internal int Position { get; } + + internal int Index { get; } + + public int Count + { + get { return Node == null ? 0 : (Node.IsList ? Node.SlotCount : 1); } + } + + public SyntaxTrivia ElementAt(int index) + { + return this[index]; + } + + /// + /// Gets the trivia at the specified index. + /// + /// The zero-based index of the trivia to get. + /// The token at the specified index. + /// + /// is less than 0.-or- is equal to or greater than . + public SyntaxTrivia this[int index] + { + get + { + if (Node != null) + { + if (Node.IsList) + { + if (unchecked((uint)index < (uint)Node.SlotCount)) + { + return Node.GetNodeSlot(index) as SyntaxTrivia; + } + } + else if (index == 0) + { + return Node as SyntaxTrivia; + } + } + + throw new ArgumentOutOfRangeException(nameof(index)); + } + } + + /// + /// The absolute span of the list elements in characters, including the leading and trailing trivia of the first and last elements. + /// + public TextSpan FullSpan + { + get + { + if (Node == null) + { + return default(TextSpan); + } + + return new TextSpan(Position, Node.FullWidth); + } + } + + /// + /// The absolute span of the list elements in characters, not including the leading and trailing trivia of the first and last elements. + /// + public TextSpan Span + { + get + { + if (Node == null) + { + return default(TextSpan); + } + + return TextSpan.FromBounds(Position + Node.Green.GetLeadingTriviaWidth(), + Position + Node.FullWidth - Node.Green.GetTrailingTriviaWidth()); + } + } + + /// + /// Returns the first trivia in the list. + /// + /// The first trivia in the list. + /// The list is empty. + public SyntaxTrivia First() + { + if (Any()) + { + return this[0]; + } + + throw new InvalidOperationException(); + } + + /// + /// Returns the last trivia in the list. + /// + /// The last trivia in the list. + /// The list is empty. + public SyntaxTrivia Last() + { + if (Any()) + { + return this[Count - 1]; + } + + throw new InvalidOperationException(); + } + + /// + /// Does this list have any items. + /// + public bool Any() + { + return Node != null; + } + + /// + /// Returns a list which contains all elements of in reversed order. + /// + /// which contains all elements of in reversed order + public Reversed Reverse() + { + return new Reversed(this); + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public int IndexOf(SyntaxTrivia triviaInList) + { + for (int i = 0, n = Count; i < n; i++) + { + var trivia = this[i]; + if (trivia == triviaInList) + { + return i; + } + } + + return -1; + } + + internal int IndexOf(SyntaxKind kind) + { + for (int i = 0, n = Count; i < n; i++) + { + if (this[i].Kind == kind) + { + return i; + } + } + + return -1; + } + + /// + /// Creates a new with the specified trivia added to the end. + /// + /// The trivia to add. + public SyntaxTriviaList Add(SyntaxTrivia trivia) + { + return Insert(Count, trivia); + } + + /// + /// Creates a new with the specified trivia added to the end. + /// + /// The trivia to add. + public SyntaxTriviaList AddRange(IEnumerable trivia) + { + return InsertRange(Count, trivia); + } + + /// + /// Creates a new with the specified trivia inserted at the index. + /// + /// The index in the list to insert the trivia at. + /// The trivia to insert. + public SyntaxTriviaList Insert(int index, SyntaxTrivia trivia) + { + if (trivia == default(SyntaxTrivia)) + { + throw new ArgumentOutOfRangeException(nameof(trivia)); + } + + return InsertRange(index, new[] { trivia }); + } + + private static readonly ObjectPool s_builderPool = + new ObjectPool(() => SyntaxTriviaListBuilder.Create()); + + private static SyntaxTriviaListBuilder GetBuilder() + => s_builderPool.Allocate(); + + private static void ClearAndFreeBuilder(SyntaxTriviaListBuilder builder) + { + // It's possible someone might create a list with a huge amount of trivia + // in it. We don't want to hold onto such items forever. So only cache + // reasonably sized lists. In IDE testing, around 99% of all trivia lists + // were 16 or less elements. + const int MaxBuilderCount = 16; + if (builder.Count <= MaxBuilderCount) + { + builder.Clear(); + s_builderPool.Free(builder); + } + } + + /// + /// Creates a new with the specified trivia inserted at the index. + /// + /// The index in the list to insert the trivia at. + /// The trivia to insert. + public SyntaxTriviaList InsertRange(int index, IEnumerable trivia) + { + var thisCount = Count; + if (index < 0 || index > thisCount) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (trivia == null) + { + throw new ArgumentNullException(nameof(trivia)); + } + + // Just return ourselves if we're not being asked to add anything. + if (trivia is ICollection triviaCollection && triviaCollection.Count == 0) + { + return this; + } + + var builder = GetBuilder(); + try + { + for (var i = 0; i < index; i++) + { + builder.Add(this[i]); + } + + builder.AddRange(trivia); + + for (var i = index; i < thisCount; i++) + { + builder.Add(this[i]); + } + + return builder.Count == thisCount ? this : builder.ToList(); + } + finally + { + ClearAndFreeBuilder(builder); + } + } + + /// + /// Creates a new with the element at the specified index removed. + /// + /// The index identifying the element to remove. + public SyntaxTriviaList RemoveAt(int index) + { + if (index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + var list = this.ToList(); + list.RemoveAt(index); + return new SyntaxTriviaList(list); + } + + /// + /// Creates a new with the specified element removed. + /// + /// The trivia element to remove. + public SyntaxTriviaList Remove(SyntaxTrivia triviaInList) + { + var index = IndexOf(triviaInList); + if (index >= 0 && index < Count) + { + return RemoveAt(index); + } + + return this; + } + + /// + /// Creates a new with the specified element replaced with new trivia. + /// + /// The trivia element to replace. + /// The trivia to replace the element with. + public SyntaxTriviaList Replace(SyntaxTrivia triviaInList, SyntaxTrivia newTrivia) + { + if (newTrivia == default(SyntaxTrivia)) + { + throw new ArgumentOutOfRangeException(nameof(newTrivia)); + } + + return ReplaceRange(triviaInList, new[] { newTrivia }); + } + + /// + /// Creates a new with the specified element replaced with new trivia. + /// + /// The trivia element to replace. + /// The trivia to replace the element with. + public SyntaxTriviaList ReplaceRange(SyntaxTrivia triviaInList, IEnumerable newTrivia) + { + var index = IndexOf(triviaInList); + if (index >= 0 && index < Count) + { + var list = this.ToList(); + list.RemoveAt(index); + list.InsertRange(index, newTrivia); + return new SyntaxTriviaList(list); + } + + throw new ArgumentOutOfRangeException(nameof(triviaInList)); + } + + // for debugging + private SyntaxTrivia[] Nodes => this.ToArray(); + + IEnumerator IEnumerable.GetEnumerator() + { + if (Node == null) + { + return SpecializedCollections.EmptyEnumerator(); + } + + return new EnumeratorImpl(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + if (Node == null) + { + return SpecializedCollections.EmptyEnumerator(); + } + + return new EnumeratorImpl(this); + } + + /// + /// get the green node at the specific slot + /// + private SyntaxNode GetNodeAt(int i) + { + return GetNodeAt(Node, i); + } + + private static SyntaxNode GetNodeAt(SyntaxNode node, int i) + { + Debug.Assert(node.IsList || (i == 0 && !node.IsList)); + return node.IsList ? node.GetNodeSlot(i) : node; + } + + public bool Equals(SyntaxTriviaList other) + { + return Node == other.Node && Index == other.Index; + } + + public static bool operator ==(SyntaxTriviaList left, SyntaxTriviaList right) + { + return left.Equals(right); + } + + public static bool operator !=(SyntaxTriviaList left, SyntaxTriviaList right) + { + return !left.Equals(right); + } + + public override bool Equals(object obj) + { + return (obj is SyntaxTriviaList) && Equals((SyntaxTriviaList)obj); + } + + public override int GetHashCode() + { + var hash = HashCodeCombiner.Start(); + hash.Add(Node); + hash.Add(Index); + return hash.CombinedHash; + } + + /// + /// Copy number of items starting at from this list into starting at . + /// + internal void CopyTo(int offset, SyntaxTrivia[] array, int arrayOffset, int count) + { + if (offset < 0 || count < 0 || Count < offset + count) + { + throw new IndexOutOfRangeException(); + } + + if (count == 0) + { + return; + } + + // get first one without creating any red node + var first = this[offset]; + array[arrayOffset] = first; + + // calculate trivia position from the first ourselves from now on + var position = first.Position; + var current = first; + + for (var i = 1; i < count; i++) + { + position += current.FullWidth; + current = GetNodeAt(offset + i) as SyntaxTrivia; + + array[arrayOffset + i] = current; + } + } + + public override string ToString() + { + return Node != null ? Node.ToString() : string.Empty; + } + + public string ToFullString() + { + return Node != null ? Node.ToFullString() : string.Empty; + } + + public static SyntaxTriviaList Create(SyntaxTrivia trivia) + { + return new SyntaxTriviaList(trivia); + } + + /// + /// Reversed enumerable. + /// + public struct Reversed : IEnumerable, IEquatable + { + private SyntaxTriviaList _list; + + public Reversed(in SyntaxTriviaList list) + { + _list = list; + } + + public Enumerator GetEnumerator() + { + return new Enumerator(in _list); + } + + IEnumerator IEnumerable.GetEnumerator() + { + if (_list.Count == 0) + { + return SpecializedCollections.EmptyEnumerator(); + } + + return new ReversedEnumeratorImpl(in _list); + } + + IEnumerator + IEnumerable.GetEnumerator() + { + if (_list.Count == 0) + { + return SpecializedCollections.EmptyEnumerator(); + } + + return new ReversedEnumeratorImpl(in _list); + } + + public override int GetHashCode() + { + return _list.GetHashCode(); + } + + public override bool Equals(object obj) + { + return obj is Reversed && Equals((Reversed)obj); + } + + public bool Equals(Reversed other) + { + return _list.Equals(other._list); + } + + [StructLayout(LayoutKind.Auto)] + public struct Enumerator + { + private readonly SyntaxNode _singleNodeOrList; + private readonly int _baseIndex; + private readonly int _count; + + private int _index; + private SyntaxNode _current; + private int _position; + + public Enumerator(in SyntaxTriviaList list) + : this() + { + if (list.Any()) + { + _singleNodeOrList = list.Node; + _baseIndex = list.Index; + _count = list.Count; + + _index = _count; + _current = null; + + var last = list.Last(); + _position = last.Position + last.FullWidth; + } + } + + public bool MoveNext() + { + if (_count == 0 || _index <= 0) + { + _current = null; + return false; + } + + _index--; + + _current = GetNodeAt(_singleNodeOrList, _index); + _position -= _current.FullWidth; + + return true; + } + + public SyntaxTrivia Current + { + get + { + if (_current == null) + { + throw new InvalidOperationException(); + } + + return (SyntaxTrivia)_current; + } + } + } + + private class ReversedEnumeratorImpl : IEnumerator + { + private Enumerator _enumerator; + + // SyntaxTriviaList is a relatively big struct so is passed as ref + internal ReversedEnumeratorImpl(in SyntaxTriviaList list) + { + _enumerator = new Enumerator(in list); + } + + public SyntaxTrivia Current => _enumerator.Current; + + object IEnumerator.Current => _enumerator.Current; + + public bool MoveNext() + { + return _enumerator.MoveNext(); + } + + public void Reset() + { + throw new NotSupportedException(); + } + + public void Dispose() + { + } + } + } + + [StructLayout(LayoutKind.Auto)] + public struct Enumerator + { + private SyntaxNode _singleNodeOrList; + private int _baseIndex; + private int _count; + + private int _index; + private SyntaxNode _current; + private int _position; + + internal Enumerator(in SyntaxTriviaList list) + { + _singleNodeOrList = list.Node; + _baseIndex = list.Index; + _count = list.Count; + + _index = -1; + _current = null; + _position = list.Position; + } + + private void InitializeFrom(SyntaxNode node, int index, int position) + { + _singleNodeOrList = node; + _baseIndex = index; + _count = node.IsList ? node.SlotCount : 1; + + _index = -1; + _current = null; + _position = position; + } + + // PERF: Used to initialize an enumerator for leading trivia directly from a token. + // This saves constructing an intermediate SyntaxTriviaList. Also, passing token + // by ref since it's a non-trivial struct + internal void InitializeFromLeadingTrivia(in SyntaxToken token) + { + InitializeFrom(token.GetLeadingTrivia().Node, 0, token.Position); + } + + // PERF: Used to initialize an enumerator for trailing trivia directly from a token. + // This saves constructing an intermediate SyntaxTriviaList. Also, passing token + // by ref since it's a non-trivial struct + internal void InitializeFromTrailingTrivia(in SyntaxToken token) + { + var leading = token.GetLeadingTrivia().Node; + var index = 0; + if (leading != null) + { + index = leading.IsList ? leading.SlotCount : 1; + } + + var trailing = token.GetTrailingTrivia().Node; + var trailingPosition = token.Position + token.FullWidth; + if (trailing != null) + { + trailingPosition -= trailing.FullWidth; + } + + InitializeFrom(trailing, index, trailingPosition); + } + + public bool MoveNext() + { + var newIndex = _index + 1; + if (newIndex >= _count) + { + // invalidate iterator + _current = null; + return false; + } + + _index = newIndex; + + if (_current != null) + { + _position += _current.FullWidth; + } + + _current = GetNodeAt(_singleNodeOrList, newIndex); + return true; + } + + public SyntaxTrivia Current + { + get + { + if (_current == null) + { + throw new InvalidOperationException(); + } + + return _current as SyntaxTrivia; + } + } + + internal bool TryMoveNextAndGetCurrent(out SyntaxTrivia current) + { + if (!MoveNext()) + { + current = default; + return false; + } + + current = _current as SyntaxTrivia; + return true; + } + } + + private class EnumeratorImpl : IEnumerator + { + private Enumerator _enumerator; + + // SyntaxTriviaList is a relatively big struct so is passed as ref + internal EnumeratorImpl(in SyntaxTriviaList list) + { + _enumerator = new Enumerator(list); + } + + public SyntaxTrivia Current => _enumerator.Current; + + object IEnumerator.Current => _enumerator.Current; + + public bool MoveNext() + { + return _enumerator.MoveNext(); + } + + public void Reset() + { + throw new NotSupportedException(); + } + + public void Dispose() + { + } + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxTriviaListBuilder.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxTriviaListBuilder.cs new file mode 100644 index 0000000000..aa0a8caaec --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxTriviaListBuilder.cs @@ -0,0 +1,138 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + internal class SyntaxTriviaListBuilder + { + private SyntaxTrivia[] _nodes; + + public SyntaxTriviaListBuilder(int size) + { + _nodes = new SyntaxTrivia[size]; + } + + public static SyntaxTriviaListBuilder Create() + { + return new SyntaxTriviaListBuilder(4); + } + + public static SyntaxTriviaList Create(IEnumerable trivia) + { + if (trivia == null) + { + return new SyntaxTriviaList(); + } + + var builder = Create(); + builder.AddRange(trivia); + return builder.ToList(); + } + + public int Count { get; private set; } + + public void Clear() + { + Count = 0; + } + + public SyntaxTrivia this[int index] + { + get + { + if (index < 0 || index > Count) + { + throw new IndexOutOfRangeException(); + } + + return _nodes[index]; + } + } + + public void AddRange(IEnumerable items) + { + if (items != null) + { + foreach (var item in items) + { + Add(item); + } + } + } + + public SyntaxTriviaListBuilder Add(SyntaxTrivia item) + { + if (_nodes == null || Count >= _nodes.Length) + { + Grow(Count == 0 ? 8 : _nodes.Length * 2); + } + + _nodes[Count++] = item; + return this; + } + + public void Add(SyntaxTrivia[] items) + { + Add(items, 0, items.Length); + } + + public void Add(SyntaxTrivia[] items, int offset, int length) + { + if (_nodes == null || Count + length > _nodes.Length) + { + Grow(Count + length); + } + + Array.Copy(items, offset, _nodes, Count, length); + Count += length; + } + + public void Add(in SyntaxTriviaList list) + { + Add(list, 0, list.Count); + } + + public void Add(in SyntaxTriviaList list, int offset, int length) + { + if (_nodes == null || Count + length > _nodes.Length) + { + Grow(Count + length); + } + + list.CopyTo(offset, _nodes, Count, length); + Count += length; + } + + private void Grow(int size) + { + var tmp = new SyntaxTrivia[size]; + Array.Copy(_nodes, tmp, _nodes.Length); + _nodes = tmp; + } + + public static implicit operator SyntaxTriviaList(SyntaxTriviaListBuilder builder) + { + return builder.ToList(); + } + + public SyntaxTriviaList ToList() + { + if (Count > 0) + { + var tmp = new ArrayElement[Count]; + for (var i = 0; i < Count; i++) + { + tmp[i].Value = _nodes[i].Green; + } + return new SyntaxTriviaList(InternalSyntax.SyntaxList.List(tmp).CreateRed(), position: 0, index: 0); + } + else + { + return default(SyntaxTriviaList); + } + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxVisitor.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxVisitor.cs new file mode 100644 index 0000000000..2ff23683f6 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxVisitor.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + /// + /// Represents a visitor that visits only the single SyntaxNode + /// passed into its Visit method and produces + /// a value of the type specified by the parameter. + /// + /// + /// The type of the return value this visitor's Visit method. + /// + internal abstract partial class SyntaxVisitor + { + public virtual TResult Visit(SyntaxNode node) + { + if (node != null) + { + return node.Accept(this); + } + + return default(TResult); + } + + public virtual TResult VisitToken(SyntaxToken token) + { + return DefaultVisit(token); + } + + public virtual TResult VisitTrivia(SyntaxTrivia trivia) + { + return DefaultVisit(trivia); + } + + protected virtual TResult DefaultVisit(SyntaxNode node) + { + return default(TResult); + } + } + + /// + /// Represents a visitor that visits only the single SyntaxNode + /// passed into its Visit method. + /// + internal abstract partial class SyntaxVisitor + { + public virtual void Visit(SyntaxNode node) + { + if (node != null) + { + node.Accept(this); + } + } + + public virtual void VisitToken(SyntaxToken token) + { + DefaultVisit(token); + } + + public virtual void VisitTrivia(SyntaxTrivia trivia) + { + DefaultVisit(trivia); + } + + public virtual void DefaultVisit(SyntaxNode node) + { + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxWalker.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxWalker.cs new file mode 100644 index 0000000000..214a0039fe --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/SyntaxWalker.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + /// + /// Represents a that descends an entire graph + /// visiting each SyntaxNode and its child SyntaxNodes and s in depth-first order. + /// + internal abstract class SyntaxWalker : SyntaxVisitor + { + public override void Visit(SyntaxNode node) + { + node?.Accept(this); + } + + public override void DefaultVisit(SyntaxNode node) + { + var children = node.ChildNodes(); + for (var i = 0; i < children.Count; i++) + { + var child = children[i]; + Visit(child); + } + } + + public override void VisitToken(SyntaxToken token) + { + VisitLeadingTrivia(token); + VisitTrailingTrivia(token); + } + + public virtual void VisitLeadingTrivia(SyntaxToken token) + { + if (token.HasLeadingTrivia) + { + foreach (var trivia in token.GetLeadingTrivia()) + { + VisitTrivia(trivia); + } + } + } + + public virtual void VisitTrailingTrivia(SyntaxToken token) + { + if (token.HasTrailingTrivia) + { + foreach (var trivia in token.GetTrailingTrivia()) + { + VisitTrivia(trivia); + } + } + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/TextSpan.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/TextSpan.cs new file mode 100644 index 0000000000..e80562b01b --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/Syntax/TextSpan.cs @@ -0,0 +1,261 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Razor.Language.Syntax +{ + /// + /// Immutable abstract representation of a span of text. For example, in an error diagnostic that reports a + /// location, it could come from a parsed string, text from a tool editor buffer, etc. + /// + internal readonly struct TextSpan : IEquatable, IComparable + { + /// + /// Creates a TextSpan instance beginning with the position Start and having the Length + /// specified with . + /// + public TextSpan(int start, int length) + { + if (start < 0) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + + if (start + length < start) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + Start = start; + Length = length; + } + + /// + /// Start point of the span. + /// + public int Start { get; } + + /// + /// End of the span. + /// + public int End => Start + Length; + + /// + /// Length of the span. + /// + public int Length { get; } + + /// + /// Determines whether or not the span is empty. + /// + public bool IsEmpty => Length == 0; + + /// + /// Determines whether the position lies within the span. + /// + /// + /// The position to check. + /// + /// + /// true if the position is greater than or equal to Start and strictly less + /// than End, otherwise false. + /// + public bool Contains(int position) + { + return unchecked((uint)(position - Start) < (uint)Length); + } + + /// + /// Determines whether falls completely within this span. + /// + /// + /// The span to check. + /// + /// + /// true if the specified span falls completely within this span, otherwise false. + /// + public bool Contains(TextSpan span) + { + return span.Start >= Start && span.End <= End; + } + + /// + /// Determines whether overlaps this span. Two spans are considered to overlap + /// if they have positions in common and neither is empty. Empty spans do not overlap with any + /// other span. + /// + /// + /// The span to check. + /// + /// + /// true if the spans overlap, otherwise false. + /// + public bool OverlapsWith(TextSpan span) + { + var overlapStart = Math.Max(Start, span.Start); + var overlapEnd = Math.Min(End, span.End); + + return overlapStart < overlapEnd; + } + + /// + /// Returns the overlap with the given span, or null if there is no overlap. + /// + /// + /// The span to check. + /// + /// + /// The overlap of the spans, or null if the overlap is empty. + /// + public TextSpan? Overlap(TextSpan span) + { + var overlapStart = Math.Max(Start, span.Start); + var overlapEnd = Math.Min(End, span.End); + + return overlapStart < overlapEnd + ? FromBounds(overlapStart, overlapEnd) + : (TextSpan?)null; + } + + /// + /// Determines whether intersects this span. Two spans are considered to + /// intersect if they have positions in common or the end of one span + /// coincides with the start of the other span. + /// + /// + /// The span to check. + /// + /// + /// true if the spans intersect, otherwise false. + /// + public bool IntersectsWith(TextSpan span) + { + return span.Start <= End && span.End >= Start; + } + + /// + /// Determines whether intersects this span. + /// A position is considered to intersect if it is between the start and + /// end positions (inclusive) of this span. + /// + /// + /// The position to check. + /// + /// + /// true if the position intersects, otherwise false. + /// + public bool IntersectsWith(int position) + { + return unchecked((uint)(position - Start) <= (uint)Length); + } + + /// + /// Returns the intersection with the given span, or null if there is no intersection. + /// + /// + /// The span to check. + /// + /// + /// The intersection of the spans, or null if the intersection is empty. + /// + public TextSpan? Intersection(TextSpan span) + { + var intersectStart = Math.Max(Start, span.Start); + var intersectEnd = Math.Min(End, span.End); + + return intersectStart <= intersectEnd + ? FromBounds(intersectStart, intersectEnd) + : (TextSpan?)null; + } + + /// + /// Creates a new from and positions as opposed to a position and length. + /// + /// The returned TextSpan contains the range with inclusive, + /// and exclusive. + /// + public static TextSpan FromBounds(int start, int end) + { + if (start < 0) + { + throw new ArgumentOutOfRangeException(nameof(start), "start must not be negative"); + } + + if (end < start) + { + throw new ArgumentOutOfRangeException(nameof(end), "end must not be less than start"); + } + + return new TextSpan(start, end - start); + } + + /// + /// Determines if two instances of are the same. + /// + public static bool operator ==(TextSpan left, TextSpan right) + { + return left.Equals(right); + } + + /// + /// Determines if two instances of are different. + /// + public static bool operator !=(TextSpan left, TextSpan right) + { + return !left.Equals(right); + } + + /// + /// Determines if current instance of is equal to another. + /// + public bool Equals(TextSpan other) + { + return Start == other.Start && Length == other.Length; + } + + /// + /// Determines if current instance of is equal to another. + /// + public override bool Equals(object obj) + { + return obj is TextSpan && Equals((TextSpan)obj); + } + + /// + /// Produces a hash code for . + /// + public override int GetHashCode() + { + var combiner = new HashCodeCombiner(); + combiner.Add(Start); + combiner.Add(Length); + + return combiner.CombinedHash; + } + + /// + /// Provides a string representation for . + /// + public override string ToString() + { + return $"[{Start}..{End})"; + } + + /// + /// Compares current instance of with another. + /// + public int CompareTo(TextSpan other) + { + var diff = Start - other.Start; + if (diff != 0) + { + return diff; + } + + return Length - other.Length; + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/TagHelperAttributeInfo.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/TagHelperAttributeInfo.cs new file mode 100644 index 0000000000..a3f15f1708 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/TagHelperAttributeInfo.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal class TagHelperAttributeInfo + { + public TagHelperAttributeInfo( + string name, + AttributeStructure attributeStructure, + bool bound) + { + Name = name; + AttributeStructure = attributeStructure; + Bound = bound; + } + + public string Name { get; } + + public AttributeStructure AttributeStructure { get; } + + public bool Bound { get; } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/TagHelperConventions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/TagHelperConventions.cs index 6e33901d0e..60618a61f4 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/TagHelperConventions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/TagHelperConventions.cs @@ -6,5 +6,7 @@ namespace Microsoft.AspNetCore.Razor.Language public static class TagHelperConventions { public static readonly string DefaultKind = "ITagHelper"; + + public static readonly string ComponentKind = "IComponent"; } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/TagHelperInfo.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/TagHelperInfo.cs new file mode 100644 index 0000000000..3cc665b4fd --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/TagHelperInfo.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal class TagHelperInfo + { + public TagHelperInfo( + string tagName, + TagMode tagMode, + TagHelperBinding bindingResult) + { + TagName = tagName; + TagMode = tagMode; + BindingResult = bindingResult; + } + + public string TagName { get; } + + public TagMode TagMode { get; } + + public TagHelperBinding BindingResult { get; } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Language/TagHelperSpanVisitor.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/TagHelperSpanVisitor.cs new file mode 100644 index 0000000000..836a45ae98 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Language/TagHelperSpanVisitor.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal class TagHelperSpanVisitor : SyntaxWalker + { + private RazorSourceDocument _source; + private List _spans; + + public TagHelperSpanVisitor(RazorSourceDocument source) + { + _source = source; + _spans = new List(); + } + + public IReadOnlyList TagHelperSpans => _spans; + + public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax node) + { + var span = new TagHelperSpanInternal(node.GetSourceSpan(_source), node.TagHelperInfo.BindingResult); + _spans.Add(span); + + base.VisitMarkupTagHelperElement(node); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Client.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Client.cs index 9378906d45..cf94173337 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Client.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Client.cs @@ -4,7 +4,7 @@ using System; using System.IO; using System.IO.Pipes; -#if NET46 +#if NETFRAMEWORK using System.Security.AccessControl; using System.Security.Principal; #endif @@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Razor.Tools ServerLogger.Log("Named pipe '{0}' connected", pipeName); cancellationToken.ThrowIfCancellationRequested(); -#if NET46 +#if NETFRAMEWORK // Verify that we own the pipe. if (!CheckPipeConnectionOwnership(stream)) { @@ -89,7 +89,7 @@ namespace Microsoft.AspNetCore.Razor.Tools } } -#if NET46 +#if NETFRAMEWORK /// /// Check to ensure that the named pipe server we connected to is owned by the same /// user. diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs index e699e24532..3ba5eb5ab5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs @@ -141,6 +141,7 @@ namespace Microsoft.AspNetCore.Razor.Tools b.Features.Add(new DefaultMetadataReferenceFeature() { References = metadataReferences }); b.Features.Add(new CompilationTagHelperFeature()); b.Features.Add(new DefaultTagHelperDescriptorProvider()); + b.Features.Add(new ComponentTagHelperDescriptorProvider()); }); var feature = engine.Engine.Features.OfType().Single(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs index a1089e4150..338c496033 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs @@ -5,8 +5,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; using Microsoft.Extensions.CommandLineUtils; using Microsoft.VisualStudio.LanguageServices.Razor.Serialization; using Newtonsoft.Json; @@ -21,12 +23,14 @@ namespace Microsoft.AspNetCore.Razor.Tools Sources = Option("-s", ".cshtml files to compile", CommandOptionType.MultipleValue); Outputs = Option("-o", "Generated output file path", CommandOptionType.MultipleValue); RelativePaths = Option("-r", "Relative path", CommandOptionType.MultipleValue); + DocumentKinds = Option("-k", "Document kind", CommandOptionType.MultipleValue); ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue); TagHelperManifest = Option("-t", "tag helper manifest file", CommandOptionType.SingleValue); Version = Option("-v|--version", "Razor language version", CommandOptionType.SingleValue); Configuration = Option("-c", "Razor configuration name", CommandOptionType.SingleValue); ExtensionNames = Option("-n", "extension name", CommandOptionType.MultipleValue); ExtensionFilePaths = Option("-e", "extension file path", CommandOptionType.MultipleValue); + GenerateDeclaration = Option("--generate-declaration", "Generate declaration", CommandOptionType.NoValue); } public CommandOption Sources { get; } @@ -35,6 +39,8 @@ namespace Microsoft.AspNetCore.Razor.Tools public CommandOption RelativePaths { get; } + public CommandOption DocumentKinds { get; } + public CommandOption ProjectDirectory { get; } public CommandOption TagHelperManifest { get; } @@ -47,6 +53,8 @@ namespace Microsoft.AspNetCore.Razor.Tools public CommandOption ExtensionFilePaths { get; } + public CommandOption GenerateDeclaration { get; } + protected override Task ExecuteCoreAsync() { if (!Parent.Checker.Check(ExtensionFilePaths.Values)) @@ -66,13 +74,13 @@ namespace Microsoft.AspNetCore.Razor.Tools var version = RazorLanguageVersion.Parse(Version.Value()); var configuration = RazorConfiguration.Create(version, Configuration.Value(), extensions); + var sourceItems = GetSourceItems(ProjectDirectory.Value(), Sources.Values, Outputs.Values, RelativePaths.Values, DocumentKinds.Values); + var result = ExecuteCore( configuration: configuration, projectDirectory: ProjectDirectory.Value(), tagHelperManifest: TagHelperManifest.Value(), - sources: Sources.Values, - outputs: Outputs.Values, - relativePaths: RelativePaths.Values); + sourceItems: sourceItems); return Task.FromResult(result); } @@ -88,11 +96,21 @@ namespace Microsoft.AspNetCore.Razor.Tools if (Outputs.Values.Count != Sources.Values.Count) { Error.WriteLine($"{Sources.Description} has {Sources.Values.Count}, but {Outputs.Description} has {Outputs.Values.Count} values."); + return false; } if (RelativePaths.Values.Count != Sources.Values.Count) { Error.WriteLine($"{Sources.Description} has {Sources.Values.Count}, but {RelativePaths.Description} has {RelativePaths.Values.Count} values."); + return false; + } + + if (DocumentKinds.Values.Count != 0 && DocumentKinds.Values.Count != Sources.Values.Count) + { + // 2.x tasks do not specify DocumentKinds - in which case, no values will be present. If a kind for one document is specified, we expect as many kind entries + // as sources. + Error.WriteLine($"{Sources.Description} has {Sources.Values.Count}, but {DocumentKinds.Description} has {DocumentKinds.Values.Count} values."); + return false; } if (string.IsNullOrEmpty(ProjectDirectory.Value())) @@ -138,26 +156,30 @@ namespace Microsoft.AspNetCore.Razor.Tools RazorConfiguration configuration, string projectDirectory, string tagHelperManifest, - List sources, - List outputs, - List relativePaths) + SourceItem[] sourceItems) { tagHelperManifest = Path.Combine(projectDirectory, tagHelperManifest); var tagHelpers = GetTagHelpers(tagHelperManifest); - var inputItems = GetInputItems(projectDirectory, sources, outputs, relativePaths); + var compositeFileSystem = new CompositeRazorProjectFileSystem(new[] { - GetVirtualRazorProjectSystem(inputItems), + GetVirtualRazorProjectSystem(sourceItems), RazorProjectFileSystem.Create(projectDirectory), }); var engine = RazorProjectEngine.Create(configuration, compositeFileSystem, b => { b.Features.Add(new StaticTagHelperFeature() { TagHelpers = tagHelpers, }); + b.Features.Add(new InputDocumentKindClassifierPass(sourceItems)); + + if (GenerateDeclaration.HasValue()) + { + b.Features.Add(new SetSuppressPrimaryMethodBodyOptionFeature()); + } }); - var results = GenerateCode(engine, inputItems); + var results = GenerateCode(engine, sourceItems); var success = true; @@ -229,13 +251,15 @@ namespace Microsoft.AspNetCore.Razor.Tools } } - private SourceItem[] GetInputItems(string projectDirectory, List sources, List outputs, List relativePath) + private SourceItem[] GetSourceItems(string projectDirectory, List sources, List outputs, List relativePath, List documentKinds) { var items = new SourceItem[sources.Count]; for (var i = 0; i < items.Length; i++) { var outputPath = Path.Combine(projectDirectory, outputs[i]); - items[i] = new SourceItem(sources[i], outputs[i], relativePath[i]); + var documentKind = documentKinds.Count > 0 ? documentKinds[i] : "mvc"; + + items[i] = new SourceItem(sources[i], outputs[i], relativePath[i], documentKind); } return items; @@ -271,9 +295,9 @@ namespace Microsoft.AspNetCore.Razor.Tools public RazorCSharpDocument CSharpDocument { get; } } - private struct SourceItem + private readonly struct SourceItem { - public SourceItem(string sourcePath, string outputPath, string physicalRelativePath) + public SourceItem(string sourcePath, string outputPath, string physicalRelativePath, string documentKind) { SourcePath = sourcePath; OutputPath = outputPath; @@ -281,6 +305,7 @@ namespace Microsoft.AspNetCore.Razor.Tools FilePath = '/' + physicalRelativePath .Replace(Path.DirectorySeparatorChar, '/') .Replace("//", "/"); + DocumentKind = documentKind; } public string SourcePath { get; } @@ -290,6 +315,8 @@ namespace Microsoft.AspNetCore.Razor.Tools public string RelativePhysicalPath { get; } public string FilePath { get; } + + public string DocumentKind { get; } } private class StaticTagHelperFeature : ITagHelperFeature @@ -300,5 +327,46 @@ namespace Microsoft.AspNetCore.Razor.Tools public IReadOnlyList GetDescriptors() => TagHelpers; } + + private class SetSuppressPrimaryMethodBodyOptionFeature : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature + { + public int Order { get; set; } + + public void Configure(RazorCodeGenerationOptionsBuilder options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.SuppressPrimaryMethodBody = true; + } + } + + private class InputDocumentKindClassifierPass : RazorEngineFeatureBase, IRazorDocumentClassifierPass + { + public InputDocumentKindClassifierPass(SourceItem[] sourceItems) + { + DocumentKinds = new Dictionary(sourceItems.Length, StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < sourceItems.Length; i++) + { + var item = sourceItems[i]; + DocumentKinds[item.SourcePath] = item.DocumentKind; + } + } + + // Run before other document classifiers + public int Order => -1000; + + public Dictionary DocumentKinds { get; } + + public void Execute(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + if (DocumentKinds.TryGetValue(codeDocument.Source.FilePath, out var kind)) + { + codeDocument.SetInputDocumentKind(kind); + } + } + } } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj b/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj index e67f36e02f..a3ab14c41f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj @@ -1,15 +1,18 @@ - + Razor is a markup syntax for adding server-side logic to web pages. This assembly contains infrastructure supporting Razor MSBuild integration. - netcoreapp2.0 + netcoreapp3.0 Exe rzc - + false false + + + false diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Program.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Program.cs index 8d9c5518c5..3cd93a2335 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Program.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Program.cs @@ -40,6 +40,9 @@ namespace Microsoft.AspNetCore.Razor.Tools outputWriter.Dispose(); errorWriter.Dispose(); + Console.Write(output); + Console.Error.Write(error); + // This will no-op if server logging is not enabled. ServerLogger.Log(output); ServerLogger.Log(error); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Properties/AssemblyInfo.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Properties/AssemblyInfo.cs index 91b536e78b..3770a8065e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Properties/AssemblyInfo.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/Properties/AssemblyInfo.cs @@ -4,6 +4,6 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.Tools.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] -[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.Design.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.NET.Sdk.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/ServerProtocol/RequestArgument.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/ServerProtocol/RequestArgument.cs index fd15c40686..96e9ae0312 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/ServerProtocol/RequestArgument.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Tools/ServerProtocol/RequestArgument.cs @@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Razor.Tools /// Strings are encoded via a length prefix as a signed /// 32-bit integer, followed by an array of characters. /// - internal struct RequestArgument + internal readonly struct RequestArgument { public readonly ArgumentId Id; public readonly int ArgumentIndex; @@ -63,5 +63,10 @@ namespace Microsoft.AspNetCore.Razor.Tools // The directory to use for temporary operations. TempDirectory, } + + public override string ToString() + { + return $"{Id} {Value}"; + } } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultDocumentServiceProviderFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultDocumentServiceProviderFactory.cs new file mode 100644 index 0000000000..ceebd2813f --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultDocumentServiceProviderFactory.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Composition; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor +{ + [Export(typeof(DocumentServiceProviderFactory))] + internal class DefaultDocumentServiceProviderFactory : DocumentServiceProviderFactory + { + public override IDocumentServiceProvider Create(DocumentSnapshot document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + return new RazorDocumentServiceProvider(document); + } + + public override IDocumentServiceProvider CreateEmpty() + { + return new RazorDocumentServiceProvider(); + } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultProjectSnapshotProjectEngineFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultProjectSnapshotProjectEngineFactory.cs index cd9ac1ee51..8651a9ced6 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultProjectSnapshotProjectEngineFactory.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultProjectSnapshotProjectEngineFactory.cs @@ -32,13 +32,8 @@ namespace Microsoft.CodeAnalysis.Razor _factories = factories; } - public override RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure) + public override RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) { - if (project == null) - { - throw new ArgumentNullException(nameof(project)); - } - if (fileSystem == null) { throw new ArgumentNullException(nameof(fileSystem)); @@ -55,7 +50,7 @@ namespace Microsoft.CodeAnalysis.Razor // // We typically want this because the language adds features over time - we don't want to a bunch of errors // to show up when a document is first opened, and then go away when the configuration loads, we'd prefer the opposite. - var configuration = project.Configuration ?? DefaultConfiguration; + configuration = configuration ?? DefaultConfiguration; // If there's no factory to handle the configuration then fall back to a very basic configuration. // diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultRazorCompletionFactsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultRazorCompletionFactsService.cs new file mode 100644 index 0000000000..5d0c542e5f --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultRazorCompletionFactsService.cs @@ -0,0 +1,92 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; + +namespace Microsoft.CodeAnalysis.Razor +{ + [Shared] + [Export(typeof(RazorCompletionFactsService))] + internal class DefaultRazorCompletionFactsService : RazorCompletionFactsService + { + private static readonly IEnumerable DefaultDirectives = new[] + { + CSharpCodeParser.AddTagHelperDirectiveDescriptor, + CSharpCodeParser.RemoveTagHelperDirectiveDescriptor, + CSharpCodeParser.TagHelperPrefixDirectiveDescriptor, + }; + + public override IReadOnlyList GetCompletionItems(RazorSyntaxTree syntaxTree, SourceSpan location) + { + var completionItems = new List(); + + if (AtDirectiveCompletionPoint(syntaxTree, location)) + { + var directiveCompletions = GetDirectiveCompletionItems(syntaxTree); + completionItems.AddRange(directiveCompletions); + } + + return completionItems; + } + + // Internal for testing + internal static List GetDirectiveCompletionItems(RazorSyntaxTree syntaxTree) + { + var directives = syntaxTree.Options.Directives.Concat(DefaultDirectives); + var completionItems = new List(); + foreach (var directive in directives) + { + var completionDisplayText = directive.DisplayName ?? directive.Directive; + var completionItem = new RazorCompletionItem( + completionDisplayText, + directive.Directive, + directive.Description, + RazorCompletionItemKind.Directive); + completionItems.Add(completionItem); + } + + return completionItems; + } + + // Internal for testing + internal static bool AtDirectiveCompletionPoint(RazorSyntaxTree syntaxTree, SourceSpan location) + { + if (syntaxTree == null) + { + return false; + } + + var change = new SourceChange(location, string.Empty); + var owner = syntaxTree.Root.LocateOwner(change); + + if (owner == null) + { + return false; + } + + // Do not provide IntelliSense for explicit expressions. Explicit expressions will usually look like: + // [@] [(] [DateTime.Now] [)] + var isImplicitExpression = owner.FirstAncestorOrSelf() != null; + if (isImplicitExpression && + owner.ChildNodes().All(n => n.IsToken && IsDirectiveCompletableToken((AspNetCore.Razor.Language.Syntax.SyntaxToken)n))) + { + return true; + } + + return false; + } + + // Internal for testing + internal static bool IsDirectiveCompletableToken(AspNetCore.Razor.Language.Syntax.SyntaxToken token) + { + return token.Kind == SyntaxKind.Identifier || + // Marker symbol + token.Kind == SyntaxKind.Marker; + } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentServiceProviderFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentServiceProviderFactory.cs new file mode 100644 index 0000000000..2abe4c61de --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentServiceProviderFactory.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal abstract class DocumentServiceProviderFactory + { + public abstract IDocumentServiceProvider CreateEmpty(); + + public abstract IDocumentServiceProvider Create(DocumentSnapshot document); + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/IDocumentExcerptService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/IDocumentExcerptService.cs new file mode 100644 index 0000000000..8729432a08 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/IDocumentExcerptService.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if DOCUMENT_SERVICE_FACTORY + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Classification; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Host +{ + /// + /// excerpt some part of + /// + internal interface IDocumentExcerptService : IDocumentService + { + /// + /// return of given and + /// + /// the result might not be an exact copy of the given source or contains more then given span + /// + Task TryExcerptAsync(Document document, TextSpan span, ExcerptMode mode, CancellationToken cancellationToken); + } + + /// + /// this mode shows intention not actual behavior. it is up to implementation how to interpret the intention. + /// + internal enum ExcerptMode + { + SingleLine, + Tooltip + } + + /// + /// Result of excerpt + /// + internal struct ExcerptResult + { + /// + /// excerpt content + /// + public readonly SourceText Content; + + /// + /// span on that given got mapped to + /// + public readonly TextSpan MappedSpan; + + /// + /// classification information on the + /// + public readonly ImmutableArray ClassifiedSpans; + + /// + /// this excerpt is from + /// + public readonly Document Document; + + /// + /// span on this excerpt is from + /// + public readonly TextSpan Span; + + public ExcerptResult(SourceText content, TextSpan mappedSpan, ImmutableArray classifiedSpans, Document document, TextSpan span) + { + Content = content; + MappedSpan = mappedSpan; + ClassifiedSpans = classifiedSpans; + + // these 2 might not actually needed + Document = document; + Span = span; + } + } +} + +#endif \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/IDocumentOperationService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/IDocumentOperationService.cs new file mode 100644 index 0000000000..ae3e5f2191 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/IDocumentOperationService.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if DOCUMENT_SERVICE_FACTORY + +namespace Microsoft.CodeAnalysis.Host +{ + /// + /// provide various operations for this document + /// + /// I followed name from EditorOperation for now. + /// + internal interface IDocumentOperationService : IDocumentService + { + /// + /// document version of + /// + bool CanApplyChange { get; } + + /// + /// indicates whether this document supports diagnostics or not + /// + bool SupportDiagnostics { get; } + } +} + +#endif \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/IDocumentService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/IDocumentService.cs new file mode 100644 index 0000000000..fe6b075d0e --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/IDocumentService.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if DOCUMENT_SERVICE_FACTORY + +namespace Microsoft.CodeAnalysis.Host +{ + /// + /// Empty interface just to mark document services. + /// + internal interface IDocumentService + { + } +} + +#endif diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/IDocumentServiceProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/IDocumentServiceProvider.cs new file mode 100644 index 0000000000..ba36d6fe04 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/IDocumentServiceProvider.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if DOCUMENT_SERVICE_FACTORY + +namespace Microsoft.CodeAnalysis.Host +{ + internal interface IDocumentServiceProvider + { + /// + /// Gets a document specific service provided by the host identified by the service type. + /// If the host does not provide the service, this method returns null. + /// + TService GetService() where TService : class, IDocumentService; + } +} + +#endif \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/ISpanMappingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/ISpanMappingService.cs new file mode 100644 index 0000000000..4c32f09180 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Host/ISpanMappingService.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#if DOCUMENT_SERVICE_FACTORY + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Host +{ + /// + /// Map spans in a document to other spans even in other document + /// + /// this will be used by various features if provided to convert span in one document to other spans. + /// + /// for example, it is used to show spans users expect in a razor file rather than spans in + /// auto generated file that is implementation detail or navigate to the right place rather + /// than the generated file and etc. + /// + internal interface ISpanMappingService : IDocumentService + { + /// + /// Map spans in the document to more appropriate locations + /// + /// in current design, this can NOT map a span to a span that is not backed by a file. + /// for example, roslyn supports someone to have a document that is not backed by a file. and current design doesn't allow + /// such document to be returned from this API + /// for example, span on razor secondary buffer document in roslyn solution mapped to a span on razor cshtml file is possible but + /// a span on razor cshtml file to a span on secondary buffer document is not possible since secondary buffer document is not backed by a file + /// + /// Document given spans belong to + /// Spans in the document + /// Cancellation token + /// Return mapped span. order of result should be same as the given span + Task> MapSpansAsync(Document document, IEnumerable spans, CancellationToken cancellationToken); + } + + /// + /// Result of span mapping + /// + internal struct MappedSpanResult + { + /// + /// Path to mapped file + /// + public readonly string FilePath; + + /// + /// LinePosition representation of the Span + /// + public readonly LinePositionSpan LinePositionSpan; + + /// + /// Mapped span + /// + public readonly TextSpan Span; + + public MappedSpanResult(string filePath, LinePositionSpan linePositionSpan, TextSpan span) + { + FilePath = filePath; + LinePositionSpan = linePositionSpan; + Span = span; + } + } +} + +#endif diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj index ca09c13e78..7849c20439 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj @@ -2,8 +2,9 @@ Razor is a markup syntax for adding server-side logic to web pages. This package contains the Razor design-time infrastructure. - net46;netstandard2.0 + netstandard2.0 false + $(DefineConstants);DOCUMENT_SERVICE_FACTORY diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSnapshotProjectEngineFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSnapshotProjectEngineFactory.cs index cf2341878d..0f44e7c4c7 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSnapshotProjectEngineFactory.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSnapshotProjectEngineFactory.cs @@ -45,7 +45,36 @@ namespace Microsoft.CodeAnalysis.Razor return Create(project, RazorProjectFileSystem.Create(Path.GetDirectoryName(project.FilePath)), configure); } - public abstract RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure); + public RazorProjectEngine Create(ProjectSnapshot project, RazorProjectFileSystem fileSystem, Action configure) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + if (fileSystem == null) + { + throw new ArgumentNullException(nameof(fileSystem)); + } + + return Create(project.Configuration, fileSystem, configure); + } + + public RazorProjectEngine Create(RazorConfiguration configuration, string directoryPath, Action configure) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + if (directoryPath == null) + { + throw new ArgumentNullException(nameof(directoryPath)); + } + + return Create(configuration, RazorProjectFileSystem.Create(directoryPath), configure); + } + + public abstract RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs index d25b38ddbe..cf5c1a7766 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs @@ -23,11 +23,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem throw new ArgumentNullException(nameof(state)); } - Project = project; + ProjectInternal = project; State = state; } - public DefaultProjectSnapshot Project { get; } + public DefaultProjectSnapshot ProjectInternal { get; } public DocumentState State { get; } @@ -35,9 +35,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public override string TargetPath => State.HostDocument.TargetPath; + public override ProjectSnapshot Project => ProjectInternal; + + public override bool SupportsOutput => true; + public override IReadOnlyList GetImports() { - return State.Imports.GetImports(Project, this); + return State.GetImports(ProjectInternal); } public override Task GetTextAsync() @@ -50,10 +54,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return State.GetTextVersionAsync(); } - public override Task GetGeneratedOutputAsync() + public override async Task GetGeneratedOutputAsync() { - // IMPORTANT: Don't put more code here. We want this to return a cached task. - return State.GeneratedOutput.GetGeneratedOutputInitializationTask(Project, this); + var (output, _, _) = await State.GetGeneratedOutputAndVersionAsync(ProjectInternal, this).ConfigureAwait(false); + return output; + } + + public override async Task GetGeneratedOutputVersionAsync() + { + var (_, _, version) = await State.GetGeneratedOutputAndVersionAsync(ProjectInternal, this).ConfigureAwait(false); + return version; } public override bool TryGetText(out SourceText result) @@ -68,14 +78,26 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public override bool TryGetGeneratedOutput(out RazorCodeDocument result) { - if (State.GeneratedOutput.IsResultAvailable) + if (State.IsGeneratedOutputResultAvailable) { - result = State.GeneratedOutput.GetGeneratedOutputInitializationTask(Project, this).Result; + result = State.GetGeneratedOutputAndVersionAsync(ProjectInternal, this).Result.output; return true; } result = null; return false; } + + public override bool TryGetGeneratedOutputVersionAsync(out VersionStamp result) + { + if (State.IsGeneratedOutputResultAvailable) + { + result = State.GetGeneratedOutputAndVersionAsync(ProjectInternal, this).Result.outputVersion; + return true; + } + + result = default(VersionStamp); + return false; + } } } \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultImportDocumentSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultImportDocumentSnapshot.cs new file mode 100644 index 0000000000..d73446362d --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultImportDocumentSnapshot.cs @@ -0,0 +1,95 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class DefaultImportDocumentSnapshot : DocumentSnapshot + { + private ProjectSnapshot _project; + private RazorProjectItem _importItem; + private SourceText _sourceText; + private VersionStamp _version; + + public DefaultImportDocumentSnapshot(ProjectSnapshot project, RazorProjectItem item) + { + _project = project; + _importItem = item; + _version = VersionStamp.Default; + } + + public override string FilePath => null; + + public override string TargetPath => null; + + public override bool SupportsOutput => false; + + public override ProjectSnapshot Project => _project; + + public override Task GetGeneratedOutputAsync() + { + throw new NotSupportedException(); + } + + public override Task GetGeneratedOutputVersionAsync() + { + throw new NotSupportedException(); + } + + public override IReadOnlyList GetImports() + { + return Array.Empty(); + } + + public async override Task GetTextAsync() + { + using (var stream = _importItem.Read()) + using (var reader = new StreamReader(stream)) + { + var content = await reader.ReadToEndAsync(); + _sourceText = SourceText.From(content); + } + + return _sourceText; + } + + public override Task GetTextVersionAsync() + { + return Task.FromResult(_version); + } + + public override bool TryGetText(out SourceText result) + { + if (_sourceText != null) + { + result = _sourceText; + return true; + } + + result = null; + return false; + } + + public override bool TryGetTextVersion(out VersionStamp result) + { + result = _version; + return true; + } + + public override bool TryGetGeneratedOutput(out RazorCodeDocument result) + { + throw new NotSupportedException(); + } + + public override bool TryGetGeneratedOutputVersionAsync(out VersionStamp result) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs index aba5e7a916..dff471af3d 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshot.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; @@ -58,22 +59,50 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem } } + public override bool IsImportDocument(DocumentSnapshot document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + return State.ImportsToRelatedDocuments.ContainsKey(document.TargetPath); + } + + public override IEnumerable GetRelatedDocuments(DocumentSnapshot document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + if (State.ImportsToRelatedDocuments.TryGetValue(document.TargetPath, out var relatedDocuments)) + { + lock (_lock) + { + return relatedDocuments.Select(GetDocument).ToArray(); + } + } + + return Array.Empty(); + } + public override RazorProjectEngine GetProjectEngine() { - return State.ProjectEngine.GetProjectEngine(this); + return State.ProjectEngine; } public override Task> GetTagHelpersAsync() { // IMPORTANT: Don't put more code here. We want this to return a cached task. - return State.TagHelpers.GetTagHelperInitializationTask(this); + return State.GetTagHelpersAsync(this); } public override bool TryGetTagHelpers(out IReadOnlyList result) { - if (State.TagHelpers.IsResultAvailable) + if (State.IsTagHelperResultAvailable) { - result = State.TagHelpers.GetTagHelperInitializationTask(this).Result; + result = State.GetTagHelpersAsync(this).Result; return true; } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs index 5a810cc803..9953878518 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectSnapshotManager.cs @@ -90,12 +90,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem var projects = new ProjectSnapshot[_projects.Count]; foreach (var entry in _projects) { - if (entry.Value.Snapshot == null) - { - entry.Value.Snapshot = new DefaultProjectSnapshot(entry.Value.State); - } - - projects[i++] = entry.Value.Snapshot; + projects[i++] = entry.Value.GetSnapshot(); } return projects; @@ -115,12 +110,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem if (_projects.TryGetValue(filePath, out var entry)) { - if (entry.Snapshot == null) - { - entry.Snapshot = new DefaultProjectSnapshot(entry.State); - } - - return entry.Snapshot; + return entry.GetSnapshot(); } return null; @@ -175,8 +165,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Document updates can no-op. if (!object.ReferenceEquals(state, entry.State)) { - _projects[hostProject.FilePath] = new Entry(state); - NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, document.FilePath, ProjectChangeKind.DocumentAdded)); + var oldSnapshot = entry.GetSnapshot(); + entry = new Entry(state); + _projects[hostProject.FilePath] = entry; + NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), document.FilePath, ProjectChangeKind.DocumentAdded)); } } } @@ -201,8 +193,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Document updates can no-op. if (!object.ReferenceEquals(state, entry.State)) { - _projects[hostProject.FilePath] = new Entry(state); - NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, document.FilePath, ProjectChangeKind.DocumentRemoved)); + var oldSnapshot = entry.GetSnapshot(); + entry = new Entry(state); + _projects[hostProject.FilePath] = entry; + NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), document.FilePath, ProjectChangeKind.DocumentRemoved)); } } } @@ -229,12 +223,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem entry.State.Documents.TryGetValue(documentFilePath, out var older)) { ProjectState state; - SourceText olderText; - VersionStamp olderVersion; var currentText = sourceText; - if (older.TryGetText(out olderText) && - older.TryGetTextVersion(out olderVersion)) + if (older.TryGetText(out var olderText) && + older.TryGetTextVersion(out var olderVersion)) { var version = currentText.ContentEquals(olderText) ? olderVersion : olderVersion.GetNewerVersion(); state = entry.State.WithChangedHostDocument(older.HostDocument, currentText, version); @@ -256,8 +248,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Document updates can no-op. if (!object.ReferenceEquals(state, entry.State)) { - _projects[projectFilePath] = new Entry(state); - NotifyListeners(new ProjectChangeEventArgs(projectFilePath, documentFilePath, ProjectChangeKind.DocumentChanged)); + var oldSnapshot = entry.GetSnapshot(); + entry = new Entry(state); + _projects[projectFilePath] = entry; + NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), documentFilePath, ProjectChangeKind.DocumentChanged)); } } } @@ -293,8 +287,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Document updates can no-op. if (!object.ReferenceEquals(state, entry.State)) { - _projects[projectFilePath] = new Entry(state); - NotifyListeners(new ProjectChangeEventArgs(projectFilePath, documentFilePath, ProjectChangeKind.DocumentChanged)); + var oldSnapshot = entry.GetSnapshot(); + entry = new Entry(state); + _projects[projectFilePath] = entry; + NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), documentFilePath, ProjectChangeKind.DocumentChanged)); } } } @@ -321,12 +317,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem entry.State.Documents.TryGetValue(documentFilePath, out var older)) { ProjectState state; - SourceText olderText; - VersionStamp olderVersion; var currentText = sourceText; - if (older.TryGetText(out olderText) && - older.TryGetTextVersion(out olderVersion)) + if (older.TryGetText(out var olderText) && + older.TryGetTextVersion(out var olderVersion)) { var version = currentText.ContentEquals(olderText) ? olderVersion : olderVersion.GetNewerVersion(); state = entry.State.WithChangedHostDocument(older.HostDocument, currentText, version); @@ -346,8 +340,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Document updates can no-op. if (!object.ReferenceEquals(state, entry.State)) { - _projects[projectFilePath] = new Entry(state); - NotifyListeners(new ProjectChangeEventArgs(projectFilePath, documentFilePath, ProjectChangeKind.DocumentChanged)); + var oldSnapshot = entry.GetSnapshot(); + entry = new Entry(state); + _projects[projectFilePath] = entry; + NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), documentFilePath, ProjectChangeKind.DocumentChanged)); } } } @@ -381,8 +377,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Document updates can no-op. if (!object.ReferenceEquals(state, entry.State)) { - _projects[projectFilePath] = new Entry(state); - NotifyListeners(new ProjectChangeEventArgs(projectFilePath, documentFilePath, ProjectChangeKind.DocumentChanged)); + var oldSnapshot = entry.GetSnapshot(); + entry = new Entry(state); + _projects[projectFilePath] = entry; + NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), documentFilePath, ProjectChangeKind.DocumentChanged)); } } } @@ -407,10 +405,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem var workspaceProject = GetWorkspaceProject(hostProject.FilePath); var state = ProjectState.Create(Workspace.Services, hostProject, workspaceProject); - _projects[hostProject.FilePath] = new Entry(state); + var entry = new Entry(state); + _projects[hostProject.FilePath] = entry; // We need to notify listeners about every project add. - NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.ProjectAdded)); + NotifyListeners(new ProjectChangeEventArgs(null, entry.GetSnapshot(), ProjectChangeKind.ProjectAdded)); } public override void HostProjectChanged(HostProject hostProject) @@ -429,9 +428,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // HostProject updates can no-op. if (!object.ReferenceEquals(state, entry.State)) { - _projects[hostProject.FilePath] = new Entry(state); - - NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.ProjectChanged)); + var oldSnapshot = entry.GetSnapshot(); + entry = new Entry(state); + _projects[hostProject.FilePath] = entry; + NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), ProjectChangeKind.ProjectChanged)); } } } @@ -445,12 +445,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem _foregroundDispatcher.AssertForegroundThread(); - if (_projects.TryGetValue(hostProject.FilePath, out var snapshot)) + if (_projects.TryGetValue(hostProject.FilePath, out var entry)) { - _projects.Remove(hostProject.FilePath); - // We need to notify listeners about every project removal. - NotifyListeners(new ProjectChangeEventArgs(hostProject.FilePath, ProjectChangeKind.ProjectRemoved)); + var oldSnapshot = entry.GetSnapshot(); + _projects.Remove(hostProject.FilePath); + NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, null, ProjectChangeKind.ProjectRemoved)); } } @@ -477,9 +477,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem if (entry.State.WorkspaceProject == null) { var state = entry.State.WithWorkspaceProject(workspaceProject); - _projects[workspaceProject.FilePath] = new Entry(state); - NotifyListeners(new ProjectChangeEventArgs(workspaceProject.FilePath, ProjectChangeKind.ProjectChanged)); + var oldSnapshot = entry.GetSnapshot(); + entry = new Entry(state); + _projects[workspaceProject.FilePath] = entry; + NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), ProjectChangeKind.ProjectChanged)); } } } @@ -505,14 +507,15 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem (entry.State.WorkspaceProject == null || entry.State.WorkspaceProject.Version.GetNewerVersion(workspaceProject.Version) == workspaceProject.Version)) { var state = entry.State.WithWorkspaceProject(workspaceProject); - + // WorkspaceProject updates can no-op. This can be the case if a build is triggered, but we've // already seen the update. if (!object.ReferenceEquals(state, entry.State)) { - _projects[workspaceProject.FilePath] = new Entry(state); - - NotifyListeners(new ProjectChangeEventArgs(workspaceProject.FilePath, ProjectChangeKind.ProjectChanged)); + var oldSnapshot = entry.GetSnapshot(); + entry = new Entry(state); + _projects[workspaceProject.FilePath] = entry; + NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), ProjectChangeKind.ProjectChanged)); } } } @@ -549,17 +552,21 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { // OK there's another WorkspaceProject, use that. state = entry.State.WithWorkspaceProject(otherWorkspaceProject); - _projects[otherWorkspaceProject.FilePath] = new Entry(state); - NotifyListeners(new ProjectChangeEventArgs(otherWorkspaceProject.FilePath, ProjectChangeKind.ProjectChanged)); + var oldSnapshot = entry.GetSnapshot(); + entry = new Entry(state); + _projects[otherWorkspaceProject.FilePath] = entry; + NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), ProjectChangeKind.ProjectChanged)); } else { - state = entry.State.WithWorkspaceProject(null); - _projects[workspaceProject.FilePath] = new Entry(state); - // Notify listeners of a change because we've removed computed state. - NotifyListeners(new ProjectChangeEventArgs(workspaceProject.FilePath, ProjectChangeKind.ProjectChanged)); + state = entry.State.WithWorkspaceProject(null); + + var oldSnapshot = entry.GetSnapshot(); + entry = new Entry(state); + _projects[workspaceProject.FilePath] = entry; + NotifyListeners(new ProjectChangeEventArgs(oldSnapshot, entry.GetSnapshot(), ProjectChangeKind.ProjectChanged)); } } } @@ -601,7 +608,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { throw new ArgumentNullException(nameof(exception)); } - + _errorReporter.ReportError(exception, workspaceProject); } @@ -644,13 +651,18 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem private class Entry { - public ProjectSnapshot Snapshot; + public ProjectSnapshot SnapshotUnsafe; public readonly ProjectState State; public Entry(ProjectState state) { State = state; } + + public ProjectSnapshot GetSnapshot() + { + return SnapshotUnsafe ?? (SnapshotUnsafe = new DefaultProjectSnapshot(State)); + } } } } \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentGeneratedOutputTracker.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentGeneratedOutputTracker.cs deleted file mode 100644 index a3e11da04f..0000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentGeneratedOutputTracker.cs +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.Text; -using Microsoft.Extensions.Internal; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal class DocumentGeneratedOutputTracker - { - private readonly object _lock; - - private DocumentGeneratedOutputTracker _older; - private Task _task; - - private IReadOnlyList _tagHelpers; - private IReadOnlyList _imports; - - public DocumentGeneratedOutputTracker(DocumentGeneratedOutputTracker older) - { - _older = older; - - _lock = new object(); - } - - public bool IsResultAvailable => _task?.IsCompleted == true; - - public DocumentGeneratedOutputTracker Older => _older; - - public Task GetGeneratedOutputInitializationTask(ProjectSnapshot project, DocumentSnapshot document) - { - if (project == null) - { - throw new ArgumentNullException(nameof(project)); - } - - if (document == null) - { - throw new ArgumentNullException(nameof(document)); - } - - if (_task == null) - { - lock (_lock) - { - if (_task == null) - { - _task = GetGeneratedOutputInitializationTaskCore(project, document); - } - } - } - - return _task; - } - - public DocumentGeneratedOutputTracker Fork() - { - return new DocumentGeneratedOutputTracker(this); - } - - private async Task GetGeneratedOutputInitializationTaskCore(ProjectSnapshot project, DocumentSnapshot document) - { - var tagHelpers = await project.GetTagHelpersAsync().ConfigureAwait(false); - var imports = await GetImportsAsync(project, document); - - if (_older != null && _older.IsResultAvailable) - { - var tagHelperDifference = new HashSet(TagHelperDescriptorComparer.Default); - tagHelperDifference.UnionWith(_older._tagHelpers); - tagHelperDifference.SymmetricExceptWith(tagHelpers); - - var importDifference = new HashSet(); - importDifference.UnionWith(_older._imports); - importDifference.SymmetricExceptWith(imports); - - if (tagHelperDifference.Count == 0 && importDifference.Count == 0) - { - // We can use the cached result. - var result = _older._task.Result; - - // Drop reference so it can be GC'ed - _older = null; - - // Cache the tag helpers and imports so the next version can use them - _tagHelpers = tagHelpers; - _imports = imports; - - return result; - } - } - - // Drop reference so it can be GC'ed - _older = null; - - // Cache the tag helpers and imports so the next version can use them - _tagHelpers = tagHelpers; - _imports = imports; - - var importSources = new List(); - foreach (var item in imports) - { - var sourceDocument = await GetRazorSourceDocumentAsync(item.Import); - importSources.Add(sourceDocument); - } - - var documentSource = await GetRazorSourceDocumentAsync(document); - - var projectEngine = project.GetProjectEngine(); - - return projectEngine.ProcessDesignTime(documentSource, importSources, tagHelpers); - } - - private async Task GetRazorSourceDocumentAsync(DocumentSnapshot document) - { - var sourceText = await document.GetTextAsync(); - - return sourceText.GetRazorSourceDocument(document.FilePath); - } - - private async Task> GetImportsAsync(ProjectSnapshot project, DocumentSnapshot document) - { - var imports = new List(); - foreach (var snapshot in document.GetImports()) - { - var versionStamp = await snapshot.GetTextVersionAsync(); - imports.Add(new ImportItem(snapshot.FilePath, versionStamp, snapshot)); - } - - return imports; - } - - private struct ImportItem : IEquatable - { - public ImportItem(string filePath, VersionStamp versionStamp, DocumentSnapshot import) - { - FilePath = filePath; - VersionStamp = versionStamp; - Import = import; - } - - public string FilePath { get; } - - public VersionStamp VersionStamp { get; } - - public DocumentSnapshot Import { get; } - - public bool Equals(ImportItem other) - { - return - FilePathComparer.Instance.Equals(FilePath, other.FilePath) && - VersionStamp == other.VersionStamp; - } - - public override bool Equals(object obj) - { - return obj is ImportItem item ? Equals(item) : false; - } - - public override int GetHashCode() - { - var hash = new HashCodeCombiner(); - hash.Add(FilePath, FilePathComparer.Instance); - hash.Add(VersionStamp); - return hash; - } - } - } -} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentImportsTracker.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentImportsTracker.cs deleted file mode 100644 index 96b927e42f..0000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentImportsTracker.cs +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal class DocumentImportsTracker - { - private readonly object _lock; - - private IReadOnlyList _imports; - - public DocumentImportsTracker() - { - _lock = new object(); - } - - public IReadOnlyList GetImports(ProjectSnapshot project, DocumentSnapshot document) - { - if (project == null) - { - throw new ArgumentNullException(nameof(project)); - } - - if (document == null) - { - throw new ArgumentNullException(nameof(document)); - } - - if (_imports == null) - { - lock (_lock) - { - if (_imports == null) - { - _imports = GetImportsCore(project, document); - } - } - } - - return _imports; - } - - private IReadOnlyList GetImportsCore(ProjectSnapshot project, DocumentSnapshot document) - { - var projectEngine = project.GetProjectEngine(); - var importFeature = projectEngine.ProjectFeatures.OfType().FirstOrDefault(); - var projectItem = projectEngine.FileSystem.GetItem(document.FilePath); - var importItems = importFeature?.GetImports(projectItem).Where(i => i.Exists); - if (importItems == null) - { - return Array.Empty(); - } - - var imports = new List(); - foreach (var item in importItems) - { - if (item.PhysicalPath == null) - { - // This is a default import. - var defaultImport = new DefaultImportDocumentSnapshot(project, item); - imports.Add(defaultImport); - } - else - { - var import = project.GetDocument(item.PhysicalPath); - if (import == null) - { - // We are not tracking this document in this project. So do nothing. - continue; - } - - imports.Add(import); - } - } - - return imports; - } - - private class DefaultImportDocumentSnapshot : DocumentSnapshot - { - private ProjectSnapshot _project; - private RazorProjectItem _importItem; - private SourceText _sourceText; - private VersionStamp _version; - private DocumentGeneratedOutputTracker _generatedOutput; - - public DefaultImportDocumentSnapshot(ProjectSnapshot project, RazorProjectItem item) - { - _project = project; - _importItem = item; - _version = VersionStamp.Default; - _generatedOutput = new DocumentGeneratedOutputTracker(null); - } - - public override string FilePath => null; - - public override string TargetPath => null; - - public override Task GetGeneratedOutputAsync() - { - return _generatedOutput.GetGeneratedOutputInitializationTask(_project, this); - } - - public override IReadOnlyList GetImports() - { - return Array.Empty(); - } - - public async override Task GetTextAsync() - { - using (var stream = _importItem.Read()) - using (var reader = new StreamReader(stream)) - { - var content = await reader.ReadToEndAsync(); - _sourceText = SourceText.From(content); - } - - return _sourceText; - } - - public override Task GetTextVersionAsync() - { - return Task.FromResult(_version); - } - - public override bool TryGetText(out SourceText result) - { - if (_sourceText != null) - { - result = _sourceText; - return true; - } - - result = null; - return false; - } - - public override bool TryGetTextVersion(out VersionStamp result) - { - result = _version; - return true; - } - - public override bool TryGetGeneratedOutput(out RazorCodeDocument result) - { - if (_generatedOutput.IsResultAvailable) - { - result = GetGeneratedOutputAsync().Result; - return true; - } - - result = null; - return false; - } - } - } -} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs index 0cc9da8f04..7dd8317ef1 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs @@ -14,6 +14,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public abstract string TargetPath { get; } + public abstract ProjectSnapshot Project { get; } + + public abstract bool SupportsOutput { get; } + public abstract IReadOnlyList GetImports(); public abstract Task GetTextAsync(); @@ -22,10 +26,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public abstract Task GetGeneratedOutputAsync(); + public abstract Task GetGeneratedOutputVersionAsync(); + public abstract bool TryGetText(out SourceText result); public abstract bool TryGetTextVersion(out VersionStamp result); public abstract bool TryGetGeneratedOutput(out RazorCodeDocument result); + + public abstract bool TryGetGeneratedOutputVersionAsync(out VersionStamp result); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs index d1fb05b67a..31b9447126 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs @@ -2,7 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; @@ -18,14 +21,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem private readonly object _lock; + private ComputedStateTracker _computedState; + private Func> _loader; private Task _loaderTask; private SourceText _sourceText; private VersionStamp? _version; - private DocumentGeneratedOutputTracker _generatedOutput; - private DocumentImportsTracker _imports; - public static DocumentState Create( HostWorkspaceServices services, HostDocument hostDocument, @@ -65,42 +67,37 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public HostWorkspaceServices Services { get; } - public DocumentGeneratedOutputTracker GeneratedOutput + public GeneratedCodeContainer GeneratedCodeContainer => HostDocument.GeneratedCodeContainer; + + public bool IsGeneratedOutputResultAvailable => ComputedState.IsResultAvailable == true; + + private ComputedStateTracker ComputedState { get { - if (_generatedOutput == null) + if (_computedState == null) { lock (_lock) { - if (_generatedOutput == null) + if (_computedState == null) { - _generatedOutput = new DocumentGeneratedOutputTracker(null); + _computedState = new ComputedStateTracker(this); } } } - return _generatedOutput; + return _computedState; } } - public DocumentImportsTracker Imports + public Task<(RazorCodeDocument output, VersionStamp inputVersion, VersionStamp outputVersion)> GetGeneratedOutputAndVersionAsync(DefaultProjectSnapshot project, DefaultDocumentSnapshot document) { - get - { - if (_imports == null) - { - lock (_lock) - { - if (_imports == null) - { - _imports = new DocumentImportsTracker(); - } - } - } + return ComputedState.GetGeneratedOutputAndVersionAsync(project, document); + } - return _imports; - } + public IReadOnlyList GetImports(DefaultProjectSnapshot project) + { + return GetImportsCore(project); } public async Task GetTextAsync() @@ -178,6 +175,23 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem state._version = _version; state._loaderTask = _loaderTask; + // Do not cache computed state + + return state; + } + + public virtual DocumentState WithImportsChange() + { + var state = new DocumentState(Services, HostDocument, _sourceText, _version, _loader); + + // The source could not have possibly changed. + state._sourceText = _sourceText; + state._version = _version; + state._loaderTask = _loaderTask; + + // Optimisically cache the computed state + state._computedState = new ComputedStateTracker(state, _computedState); + return state; } @@ -190,8 +204,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem state._version = _version; state._loaderTask = _loaderTask; - // Opportunistically cache the generated code - state._generatedOutput = _generatedOutput?.Fork(); + // Optimisically cache the computed state + state._computedState = new ComputedStateTracker(state, _computedState); return state; } @@ -203,6 +217,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem throw new ArgumentNullException(nameof(sourceText)); } + // Do not cache the computed state + return new DocumentState(Services, HostDocument, sourceText, version, null); } @@ -213,7 +229,239 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem throw new ArgumentNullException(nameof(loader)); } + // Do not cache the computed state + return new DocumentState(Services, HostDocument, null, null, loader); } + + private IReadOnlyList GetImportsCore(DefaultProjectSnapshot project) + { + var projectEngine = project.GetProjectEngine(); + var importFeature = projectEngine.ProjectFeatures.OfType().FirstOrDefault(); + var projectItem = projectEngine.FileSystem.GetItem(HostDocument.FilePath); + var importItems = importFeature?.GetImports(projectItem); + if (importItems == null) + { + return Array.Empty(); + } + + var imports = new List(); + foreach (var item in importItems) + { + if (item.PhysicalPath == null) + { + // This is a default import. + var defaultImport = new DefaultImportDocumentSnapshot(project, item); + imports.Add(defaultImport); + } + else + { + var import = project.GetDocument(item.PhysicalPath); + if (import == null) + { + // We are not tracking this document in this project. So do nothing. + continue; + } + + imports.Add(import); + } + } + + return imports; + } + + // See design notes on ProjectState.ComputedStateTracker. + private class ComputedStateTracker + { + private readonly object _lock; + + private ComputedStateTracker _older; + public Task<(RazorCodeDocument, VersionStamp, VersionStamp)> TaskUnsafe; + + public ComputedStateTracker(DocumentState state, ComputedStateTracker older = null) + { + _lock = state._lock; + _older = older; + } + + public bool IsResultAvailable => TaskUnsafe?.IsCompleted == true; + + public Task<(RazorCodeDocument, VersionStamp, VersionStamp)> GetGeneratedOutputAndVersionAsync(DefaultProjectSnapshot project, DocumentSnapshot document) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + if (TaskUnsafe == null) + { + lock (_lock) + { + if (TaskUnsafe == null) + { + TaskUnsafe = GetGeneratedOutputAndVersionCoreAsync(project, document); + } + } + } + + return TaskUnsafe; + } + + private async Task<(RazorCodeDocument, VersionStamp, VersionStamp)> GetGeneratedOutputAndVersionCoreAsync(DefaultProjectSnapshot project, DocumentSnapshot document) + { + // We only need to produce the generated code if any of our inputs is newer than the + // previously cached output. + // + // First find the versions that are the inputs: + // - The project + computed state + // - The imports + // - This document + // + // All of these things are cached, so no work is wasted if we do need to generate the code. + var computedStateVersion = await project.State.GetComputedStateVersionAsync(project).ConfigureAwait(false); + var documentCollectionVersion = project.State.DocumentCollectionVersion; + var imports = await GetImportsAsync(project, document).ConfigureAwait(false); + var documentVersion = await document.GetTextVersionAsync().ConfigureAwait(false); + + // OK now that have the previous output and all of the versions, we can see if anything + // has changed that would require regenerating the code. + var inputVersion = documentVersion; + if (inputVersion.GetNewerVersion(computedStateVersion) == computedStateVersion) + { + inputVersion = computedStateVersion; + } + + if (inputVersion.GetNewerVersion(documentCollectionVersion) == documentCollectionVersion) + { + inputVersion = documentCollectionVersion; + } + + for (var i = 0; i < imports.Count; i++) + { + var importVersion = imports[i].Version; + if (inputVersion.GetNewerVersion(importVersion) == importVersion) + { + inputVersion = importVersion; + } + } + + RazorCodeDocument olderOutput = null; + var olderInputVersion = default(VersionStamp); + var olderOutputVersion = default(VersionStamp); + if (_older?.TaskUnsafe != null) + { + (olderOutput, olderInputVersion, olderOutputVersion) = await _older.TaskUnsafe.ConfigureAwait(false); + if (inputVersion.GetNewerVersion(olderInputVersion) == olderInputVersion) + { + // Nothing has changed, we can use the cached result. + lock (_lock) + { + TaskUnsafe = _older.TaskUnsafe; + _older = null; + return (olderOutput, olderInputVersion, olderOutputVersion); + } + } + } + + // OK we have to generate the code. + var tagHelpers = await project.GetTagHelpersAsync().ConfigureAwait(false); + var importSources = new List(); + foreach (var item in imports) + { + var sourceDocument = await GetRazorSourceDocumentAsync(item.Document).ConfigureAwait(false); + importSources.Add(sourceDocument); + } + + var documentSource = await GetRazorSourceDocumentAsync(document).ConfigureAwait(false); + + var projectEngine = project.GetProjectEngine(); + + var codeDocument = projectEngine.ProcessDesignTime(documentSource, importSources, tagHelpers); + var csharpDocument = codeDocument.GetCSharpDocument(); + + // OK now we've generated the code. Let's check if the output is actually different. This is + // a valuable optimization for our use cases because lots of changes you could make require + // us to run code generation, but don't change the result. + // + // Note that we're talking about the effect on the generated C# code here (not the other artifacts). + // This is the reason why we have two versions associated with the output. + // + // The INPUT version is related the .cshtml files and tag helpers + // The OUTPUT version is related to the generated C#. + // + // Examples: + // + // A change to a tag helper not used by this document - updates the INPUT version, but not + // the OUTPUT version. + // + // A change in the HTML - updates the INPUT version, but not the OUTPUT version. + // + // + // Razor IDE features should always retrieve the output and party on it regardless. Depending + // on the use cases we may or may not need to synchronize the output. + + var outputVersion = inputVersion; + if (olderOutput != null) + { + if (string.Equals( + olderOutput.GetCSharpDocument().GeneratedCode, + csharpDocument.GeneratedCode, + StringComparison.Ordinal)) + { + outputVersion = olderOutputVersion; + } + } + + if (document is DefaultDocumentSnapshot defaultDocument) + { + defaultDocument.State.HostDocument.GeneratedCodeContainer.SetOutput( + defaultDocument, + csharpDocument, + inputVersion, + outputVersion); + } + + return (codeDocument, inputVersion, outputVersion); + } + + private async Task GetRazorSourceDocumentAsync(DocumentSnapshot document) + { + var sourceText = await document.GetTextAsync(); + return sourceText.GetRazorSourceDocument(document.FilePath); + } + + private async Task> GetImportsAsync(ProjectSnapshot project, DocumentSnapshot document) + { + var imports = new List(); + foreach (var snapshot in document.GetImports()) + { + var versionStamp = await snapshot.GetTextVersionAsync(); + imports.Add(new ImportItem(snapshot.FilePath, versionStamp, snapshot)); + } + + return imports; + } + + private readonly struct ImportItem + { + public ImportItem(string filePath, VersionStamp version, DocumentSnapshot document) + { + FilePath = filePath; + Version = version; + Document = document; + } + + public string FilePath { get; } + + public VersionStamp Version { get; } + + public DocumentSnapshot Document { get; } + } + } } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/EmptyTextLoader.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/EmptyTextLoader.cs new file mode 100644 index 0000000000..765523fcf8 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/EmptyTextLoader.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class EmptyTextLoader : TextLoader + { + private readonly string _filePath; + private readonly VersionStamp _version; + + public EmptyTextLoader(string filePath) + { + _filePath = filePath; + _version = VersionStamp.Create(); // Version will never change so this can be reused. + } + + public override Task LoadTextAndVersionAsync(Workspace workspace, DocumentId documentId, CancellationToken cancellationToken) + { + return Task.FromResult(TextAndVersion.Create(SourceText.From(""), _version, _filePath)); + } + } +} \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/EphemeralProjectSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/EphemeralProjectSnapshot.cs index cd87c074de..82f9a93196 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/EphemeralProjectSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/EphemeralProjectSnapshot.cs @@ -56,6 +56,26 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return null; } + public override bool IsImportDocument(DocumentSnapshot document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + return false; + } + + public override IEnumerable GetRelatedDocuments(DocumentSnapshot document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + return Array.Empty(); + } + public override RazorProjectEngine GetProjectEngine() { return _projectEngine.Value; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/GeneratedCodeContainer.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/GeneratedCodeContainer.cs new file mode 100644 index 0000000000..dec2a48d4a --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/GeneratedCodeContainer.cs @@ -0,0 +1,184 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class GeneratedCodeContainer + { + public event EventHandler GeneratedCodeChanged; + + private SourceText _source; + private VersionStamp? _inputVersion; + private VersionStamp? _outputVersion; + private RazorCSharpDocument _output; + private DocumentSnapshot _latestDocument; + + private readonly object _setOutputLock = new object(); + private readonly TextContainer _textContainer; + + public GeneratedCodeContainer() + { + _textContainer = new TextContainer(_setOutputLock); + _textContainer.TextChanged += TextContainer_TextChanged; + } + + public SourceText Source + { + get + { + lock (_setOutputLock) + { + return _source; + } + } + } + + public VersionStamp InputVersion + { + get + { + lock (_setOutputLock) + { + return _inputVersion.Value; + } + } + } + + public VersionStamp OutputVersion + { + get + { + lock (_setOutputLock) + { + return _outputVersion.Value; + } + } + } + + public RazorCSharpDocument Output + { + get + { + lock (_setOutputLock) + { + return _output; + } + } + } + + public DocumentSnapshot LatestDocument + { + get + { + lock (_setOutputLock) + { + return _latestDocument; + } + } + } + + public SourceTextContainer SourceTextContainer + { + get + { + lock (_setOutputLock) + { + return _textContainer; + } + } + } + + public void SetOutput( + DefaultDocumentSnapshot document, + RazorCSharpDocument output, + VersionStamp inputVersion, + VersionStamp outputVersion) + { + lock (_setOutputLock) + { + if (_inputVersion.HasValue && + _inputVersion != inputVersion && + _inputVersion == _inputVersion.Value.GetNewerVersion(inputVersion)) + { + // Latest document is newer than the provided document. + return; + } + + if (!document.TryGetText(out var source)) + { + Debug.Fail("The text should have already been evaluated."); + return; + } + + _source = source; + _inputVersion = inputVersion; + _outputVersion = outputVersion; + _output = output; + _latestDocument = document; + _textContainer.SetText(SourceText.From(_output.GeneratedCode)); + } + } + + private void TextContainer_TextChanged(object sender, TextChangeEventArgs args) + { + GeneratedCodeChanged?.Invoke(this, args); + } + + private class TextContainer : SourceTextContainer + { + public override event EventHandler TextChanged; + + private readonly object _outerLock; + private SourceText _currentText; + + public TextContainer(object outerLock) + : this(SourceText.From(string.Empty)) + { + _outerLock = outerLock; + } + + public TextContainer(SourceText sourceText) + { + if (sourceText == null) + { + throw new ArgumentNullException(nameof(sourceText)); + } + + _currentText = sourceText; + } + + public override SourceText CurrentText + { + get + { + lock (_outerLock) + { + return _currentText; + } + } + } + + public void SetText(SourceText sourceText) + { + if (sourceText == null) + { + throw new ArgumentNullException(nameof(sourceText)); + } + + lock (_outerLock) + { + + var e = new TextChangeEventArgs(_currentText, sourceText); + _currentText = sourceText; + + TextChanged?.Invoke(this, e); + } + } + } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/GeneratedOutputTextLoader.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/GeneratedOutputTextLoader.cs new file mode 100644 index 0000000000..8ca7821b63 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/GeneratedOutputTextLoader.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class GeneratedOutputTextLoader : TextLoader + { + private readonly DocumentSnapshot _document; + private readonly string _filePath; + private readonly VersionStamp _version; + + public GeneratedOutputTextLoader(DocumentSnapshot document, string filePath) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + _document = document; + _filePath = filePath; + _version = VersionStamp.Create(); // Version will never change so this can be reused. + } + + public override async Task LoadTextAndVersionAsync(Workspace workspace, DocumentId documentId, CancellationToken cancellationToken) + { + var output = await _document.GetGeneratedOutputAsync().ConfigureAwait(false); + return TextAndVersion.Create(SourceText.From(output.GetCSharpDocument().GeneratedCode), _version, _filePath); + } + } +} \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs index 859cc0df32..77d62eb854 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs @@ -21,10 +21,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem FilePath = filePath; TargetPath = targetPath; + GeneratedCodeContainer = new GeneratedCodeContainer(); } public string FilePath { get; } public string TargetPath { get; } + + public GeneratedCodeContainer GeneratedCodeContainer { get; } } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/LiveShareProjectSnapshotBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/LiveShareProjectSnapshotBase.cs index f03a31b0ae..ae13f49735 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/LiveShareProjectSnapshotBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/LiveShareProjectSnapshotBase.cs @@ -27,6 +27,10 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces.ProjectSystem public override DocumentSnapshot GetDocument(string filePath) => throw new NotImplementedException(); + public override bool IsImportDocument(DocumentSnapshot document) => throw new NotImplementedException(); + + public override IEnumerable GetRelatedDocuments(DocumentSnapshot document) => throw new NotImplementedException(); + public override RazorProjectEngine GetProjectEngine() => throw new NotImplementedException(); public override Task> GetTagHelpersAsync() => throw new NotImplementedException(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeEventArgs.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeEventArgs.cs index a163ba5f1b..3e0dd3ff68 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeEventArgs.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectChangeEventArgs.cs @@ -7,29 +7,39 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal class ProjectChangeEventArgs : EventArgs { - public ProjectChangeEventArgs(string projectFilePath, ProjectChangeKind kind) + public ProjectChangeEventArgs(ProjectSnapshot older, ProjectSnapshot newer, ProjectChangeKind kind) { - if (projectFilePath == null) + if (older == null && newer == null) { - throw new ArgumentNullException(nameof(projectFilePath)); + throw new ArgumentException("Both projects cannot be null."); } - ProjectFilePath = projectFilePath; + Older = older; + Newer = newer; Kind = kind; + + ProjectFilePath = older?.FilePath ?? newer.FilePath; } - public ProjectChangeEventArgs(string projectFilePath, string documentFilePath, ProjectChangeKind kind) + public ProjectChangeEventArgs(ProjectSnapshot older, ProjectSnapshot newer, string documentFilePath, ProjectChangeKind kind) { - if (projectFilePath == null) + if (older == null && newer == null) { - throw new ArgumentNullException(nameof(projectFilePath)); + throw new ArgumentException("Both projects cannot be null."); } - ProjectFilePath = projectFilePath; + Older = older; + Newer = newer; DocumentFilePath = documentFilePath; Kind = kind; + + ProjectFilePath = older?.FilePath ?? newer.FilePath; } + public ProjectSnapshot Older { get; } + + public ProjectSnapshot Newer { get; } + public string ProjectFilePath { get; } public string DocumentFilePath { get; } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectEngineTracker.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectEngineTracker.cs deleted file mode 100644 index 6e54daed0b..0000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectEngineTracker.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.Host; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal class ProjectEngineTracker - { - private const ProjectDifference Mask = ProjectDifference.ConfigurationChanged; - - private readonly object _lock = new object(); - - private readonly HostWorkspaceServices _services; - private RazorProjectEngine _projectEngine; - - public ProjectEngineTracker(ProjectState state) - { - if (state == null) - { - throw new ArgumentNullException(nameof(state)); - } - - _services = state.Services; - } - - public ProjectEngineTracker ForkFor(ProjectState state, ProjectDifference difference) - { - if (state == null) - { - throw new ArgumentNullException(nameof(state)); - } - - if ((difference & Mask) != 0) - { - return null; - } - - return this; - } - - public RazorProjectEngine GetProjectEngine(ProjectSnapshot snapshot) - { - if (snapshot == null) - { - throw new ArgumentNullException(nameof(snapshot)); - } - - if (_projectEngine == null) - { - lock (_lock) - { - if (_projectEngine == null) - { - var factory = _services.GetRequiredService(); - _projectEngine = factory.Create(snapshot); - } - } - } - - return _projectEngine; - } - } -} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs index 418d117ff2..cf091a4f19 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs @@ -25,6 +25,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public abstract DocumentSnapshot GetDocument(string filePath); + public abstract bool IsImportDocument(DocumentSnapshot document); + + /// + /// If the provided document is an import document, gets the other documents in the project + /// that include directives specified by the provided document. Otherwise returns an empty + /// list. + /// + /// The document. + /// A list of related documents. + public abstract IEnumerable GetRelatedDocuments(DocumentSnapshot document); + public abstract Task> GetTagHelpersAsync(); public abstract bool TryGetTagHelpers(out IReadOnlyList result); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs index 6670ba55a2..7708b0c709 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs @@ -3,7 +3,11 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; @@ -12,12 +16,24 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Internal tracker for DefaultProjectSnapshot internal class ProjectState { - private static readonly IReadOnlyDictionary EmptyDocuments = new Dictionary(); + private const ProjectDifference ClearComputedStateMask = ProjectDifference.ConfigurationChanged; + private const ProjectDifference ClearCachedTagHelpersMask = + ProjectDifference.ConfigurationChanged | + ProjectDifference.WorkspaceProjectAdded | + ProjectDifference.WorkspaceProjectChanged | + ProjectDifference.WorkspaceProjectRemoved; + + private const ProjectDifference ClearDocumentCollectionVersionMask = + ProjectDifference.ConfigurationChanged | + ProjectDifference.DocumentAdded | + ProjectDifference.DocumentRemoved; + + private static readonly ImmutableDictionary EmptyDocuments = ImmutableDictionary.Create(FilePathComparer.Instance); + private static readonly ImmutableDictionary> EmptyImportsToRelatedDocuments = ImmutableDictionary.Create>(FilePathComparer.Instance); private readonly object _lock; - private ProjectEngineTracker _projectEngine; - private ProjectTagHelperTracker _tagHelpers; + private ComputedStateTracker _computedState; public static ProjectState Create(HostWorkspaceServices services, HostProject hostProject, Project workspaceProject = null) { @@ -33,7 +49,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return new ProjectState(services, hostProject, workspaceProject); } - + private ProjectState( HostWorkspaceServices services, HostProject hostProject, @@ -43,7 +59,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem HostProject = hostProject; WorkspaceProject = workspaceProject; Documents = EmptyDocuments; + ImportsToRelatedDocuments = EmptyImportsToRelatedDocuments; Version = VersionStamp.Create(); + DocumentCollectionVersion = Version; _lock = new object(); } @@ -53,7 +71,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem ProjectDifference difference, HostProject hostProject, Project workspaceProject, - IReadOnlyDictionary documents) + ImmutableDictionary documents, + ImmutableDictionary> importsToRelatedDocuments) { if (older == null) { @@ -70,21 +89,49 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem throw new ArgumentNullException(nameof(documents)); } + if (importsToRelatedDocuments == null) + { + throw new ArgumentNullException(nameof(importsToRelatedDocuments)); + } + Services = older.Services; Version = older.Version.GetNewerVersion(); HostProject = hostProject; WorkspaceProject = workspaceProject; Documents = documents; + ImportsToRelatedDocuments = importsToRelatedDocuments; _lock = new object(); - _projectEngine = older._projectEngine?.ForkFor(this, difference); - _tagHelpers = older._tagHelpers?.ForkFor(this, difference); + if ((difference & ClearDocumentCollectionVersionMask) == 0) + { + // Document collection hasn't changed + DocumentCollectionVersion = older.DocumentCollectionVersion; + } + else + { + DocumentCollectionVersion = Version; + } + + if ((difference & ClearComputedStateMask) == 0 && older._computedState != null) + { + // Optimistically cache the RazorProjectEngine. + _computedState = new ComputedStateTracker(this, older._computedState); + } + + if ((difference & ClearCachedTagHelpersMask) == 0 && _computedState != null) + { + // It's OK to keep the computed Tag Helpers. + _computedState.TaskUnsafe = older._computedState?.TaskUnsafe; + } } // Internal set for testing. - public IReadOnlyDictionary Documents { get; internal set; } + public ImmutableDictionary Documents { get; internal set; } + + // Internal set for testing. + public ImmutableDictionary> ImportsToRelatedDocuments { get; internal set; } public HostProject HostProject { get; } @@ -92,46 +139,68 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public Project WorkspaceProject { get; } + /// + /// Gets the version of this project, INCLUDING content changes. The is + /// incremented for each new instance created. + /// public VersionStamp Version { get; } - // Computed State - public ProjectEngineTracker ProjectEngine + /// + /// Gets the version of this project, NOT INCLUDING computed or content changes. The + /// is incremented each time the configuration changes or + /// a document is added or removed. + /// + public VersionStamp DocumentCollectionVersion { get; } + + public RazorProjectEngine ProjectEngine => ComputedState.ProjectEngine; + + public bool IsTagHelperResultAvailable => ComputedState.TaskUnsafe?.IsCompleted == true; + + private ComputedStateTracker ComputedState { get { - if (_projectEngine == null) + if (_computedState == null) { lock (_lock) { - if (_projectEngine == null) + if (_computedState == null) { - _projectEngine = new ProjectEngineTracker(this); + _computedState = new ComputedStateTracker(this); } } } - return _projectEngine; + return _computedState; } } - // Computed State - public ProjectTagHelperTracker TagHelpers + /// + /// Gets the version of this project based on the computed state, NOT INCLUDING content + /// changes. The computed state is guaranteed to change when the configuration or tag helpers + /// change. + /// + /// Asynchronously returns the computed version. + public async Task GetComputedStateVersionAsync(ProjectSnapshot snapshot) { - get + if (snapshot == null) { - if (_tagHelpers == null) - { - lock (_lock) - { - if (_tagHelpers == null) - { - _tagHelpers = new ProjectTagHelperTracker(this); - } - } - } - - return _tagHelpers; + throw new ArgumentNullException(nameof(snapshot)); } + + var (_, version) = await ComputedState.GetTagHelpersAndVersionAsync(snapshot).ConfigureAwait(false); + return version; + } + + public async Task> GetTagHelpersAsync(ProjectSnapshot snapshot) + { + if (snapshot == null) + { + throw new ArgumentNullException(nameof(snapshot)); + } + + var (tagHelpers, _) = await ComputedState.GetTagHelpersAndVersionAsync(snapshot).ConfigureAwait(false); + return tagHelpers; } public ProjectState WithAddedHostDocument(HostDocument hostDocument, Func> loader) @@ -153,16 +222,23 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return this; } - var documents = new Dictionary(FilePathComparer.Instance); - foreach (var kvp in Documents) - { - documents.Add(kvp.Key, kvp.Value); - } - - documents.Add(hostDocument.FilePath, DocumentState.Create(Services, hostDocument, loader)); + var documents = Documents.Add(hostDocument.FilePath, DocumentState.Create(Services, hostDocument, loader)); - var difference = ProjectDifference.DocumentAdded; - var state = new ProjectState(this, difference, HostProject, WorkspaceProject, documents); + // Compute the effect on the import map + var importTargetPaths = GetImportDocumentTargetPaths(hostDocument.TargetPath); + var importsToRelatedDocuments = AddToImportsToRelatedDocuments(ImportsToRelatedDocuments, hostDocument, importTargetPaths); + + // Now check if the updated document is an import - it's important this this happens after + // updating the imports map. + if (importsToRelatedDocuments.TryGetValue(hostDocument.TargetPath, out var relatedDocuments)) + { + foreach (var relatedDocument in relatedDocuments) + { + documents = documents.SetItem(relatedDocument, documents[relatedDocument].WithImportsChange()); + } + } + + var state = new ProjectState(this, ProjectDifference.DocumentAdded, HostProject, WorkspaceProject, documents, importsToRelatedDocuments); return state; } @@ -178,16 +254,23 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return this; } - var documents = new Dictionary(FilePathComparer.Instance); - foreach (var kvp in Documents) + var documents = Documents.Remove(hostDocument.FilePath); + + // First check if the updated document is an import - it's important that this happens + // before updating the imports map. + if (ImportsToRelatedDocuments.TryGetValue(hostDocument.TargetPath, out var relatedDocuments)) { - documents.Add(kvp.Key, kvp.Value); + foreach (var relatedDocument in relatedDocuments) + { + documents = documents.SetItem(relatedDocument, documents[relatedDocument].WithImportsChange()); + } } - documents.Remove(hostDocument.FilePath); + // Compute the effect on the import map + var importTargetPaths = GetImportDocumentTargetPaths(hostDocument.TargetPath); + var importsToRelatedDocuments = RemoveFromImportsToRelatedDocuments(ImportsToRelatedDocuments, hostDocument, importTargetPaths); - var difference = ProjectDifference.DocumentRemoved; - var state = new ProjectState(this, difference, HostProject, WorkspaceProject, documents); + var state = new ProjectState(this, ProjectDifference.DocumentRemoved, HostProject, WorkspaceProject, documents, importsToRelatedDocuments); return state; } @@ -198,23 +281,22 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem throw new ArgumentNullException(nameof(hostDocument)); } - if (!Documents.ContainsKey(hostDocument.FilePath)) + if (!Documents.TryGetValue(hostDocument.FilePath, out var document)) { return this; } - var documents = new Dictionary(FilePathComparer.Instance); - foreach (var kvp in Documents) + var documents = Documents.SetItem(hostDocument.FilePath, document.WithText(sourceText, version)); + + if (ImportsToRelatedDocuments.TryGetValue(hostDocument.TargetPath, out var relatedDocuments)) { - documents.Add(kvp.Key, kvp.Value); + foreach (var relatedDocument in relatedDocuments) + { + documents = documents.SetItem(relatedDocument, documents[relatedDocument].WithImportsChange()); + } } - if (documents.TryGetValue(hostDocument.FilePath, out var document)) - { - documents[hostDocument.FilePath] = document.WithText(sourceText, version); - } - - var state = new ProjectState(this, ProjectDifference.DocumentChanged, HostProject, WorkspaceProject, documents); + var state = new ProjectState(this, ProjectDifference.DocumentChanged, HostProject, WorkspaceProject, documents, ImportsToRelatedDocuments); return state; } @@ -225,23 +307,22 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem throw new ArgumentNullException(nameof(hostDocument)); } - if (!Documents.ContainsKey(hostDocument.FilePath)) + if (!Documents.TryGetValue(hostDocument.FilePath, out var document)) { return this; } - var documents = new Dictionary(FilePathComparer.Instance); - foreach (var kvp in Documents) + var documents = Documents.SetItem(hostDocument.FilePath, document.WithTextLoader(loader)); + + if (ImportsToRelatedDocuments.TryGetValue(hostDocument.TargetPath, out var relatedDocuments)) { - documents.Add(kvp.Key, kvp.Value); + foreach (var relatedDocument in relatedDocuments) + { + documents = documents.SetItem(relatedDocument, documents[relatedDocument].WithImportsChange()); + } } - if (documents.TryGetValue(hostDocument.FilePath, out var document)) - { - documents[hostDocument.FilePath] = document.WithTextLoader(loader); - } - - var state = new ProjectState(this, ProjectDifference.DocumentChanged, HostProject, WorkspaceProject, documents); + var state = new ProjectState(this, ProjectDifference.DocumentChanged, HostProject, WorkspaceProject, documents, ImportsToRelatedDocuments); return state; } @@ -257,14 +338,18 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return this; } - var difference = ProjectDifference.ConfigurationChanged; - var documents = new Dictionary(FilePathComparer.Instance); - foreach (var kvp in Documents) + var documents = Documents.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.WithConfigurationChange(), FilePathComparer.Instance); + + // If the host project has changed then we need to recompute the imports map + var importsToRelatedDocuments = EmptyImportsToRelatedDocuments; + + foreach (var document in documents) { - documents.Add(kvp.Key, kvp.Value.WithConfigurationChange()); + var importTargetPaths = GetImportDocumentTargetPaths(document.Value.HostDocument.TargetPath); + importsToRelatedDocuments = AddToImportsToRelatedDocuments(ImportsToRelatedDocuments, document.Value.HostDocument, importTargetPaths); } - var state = new ProjectState(this, difference, hostProject, WorkspaceProject, documents); + var state = new ProjectState(this, ProjectDifference.ConfigurationChanged, hostProject, WorkspaceProject, documents, importsToRelatedDocuments); return state; } @@ -291,14 +376,161 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return this; } - var documents = new Dictionary(FilePathComparer.Instance); - foreach (var kvp in Documents) + var documents = Documents.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.WithWorkspaceProjectChange(), FilePathComparer.Instance); + var state = new ProjectState(this, difference, HostProject, workspaceProject, documents, ImportsToRelatedDocuments); + return state; + } + + private static ImmutableDictionary> AddToImportsToRelatedDocuments( + ImmutableDictionary> importsToRelatedDocuments, + HostDocument hostDocument, + List importTargetPaths) + { + foreach (var importTargetPath in importTargetPaths) { - documents.Add(kvp.Key, kvp.Value.WithWorkspaceProjectChange()); + if (!importsToRelatedDocuments.TryGetValue(importTargetPath, out var relatedDocuments)) + { + relatedDocuments = ImmutableArray.Create(); + } + + relatedDocuments = relatedDocuments.Add(hostDocument.FilePath); + importsToRelatedDocuments = importsToRelatedDocuments.SetItem(importTargetPath, relatedDocuments); } - var state = new ProjectState(this, difference, HostProject, workspaceProject, documents); - return state; + return importsToRelatedDocuments; + } + + private static ImmutableDictionary> RemoveFromImportsToRelatedDocuments( + ImmutableDictionary> importsToRelatedDocuments, + HostDocument hostDocument, + List importTargetPaths) + { + foreach (var importTargetPath in importTargetPaths) + { + if (importsToRelatedDocuments.TryGetValue(importTargetPath, out var relatedDocuments)) + { + relatedDocuments = relatedDocuments.Remove(hostDocument.FilePath); + if (relatedDocuments.Length > 0) + { + importsToRelatedDocuments = importsToRelatedDocuments.SetItem(importTargetPath, relatedDocuments); + } + else + { + importsToRelatedDocuments = importsToRelatedDocuments.Remove(importTargetPath); + } + } + } + + return importsToRelatedDocuments; + } + + private RazorProjectEngine CreateProjectEngine() + { + var factory = Services.GetRequiredService(); + return factory.Create(HostProject.Configuration, Path.GetDirectoryName(HostProject.FilePath), configure: null); + } + + public List GetImportDocumentTargetPaths(string targetPath) + { + var projectEngine = ComputedState.ProjectEngine; + var importFeature = projectEngine.ProjectFeatures.OfType().FirstOrDefault(); + var projectItem = projectEngine.FileSystem.GetItem(targetPath); + var importItems = importFeature?.GetImports(projectItem).Where(i => i.FilePath != null); + + // Target path looks like `Foo\\Bar.cshtml` + var targetPaths = new List(); + foreach (var importItem in importItems) + { + var itemTargetPath = importItem.FilePath.Replace('/', '\\').TrimStart('\\'); + targetPaths.Add(itemTargetPath); + } + + return targetPaths; + } + + // ComputedStateTracker is the 'holder' of all of the state that can be cached based on + // the data in a ProjectState. It should not hold onto a ProjectState directly + // as that could lead to things being in memory longer than we want them to. + // + // Rather, a ComputedStateTracker instance can hold on to a previous instance from an older + // version of the same project. + private class ComputedStateTracker + { + // ProjectState.Version + private readonly VersionStamp _projectStateVersion; + private readonly object _lock; + + private ComputedStateTracker _older; // We be set to null when state is computed + public Task<(IReadOnlyList, VersionStamp)> TaskUnsafe; + + public ComputedStateTracker(ProjectState state, ComputedStateTracker older = null) + { + _projectStateVersion = state.Version; + _lock = state._lock; + _older = older; + + ProjectEngine = _older?.ProjectEngine; + if (ProjectEngine == null) + { + ProjectEngine = state.CreateProjectEngine(); + } + } + + public RazorProjectEngine ProjectEngine { get; } + + public Task<(IReadOnlyList, VersionStamp)> GetTagHelpersAndVersionAsync(ProjectSnapshot snapshot) + { + if (TaskUnsafe == null) + { + lock (_lock) + { + if (TaskUnsafe == null) + { + TaskUnsafe = GetTagHelpersAndVersionCoreAsync(snapshot); + } + } + } + + return TaskUnsafe; + } + + private async Task<(IReadOnlyList, VersionStamp)> GetTagHelpersAndVersionCoreAsync(ProjectSnapshot snapshot) + { + // Don't allow synchronous execution - we expect this to always be called with the lock. + await Task.Yield(); + + var services = ((DefaultProjectSnapshot)snapshot).State.Services; + var resolver = services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); + + var tagHelpers = (await resolver.GetTagHelpersAsync(snapshot).ConfigureAwait(false)).Descriptors; + if (_older?.TaskUnsafe != null) + { + // We have something to diff against. + var (olderTagHelpers, olderVersion) = await _older.TaskUnsafe.ConfigureAwait(false); + + var difference = new HashSet(TagHelperDescriptorComparer.Default); + difference.UnionWith(olderTagHelpers); + difference.SymmetricExceptWith(tagHelpers); + + if (difference.Count == 0) + { + lock (_lock) + { + + // Everything is the same. Return the cached version. + TaskUnsafe = _older.TaskUnsafe; + _older = null; + return (olderTagHelpers, olderVersion); + } + } + } + + lock (_lock) + { + _older = null; + return (tagHelpers, _projectStateVersion); + } + } } } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectTagHelperTracker.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectTagHelperTracker.cs deleted file mode 100644 index 0c3be4ddec..0000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectTagHelperTracker.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.Host; - -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem -{ - internal class ProjectTagHelperTracker - { - private const ProjectDifference Mask = - ProjectDifference.ConfigurationChanged | - ProjectDifference.WorkspaceProjectAdded | - ProjectDifference.WorkspaceProjectChanged | - ProjectDifference.WorkspaceProjectRemoved; - - private readonly object _lock = new object(); - private readonly HostWorkspaceServices _services; - - private Task> _task; - - public ProjectTagHelperTracker(ProjectState state) - { - if (state == null) - { - throw new ArgumentNullException(nameof(state)); - } - - _services = state.Services; - } - - public bool IsResultAvailable => _task?.IsCompleted == true; - - public ProjectTagHelperTracker ForkFor(ProjectState state, ProjectDifference difference) - { - if (state == null) - { - throw new ArgumentNullException(nameof(state)); - } - - if ((difference & Mask) != 0) - { - return null; - } - - return this; - } - - public Task> GetTagHelperInitializationTask(ProjectSnapshot snapshot) - { - if (snapshot == null) - { - throw new ArgumentNullException(nameof(snapshot)); - } - - if (_task == null) - { - lock (_lock) - { - if (_task == null) - { - _task = GetTagHelperInitializationTaskCore(snapshot); - } - } - } - - return _task; - } - - private async Task> GetTagHelperInitializationTaskCore(ProjectSnapshot snapshot) - { - var resolver = _services.GetLanguageServices(RazorLanguage.Name).GetRequiredService(); - return (await resolver.GetTagHelpersAsync(snapshot)).Descriptors; - } - } -} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs index fefb5cd1de..905e1822ed 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Properties/AssemblyInfo.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("rzls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.LanguageServer.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.LiveShare.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.LiveShare.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.Performance, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorCompletionFactsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorCompletionFactsService.cs new file mode 100644 index 0000000000..f5051dd536 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorCompletionFactsService.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal abstract class RazorCompletionFactsService + { + public abstract IReadOnlyList GetCompletionItems(RazorSyntaxTree syntaxTree, SourceSpan location); + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorCompletionItem.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorCompletionItem.cs new file mode 100644 index 0000000000..87bf19c858 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorCompletionItem.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal sealed class RazorCompletionItem + { + public RazorCompletionItem( + string displayText, + string insertText, + string description, + RazorCompletionItemKind kind) + { + if (displayText == null) + { + throw new ArgumentNullException(nameof(displayText)); + } + + if (insertText == null) + { + throw new ArgumentNullException(nameof(insertText)); + } + + if (description == null) + { + throw new ArgumentNullException(nameof(description)); + } + + DisplayText = displayText; + InsertText = insertText; + Description = description; + Kind = kind; + } + + public string DisplayText { get; } + + public string InsertText { get; } + + public string Description { get; } + + public RazorCompletionItemKind Kind { get; } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorCompletionItemKind.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorCompletionItemKind.cs new file mode 100644 index 0000000000..64d2e1ccca --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorCompletionItemKind.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.CodeAnalysis.Razor +{ + internal enum RazorCompletionItemKind + { + Directive + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorDocumentExcerptService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorDocumentExcerptService.cs new file mode 100644 index 0000000000..bb3ba757cc --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorDocumentExcerptService.cs @@ -0,0 +1,267 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Classification; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal class RazorDocumentExcerptService : IDocumentExcerptService + { + private readonly DocumentSnapshot _document; + private readonly ISpanMappingService _mapper; + + public RazorDocumentExcerptService(DocumentSnapshot document, ISpanMappingService mapper) + { + if (mapper == null) + { + throw new ArgumentNullException(nameof(mapper)); + } + + _document = document; + _mapper = mapper; + } + + public async Task TryExcerptAsync( + Document document, + TextSpan span, + ExcerptMode mode, + CancellationToken cancellationToken) + { + var result = await TryGetExcerptInternalAsync(document, span, (ExcerptModeInternal)mode, cancellationToken).ConfigureAwait(false); + return result?.ToExcerptResult(); + } + + public async Task TryGetExcerptInternalAsync( + Document document, + TextSpan span, + ExcerptModeInternal mode, + CancellationToken cancellationToken) + { + if (_document == null) + { + return null; + } + + var mapped = await _mapper.MapSpansAsync(document, new[] { span }, cancellationToken).ConfigureAwait(false); + if (mapped.Length == 0 || mapped[0].Equals(default(MappedSpanResult))) + { + return null; + } + + var project = _document.Project; + var primaryDocument = project.GetDocument(mapped[0].FilePath); + if (primaryDocument == null) + { + return null; + } + + var primaryText = await primaryDocument.GetTextAsync().ConfigureAwait(false); + var primarySpan = primaryText.Lines.GetTextSpan(mapped[0].LinePositionSpan); + + var secondaryDocument = document; + var secondarySpan = span; + + // First compute the range of text we want to we to display relative to the primary document. + var excerptSpan = ChooseExcerptSpan(primaryText, primarySpan, mode); + + // Then we'll classify the spans based on the primary document, since that's the coordinate + // space that our output mappings use. + var output = await _document.GetGeneratedOutputAsync().ConfigureAwait(false); + var mappings = output.GetCSharpDocument().SourceMappings; + var classifiedSpans = await ClassifyPreviewAsync( + primaryText, + excerptSpan, + secondaryDocument, + mappings, + cancellationToken).ConfigureAwait(false); + + // Now translate everything to be relative to the excerpt + var offset = 0 - excerptSpan.Start; + var excerptText = primaryText.GetSubText(excerptSpan); + excerptSpan = new TextSpan(0, excerptSpan.Length); + primarySpan = new TextSpan(primarySpan.Start + offset, primarySpan.Length); + + for (var i = 0; i < classifiedSpans.Count; i++) + { + var classifiedSpan = classifiedSpans[i]; + var updated = new TextSpan(classifiedSpan.TextSpan.Start + offset, classifiedSpan.TextSpan.Length); + Debug.Assert(excerptSpan.Contains(updated)); + + classifiedSpans[i] = new ClassifiedSpan(classifiedSpan.ClassificationType, updated); + } + + return new ExcerptResultInternal(excerptText, primarySpan, classifiedSpans.ToImmutable(), document, span); + } + + private TextSpan ChooseExcerptSpan(SourceText primaryText, TextSpan primarySpan, ExcerptModeInternal mode) + { + var startLine = primaryText.Lines.GetLineFromPosition(primarySpan.Start); + var endLine = primaryText.Lines.GetLineFromPosition(primarySpan.End); + + // If we're showing a single line then this will do. Otherwise expand the range by 3 in + // each direction (if possible). + if (mode == ExcerptModeInternal.Tooltip) + { + var index = Math.Max(startLine.LineNumber - 3, 0); + startLine = primaryText.Lines[index]; + } + + if (mode == ExcerptModeInternal.Tooltip) + { + var index = Math.Min(endLine.LineNumber + 3, primaryText.Lines.Count - 1); + endLine = primaryText.Lines[index]; + } + + return new TextSpan(startLine.Start, endLine.End - startLine.Start); + } + + private async Task.Builder> ClassifyPreviewAsync( + SourceText primaryText, + TextSpan excerptSpan, + Document secondaryDocument, + IReadOnlyList mappings, + CancellationToken cancellationToken) + { + var builder = ImmutableArray.CreateBuilder(); + + var sorted = new List(mappings); + sorted.Sort((x, y) => x.OriginalSpan.AbsoluteIndex.CompareTo(y.OriginalSpan.AbsoluteIndex)); + + // The algorithm here is to iterate through the source mappings (sorted) and use the C# classifier + // on the spans that are known to the C#. For the spans that are not known to be C# then + // we just treat them as text since we'd don't currently have our own classifications. + + var remainingSpan = excerptSpan; + for (var i = 0; i < sorted.Count && excerptSpan.Length > 0; i++) + { + var primarySpan = sorted[i].OriginalSpan.AsTextSpan(); + var intersection = primarySpan.Intersection(remainingSpan); + if (intersection == null) + { + // This span is outside the area we're interested in. + continue; + } + + // OK this span intersects with the excerpt span, so we will process it. Let's compute + // the secondary span that matches the intersection. + var secondarySpan = sorted[i].GeneratedSpan.AsTextSpan(); + secondarySpan = new TextSpan(secondarySpan.Start + intersection.Value.Start - primarySpan.Start, intersection.Value.Length); + primarySpan = intersection.Value; + + if (remainingSpan.Start < primarySpan.Start) + { + // The position is before the next C# span. Classify everything up to the C# start + // as text. + builder.Add(new ClassifiedSpan(ClassificationTypeNames.Text, new TextSpan(remainingSpan.Start, primarySpan.Start - remainingSpan.Start))); + + // Advance to the start of the C# span. + remainingSpan = new TextSpan(primarySpan.Start, remainingSpan.Length - (primarySpan.Start - remainingSpan.Start)); + } + + // We should be able to process this whole span as C#, so classify it. + // + // However, we'll have to translate it to the the secondary document's coordinates to do that. + Debug.Assert(remainingSpan.Contains(primarySpan) && remainingSpan.Start == primarySpan.Start); + var classifiedSecondarySpans = await Classifier.GetClassifiedSpansAsync( + secondaryDocument, + secondarySpan, + cancellationToken); + + // NOTE: The Classifier will only returns spans for things that it understands. That means + // that whitespace is not classified. The preview expects us to provide contiguous spans, + // so we are going to have to fill in the gaps. + + // Now we have to translate back to the primary document's coordinates. + var offset = primarySpan.Start - secondarySpan.Start; + foreach (var classifiedSecondarySpan in classifiedSecondarySpans) + { + Debug.Assert(secondarySpan.Contains(classifiedSecondarySpan.TextSpan)); + + var updated = new TextSpan(classifiedSecondarySpan.TextSpan.Start + offset, classifiedSecondarySpan.TextSpan.Length); + Debug.Assert(primarySpan.Contains(updated)); + + // Make sure that we're not introducing a gap. Remember, we need to fill in the whitespace. + if (remainingSpan.Start < updated.Start) + { + builder.Add(new ClassifiedSpan( + ClassificationTypeNames.Text, + new TextSpan(remainingSpan.Start, updated.Start - remainingSpan.Start))); + remainingSpan = new TextSpan(updated.Start, remainingSpan.Length - (updated.Start - remainingSpan.Start)); + } + + builder.Add(new ClassifiedSpan(classifiedSecondarySpan.ClassificationType, updated)); + remainingSpan = new TextSpan(updated.End, remainingSpan.Length - (updated.End - remainingSpan.Start)); + } + + // Make sure that we're not introducing a gap. Remember, we need to fill in the whitespace. + if (remainingSpan.Start < primarySpan.End) + { + builder.Add(new ClassifiedSpan( + ClassificationTypeNames.Text, + new TextSpan(remainingSpan.Start, primarySpan.End - remainingSpan.Start))); + remainingSpan = new TextSpan(primarySpan.End, remainingSpan.Length - (primarySpan.End - remainingSpan.Start)); + } + + } + + // Deal with residue + if (remainingSpan.Length > 0) + { + // Trailing Razor/markup text. + builder.Add(new ClassifiedSpan(ClassificationTypeNames.Text, remainingSpan)); + } + + return builder; + } + + // We have IVT access to the Roslyn APIs for product code, but not for testing. + public enum ExcerptModeInternal + { + SingleLine = ExcerptMode.SingleLine, + Tooltip = ExcerptMode.Tooltip, + } + + // We have IVT access to the Roslyn APIs for product code, but not for testing. + public readonly struct ExcerptResultInternal + { + public readonly SourceText Content; + + public readonly TextSpan MappedSpan; + + public readonly ImmutableArray ClassifiedSpans; + + public readonly Document Document; + + public readonly TextSpan Span; + + public ExcerptResultInternal( + SourceText content, + TextSpan mappedSpan, + ImmutableArray classifiedSpans, + Document document, + TextSpan span) + { + Content = content; + MappedSpan = mappedSpan; + ClassifiedSpans = classifiedSpans; + Document = document; + Span = span; + } + + public ExcerptResult ToExcerptResult() + { + return new ExcerptResult(Content, MappedSpan, ClassifiedSpans, Document, Span); + } + } + } +} \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorDocumentServiceProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorDocumentServiceProvider.cs new file mode 100644 index 0000000000..68a38f1c08 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorDocumentServiceProvider.cs @@ -0,0 +1,71 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Host +{ + internal class RazorDocumentServiceProvider : IDocumentServiceProvider, IDocumentOperationService + { + private readonly DocumentSnapshot _document; + private readonly object _lock; + + private RazorSpanMappingService _spanMappingService; + private RazorDocumentExcerptService _excerptService; + + public RazorDocumentServiceProvider() + : this(null) + { + } + + public RazorDocumentServiceProvider(DocumentSnapshot document) + { + _document = document; + + _lock = new object(); + } + + public bool CanApplyChange => false; + + public bool SupportDiagnostics => false; + + public TService GetService() where TService : class, IDocumentService + { + if (typeof(TService) == typeof(ISpanMappingService)) + { + if (_spanMappingService == null) + { + lock (_lock) + { + if (_spanMappingService == null) + { + _spanMappingService = new RazorSpanMappingService(_document); + } + } + } + + return (TService)(object)_spanMappingService; + } + + if (typeof(TService) == typeof(IDocumentExcerptService)) + { + if (_excerptService == null) + { + lock (_lock) + { + if (_excerptService == null) + { + _excerptService = new RazorDocumentExcerptService(_document, GetService()); + } + } + } + + return (TService)(object)_excerptService; + } + + return this as TService; + } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSpanMappingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSpanMappingService.cs new file mode 100644 index 0000000000..7303d8a69a --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSpanMappingService.cs @@ -0,0 +1,102 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal class RazorSpanMappingService: ISpanMappingService + { + private readonly DocumentSnapshot _document; + + public RazorSpanMappingService(DocumentSnapshot document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + _document = document; + } + + public async Task> MapSpansAsync( + Document document, + IEnumerable spans, + CancellationToken cancellationToken) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + if (spans == null) + { + throw new ArgumentNullException(nameof(spans)); + } + + // Called on an uninitialized document. + if (_document == null) + { + return ImmutableArray.Create(); + } + + var source = await _document.GetTextAsync().ConfigureAwait(false); + var output = await _document.GetGeneratedOutputAsync().ConfigureAwait(false); + + var results = ImmutableArray.CreateBuilder(); + foreach (var span in spans) + { + if (TryGetLinePositionSpan(span, source, output.GetCSharpDocument(), out var linePositionSpan)) + { + results.Add(new MappedSpanResult(output.Source.FilePath, linePositionSpan, span)); + } + else + { + results.Add(default); + } + } + + return results.ToImmutable(); + } + + // Internal for testing. + internal static bool TryGetLinePositionSpan(TextSpan span, SourceText source, RazorCSharpDocument output, out LinePositionSpan linePositionSpan) + { + var mappings = output.SourceMappings; + for (var i = 0; i < mappings.Count; i++) + { + var mapping = mappings[i]; + var original = mapping.OriginalSpan.AsTextSpan(); + var generated = mapping.GeneratedSpan.AsTextSpan(); + + if (!generated.Contains(span)) + { + // If the search span isn't contained within the generated span, it is not a match. + // A C# identifier won't cover multiple generated spans. + continue; + } + + var leftOffset = span.Start - generated.Start; + var rightOffset = span.End - generated.End; + if (leftOffset >= 0 && rightOffset <= 0) + { + // This span mapping contains the span. + var adjusted = new TextSpan(original.Start + leftOffset, (original.End + rightOffset) - (original.Start + leftOffset)); + linePositionSpan = source.Lines.GetLinePositionSpan(adjusted); + return true; + } + } + + linePositionSpan = default; + return false; + } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolutionResult.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolutionResult.cs index 0e8c7b19ef..581892a9fb 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolutionResult.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolutionResult.cs @@ -7,9 +7,9 @@ using Microsoft.AspNetCore.Razor.Language; namespace Microsoft.CodeAnalysis.Razor { - public sealed class TagHelperResolutionResult + internal sealed class TagHelperResolutionResult { - internal static TagHelperResolutionResult Empty = new TagHelperResolutionResult(Array.Empty(), Array.Empty()); + internal static readonly TagHelperResolutionResult Empty = new TagHelperResolutionResult(Array.Empty(), Array.Empty()); public TagHelperResolutionResult(IReadOnlyList descriptors, IReadOnlyList diagnostics) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor/ComponentTagHelperDescriptorProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor/ComponentTagHelperDescriptorProvider.cs new file mode 100644 index 0000000000..def7c30ac0 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor/ComponentTagHelperDescriptorProvider.cs @@ -0,0 +1,132 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Razor +{ + public class ComponentTagHelperDescriptorProvider : RazorEngineFeatureBase, ITagHelperDescriptorProvider + { + public int Order { get; set; } + + public void Execute(TagHelperDescriptorProviderContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var compilation = context.GetCompilation(); + if (compilation == null) + { + // No compilation, nothing to do. + return; + } + + var componentSymbol = compilation.GetTypeByMetadataName(TagHelperTypes.IComponent); + if (componentSymbol == null || componentSymbol.TypeKind == TypeKind.Error) + { + // Could not find attributes we care about in the compilation. Nothing to do. + return; + } + + var types = new List(); + var visitor = new ComponentTypeVisitor(componentSymbol, types); + + // We always visit the global namespace. + visitor.Visit(compilation.Assembly.GlobalNamespace); + + foreach (var reference in compilation.References) + { + if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly) + { + if (IsTagHelperAssembly(assembly)) + { + visitor.Visit(assembly.GlobalNamespace); + } + } + } + + for (var i = 0; i < types.Count; i++) + { + var type = types[i]; + var descriptor = CreateDescriptor(type); + + if (descriptor != null) + { + context.Results.Add(descriptor); + } + } + } + + private static TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type) + { + var typeName = type.ToDisplayString(DefaultTagHelperDescriptorFactory.FullNameTypeDisplayFormat); + var assemblyName = type.ContainingAssembly.Identity.Name; + + var descriptorBuilder = TagHelperDescriptorBuilder.Create(TagHelperConventions.ComponentKind, typeName, assemblyName); + descriptorBuilder.SetTypeName(typeName); + descriptorBuilder.Metadata[TagHelperMetadata.Runtime.Name] = TagHelperConventions.ComponentKind; + + // Components have very simple matching rules. The type name (short) matches the tag name. + descriptorBuilder.TagMatchingRule(r => r.TagName = type.Name); + + var descriptor = descriptorBuilder.Build(); + return descriptor; + } + + private bool IsTagHelperAssembly(IAssemblySymbol assembly) + { + return assembly.Name != null && !assembly.Name.StartsWith("System.", StringComparison.Ordinal); + } + + // Visits top-level types and finds interface implementations. + internal class ComponentTypeVisitor : SymbolVisitor + { + private readonly INamedTypeSymbol _componentSymbol; + private readonly List _results; + + public ComponentTypeVisitor(INamedTypeSymbol componentSymbol, List results) + { + _componentSymbol = componentSymbol; + _results = results; + } + + public override void VisitNamedType(INamedTypeSymbol symbol) + { + if (IsComponent(symbol)) + { + _results.Add(symbol); + } + } + + public override void VisitNamespace(INamespaceSymbol symbol) + { + foreach (var member in symbol.GetMembers()) + { + Visit(member); + } + } + + public override void VisitAssembly(IAssemblySymbol symbol) + { + // This as a simple yet high-value optimization that excludes the vast majority of + // assemblies that (by definition) can't contain a component. + if (symbol.Name != null && !symbol.Name.StartsWith("System.", StringComparison.Ordinal)) + { + Visit(symbol.GlobalNamespace); + } + } + + internal bool IsComponent(INamedTypeSymbol symbol) + { + return + symbol.DeclaredAccessibility == Accessibility.Public && + !symbol.IsAbstract && + symbol.AllInterfaces.Contains(_componentSymbol); + } + } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorFactory.cs index 9b6b07f639..333003f5b2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorFactory.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor/DefaultTagHelperDescriptorFactory.cs @@ -23,7 +23,7 @@ namespace Microsoft.CodeAnalysis.Razor private readonly INamedTypeSymbol _restrictChildrenAttributeSymbol; private readonly INamedTypeSymbol _editorBrowsableAttributeSymbol; - private static readonly SymbolDisplayFormat FullNameTypeDisplayFormat = + internal static readonly SymbolDisplayFormat FullNameTypeDisplayFormat = SymbolDisplayFormat.FullyQualifiedFormat .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted) .WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions & (~SymbolDisplayMiscellaneousOptions.UseSpecialTypes)); @@ -434,6 +434,6 @@ namespace Microsoft.CodeAnalysis.Razor return false; } - private static string GetFullName(ITypeSymbol type) => type.ToDisplayString(FullNameTypeDisplayFormat); + protected static string GetFullName(ITypeSymbol type) => type.ToDisplayString(FullNameTypeDisplayFormat); } } \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor/Microsoft.CodeAnalysis.Razor.csproj b/src/Razor/src/Microsoft.CodeAnalysis.Razor/Microsoft.CodeAnalysis.Razor.csproj index 4391fc3cdf..e1efe71870 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor/Microsoft.CodeAnalysis.Razor.csproj +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor/Microsoft.CodeAnalysis.Razor.csproj @@ -2,7 +2,7 @@ Razor is a markup syntax for adding server-side logic to web pages. This package contains the Razor design-time infrastructure. - net46;netstandard2.0 + netstandard2.0 @@ -13,9 +13,4 @@ - - - - - diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor/Properties/AssemblyInfo.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor/Properties/AssemblyInfo.cs index 0de18b91dc..2da745cbad 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor/Properties/AssemblyInfo.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor/Properties/AssemblyInfo.cs @@ -2,7 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Runtime.CompilerServices; - +[assembly: InternalsVisibleTo("rzls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.LanguageServer.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.LiveShare.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.LiveShare.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.CodeAnalysis.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor/SourceSpanExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor/SourceSpanExtensions.cs new file mode 100644 index 0000000000..51fd517024 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor/SourceSpanExtensions.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal static class SourceSpanExtensions + { + public static TextSpan AsTextSpan(this SourceSpan sourceSpan) + { + return new TextSpan(sourceSpan.AbsoluteIndex, sourceSpan.Length); + } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor/TagHelperTypes.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor/TagHelperTypes.cs index d2ea2e3507..c0989576c7 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor/TagHelperTypes.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor/TagHelperTypes.cs @@ -7,6 +7,8 @@ namespace Microsoft.CodeAnalysis.Razor { public const string ITagHelper = "Microsoft.AspNetCore.Razor.TagHelpers.ITagHelper"; + public const string IComponent = "Microsoft.AspNetCore.Components.IComponent"; + public const string IDictionary = "System.Collections.Generic.IDictionary`2"; public const string HtmlAttributeNameAttribute = "Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute"; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj index 369dd82020..74ac5b3583 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj @@ -2,7 +2,7 @@ Razor is a markup syntax for adding server-side logic to web pages. This package contains the Razor design-time infrastructure. - net46 + net472 false @@ -25,4 +25,8 @@ + + + + diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs index 289d75cc0f..32e8e3bcb6 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RazorServiceBase.cs @@ -76,6 +76,16 @@ namespace Microsoft.CodeAnalysis.Remote.Razor return null; } + public override bool IsImportDocument(DocumentSnapshot document) + { + throw new NotImplementedException(); + } + + public override IEnumerable GetRelatedDocuments(DocumentSnapshot document) + { + throw new NotImplementedException(); + } + public override RazorProjectEngine GetProjectEngine() { throw new NotImplementedException(); diff --git a/src/Razor/src/Microsoft.NET.Sdk.Razor/DotnetToolTask.cs b/src/Razor/src/Microsoft.NET.Sdk.Razor/DotnetToolTask.cs index d0ec48f2b2..79694479a4 100644 --- a/src/Razor/src/Microsoft.NET.Sdk.Razor/DotnetToolTask.cs +++ b/src/Razor/src/Microsoft.NET.Sdk.Razor/DotnetToolTask.cs @@ -124,7 +124,7 @@ namespace Microsoft.AspNetCore.Razor.Tasks string commandLineCommands, out int result) { -#if !NET46 +#if !NETFRAMEWORK if (!SuppressCurrentUserOnlyPipeOptions && !Enum.IsDefined(typeof(PipeOptions), PipeOptionCurrentUserOnly)) { // For security reasons, we don't want to spin up a server that doesn't diff --git a/src/Razor/src/Microsoft.NET.Sdk.Razor/Microsoft.NET.Sdk.Razor.csproj b/src/Razor/src/Microsoft.NET.Sdk.Razor/Microsoft.NET.Sdk.Razor.csproj index a835c5d06a..6f6d4be734 100644 --- a/src/Razor/src/Microsoft.NET.Sdk.Razor/Microsoft.NET.Sdk.Razor.csproj +++ b/src/Razor/src/Microsoft.NET.Sdk.Razor/Microsoft.NET.Sdk.Razor.csproj @@ -1,30 +1,22 @@ - + Razor is a markup syntax for adding server-side logic to web pages. This package contains MSBuild support for Razor. - netstandard2.0;net46 + netcoreapp3.0;netstandard2.0;net46 Microsoft.NET.Sdk.Razor.Tasks - false - + $(MSBuildProjectName).nuspec + true + bin\$(Configuration)\sdk-output\ - - - - - - - - - - - - + + + - - + + @@ -41,4 +33,64 @@ Shared\Client.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + id=$(MSBuildProjectName); + version=$(PackageVersion); + authors=$(Authors); + licenseUrl=$(PackageLicenseUrl); + projectUrl=$(PackageProjectUrl); + iconUrl=$(PackageIconUrl); + copyright=$(Copyright); + description=$(Description); + tags=$([MSBuild]::Escape($(PackageTags))); + serviceable=$(Serviceable); + repositoryUrl=$(RepositoryUrl); + repositoryCommit=$(RepositoryCommit); + targetframework=$(TargetFramework); + outputpath=$(OutputPath)\sdk-output; + + + diff --git a/src/Razor/src/Microsoft.NET.Sdk.Razor/Microsoft.NET.Sdk.Razor.nuspec b/src/Razor/src/Microsoft.NET.Sdk.Razor/Microsoft.NET.Sdk.Razor.nuspec new file mode 100644 index 0000000000..8593fba340 --- /dev/null +++ b/src/Razor/src/Microsoft.NET.Sdk.Razor/Microsoft.NET.Sdk.Razor.nuspec @@ -0,0 +1,25 @@ + + + + $id$ + $version$ + $authors$ + true + $licenseUrl$ + $projectUrl$ + $iconUrl$ + $description$ + $copyright$ + $tags$ + $serviceable$ + + + + + + + + + + + diff --git a/src/Razor/src/Microsoft.NET.Sdk.Razor/RazorGenerate.cs b/src/Razor/src/Microsoft.NET.Sdk.Razor/RazorGenerate.cs index 0ed71d0ada..61246c8484 100644 --- a/src/Razor/src/Microsoft.NET.Sdk.Razor/RazorGenerate.cs +++ b/src/Razor/src/Microsoft.NET.Sdk.Razor/RazorGenerate.cs @@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.Razor.Tasks }; private const string GeneratedOutput = "GeneratedOutput"; + private const string DocumentKind = "DocumentKind"; private const string TargetPath = "TargetPath"; private const string FullPath = "FullPath"; private const string Identity = "Identity"; @@ -41,6 +42,8 @@ namespace Microsoft.AspNetCore.Razor.Tasks [Required] public string TagHelperManifest { get; set; } + public bool GenerateDeclaration { get; set; } + internal override string Command => "generate"; protected override bool ValidateParameters() @@ -99,6 +102,13 @@ namespace Microsoft.AspNetCore.Razor.Tasks builder.AppendLine("-o"); var outputPath = Path.Combine(ProjectRoot, input.GetMetadata(GeneratedOutput)); builder.AppendLine(outputPath); + + var kind = input.GetMetadata(DocumentKind); + if (!string.IsNullOrEmpty(kind)) + { + builder.AppendLine("-k"); + builder.AppendLine(kind); + } } builder.AppendLine("-p"); @@ -113,6 +123,11 @@ namespace Microsoft.AspNetCore.Razor.Tasks builder.AppendLine("-c"); builder.AppendLine(Configuration[0].GetMetadata(Identity)); + if (GenerateDeclaration) + { + builder.AppendLine("--generate-declaration"); + } + for (var i = 0; i < Extensions.Length; i++) { builder.AppendLine("-n"); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets b/src/Razor/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.CodeGeneration.targets similarity index 80% rename from src/Razor/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets rename to src/Razor/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.CodeGeneration.targets index c81d741e1f..72d170a7d2 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets +++ b/src/Razor/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.CodeGeneration.targets @@ -1,3 +1,15 @@ + + <_RazorTagHelperInputCache>$(IntermediateOutputPath)$(TargetName).TagHelpers.input.cache <_RazorTagHelperOutputCache>$(IntermediateOutputPath)$(TargetName).TagHelpers.output.cache - + <_RazorGenerateInputsHash> <_RazorGenerateInputsHashFile>$(IntermediateOutputPath)$(MSBuildProjectName).RazorCoreGenerate.cache + + <_RazorToolAssembly Condition="'$(_RazorToolAssembly)'==''">$(RazorSdkDirectoryRoot)tools\netcoreapp3.0\rzc.dll - @@ -56,14 +70,19 @@
+ + - + + + + + + <_RazorComponentInputHash> + <_RazorComponentInputCacheFile>$(IntermediateOutputPath)$(MSBuildProjectName).RazorComponent.input.cache + + + + + <_RazorComponentDeclarationAssembly Include="$(_RazorComponentDeclarationOutputPath)$(TargetName).dll" /> + + + + + + + + + + + + + + + + + + + + + <_RazorComponentDeclaration Include="%(RazorComponentWithTargetPath.GeneratedDeclaration)" /> + <_RazorComponentDefinition Include="%(RazorComponentWithTargetPath.GeneratedOutput)" /> + + + + + + _HashRazorComponentInputs; + _ResolveRazorComponentOutputs; + + + + + + + + <_RazorComponentDeclarationSources Include="@(RazorComponentWithTargetPath)"> + %(RazorComponentWithTargetPath.GeneratedDeclaration) + + + + + + + + <_RazorComponentDeclarationManifest>$(IntermediateOutputPath)$(MSBuildProjectName).RazorComponents.declaration.json + + + + + + + + + + + + + + + + + + + + $(NoWarn);1701;1702 + + + + + $(NoWarn);2008 + + + + + + + + + + + $(AppConfig) + + + + + false + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + <_RazorGenerateTargetReferenceAssembly>@(_RazorComponentDeclarationAssembly->Metadata('FullPath')) + + + + + + + + + + + + + + RazorGenerateComponentDeclaration; + RazorCompileComponentDeclaration; + RazorGenerateComponentDefinition; + + + + + + + diff --git a/src/Razor/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.Configuration.targets b/src/Razor/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.Configuration.targets new file mode 100644 index 0000000000..995cb2cd90 --- /dev/null +++ b/src/Razor/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.Configuration.targets @@ -0,0 +1,92 @@ + + + + + + true + + + true + + + $(GenerateRazorAssemblyInfo) + + + Microsoft.AspNetCore.Mvc.ApplicationParts.CompiledRazorAssemblyApplicationPartFactory, Microsoft.AspNetCore.Mvc.Razor + + + true + + + + <_TargetingNETCoreApp30OrLater Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND '$(_TargetFrameworkVersionWithoutV)' > '2.9'">true + + + + 3.0 + + + + + MVC-3.0 + + + + + + MVC-3.0;$(CustomRazorExtension) + + + + + + Microsoft.AspNetCore.Mvc.Razor.Extensions + $(RazorSdkDirectoryRoot)extensions\mvc-3-0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll + + + + + <_RazorAssemblyAttribute Include="Microsoft.AspNetCore.Mvc.ApplicationParts.RelatedAssemblyAttribute"> + <_Parameter1>$(RazorTargetName) + + + + + + <_Parameter1>$(ProvideApplicationPartFactoryAttributeTypeName) + + + + diff --git a/src/Razor/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.DesignTime.targets b/src/Razor/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.DesignTime.targets index 64c5d8f61a..6df58f657e 100644 --- a/src/Razor/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.DesignTime.targets +++ b/src/Razor/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.DesignTime.targets @@ -21,6 +21,12 @@ Copyright (c) .NET Foundation. All rights reserved. we use the lack of the capability to detect downlevel scenarios. --> + + + true + + true + - .g.cshtml.cs + .g.cs + + + .g.i.cs true + + diff --git a/src/Razor/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets b/src/Razor/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets index 1790ba588d..704b14d22a 100644 --- a/src/Razor/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets +++ b/src/Razor/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets @@ -22,10 +22,15 @@ Copyright (c) .NET Foundation. All rights reserved. - $(MSBuildThisFileDirectory)..\..\tasks\ + + $(MSBuildThisFileDirectory)..\..\ + $(RazorSdkDirectoryRoot)tasks\ <_RazorSdkTasksTFM Condition=" '$(MSBuildRuntimeType)' == 'Core'">netstandard2.0 <_RazorSdkTasksTFM Condition=" '$(_RazorSdkTasksTFM)' == ''">net46 $(RazorSdkBuildTasksDirectoryRoot)$(_RazorSdkTasksTFM)\Microsoft.NET.Sdk.Razor.Tasks.dll + + <_TargetFrameworkVersionWithoutV>$(TargetFrameworkVersion.TrimStart('vV')) + <_EnableAllInclusiveRazorSdk Condition="'$(_EnableInclusiveRazorSdk)' == '' AND '$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND '$(_TargetFrameworkVersionWithoutV)' > '2.9'">true $(IntermediateOutputPath)Razor\ + <_RazorComponentDeclarationOutputPath Condition="'$(_RazorComponentDeclarationOutputPath)'==''">$(IntermediateOutputPath)RazorDeclaration\ + + + .Views + .Razor - + $(TargetName)$(RazorTargetNameSuffix) - - PrecompilationTool + + PrecompilationTool true @@ -200,7 +228,7 @@ Copyright (c) .NET Foundation. All rights reserved. - + <_RazorDebugSymbolsIntermediatePath Condition="'$(_RazorDebugSymbolsProduced)'=='true'" Include="$(IntermediateOutputPath)$(RazorTargetName).pdb" /> @@ -219,13 +247,24 @@ Copyright (c) .NET Foundation. All rights reserved. Condition="'$(RazorCompileOnBuild)'=='true' AND '@(Content->WithMetadataValue('Extension', '.cshtml'))' != ''" /> - + + + + + Condition="'$(IsRazorCompilerReferenced)' == 'true' AND '$(RazorCodeGenerationTargetsPath)' != '' AND Exists('$(RazorCodeGenerationTargetsPath)')" /> + + + + + + + + @@ -239,9 +278,15 @@ Copyright (c) .NET Foundation. All rights reserved. + + + + + + @@ -252,15 +297,15 @@ Copyright (c) .NET Foundation. All rights reserved. Computes the applicable @(ResolvedRazorConfiguration) and @(ResolvedRazorExtension) items that match the project's configuration. --> - - + - + @@ -280,7 +325,7 @@ Copyright (c) .NET Foundation. All rights reserved. <_GenerateRazorAssemblyInfoDependsOn>RazorGetAssemblyAttributes;$(_GenerateRazorAssemblyInfoDependsOn) - @@ -335,7 +380,7 @@ Copyright (c) .NET Foundation. All rights reserved. '$(CopyRazorGenerateFilesToPublishDirectory)'=='false' and '$(ResolvedRazorCompileToolset)'=='RazorSdk' and '$(RazorCompileOnPublish)'=='true'"> - + @@ -347,6 +392,40 @@ Copyright (c) .NET Foundation. All rights reserved. + + + + + + + + + + + + + + + + $(RazorGenerateIntermediateOutputPath)%(RazorComponentWithTargetPath.TargetPath)$(RazorGenerateOutputFileExtension) + + + + $(_RazorComponentDeclarationOutputPath)%(RazorComponentWithTargetPath.TargetPath)$(RazorGenerateOutputFileExtension) + + + + component + + + + @@ -354,7 +433,11 @@ Copyright (c) .NET Foundation. All rights reserved. - $(RazorGenerateIntermediateOutputPath)$([System.IO.Path]::ChangeExtension('%(RazorGenerateWithTargetPath.TargetPath)', '$(RazorGenerateOutputFileExtension)')) + $(RazorGenerateIntermediateOutputPath)%(RazorGenerateWithTargetPath.TargetPath)$(RazorGenerateOutputFileExtension) + + + + mvc @@ -367,8 +450,20 @@ Copyright (c) .NET Foundation. All rights reserved. DependsOnTargets="ResolveReferences"> - + + + + <_RazorGenerateTargetReferenceAssembly Condition="'$(_RazorGenerateTargetReferenceAssembly)'==''"> + @(IntermediateAssembly->Metadata('FullPath')) + + + <_RazorCompileTargetReferenceAssembly Condition="'$(_RazorCompileTargetReferenceAssembly)'==''"> + @(IntermediateAssembly->Metadata('FullPath')) + + - - @@ -428,7 +523,7 @@ Copyright (c) .NET Foundation. All rights reserved. - + - @@ -500,8 +595,8 @@ Copyright (c) .NET Foundation. All rights reserved. Called as part of CopyFilesToOutputDirectory - this target is called when building the project to copy files to the output directory. --> - @@ -522,9 +617,9 @@ Copyright (c) .NET Foundation. All rights reserved. - @@ -550,7 +645,7 @@ Copyright (c) .NET Foundation. All rights reserved. - + - diff --git a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs index 4de857b966..1c04b06710 100644 --- a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs +++ b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/BraceSmartIndenter.cs @@ -5,12 +5,12 @@ using System; using System.Text; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.CodeAnalysis.Razor; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Operations; using ITextBuffer = Microsoft.VisualStudio.Text.ITextBuffer; -using Span = Microsoft.AspNetCore.Razor.Language.Legacy.Span; namespace Microsoft.VisualStudio.Editor.Razor { @@ -274,13 +274,15 @@ namespace Microsoft.VisualStudio.Editor.Razor } // Internal for testing - internal static bool ContainsInvalidContent(Span owner) + internal static bool ContainsInvalidContent(SyntaxNode owner) { - // We only support whitespace based content. Any non-whitespace content is an unkonwn to us + // We only support whitespace based content. Any non-whitespace content is an unknown to us // in regards to indentation. - for (var i = 0; i < owner.Tokens.Count; i++) + var children = owner.ChildNodes(); + for (var i = 0; i < children.Count; i++) { - if (!string.IsNullOrWhiteSpace(owner.Tokens[i].Content)) + if (!(children[i] is SyntaxToken token) || + !string.IsNullOrWhiteSpace(token.Content)) { return true; } @@ -290,18 +292,18 @@ namespace Microsoft.VisualStudio.Editor.Razor } // Internal for testing - internal static bool IsUnlinkedSpan(Span owner) + internal static bool IsUnlinkedSpan(SyntaxNode owner) { return owner == null || - owner.Next == null || - owner.Previous == null; + owner.NextSpan() == null || + owner.PreviousSpan() == null; } // Internal for testing - internal static bool SurroundedByInvalidContent(Span owner) + internal static bool SurroundedByInvalidContent(SyntaxNode owner) { - return owner.Next.Kind != SpanKindInternal.MetaCode || - owner.Previous.Kind != SpanKindInternal.MetaCode; + return !owner.NextSpan().IsMetaCodeSpanKind() || + !owner.PreviousSpan().IsMetaCodeSpanKind(); } internal static bool BeforeClosingBrace(int linePosition, ITextSnapshotLine lineSnapshot) diff --git a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/DefaultRazorIndentationFactsService.cs b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/DefaultRazorIndentationFactsService.cs index f69274fa15..8b460df9c0 100644 --- a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/DefaultRazorIndentationFactsService.cs +++ b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/DefaultRazorIndentationFactsService.cs @@ -5,8 +5,8 @@ using System; using System.ComponentModel.Composition; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.VisualStudio.Text; -using Span = Microsoft.AspNetCore.Razor.Language.Legacy.Span; namespace Microsoft.VisualStudio.Editor.Razor { @@ -63,8 +63,8 @@ namespace Microsoft.VisualStudio.Editor.Razor var previousLineEndIndex = GetPreviousLineEndIndex(syntaxTreeSnapshot, line); var simulatedChange = new SourceChange(previousLineEndIndex, 0, string.Empty); - var owningSpan = syntaxTree.Root.LocateOwner(simulatedChange); - if (owningSpan == null || owningSpan.Kind == SpanKindInternal.Code) + var owner = syntaxTree.Root.LocateOwner(simulatedChange); + if (owner == null || owner.IsCodeSpanKind()) { // Example, // @{\n @@ -74,20 +74,19 @@ namespace Microsoft.VisualStudio.Editor.Razor } int? desiredIndentation = null; - SyntaxTreeNode owningChild = owningSpan; - while (owningChild.Parent != null) + while (owner.Parent != null) { - var owningParent = owningChild.Parent; - for (var i = 0; i < owningParent.Children.Count; i++) + var children = owner.Parent.ChildNodes(); + for (var i = 0; i < children.Count; i++) { - var currentChild = owningParent.Children[i]; + var currentChild = children[i]; if (IsCSharpOpenCurlyBrace(currentChild)) { - var lineText = line.Snapshot.GetLineFromLineNumber(currentChild.Start.LineIndex).GetText(); + var lineText = line.Snapshot.GetLineFromLineNumber(currentChild.GetSourceLocation(syntaxTree.Source).LineIndex).GetText(); desiredIndentation = GetIndentLevelOfLine(lineText, tabSize) + indentSize; } - if (currentChild == owningChild) + if (currentChild == owner) { break; } @@ -98,7 +97,7 @@ namespace Microsoft.VisualStudio.Editor.Razor return desiredIndentation; } - owningChild = owningParent; + owner = owner.Parent; } // Couldn't determine indentation @@ -139,12 +138,12 @@ namespace Microsoft.VisualStudio.Editor.Razor } // Internal for testing - internal static bool IsCSharpOpenCurlyBrace(SyntaxTreeNode currentChild) + internal static bool IsCSharpOpenCurlyBrace(SyntaxNode node) { - return currentChild is Span currentSpan && - currentSpan.Tokens.Count == 1 && - currentSpan.Tokens[0] is CSharpToken symbol && - symbol.Type == CSharpTokenType.LeftBrace; + var children = node.ChildNodes(); + return children.Count == 1 && + children[0].IsToken && + children[0].Kind == SyntaxKind.LeftBrace; } } } diff --git a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_0.cs b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_0.cs index ebf9e144f5..ef3c1779f0 100644 --- a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_0.cs +++ b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_0.cs @@ -11,7 +11,7 @@ namespace Microsoft.VisualStudio.Editor.Razor [ExportCustomProjectEngineFactory("MVC-2.0", SupportsSerialization = true)] internal class LegacyProjectEngineFactory_2_0 : IProjectEngineFactory { - private const string AssemblyName = "Microsoft.AspNetCore.Mvc.Razor.Extensions"; + private const string AssemblyName = "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X"; public RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) { // Rewrite the assembly name into a full name just like this one, but with the name of the MVC design time assembly. diff --git a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_1.cs b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_1.cs index df8d04667c..35ec395c2e 100644 --- a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_1.cs +++ b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_2_1.cs @@ -13,7 +13,7 @@ namespace Microsoft.VisualStudio.Editor.Razor [ExportCustomProjectEngineFactory("MVC-2.1", SupportsSerialization = true)] internal class LegacyProjectEngineFactory_2_1 : IProjectEngineFactory { - private const string AssemblyName = "Microsoft.AspNetCore.Mvc.Razor.Extensions"; + private const string AssemblyName = "Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X"; public RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) { // Rewrite the assembly name into a full name just like this one, but with the name of the MVC design time assembly. diff --git a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_3_0.cs b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_3_0.cs new file mode 100644 index 0000000000..afdb06b05d --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/LegacyProjectEngineFactory_3_0.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + [ExportCustomProjectEngineFactory("MVC-3.0", SupportsSerialization = true)] + internal class LegacyProjectEngineFactory_3_0 : IProjectEngineFactory + { + private const string AssemblyName = "Microsoft.AspNetCore.Mvc.Razor.Extensions"; + public RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem, Action configure) + { + // Rewrite the assembly name into a full name just like this one, but with the name of the MVC design time assembly. + var assemblyName = new AssemblyName(typeof(LegacyProjectEngineFactory_3_0).Assembly.FullName); + assemblyName.Name = AssemblyName; + + var extension = new AssemblyExtension(configuration.ConfigurationName, Assembly.Load(assemblyName)); + var initializer = extension.CreateInitializer(); + + return RazorProjectEngine.Create(configuration, fileSystem, b => + { + initializer.Initialize(b); + configure?.Invoke(b); + }); + } + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/Microsoft.VisualStudio.Editor.Razor.csproj b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/Microsoft.VisualStudio.Editor.Razor.csproj index aa50d39965..7ae2c1ff0a 100644 --- a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/Microsoft.VisualStudio.Editor.Razor.csproj +++ b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/Microsoft.VisualStudio.Editor.Razor.csproj @@ -1,7 +1,7 @@  - net46 + net472 Razor is a markup syntax for adding server-side logic to web pages. This package contains the Visual Studio agnostic Razor design-time infrastructure. false @@ -9,7 +9,10 @@ - + + + + diff --git a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/Properties/AssemblyInfo.cs b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/Properties/AssemblyInfo.cs index 47ff95c911..1099d78d69 100644 --- a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/Properties/AssemblyInfo.cs +++ b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/Properties/AssemblyInfo.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("rzls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.LanguageServer.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.LiveShare.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.LiveShare.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/RazorDirectiveCompletionProvider.cs b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/RazorDirectiveCompletionProvider.cs index 6a2d017716..89d723159c 100644 --- a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/RazorDirectiveCompletionProvider.cs +++ b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/RazorDirectiveCompletionProvider.cs @@ -16,6 +16,7 @@ using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Tags; using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Projection; @@ -36,16 +37,33 @@ namespace Microsoft.VisualStudio.Editor.Razor CSharpCodeParser.TagHelperPrefixDirectiveDescriptor, }; private readonly Lazy _codeDocumentProvider; + private readonly Lazy _dependencies; + private readonly RazorTextBufferProvider _textBufferProvider; [ImportingConstructor] - public RazorDirectiveCompletionProvider([Import(typeof(RazorCodeDocumentProvider))] Lazy codeDocumentProvider) + public RazorDirectiveCompletionProvider( + [Import(typeof(RazorCodeDocumentProvider))] Lazy codeDocumentProvider, + [Import(typeof(CompletionProviderDependencies))] Lazy dependencies, + RazorTextBufferProvider textBufferProvider) { if (codeDocumentProvider == null) { throw new ArgumentNullException(nameof(codeDocumentProvider)); } + if (dependencies == null) + { + throw new ArgumentNullException(nameof(dependencies)); + } + + if (textBufferProvider == null) + { + throw new ArgumentNullException(nameof(textBufferProvider)); + } + _codeDocumentProvider = codeDocumentProvider; + _dependencies = dependencies; + _textBufferProvider = textBufferProvider; } public override Task GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken) @@ -96,6 +114,13 @@ namespace Microsoft.VisualStudio.Editor.Razor [MethodImpl(MethodImplOptions.NoInlining)] private Task AddCompletionItems(CompletionContext context) { + if (!_textBufferProvider.TryGetFromDocument(context.Document, out var textBuffer) || + _dependencies.Value.AsyncCompletionBroker.IsCompletionSupported(textBuffer.ContentType)) + { + // Async completion is supported that code path will handle completion. + return Task.CompletedTask; + } + if (!_codeDocumentProvider.Value.TryGetFromDocument(context.Document, out var codeDocument)) { // A Razor code document has not yet been associated with the document. @@ -109,70 +134,41 @@ namespace Microsoft.VisualStudio.Editor.Razor return Task.CompletedTask; } - if (!AtDirectiveCompletionPoint(syntaxTree, context)) + if (!TryGetRazorSnapshotPoint(context, out var razorSnapshotPoint)) { - // Can't have a valid directive at the current location. + // Could not find associated Razor location. return Task.CompletedTask; } - var completionItems = GetCompletionItems(syntaxTree); - context.AddItems(completionItems); + var location = new SourceSpan(razorSnapshotPoint.Position, 0); + var razorCompletionItems = _dependencies.Value.CompletionFactsService.GetCompletionItems(syntaxTree, location); - return Task.CompletedTask; - } - - // Internal virtual for testing - internal virtual IEnumerable GetCompletionItems(RazorSyntaxTree syntaxTree) - { - var directives = syntaxTree.Options.Directives.Concat(DefaultDirectives); - var completionItems = new List(); - foreach (var directive in directives) + foreach (var razorCompletionItem in razorCompletionItems) { - var propertyDictionary = new Dictionary(StringComparer.Ordinal); - - if (!string.IsNullOrEmpty(directive.Description)) + if (razorCompletionItem.Kind != RazorCompletionItemKind.Directive) { - propertyDictionary[DescriptionKey] = directive.Description; + // Don't support any other types of completion kinds other than directives. + continue; + } + + var propertyDictionary = new Dictionary(StringComparer.Ordinal); + if (!string.IsNullOrEmpty(razorCompletionItem.Description)) + { + propertyDictionary[DescriptionKey] = razorCompletionItem.Description; } var completionItem = CompletionItem.Create( - directive.Directive, + razorCompletionItem.InsertText, // This groups all Razor directives together sortText: "_RazorDirective_", rules: CompletionItemRules.Create(formatOnCommit: false), tags: ImmutableArray.Create(WellKnownTags.Intrinsic), properties: propertyDictionary.ToImmutableDictionary()); - completionItems.Add(completionItem); + + context.AddItem(completionItem); } - return completionItems; - } - - // Internal for testing - internal bool AtDirectiveCompletionPoint(RazorSyntaxTree syntaxTree, CompletionContext context) - { - if (TryGetRazorSnapshotPoint(context, out var razorSnapshotPoint)) - { - var change = new SourceChange(razorSnapshotPoint.Position, 0, string.Empty); - var owner = syntaxTree.Root.LocateOwner(change); - - if (owner == null) - { - return false; - } - - if (owner.ChunkGenerator is ExpressionChunkGenerator && - owner.Tokens.All(IsDirectiveCompletableSymbol) && - // Do not provide IntelliSense for explicit expressions. Explicit expressions will usually look like: - // [@] [(] [DateTime.Now] [)] - owner.Parent?.Children.Count > 1 && - owner.Parent.Children[1] == owner) - { - return true; - } - } - - return false; + return Task.CompletedTask; } protected virtual bool TryGetRazorSnapshotPoint(CompletionContext context, out SnapshotPoint snapshotPoint) @@ -203,17 +199,41 @@ namespace Microsoft.VisualStudio.Editor.Razor return false; } + } - private static bool IsDirectiveCompletableSymbol(AspNetCore.Razor.Language.Legacy.IToken symbol) + // These types are only for this class to provide indirection for assembly loads. + internal abstract class CompletionProviderDependencies + { + public abstract RazorCompletionFactsService CompletionFactsService { get; } + + public abstract IAsyncCompletionBroker AsyncCompletionBroker { get; } + } + + [System.Composition.Shared] + [Export(typeof(CompletionProviderDependencies))] + internal class DefaultCompletionProviderDependencies : CompletionProviderDependencies + { + [ImportingConstructor] + public DefaultCompletionProviderDependencies( + RazorCompletionFactsService completionFactsService, + IAsyncCompletionBroker asyncCompletionBroker) { - if (!(symbol is CSharpToken csharpSymbol)) + if (completionFactsService == null) { - return false; + throw new ArgumentNullException(nameof(completionFactsService)); } - return csharpSymbol.Type == CSharpTokenType.Identifier || - // Marker symbol - csharpSymbol.Type == CSharpTokenType.Unknown; + if (asyncCompletionBroker == null) + { + throw new ArgumentNullException(nameof(asyncCompletionBroker)); + } + + CompletionFactsService = completionFactsService; + AsyncCompletionBroker = asyncCompletionBroker; } + + public override RazorCompletionFactsService CompletionFactsService { get; } + + public override IAsyncCompletionBroker AsyncCompletionBroker { get; } } } diff --git a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/RazorDirectiveCompletionSource.cs b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/RazorDirectiveCompletionSource.cs new file mode 100644 index 0000000000..e4170e8acb --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/RazorDirectiveCompletionSource.cs @@ -0,0 +1,122 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.VisualStudio.Core.Imaging; +using Microsoft.VisualStudio.Imaging; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Adornments; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + internal class RazorDirectiveCompletionSource : IAsyncCompletionSource + { + // Internal for testing + internal static readonly object DescriptionKey = new object(); + // Hardcoding the Guid here to avoid a reference to Microsoft.VisualStudio.ImageCatalog.dll + // that is not present in Visual Studio for Mac + internal static readonly Guid ImageCatalogGuid = new Guid("{ae27a6b0-e345-4288-96df-5eaf394ee369}"); + internal static readonly ImageElement DirectiveImageGlyph = new ImageElement( + new ImageId(ImageCatalogGuid, 3233), // KnownImageIds.Type = 3233 + "Razor Directive."); + internal static readonly ImmutableArray DirectiveCompletionFilters = new[] { + new CompletionFilter("Razor Directive", "r", DirectiveImageGlyph) + }.ToImmutableArray(); + + // Internal for testing + internal readonly VisualStudioRazorParser _parser; + private readonly RazorCompletionFactsService _completionFactsService; + private readonly ForegroundDispatcher _foregroundDispatcher; + + public RazorDirectiveCompletionSource( + ForegroundDispatcher foregroundDispatcher, + VisualStudioRazorParser parser, + RazorCompletionFactsService completionFactsService) + { + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + if (parser == null) + { + throw new ArgumentNullException(nameof(parser)); + } + + if (completionFactsService == null) + { + throw new ArgumentNullException(nameof(completionFactsService)); + } + + _foregroundDispatcher = foregroundDispatcher; + _parser = parser; + _completionFactsService = completionFactsService; + } + + public Task GetCompletionContextAsync( + IAsyncCompletionSession session, + CompletionTrigger trigger, + SnapshotPoint triggerLocation, + SnapshotSpan applicableSpan, + CancellationToken token) + { + _foregroundDispatcher.AssertBackgroundThread(); + + var syntaxTree = _parser.CodeDocument?.GetSyntaxTree(); + var location = new SourceSpan(applicableSpan.Start.Position, applicableSpan.Length); + var razorCompletionItems = _completionFactsService.GetCompletionItems(syntaxTree, location); + + var completionItems = new List(); + foreach (var razorCompletionItem in razorCompletionItems) + { + if (razorCompletionItem.Kind != RazorCompletionItemKind.Directive) + { + // Don't support any other types of completion kinds other than directives. + continue; + } + + var completionItem = new CompletionItem( + displayText: razorCompletionItem.DisplayText, + filterText: razorCompletionItem.DisplayText, + insertText: razorCompletionItem.InsertText, + source: this, + icon: DirectiveImageGlyph, + filters: DirectiveCompletionFilters, + suffix: string.Empty, + sortText: razorCompletionItem.DisplayText, + attributeIcons: ImmutableArray.Empty); + completionItem.Properties.AddProperty(DescriptionKey, razorCompletionItem.Description); + completionItems.Add(completionItem); + } + var context = new CompletionContext(completionItems.ToImmutableArray()); + return Task.FromResult(context); + } + + public Task GetDescriptionAsync(IAsyncCompletionSession session, CompletionItem item, CancellationToken token) + { + if (!item.Properties.TryGetProperty(DescriptionKey, out var directiveDescription)) + { + directiveDescription = string.Empty; + } + + return Task.FromResult(directiveDescription); + } + + public CompletionStartData InitializeCompletion(CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token) + { + // The applicable span for completion is the piece of text a completion is for. For example: + // @Date|Time.Now + // If you trigger completion at the | then the applicable span is the region of 'DateTime'; however, Razor + // doesn't know this information so we rely on Roslyn to define what the applicable span for a completion is. + return CompletionStartData.ParticipatesInCompletionIfAny; + } + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/RazorDirectiveCompletionSourceProvider.cs b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/RazorDirectiveCompletionSourceProvider.cs new file mode 100644 index 0000000000..e1416c2b6e --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/RazorDirectiveCompletionSourceProvider.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.ComponentModel.Composition; +using System.Linq; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Editor.Razor +{ + [System.Composition.Shared] + [Export(typeof(IAsyncCompletionSourceProvider))] + [Name("Razor directive completion provider.")] + [ContentType(RazorLanguage.CoreContentType)] + internal class RazorDirectiveCompletionSourceProvider : IAsyncCompletionSourceProvider + { + private readonly ForegroundDispatcher _foregroundDispatcher; + private readonly RazorCompletionFactsService _completionFactsService; + + [ImportingConstructor] + public RazorDirectiveCompletionSourceProvider( + ForegroundDispatcher foregroundDispatcher, + RazorCompletionFactsService completionFactsService) + { + if (foregroundDispatcher == null) + { + throw new ArgumentNullException(nameof(foregroundDispatcher)); + } + + if (completionFactsService == null) + { + throw new ArgumentNullException(nameof(completionFactsService)); + } + + _foregroundDispatcher = foregroundDispatcher; + _completionFactsService = completionFactsService; + } + + public IAsyncCompletionSource GetOrCreate(ITextView textView) + { + if (textView == null) + { + throw new ArgumentNullException(nameof(textView)); + } + + var razorBuffer = textView.BufferGraph.GetRazorBuffers().FirstOrDefault(); + if (!razorBuffer.Properties.TryGetProperty(typeof(RazorDirectiveCompletionSource), out IAsyncCompletionSource completionSource) || + completionSource == null) + { + completionSource = CreateCompletionSource(razorBuffer); + razorBuffer.Properties.AddProperty(typeof(RazorDirectiveCompletionSource), completionSource); + } + + return completionSource; + } + + // Internal for testing + internal IAsyncCompletionSource CreateCompletionSource(ITextBuffer razorBuffer) + { + if (!razorBuffer.Properties.TryGetProperty(typeof(VisualStudioRazorParser), out VisualStudioRazorParser parser)) + { + // Parser hasn't been associated with the text buffer yet. + return null; + } + + var completionSource = new RazorDirectiveCompletionSource(_foregroundDispatcher, parser, _completionFactsService); + return completionSource; + } + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/RazorSyntaxTreePartialParser.cs b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/RazorSyntaxTreePartialParser.cs index 377351c959..36b4955e3d 100644 --- a/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/RazorSyntaxTreePartialParser.cs +++ b/src/Razor/src/Microsoft.VisualStudio.Editor.Razor/RazorSyntaxTreePartialParser.cs @@ -4,13 +4,13 @@ using System; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; -using Span = Microsoft.AspNetCore.Razor.Language.Legacy.Span; +using Microsoft.AspNetCore.Razor.Language.Syntax; namespace Microsoft.VisualStudio.Editor.Razor { internal class RazorSyntaxTreePartialParser { - private Span _lastChangeOwner; + private SyntaxNode _lastChangeOwner; private bool _lastResultProvisional; public RazorSyntaxTreePartialParser(RazorSyntaxTree syntaxTree) @@ -20,13 +20,15 @@ namespace Microsoft.VisualStudio.Editor.Razor throw new ArgumentNullException(nameof(syntaxTree)); } - // We mutate the existing syntax tree so we need to clone the one passed in so our mutations don't - // impact external state. - SyntaxTreeRoot = (Block)syntaxTree.Root.Clone(); + OriginalSyntaxTree = syntaxTree; + ModifiedSyntaxTreeRoot = syntaxTree.Root; } // Internal for testing - internal Block SyntaxTreeRoot { get; } + internal RazorSyntaxTree OriginalSyntaxTree { get; } + + // Internal for testing + internal SyntaxNode ModifiedSyntaxTreeRoot { get; private set; } public PartialParseResultInternal Parse(SourceChange change) { @@ -43,20 +45,24 @@ namespace Microsoft.VisualStudio.Editor.Razor var result = PartialParseResultInternal.Rejected; // Try the last change owner - if (_lastChangeOwner != null && _lastChangeOwner.EditHandler.OwnsChange(_lastChangeOwner, change)) + if (_lastChangeOwner != null) { - var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change); - result = editResult.Result; - if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected) + var editHandler = _lastChangeOwner.GetSpanContext()?.EditHandler ?? SpanEditHandler.CreateDefault(); + if (editHandler.OwnsChange(_lastChangeOwner, change)) { - _lastChangeOwner.ReplaceWith(editResult.EditedSpan); + var editResult = editHandler.ApplyChange(_lastChangeOwner, change); + result = editResult.Result; + if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected) + { + ReplaceLastChangeOwner(editResult.EditedNode); + } } return result; } // Locate the span responsible for this change - _lastChangeOwner = SyntaxTreeRoot.LocateOwner(change); + _lastChangeOwner = ModifiedSyntaxTreeRoot.LocateOwner(change); if (_lastResultProvisional) { @@ -65,15 +71,29 @@ namespace Microsoft.VisualStudio.Editor.Razor } else if (_lastChangeOwner != null) { - var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change); + var editHandler = _lastChangeOwner.GetSpanContext()?.EditHandler ?? SpanEditHandler.CreateDefault(); + var editResult = editHandler.ApplyChange(_lastChangeOwner, change); result = editResult.Result; if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected) { - _lastChangeOwner.ReplaceWith(editResult.EditedSpan); + ReplaceLastChangeOwner(editResult.EditedNode); } } return result; } + + private void ReplaceLastChangeOwner(SyntaxNode editedNode) + { + ModifiedSyntaxTreeRoot = ModifiedSyntaxTreeRoot.ReplaceNode(_lastChangeOwner, editedNode); + foreach (var node in ModifiedSyntaxTreeRoot.DescendantNodes()) + { + if (node.Green == editedNode.Green) + { + _lastChangeOwner = node; + break; + } + } + } } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentGenerator/BackgroundDocumentGenerator.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundDocumentGenerator.cs similarity index 77% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentGenerator/BackgroundDocumentGenerator.cs rename to src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundDocumentGenerator.cs index eba3c12e25..947dc1155a 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentGenerator/BackgroundDocumentGenerator.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundDocumentGenerator.cs @@ -12,14 +12,13 @@ using Microsoft.Extensions.Internal; namespace Microsoft.CodeAnalysis.Razor { - // Deliberately not exported for now, until this feature is working end to end. - // [Export(typeof(ProjectSnapshotChangeTrigger))] + [Export(typeof(ProjectSnapshotChangeTrigger))] internal class BackgroundDocumentGenerator : ProjectSnapshotChangeTrigger { - private ForegroundDispatcher _foregroundDispatcher; + private readonly ForegroundDispatcher _foregroundDispatcher; private ProjectSnapshotManagerBase _projectManager; - private readonly Dictionary _files; + private readonly Dictionary _work; private Timer _timer; [ImportingConstructor] @@ -31,17 +30,16 @@ namespace Microsoft.CodeAnalysis.Razor } _foregroundDispatcher = foregroundDispatcher; - - _files = new Dictionary(); + _work = new Dictionary(); } public bool HasPendingNotifications { get { - lock (_files) + lock (_work) { - return _files.Count > 0; + return _work.Count > 0; } } } @@ -135,11 +133,11 @@ namespace Microsoft.CodeAnalysis.Razor _foregroundDispatcher.AssertForegroundThread(); - lock (_files) + lock (_work) { // We only want to store the last 'seen' version of any given document. That way when we pick one to process // it's always the best version to use. - _files[new DocumentKey(project.FilePath, document.FilePath)] = document; + _work[new DocumentKey(project.FilePath, document.FilePath)] = document; StartWorker(); } @@ -172,18 +170,18 @@ namespace Microsoft.CodeAnalysis.Razor OnStartingBackgroundWork(); - DocumentSnapshot[] work; - lock (_files) + KeyValuePair[] work; + lock (_work) { - work = _files.Values.ToArray(); - _files.Clear(); + work = _work.ToArray(); + _work.Clear(); } OnBackgroundCapturedWorkload(); for (var i = 0; i < work.Length; i++) { - var document = work[i]; + var document = work[i].Value; try { await ProcessDocument(document); @@ -196,14 +194,14 @@ namespace Microsoft.CodeAnalysis.Razor OnCompletingBackgroundWork(); - lock (_files) + lock (_work) { // Resetting the timer allows another batch of work to start. _timer.Dispose(); _timer = null; // If more work came in while we were running start the worker again. - if (_files.Count > 0) + if (_work.Count > 0) { StartWorker(); } @@ -238,35 +236,61 @@ namespace Microsoft.CodeAnalysis.Razor switch (e.Kind) { case ProjectChangeKind.ProjectAdded: + { + var projectSnapshot = e.Newer; + foreach (var documentFilePath in projectSnapshot.DocumentFilePaths) + { + Enqueue(projectSnapshot, projectSnapshot.GetDocument(documentFilePath)); + } + + break; + } case ProjectChangeKind.ProjectChanged: { - var project = _projectManager.GetLoadedProject(e.ProjectFilePath); - foreach (var documentFilePath in project.DocumentFilePaths) + var projectSnapshot = e.Newer; + foreach (var documentFilePath in projectSnapshot.DocumentFilePaths) { - Enqueue(project, project.GetDocument(documentFilePath)); + Enqueue(projectSnapshot, projectSnapshot.GetDocument(documentFilePath)); + } + + break; + } + + case ProjectChangeKind.DocumentAdded: + case ProjectChangeKind.DocumentChanged: + { + var project = e.Newer; + var document = project.GetDocument(e.DocumentFilePath); + + Enqueue(project, document); + foreach (var relatedDocument in project.GetRelatedDocuments(document)) + { + Enqueue(project, document); + } + + break; + } + + case ProjectChangeKind.DocumentRemoved: + { + // For removals use the old snapshot to find the removed document, so we can figure out + // what the imports were in the new snapshot. + var document = e.Older.GetDocument(e.DocumentFilePath); + + foreach (var relatedDocument in e.Newer.GetRelatedDocuments(document)) + { + Enqueue(e.Newer, document); } break; } case ProjectChangeKind.ProjectRemoved: - // ignore - break; - - case ProjectChangeKind.DocumentAdded: - case ProjectChangeKind.DocumentChanged: { - var project = _projectManager.GetLoadedProject(e.ProjectFilePath); - Enqueue(project, project.GetDocument(e.DocumentFilePath)); - + // ignore break; } - - case ProjectChangeKind.DocumentRemoved: - // ignore - break; - default: throw new InvalidOperationException($"Unknown ProjectChangeKind {e.Kind}"); } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj index 32296b51c6..310b17306b 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj @@ -1,10 +1,12 @@  - net46 + net472 Razor is a markup syntax for adding server-side logic to web pages. This package contains the Razor design-time infrastructure for Visual Studio. false ..\Microsoft.NET.Sdk.Razor\build\netstandard2.0\Rules\ + $(DefineConstants);WORKSPACE_PROJECT_CONTEXT_FACTORY + Microsoft.VisualStudio.LanguageServices.Razor.ruleset @@ -16,6 +18,7 @@ + @@ -30,9 +33,16 @@ + + + + + + + diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.ruleset b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.ruleset new file mode 100644 index 0000000000..3f14803c35 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.ruleset @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs index 68dfc4312a..97978c18cd 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs @@ -7,15 +7,22 @@ using System.Collections.Immutable; using System.ComponentModel.Composition; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.VisualStudio; using Microsoft.VisualStudio.LanguageServices; using Microsoft.VisualStudio.ProjectSystem; using Microsoft.VisualStudio.ProjectSystem.Properties; +using Microsoft.VisualStudio.TextManager.Interop; using Item = System.Collections.Generic.KeyValuePair>; +#if WORKSPACE_PROJECT_CONTEXT_FACTORY +using IWorkspaceProjectContextFactory = Microsoft.VisualStudio.LanguageServices.ProjectSystem.IWorkspaceProjectContextFactory2; +#endif + namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { // Somewhat similar to https://github.com/dotnet/project-system/blob/fa074d228dcff6dae9e48ce43dd4a3a5aa22e8f0/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs @@ -23,16 +30,18 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // This class is responsible for intializing the Razor ProjectSnapshotManager for cases where // MSBuild provides configuration support (>= 2.1). [AppliesTo("DotNetCoreRazor & DotNetCoreRazorConfiguration")] + [ExportVsProfferedProjectService(typeof(IVsContainedLanguageProjectNameProvider))] [Export(ExportContractNames.Scopes.UnconfiguredProject, typeof(IProjectDynamicLoadComponent))] - internal class DefaultRazorProjectHost : RazorProjectHostBase + internal class DefaultRazorProjectHost : RazorProjectHostBase, IVsContainedLanguageProjectNameProvider { private IDisposable _subscription; [ImportingConstructor] public DefaultRazorProjectHost( IUnconfiguredProjectCommonServices commonServices, - [Import(typeof(VisualStudioWorkspace))] Workspace workspace) - : base(commonServices, workspace) + [Import(typeof(VisualStudioWorkspace))] Workspace workspace, + Lazy projectContextFactory) + : base(commonServices, workspace, projectContextFactory) { } @@ -68,6 +77,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem Rules.RazorConfiguration.SchemaName, Rules.RazorExtension.SchemaName, Rules.RazorGenerateWithTargetPath.SchemaName, + ManagedProjectSystemSchema.CompilerCommandLineArgs.SchemaName, + ManagedProjectSystemSchema.ConfigurationGeneral.SchemaName, + ManagedProjectSystemSchema.ResolvedCompilationReference.SchemaName, }); } @@ -107,9 +119,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem var documents = GetCurrentDocuments(update.Value); var changedDocuments = GetChangedAndRemovedDocuments(update.Value); + var references = GetReferences(update.Value); + TryGetCommandLineOptions(update.Value.CurrentState, out var commandLineOptions); + await UpdateAsync(() => { UpdateProjectUnsafe(hostProject); + UpdateWorkspaceProjectOptionsUnsafe(commandLineOptions); + UpdateWorkspaceProjectReferencesUnsafe(references); for (var i = 0; i < changedDocuments.Length; i++) { @@ -302,6 +319,77 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return true; } + + // This is temporary code for initializing the companion project. We expect + // this to be provided by the Managed Project System in the near future. + internal static bool TryGetReferences( + IImmutableDictionary state, + out string[] references) + { + if (!state.TryGetValue(ManagedProjectSystemSchema.ResolvedCompilationReference.ItemName, out var rule)) + { + references = null; + return false; + } + + var items = rule.Items; + var referencesList = new List(); + foreach (var item in items) + { + var reference = item.Key; + if (!referencesList.Contains(reference, FilePathComparer.Instance)) + { + referencesList.Add(reference); + } + } + + references = referencesList.ToArray(); + return true; + } + + // This is temporary code for initializing the companion project. We expect + // this to be provided by the Managed Project System in the near future. + internal static bool TryGetCommandLineOptions( + IImmutableDictionary state, + out string commandLineOptions) + { + if (!state.TryGetValue(ManagedProjectSystemSchema.CompilerCommandLineArgs.ItemName, out var rule)) + { + commandLineOptions = null; + return false; + } + + commandLineOptions = string.Join(" ", rule.Items.Select(kvp => kvp.Key)); + return true; + } + + // This is temporary code for initializing the companion project. We expect + // this to be provided by the Managed Project System in the near future. + internal static bool TryGetTargetPath( + IImmutableDictionary state, + out string targetPath) + { + if (!state.TryGetValue(ManagedProjectSystemSchema.ConfigurationGeneral.SchemaName, out var rule)) + { + targetPath = null; + return false; + } + + if (!rule.Properties.TryGetValue(ManagedProjectSystemSchema.ConfigurationGeneral.TargetPathPropertyName, out targetPath)) + { + targetPath = null; + return false; + } + + if (string.IsNullOrEmpty(targetPath)) + { + targetPath = null; + return false; + } + + return true; + } + private HostDocument[] GetCurrentDocuments(IProjectSubscriptionUpdate update) { if (!update.CurrentState.TryGetValue(Rules.RazorGenerateWithTargetPath.SchemaName, out var rule)) @@ -348,5 +436,37 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return documents.ToArray(); } + + // This is temporary code for initializing the companion project. We expect + // this to be provided by the Managed Project System in the near future. + private string[] GetReferences(IProjectSubscriptionUpdate update) + { + if (!TryGetReferences(update.CurrentState, out var references)) + { + return Array.Empty(); + } + + if (TryGetTargetPath(update.CurrentState, out var targetPath)) + { + references = references.Concat(new[] { targetPath, }).ToArray(); + } + + return references; + } + + // This is temporary code for initializing the companion project. We expect + // this to be provided by the Managed Project System in the near future. + public int GetProjectName([In] uint itemid, [MarshalAs(UnmanagedType.BStr)] out string pbstrProjectName) + { + if (Current == null) + { + pbstrProjectName = null; + + return VSConstants.E_INVALIDARG; + } + + pbstrProjectName = Path.GetFileNameWithoutExtension(Current.FilePath) + " (Razor)"; + return VSConstants.S_OK; + } } } \ No newline at end of file diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultWorkspaceProjectContextFactory.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultWorkspaceProjectContextFactory.cs new file mode 100644 index 0000000000..0842ecc8fc --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultWorkspaceProjectContextFactory.cs @@ -0,0 +1,101 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +// Temporary code until we get access to these APIs +#if WORKSPACE_PROJECT_CONTEXT_FACTORY + +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList; +using IWorkspaceProjectContextFactory = Microsoft.VisualStudio.LanguageServices.ProjectSystem.IWorkspaceProjectContextFactory2; + +namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem +{ + [Export(typeof(IWorkspaceProjectContextFactory))] + internal class DefaultWorkspaceProjectContextFactory : IWorkspaceProjectContextFactory + { + public IWorkspaceProjectContext CreateProjectContext(string languageName, string projectDisplayName, string projectFilePath, Guid projectGuid, object hierarchy, string binOutputPath) + { + return new WorkspaceProjectContext(); + } + + public IWorkspaceProjectContext CreateProjectContext(string languageName, string projectDisplayName, string projectFilePath, Guid projectGuid, object hierarchy, string binOutputPath, ProjectExternalErrorReporter errorReporter) + { + return new WorkspaceProjectContext(); + } + + private class WorkspaceProjectContext : IWorkspaceProjectContext + { + public string DisplayName { get; set; } + public string ProjectFilePath { get; set; } + public Guid Guid { get; set; } + public bool LastDesignTimeBuildSucceeded { get; set; } + public string BinOutputPath { get; set; } + + public void AddAdditionalFile(string filePath, bool isInCurrentContext = true) + { + } + + public void AddAnalyzerReference(string referencePath) + { + } + + public void AddMetadataReference(string referencePath, MetadataReferenceProperties properties) + { + } + + public void AddProjectReference(IWorkspaceProjectContext project, MetadataReferenceProperties properties) + { + } + + public void AddSourceFile(string filePath, bool isInCurrentContext, IEnumerable folderNames, SourceCodeKind sourceCodeKind) + { + } + + public void AddDynamicSourceFile(string filePath, IEnumerable folderNames = null) + { + } + + public void Dispose() + { + } + + public void RemoveAdditionalFile(string filePath) + { + } + + public void RemoveAnalyzerReference(string referencePath) + { + } + + public void RemoveMetadataReference(string referencePath) + { + } + + public void RemoveProjectReference(IWorkspaceProjectContext project) + { + } + + public void RemoveSourceFile(string filePath) + { + } + + public void RemoveDynamicSourceFile(string filePath) + { + + } + + public void SetOptions(string commandLineForOptions) + { + } + + public void SetRuleSetFile(string filePath) + { + } + } + } +} + +#endif diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs index 5dedf67af9..ef33b79723 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs @@ -38,7 +38,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public FallbackRazorProjectHost( IUnconfiguredProjectCommonServices commonServices, [Import(typeof(VisualStudioWorkspace))] Workspace workspace) - : base(commonServices, workspace) + : base(commonServices, workspace, projectContextFactory: null) { } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/GeneratedDocumentTextLoader.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/GeneratedDocumentTextLoader.cs new file mode 100644 index 0000000000..3aa5124572 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/GeneratedDocumentTextLoader.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class GeneratedDocumentTextLoader : TextLoader + { + private readonly DocumentSnapshot _document; + private readonly string _filePath; + private readonly VersionStamp _version; + + public GeneratedDocumentTextLoader(DocumentSnapshot document, string filePath) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + _document = document; + _filePath = filePath; + _version = VersionStamp.Create(); + } + + public override async Task LoadTextAndVersionAsync(Workspace workspace, DocumentId documentId, CancellationToken cancellationToken) + { + var output = await _document.GetGeneratedOutputAsync().ConfigureAwait(false); + return TextAndVersion.Create(SourceText.From(output.GetCSharpDocument().GeneratedCode), _version, _filePath); + } + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IWorkspaceProjectContext.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IWorkspaceProjectContext.cs new file mode 100644 index 0000000000..91d7f2e90e --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IWorkspaceProjectContext.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +// Temporary code until we get access to these APIs +#if WORKSPACE_PROJECT_CONTEXT_FACTORY + +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem +{ + internal interface IWorkspaceProjectContext : IDisposable + { + // Project properties. + string DisplayName { get; set; } + string ProjectFilePath { get; set; } + Guid Guid { get; set; } + bool LastDesignTimeBuildSucceeded { get; set; } + string BinOutputPath { get; set; } + + // Options. + void SetOptions(string commandLineForOptions); + + // References. + void AddMetadataReference(string referencePath, MetadataReferenceProperties properties); + void RemoveMetadataReference(string referencePath); + void AddProjectReference(IWorkspaceProjectContext project, MetadataReferenceProperties properties); + void RemoveProjectReference(IWorkspaceProjectContext project); + void AddAnalyzerReference(string referencePath); + void RemoveAnalyzerReference(string referencePath); + + // Files. + void AddSourceFile(string filePath, bool isInCurrentContext, IEnumerable folderNames, SourceCodeKind sourceCodeKind); + void AddDynamicSourceFile(string filePath, IEnumerable folderNames = null); + void RemoveSourceFile(string filePath); + void RemoveDynamicSourceFile(string filePath); + void AddAdditionalFile(string filePath, bool isInCurrentContext = true); + void RemoveAdditionalFile(string filePath); + void SetRuleSetFile(string filePath); + } +} + +#endif diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IWorkspaceProjectContextFactory.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IWorkspaceProjectContextFactory.cs new file mode 100644 index 0000000000..0cb912b627 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IWorkspaceProjectContextFactory.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +// Temporary code until we get access to these APIs +#if WORKSPACE_PROJECT_CONTEXT_FACTORY + +using System; +using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList; + +namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem +{ + // The 2 is needed here to prevent clashes with the real interface. MEF is based on the FullName + // as a string. + internal interface IWorkspaceProjectContextFactory2 + { + IWorkspaceProjectContext CreateProjectContext(string languageName, string projectDisplayName, string projectFilePath, Guid projectGuid, object hierarchy, string binOutputPath); + + + IWorkspaceProjectContext CreateProjectContext(string languageName, string projectDisplayName, string projectFilePath, Guid projectGuid, object hierarchy, string binOutputPath, ProjectExternalErrorReporter errorReporter); + } +} + +#endif \ No newline at end of file diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ManagedProjectSystemSchema.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ManagedProjectSystemSchema.cs index a56464e495..9f67217eb6 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ManagedProjectSystemSchema.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ManagedProjectSystemSchema.cs @@ -6,6 +6,20 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Well-Known Schema and property names defined by the ManagedProjectSystem internal static class ManagedProjectSystemSchema { + public static class CompilerCommandLineArgs + { + public static readonly string SchemaName = "CompilerCommandLineArgs"; + + public static readonly string ItemName = "CompilerCommandLineArgs"; + } + + public static class ConfigurationGeneral + { + public static readonly string SchemaName = "ConfigurationGeneral"; + + public static readonly string TargetPathPropertyName = "TargetPath"; + } + public static class ResolvedCompilationReference { public static readonly string SchemaName = "ResolvedCompilationReference"; diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ProjectExternalErrorReporter.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ProjectExternalErrorReporter.cs new file mode 100644 index 0000000000..d9c230f65d --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ProjectExternalErrorReporter.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +// Temporary code until we get access to these APIs +#if WORKSPACE_PROJECT_CONTEXT_FACTORY + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.TaskList +{ + internal class ProjectExternalErrorReporter + { + } +} + +#endif \ No newline at end of file diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs index e39a866f05..6d5700a796 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs @@ -5,27 +5,40 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Diagnostics; +using System.IO; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.VisualStudio.Composition; using Microsoft.VisualStudio.LanguageServices; +using Microsoft.VisualStudio.LanguageServices.ProjectSystem; using Microsoft.VisualStudio.ProjectSystem; using Microsoft.VisualStudio.Threading; +#if WORKSPACE_PROJECT_CONTEXT_FACTORY +using IWorkspaceProjectContextFactory = Microsoft.VisualStudio.LanguageServices.ProjectSystem.IWorkspaceProjectContextFactory2; +#endif + namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal abstract class RazorProjectHostBase : OnceInitializedOnceDisposedAsync, IProjectDynamicLoadComponent { private readonly Workspace _workspace; + private readonly Lazy _projectContextFactory; private readonly AsyncSemaphore _lock; private ProjectSnapshotManagerBase _projectManager; private HostProject _current; + private IWorkspaceProjectContext _projectContext; private Dictionary _currentDocuments; + private HashSet _references; + private string _commandLineOptions; public RazorProjectHostBase( IUnconfiguredProjectCommonServices commonServices, - [Import(typeof(VisualStudioWorkspace))] Workspace workspace) + [Import(typeof(VisualStudioWorkspace))] Workspace workspace, + Lazy projectContextFactory) : base(commonServices.ThreadingService.JoinableTaskContext) { if (commonServices == null) @@ -40,9 +53,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem CommonServices = commonServices; _workspace = workspace; + _projectContextFactory = projectContextFactory; _lock = new AsyncSemaphore(initialCount: 1); _currentDocuments = new Dictionary(FilePathComparer.Instance); + _references = new HashSet(FilePathComparer.Instance); } // Internal for testing @@ -73,6 +88,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem _lock = new AsyncSemaphore(initialCount: 1); _currentDocuments = new Dictionary(FilePathComparer.Instance); + _references = new HashSet(FilePathComparer.Instance); } protected HostProject Current => _current; @@ -129,6 +145,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { var filePath = CommonServices.UnconfiguredProject.FullPath; UpdateProjectUnsafe(new HostProject(filePath, old.Configuration)); + UpdateWorkspaceProjectOptionsUnsafe(_commandLineOptions); + UpdateWorkspaceProjectReferencesUnsafe(_references.ToArray()); // This should no-op in the common case, just putting it here for insurance. for (var i = 0; i < oldDocuments.Length; i++) @@ -153,6 +171,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return _projectManager; } + private IWorkspaceProjectContextFactory GetProjectContextFactory() + { + CommonServices.ThreadingService.VerifyOnUIThread(); + return _projectContextFactory?.Value; + } + protected async Task UpdateAsync(Action action) { await CommonServices.ThreadingService.SwitchToUIThread(); @@ -174,12 +198,40 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem } else if (_current == null && project != null) { + // This is temporary code for initializing the companion project. We expect + // this to be provided by the Managed Project System in the near future. + var projectContextFactory = GetProjectContextFactory(); + if (projectContextFactory != null) + { + var assembly = Assembly.Load("Microsoft.VisualStudio.ProjectSystem.Managed, Version=2.7.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"); + var type = assembly.GetType("Microsoft.VisualStudio.ProjectSystem.LanguageServices.IProjectHostProvider"); + + var exportProviderType = CommonServices.UnconfiguredProject.Services.ExportProvider.GetType(); + var method = exportProviderType.GetMethod(nameof(ExportProvider.GetExportedValue), Array.Empty()).MakeGenericMethod(type); + var export = method.Invoke(CommonServices.UnconfiguredProject.Services.ExportProvider, Array.Empty()); + var host = new IProjectHostProvider(export); + + var displayName = Path.GetFileNameWithoutExtension(CommonServices.UnconfiguredProject.FullPath) + " (Razor)"; + _projectContext = projectContextFactory.CreateProjectContext( + LanguageNames.CSharp, + displayName, + CommonServices.UnconfiguredProject.FullPath, + Guid.NewGuid(), + host.UnconfiguredProjectHostObject.ActiveIntellisenseProjectHostObject, + null, + null); + } + + // END temporary code + projectManager.HostProjectAdded(project); } else if (_current != null && project == null) { Debug.Assert(_currentDocuments.Count == 0); projectManager.HostProjectRemoved(_current); + _projectContext?.Dispose(); + _projectContext = null; } else { @@ -189,6 +241,56 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem _current = project; } + protected void UpdateWorkspaceProjectOptionsUnsafe(string commandLineOptions) + { + if (_projectContext == null) + { + _commandLineOptions = null; + return; + } + + if (!string.Equals(_commandLineOptions, commandLineOptions)) + { + _projectContext.SetOptions(commandLineOptions); + _commandLineOptions = commandLineOptions; + } + } + + protected void UpdateWorkspaceProjectReferencesUnsafe(string[] references) + { + if (_projectContext == null) + { + _references.Clear(); + return; + } + + var newer = new HashSet(references, FilePathComparer.Instance); + var older = new HashSet(_references, FilePathComparer.Instance); + + if (older.SetEquals(newer)) + { + return; + } + + var remove = new HashSet(older, FilePathComparer.Instance); + remove.ExceptWith(newer); + + var add = new HashSet(newer, FilePathComparer.Instance); + add.ExceptWith(older); + + foreach (var reference in remove) + { + _references.Remove(reference); + _projectContext.RemoveMetadataReference(reference); + } + + foreach (var reference in add) + { + _references.Add(reference); + _projectContext.AddMetadataReference(reference, new MetadataReferenceProperties()); + } + } + protected void AddDocumentUnsafe(HostDocument document) { var projectManager = GetProjectManager(); @@ -200,6 +302,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem } projectManager.DocumentAdded(_current, document, new FileTextLoader(document.FilePath, null)); + _projectContext?.AddDynamicSourceFile(document.FilePath, GetFolders(document)); _currentDocuments.Add(document.FilePath, document); } @@ -207,6 +310,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { var projectManager = GetProjectManager(); + _projectContext?.RemoveDynamicSourceFile(document.FilePath); projectManager.DocumentRemoved(_current, document); _currentDocuments.Remove(document.FilePath); } @@ -217,6 +321,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem foreach (var kvp in _currentDocuments) { + _projectContext?.RemoveSourceFile(kvp.Value.FilePath); _projectManager.DocumentRemoved(_current, kvp.Value); } @@ -249,5 +354,48 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { await OnProjectRenamingAsync().ConfigureAwait(false); } + + private static IEnumerable GetFolders(HostDocument document) + { + var split = document.TargetPath.Split('/'); + return split.Take(split.Length - 1); + } + + private class IUnconfiguredProjectHostObject + { + private readonly object _inner; + + public IUnconfiguredProjectHostObject(object inner) + { + _inner = inner; + } + + public object ActiveIntellisenseProjectHostObject + { + get + { + return _inner.GetType().GetProperty(nameof(ActiveIntellisenseProjectHostObject)).GetValue(_inner); + } + } + } + + private class IProjectHostProvider + { + private readonly object _inner; + + public IProjectHostProvider(object inner) + { + _inner = inner; + } + + public IUnconfiguredProjectHostObject UnconfiguredProjectHostObject + { + get + { + var inner = _inner.GetType().GetProperty(nameof(UnconfiguredProjectHostObject)).GetValue(_inner); + return new IUnconfiguredProjectHostObject(inner); + } + } + } } } \ No newline at end of file diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorConfiguration.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorConfiguration.cs index d8c2484f5b..a21bc050c9 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorConfiguration.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorConfiguration.cs @@ -8,205 +8,268 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules { - - - internal partial class RazorConfiguration { - - /// Backing field for deserialized rule.. - private static Microsoft.Build.Framework.XamlTypes.Rule deserializedFallbackRule; - - /// The name of the schema to look for at runtime to fulfill property access. - internal const string SchemaName = "RazorConfiguration"; - - /// The ItemType given in the Rule.DataSource property. May not apply to every Property's individual DataSource. - internal const string PrimaryDataSourceItemType = "RazorConfiguration"; - - /// The Label given in the Rule.DataSource property. May not apply to every Property's individual DataSource. - internal const string PrimaryDataSourceLabel = ""; - - /// Razor Extensions (The "Extensions" property). - internal const string ExtensionsProperty = "Extensions"; - - /// Backing field for the property. - private Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule; - - /// Backing field for the file name of the rule property. - private string file; - - /// Backing field for the ItemType property. - private string itemType; - - /// Backing field for the ItemName property. - private string itemName; - - /// Configured Project - private Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject; - - /// The dictionary of named catalogs. - private System.Collections.Immutable.IImmutableDictionary catalogs; - - /// Backing field for the property. - private Microsoft.VisualStudio.ProjectSystem.Properties.IRule fallbackRule; - - /// Thread locking object - private object locker = new object(); - - /// Initializes a new instance of the RazorConfiguration class. - internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule) { - this.rule = rule; - } - - /// Initializes a new instance of the RazorConfiguration class. - internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, string file, string itemType, string itemName) : - this(GetRule(System.Collections.Immutable.ImmutableDictionary.GetValueOrDefault(catalogs, context), file, itemType, itemName)) { - if ((configuredProject == null)) { - throw new System.ArgumentNullException("configuredProject"); - } - this.configuredProject = configuredProject; - this.catalogs = catalogs; - this.file = file; - this.itemType = itemType; - this.itemName = itemName; - } - - /// Initializes a new instance of the RazorConfiguration class. - internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject) : - this(rule) { - if ((rule == null)) { - throw new System.ArgumentNullException("rule"); - } - if ((configuredProject == null)) { - throw new System.ArgumentNullException("configuredProject"); - } - this.configuredProject = configuredProject; - this.rule = rule; - this.file = this.rule.File; - this.itemType = this.rule.ItemType; - this.itemName = this.rule.ItemName; - } - - /// Initializes a new instance of the RazorConfiguration class. - internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertyContext) : - this(configuredProject, catalogs, context, GetContextFile(propertyContext), propertyContext.ItemType, propertyContext.ItemName) { - } - - /// Initializes a new instance of the RazorConfiguration class that assumes a project context (neither property sheet nor items). - internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs) : - this(configuredProject, catalogs, "Project", null, null, null) { - } - - /// Gets the IRule used to get and set properties. - public Microsoft.VisualStudio.ProjectSystem.Properties.IRule Rule { - get { - return this.rule; - } - } - - /// Razor Extensions - internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty Extensions { - get { - Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; - if ((localRule == null)) { - localRule = this.GeneratedFallbackRule; - } - if ((localRule == null)) { - return null; - } - Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(ExtensionsProperty))); - if (((property == null) - && (this.GeneratedFallbackRule != null))) { - localRule = this.GeneratedFallbackRule; - property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(ExtensionsProperty))); - } - return property; - } - } - - /// Get the fallback rule if the current rule on disk is missing or a property in the rule on disk is missing - private Microsoft.VisualStudio.ProjectSystem.Properties.IRule GeneratedFallbackRule { - get { - if (((this.fallbackRule == null) - && (this.configuredProject != null))) { - System.Threading.Monitor.Enter(this.locker); - try { - if ((this.fallbackRule == null)) { - this.InitializeFallbackRule(); - } - } - finally { - System.Threading.Monitor.Exit(this.locker); - } - } - return this.fallbackRule; - } - } - - private static Microsoft.VisualStudio.ProjectSystem.Properties.IRule GetRule(Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog, string file, string itemType, string itemName) { - if ((catalog == null)) { - return null; - } - return catalog.BindToContext(SchemaName, file, itemType, itemName); - } - - private static string GetContextFile(Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertiesContext) { - if ((propertiesContext.IsProjectFile == true)) { - return null; - } - else { - return propertiesContext.File; - } - } - - private void InitializeFallbackRule() { - if ((this.configuredProject == null)) { - return; - } - Microsoft.Build.Framework.XamlTypes.Rule unboundRule = RazorConfiguration.deserializedFallbackRule; - if ((unboundRule == null)) { - System.IO.Stream xamlStream = null; - System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly(); - try { - xamlStream = thisAssembly.GetManifestResourceStream("XamlRuleToCode:RazorConfiguration.xaml"); - Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode root = ((Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode)(System.Xaml.XamlServices.Load(xamlStream))); - System.Collections.Generic.IEnumerator ruleEnumerator = root.GetSchemaObjects(typeof(Microsoft.Build.Framework.XamlTypes.Rule)).GetEnumerator(); - for ( - ; ((unboundRule == null) - && ruleEnumerator.MoveNext()); - ) { - Microsoft.Build.Framework.XamlTypes.Rule t = ((Microsoft.Build.Framework.XamlTypes.Rule)(ruleEnumerator.Current)); - if (System.StringComparer.OrdinalIgnoreCase.Equals(t.Name, SchemaName)) { - unboundRule = t; - unboundRule.Name = "30e71838-2cb8-4c67-ab28-7670763124af"; - RazorConfiguration.deserializedFallbackRule = unboundRule; - } - } - } - finally { - if ((xamlStream != null)) { - ((System.IDisposable)(xamlStream)).Dispose(); - } - } - } - this.configuredProject.Services.AdditionalRuleDefinitions.AddRuleDefinition(unboundRule, "FallbackRuleCodeGenerationContext"); - Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog = this.configuredProject.Services.PropertyPagesCatalog.GetMemoryOnlyCatalog("FallbackRuleCodeGenerationContext"); - this.fallbackRule = catalog.BindToContext(unboundRule.Name, this.file, this.itemType, this.itemName); - } - } - - internal partial class RazorProjectProperties { - - private static System.Func>, object, RazorConfiguration> CreateRazorConfigurationPropertiesDelegate = new System.Func>, object, RazorConfiguration>(CreateRazorConfigurationProperties); - - private static RazorConfiguration CreateRazorConfigurationProperties(System.Threading.Tasks.Task> namedCatalogs, object state) { - RazorProjectProperties that = ((RazorProjectProperties)(state)); - return new RazorConfiguration(that.ConfiguredProject, namedCatalogs.Result, "Project", that.File, that.ItemType, that.ItemName); - } - - /// Gets the strongly-typed property accessor used to get and set Configuration Properties properties. - internal System.Threading.Tasks.Task GetRazorConfigurationPropertiesAsync() { - System.Threading.Tasks.Task> namedCatalogsTask = this.GetNamedCatalogsAsync(); - return namedCatalogsTask.ContinueWith(CreateRazorConfigurationPropertiesDelegate, this, System.Threading.CancellationToken.None, System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously, System.Threading.Tasks.TaskScheduler.Default); - } - } +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules +{ + + + internal partial class RazorConfiguration + { + + /// Backing field for deserialized rule.. + private static Microsoft.Build.Framework.XamlTypes.Rule deserializedFallbackRule; + + /// The name of the schema to look for at runtime to fulfill property access. + internal const string SchemaName = "RazorConfiguration"; + + /// The ItemType given in the Rule.DataSource property. May not apply to every Property's individual DataSource. + internal const string PrimaryDataSourceItemType = "RazorConfiguration"; + + /// The Label given in the Rule.DataSource property. May not apply to every Property's individual DataSource. + internal const string PrimaryDataSourceLabel = ""; + + /// Razor Extensions (The "Extensions" property). + internal const string ExtensionsProperty = "Extensions"; + + /// Backing field for the property. + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule; + + /// Backing field for the file name of the rule property. + private string file; + + /// Backing field for the ItemType property. + private string itemType; + + /// Backing field for the ItemName property. + private string itemName; + + /// Configured Project + private Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject; + + /// The dictionary of named catalogs. + private System.Collections.Immutable.IImmutableDictionary catalogs; + + /// Backing field for the property. + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule fallbackRule; + + /// Thread locking object + private object locker = new object(); + + /// Initializes a new instance of the RazorConfiguration class. + internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule) + { + this.rule = rule; + } + + /// Initializes a new instance of the RazorConfiguration class. + internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, string file, string itemType, string itemName) : + this(GetRule(System.Collections.Immutable.ImmutableDictionary.GetValueOrDefault(catalogs, context), file, itemType, itemName)) + { + if ((configuredProject == null)) + { + throw new System.ArgumentNullException("configuredProject"); + } + this.configuredProject = configuredProject; + this.catalogs = catalogs; + this.file = file; + this.itemType = itemType; + this.itemName = itemName; + } + + /// Initializes a new instance of the RazorConfiguration class. + internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject) : + this(rule) + { + if ((rule == null)) + { + throw new System.ArgumentNullException("rule"); + } + if ((configuredProject == null)) + { + throw new System.ArgumentNullException("configuredProject"); + } + this.configuredProject = configuredProject; + this.rule = rule; + this.file = this.rule.File; + this.itemType = this.rule.ItemType; + this.itemName = this.rule.ItemName; + } + + /// Initializes a new instance of the RazorConfiguration class. + internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertyContext) : + this(configuredProject, catalogs, context, GetContextFile(propertyContext), propertyContext.ItemType, propertyContext.ItemName) + { + } + + /// Initializes a new instance of the RazorConfiguration class that assumes a project context (neither property sheet nor items). + internal RazorConfiguration(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs) : + this(configuredProject, catalogs, "Project", null, null, null) + { + } + + /// Gets the IRule used to get and set properties. + public Microsoft.VisualStudio.ProjectSystem.Properties.IRule Rule + { + get + { + return this.rule; + } + } + + /// Razor Extensions + internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty Extensions + { + get + { + Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; + if ((localRule == null)) + { + localRule = this.GeneratedFallbackRule; + } + if ((localRule == null)) + { + return null; + } + Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(ExtensionsProperty))); + if (((property == null) + && (this.GeneratedFallbackRule != null))) + { + localRule = this.GeneratedFallbackRule; + property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(ExtensionsProperty))); + } + return property; + } + } + + /// Get the fallback rule if the current rule on disk is missing or a property in the rule on disk is missing + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule GeneratedFallbackRule + { + get + { + if (((this.fallbackRule == null) + && (this.configuredProject != null))) + { + System.Threading.Monitor.Enter(this.locker); + try + { + if ((this.fallbackRule == null)) + { + this.InitializeFallbackRule(); + } + } + finally + { + System.Threading.Monitor.Exit(this.locker); + } + } + return this.fallbackRule; + } + } + + private static Microsoft.VisualStudio.ProjectSystem.Properties.IRule GetRule(Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog, string file, string itemType, string itemName) + { + if ((catalog == null)) + { + return null; + } + return catalog.BindToContext(SchemaName, file, itemType, itemName); + } + + private static string GetContextFile(Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertiesContext) + { + if ((propertiesContext.IsProjectFile == true)) + { + return null; + } + else + { + return propertiesContext.File; + } + } + + private void InitializeFallbackRule() + { + if ((this.configuredProject == null)) + { + return; + } + Microsoft.Build.Framework.XamlTypes.Rule unboundRule = RazorConfiguration.deserializedFallbackRule; + if ((unboundRule == null)) + { + System.IO.Stream xamlStream = null; + System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly(); + try + { + xamlStream = thisAssembly.GetManifestResourceStream("XamlRuleToCode:RazorConfiguration.xaml"); + Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode root = ((Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode)(System.Xaml.XamlServices.Load(xamlStream))); + System.Collections.Generic.IEnumerator ruleEnumerator = root.GetSchemaObjects(typeof(Microsoft.Build.Framework.XamlTypes.Rule)).GetEnumerator(); + for ( + ; ((unboundRule == null) + && ruleEnumerator.MoveNext()); + ) + { + Microsoft.Build.Framework.XamlTypes.Rule t = ((Microsoft.Build.Framework.XamlTypes.Rule)(ruleEnumerator.Current)); + if (System.StringComparer.OrdinalIgnoreCase.Equals(t.Name, SchemaName)) + { + unboundRule = t; + unboundRule.Name = "f269edfbdecc0079d5f539c22552711e194f39b2367903515ae5440a64b20a0f"; + RazorConfiguration.deserializedFallbackRule = unboundRule; + } + } + } + finally + { + if ((xamlStream != null)) + { + ((System.IDisposable)(xamlStream)).Dispose(); + } + } + } + this.configuredProject.Services.AdditionalRuleDefinitions.AddRuleDefinition(unboundRule, "FallbackRuleCodeGenerationContext"); + Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog = this.configuredProject.Services.PropertyPagesCatalog.GetMemoryOnlyCatalog("FallbackRuleCodeGenerationContext"); + this.fallbackRule = catalog.BindToContext(unboundRule.Name, this.file, this.itemType, this.itemName); + } + } + + internal partial class RazorProjectProperties + { + + private static System.Func>, object, RazorConfiguration> CreateRazorConfigurationPropertiesDelegate = new System.Func>, object, RazorConfiguration>(CreateRazorConfigurationProperties); + + private static RazorConfiguration CreateRazorConfigurationProperties(System.Threading.Tasks.Task> namedCatalogs, object state) + { + RazorProjectProperties that = ((RazorProjectProperties)(state)); + return new RazorConfiguration(that.ConfiguredProject, namedCatalogs.Result, "Project", that.File, that.ItemType, that.ItemName); + } + + /// Gets the strongly-typed property accessor used to get and set Configuration Properties properties. + internal System.Threading.Tasks.Task GetRazorConfigurationPropertiesAsync() + { + System.Threading.Tasks.Task> namedCatalogsTask = this.GetNamedCatalogsAsync(); + return namedCatalogsTask.ContinueWith(CreateRazorConfigurationPropertiesDelegate, this, System.Threading.CancellationToken.None, System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously, System.Threading.Tasks.TaskScheduler.Default); + } + + /// Gets the strongly-typed property accessor used to get value from the current project snapshot Configuration Properties properties. + internal bool TryGetCurrentRazorConfigurationPropertiesSnapshot(out RazorConfiguration snapshot, [System.Runtime.InteropServices.OptionalAttribute()] [System.Runtime.InteropServices.DefaultParameterValueAttribute(true)] bool requiredToMatchProjectVersion) + { + snapshot = null; + Microsoft.VisualStudio.ProjectSystem.IProjectVersionedValue catalogSnapshot; + if (this.TryGetCurrentCatalogSnapshot(out catalogSnapshot)) + { + if (requiredToMatchProjectVersion) + { + if ((this.ConfiguredProject.ProjectVersion.CompareTo(catalogSnapshot.DataSourceVersions[Microsoft.VisualStudio.ProjectSystem.ProjectDataSources.ConfiguredProjectVersion]) != 0)) + { + return false; + } + } + Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule = this.GetSnapshotRule(catalogSnapshot.Value, "Project", RazorConfiguration.SchemaName); + if ((rule != null)) + { + snapshot = new RazorConfiguration(rule, this.ConfiguredProject); + return true; + } + } + return false; + } + } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorExtension.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorExtension.cs index 0ce503811b..6000099f24 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorExtension.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorExtension.cs @@ -8,228 +8,296 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules { - - - internal partial class RazorExtension { - - /// Backing field for deserialized rule.. - private static Microsoft.Build.Framework.XamlTypes.Rule deserializedFallbackRule; - - /// The name of the schema to look for at runtime to fulfill property access. - internal const string SchemaName = "RazorExtension"; - - /// The ItemType given in the Rule.DataSource property. May not apply to every Property's individual DataSource. - internal const string PrimaryDataSourceItemType = "RazorExtension"; - - /// The Label given in the Rule.DataSource property. May not apply to every Property's individual DataSource. - internal const string PrimaryDataSourceLabel = ""; - - /// Razor Extension Assembly Name (The "AssemblyName" property). - internal const string AssemblyNameProperty = "AssemblyName"; - - /// Razor Extension Assembly File Path (The "AssemblyFilePath" property). - internal const string AssemblyFilePathProperty = "AssemblyFilePath"; - - /// Backing field for the property. - private Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule; - - /// Backing field for the file name of the rule property. - private string file; - - /// Backing field for the ItemType property. - private string itemType; - - /// Backing field for the ItemName property. - private string itemName; - - /// Configured Project - private Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject; - - /// The dictionary of named catalogs. - private System.Collections.Immutable.IImmutableDictionary catalogs; - - /// Backing field for the property. - private Microsoft.VisualStudio.ProjectSystem.Properties.IRule fallbackRule; - - /// Thread locking object - private object locker = new object(); - - /// Initializes a new instance of the RazorExtension class. - internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule) { - this.rule = rule; - } - - /// Initializes a new instance of the RazorExtension class. - internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, string file, string itemType, string itemName) : - this(GetRule(System.Collections.Immutable.ImmutableDictionary.GetValueOrDefault(catalogs, context), file, itemType, itemName)) { - if ((configuredProject == null)) { - throw new System.ArgumentNullException("configuredProject"); - } - this.configuredProject = configuredProject; - this.catalogs = catalogs; - this.file = file; - this.itemType = itemType; - this.itemName = itemName; - } - - /// Initializes a new instance of the RazorExtension class. - internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject) : - this(rule) { - if ((rule == null)) { - throw new System.ArgumentNullException("rule"); - } - if ((configuredProject == null)) { - throw new System.ArgumentNullException("configuredProject"); - } - this.configuredProject = configuredProject; - this.rule = rule; - this.file = this.rule.File; - this.itemType = this.rule.ItemType; - this.itemName = this.rule.ItemName; - } - - /// Initializes a new instance of the RazorExtension class. - internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertyContext) : - this(configuredProject, catalogs, context, GetContextFile(propertyContext), propertyContext.ItemType, propertyContext.ItemName) { - } - - /// Initializes a new instance of the RazorExtension class that assumes a project context (neither property sheet nor items). - internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs) : - this(configuredProject, catalogs, "Project", null, null, null) { - } - - /// Gets the IRule used to get and set properties. - public Microsoft.VisualStudio.ProjectSystem.Properties.IRule Rule { - get { - return this.rule; - } - } - - /// Razor Extension Assembly Name - internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty AssemblyName { - get { - Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; - if ((localRule == null)) { - localRule = this.GeneratedFallbackRule; - } - if ((localRule == null)) { - return null; - } - Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(AssemblyNameProperty))); - if (((property == null) - && (this.GeneratedFallbackRule != null))) { - localRule = this.GeneratedFallbackRule; - property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(AssemblyNameProperty))); - } - return property; - } - } - - /// Razor Extension Assembly File Path - internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty AssemblyFilePath { - get { - Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; - if ((localRule == null)) { - localRule = this.GeneratedFallbackRule; - } - if ((localRule == null)) { - return null; - } - Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(AssemblyFilePathProperty))); - if (((property == null) - && (this.GeneratedFallbackRule != null))) { - localRule = this.GeneratedFallbackRule; - property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(AssemblyFilePathProperty))); - } - return property; - } - } - - /// Get the fallback rule if the current rule on disk is missing or a property in the rule on disk is missing - private Microsoft.VisualStudio.ProjectSystem.Properties.IRule GeneratedFallbackRule { - get { - if (((this.fallbackRule == null) - && (this.configuredProject != null))) { - System.Threading.Monitor.Enter(this.locker); - try { - if ((this.fallbackRule == null)) { - this.InitializeFallbackRule(); - } - } - finally { - System.Threading.Monitor.Exit(this.locker); - } - } - return this.fallbackRule; - } - } - - private static Microsoft.VisualStudio.ProjectSystem.Properties.IRule GetRule(Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog, string file, string itemType, string itemName) { - if ((catalog == null)) { - return null; - } - return catalog.BindToContext(SchemaName, file, itemType, itemName); - } - - private static string GetContextFile(Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertiesContext) { - if ((propertiesContext.IsProjectFile == true)) { - return null; - } - else { - return propertiesContext.File; - } - } - - private void InitializeFallbackRule() { - if ((this.configuredProject == null)) { - return; - } - Microsoft.Build.Framework.XamlTypes.Rule unboundRule = RazorExtension.deserializedFallbackRule; - if ((unboundRule == null)) { - System.IO.Stream xamlStream = null; - System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly(); - try { - xamlStream = thisAssembly.GetManifestResourceStream("XamlRuleToCode:RazorExtension.xaml"); - Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode root = ((Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode)(System.Xaml.XamlServices.Load(xamlStream))); - System.Collections.Generic.IEnumerator ruleEnumerator = root.GetSchemaObjects(typeof(Microsoft.Build.Framework.XamlTypes.Rule)).GetEnumerator(); - for ( - ; ((unboundRule == null) - && ruleEnumerator.MoveNext()); - ) { - Microsoft.Build.Framework.XamlTypes.Rule t = ((Microsoft.Build.Framework.XamlTypes.Rule)(ruleEnumerator.Current)); - if (System.StringComparer.OrdinalIgnoreCase.Equals(t.Name, SchemaName)) { - unboundRule = t; - unboundRule.Name = "6b577687-703b-41a1-8f5f-c44644f65f2a"; - RazorExtension.deserializedFallbackRule = unboundRule; - } - } - } - finally { - if ((xamlStream != null)) { - ((System.IDisposable)(xamlStream)).Dispose(); - } - } - } - this.configuredProject.Services.AdditionalRuleDefinitions.AddRuleDefinition(unboundRule, "FallbackRuleCodeGenerationContext"); - Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog = this.configuredProject.Services.PropertyPagesCatalog.GetMemoryOnlyCatalog("FallbackRuleCodeGenerationContext"); - this.fallbackRule = catalog.BindToContext(unboundRule.Name, this.file, this.itemType, this.itemName); - } - } - - internal partial class RazorProjectProperties { - - private static System.Func>, object, RazorExtension> CreateRazorExtensionPropertiesDelegate = new System.Func>, object, RazorExtension>(CreateRazorExtensionProperties); - - private static RazorExtension CreateRazorExtensionProperties(System.Threading.Tasks.Task> namedCatalogs, object state) { - RazorProjectProperties that = ((RazorProjectProperties)(state)); - return new RazorExtension(that.ConfiguredProject, namedCatalogs.Result, "Project", that.File, that.ItemType, that.ItemName); - } - - /// Gets the strongly-typed property accessor used to get and set Extension Properties properties. - internal System.Threading.Tasks.Task GetRazorExtensionPropertiesAsync() { - System.Threading.Tasks.Task> namedCatalogsTask = this.GetNamedCatalogsAsync(); - return namedCatalogsTask.ContinueWith(CreateRazorExtensionPropertiesDelegate, this, System.Threading.CancellationToken.None, System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously, System.Threading.Tasks.TaskScheduler.Default); - } - } +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules +{ + + + internal partial class RazorExtension + { + + /// Backing field for deserialized rule.. + private static Microsoft.Build.Framework.XamlTypes.Rule deserializedFallbackRule; + + /// The name of the schema to look for at runtime to fulfill property access. + internal const string SchemaName = "RazorExtension"; + + /// The ItemType given in the Rule.DataSource property. May not apply to every Property's individual DataSource. + internal const string PrimaryDataSourceItemType = "RazorExtension"; + + /// The Label given in the Rule.DataSource property. May not apply to every Property's individual DataSource. + internal const string PrimaryDataSourceLabel = ""; + + /// Razor Extension Assembly Name (The "AssemblyName" property). + internal const string AssemblyNameProperty = "AssemblyName"; + + /// Razor Extension Assembly File Path (The "AssemblyFilePath" property). + internal const string AssemblyFilePathProperty = "AssemblyFilePath"; + + /// Backing field for the property. + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule; + + /// Backing field for the file name of the rule property. + private string file; + + /// Backing field for the ItemType property. + private string itemType; + + /// Backing field for the ItemName property. + private string itemName; + + /// Configured Project + private Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject; + + /// The dictionary of named catalogs. + private System.Collections.Immutable.IImmutableDictionary catalogs; + + /// Backing field for the property. + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule fallbackRule; + + /// Thread locking object + private object locker = new object(); + + /// Initializes a new instance of the RazorExtension class. + internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule) + { + this.rule = rule; + } + + /// Initializes a new instance of the RazorExtension class. + internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, string file, string itemType, string itemName) : + this(GetRule(System.Collections.Immutable.ImmutableDictionary.GetValueOrDefault(catalogs, context), file, itemType, itemName)) + { + if ((configuredProject == null)) + { + throw new System.ArgumentNullException("configuredProject"); + } + this.configuredProject = configuredProject; + this.catalogs = catalogs; + this.file = file; + this.itemType = itemType; + this.itemName = itemName; + } + + /// Initializes a new instance of the RazorExtension class. + internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject) : + this(rule) + { + if ((rule == null)) + { + throw new System.ArgumentNullException("rule"); + } + if ((configuredProject == null)) + { + throw new System.ArgumentNullException("configuredProject"); + } + this.configuredProject = configuredProject; + this.rule = rule; + this.file = this.rule.File; + this.itemType = this.rule.ItemType; + this.itemName = this.rule.ItemName; + } + + /// Initializes a new instance of the RazorExtension class. + internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertyContext) : + this(configuredProject, catalogs, context, GetContextFile(propertyContext), propertyContext.ItemType, propertyContext.ItemName) + { + } + + /// Initializes a new instance of the RazorExtension class that assumes a project context (neither property sheet nor items). + internal RazorExtension(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs) : + this(configuredProject, catalogs, "Project", null, null, null) + { + } + + /// Gets the IRule used to get and set properties. + public Microsoft.VisualStudio.ProjectSystem.Properties.IRule Rule + { + get + { + return this.rule; + } + } + + /// Razor Extension Assembly Name + internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty AssemblyName + { + get + { + Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; + if ((localRule == null)) + { + localRule = this.GeneratedFallbackRule; + } + if ((localRule == null)) + { + return null; + } + Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(AssemblyNameProperty))); + if (((property == null) + && (this.GeneratedFallbackRule != null))) + { + localRule = this.GeneratedFallbackRule; + property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(AssemblyNameProperty))); + } + return property; + } + } + + /// Razor Extension Assembly File Path + internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty AssemblyFilePath + { + get + { + Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; + if ((localRule == null)) + { + localRule = this.GeneratedFallbackRule; + } + if ((localRule == null)) + { + return null; + } + Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(AssemblyFilePathProperty))); + if (((property == null) + && (this.GeneratedFallbackRule != null))) + { + localRule = this.GeneratedFallbackRule; + property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(AssemblyFilePathProperty))); + } + return property; + } + } + + /// Get the fallback rule if the current rule on disk is missing or a property in the rule on disk is missing + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule GeneratedFallbackRule + { + get + { + if (((this.fallbackRule == null) + && (this.configuredProject != null))) + { + System.Threading.Monitor.Enter(this.locker); + try + { + if ((this.fallbackRule == null)) + { + this.InitializeFallbackRule(); + } + } + finally + { + System.Threading.Monitor.Exit(this.locker); + } + } + return this.fallbackRule; + } + } + + private static Microsoft.VisualStudio.ProjectSystem.Properties.IRule GetRule(Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog, string file, string itemType, string itemName) + { + if ((catalog == null)) + { + return null; + } + return catalog.BindToContext(SchemaName, file, itemType, itemName); + } + + private static string GetContextFile(Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertiesContext) + { + if ((propertiesContext.IsProjectFile == true)) + { + return null; + } + else + { + return propertiesContext.File; + } + } + + private void InitializeFallbackRule() + { + if ((this.configuredProject == null)) + { + return; + } + Microsoft.Build.Framework.XamlTypes.Rule unboundRule = RazorExtension.deserializedFallbackRule; + if ((unboundRule == null)) + { + System.IO.Stream xamlStream = null; + System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly(); + try + { + xamlStream = thisAssembly.GetManifestResourceStream("XamlRuleToCode:RazorExtension.xaml"); + Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode root = ((Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode)(System.Xaml.XamlServices.Load(xamlStream))); + System.Collections.Generic.IEnumerator ruleEnumerator = root.GetSchemaObjects(typeof(Microsoft.Build.Framework.XamlTypes.Rule)).GetEnumerator(); + for ( + ; ((unboundRule == null) + && ruleEnumerator.MoveNext()); + ) + { + Microsoft.Build.Framework.XamlTypes.Rule t = ((Microsoft.Build.Framework.XamlTypes.Rule)(ruleEnumerator.Current)); + if (System.StringComparer.OrdinalIgnoreCase.Equals(t.Name, SchemaName)) + { + unboundRule = t; + unboundRule.Name = "e8ad5dd707beca947f7c014baa70b46c1d7a7fd6c8b49a5511ab299d8e1a3c70"; + RazorExtension.deserializedFallbackRule = unboundRule; + } + } + } + finally + { + if ((xamlStream != null)) + { + ((System.IDisposable)(xamlStream)).Dispose(); + } + } + } + this.configuredProject.Services.AdditionalRuleDefinitions.AddRuleDefinition(unboundRule, "FallbackRuleCodeGenerationContext"); + Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog = this.configuredProject.Services.PropertyPagesCatalog.GetMemoryOnlyCatalog("FallbackRuleCodeGenerationContext"); + this.fallbackRule = catalog.BindToContext(unboundRule.Name, this.file, this.itemType, this.itemName); + } + } + + internal partial class RazorProjectProperties + { + + private static System.Func>, object, RazorExtension> CreateRazorExtensionPropertiesDelegate = new System.Func>, object, RazorExtension>(CreateRazorExtensionProperties); + + private static RazorExtension CreateRazorExtensionProperties(System.Threading.Tasks.Task> namedCatalogs, object state) + { + RazorProjectProperties that = ((RazorProjectProperties)(state)); + return new RazorExtension(that.ConfiguredProject, namedCatalogs.Result, "Project", that.File, that.ItemType, that.ItemName); + } + + /// Gets the strongly-typed property accessor used to get and set Extension Properties properties. + internal System.Threading.Tasks.Task GetRazorExtensionPropertiesAsync() + { + System.Threading.Tasks.Task> namedCatalogsTask = this.GetNamedCatalogsAsync(); + return namedCatalogsTask.ContinueWith(CreateRazorExtensionPropertiesDelegate, this, System.Threading.CancellationToken.None, System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously, System.Threading.Tasks.TaskScheduler.Default); + } + + /// Gets the strongly-typed property accessor used to get value from the current project snapshot Extension Properties properties. + internal bool TryGetCurrentRazorExtensionPropertiesSnapshot(out RazorExtension snapshot, [System.Runtime.InteropServices.OptionalAttribute()] [System.Runtime.InteropServices.DefaultParameterValueAttribute(true)] bool requiredToMatchProjectVersion) + { + snapshot = null; + Microsoft.VisualStudio.ProjectSystem.IProjectVersionedValue catalogSnapshot; + if (this.TryGetCurrentCatalogSnapshot(out catalogSnapshot)) + { + if (requiredToMatchProjectVersion) + { + if ((this.ConfiguredProject.ProjectVersion.CompareTo(catalogSnapshot.DataSourceVersions[Microsoft.VisualStudio.ProjectSystem.ProjectDataSources.ConfiguredProjectVersion]) != 0)) + { + return false; + } + } + Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule = this.GetSnapshotRule(catalogSnapshot.Value, "Project", RazorExtension.SchemaName); + if ((rule != null)) + { + snapshot = new RazorExtension(rule, this.ConfiguredProject); + return true; + } + } + return false; + } + } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGeneral.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGeneral.cs index 860fb1a80b..3c85d96d87 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGeneral.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGeneral.cs @@ -8,228 +8,296 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules { - - - internal partial class RazorGeneral { - - /// Backing field for deserialized rule.. - private static Microsoft.Build.Framework.XamlTypes.Rule deserializedFallbackRule; - - /// The name of the schema to look for at runtime to fulfill property access. - internal const string SchemaName = "RazorGeneral"; - - /// The ItemType given in the Rule.DataSource property. May not apply to every Property's individual DataSource. - internal const string PrimaryDataSourceItemType = null; - - /// The Label given in the Rule.DataSource property. May not apply to every Property's individual DataSource. - internal const string PrimaryDataSourceLabel = ""; - - /// Razor Language Version (The "RazorLangVersion" property). - internal const string RazorLangVersionProperty = "RazorLangVersion"; - - /// Razor Configuration Name (The "RazorDefaultConfiguration" property). - internal const string RazorDefaultConfigurationProperty = "RazorDefaultConfiguration"; - - /// Backing field for the property. - private Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule; - - /// Backing field for the file name of the rule property. - private string file; - - /// Backing field for the ItemType property. - private string itemType; - - /// Backing field for the ItemName property. - private string itemName; - - /// Configured Project - private Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject; - - /// The dictionary of named catalogs. - private System.Collections.Immutable.IImmutableDictionary catalogs; - - /// Backing field for the property. - private Microsoft.VisualStudio.ProjectSystem.Properties.IRule fallbackRule; - - /// Thread locking object - private object locker = new object(); - - /// Initializes a new instance of the RazorGeneral class. - internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule) { - this.rule = rule; - } - - /// Initializes a new instance of the RazorGeneral class. - internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, string file, string itemType, string itemName) : - this(GetRule(System.Collections.Immutable.ImmutableDictionary.GetValueOrDefault(catalogs, context), file, itemType, itemName)) { - if ((configuredProject == null)) { - throw new System.ArgumentNullException("configuredProject"); - } - this.configuredProject = configuredProject; - this.catalogs = catalogs; - this.file = file; - this.itemType = itemType; - this.itemName = itemName; - } - - /// Initializes a new instance of the RazorGeneral class. - internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject) : - this(rule) { - if ((rule == null)) { - throw new System.ArgumentNullException("rule"); - } - if ((configuredProject == null)) { - throw new System.ArgumentNullException("configuredProject"); - } - this.configuredProject = configuredProject; - this.rule = rule; - this.file = this.rule.File; - this.itemType = this.rule.ItemType; - this.itemName = this.rule.ItemName; - } - - /// Initializes a new instance of the RazorGeneral class. - internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertyContext) : - this(configuredProject, catalogs, context, GetContextFile(propertyContext), propertyContext.ItemType, propertyContext.ItemName) { - } - - /// Initializes a new instance of the RazorGeneral class that assumes a project context (neither property sheet nor items). - internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs) : - this(configuredProject, catalogs, "Project", null, null, null) { - } - - /// Gets the IRule used to get and set properties. - public Microsoft.VisualStudio.ProjectSystem.Properties.IRule Rule { - get { - return this.rule; - } - } - - /// Razor Language Version - internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty RazorLangVersion { - get { - Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; - if ((localRule == null)) { - localRule = this.GeneratedFallbackRule; - } - if ((localRule == null)) { - return null; - } - Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(RazorLangVersionProperty))); - if (((property == null) - && (this.GeneratedFallbackRule != null))) { - localRule = this.GeneratedFallbackRule; - property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(RazorLangVersionProperty))); - } - return property; - } - } - - /// Razor Configuration Name - internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty RazorDefaultConfiguration { - get { - Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; - if ((localRule == null)) { - localRule = this.GeneratedFallbackRule; - } - if ((localRule == null)) { - return null; - } - Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(RazorDefaultConfigurationProperty))); - if (((property == null) - && (this.GeneratedFallbackRule != null))) { - localRule = this.GeneratedFallbackRule; - property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(RazorDefaultConfigurationProperty))); - } - return property; - } - } - - /// Get the fallback rule if the current rule on disk is missing or a property in the rule on disk is missing - private Microsoft.VisualStudio.ProjectSystem.Properties.IRule GeneratedFallbackRule { - get { - if (((this.fallbackRule == null) - && (this.configuredProject != null))) { - System.Threading.Monitor.Enter(this.locker); - try { - if ((this.fallbackRule == null)) { - this.InitializeFallbackRule(); - } - } - finally { - System.Threading.Monitor.Exit(this.locker); - } - } - return this.fallbackRule; - } - } - - private static Microsoft.VisualStudio.ProjectSystem.Properties.IRule GetRule(Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog, string file, string itemType, string itemName) { - if ((catalog == null)) { - return null; - } - return catalog.BindToContext(SchemaName, file, itemType, itemName); - } - - private static string GetContextFile(Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertiesContext) { - if ((propertiesContext.IsProjectFile == true)) { - return null; - } - else { - return propertiesContext.File; - } - } - - private void InitializeFallbackRule() { - if ((this.configuredProject == null)) { - return; - } - Microsoft.Build.Framework.XamlTypes.Rule unboundRule = RazorGeneral.deserializedFallbackRule; - if ((unboundRule == null)) { - System.IO.Stream xamlStream = null; - System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly(); - try { - xamlStream = thisAssembly.GetManifestResourceStream("XamlRuleToCode:RazorGeneral.xaml"); - Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode root = ((Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode)(System.Xaml.XamlServices.Load(xamlStream))); - System.Collections.Generic.IEnumerator ruleEnumerator = root.GetSchemaObjects(typeof(Microsoft.Build.Framework.XamlTypes.Rule)).GetEnumerator(); - for ( - ; ((unboundRule == null) - && ruleEnumerator.MoveNext()); - ) { - Microsoft.Build.Framework.XamlTypes.Rule t = ((Microsoft.Build.Framework.XamlTypes.Rule)(ruleEnumerator.Current)); - if (System.StringComparer.OrdinalIgnoreCase.Equals(t.Name, SchemaName)) { - unboundRule = t; - unboundRule.Name = "e0400917-b7e9-4731-b3e6-447b229db9d4"; - RazorGeneral.deserializedFallbackRule = unboundRule; - } - } - } - finally { - if ((xamlStream != null)) { - ((System.IDisposable)(xamlStream)).Dispose(); - } - } - } - this.configuredProject.Services.AdditionalRuleDefinitions.AddRuleDefinition(unboundRule, "FallbackRuleCodeGenerationContext"); - Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog = this.configuredProject.Services.PropertyPagesCatalog.GetMemoryOnlyCatalog("FallbackRuleCodeGenerationContext"); - this.fallbackRule = catalog.BindToContext(unboundRule.Name, this.file, this.itemType, this.itemName); - } - } - - internal partial class RazorProjectProperties { - - private static System.Func>, object, RazorGeneral> CreateRazorGeneralPropertiesDelegate = new System.Func>, object, RazorGeneral>(CreateRazorGeneralProperties); - - private static RazorGeneral CreateRazorGeneralProperties(System.Threading.Tasks.Task> namedCatalogs, object state) { - RazorProjectProperties that = ((RazorProjectProperties)(state)); - return new RazorGeneral(that.ConfiguredProject, namedCatalogs.Result, "Project", that.File, that.ItemType, that.ItemName); - } - - /// Gets the strongly-typed property accessor used to get and set Razor Properties properties. - internal System.Threading.Tasks.Task GetRazorGeneralPropertiesAsync() { - System.Threading.Tasks.Task> namedCatalogsTask = this.GetNamedCatalogsAsync(); - return namedCatalogsTask.ContinueWith(CreateRazorGeneralPropertiesDelegate, this, System.Threading.CancellationToken.None, System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously, System.Threading.Tasks.TaskScheduler.Default); - } - } +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules +{ + + + internal partial class RazorGeneral + { + + /// Backing field for deserialized rule.. + private static Microsoft.Build.Framework.XamlTypes.Rule deserializedFallbackRule; + + /// The name of the schema to look for at runtime to fulfill property access. + internal const string SchemaName = "RazorGeneral"; + + /// The ItemType given in the Rule.DataSource property. May not apply to every Property's individual DataSource. + internal const string PrimaryDataSourceItemType = null; + + /// The Label given in the Rule.DataSource property. May not apply to every Property's individual DataSource. + internal const string PrimaryDataSourceLabel = ""; + + /// Razor Language Version (The "RazorLangVersion" property). + internal const string RazorLangVersionProperty = "RazorLangVersion"; + + /// Razor Configuration Name (The "RazorDefaultConfiguration" property). + internal const string RazorDefaultConfigurationProperty = "RazorDefaultConfiguration"; + + /// Backing field for the property. + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule; + + /// Backing field for the file name of the rule property. + private string file; + + /// Backing field for the ItemType property. + private string itemType; + + /// Backing field for the ItemName property. + private string itemName; + + /// Configured Project + private Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject; + + /// The dictionary of named catalogs. + private System.Collections.Immutable.IImmutableDictionary catalogs; + + /// Backing field for the property. + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule fallbackRule; + + /// Thread locking object + private object locker = new object(); + + /// Initializes a new instance of the RazorGeneral class. + internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule) + { + this.rule = rule; + } + + /// Initializes a new instance of the RazorGeneral class. + internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, string file, string itemType, string itemName) : + this(GetRule(System.Collections.Immutable.ImmutableDictionary.GetValueOrDefault(catalogs, context), file, itemType, itemName)) + { + if ((configuredProject == null)) + { + throw new System.ArgumentNullException("configuredProject"); + } + this.configuredProject = configuredProject; + this.catalogs = catalogs; + this.file = file; + this.itemType = itemType; + this.itemName = itemName; + } + + /// Initializes a new instance of the RazorGeneral class. + internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject) : + this(rule) + { + if ((rule == null)) + { + throw new System.ArgumentNullException("rule"); + } + if ((configuredProject == null)) + { + throw new System.ArgumentNullException("configuredProject"); + } + this.configuredProject = configuredProject; + this.rule = rule; + this.file = this.rule.File; + this.itemType = this.rule.ItemType; + this.itemName = this.rule.ItemName; + } + + /// Initializes a new instance of the RazorGeneral class. + internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertyContext) : + this(configuredProject, catalogs, context, GetContextFile(propertyContext), propertyContext.ItemType, propertyContext.ItemName) + { + } + + /// Initializes a new instance of the RazorGeneral class that assumes a project context (neither property sheet nor items). + internal RazorGeneral(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs) : + this(configuredProject, catalogs, "Project", null, null, null) + { + } + + /// Gets the IRule used to get and set properties. + public Microsoft.VisualStudio.ProjectSystem.Properties.IRule Rule + { + get + { + return this.rule; + } + } + + /// Razor Language Version + internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty RazorLangVersion + { + get + { + Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; + if ((localRule == null)) + { + localRule = this.GeneratedFallbackRule; + } + if ((localRule == null)) + { + return null; + } + Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(RazorLangVersionProperty))); + if (((property == null) + && (this.GeneratedFallbackRule != null))) + { + localRule = this.GeneratedFallbackRule; + property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(RazorLangVersionProperty))); + } + return property; + } + } + + /// Razor Configuration Name + internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty RazorDefaultConfiguration + { + get + { + Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; + if ((localRule == null)) + { + localRule = this.GeneratedFallbackRule; + } + if ((localRule == null)) + { + return null; + } + Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(RazorDefaultConfigurationProperty))); + if (((property == null) + && (this.GeneratedFallbackRule != null))) + { + localRule = this.GeneratedFallbackRule; + property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(RazorDefaultConfigurationProperty))); + } + return property; + } + } + + /// Get the fallback rule if the current rule on disk is missing or a property in the rule on disk is missing + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule GeneratedFallbackRule + { + get + { + if (((this.fallbackRule == null) + && (this.configuredProject != null))) + { + System.Threading.Monitor.Enter(this.locker); + try + { + if ((this.fallbackRule == null)) + { + this.InitializeFallbackRule(); + } + } + finally + { + System.Threading.Monitor.Exit(this.locker); + } + } + return this.fallbackRule; + } + } + + private static Microsoft.VisualStudio.ProjectSystem.Properties.IRule GetRule(Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog, string file, string itemType, string itemName) + { + if ((catalog == null)) + { + return null; + } + return catalog.BindToContext(SchemaName, file, itemType, itemName); + } + + private static string GetContextFile(Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertiesContext) + { + if ((propertiesContext.IsProjectFile == true)) + { + return null; + } + else + { + return propertiesContext.File; + } + } + + private void InitializeFallbackRule() + { + if ((this.configuredProject == null)) + { + return; + } + Microsoft.Build.Framework.XamlTypes.Rule unboundRule = RazorGeneral.deserializedFallbackRule; + if ((unboundRule == null)) + { + System.IO.Stream xamlStream = null; + System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly(); + try + { + xamlStream = thisAssembly.GetManifestResourceStream("XamlRuleToCode:RazorGeneral.xaml"); + Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode root = ((Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode)(System.Xaml.XamlServices.Load(xamlStream))); + System.Collections.Generic.IEnumerator ruleEnumerator = root.GetSchemaObjects(typeof(Microsoft.Build.Framework.XamlTypes.Rule)).GetEnumerator(); + for ( + ; ((unboundRule == null) + && ruleEnumerator.MoveNext()); + ) + { + Microsoft.Build.Framework.XamlTypes.Rule t = ((Microsoft.Build.Framework.XamlTypes.Rule)(ruleEnumerator.Current)); + if (System.StringComparer.OrdinalIgnoreCase.Equals(t.Name, SchemaName)) + { + unboundRule = t; + unboundRule.Name = "6851f2efc446915a1288ec829aa82f7dc9bcfc6addb65b97866a1bc9e30b3348"; + RazorGeneral.deserializedFallbackRule = unboundRule; + } + } + } + finally + { + if ((xamlStream != null)) + { + ((System.IDisposable)(xamlStream)).Dispose(); + } + } + } + this.configuredProject.Services.AdditionalRuleDefinitions.AddRuleDefinition(unboundRule, "FallbackRuleCodeGenerationContext"); + Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog = this.configuredProject.Services.PropertyPagesCatalog.GetMemoryOnlyCatalog("FallbackRuleCodeGenerationContext"); + this.fallbackRule = catalog.BindToContext(unboundRule.Name, this.file, this.itemType, this.itemName); + } + } + + internal partial class RazorProjectProperties + { + + private static System.Func>, object, RazorGeneral> CreateRazorGeneralPropertiesDelegate = new System.Func>, object, RazorGeneral>(CreateRazorGeneralProperties); + + private static RazorGeneral CreateRazorGeneralProperties(System.Threading.Tasks.Task> namedCatalogs, object state) + { + RazorProjectProperties that = ((RazorProjectProperties)(state)); + return new RazorGeneral(that.ConfiguredProject, namedCatalogs.Result, "Project", that.File, that.ItemType, that.ItemName); + } + + /// Gets the strongly-typed property accessor used to get and set Razor Properties properties. + internal System.Threading.Tasks.Task GetRazorGeneralPropertiesAsync() + { + System.Threading.Tasks.Task> namedCatalogsTask = this.GetNamedCatalogsAsync(); + return namedCatalogsTask.ContinueWith(CreateRazorGeneralPropertiesDelegate, this, System.Threading.CancellationToken.None, System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously, System.Threading.Tasks.TaskScheduler.Default); + } + + /// Gets the strongly-typed property accessor used to get value from the current project snapshot Razor Properties properties. + internal bool TryGetCurrentRazorGeneralPropertiesSnapshot(out RazorGeneral snapshot, [System.Runtime.InteropServices.OptionalAttribute()] [System.Runtime.InteropServices.DefaultParameterValueAttribute(true)] bool requiredToMatchProjectVersion) + { + snapshot = null; + Microsoft.VisualStudio.ProjectSystem.IProjectVersionedValue catalogSnapshot; + if (this.TryGetCurrentCatalogSnapshot(out catalogSnapshot)) + { + if (requiredToMatchProjectVersion) + { + if ((this.ConfiguredProject.ProjectVersion.CompareTo(catalogSnapshot.DataSourceVersions[Microsoft.VisualStudio.ProjectSystem.ProjectDataSources.ConfiguredProjectVersion]) != 0)) + { + return false; + } + } + Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule = this.GetSnapshotRule(catalogSnapshot.Value, "Project", RazorGeneral.SchemaName); + if ((rule != null)) + { + snapshot = new RazorGeneral(rule, this.ConfiguredProject); + return true; + } + } + return false; + } + } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGenerateWithTargetPath.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGenerateWithTargetPath.cs index 8428e7244f..3b58d8dd23 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGenerateWithTargetPath.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/Rules/RazorGenerateWithTargetPath.cs @@ -8,205 +8,268 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules { - - - internal partial class RazorGenerateWithTargetPath { - - /// Backing field for deserialized rule.. - private static Microsoft.Build.Framework.XamlTypes.Rule deserializedFallbackRule; - - /// The name of the schema to look for at runtime to fulfill property access. - internal const string SchemaName = "RazorGenerateWithTargetPath"; - - /// The ItemType given in the Rule.DataSource property. May not apply to every Property's individual DataSource. - internal const string PrimaryDataSourceItemType = "RazorGenerateWithTargetPath"; - - /// The Label given in the Rule.DataSource property. May not apply to every Property's individual DataSource. - internal const string PrimaryDataSourceLabel = ""; - - /// Target Path (The "TargetPath" property). - internal const string TargetPathProperty = "TargetPath"; - - /// Backing field for the property. - private Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule; - - /// Backing field for the file name of the rule property. - private string file; - - /// Backing field for the ItemType property. - private string itemType; - - /// Backing field for the ItemName property. - private string itemName; - - /// Configured Project - private Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject; - - /// The dictionary of named catalogs. - private System.Collections.Immutable.IImmutableDictionary catalogs; - - /// Backing field for the property. - private Microsoft.VisualStudio.ProjectSystem.Properties.IRule fallbackRule; - - /// Thread locking object - private object locker = new object(); - - /// Initializes a new instance of the RazorGenerateWithTargetPath class. - internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule) { - this.rule = rule; - } - - /// Initializes a new instance of the RazorGenerateWithTargetPath class. - internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, string file, string itemType, string itemName) : - this(GetRule(System.Collections.Immutable.ImmutableDictionary.GetValueOrDefault(catalogs, context), file, itemType, itemName)) { - if ((configuredProject == null)) { - throw new System.ArgumentNullException("configuredProject"); - } - this.configuredProject = configuredProject; - this.catalogs = catalogs; - this.file = file; - this.itemType = itemType; - this.itemName = itemName; - } - - /// Initializes a new instance of the RazorGenerateWithTargetPath class. - internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject) : - this(rule) { - if ((rule == null)) { - throw new System.ArgumentNullException("rule"); - } - if ((configuredProject == null)) { - throw new System.ArgumentNullException("configuredProject"); - } - this.configuredProject = configuredProject; - this.rule = rule; - this.file = this.rule.File; - this.itemType = this.rule.ItemType; - this.itemName = this.rule.ItemName; - } - - /// Initializes a new instance of the RazorGenerateWithTargetPath class. - internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertyContext) : - this(configuredProject, catalogs, context, GetContextFile(propertyContext), propertyContext.ItemType, propertyContext.ItemName) { - } - - /// Initializes a new instance of the RazorGenerateWithTargetPath class that assumes a project context (neither property sheet nor items). - internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs) : - this(configuredProject, catalogs, "Project", null, null, null) { - } - - /// Gets the IRule used to get and set properties. - public Microsoft.VisualStudio.ProjectSystem.Properties.IRule Rule { - get { - return this.rule; - } - } - - /// Target Path - internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty TargetPath { - get { - Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; - if ((localRule == null)) { - localRule = this.GeneratedFallbackRule; - } - if ((localRule == null)) { - return null; - } - Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(TargetPathProperty))); - if (((property == null) - && (this.GeneratedFallbackRule != null))) { - localRule = this.GeneratedFallbackRule; - property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(TargetPathProperty))); - } - return property; - } - } - - /// Get the fallback rule if the current rule on disk is missing or a property in the rule on disk is missing - private Microsoft.VisualStudio.ProjectSystem.Properties.IRule GeneratedFallbackRule { - get { - if (((this.fallbackRule == null) - && (this.configuredProject != null))) { - System.Threading.Monitor.Enter(this.locker); - try { - if ((this.fallbackRule == null)) { - this.InitializeFallbackRule(); - } - } - finally { - System.Threading.Monitor.Exit(this.locker); - } - } - return this.fallbackRule; - } - } - - private static Microsoft.VisualStudio.ProjectSystem.Properties.IRule GetRule(Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog, string file, string itemType, string itemName) { - if ((catalog == null)) { - return null; - } - return catalog.BindToContext(SchemaName, file, itemType, itemName); - } - - private static string GetContextFile(Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertiesContext) { - if ((propertiesContext.IsProjectFile == true)) { - return null; - } - else { - return propertiesContext.File; - } - } - - private void InitializeFallbackRule() { - if ((this.configuredProject == null)) { - return; - } - Microsoft.Build.Framework.XamlTypes.Rule unboundRule = RazorGenerateWithTargetPath.deserializedFallbackRule; - if ((unboundRule == null)) { - System.IO.Stream xamlStream = null; - System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly(); - try { - xamlStream = thisAssembly.GetManifestResourceStream("XamlRuleToCode:RazorGenerateWithTargetPath.xaml"); - Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode root = ((Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode)(System.Xaml.XamlServices.Load(xamlStream))); - System.Collections.Generic.IEnumerator ruleEnumerator = root.GetSchemaObjects(typeof(Microsoft.Build.Framework.XamlTypes.Rule)).GetEnumerator(); - for ( - ; ((unboundRule == null) - && ruleEnumerator.MoveNext()); - ) { - Microsoft.Build.Framework.XamlTypes.Rule t = ((Microsoft.Build.Framework.XamlTypes.Rule)(ruleEnumerator.Current)); - if (System.StringComparer.OrdinalIgnoreCase.Equals(t.Name, SchemaName)) { - unboundRule = t; - unboundRule.Name = "4d01f23e-96db-4c90-9c01-17b7f8b61243"; - RazorGenerateWithTargetPath.deserializedFallbackRule = unboundRule; - } - } - } - finally { - if ((xamlStream != null)) { - ((System.IDisposable)(xamlStream)).Dispose(); - } - } - } - this.configuredProject.Services.AdditionalRuleDefinitions.AddRuleDefinition(unboundRule, "FallbackRuleCodeGenerationContext"); - Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog = this.configuredProject.Services.PropertyPagesCatalog.GetMemoryOnlyCatalog("FallbackRuleCodeGenerationContext"); - this.fallbackRule = catalog.BindToContext(unboundRule.Name, this.file, this.itemType, this.itemName); - } - } - - internal partial class RazorProjectProperties { - - private static System.Func>, object, RazorGenerateWithTargetPath> CreateRazorGenerateWithTargetPathPropertiesDelegate = new System.Func>, object, RazorGenerateWithTargetPath>(CreateRazorGenerateWithTargetPathProperties); - - private static RazorGenerateWithTargetPath CreateRazorGenerateWithTargetPathProperties(System.Threading.Tasks.Task> namedCatalogs, object state) { - RazorProjectProperties that = ((RazorProjectProperties)(state)); - return new RazorGenerateWithTargetPath(that.ConfiguredProject, namedCatalogs.Result, "Project", that.File, that.ItemType, that.ItemName); - } - - /// Gets the strongly-typed property accessor used to get and set Razor Document Properties properties. - internal System.Threading.Tasks.Task GetRazorGenerateWithTargetPathPropertiesAsync() { - System.Threading.Tasks.Task> namedCatalogsTask = this.GetNamedCatalogsAsync(); - return namedCatalogsTask.ContinueWith(CreateRazorGenerateWithTargetPathPropertiesDelegate, this, System.Threading.CancellationToken.None, System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously, System.Threading.Tasks.TaskScheduler.Default); - } - } +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem.Rules +{ + + + internal partial class RazorGenerateWithTargetPath + { + + /// Backing field for deserialized rule.. + private static Microsoft.Build.Framework.XamlTypes.Rule deserializedFallbackRule; + + /// The name of the schema to look for at runtime to fulfill property access. + internal const string SchemaName = "RazorGenerateWithTargetPath"; + + /// The ItemType given in the Rule.DataSource property. May not apply to every Property's individual DataSource. + internal const string PrimaryDataSourceItemType = "RazorGenerateWithTargetPath"; + + /// The Label given in the Rule.DataSource property. May not apply to every Property's individual DataSource. + internal const string PrimaryDataSourceLabel = ""; + + /// (The "TargetPath" property). + internal const string TargetPathProperty = "TargetPath"; + + /// Backing field for the property. + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule; + + /// Backing field for the file name of the rule property. + private string file; + + /// Backing field for the ItemType property. + private string itemType; + + /// Backing field for the ItemName property. + private string itemName; + + /// Configured Project + private Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject; + + /// The dictionary of named catalogs. + private System.Collections.Immutable.IImmutableDictionary catalogs; + + /// Backing field for the property. + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule fallbackRule; + + /// Thread locking object + private object locker = new object(); + + /// Initializes a new instance of the RazorGenerateWithTargetPath class. + internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule) + { + this.rule = rule; + } + + /// Initializes a new instance of the RazorGenerateWithTargetPath class. + internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, string file, string itemType, string itemName) : + this(GetRule(System.Collections.Immutable.ImmutableDictionary.GetValueOrDefault(catalogs, context), file, itemType, itemName)) + { + if ((configuredProject == null)) + { + throw new System.ArgumentNullException("configuredProject"); + } + this.configuredProject = configuredProject; + this.catalogs = catalogs; + this.file = file; + this.itemType = itemType; + this.itemName = itemName; + } + + /// Initializes a new instance of the RazorGenerateWithTargetPath class. + internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule, Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject) : + this(rule) + { + if ((rule == null)) + { + throw new System.ArgumentNullException("rule"); + } + if ((configuredProject == null)) + { + throw new System.ArgumentNullException("configuredProject"); + } + this.configuredProject = configuredProject; + this.rule = rule; + this.file = this.rule.File; + this.itemType = this.rule.ItemType; + this.itemName = this.rule.ItemName; + } + + /// Initializes a new instance of the RazorGenerateWithTargetPath class. + internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs, string context, Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertyContext) : + this(configuredProject, catalogs, context, GetContextFile(propertyContext), propertyContext.ItemType, propertyContext.ItemName) + { + } + + /// Initializes a new instance of the RazorGenerateWithTargetPath class that assumes a project context (neither property sheet nor items). + internal RazorGenerateWithTargetPath(Microsoft.VisualStudio.ProjectSystem.ConfiguredProject configuredProject, System.Collections.Immutable.IImmutableDictionary catalogs) : + this(configuredProject, catalogs, "Project", null, null, null) + { + } + + /// Gets the IRule used to get and set properties. + public Microsoft.VisualStudio.ProjectSystem.Properties.IRule Rule + { + get + { + return this.rule; + } + } + + /// TargetPath + internal Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty TargetPath + { + get + { + Microsoft.VisualStudio.ProjectSystem.Properties.IRule localRule = this.rule; + if ((localRule == null)) + { + localRule = this.GeneratedFallbackRule; + } + if ((localRule == null)) + { + return null; + } + Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(TargetPathProperty))); + if (((property == null) + && (this.GeneratedFallbackRule != null))) + { + localRule = this.GeneratedFallbackRule; + property = ((Microsoft.VisualStudio.ProjectSystem.Properties.IEvaluatedProperty)(localRule.GetProperty(TargetPathProperty))); + } + return property; + } + } + + /// Get the fallback rule if the current rule on disk is missing or a property in the rule on disk is missing + private Microsoft.VisualStudio.ProjectSystem.Properties.IRule GeneratedFallbackRule + { + get + { + if (((this.fallbackRule == null) + && (this.configuredProject != null))) + { + System.Threading.Monitor.Enter(this.locker); + try + { + if ((this.fallbackRule == null)) + { + this.InitializeFallbackRule(); + } + } + finally + { + System.Threading.Monitor.Exit(this.locker); + } + } + return this.fallbackRule; + } + } + + private static Microsoft.VisualStudio.ProjectSystem.Properties.IRule GetRule(Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog, string file, string itemType, string itemName) + { + if ((catalog == null)) + { + return null; + } + return catalog.BindToContext(SchemaName, file, itemType, itemName); + } + + private static string GetContextFile(Microsoft.VisualStudio.ProjectSystem.Properties.IProjectPropertiesContext propertiesContext) + { + if ((propertiesContext.IsProjectFile == true)) + { + return null; + } + else + { + return propertiesContext.File; + } + } + + private void InitializeFallbackRule() + { + if ((this.configuredProject == null)) + { + return; + } + Microsoft.Build.Framework.XamlTypes.Rule unboundRule = RazorGenerateWithTargetPath.deserializedFallbackRule; + if ((unboundRule == null)) + { + System.IO.Stream xamlStream = null; + System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly(); + try + { + xamlStream = thisAssembly.GetManifestResourceStream("XamlRuleToCode:RazorGenerateWithTargetPath.xaml"); + Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode root = ((Microsoft.Build.Framework.XamlTypes.IProjectSchemaNode)(System.Xaml.XamlServices.Load(xamlStream))); + System.Collections.Generic.IEnumerator ruleEnumerator = root.GetSchemaObjects(typeof(Microsoft.Build.Framework.XamlTypes.Rule)).GetEnumerator(); + for ( + ; ((unboundRule == null) + && ruleEnumerator.MoveNext()); + ) + { + Microsoft.Build.Framework.XamlTypes.Rule t = ((Microsoft.Build.Framework.XamlTypes.Rule)(ruleEnumerator.Current)); + if (System.StringComparer.OrdinalIgnoreCase.Equals(t.Name, SchemaName)) + { + unboundRule = t; + unboundRule.Name = "4325a10cce970a389c33b97140c4307d2e10664e053c75cf9e0e0fd08288774f"; + RazorGenerateWithTargetPath.deserializedFallbackRule = unboundRule; + } + } + } + finally + { + if ((xamlStream != null)) + { + ((System.IDisposable)(xamlStream)).Dispose(); + } + } + } + this.configuredProject.Services.AdditionalRuleDefinitions.AddRuleDefinition(unboundRule, "FallbackRuleCodeGenerationContext"); + Microsoft.VisualStudio.ProjectSystem.Properties.IPropertyPagesCatalog catalog = this.configuredProject.Services.PropertyPagesCatalog.GetMemoryOnlyCatalog("FallbackRuleCodeGenerationContext"); + this.fallbackRule = catalog.BindToContext(unboundRule.Name, this.file, this.itemType, this.itemName); + } + } + + internal partial class RazorProjectProperties + { + + private static System.Func>, object, RazorGenerateWithTargetPath> CreateRazorGenerateWithTargetPathPropertiesDelegate = new System.Func>, object, RazorGenerateWithTargetPath>(CreateRazorGenerateWithTargetPathProperties); + + private static RazorGenerateWithTargetPath CreateRazorGenerateWithTargetPathProperties(System.Threading.Tasks.Task> namedCatalogs, object state) + { + RazorProjectProperties that = ((RazorProjectProperties)(state)); + return new RazorGenerateWithTargetPath(that.ConfiguredProject, namedCatalogs.Result, "Project", that.File, that.ItemType, that.ItemName); + } + + /// Gets the strongly-typed property accessor used to get and set Razor Document Properties properties. + internal System.Threading.Tasks.Task GetRazorGenerateWithTargetPathPropertiesAsync() + { + System.Threading.Tasks.Task> namedCatalogsTask = this.GetNamedCatalogsAsync(); + return namedCatalogsTask.ContinueWith(CreateRazorGenerateWithTargetPathPropertiesDelegate, this, System.Threading.CancellationToken.None, System.Threading.Tasks.TaskContinuationOptions.ExecuteSynchronously, System.Threading.Tasks.TaskScheduler.Default); + } + + /// Gets the strongly-typed property accessor used to get value from the current project snapshot Razor Document Properties properties. + internal bool TryGetCurrentRazorGenerateWithTargetPathPropertiesSnapshot(out RazorGenerateWithTargetPath snapshot, [System.Runtime.InteropServices.OptionalAttribute()] [System.Runtime.InteropServices.DefaultParameterValueAttribute(true)] bool requiredToMatchProjectVersion) + { + snapshot = null; + Microsoft.VisualStudio.ProjectSystem.IProjectVersionedValue catalogSnapshot; + if (this.TryGetCurrentCatalogSnapshot(out catalogSnapshot)) + { + if (requiredToMatchProjectVersion) + { + if ((this.ConfiguredProject.ProjectVersion.CompareTo(catalogSnapshot.DataSourceVersions[Microsoft.VisualStudio.ProjectSystem.ProjectDataSources.ConfiguredProjectVersion]) != 0)) + { + return false; + } + } + Microsoft.VisualStudio.ProjectSystem.Properties.IRule rule = this.GetSnapshotRule(catalogSnapshot.Value, "Project", RazorGenerateWithTargetPath.SchemaName); + if ((rule != null)) + { + snapshot = new RazorGenerateWithTargetPath(rule, this.ConfiguredProject); + return true; + } + } + return false; + } + } } diff --git a/src/Razor/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Microsoft.VisualStudio.Mac.LanguageServices.Razor.csproj b/src/Razor/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Microsoft.VisualStudio.Mac.LanguageServices.Razor.csproj index 69e9071704..b3c81528d1 100644 --- a/src/Razor/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Microsoft.VisualStudio.Mac.LanguageServices.Razor.csproj +++ b/src/Razor/src/Microsoft.VisualStudio.Mac.LanguageServices.Razor/Microsoft.VisualStudio.Mac.LanguageServices.Razor.csproj @@ -1,7 +1,7 @@  - net461 + net472 Razor is a markup syntax for adding server-side logic to web pages. This package contains the Razor design-time infrastructure for Visual Studio for Mac. false @@ -15,4 +15,8 @@ + + + + diff --git a/src/Razor/src/RazorPageGenerator/Program.cs b/src/Razor/src/RazorPageGenerator/Program.cs index 49a3154c90..5661d34cd3 100644 --- a/src/Razor/src/RazorPageGenerator/Program.cs +++ b/src/Razor/src/RazorPageGenerator/Program.cs @@ -61,7 +61,6 @@ Examples: @class.Modifiers.Add("internal"); }); - FunctionsDirective.Register(builder); InheritsDirective.Register(builder); SectionDirective.Register(builder); diff --git a/src/Razor/src/RazorPageGenerator/RazorPageGenerator.csproj b/src/Razor/src/RazorPageGenerator/RazorPageGenerator.csproj index 2668c16904..c7a690e4d6 100644 --- a/src/Razor/src/RazorPageGenerator/RazorPageGenerator.csproj +++ b/src/Razor/src/RazorPageGenerator/RazorPageGenerator.csproj @@ -2,7 +2,7 @@ Builds Razor pages for views in a project. For internal use only. - netcoreapp2.1 + netcoreapp3.0 dotnet-razorpagegenerator RazorPageGenerator Exe diff --git a/src/Razor/test/Directory.Build.props b/src/Razor/test/Directory.Build.props index 1b12a7a57d..981d3641f8 100644 --- a/src/Razor/test/Directory.Build.props +++ b/src/Razor/test/Directory.Build.props @@ -2,7 +2,7 @@ - netcoreapp2.2 + netcoreapp3.0 $(DeveloperBuildTestTfms) $(StandardTestTfms) net461;$(StandardTestTfms) diff --git a/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/InjectDirectiveTest.cs b/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/InjectDirectiveTest.cs index eda37a3b99..54a2fa4fd1 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/InjectDirectiveTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/InjectDirectiveTest.cs @@ -194,22 +194,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions } } - return codeDocument.GetDocumentIntermediateNode(); - } - - private string GetCSharpContent(IntermediateNode node) - { - var builder = new StringBuilder(); - for (var i = 0; i < node.Children.Count; i++) - { - var child = node.Children[i] as IntermediateToken; - if (child.Kind == TokenKind.CSharp) - { - builder.Append(child.Content); - } - } - - return builder.ToString(); + var irDocument = codeDocument.GetDocumentIntermediateNode(); + irDocument.DocumentKind = MvcViewDocumentClassifierPass.MvcViewDocumentKind; + return irDocument; } private class ClassNodeVisitor : IntermediateNodeWalker diff --git a/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/IntegrationTests/CodeGenerationIntegrationTest.cs b/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/IntegrationTests/CodeGenerationIntegrationTest.cs index b36e2dcad9..8273810ca3 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/IntegrationTests/CodeGenerationIntegrationTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/IntegrationTests/CodeGenerationIntegrationTest.cs @@ -19,9 +19,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.IntegrationTests : base(generateBaselines: null) { Configuration = RazorConfiguration.Create( - RazorLanguageVersion.Version_2_0, - "MVC-2.1", - new[] { new AssemblyExtension("MVC-2.1", typeof(ExtensionInitializer).Assembly) }); + RazorLanguageVersion.Version_3_0, + "MVC-3.0", + new[] { new AssemblyExtension("MVC-3.0", typeof(ExtensionInitializer).Assembly) }); } protected override CSharpCompilation BaseCompilation => DefaultBaseCompilation; diff --git a/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test.csproj b/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test.csproj index 8dcca8cc7c..86d975d404 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test.csproj +++ b/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test.csproj @@ -41,11 +41,11 @@ - - - - + + + + diff --git a/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Basic_Runtime.codegen.cs b/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Basic_Runtime.codegen.cs index 8e6b1a7bd7..ec84263550 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Basic_Runtime.codegen.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/Basic_Runtime.codegen.cs @@ -19,9 +19,7 @@ namespace AspNetCore #pragma warning disable 1998 public async override global::System.Threading.Tasks.Task ExecuteAsync() { - BeginContext(0, 4, true); WriteLiteral("