Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getClientNamespace,
getClientOptions,
getHttpOperationParameter,
getParamAlias,
isHttpMetadata,
SdkBodyParameter,
SdkBuiltInKinds,
Expand Down Expand Up @@ -562,6 +563,8 @@ export function fromMethodParameter(

const parameterType = fromSdkType(sdkContext, p.type, p, namespace);

const paramAlias = p.__raw ? getParamAlias(sdkContext, p.__raw) : undefined;

retVar = {
kind: "method",
name: p.name,
Expand All @@ -578,6 +581,7 @@ export function fromMethodParameter(
readOnly: isReadOnly(p),
access: p.access,
decorators: p.decorators,
...(paramAlias && { paramAlias }),
};

sdkContext.__typeCache.updateSdkMethodParameterReferences(p, retVar);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ export interface InputMethodParameter extends InputPropertyTypeBase {
location: RequestLocation;
scope: InputParameterScope;
serializedName: string;
paramAlias?: string;
}

export interface InputQueryParameter extends InputPropertyTypeBase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TestHost } from "@typespec/compiler/testing";
import { ok, strictEqual } from "assert";
import { beforeEach, describe, it, vi } from "vitest";
import { createModel } from "../../src/lib/client-model-builder.js";
import { InputMethodParameter } from "../../src/type/input-type.js";
import {
createCSharpSdkContext,
createEmitterContext,
Expand Down Expand Up @@ -123,4 +124,82 @@ describe("ClientInitialization", () => {
ok("initializedBy" in childClient, "Child client should have initializedBy field");
}
});

it("should include paramAlias on client parameters when @paramAlias is used", async () => {
const program = await typeSpecCompile(
`
@service(#{
title: "Test Service",
})
@server("https://example.com", "Test endpoint")
namespace TestService;

op upload(@path blobName: string): void;

model TestServiceClientOptions {
@paramAlias("blobName")
blob: string;
}

@@clientInitialization(TestService, {parameters: TestServiceClientOptions});
`,
runner,
{ IsNamespaceNeeded: false, IsTCGCNeeded: true },
);

const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const root = createModel(sdkContext);

const client = root.clients[0];
ok(client, "Client should exist");
ok(client.parameters, "Client should have parameters");

// Find the method parameter with paramAlias
const blobParam = client.parameters.find((p) => p.kind === "method" && p.name === "blob") as
| InputMethodParameter
| undefined;
ok(blobParam, "Should have a 'blob' method parameter");
strictEqual(blobParam.paramAlias, "blobName", "paramAlias should be 'blobName'");
});

it("should not include paramAlias when @paramAlias is not used", async () => {
const program = await typeSpecCompile(
`
@service(#{
title: "Test Service",
})
@server("https://example.com", "Test endpoint")
namespace TestService;

op upload(@path blobName: string): void;

model TestServiceClientOptions {
blobName: string;
}

@@clientInitialization(TestService, {parameters: TestServiceClientOptions});
`,
runner,
{ IsNamespaceNeeded: false, IsTCGCNeeded: true },
);

const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const root = createModel(sdkContext);

const client = root.clients[0];
ok(client, "Client should exist");
ok(client.parameters, "Client should have parameters");

const blobParam = client.parameters.find(
(p) => p.kind === "method" && p.name === "blobName",
) as InputMethodParameter | undefined;
ok(blobParam, "Should have a 'blobName' method parameter");
strictEqual(
blobParam.paramAlias,
undefined,
"paramAlias should be undefined when @paramAlias is not used",
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ public ClientProvider(InputClient inputClient)
_additionalClientFields = new(BuildAdditionalClientFields);
_subClientInternalConstructorParams = new(GetSubClientInternalConstructorParameters);
_clientParameters = new(GetClientParameters);
_effectiveClientParamNames = new(() => GetEffectiveParameterNames(_inputClient.Parameters));
_subClients = new(GetSubClients);
_allClientParameters = GetAllClientParameters();
}
Expand Down Expand Up @@ -321,15 +322,36 @@ private IReadOnlyList<ParameterProvider> GetSubClientInternalConstructorParamete
/// </summary>
internal bool HasAccessorOnlyParameters(InputClient parentInputClient)
{
var parentParamNames = parentInputClient.Parameters
.Select(p => p.Name)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var parentParamNames = GetEffectiveParameterNames(parentInputClient.Parameters);

return _inputClient.Parameters
.Where(p => !p.IsApiVersion && !(p is InputEndpointParameter ep && ep.IsEndpoint))
.Any(p => !parentParamNames.Contains(p.Name));
.Any(p => !IsSupersededByClientParameter(p, parentParamNames));
}

/// <summary>
/// Builds a set of effective parameter names. When a parameter has a ParamAlias,
/// the alias is used instead of the parameter name.
/// </summary>
private static HashSet<string> GetEffectiveParameterNames(IReadOnlyList<InputParameter> parameters)
{
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var p in parameters)
{
if (p is InputMethodParameter { ParamAlias: string alias })
{
names.Add(alias);
}
else
{
names.Add(p.Name);
}
}
return names;
}

private Lazy<HashSet<string>> _effectiveClientParamNames;

private Lazy<IReadOnlyList<ParameterProvider>> _clientParameters;
internal IReadOnlyList<ParameterProvider> ClientParameters => _clientParameters.Value;
private IReadOnlyList<ParameterProvider> GetClientParameters()
Expand Down Expand Up @@ -1081,11 +1103,8 @@ protected override ScmMethodProvider[] BuildMethods()

// Identify subclient-specific parameters by comparing with the parent's input parameters.
// Parameters present on both parent and subclient are shared (sourced from parent fields/properties).
var parentInputParamNames = _inputClient.Parameters
.Select(p => p.Name)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var subClientSpecificParamNames = subClient._inputClient.Parameters
.Where(p => !parentInputParamNames.Contains(p.Name))
.Where(p => !IsSupersededByClientParameter(p, _effectiveClientParamNames.Value))
.Select(p => p.Name)
.ToHashSet(StringComparer.OrdinalIgnoreCase);

Expand Down Expand Up @@ -1435,10 +1454,17 @@ private IReadOnlyList<ClientProvider> GetSubClients()

private IReadOnlyList<InputParameter> GetAllClientParameters()
{
// Get all parameters from the client and its methods, deduplicating by SerializedName to handle renamed parameters
var clientParamNames = _effectiveClientParamNames.Value;

// Get all parameters from the client and its methods, deduplicating by SerializedName to handle renamed parameters.
// When @paramAlias is used (via @clientInitialization), an operation parameter may map
// to a client parameter via MethodParameterSegments or ParamAlias. Exclude such operation
// parameters since the client parameter supersedes them.
var parameters = _inputClient.Parameters.Concat(
_inputClient.Methods.SelectMany(m => m.Operation.Parameters)
.Where(p => p.Scope == InputParameterScope.Client)).DistinctBy(p => p.SerializedName ?? p.Name).ToArray();
.Where(p => p.Scope == InputParameterScope.Client)
.Where(p => !IsSupersededByClientParameter(p, clientParamNames)))
.DistinctBy(p => p.SerializedName ?? p.Name).ToArray();

foreach (var subClient in _subClients.Value)
{
Expand All @@ -1450,6 +1476,26 @@ private IReadOnlyList<InputParameter> GetAllClientParameters()
return parameters;
}

/// <summary>
/// Determines whether a parameter is superseded by an existing client parameter,
/// either by direct name match or via MethodParameterSegments.
/// </summary>
private static bool IsSupersededByClientParameter(InputParameter param, HashSet<string> clientParamNames)
{
if (clientParamNames.Contains(param.Name))
{
return true;
}

if (param.MethodParameterSegments is { Count: > 0 } segments &&
clientParamNames.Contains(segments[0].Name))
{
return true;
}

return false;
}

private FieldProvider BuildTokenCredentialScopesField(InputOAuth2Auth oauth2Auth, CSharpType tokenCredentialType)
{
return tokenCredentialType.Equals(ClientPipelineProvider.Instance.TokenCredentialType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,17 @@ private MethodBodyStatements BuildMessage(
paramMap[param.Name] = param;
}

// Register client parameters under their paramAlias names so that operation parameters
// (which use the original name) can find the corresponding client parameter.
foreach (var inputParam in _inputClient.Parameters)
{
if (inputParam is InputMethodParameter { ParamAlias: string alias } &&
paramMap.TryGetValue(inputParam.Name, out var aliasedParam))
{
paramMap[alias] = aliasedParam;
}
}

InputPagingServiceMethod? pagingServiceMethod = serviceMethod as InputPagingServiceMethod;
var uriBuilderType =
ScmCodeModelGenerator.Instance.TypeFactory.HttpRequestApi.ToExpression().UriBuilderType;
Expand Down Expand Up @@ -715,7 +726,8 @@ private void AddUriSegments(
/* when the parameter is in operation.uri, it is client parameter
* It is not operation parameter and not in inputParamHash list.
*/
var isClientParameter = ClientProvider.ClientParameters.Any(p => string.Equals(p.Name, paramName, StringComparison.OrdinalIgnoreCase));
var isClientParameter = ClientProvider.ClientParameters.Any(p => string.Equals(p.Name, paramName, StringComparison.OrdinalIgnoreCase))
|| _inputClient.Parameters.Any(p => p is InputMethodParameter { ParamAlias: string alias } && string.Equals(alias, paramName, StringComparison.OrdinalIgnoreCase));
CSharpType? type;
SerializationFormat? serializationFormat;
ValueExpression? valueExpression;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,45 @@ public void SubClientSummaryIsPopulatedWithDefaultDocs()
Assert.AreEqual("/// <summary> The Test sub-client. </summary>\n", client!.XmlDocs.Summary!.ToDisplayString());
}

[Test]
public void HasAccessorOnlyParameters_ReturnsFalse_WhenSubClientParamMatchesParentAlias()
{
// Parent has parameter "blobName" with paramAlias "name"
// Subclient has parameter "name"
// The alias should make it recognized as a shared parameter, not subclient-specific
var parentClient = InputFactory.Client(
"ParentClient",
parameters: [InputFactory.MethodParameter("blobName", InputPrimitiveType.String, isRequired: true, scope: InputParameterScope.Client, paramAlias: "name")]);
var subClient = InputFactory.Client(
"SubClient",
parent: parentClient,
parameters: [InputFactory.MethodParameter("name", InputPrimitiveType.String, isRequired: true, scope: InputParameterScope.Client)]);

MockHelpers.LoadMockGenerator(clients: () => [parentClient]);

var subClientProvider = new ClientProvider(subClient);
Assert.IsFalse(subClientProvider.HasAccessorOnlyParameters(parentClient));
}

[Test]
public void HasAccessorOnlyParameters_ReturnsTrue_WhenSubClientParamDoesNotMatchParentAlias()
{
// Parent has parameter "blobName" with paramAlias "name"
// Subclient has parameter "color" — not matching either parent name or alias
var parentClient = InputFactory.Client(
"ParentClient",
parameters: [InputFactory.MethodParameter("blobName", InputPrimitiveType.String, isRequired: true, scope: InputParameterScope.Client, paramAlias: "name")]);
var subClient = InputFactory.Client(
"SubClient",
parent: parentClient,
parameters: [InputFactory.MethodParameter("color", InputPrimitiveType.String, isRequired: true, scope: InputParameterScope.Client)]);

MockHelpers.LoadMockGenerator(clients: () => [parentClient]);

var subClientProvider = new ClientProvider(subClient);
Assert.IsTrue(subClientProvider.HasAccessorOnlyParameters(parentClient));
}

private class MockClientProvider : ClientProvider
{
private readonly string[] _expectedSubClientFactoryMethodNames;
Expand Down
Loading
Loading