Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
42dd88a
add acaornym naming for dates
jorgerangel-msft Aug 13, 2026
1582644
merge
jorgerangel-msft Aug 13, 2026
9b0729d
shorten exclusion list
jorgerangel-msft Aug 13, 2026
8bde832
fix: cover date time name exclusions
Copilot Aug 13, 2026
875ac14
fix: preserve normalized parameter names
Copilot Aug 14, 2026
aa00bd3
fix: address date naming review feedback
Copilot Aug 14, 2026
ea24102
fix: simplify date naming normalization
Copilot Aug 14, 2026
b02cbaf
fix: refine date naming tests
Copilot Aug 14, 2026
de464c1
fix: preserve custom back compat parameter names
Copilot Aug 14, 2026
f177ea1
fix: generalize back compat parameter name restoration
Copilot Aug 14, 2026
862f367
fix: preserve transformed back compat parameter names
Copilot Aug 14, 2026
06f7bd6
fix: simplify parameter name preservation
Copilot Aug 14, 2026
c87e8c0
Merge remote-tracking branch 'origin/main' into jorgerangel-msft-fix-…
Copilot Aug 14, 2026
8f36465
test: preserve XML date normalization assertion
Copilot Aug 17, 2026
87a9e43
fix: retain request content input metadata
Copilot Aug 17, 2026
0654a31
test: cover date name backcompat constructors
Copilot Aug 17, 2026
1a5e99d
test: restore back compat test fixtures
Copilot Aug 17, 2026
5c1aace
test: validate date suffix ctor tests with TestData
Copilot Aug 17, 2026
800ac03
Merge remote-tracking branch 'origin/main' into jorgerangel-msft-fix-…
Copilot Aug 17, 2026
8b0033b
Merge branch 'main' into jorgerangel-msft-fix-unbranded-date-suffixes
jorgerangel-msft Aug 18, 2026
28d2373
fix: normalize date suffix for all input parameters
Copilot Aug 18, 2026
df07338
Merge remote-tracking branch 'origin/main' into jorgerangel-msft-fix-…
Copilot Aug 18, 2026
e46c8f6
test: validate convenience method signature in TestData
Copilot Aug 18, 2026
3d558b1
Add back-compat regression test for date parameter forwarding in conv…
Copilot Aug 18, 2026
02be6df
Validate async overloads in date parameter back-compat regression test
Copilot Aug 18, 2026
357194c
Merge branch 'main' into jorgerangel-msft-fix-unbranded-date-suffixes
Copilot Aug 19, 2026
05e8a3f
fix: map noun prefixes to verb forms in date name normalization
Copilot Aug 19, 2026
f1aaabc
fixes
jorgerangel-msft Aug 20, 2026
c611539
Merge branch 'main' of https://github.com/microsoft/typespec into pr/…
jorgerangel-msft Aug 20, 2026
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 @@ -55,7 +55,7 @@ protected override void CompareModels(XmlAdvancedModel model, XmlAdvancedModel m
Assert.AreEqual(model.Metadata.Count, model2.Metadata.Count);

// Compare date/time and duration
Assert.AreEqual(model.CreatedAt, model2.CreatedAt);
Assert.AreEqual(model.CreatedOn, model2.CreatedOn);
Assert.AreEqual(model.Duration, model2.Duration);

// Compare enums
Expand Down Expand Up @@ -102,7 +102,7 @@ protected override void VerifyModel(XmlAdvancedModel model, string format)
Assert.AreEqual("value2", model.Metadata["key2"]);

// Verify date/time
Assert.AreEqual(new DateTimeOffset(2024, 1, 15, 10, 30, 0, TimeSpan.Zero), model.CreatedAt);
Assert.AreEqual(new DateTimeOffset(2024, 1, 15, 10, 30, 0, TimeSpan.Zero), model.CreatedOn);
Assert.AreEqual(new TimeSpan(1, 30, 0), model.Duration);

// Verify enums
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2805,6 +2805,57 @@ private static void AssertHasFields(TypeProvider provider, IReadOnlyList<Expecte
}
}

