Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create source generator for configuration binding #82179

Merged
merged 7 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/project/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,20 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL
| __`SYSLIB1097`__ | _`SYSLIB1092`-`SYSLIB1099` reserved for Microsoft.Interop.ComInteropGenerator._ |
| __`SYSLIB1098`__ | _`SYSLIB1092`-`SYSLIB1099` reserved for Microsoft.Interop.ComInteropGenerator._ |
| __`SYSLIB1099`__ | _`SYSLIB1092`-`SYSLIB1099` reserved for Microsoft.Interop.ComInteropGenerator._ |
| __`SYSLIB1100`__ | Configuration binding generator: type is not supported. |
| __`SYSLIB1101`__ | Configuration binding generator: property on type is not supported. |
| __`SYSLIB1102`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |
layomia marked this conversation as resolved.
Show resolved Hide resolved
| __`SYSLIB1103`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |
| __`SYSLIB1104`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |
| __`SYSLIB1105`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |
| __`SYSLIB1106`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |
| __`SYSLIB1107`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |
| __`SYSLIB1108`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |
| __`SYSLIB1109`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |
| __`SYSLIB1110`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |
| __`SYSLIB1111`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |
| __`SYSLIB1112`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |
| __`SYSLIB1113`__ | *_`SYSLIB1101`-`SYSLIB1113` reserved for Microsoft.Extensions.Configuration.Binder.SourceGeneration.* |


### Diagnostic Suppressions (`SYSLIBSUPPRESS****`)
Expand Down
17 changes: 17 additions & 0 deletions eng/OffByDefaultRoslynComponent.targets.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project>
<Target Name="_{TargetPrefix}GatherAnalyzers">
<ItemGroup>
<_{TargetPrefix}Analyzer Include="@(Analyzer)" Condition="'%(Analyzer.NuGetPackageId)' == '{NuGetPackageId}'" />
</ItemGroup>
</Target>
layomia marked this conversation as resolved.
Show resolved Hide resolved

<Target Name="_{TargetPrefix}RemoveAnalyzer"
Condition="'$({EnableSourceGeneratorPropertyName})' != 'true'"
AfterTargets="ResolvePackageDependenciesForBuild;ResolveNuGetPackageAssets"
DependsOnTargets="_{TargetPrefix}GatherAnalyzers">

<ItemGroup>
<Analyzer Remove="@(_{TargetPrefix}Analyzer)" />
</ItemGroup>
</Target>
</Project>
37 changes: 36 additions & 1 deletion eng/packaging.targets
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@
<IncludeMultiTargetRoslynComponentTargets Condition="'$(IncludeMultiTargetRoslynComponentTargets)' == ''">true</IncludeMultiTargetRoslynComponentTargets>
</PropertyGroup>

<!-- In packages that contain Analyzers, include a .targets file that will select the correct analyzer. -->
<!-- In packages that contain multi-target Analyzers, include a .targets file that will select the correct analyzer. -->
<Target Name="IncludeMultiTargetRoslynComponentTargetsInPackage"
AfterTargets="IncludeAnalyzersInPackage"
Condition="'@(ProjectReference)' != '' and
Expand Down Expand Up @@ -193,6 +193,41 @@
Overwrite="true" />
</Target>

<PropertyGroup>
<_OffByDefaultRoslynComponentTargetsTemplate>$(MSBuildThisFileDirectory)OffByDefaultRoslynComponent.targets.template</_OffByDefaultRoslynComponentTargetsTemplate>
<OffByDefaultRoslynComponentTargetsFileIntermediatePath>$(IntermediateOutputPath)OffByDefaultRoslynComponent.targets</OffByDefaultRoslynComponentTargetsFileIntermediatePath>
<RoslynComponentsAreOffByDefault Condition="'$(RoslynComponentsAreOffByDefault)' == ''">false</RoslynComponentsAreOffByDefault>
</PropertyGroup>

<!-- In packages that contain Analyzers that are off by default, include a .targets file that will exclude the analyzer. -->
<Target Name="ExcludeOffByDefaultRoslynComponentTargetsInPackage"
AfterTargets="IncludeAnalyzersInPackage"
Condition="'@(ProjectReference)' != '' and
@(ProjectReference->AnyHaveMetadataValue('PackAsAnalyzer', 'true')) and
'$(RoslynComponentsAreOffByDefault)' == 'true'"
DependsOnTargets="GenerateOffByDefaultRoslynComponentTargetsFile">
<ItemGroup>
<Content Include="$(OffByDefaultRoslynComponentTargetsFileIntermediatePath)" PackagePath="buildTransitive\netstandard2.0\$(PackageId).targets" />
<Content Include="$(OffByDefaultRoslynComponentTargetsFileIntermediatePath)" PackagePath="buildTransitive\%(NETStandardCompatError.Supported)\$(PackageId).targets" Condition="'@(NETStandardCompatError)' != ''" />
</ItemGroup>
</Target>

