Skip to content

Commit 88b633e

Browse files
committed
Merge branch 'refs/heads/dev' into new-extensions
# Conflicts: # src/RestSharp/Extensions/GenerateImmutableAttribute.cs # src/RestSharp/Response/RestResponseBase.cs
2 parents 726f5c9 + 57f6b3a commit 88b633e

File tree

11 files changed

+187
-46
lines changed

11 files changed

+187
-46
lines changed

.github/workflows/test-results.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
steps:
2222
-
2323
name: Download and Extract Artifacts
24-
uses: dawidd6/action-download-artifact@09f2f74827fd3a8607589e5ad7f9398816f540fe
24+
uses: dawidd6/action-download-artifact@deb3bb83256a78589fef6a7b942e5f2573ad7c13
2525
with:
2626
run_id: ${{ github.event.workflow_run.id }}
2727
path: artifacts

gen/SourceGenerator/Extensions.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) .NET Foundation and Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace SourceGenerator;
16+
17+
static class Extensions {
18+
public static IEnumerable<ClassDeclarationSyntax> FindClasses(this Compilation compilation, Func<ClassDeclarationSyntax, bool> predicate)
19+
=> compilation.SyntaxTrees
20+
.Select(tree => compilation.GetSemanticModel(tree))
21+
.SelectMany(model => model.SyntaxTree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>())
22+
.Where(predicate);
23+
24+
public static IEnumerable<ClassDeclarationSyntax> FindAnnotatedClass(this Compilation compilation, string attributeName, bool strict) {
25+
return compilation.FindClasses(
26+
syntax => syntax.AttributeLists.Any(list => list.Attributes.Any(CheckAttribute))
27+
);
28+
29+
bool CheckAttribute(AttributeSyntax attr) {
30+
var name = attr.Name.ToString();
31+
return strict ? name == attributeName : name.StartsWith(attributeName);
32+
}
33+
}
34+
35+
public static IEnumerable<ITypeSymbol> GetBaseTypesAndThis(this ITypeSymbol type) {
36+
var current = type;
37+
38+
while (current != null) {
39+
yield return current;
40+
41+
current = current.BaseType;
42+
}
43+
}
44+
}

gen/SourceGenerator/ImmutableGenerator.cs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,6 @@
1313
// limitations under the License.
1414
//
1515

16-
using System.Text;
17-
using Microsoft.CodeAnalysis;
18-
using Microsoft.CodeAnalysis.CSharp;
19-
using Microsoft.CodeAnalysis.CSharp.Syntax;
20-
using Microsoft.CodeAnalysis.Text;
21-
2216
namespace SourceGenerator;
2317