// Date parameter names are normalized (requestDate -> requestOn) on both the protocol and the
// convenience surface, so the previously published name is restored consistently for both and the
// convenience method forwards every argument positionally. When only one surface is normalized, the
// date argument is dropped (passed as null) and the remaining arguments are passed by name.
[Test]
public async Task BackCompatibility_DateParameterNameIsPreservedInConvenienceCall()
{
var dateType = new InputDateTimeType(
DateTimeKnownEncoding.Rfc7231,
"utcDateTime",
"TypeSpec.utcDateTime",
InputPrimitiveType.String);
var operation = InputFactory.Operation(
"TestMethod",
parameters:
[
InputFactory.HeaderParameter("requestDate", dateType),
InputFactory.HeaderParameter("ifMatch", InputPrimitiveType.String)
]);
var method = InputFactory.BasicServiceMethod(
"TestMethod",
operation,
parameters:
[
InputFactory.MethodParameter("requestDate", dateType, location: InputRequestLocation.Header),
InputFactory.MethodParameter("ifMatch", InputPrimitiveType.String, location: InputRequestLocation.Header)
]);
var client = InputFactory.Client(TestClientName, methods: [method]);

var generator = await MockHelpers.LoadMockGeneratorAsync(
clients: () => [client],
lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var clientProvider = generator.Object.OutputLibrary.TypeProviders.OfType<ClientProvider>().FirstOrDefault();
Assert.IsNotNull(clientProvider);
Assert.IsNotNull(clientProvider!.LastContractView);

clientProvider!.ProcessTypeForBackCompatibility();

using var writer = new CodeWriter();
foreach (var methodName in new[] { "TestMethod", "TestMethodAsync" })
{
writer.WriteMethod(clientProvider.Methods
.Single(m => m.Signature.Name == methodName && m is ScmMethodProvider { Kind: ScmMethodKind.Protocol }));
writer.WriteMethod(clientProvider.Methods
.Single(m => m.Signature.Name == methodName && m is ScmMethodProvider { Kind: ScmMethodKind.Convenience }));
}

Assert.AreEqual(Helpers.GetExpectedFromFile(), writer.ToString(false));
}

[Test]
public async Task BackCompatibility_ProtocolMethodParamOrderChanged()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
public virtual global::System.ClientModel.ClientResult TestMethod(global::System.DateTimeOffset? requestDate, string ifMatch, global::System.ClientModel.Primitives.RequestOptions options)
{
using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateTestMethodRequest(requestDate, ifMatch, options);
return global::System.ClientModel.ClientResult.FromResponse(Pipeline.ProcessMessage(message, options));
}
public virtual global::System.ClientModel.ClientResult TestMethod(global::System.DateTimeOffset? requestDate = default, string ifMatch = default, global::System.Threading.CancellationToken cancellationToken = default)
{
return this.TestMethod(requestDate, ifMatch, cancellationToken.ToRequestOptions());
}
public virtual async global::System.Threading.Tasks.Task<global::System.ClientModel.ClientResult> TestMethodAsync(global::System.DateTimeOffset? requestDate, string ifMatch, global::System.ClientModel.Primitives.RequestOptions options)
{
using global::System.ClientModel.Primitives.PipelineMessage message = this.CreateTestMethodRequest(requestDate, ifMatch, options);
return global::System.ClientModel.ClientResult.FromResponse(await Pipeline.ProcessMessageAsync(message, options).ConfigureAwait(false));
}
public virtual async global::System.Threading.Tasks.Task<global::System.ClientModel.ClientResult> TestMethodAsync(global::System.DateTimeOffset? requestDate = default, string ifMatch = default, global::System.Threading.CancellationToken cancellationToken = default)
{
return await this.TestMethodAsync(requestDate, ifMatch, cancellationToken.ToRequestOptions()).ConfigureAwait(false);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.ClientModel;
using System.ClientModel.Primitives;
using System.Threading.Tasks;

namespace Sample
{
public partial class TestClient
{
public virtual ClientResult TestMethod(DateTimeOffset? requestDate, string ifMatch, RequestOptions options)
{
throw new NotImplementedException();
}

public virtual Task<ClientResult> TestMethodAsync(DateTimeOffset? requestDate, string ifMatch, RequestOptions options)
{
throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ internal virtual void XmlModelWriteCore(global::System.Xml.XmlWriter writer, glo
throw new global::System.FormatException($"The model {nameof(global::Sample.Models.TestXmlModel)} does not support writing '{format}' format.");
}

if (global::Sample.Optional.IsDefined(Timestamp))
if (global::Sample.Optional.IsDefined(On))
{
writer.WriteStartElement("timestamp");
writer.WriteStringValue(Timestamp.Value, "O");
writer.WriteStringValue(On.Value, "O");
writer.WriteEndElement();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ public void XmlDeserializationHandlesDateTimeOffsetProperty()
Assert.IsNotNull(xmlDeserializationMethod);
var methodBody = xmlDeserializationMethod!.BodyStatements!.ToDisplayString();

Assert.IsTrue(methodBody.Contains("timestamp = child.GetDateTimeOffset(\"O\")"),
Comment thread
jorgerangel-msft marked this conversation as resolved.
Assert.IsTrue(methodBody.Contains("@on = child.GetDateTimeOffset(\"O\")"),
$"DateTimeOffset property should use child.GetDateTimeOffset(\"O\") with RFC3339 format. Actual:\n{methodBody}");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ public void XmlSerializationHandlesDateTimeOffsetProperty()
Assert.IsNotNull(xmlSerializationMethod);
var methodBody = xmlSerializationMethod!.BodyStatements!.ToDisplayString();

Assert.IsTrue(methodBody.Contains("WriteStringValue") && methodBody.Contains("Timestamp"),
Assert.IsTrue(methodBody.Contains("writer.WriteStringValue(On.Value, \"O\")"),
Comment thread
jorgerangel-msft marked this conversation as resolved.
$"DateTimeOffset property should be serialized with WriteStringValue. Actual:\n{methodBody}");
Comment thread
jorgerangel-msft marked this conversation as resolved.
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,53 @@ public async Task SpreadModelWithOptionalDictionaryIsNotNull()
Assert.AreEqual(Helpers.GetExpectedFromFile(), methodBody);
}

[Test]
public async Task ConvenienceMethodForwardsNormalizedDateParameter()
{
var dateType = new InputDateTimeType(
DateTimeKnownEncoding.Rfc7231,
"utcDateTime",
"TypeSpec.utcDateTime",
InputPrimitiveType.String);
var operation = InputFactory.Operation(
"GetThing",
parameters: [InputFactory.HeaderParameter("requestDate", dateType, isRequired: true)],
responses: [InputFactory.OperationResponse([204])]);
var serviceMethod = InputFactory.BasicServiceMethod(
"GetThing",
operation,
parameters:
[
InputFactory.MethodParameter(
"requestDate",
dateType,
isRequired: true,
location: InputRequestLocation.Header)
]);
var inputClient = InputFactory.Client("TestClient", methods: [serviceMethod]);
await MockHelpers.LoadMockGeneratorAsync(clients: () => [inputClient]);

var client = ScmCodeModelGenerator.Instance.TypeFactory.CreateClient(inputClient);
Assert.IsNotNull(client);
var methodCollection = new ScmMethodProviderCollection(serviceMethod, client!);

// Both the protocol and convenience methods use the normalized name so that the
// convenience method still forwards the value to the protocol method.
foreach (var method in methodCollection)
{
Assert.IsTrue(method.Signature.Parameters.Any(p => p.Name == "requestOn"));
}

var asyncConvenienceMethod = methodCollection.Single(m =>
m.Signature.Name.EndsWith("Async")
&& m.Signature.Parameters.Any(p => p.Type.Equals(typeof(CancellationToken))));
using var writer = new CodeWriter();
writer.WriteMethod(asyncConvenienceMethod);
Assert.AreEqual(
Helpers.GetExpectedFromFile(),
writer.ToString(false));
}

[Test]
public void ListMethodWithNoPaging()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public virtual async global::System.Threading.Tasks.Task<global::System.ClientModel.ClientResult> GetThingAsync(global::System.DateTimeOffset requestOn, global::System.Threading.CancellationToken cancellationToken = default)
{
return await this.GetThingAsync(requestOn, cancellationToken.ToRequestOptions()).ConfigureAwait(false);
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ public sealed class ParameterProvider : IEquatable<ParameterProvider>
public ParameterProvider(InputParameter inputParameter)
{
InputParameter = inputParameter;
Name = inputParameter.Name;
Name = !inputParameter.IsExactName && inputParameter.Type.IsDateTimeInputType()
? inputParameter.Name.NormalizeDateTimeSuffix()
: inputParameter.Name;
Description = DocHelpers.GetFormattableDescription(inputParameter.Summary, inputParameter.Doc) ?? FormattableStringHelpers.Empty;
var type = CodeModelGenerator.Instance.TypeFactory.CreateCSharpType(inputParameter.Type) ?? throw new InvalidOperationException($"Failed to create CSharpType for {inputParameter.Type}");
if (!inputParameter.IsRequired)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ private PropertyProvider(InputProperty inputProperty, CSharpType propertyType, T
(lastContractProperties is null ||
!lastContractProperties.Any(p => p.Name == legacyName)))
{
identifierName = identifierName.NormalizeCSharpAcronyms();
identifierName = identifierName
.NormalizeCSharpAcronyms(inputProperty.Type.IsDateTimeInputType());
}
Name = identifierName == enclosingType.Name
? $"{identifierName}Property"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Microsoft.TypeSpec.Generator.Input;

namespace Microsoft.TypeSpec.Generator.Utilities
{
Expand All @@ -20,8 +22,9 @@ private static readonly (string Source, string Replacement)[] _acronymRenamingRu
("Os", "OS")
Comment thread
jorgerangel-msft marked this conversation as resolved.
];

public static string NormalizeCSharpAcronyms(this string name)
public static string NormalizeCSharpAcronyms(this string name, bool normalizeDateTimeSuffix = false)
Comment thread
jorgerangel-msft marked this conversation as resolved.
{
name = normalizeDateTimeSuffix ? name.NormalizeDateTimeSuffix() : name;
StringBuilder? normalizedName = null;
int segmentStart = 0;
for (int index = 0; index < name.Length - 1; index++)
Expand Down Expand Up @@ -57,6 +60,112 @@ public static string NormalizeCSharpAcronyms(this string name)
return normalizedName.ToString();
}

public static string NormalizeDateTimeSuffix(this string name)
{
if (DateTimeNameRules.HasExcludedComponent(name))
{
return name;
}

var suffixLength = DateTimeNameRules.GetSuffixLength(name);
if (suffixLength == 0)
{
return name;
}

var prefix = DateTimeNameRules.ToVerbForm(name[..^suffixLength]);
var onSuffix = prefix.Length == 0 && char.IsLower(name[0])
? DateTimeNameRules.LowercaseOnSuffix
: DateTimeNameRules.OnSuffix;
return prefix + onSuffix;
}

private static class DateTimeNameRules
{
private const string AtSuffix = "At";
private const string DateSuffix = "Date";
private const string DateTimeSuffix = "DateTime";
private const string FromName = "From";
internal const string LowercaseOnSuffix = "on";
internal const string OnSuffix = "On";
private const string PointInTimeName = "PointInTime";
private const string StatusTimeStampName = "StatusTimeStamp";
private const string StatusTimestampName = "StatusTimestamp";
private const string TimeStampSuffix = "TimeStamp";
private const string TimeSuffix = "Time";
private const string TimestampSuffix = "Timestamp";
private const string ToName = "To";

// Complete prefixes that read better as verbs when combined with the "On" suffix.
private static readonly Dictionary<string, string> _nounToVerbMap = new(StringComparer.OrdinalIgnoreCase)
{
["Creation"] = "Created",
["Deletion"] = "Deleted",
["Expiration"] = "Expire",
["Modification"] = "Modified"
};

internal static string ToVerbForm(string prefix)
{
if (!_nounToVerbMap.TryGetValue(prefix, out var verb))
{
return prefix;
}

return char.IsLower(prefix[0])
? char.ToLowerInvariant(verb[0]) + verb[1..]
: verb;
}

internal static bool HasExcludedComponent(string name)
{
// StatusTimestamp is a semantic compound. Keep the exclusion exact so names such as
// LastSyncTimestamp continue to normalize.
return name.StartsWith(FromName, StringComparison.OrdinalIgnoreCase) ||
name.StartsWith(ToName, StringComparison.OrdinalIgnoreCase) ||
name.EndsWith(PointInTimeName, StringComparison.OrdinalIgnoreCase) ||
name.Equals(StatusTimestampName, StringComparison.OrdinalIgnoreCase) ||
name.Equals(StatusTimeStampName, StringComparison.OrdinalIgnoreCase);
}

internal static int GetSuffixLength(string name)
{
if (name.EndsWith(TimestampSuffix, StringComparison.Ordinal) ||
name.EndsWith(TimeStampSuffix, StringComparison.Ordinal) ||
name.Equals(TimestampSuffix, StringComparison.OrdinalIgnoreCase))
{
return TimestampSuffix.Length;
}

if (name.Length > DateTimeSuffix.Length && name.EndsWith(DateTimeSuffix, StringComparison.Ordinal))
{
return DateTimeSuffix.Length;
}

if (name.Length > TimeSuffix.Length && name.EndsWith(TimeSuffix, StringComparison.Ordinal))
{
return TimeSuffix.Length;
}

if (name.Equals(DateSuffix, StringComparison.OrdinalIgnoreCase) ||
name.EndsWith(DateSuffix, StringComparison.Ordinal))
{
return DateSuffix.Length;
}

return name.Length > AtSuffix.Length && name.EndsWith(AtSuffix, StringComparison.Ordinal)
? AtSuffix.Length
: 0;
}
}

public static bool IsDateTimeInputType(this InputType inputType) => inputType switch
{
InputDateTimeType => true,
InputPrimitiveType { Kind: InputPrimitiveTypeKind.PlainDate } => true,
InputNullableType nullableType => IsDateTimeInputType(nullableType.Type),
_ => false
};
[return: NotNullIfNotNull(nameof(name))]
public static string? NormalizeCSharpUrlSuffix(this string? name)
=> !string.IsNullOrEmpty(name) && name.EndsWith("Url", StringComparison.Ordinal)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Linq;
using Microsoft.TypeSpec.Generator.EmitterRpc;
using Microsoft.TypeSpec.Generator.Expressions;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Input.Extensions;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Providers;
Expand Down Expand Up @@ -189,13 +190,9 @@ public static void RestorePreviousParameterNames(
string? preservedName = null;

var inputParameter = parameter.InputParameter;
if (inputParameter is not null && string.Equals(parameter.Name, inputParameter.Name, StringComparison.Ordinal))
if (inputParameter is not null && !parameter.IsContentParameter)
Comment thread
jorgerangel-msft marked this conversation as resolved.
{
var originalName = inputParameter.OriginalName;
Comment thread
jorgerangel-msft marked this conversation as resolved.
if (!string.IsNullOrEmpty(originalName))
{
preservedName = FindPreviousParameterName(lastContractView, originalName, method.Signature.Name);
}
preservedName = FindPreviousParameterName(lastContractView, inputParameter.OriginalName, method.Signature.Name);
}

// Fall back to a positional match for synthesized parameters
Expand Down
Loading
Loading