<Target Name="GenerateOffByDefaultRoslynComponentTargetsFile"
Inputs="$(MSBuildProjectFullPath);$(_OffByDefaultRoslynComponentTargetsTemplate)"
Outputs="$(OffByDefaultRoslynComponentTargetsFileIntermediatePath)">
<PropertyGroup>
<_OffByDefaultRoslynComponentTargetPrefix>$(PackageId.Replace('.', '_'))</_OffByDefaultRoslynComponentTargetPrefix>
<EnableSourceGeneratorPropertyName Condition="'$(EnableSourceGeneratorPropertyName)' == ''">Enable$(PackageId.Replace('.', ''))SourceGenerator</EnableSourceGeneratorPropertyName>
</PropertyGroup>

<WriteLinesToFile File="$(OffByDefaultRoslynComponentTargetsFileIntermediatePath)"
Lines="$([System.IO.File]::ReadAllText('$(_OffByDefaultRoslynComponentTargetsTemplate)')
.Replace('{TargetPrefix}', '$(_OffByDefaultRoslynComponentTargetPrefix)')
.Replace('{NuGetPackageId}', '$(PackageId)')
.Replace('{EnableSourceGeneratorPropertyName}', '$(EnableSourceGeneratorPropertyName)'))"
Overwrite="true" />
</Target>

<!-- Add targets file that marks a .NETStandard applicable tfm as unsupported. -->
<Target Name="AddNETStandardCompatErrorFileForPackaging"
Condition="'@(NETStandardCompatError)' != '' and '$(DisableNETStandardCompatErrors)' != 'true'"
Expand Down
38 changes: 35 additions & 3 deletions src/libraries/Common/tests/SourceGenerators/RoslynTestUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,27 +266,51 @@ public static async Task<IList<string>> RunAnalyzerAndFixer(
for (int i = 0; i < count; i++)
{
SourceText s = await proj.FindDocument(l[i]).GetTextAsync().ConfigureAwait(false);
results.Add(s.ToString().Replace("\r\n", "\n", StringComparison.Ordinal));
results.Add(Replace(s.ToString(), "\r\n", "\n"));
layomia marked this conversation as resolved.
Show resolved Hide resolved
}
}
else
{
for (int i = 0; i < count; i++)
{
SourceText s = await proj.FindDocument($"src-{i}.cs").GetTextAsync().ConfigureAwait(false);
results.Add(s.ToString().Replace("\r\n", "\n", StringComparison.Ordinal));
results.Add(Replace(s.ToString(), "\r\n", "\n"));
}
}

if (extraFile != null)
{
SourceText s = await proj.FindDocument(extraFile).GetTextAsync().ConfigureAwait(false);
results.Add(s.ToString().Replace("\r\n", "\n", StringComparison.Ordinal));
results.Add(Replace(s.ToString(), "\r\n", "\n"));
}

return results;
}