2418
[Generator]
@@ -28,10 +22,7 @@ public void Initialize(GeneratorInitializationContext context) { }
2822
public void Execute(GeneratorExecutionContext context) {
2923
var compilation = context.Compilation;
3024

31-
var mutableClasses = compilation.SyntaxTrees
32-
.Select(tree => compilation.GetSemanticModel(tree))
33-
.SelectMany(model => model.SyntaxTree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>())
34-
.Where(syntax => syntax.AttributeLists.Any(list => list.Attributes.Any(attr => attr.Name.ToString() == "GenerateImmutable")));
25+
var mutableClasses = compilation.FindAnnotatedClass("GenerateImmutable", strict: true);
3526

3627
foreach (var mutableClass in mutableClasses) {
3728
var immutableClass = GenerateImmutableClass(mutableClass, compilation);
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (c) .NET Foundation and Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace SourceGenerator;
16+
17+
[Generator]
18+
public class InheritedCloneGenerator : ISourceGenerator {
19+
const string AttributeName = "GenerateClone";
20+
21+
public void Initialize(GeneratorInitializationContext context) { }
22+
23+
public void Execute(GeneratorExecutionContext context) {
24+
var compilation = context.Compilation;
25+
26+
var candidates = compilation.FindAnnotatedClass(AttributeName, false);
27+
28+
foreach (var candidate in candidates) {
29+
var semanticModel = compilation.GetSemanticModel(candidate.SyntaxTree);
30+
var genericClassSymbol = semanticModel.GetDeclaredSymbol(candidate);
31+
if (genericClassSymbol == null) continue;
32+
33+
// Get the method name from the attribute Name argument
34+
var attributeData = genericClassSymbol.GetAttributes().FirstOrDefault(a => a.AttributeClass?.Name == $"{AttributeName}Attribute");
35+
var methodName = (string)attributeData.NamedArguments.FirstOrDefault(arg => arg.Key == "Name").Value.Value;
36+
37+
// Get the generic argument type where properties need to be copied from
38+
var attributeSyntax = candidate.AttributeLists
39+
.SelectMany(l => l.Attributes)
40+
.FirstOrDefault(a => a.Name.ToString().StartsWith(AttributeName));
41+
if (attributeSyntax == null) continue; // This should never happen
42+
43+
var typeArgumentSyntax = ((GenericNameSyntax)attributeSyntax.Name).TypeArgumentList.Arguments[0];
44+
var typeSymbol = (INamedTypeSymbol)semanticModel.GetSymbolInfo(typeArgumentSyntax).Symbol;
45+
46+
var code = GenerateMethod(candidate, genericClassSymbol, typeSymbol, methodName);
47+
context.AddSource($"{genericClassSymbol.Name}.Clone.g.cs", SourceText.From(code, Encoding.UTF8));
48+
}
49+
}
50+
51+
static string GenerateMethod(
52+
TypeDeclarationSyntax classToExtendSyntax,
53+
INamedTypeSymbol classToExtendSymbol,
54+
INamedTypeSymbol classToClone,
55+
string methodName
56+
) {
57+
var namespaceName = classToExtendSymbol.ContainingNamespace.ToDisplayString();
58+
var className = classToExtendSyntax.Identifier.Text;
59+
var genericTypeParameters = string.Join(", ", classToExtendSymbol.TypeParameters.Select(tp => tp.Name));
60+
var classDeclaration = classToExtendSymbol.TypeParameters.Length > 0 ? $"{className}<{genericTypeParameters}>" : className;
61+
62+
var all = classToClone.GetBaseTypesAndThis();
63+
var props = all.SelectMany(x => x.GetMembers().OfType<IPropertySymbol>()).ToArray();
64+
var usings = classToExtendSyntax.SyntaxTree.GetCompilationUnitRoot().Usings.Select(u => u.ToString());
65+
66+
var constructorParams = classToExtendSymbol.Constructors.First().Parameters.ToArray();
67+
var constructorArgs = string.Join(", ", constructorParams.Select(p => $"original.{GetPropertyName(p.Name, props)}"));
68+
var constructorParamNames = constructorParams.Select(p => p.Name).ToArray();
69+
70+
var properties = props
71+
// ReSharper disable once PossibleUnintendedLinearSearchInSet
72+
.Where(prop => !constructorParamNames.Contains(prop.Name, StringComparer.OrdinalIgnoreCase) && prop.SetMethod != null)
73+
.Select(prop => $" {prop.Name} = original.{prop.Name},")
74+
.ToArray();
75+
76+
const string template = """
77+
{Usings}
78+
79+
namespace {Namespace};
80+
81+
public partial class {ClassDeclaration} {
82+
public static {ClassDeclaration} {MethodName}({OriginalClassName} original)
83+
=> new {ClassDeclaration}({ConstructorArgs}) {
84+
{Properties}
85+
};
86+
}
87+
""";
88+
89+
var code = template
90+
.Replace("{Usings}", string.Join("\n", usings))
91+
.Replace("{Namespace}", namespaceName)
92+
.Replace("{ClassDeclaration}", classDeclaration)
93+
.Replace("{OriginalClassName}", classToClone.Name)
94+
.Replace("{MethodName}", methodName)
95+
.Replace("{ConstructorArgs}", constructorArgs)
96+
.Replace("{Properties}", string.Join("\n", properties).TrimEnd(','));
97+
98+
return code;
99+
100+
static string GetPropertyName(string parameterName, IPropertySymbol[] properties) {
101+
var property = properties.FirstOrDefault(p => string.Equals(p.Name, parameterName, StringComparison.OrdinalIgnoreCase));
102+
return property?.Name ?? parameterName;
103+
}
104+
}
105+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"$schema": "http://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"Generators": {
5+
"commandName": "DebugRoslynComponent",
6+
"targetProject": "../../src/RestSharp/RestSharp.csproj"
7+
}
8+
}
9+
}

gen/SourceGenerator/SourceGenerator.csproj

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,18 @@
99
<IsPackable>false</IsPackable>
1010
</PropertyGroup>
1111
<ItemGroup>
12-
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="All" />
13-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="All" />
12+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="All"/>
13+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="All"/>
1414
</ItemGroup>
1515
<ItemGroup>
16-
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
16+
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false"/>
17+
</ItemGroup>
18+
<ItemGroup>
19+
20+
<Using Include="System.Text"/>
21+
<Using Include="Microsoft.CodeAnalysis"/>
22+
<Using Include="Microsoft.CodeAnalysis.CSharp"/>
23+
<Using Include="Microsoft.CodeAnalysis.CSharp.Syntax"/>
24+
<Using Include="Microsoft.CodeAnalysis.Text"/>
1725
</ItemGroup>
1826
</Project>

