-
Notifications
You must be signed in to change notification settings - Fork 308
Refactor XmlDocProvider
#7904
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
base: main
Are you sure you want to change the base?
Refactor XmlDocProvider
#7904
Changes from all commits
eb58a10
8beb435
7869f42
9dc5f16
b217788
1e97044
104272d
de306a0
6687b15
f30be10
e59bd57
a970463
9941601
8ba71e2
351a482
77d8bc7
935f54f
368e0f7
77ab98e
0ac4a16
5e90929
c87ab12
590f81e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using System.Linq; | ||
using Microsoft.TypeSpec.Generator.ClientModel.Providers; | ||
using Microsoft.TypeSpec.Generator.Input; | ||
using Microsoft.TypeSpec.Generator.Providers; | ||
using Microsoft.TypeSpec.Generator.Tests.Common; | ||
using NUnit.Framework; | ||
|
||
namespace Microsoft.TypeSpec.Generator.ClientModel.Tests.Providers.ClientProviders; | ||
|
||
public class ClientProviderVisitorsTests | ||
{ | ||
private const string TestClientName = "TestClient"; | ||
private static readonly InputClient _testClient = InputFactory.Client(TestClientName, | ||
methods: [ | ||
InputFactory.BasicServiceMethod( | ||
"Operation", | ||
InputFactory.Operation( | ||
"Operation", | ||
parameters: | ||
[ | ||
InputFactory.Parameter( | ||
"queryParam", | ||
InputPrimitiveType.String, | ||
isRequired: true, | ||
location: InputRequestLocation.Query) | ||
]))]); | ||
|
||
[Test] | ||
public void ValidateXmlDocShouldChangeFromVisitors() | ||
{ | ||
MockHelpers.LoadMockGenerator( | ||
createClientCore: inputClient => new MockClientProvider(inputClient), | ||
clients: () => [_testClient], | ||
includeXmlDocs: true | ||
); | ||
var testVisitor = new TestVisitor(); | ||
ScmCodeModelGenerator.Instance.AddVisitor(testVisitor); | ||
|
||
// visit the library | ||
testVisitor.DoVisitLibrary(CodeModelGenerator.Instance.OutputLibrary); | ||
|
||
// check if the parameter names in xml docs are changed accordingly | ||
// find the client in outputlibrary | ||
var client = ScmCodeModelGenerator.Instance.OutputLibrary.TypeProviders.OfType<ClientProvider>().FirstOrDefault()!; | ||
Assert.IsNotNull(client); | ||
var writer = ScmCodeModelGenerator.Instance.GetWriter(client); | ||
var file = writer.Write(); | ||
|
||
Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); | ||
} | ||
|
||
private class TestVisitor : ScmLibraryVisitor | ||
{ | ||
public void DoVisitLibrary(OutputLibrary library) | ||
{ | ||
VisitLibrary(library); | ||
} | ||
|
||
protected internal override ScmMethodProvider? VisitMethod(ScmMethodProvider method) | ||
{ | ||
// modify the parameter names in-place | ||
foreach (var parameter in method.Signature.Parameters) | ||
{ | ||
if (parameter.Name == "queryParam") | ||
{ | ||
// modify the parameter name | ||
parameter.Update(name: "queryParam_modified"); | ||
} | ||
} | ||
return method; | ||
} | ||
} | ||
|
||
private class MockClientProvider : ClientProvider | ||
{ | ||
public MockClientProvider(InputClient client) : base(client) | ||
{ } | ||
|
||
// ignore all the ctors to make the output more clear | ||
protected override ConstructorProvider[] BuildConstructors() => []; | ||
|
||
// ignore all the fields to make the output more clear | ||
protected override FieldProvider[] BuildFields() => []; | ||
|
||
protected override PropertyProvider[] BuildProperties() => []; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// <auto-generated/> | ||
|
||
#nullable disable | ||
|
||
using System; | ||
using System.ClientModel; | ||
using System.ClientModel.Primitives; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace Sample | ||
{ | ||
/// <summary> TestClient description. </summary> | ||
public partial class TestClient | ||
{ | ||
/// <summary> | ||
/// [Protocol Method] Operation description | ||
/// <list type="bullet"> | ||
/// <item> | ||
/// <description> This <see href="https://aka.ms/azsdk/net/protocol-methods">protocol method</see> allows explicit creation of the request and processing of the response for advanced scenarios. </description> | ||
/// </item> | ||
/// </list> | ||
/// </summary> | ||
/// <param name="queryParam_modified"> queryParam description. </param> | ||
/// <param name="options"> The request options, which can override default behaviors of the client pipeline on a per-call basis. </param> | ||
/// <exception cref="global::System.ArgumentNullException"> <paramref name="queryParam_modified"/> is null. </exception> | ||
/// <exception cref="global::System.ArgumentException"> <paramref name="queryParam_modified"/> is an empty string, and was expected to be non-empty. </exception> | ||
/// <exception cref="global::System.ClientModel.ClientResultException"> Service returned a non-success status code. </exception> | ||
/// <returns> The response returned from the service. </returns> | ||
public virtual global::System.ClientModel.ClientResult Operation(string queryParam_modified, global::System.ClientModel.Primitives.RequestOptions options) | ||
{ | ||
global::Sample.Argument.AssertNotNullOrEmpty(queryParam_modified, nameof(queryParam_modified)); | ||
|
||
using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateOperationRequest(queryParam_modified, options); | ||
return global::System.ClientModel.ClientResult.FromResponse(Pipeline.ProcessMessage(message, options)); | ||
} | ||
|
||
/// <summary> | ||
/// [Protocol Method] Operation description | ||
/// <list type="bullet"> | ||
/// <item> | ||
/// <description> This <see href="https://aka.ms/azsdk/net/protocol-methods">protocol method</see> allows explicit creation of the request and processing of the response for advanced scenarios. </description> | ||
/// </item> | ||
/// </list> | ||
/// </summary> | ||
/// <param name="queryParam_modified"> queryParam description. </param> | ||
/// <param name="options"> The request options, which can override default behaviors of the client pipeline on a per-call basis. </param> | ||
/// <exception cref="global::System.ArgumentNullException"> <paramref name="queryParam_modified"/> is null. </exception> | ||
/// <exception cref="global::System.ArgumentException"> <paramref name="queryParam_modified"/> is an empty string, and was expected to be non-empty. </exception> | ||
/// <exception cref="global::System.ClientModel.ClientResultException"> Service returned a non-success status code. </exception> | ||
/// <returns> The response returned from the service. </returns> | ||
public virtual async global::System.Threading.Tasks.Task<global::System.ClientModel.ClientResult> OperationAsync(string queryParam_modified, global::System.ClientModel.Primitives.RequestOptions options) | ||
{ | ||
global::Sample.Argument.AssertNotNullOrEmpty(queryParam_modified, nameof(queryParam_modified)); | ||
|
||
using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateOperationRequest(queryParam_modified, options); | ||
return global::System.ClientModel.ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false)); | ||
} | ||
|
||
/// <summary> Operation description. </summary> | ||
/// <param name="cancellationToken"> The cancellation token that can be used to cancel the operation. </param> | ||
/// <exception cref="global::System.ClientModel.ClientResultException"> Service returned a non-success status code. </exception> | ||
public virtual global::System.ClientModel.ClientResult Operation(global::System.Threading.CancellationToken cancellationToken = default) | ||
{ | ||
return this.Operation(cancellationToken.CanBeCanceled ? new global::System.ClientModel.Primitives.RequestOptions { CancellationToken = cancellationToken } : null); | ||
} | ||
|
||
/// <summary> Operation description. </summary> | ||
/// <param name="cancellationToken"> The cancellation token that can be used to cancel the operation. </param> | ||
/// <exception cref="global::System.ClientModel.ClientResultException"> Service returned a non-success status code. </exception> | ||
public virtual async global::System.Threading.Tasks.Task<global::System.ClientModel.ClientResult> OperationAsync(global::System.Threading.CancellationToken cancellationToken = default) | ||
{ | ||
return await this.OperationAsync(cancellationToken.CanBeCanceled ? new global::System.ClientModel.Primitives.RequestOptions { CancellationToken = cancellationToken } : null).ConfigureAwait(false); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -89,15 +89,9 @@ protected override MethodProvider[] BuildMethods() | |
$"A new {modelProvider.Type:C} instance for mocking.", | ||
GetParameters(modelProvider, fullConstructor)); | ||
|
||
var parameters = new List<XmlDocParamStatement>(signature.Parameters.Count); | ||
foreach (var param in signature.Parameters) | ||
{ | ||
parameters.Add(new XmlDocParamStatement(param)); | ||
} | ||
|
||
var docs = new XmlDocProvider( | ||
modelProvider.XmlDocs.Summary, | ||
parameters, | ||
signature.Parameters, | ||
returns: new XmlDocReturnsStatement($"A new {modelProvider.Type:C} instance for mocking.")); | ||
|
||
MethodBodyStatement statements = ConstructMethodBody(signature, typeToInstantiate); | ||
|
@@ -271,7 +265,7 @@ private static bool TryBuildMethodArgumentsForOverload( | |
return false; | ||
} | ||
|
||
var currentParameters = currentMethod.Parameters.ToHashSet(); | ||
var currentParameters = currentMethod.Parameters.ToHashSet(ParameterProvider.EqualityByNameAndType); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since in this context, we are trying to compare the parameters by their names and types, we used a comparer here to do that, and its default behavior in a dictionary is by reference equality. |
||
foreach (var parameter in previousMethod.Parameters) | ||
{ | ||
if (!currentParameters.Contains(parameter)) | ||
|
@@ -281,7 +275,7 @@ private static bool TryBuildMethodArgumentsForOverload( | |
} | ||
|
||
// Build the arguments for the overload | ||
var previousParameters = previousMethod.Parameters.ToHashSet(); | ||
var previousParameters = previousMethod.Parameters.ToHashSet(ParameterProvider.EqualityByNameAndType); | ||
List<ValueExpression> arguments = new(currentMethodParameterCount); | ||
|
||
foreach (var parameter in currentMethod.Parameters) | ||
|
@@ -470,7 +464,7 @@ private static bool ContainsSameParameters(MethodSignature method1, MethodSignat | |
return false; | ||
} | ||
|
||
HashSet<ParameterProvider> method1Parameters = [.. method1.Parameters]; | ||
HashSet<ParameterProvider> method1Parameters = method1.Parameters.ToHashSet(ParameterProvider.EqualityByNameAndType); | ||
foreach (var method2Param in method2.Parameters) | ||
{ | ||
if (!method1Parameters.Contains(method2Param)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ | |
using System.Diagnostics; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Linq; | ||
using System.Runtime.CompilerServices; | ||
using Microsoft.TypeSpec.Generator.Expressions; | ||
using Microsoft.TypeSpec.Generator.Input; | ||
using Microsoft.TypeSpec.Generator.Input.Extensions; | ||
|
@@ -158,13 +159,10 @@ public bool Equals(ParameterProvider? y) | |
|
||
public override int GetHashCode() | ||
{ | ||
return GetHashCode(this); | ||
} | ||
|
||
private int GetHashCode([DisallowNull] ParameterProvider obj) | ||
{ | ||
// remove type as part of the hash code generation as the type might have changes between versions | ||
return HashCode.Combine(obj.Name); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i change this because the
|
||
// we must override the GetHashCode method because we overrode Equals | ||
// but we really do not want to change the behavior of GetHashCode to keep it immutable per instance | ||
// therefore we use this implementation | ||
return RuntimeHelpers.GetHashCode(this); // gets the hash code based on object reference | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does this solve the problem? If we are getting hashcode based on object reference, won't two different instances end up in different buckets and make the look up fail? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should just not override Equals here, and force callers to use the new Comparer for use in sets. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. :( I tried to remove that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So this is to make the default look up behavior to be by object reference. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we want to use reference semantics, we should just not override Equals/GetHashCode. By overriding these methods, we are opting out of reference semantics. |
||
} | ||
|
||
private string GetDebuggerDisplay() | ||
|
@@ -333,5 +331,27 @@ public void Update( | |
Validation = validation.Value; | ||
} | ||
} | ||
|
||
internal static IEqualityComparer<ParameterProvider> EqualityByNameAndType = new ParameterProviderEqualityComparer(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: NameAndTypeComparer |
||
|
||
private struct ParameterProviderEqualityComparer : IEqualityComparer<ParameterProvider> | ||
{ | ||
public bool Equals(ParameterProvider? x, ParameterProvider? y) | ||
{ | ||
if (ReferenceEquals(x, y)) | ||
{ | ||
return true; | ||
} | ||
if (x is null || y is null) | ||
{ | ||
return false; | ||
} | ||
return x.Equals(y); | ||
} | ||
public int GetHashCode([DisallowNull] ParameterProvider obj) | ||
{ | ||
return HashCode.Combine(obj.Name); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I don't think we use file scoped namespaces anywhere in the codebase.