public static bool CompareLines(string[] expectedLines, SourceText sourceText, out string message)
layomia marked this conversation as resolved.
Show resolved Hide resolved
{
if (expectedLines.Length != sourceText.Lines.Count)
{
message = string.Format("Line numbers do not match. Expected: {0} lines, but generated {1}",
expectedLines.Length, sourceText.Lines.Count);
layomia marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
int index = 0;
foreach (TextLine textLine in sourceText.Lines)
{
string expectedLine = expectedLines[index];
if (!expectedLine.Equals(textLine.ToString(), StringComparison.Ordinal))
layomia marked this conversation as resolved.
Show resolved Hide resolved
{
message = string.Format("Line {0} does not match.{1}Expected Line:{1}{2}{1}Actual Line:{1}{3}",
textLine.LineNumber + 1, Environment.NewLine, expectedLine, textLine);
return false;
}
index++;
}
message = string.Empty;
return true;
}

private static async Task<Project> RecreateProjectDocumentsAsync(Project project)
{
foreach (DocumentId documentId in project.DocumentIds)
Expand All @@ -304,5 +328,13 @@ private static async Task<Document> RecreateDocumentAsync(Document document)
SourceText newText = await document.GetTextAsync().ConfigureAwait(false);
return document.WithText(SourceText.From(newText.ToString(), newText.Encoding, newText.ChecksumAlgorithm));
}

private static string Replace(string text, string oldText, string newText) =>
text.Replace(
oldText, newText
#if NETCOREAPP
, StringComparison.Ordinal
#endif
);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Microsoft Visual Studio Solution File, Format Version 12.00
#
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{5FB89358-3575-45AA-ACFE-EF3598B9AB7E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration.Abstractions", "..\Microsoft.Extensions.Configuration.Abstractions\ref\Microsoft.Extensions.Configuration.Abstractions.csproj", "{CB09105A-F475-4A91-8836-434FA175F4F9}"
Expand All @@ -9,8 +10,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Config
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration.Binder", "src\Microsoft.Extensions.Configuration.Binder.csproj", "{8C7443D8-864A-4DAE-8835-108580C289F5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration.Binder.Tests", "tests\Microsoft.Extensions.Configuration.Binder.Tests.csproj", "{FD4C7C59-55A7-42C8-9B75-43728FAFDD84}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration", "..\Microsoft.Extensions.Configuration\ref\Microsoft.Extensions.Configuration.csproj", "{39D99379-E744-4295-9CA8-B5C6DE286EA0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration", "..\Microsoft.Extensions.Configuration\src\Microsoft.Extensions.Configuration.csproj", "{5177A566-05AF-4DF0-93CC-D2876F7E6EBB}"
Expand All @@ -35,6 +34,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{94EEF122-C30
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{CC3961B0-C62D-44B9-91DB-11D94A3F91A5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Configuration.Binder.SourceGeneration", "gen\Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj", "{D4B3EEA1-7394-49EA-A088-897C0CD26D11}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests", "tests\SourceGenerationTests\Microsoft.Extensions.Configuration.Binder.SourceGeneration.Tests.csproj", "{56F4D38E-41A0-45E2-9F04-9E670D002E71}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Configuration.Binder.Tests", "tests\UnitTests\Microsoft.Extensions.Configuration.Binder.Tests.csproj", "{75BA0154-A24D-421E-9046-C7949DF12A55}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -61,10 +66,6 @@ Global
{8C7443D8-864A-4DAE-8835-108580C289F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C7443D8-864A-4DAE-8835-108580C289F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C7443D8-864A-4DAE-8835-108580C289F5}.Release|Any CPU.Build.0 = Release|Any CPU
{FD4C7C59-55A7-42C8-9B75-43728FAFDD84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FD4C7C59-55A7-42C8-9B75-43728FAFDD84}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FD4C7C59-55A7-42C8-9B75-43728FAFDD84}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FD4C7C59-55A7-42C8-9B75-43728FAFDD84}.Release|Any CPU.Build.0 = Release|Any CPU
{39D99379-E744-4295-9CA8-B5C6DE286EA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{39D99379-E744-4295-9CA8-B5C6DE286EA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39D99379-E744-4295-9CA8-B5C6DE286EA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -97,13 +98,36 @@ Global
{6E58AF2F-AB35-4279-9135-67E97BCE1432}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6E58AF2F-AB35-4279-9135-67E97BCE1432}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6E58AF2F-AB35-4279-9135-67E97BCE1432}.Release|Any CPU.Build.0 = Release|Any CPU
{06BAC1EF-96B3-4F9A-A962-D10204EAF6EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{06BAC1EF-96B3-4F9A-A962-D10204EAF6EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{06BAC1EF-96B3-4F9A-A962-D10204EAF6EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{06BAC1EF-96B3-4F9A-A962-D10204EAF6EF}.Release|Any CPU.Build.0 = Release|Any CPU
{C459288D-8A57-456E-B5B3-861C8043EEE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C459288D-8A57-456E-B5B3-861C8043EEE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C459288D-8A57-456E-B5B3-861C8043EEE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C459288D-8A57-456E-B5B3-861C8043EEE0}.Release|Any CPU.Build.0 = Release|Any CPU
{D4B3EEA1-7394-49EA-A088-897C0CD26D11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D4B3EEA1-7394-49EA-A088-897C0CD26D11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4B3EEA1-7394-49EA-A088-897C0CD26D11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4B3EEA1-7394-49EA-A088-897C0CD26D11}.Release|Any CPU.Build.0 = Release|Any CPU
{EA44B6C3-BC98-4253-8BDE-5848942967A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EA44B6C3-BC98-4253-8BDE-5848942967A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA44B6C3-BC98-4253-8BDE-5848942967A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA44B6C3-BC98-4253-8BDE-5848942967A6}.Release|Any CPU.Build.0 = Release|Any CPU
{56F4D38E-41A0-45E2-9F04-9E670D002E71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{56F4D38E-41A0-45E2-9F04-9E670D002E71}.Debug|Any CPU.Build.0 = Debug|Any CPU
{56F4D38E-41A0-45E2-9F04-9E670D002E71}.Release|Any CPU.ActiveCfg = Release|Any CPU
{56F4D38E-41A0-45E2-9F04-9E670D002E71}.Release|Any CPU.Build.0 = Release|Any CPU
{75BA0154-A24D-421E-9046-C7949DF12A55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75BA0154-A24D-421E-9046-C7949DF12A55}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75BA0154-A24D-421E-9046-C7949DF12A55}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75BA0154-A24D-421E-9046-C7949DF12A55}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5FB89358-3575-45AA-ACFE-EF3598B9AB7E} = {AAE738F3-89AC-4406-B1D9-A61A2C3A1CF0}
{FD4C7C59-55A7-42C8-9B75-43728FAFDD84} = {AAE738F3-89AC-4406-B1D9-A61A2C3A1CF0}
{CB09105A-F475-4A91-8836-434FA175F4F9} = {F43534CA-C419-405E-B239-CDE2BDC703BE}
{F05212B2-FD66-4E8E-AEA0-FAC06A0D6808} = {F43534CA-C419-405E-B239-CDE2BDC703BE}
{39D99379-E744-4295-9CA8-B5C6DE286EA0} = {F43534CA-C419-405E-B239-CDE2BDC703BE}
Expand All @@ -116,6 +140,12 @@ Global
{9B21B87F-084B-411B-A513-C22B5B961BF3} = {94EEF122-C307-4BF0-88FE-263B89B59F9F}
{05B7F752-4991-4DC8-9B06-8269211E7817} = {CC3961B0-C62D-44B9-91DB-11D94A3F91A5}
{6E58AF2F-AB35-4279-9135-67E97BCE1432} = {CC3961B0-C62D-44B9-91DB-11D94A3F91A5}
{06BAC1EF-96B3-4F9A-A962-D10204EAF6EF} = {CC3961B0-C62D-44B9-91DB-11D94A3F91A5}
{C459288D-8A57-456E-B5B3-861C8043EEE0} = {CC3961B0-C62D-44B9-91DB-11D94A3F91A5}
{D4B3EEA1-7394-49EA-A088-897C0CD26D11} = {CC3961B0-C62D-44B9-91DB-11D94A3F91A5}
{EA44B6C3-BC98-4253-8BDE-5848942967A6} = {AAE738F3-89AC-4406-B1D9-A61A2C3A1CF0}
{56F4D38E-41A0-45E2-9F04-9E670D002E71} = {AAE738F3-89AC-4406-B1D9-A61A2C3A1CF0}
{75BA0154-A24D-421E-9046-C7949DF12A55} = {AAE738F3-89AC-4406-B1D9-A61A2C3A1CF0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A97DC4BF-32F0-46E8-B91C-84D1E7F2A27E}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.CodeAnalysis;

namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
{
internal abstract record CollectionSpec : TypeSpec
{
public CollectionSpec(ITypeSymbol type) : base(type)
{
IsReadOnly = type.IsReadOnly;
IsInterface = type is INamedTypeSymbol { TypeKind: TypeKind.Interface };
}

public required TypeSpec ElementType { get; init; }

public bool IsReadOnly { get; }

public bool IsInterface { get; }

public CollectionSpec? ConcreteType { get; init; }
}

internal sealed record EnumerableSpec : CollectionSpec
{
public EnumerableSpec(ITypeSymbol type) : base(type) { }

public override TypeSpecKind SpecKind { get; init; } = TypeSpecKind.Enumerable;
}

internal sealed record DictionarySpec : CollectionSpec
{
public DictionarySpec(INamedTypeSymbol type) : base(type) { }

public override TypeSpecKind SpecKind => TypeSpecKind.Dictionary;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Should this be { get; init; } as EnumerableSpec?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends: if it is converted into an auto-property then it will be part of the record's equality comparison implementation.

FWIW the equality comparison generated for C# records does take polymorphism into account, so you wouldn't need to take the "kind" into account:

https://sharplab.io/#v2:D4AQTAjAsAUCDMACciDCiDetE+UgTgKYDGA9vgCaIBCAhgM6EAUAlgHYAuiAHgJQDc2XEJwJERMpUQARQvhYA3QhVaceAGkTsuAT16IAXDQbM+mAL6xzQA==


public required TypeSpec KeyType { get; init; }
}
}
Loading