src/RestSharp/Extensions/GenerateImmutableAttribute.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,10 @@ namespace RestSharp.Extensions;
1818
[AttributeUsage(AttributeTargets.Class)]
1919
class GenerateImmutableAttribute : Attribute;
2020

21+
[AttributeUsage(AttributeTargets.Class)]
22+
class GenerateCloneAttribute<T> : Attribute where T : class {
23+
public string? Name { get; set; }
24+
};
25+
2126
[AttributeUsage(AttributeTargets.Property)]
2227
class Exclude : Attribute;

src/RestSharp/Extensions/HttpResponseExtensions.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,12 @@ static class HttpResponseExtensions {
2727
: new HttpRequestException($"Request failed with status code {httpResponse.StatusCode}");
2828
#endif
2929

30-
public static string GetResponseString(this HttpResponseMessage response, byte[] bytes, Encoding clientEncoding) {
30+
public static async Task<string> GetResponseString(this HttpResponseMessage response, byte[] bytes, Encoding clientEncoding) {
3131
var encodingString = response.Content.Headers.ContentType?.CharSet;
3232
var encoding = encodingString != null ? TryGetEncoding(encodingString) : clientEncoding;
3333

3434
using var reader = new StreamReader(new MemoryStream(bytes), encoding);
35-
return reader.ReadToEnd();
36-
35+
return await reader.ReadToEndAsync();
3736
Encoding TryGetEncoding(string es) {
3837
try {
3938
return Encoding.GetEncoding(es);
@@ -69,4 +68,4 @@ Encoding TryGetEncoding(string es) {
6968
return original == null ? null : streamWriter(original);
7069
}
7170
}
72-
}
71+
}

src/RestSharp/Response/RestResponse.cs

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,34 +24,13 @@ namespace RestSharp;
2424
/// Container for data sent back from API including deserialized data
2525
/// </summary>
2626
/// <typeparam name="T">Type of data to deserialize to</typeparam>
27-
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + "()}")]
28-
public class RestResponse<T>(RestRequest request) : RestResponse(request) {
27+
[GenerateClone<RestResponse>(Name = "FromResponse")]
28+
[DebuggerDisplay($"{{{nameof(DebuggerDisplay)}()}}")]
29+
public partial class RestResponse<T>(RestRequest request) : RestResponse(request) {
2930
/// <summary>
3031
/// Deserialized entity data
3132
/// </summary>
3233
public T? Data { get; set; }
33-
34-
public static RestResponse<T> FromResponse(RestResponse response)
35-
=> new(response.Request) {
36-
Content = response.Content,
37-
ContentEncoding = response.ContentEncoding,
38-
ContentHeaders = response.ContentHeaders,
39-
ContentLength = response.ContentLength,
40-
ContentType = response.ContentType,
41-
Cookies = response.Cookies,
42-
ErrorException = response.ErrorException,
43-
ErrorMessage = response.ErrorMessage,
44-
Headers = response.Headers,
45-
IsSuccessStatusCode = response.IsSuccessStatusCode,
46-
RawBytes = response.RawBytes,
47-
ResponseStatus = response.ResponseStatus,
48-
ResponseUri = response.ResponseUri,
49-
RootElement = response.RootElement,
50-
Server = response.Server,
51-
StatusCode = response.StatusCode,
52-
StatusDescription = response.StatusDescription,
53-
Version = response.Version
54-
};
5534
}
5635

5736
/// <summary>
@@ -77,7 +56,7 @@ async Task<RestResponse> GetDefaultResponse() {
7756
#endif
7857

7958
var bytes = stream == null ? null : await stream.ReadAsBytes(cancellationToken).ConfigureAwait(false);
80-
var content = bytes == null ? null : httpResponse.GetResponseString(bytes, encoding);
59+
var content = bytes == null ? null : await httpResponse.GetResponseString(bytes, encoding);
8160

8261
return new RestResponse(request) {
8362
Content = content,

src/RestSharp/Response/RestResponseBase.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,13 @@ protected RestResponseBase(RestRequest request) {
6666
public HttpStatusCode StatusCode { get; set; }
6767

6868
/// <summary>
69-
/// Whether or not the HTTP response status code indicates success
69+
/// Whether the HTTP response status code indicates success
7070
/// </summary>
7171
public bool IsSuccessStatusCode { get; set; }
7272

7373
/// <summary>
74-
/// Whether or not the HTTP response status code indicates success and no other error occurred (deserialization, timeout, ...)
74+
/// Whether the HTTP response status code indicates success and no other error occurred
75+
/// (deserialization, timeout, ...)
7576
/// </summary>
7677
public bool IsSuccessful => IsSuccessStatusCode && ResponseStatus == ResponseStatus.Completed;
7778

0 commit comments

Comments
 (0)