From 42dd88a3cc8cd3e62f75a586c64da60bb7b7517d Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Thu, 13 Aug 2026 16:01:20 -0500 Subject: [PATCH 1/8] add acaornym naming for dates --- .../XmlAdvancedModelXmlTests.cs | 4 +- ...onHandlesNullableDateTimeOffsetProperty.cs | 4 +- .../XmlDeserializationTests.cs | 2 +- .../XmlSerializationTests.cs | 2 +- .../src/Providers/ParameterProvider.cs | 4 +- .../src/Providers/PropertyProvider.cs | 4 +- .../src/Utilities/CSharpNameExtensions.cs | 89 +++++++++++++++++++ .../test/Providers/ParameterProviderTests.cs | 43 +++++++++ .../test/Providers/PropertyProviderTests.cs | 88 ++++++++++++++++++ .../Models/XmlAdvancedModel.Serialization.cs | 8 +- .../src/Generated/Models/XmlAdvancedModel.cs | 14 +-- .../Generated/SampleTypeSpecModelFactory.cs | 6 +- 12 files changed, 246 insertions(+), 22 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ModelReaderWriterValidation/TestProjects/Sample_TypeSpec/XmlAdvancedModelXmlTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ModelReaderWriterValidation/TestProjects/Sample_TypeSpec/XmlAdvancedModelXmlTests.cs index f5cb1a54a62..7515ce441c1 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ModelReaderWriterValidation/TestProjects/Sample_TypeSpec/XmlAdvancedModelXmlTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/ModelReaderWriterValidation/TestProjects/Sample_TypeSpec/XmlAdvancedModelXmlTests.cs @@ -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 @@ -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 diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationTests/XmlSerializationHandlesNullableDateTimeOffsetProperty.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationTests/XmlSerializationHandlesNullableDateTimeOffsetProperty.cs index a64a6102cfb..a2e945aa23e 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationTests/XmlSerializationHandlesNullableDateTimeOffsetProperty.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/XmlSerializationTests/XmlSerializationHandlesNullableDateTimeOffsetProperty.cs @@ -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(); } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlDeserializationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlDeserializationTests.cs index c34ae949f14..52666216903 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlDeserializationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlDeserializationTests.cs @@ -261,7 +261,7 @@ public void XmlDeserializationHandlesDateTimeOffsetProperty() Assert.IsNotNull(xmlDeserializationMethod); var methodBody = xmlDeserializationMethod!.BodyStatements!.ToDisplayString(); - Assert.IsTrue(methodBody.Contains("timestamp = child.GetDateTimeOffset(\"O\")"), + Assert.IsTrue(methodBody.Contains("@on = child.GetDateTimeOffset(\"O\")"), $"DateTimeOffset property should use child.GetDateTimeOffset(\"O\") with RFC3339 format. Actual:\n{methodBody}"); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlSerializationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlSerializationTests.cs index 91417351981..563329b08be 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlSerializationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlSerializationTests.cs @@ -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\")"), $"DateTimeOffset property should be serialized with WriteStringValue. Actual:\n{methodBody}"); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs index b253e910c6f..cf2024e6736 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs @@ -63,7 +63,9 @@ public sealed class ParameterProvider : IEquatable public ParameterProvider(InputParameter inputParameter) { InputParameter = inputParameter; - Name = inputParameter.Name; + Name = inputParameter is InputMethodParameter && !inputParameter.IsExactName + ? inputParameter.Name.NormalizeDateTimeSuffix(inputParameter.Type) + : 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) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs index b8dfe9fe276..3bf19d3cac5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs @@ -111,7 +111,9 @@ private PropertyProvider(InputProperty inputProperty, CSharpType propertyType, T (lastContractProperties is null || !lastContractProperties.Any(p => p.Name == legacyName))) { - identifierName = identifierName.NormalizeCSharpAcronyms(); + identifierName = identifierName + .NormalizeDateTimeSuffix(inputProperty.Type) + .NormalizeCSharpAcronyms(); } Name = identifierName == enclosingType.Name ? $"{identifierName}Property" diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/CSharpNameExtensions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/CSharpNameExtensions.cs index 39fb45aff2e..409792f9465 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/CSharpNameExtensions.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/CSharpNameExtensions.cs @@ -2,7 +2,9 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Text; +using Microsoft.TypeSpec.Generator.Input; namespace Microsoft.TypeSpec.Generator.Utilities { @@ -19,6 +21,18 @@ private static readonly (string Source, string Replacement)[] _acronymRenamingRu ("Os", "OS") ]; + private static readonly Dictionary _dateTimeNounRenamingRules = new(StringComparer.Ordinal) + { + ["Creation"] = "Created", + ["creation"] = "created", + ["Deletion"] = "Deleted", + ["deletion"] = "deleted", + ["Expiration"] = "Expire", + ["expiration"] = "expire", + ["Modification"] = "Modified", + ["modification"] = "modified" + }; + public static string NormalizeCSharpAcronyms(this string name) { StringBuilder? normalizedName = null; @@ -55,5 +69,80 @@ public static string NormalizeCSharpAcronyms(this string name) normalizedName.Append(name, segmentStart, name.Length - segmentStart); return normalizedName.ToString(); } + + public static string NormalizeDateTimeSuffix(this string name, InputType inputType) + { + if (!IsDateTimeInputType(inputType) || + name.StartsWith("From", StringComparison.Ordinal) || + name.StartsWith("from", StringComparison.Ordinal) || + name.StartsWith("To", StringComparison.Ordinal) || + name.StartsWith("to", StringComparison.Ordinal) || + name.EndsWith("PointInTime", StringComparison.Ordinal) || + name.Equals("pointInTime", StringComparison.Ordinal)) + { + return name; + } + + var suffixLength = GetDateTimeSuffixLength(name); + if (suffixLength == 0) + { + return name; + } + + var prefix = name[..^suffixLength]; + if (_dateTimeNounRenamingRules.TryGetValue(prefix, out var replacement)) + { + prefix = replacement; + } + + var onSuffix = prefix.Length == 0 && char.IsLower(name[0]) ? "on" : "On"; + return prefix + onSuffix; + } + + private static int GetDateTimeSuffixLength(string name) + { + if (name.EndsWith("Timestamp", StringComparison.Ordinal) || + name.EndsWith("TimeStamp", StringComparison.Ordinal)) + { + return 9; + } + + if (name.Equals("timestamp", StringComparison.Ordinal) || + name.Equals("timeStamp", StringComparison.Ordinal)) + { + return 9; + } + + if (name.Length > 8 && name.EndsWith("DateTime", StringComparison.Ordinal)) + { + return 8; + } + + if ((name.Length > 4 && name.EndsWith("Time", StringComparison.Ordinal)) || + name.EndsWith("Date", StringComparison.Ordinal)) + { + return 4; + } + + if (name.Equals("date", StringComparison.Ordinal)) + { + return 4; + } + + if (name.Length > 2 && name.EndsWith("At", StringComparison.Ordinal)) + { + return 2; + } + + return 0; + } + + private static bool IsDateTimeInputType(InputType inputType) => inputType switch + { + InputDateTimeType => true, + InputPrimitiveType { Kind: InputPrimitiveTypeKind.PlainDate } => true, + InputNullableType nullableType => IsDateTimeInputType(nullableType.Type), + _ => false + }; } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs index c4671db76a8..256e10d88d5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs @@ -67,6 +67,27 @@ public void ValidateArrayHandling() Assert.IsTrue(parameter.ToPublicInputParameter().Type.Equals(typeof(IEnumerable))); } + [TestCaseSource(nameof(DateTimeParameterNameTestCases))] + public void MethodParameterNameNormalizesDateTimeSuffix( + string inputName, + InputType inputType, + bool isExactName, + string expectedName) + { + MockHelpers.LoadMockGenerator(); + var inputParameter = InputFactory.MethodParameter( + inputName, + inputType, + isRequired: true, + isExactName: isExactName); + + var parameter = CodeModelGenerator.Instance.TypeFactory.CreateParameter(inputParameter); + + Assert.IsNotNull(parameter); + Assert.AreEqual(expectedName, parameter!.Name); + Assert.AreEqual(inputName, parameter.WireInfo.SerializedName); + } + private static IEnumerable ValueInputTypes() { yield return InputPrimitiveType.Int32; @@ -74,6 +95,28 @@ private static IEnumerable ValueInputTypes() yield return InputFactory.Int32Enum("inputEnum", [("foo", 1)], isExtensible: true); } + private static IEnumerable DateTimeParameterNameTestCases() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + + yield return new TestCaseData("startTime", dateTime, false, "startOn"); + yield return new TestCaseData("creationTimestamp", dateTime, false, "createdOn"); + yield return new TestCaseData("timestamp", dateTime, false, "on"); + yield return new TestCaseData("date", InputPrimitiveType.PlainDate, false, "on"); + yield return new TestCaseData("modifiedAt", dateTime.WithNullable(true), false, "modifiedOn"); + yield return new TestCaseData("fromTime", dateTime, false, "fromTime"); + yield return new TestCaseData("toDate", dateTime, false, "toDate"); + yield return new TestCaseData("pointInTime", dateTime, false, "pointInTime"); + yield return new TestCaseData("recoveryPointInTime", dateTime, false, "recoveryPointInTime"); + yield return new TestCaseData("startTime", InputPrimitiveType.String, false, "startTime"); + yield return new TestCaseData("creationTimestamp", InputPrimitiveType.String, false, "creationTimestamp"); + yield return new TestCaseData("creationTimestamp", dateTime, true, "creationTimestamp"); + } + private static IEnumerable NotEqualsTestCases() { yield return new TestCaseData( diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs index 96c3fb419ca..617610a1dd6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs @@ -129,6 +129,55 @@ public void TestPropertyNameNormalizesAcronymCasing(string inputName, bool isExa Assert.AreEqual(expectedName, property.Name); } + [TestCaseSource(nameof(DateTimePropertyNameTestCases))] + public void TestPropertyNameNormalizesDateTimeSuffix( + string inputName, + InputType inputType, + bool isExactName, + string expectedName) + { + var inputProperty = InputFactory.Property( + inputName, + inputType, + isRequired: true, + isExactName: isExactName); + InputFactory.Model("TestModel", properties: [inputProperty]); + + var property = new PropertyProvider(inputProperty, new TestTypeProvider()); + + Assert.AreEqual(expectedName, property.Name); + Assert.AreEqual(inputName.ToVariableName(), property.WireInfo?.SerializedName); + } + + [Test] + public void TestPropertyNamePreservesLastContractDateTimeSuffix() + { + var lastContract = new TestTypeProvider(properties: + [ + new PropertyProvider( + description: null, + modifiers: MethodSignatureModifiers.Public, + type: typeof(DateTimeOffset), + name: "StartTime", + body: new AutoPropertyBody(HasSetter: true), + enclosingType: TestTypeProvider.Empty) + ]); + var enclosingType = new TestTypeProviderWithLastContract(lastContract); + var inputProperty = InputFactory.Property( + "StartTime", + new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String), + isRequired: true); + InputFactory.Model("TestModel", properties: [inputProperty]); + + var property = new PropertyProvider(inputProperty, enclosingType); + + Assert.AreEqual("StartTime", property.Name); + } + [TestCaseSource(nameof(CollectionPropertyTestCases))] public void CollectionProperty(CSharpType coreType, InputModelProperty collectionProperty, CSharpType expectedType) { @@ -202,6 +251,45 @@ public void TestPropertyNameConflictsWithTypeNameAfterAcronymNormalization() Assert.AreEqual("IPAddressProperty", property.Name); } + private static IEnumerable DateTimePropertyNameTestCases() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + + yield return new TestCaseData("StartTime", dateTime, false, "StartOn"); + yield return new TestCaseData("CreationDateTime", dateTime, false, "CreatedOn"); + yield return new TestCaseData("DeletionTimestamp", dateTime, false, "DeletedOn"); + yield return new TestCaseData("ModificationTimeStamp", dateTime, false, "ModifiedOn"); + yield return new TestCaseData("Timestamp", dateTime, false, "On"); + yield return new TestCaseData("ExpirationDate", dateTime, false, "ExpireOn"); + yield return new TestCaseData("RecordedAt", dateTime, false, "RecordedOn"); + yield return new TestCaseData("Date", InputPrimitiveType.PlainDate, false, "On"); + yield return new TestCaseData("SnapshotTimestamp", dateTime.WithNullable(true), false, "SnapshotOn"); + yield return new TestCaseData("FromTime", dateTime, false, "FromTime"); + yield return new TestCaseData("ToDate", dateTime, false, "ToDate"); + yield return new TestCaseData("RecoveryPointInTime", dateTime, false, "RecoveryPointInTime"); + yield return new TestCaseData("StartTime", InputPrimitiveType.String, false, "StartTime"); + yield return new TestCaseData("CreationTimestamp", InputPrimitiveType.String, false, "CreationTimestamp"); + yield return new TestCaseData("CreationTimestamp", dateTime, true, "CreationTimestamp"); + } + + private sealed class TestTypeProviderWithLastContract : TestTypeProvider + { + private readonly TypeProvider _lastContract; + + public TestTypeProviderWithLastContract(TypeProvider lastContract) + { + _lastContract = lastContract; + } + + private protected override TypeProvider? BuildLastContractView( + string? generatedTypeName = default, + string? generatedTypeNamespace = default) => _lastContract; + } + [Test] public void CanUpdatePropertyProvider() { diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.Serialization.cs index f1d688b9098..e1cf5add97d 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.Serialization.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.Serialization.cs @@ -226,7 +226,7 @@ internal virtual void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptio } writer.WriteEndElement(); writer.WriteStartElement("createdAt"); - writer.WriteStringValue(CreatedAt, "O"); + writer.WriteStringValue(CreatedOn, "O"); writer.WriteEndElement(); writer.WriteStartElement("duration"); writer.WriteStringValue(Duration, "P"); @@ -390,7 +390,7 @@ internal static XmlAdvancedModel DeserializeXmlAdvancedModel(XElement element, M XmlNestedModel nestedModel = default; XmlNestedModel optionalNestedModel = default; IDictionary metadata = default; - DateTimeOffset createdAt = default; + DateTimeOffset createdOn = default; TimeSpan duration = default; BinaryData data = default; IDictionary optionalRecordUnknown = default; @@ -544,7 +544,7 @@ internal static XmlAdvancedModel DeserializeXmlAdvancedModel(XElement element, M } if (localName == "createdAt") { - createdAt = child.GetDateTimeOffset("O"); + createdOn = child.GetDateTimeOffset("O"); continue; } if (localName == "duration") @@ -717,7 +717,7 @@ internal static XmlAdvancedModel DeserializeXmlAdvancedModel(XElement element, M nestedModel, optionalNestedModel, metadata, - createdAt, + createdOn, duration, data, optionalRecordUnknown ?? new ChangeTrackingDictionary(), diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.cs index 32a82daf882..e52d1f3f9d5 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/XmlAdvancedModel.cs @@ -34,7 +34,7 @@ public partial class XmlAdvancedModel /// A wrapped array with custom wrapper name. /// A nested model property. /// A dictionary property. - /// A date-time property. + /// A date-time property. /// A duration property. /// A bytes property. /// A fixed enum property. @@ -51,7 +51,7 @@ public partial class XmlAdvancedModel /// /// /// , , , , , , , , , , , , , , , , , , , , , or is null. - public XmlAdvancedModel(string name, int age, bool enabled, float score, string nullableString, string id, int version, bool isActive, string originalName, string xmlIdentifier, string content, IEnumerable unwrappedStrings, IEnumerable unwrappedCounts, IEnumerable unwrappedItems, IEnumerable wrappedColors, IEnumerable items, XmlNestedModel nestedModel, IDictionary metadata, DateTimeOffset createdAt, TimeSpan duration, BinaryData data, StringFixedEnum fixedEnum, StringExtensibleEnum extensibleEnum, string label, int daysUsed, IEnumerable fooItems, XmlNestedModel anotherModel, IEnumerable modelsWithNamespaces, IEnumerable unwrappedModelsWithNamespaces, IEnumerable> listOfListFoo, IDictionary dictionaryFoo, IDictionary> dictionaryOfDictionaryFoo, IDictionary> dictionaryListFoo, IEnumerable> listOfDictionaryFoo) + public XmlAdvancedModel(string name, int age, bool enabled, float score, string nullableString, string id, int version, bool isActive, string originalName, string xmlIdentifier, string content, IEnumerable unwrappedStrings, IEnumerable unwrappedCounts, IEnumerable unwrappedItems, IEnumerable wrappedColors, IEnumerable items, XmlNestedModel nestedModel, IDictionary metadata, DateTimeOffset createdOn, TimeSpan duration, BinaryData data, StringFixedEnum fixedEnum, StringExtensibleEnum extensibleEnum, string label, int daysUsed, IEnumerable fooItems, XmlNestedModel anotherModel, IEnumerable modelsWithNamespaces, IEnumerable unwrappedModelsWithNamespaces, IEnumerable> listOfListFoo, IDictionary dictionaryFoo, IDictionary> dictionaryOfDictionaryFoo, IDictionary> dictionaryListFoo, IEnumerable> listOfDictionaryFoo) { Argument.AssertNotNull(name, nameof(name)); Argument.AssertNotNull(id, nameof(id)); @@ -95,7 +95,7 @@ public XmlAdvancedModel(string name, int age, bool enabled, float score, string Items = items.ToList(); NestedModel = nestedModel; Metadata = metadata; - CreatedAt = createdAt; + CreatedOn = createdOn; Duration = duration; Data = data; OptionalRecordUnknown = new ChangeTrackingDictionary(); @@ -136,7 +136,7 @@ public XmlAdvancedModel(string name, int age, bool enabled, float score, string /// A nested model property. /// An optional nested model. /// A dictionary property. - /// A date-time property. + /// A date-time property. /// A duration property. /// A bytes property. /// optional record of unknown. @@ -155,7 +155,7 @@ public XmlAdvancedModel(string name, int age, bool enabled, float score, string /// /// /// - internal XmlAdvancedModel(string name, int age, bool enabled, float score, string optionalString, int? optionalInt, string nullableString, string id, int version, bool isActive, string originalName, string xmlIdentifier, string content, IList unwrappedStrings, IList unwrappedCounts, IList unwrappedItems, IList wrappedColors, IList items, XmlNestedModel nestedModel, XmlNestedModel optionalNestedModel, IDictionary metadata, DateTimeOffset createdAt, TimeSpan duration, BinaryData data, IDictionary optionalRecordUnknown, StringFixedEnum fixedEnum, StringExtensibleEnum extensibleEnum, IntFixedEnum? optionalFixedEnum, IntExtensibleEnum? optionalExtensibleEnum, string label, int daysUsed, IList fooItems, XmlNestedModel anotherModel, IList modelsWithNamespaces, IList unwrappedModelsWithNamespaces, IList> listOfListFoo, IDictionary dictionaryFoo, IDictionary> dictionaryOfDictionaryFoo, IDictionary> dictionaryListFoo, IList> listOfDictionaryFoo) + internal XmlAdvancedModel(string name, int age, bool enabled, float score, string optionalString, int? optionalInt, string nullableString, string id, int version, bool isActive, string originalName, string xmlIdentifier, string content, IList unwrappedStrings, IList unwrappedCounts, IList unwrappedItems, IList wrappedColors, IList items, XmlNestedModel nestedModel, XmlNestedModel optionalNestedModel, IDictionary metadata, DateTimeOffset createdOn, TimeSpan duration, BinaryData data, IDictionary optionalRecordUnknown, StringFixedEnum fixedEnum, StringExtensibleEnum extensibleEnum, IntFixedEnum? optionalFixedEnum, IntExtensibleEnum? optionalExtensibleEnum, string label, int daysUsed, IList fooItems, XmlNestedModel anotherModel, IList modelsWithNamespaces, IList unwrappedModelsWithNamespaces, IList> listOfListFoo, IDictionary dictionaryFoo, IDictionary> dictionaryOfDictionaryFoo, IDictionary> dictionaryListFoo, IList> listOfDictionaryFoo) { Name = name; Age = age; @@ -178,7 +178,7 @@ internal XmlAdvancedModel(string name, int age, bool enabled, float score, strin NestedModel = nestedModel; OptionalNestedModel = optionalNestedModel; Metadata = metadata; - CreatedAt = createdAt; + CreatedOn = createdOn; Duration = duration; Data = data; OptionalRecordUnknown = optionalRecordUnknown; @@ -263,7 +263,7 @@ internal XmlAdvancedModel(string name, int age, bool enabled, float score, strin public IDictionary Metadata { get; } /// A date-time property. - public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset CreatedOn { get; set; } /// A duration property. public TimeSpan Duration { get; set; } diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecModelFactory.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecModelFactory.cs index 3a65a21dc61..65fc0e6464a 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecModelFactory.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/SampleTypeSpecModelFactory.cs @@ -270,7 +270,7 @@ public static AnotherDynamicModel AnotherDynamicModel(string bar = default) /// A nested model property. /// An optional nested model. /// A dictionary property. - /// A date-time property. + /// A date-time property. /// A duration property. /// A bytes property. /// optional record of unknown. @@ -290,7 +290,7 @@ public static AnotherDynamicModel AnotherDynamicModel(string bar = default) /// /// /// A new instance for mocking. - public static XmlAdvancedModel XmlAdvancedModel(string name = default, int age = default, bool enabled = default, float score = default, string optionalString = default, int? optionalInt = default, string nullableString = default, string id = default, int version = default, bool isActive = default, string originalName = default, string xmlIdentifier = default, string content = default, IEnumerable unwrappedStrings = default, IEnumerable unwrappedCounts = default, IEnumerable unwrappedItems = default, IEnumerable wrappedColors = default, IEnumerable items = default, XmlNestedModel nestedModel = default, XmlNestedModel optionalNestedModel = default, IDictionary metadata = default, DateTimeOffset createdAt = default, TimeSpan duration = default, BinaryData data = default, IDictionary optionalRecordUnknown = default, StringFixedEnum fixedEnum = default, StringExtensibleEnum extensibleEnum = default, IntFixedEnum? optionalFixedEnum = default, IntExtensibleEnum? optionalExtensibleEnum = default, string label = default, int daysUsed = default, IEnumerable fooItems = default, XmlNestedModel anotherModel = default, IEnumerable modelsWithNamespaces = default, IEnumerable unwrappedModelsWithNamespaces = default, IEnumerable> listOfListFoo = default, IDictionary dictionaryFoo = default, IDictionary> dictionaryOfDictionaryFoo = default, IDictionary> dictionaryListFoo = default, IEnumerable> listOfDictionaryFoo = default) + public static XmlAdvancedModel XmlAdvancedModel(string name = default, int age = default, bool enabled = default, float score = default, string optionalString = default, int? optionalInt = default, string nullableString = default, string id = default, int version = default, bool isActive = default, string originalName = default, string xmlIdentifier = default, string content = default, IEnumerable unwrappedStrings = default, IEnumerable unwrappedCounts = default, IEnumerable unwrappedItems = default, IEnumerable wrappedColors = default, IEnumerable items = default, XmlNestedModel nestedModel = default, XmlNestedModel optionalNestedModel = default, IDictionary metadata = default, DateTimeOffset createdOn = default, TimeSpan duration = default, BinaryData data = default, IDictionary optionalRecordUnknown = default, StringFixedEnum fixedEnum = default, StringExtensibleEnum extensibleEnum = default, IntFixedEnum? optionalFixedEnum = default, IntExtensibleEnum? optionalExtensibleEnum = default, string label = default, int daysUsed = default, IEnumerable fooItems = default, XmlNestedModel anotherModel = default, IEnumerable modelsWithNamespaces = default, IEnumerable unwrappedModelsWithNamespaces = default, IEnumerable> listOfListFoo = default, IDictionary dictionaryFoo = default, IDictionary> dictionaryOfDictionaryFoo = default, IDictionary> dictionaryListFoo = default, IEnumerable> listOfDictionaryFoo = default) { unwrappedStrings ??= new ChangeTrackingList(); unwrappedCounts ??= new ChangeTrackingList(); @@ -330,7 +330,7 @@ public static XmlAdvancedModel XmlAdvancedModel(string name = default, int age = nestedModel, optionalNestedModel, metadata, - createdAt, + createdOn, duration, data, optionalRecordUnknown, From 9b0729de6fbe83772ef17d4d2dd4250f4817dc1d Mon Sep 17 00:00:00 2001 From: Jorge Rangel Date: Thu, 13 Aug 2026 16:54:15 -0500 Subject: [PATCH 2/8] shorten exclusion list --- .../src/Shared/CSharpNameExtensions.cs | 32 +++++++------------ .../test/Providers/ParameterProviderTests.cs | 2 +- .../test/Providers/PropertyProviderTests.cs | 8 ++--- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs index 959d82faa82..1dbe5a675d5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs @@ -22,16 +22,10 @@ private static readonly (string Source, string Replacement)[] _acronymRenamingRu ("Os", "OS") ]; - private static readonly Dictionary _dateTimeNounRenamingRules = new(StringComparer.Ordinal) + private static readonly HashSet _dateTimePrefixExclusions = new(StringComparer.OrdinalIgnoreCase) { - ["Creation"] = "Created", - ["creation"] = "created", - ["Deletion"] = "Deleted", - ["deletion"] = "deleted", - ["Expiration"] = "Expire", - ["expiration"] = "expire", - ["Modification"] = "Modified", - ["modification"] = "modified" + "From", + "To" }; public static string NormalizeCSharpAcronyms(this string name) @@ -74,12 +68,8 @@ public static string NormalizeCSharpAcronyms(this string name) public static string NormalizeDateTimeSuffix(this string name, InputType inputType) { if (!IsDateTimeInputType(inputType) || - name.StartsWith("From", StringComparison.Ordinal) || - name.StartsWith("from", StringComparison.Ordinal) || - name.StartsWith("To", StringComparison.Ordinal) || - name.StartsWith("to", StringComparison.Ordinal) || - name.EndsWith("PointInTime", StringComparison.Ordinal) || - name.Equals("pointInTime", StringComparison.Ordinal)) + HasExcludedDateTimePrefix(name) || + name.EndsWith("PointInTime", StringComparison.OrdinalIgnoreCase)) { return name; } @@ -91,15 +81,17 @@ public static string NormalizeDateTimeSuffix(this string name, InputType inputTy } var prefix = name[..^suffixLength]; - if (_dateTimeNounRenamingRules.TryGetValue(prefix, out var replacement)) - { - prefix = replacement; - } - var onSuffix = prefix.Length == 0 && char.IsLower(name[0]) ? "on" : "On"; return prefix + onSuffix; } + private static bool HasExcludedDateTimePrefix(string name) + { + var lookup = _dateTimePrefixExclusions.GetAlternateLookup>(); + return (name.Length >= 4 && lookup.Contains(name.AsSpan(0, 4))) || + (name.Length >= 2 && lookup.Contains(name.AsSpan(0, 2))); + } + private static int GetDateTimeSuffixLength(string name) { if (name.EndsWith("Timestamp", StringComparison.Ordinal) || diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs index 256e10d88d5..2cd28132ecc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs @@ -104,7 +104,7 @@ private static IEnumerable DateTimeParameterNameTestCases() InputPrimitiveType.String); yield return new TestCaseData("startTime", dateTime, false, "startOn"); - yield return new TestCaseData("creationTimestamp", dateTime, false, "createdOn"); + yield return new TestCaseData("creationTimestamp", dateTime, false, "creationOn"); yield return new TestCaseData("timestamp", dateTime, false, "on"); yield return new TestCaseData("date", InputPrimitiveType.PlainDate, false, "on"); yield return new TestCaseData("modifiedAt", dateTime.WithNullable(true), false, "modifiedOn"); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs index 617610a1dd6..d245dec0734 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs @@ -260,11 +260,11 @@ private static IEnumerable DateTimePropertyNameTestCases() InputPrimitiveType.String); yield return new TestCaseData("StartTime", dateTime, false, "StartOn"); - yield return new TestCaseData("CreationDateTime", dateTime, false, "CreatedOn"); - yield return new TestCaseData("DeletionTimestamp", dateTime, false, "DeletedOn"); - yield return new TestCaseData("ModificationTimeStamp", dateTime, false, "ModifiedOn"); + yield return new TestCaseData("CreationDateTime", dateTime, false, "CreationOn"); + yield return new TestCaseData("DeletionTimestamp", dateTime, false, "DeletionOn"); + yield return new TestCaseData("ModificationTimeStamp", dateTime, false, "ModificationOn"); yield return new TestCaseData("Timestamp", dateTime, false, "On"); - yield return new TestCaseData("ExpirationDate", dateTime, false, "ExpireOn"); + yield return new TestCaseData("ExpirationDate", dateTime, false, "ExpirationOn"); yield return new TestCaseData("RecordedAt", dateTime, false, "RecordedOn"); yield return new TestCaseData("Date", InputPrimitiveType.PlainDate, false, "On"); yield return new TestCaseData("SnapshotTimestamp", dateTime.WithNullable(true), false, "SnapshotOn"); From 8bde832df216a198f8c32072953d58953f5ce45b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:48:20 +0000 Subject: [PATCH 3/8] fix: cover date time name exclusions Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Shared/CSharpNameExtensions.cs | 15 ++++++----- .../Utilities/CSharpNameExtensionsTests.cs | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs index 1dbe5a675d5..da48bdaf360 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs @@ -22,10 +22,11 @@ private static readonly (string Source, string Replacement)[] _acronymRenamingRu ("Os", "OS") ]; - private static readonly HashSet _dateTimePrefixExclusions = new(StringComparer.OrdinalIgnoreCase) + private static readonly HashSet _dateTimeNameExclusions = new(StringComparer.OrdinalIgnoreCase) { "From", - "To" + "To", + "PointInTime" }; public static string NormalizeCSharpAcronyms(this string name) @@ -68,8 +69,7 @@ public static string NormalizeCSharpAcronyms(this string name) public static string NormalizeDateTimeSuffix(this string name, InputType inputType) { if (!IsDateTimeInputType(inputType) || - HasExcludedDateTimePrefix(name) || - name.EndsWith("PointInTime", StringComparison.OrdinalIgnoreCase)) + HasExcludedDateTimeNameComponent(name)) { return name; } @@ -85,11 +85,12 @@ public static string NormalizeDateTimeSuffix(this string name, InputType inputTy return prefix + onSuffix; } - private static bool HasExcludedDateTimePrefix(string name) + private static bool HasExcludedDateTimeNameComponent(string name) { - var lookup = _dateTimePrefixExclusions.GetAlternateLookup>(); + var lookup = _dateTimeNameExclusions.GetAlternateLookup>(); return (name.Length >= 4 && lookup.Contains(name.AsSpan(0, 4))) || - (name.Length >= 2 && lookup.Contains(name.AsSpan(0, 2))); + (name.Length >= 2 && lookup.Contains(name.AsSpan(0, 2))) || + (name.Length >= "PointInTime".Length && lookup.Contains(name.AsSpan(^"PointInTime".Length))); } private static int GetDateTimeSuffixLength(string name) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/CSharpNameExtensionsTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/CSharpNameExtensionsTests.cs index 1e84328cb7f..25c54e0b0fc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/CSharpNameExtensionsTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/CSharpNameExtensionsTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Collections.Generic; +using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Utilities; using NUnit.Framework; @@ -19,5 +21,29 @@ public void TestNormalizeCSharpUrlSuffix(string name, string expected) { Assert.AreEqual(expected, name.NormalizeCSharpUrlSuffix()); } + + [TestCaseSource(nameof(DateTimeNameTestCases))] + public void TestNormalizeDateTimeSuffix(string name, InputType type, string expected) + { + Assert.AreEqual(expected, name.NormalizeDateTimeSuffix(type)); + } + + private static IEnumerable DateTimeNameTestCases() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + + yield return new TestCaseData("startTime", dateTime, "startOn"); + yield return new TestCaseData("Date", InputPrimitiveType.PlainDate, "On"); + yield return new TestCaseData("date", InputPrimitiveType.PlainDate, "on"); + yield return new TestCaseData("fromTime", dateTime, "fromTime"); + yield return new TestCaseData("toDate", dateTime, "toDate"); + yield return new TestCaseData("pointInTime", dateTime, "pointInTime"); + yield return new TestCaseData("recoveryPointInTime", dateTime, "recoveryPointInTime"); + yield return new TestCaseData("startTime", InputPrimitiveType.String, "startTime"); + } } } From 875ac14e72c20df3f976dd2af09cdc92723d000a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:47:52 +0000 Subject: [PATCH 4/8] fix: preserve normalized parameter names Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Utilities/BackCompatHelper.cs | 6 ++++- .../TestClient.cs | 7 ++++++ .../test/Providers/TypeProviderTests.cs | 22 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/RestorePreviousParameterNamesRestoresNormalizedMethodParameterName/TestClient.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs index 979d40194ca..e8bd8c2f96b 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs @@ -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; @@ -189,7 +190,10 @@ 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 && + (string.Equals(parameter.Name, inputParameter.Name, StringComparison.Ordinal) || + (inputParameter is InputMethodParameter { IsExactName: false } && + string.Equals(parameter.Name, inputParameter.Name.NormalizeDateTimeSuffix(inputParameter.Type), StringComparison.Ordinal)))) { var originalName = inputParameter.OriginalName; if (!string.IsNullOrEmpty(originalName)) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/RestorePreviousParameterNamesRestoresNormalizedMethodParameterName/TestClient.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/RestorePreviousParameterNamesRestoresNormalizedMethodParameterName/TestClient.cs new file mode 100644 index 00000000000..a38628fee6c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/RestorePreviousParameterNamesRestoresNormalizedMethodParameterName/TestClient.cs @@ -0,0 +1,7 @@ +namespace Test +{ + public class TestClient + { + public string Foo(string startTime) { return null; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index b4b6d14584d..b8948ca88de 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -786,6 +786,28 @@ public void FindPreviousParameterNameReturnsNullWithoutLastContract() Assert.IsNull(BackCompatHelper.FindPreviousParameterName(typeProvider.LastContractView, "oldParam", "Foo")); } + [Test] + public async Task RestorePreviousParameterNamesRestoresNormalizedMethodParameterName() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var parameter = new ParameterProvider(InputFactory.MethodParameter("startTime", dateTime, isRequired: true)); + var method = new MethodProvider( + new MethodSignature("Foo", $"", MethodSignatureModifiers.Public, new CSharpType(typeof(string)), $"", [parameter]), + Snippet.Return(Snippet.Null), + new TestTypeProvider()); + var typeProvider = new TestTypeProvider(name: "TestClient", methods: [method]); + + BackCompatHelper.RestorePreviousParameterNames(typeProvider, typeProvider.Methods); + + Assert.AreEqual("startTime", parameter.Name); + } + // A reorder combined with a casing-only rename must still restore the published spelling. // The reorder matches parameters via ToVariableName (so current "URL" aligns with published // "url"), which previously short-circuited the exact-name pass and left the CP0017-breaking From aa00bd30bfa0e24445901459aa243451371ba704 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:30:22 +0000 Subject: [PATCH 5/8] fix: address date naming review feedback Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../XmlDeserializationTests.cs | 4 +- .../src/Providers/ParameterProvider.cs | 3 +- .../src/Providers/PropertyProvider.cs | 3 +- .../src/Shared/CSharpNameExtensions.cs | 79 ++++++++++++------- .../src/Utilities/BackCompatHelper.cs | 3 +- .../test/Providers/ParameterProviderTests.cs | 4 +- .../test/Providers/PropertyProviderTests.cs | 51 +++++------- ...vesLastContractDateTimeSuffix(Expected).cs | 27 +++++++ .../TestModel.cs | 7 ++ ...NormalizedMethodParameterName(Expected).cs | 16 ++++ .../test/Providers/TypeProviderTests.cs | 3 +- .../Utilities/CSharpNameExtensionsTests.cs | 14 +++- 12 files changed, 142 insertions(+), 72 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesLastContractDateTimeSuffix(Expected).cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesLastContractDateTimeSuffix/TestModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/RestorePreviousParameterNamesRestoresNormalizedMethodParameterName(Expected).cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlDeserializationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlDeserializationTests.cs index 52666216903..d0ae40f8f84 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlDeserializationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/XmlDeserializationTests.cs @@ -261,8 +261,8 @@ public void XmlDeserializationHandlesDateTimeOffsetProperty() Assert.IsNotNull(xmlDeserializationMethod); var methodBody = xmlDeserializationMethod!.BodyStatements!.ToDisplayString(); - Assert.IsTrue(methodBody.Contains("@on = child.GetDateTimeOffset(\"O\")"), - $"DateTimeOffset property should use child.GetDateTimeOffset(\"O\") with RFC3339 format. Actual:\n{methodBody}"); + Assert.IsTrue(methodBody.Contains("GetDateTimeOffset(\"O\")"), + $"DateTimeOffset property should use RFC3339 format. Actual:\n{methodBody}"); } [Test] diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs index cf2024e6736..496612af7d3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs @@ -64,7 +64,8 @@ public ParameterProvider(InputParameter inputParameter) { InputParameter = inputParameter; Name = inputParameter is InputMethodParameter && !inputParameter.IsExactName - ? inputParameter.Name.NormalizeDateTimeSuffix(inputParameter.Type) + && 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}"); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs index 3bf19d3cac5..4984219b395 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs @@ -112,8 +112,7 @@ private PropertyProvider(InputProperty inputProperty, CSharpType propertyType, T !lastContractProperties.Any(p => p.Name == legacyName))) { identifierName = identifierName - .NormalizeDateTimeSuffix(inputProperty.Type) - .NormalizeCSharpAcronyms(); + .NormalizeCSharpAcronyms(inputProperty.Type.IsDateTimeInputType()); } Name = identifierName == enclosingType.Name ? $"{identifierName}Property" diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs index da48bdaf360..c9d9a40b103 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs @@ -11,6 +11,18 @@ namespace Microsoft.TypeSpec.Generator.Utilities { internal static class CSharpNameExtensions { + private const string DateSuffix = "Date"; + private const string DateTimeSuffix = "DateTime"; + private const string FromName = "From"; + private const string LowercaseOnSuffix = "on"; + private const string OnSuffix = "On"; + private const string PointInTimeName = "PointInTime"; + private const string TimeStampSuffix = "TimeStamp"; + private const string TimeSuffix = "Time"; + private const string TimestampSuffix = "Timestamp"; + private const string ToName = "To"; + private const string AtSuffix = "At"; + private static readonly (string Source, string Replacement)[] _acronymRenamingRules = [ ("Ipv4", "IPv4"), @@ -24,14 +36,17 @@ private static readonly (string Source, string Replacement)[] _acronymRenamingRu private static readonly HashSet _dateTimeNameExclusions = new(StringComparer.OrdinalIgnoreCase) { - "From", - "To", - "PointInTime" + FromName, + ToName, + PointInTimeName }; - public static string NormalizeCSharpAcronyms(this string name) + public static string NormalizeCSharpAcronyms(this string name, bool normalizeDateTimeSuffix = false) { - StringBuilder? normalizedName = null; + var suffixLength = normalizeDateTimeSuffix && !HasExcludedDateTimeNameComponent(name) + ? GetDateTimeSuffixLength(name) + : 0; + StringBuilder? normalizedName = suffixLength > 0 ? new(name.Length - suffixLength + OnSuffix.Length) : null; int segmentStart = 0; for (int index = 0; index < name.Length - 1; index++) { @@ -62,14 +77,17 @@ public static string NormalizeCSharpAcronyms(this string name) return name; } - normalizedName.Append(name, segmentStart, name.Length - segmentStart); + normalizedName.Append(name, segmentStart, name.Length - suffixLength - segmentStart); + if (suffixLength > 0) + { + normalizedName.Append(name.Length == suffixLength && char.IsLower(name[0]) ? LowercaseOnSuffix : OnSuffix); + } return normalizedName.ToString(); } - public static string NormalizeDateTimeSuffix(this string name, InputType inputType) + public static string NormalizeDateTimeSuffix(this string name) { - if (!IsDateTimeInputType(inputType) || - HasExcludedDateTimeNameComponent(name)) + if (HasExcludedDateTimeNameComponent(name)) { return name; } @@ -81,57 +99,60 @@ public static string NormalizeDateTimeSuffix(this string name, InputType inputTy } var prefix = name[..^suffixLength]; - var onSuffix = prefix.Length == 0 && char.IsLower(name[0]) ? "on" : "On"; + var onSuffix = prefix.Length == 0 && char.IsLower(name[0]) ? LowercaseOnSuffix : OnSuffix; return prefix + onSuffix; } private static bool HasExcludedDateTimeNameComponent(string name) { var lookup = _dateTimeNameExclusions.GetAlternateLookup>(); - return (name.Length >= 4 && lookup.Contains(name.AsSpan(0, 4))) || - (name.Length >= 2 && lookup.Contains(name.AsSpan(0, 2))) || - (name.Length >= "PointInTime".Length && lookup.Contains(name.AsSpan(^"PointInTime".Length))); + return (name.Length >= FromName.Length && lookup.Contains(name.AsSpan(0, FromName.Length))) || + (name.Length >= ToName.Length && lookup.Contains(name.AsSpan(0, ToName.Length))) || + (name.Length >= PointInTimeName.Length && lookup.Contains(name.AsSpan(^PointInTimeName.Length))); } private static int GetDateTimeSuffixLength(string name) { - if (name.EndsWith("Timestamp", StringComparison.Ordinal) || - name.EndsWith("TimeStamp", StringComparison.Ordinal)) + if (name.EndsWith(TimestampSuffix, StringComparison.Ordinal) || + name.EndsWith(TimeStampSuffix, StringComparison.Ordinal)) + { + return TimestampSuffix.Length; + } + + if (name.Equals(TimestampSuffix, StringComparison.OrdinalIgnoreCase)) { - return 9; + return TimestampSuffix.Length; } - if (name.Equals("timestamp", StringComparison.Ordinal) || - name.Equals("timeStamp", StringComparison.Ordinal)) + if (name.Length > DateTimeSuffix.Length && name.EndsWith(DateTimeSuffix, StringComparison.Ordinal)) { - return 9; + return DateTimeSuffix.Length; } - if (name.Length > 8 && name.EndsWith("DateTime", StringComparison.Ordinal)) + if (name.Length > TimeSuffix.Length && name.EndsWith(TimeSuffix, StringComparison.Ordinal)) { - return 8; + return TimeSuffix.Length; } - if ((name.Length > 4 && name.EndsWith("Time", StringComparison.Ordinal)) || - name.EndsWith("Date", StringComparison.Ordinal)) + if (name.Equals(DateSuffix, StringComparison.OrdinalIgnoreCase)) { - return 4; + return DateSuffix.Length; } - if (name.Equals("date", StringComparison.Ordinal)) + if (name.EndsWith(DateSuffix, StringComparison.Ordinal)) { - return 4; + return DateSuffix.Length; } - if (name.Length > 2 && name.EndsWith("At", StringComparison.Ordinal)) + if (name.Length > AtSuffix.Length && name.EndsWith(AtSuffix, StringComparison.Ordinal)) { - return 2; + return AtSuffix.Length; } return 0; } - private static bool IsDateTimeInputType(InputType inputType) => inputType switch + public static bool IsDateTimeInputType(this InputType inputType) => inputType switch { InputDateTimeType => true, InputPrimitiveType { Kind: InputPrimitiveTypeKind.PlainDate } => true, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs index e8bd8c2f96b..c30d1aa2e44 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs @@ -193,7 +193,8 @@ public static void RestorePreviousParameterNames( if (inputParameter is not null && (string.Equals(parameter.Name, inputParameter.Name, StringComparison.Ordinal) || (inputParameter is InputMethodParameter { IsExactName: false } && - string.Equals(parameter.Name, inputParameter.Name.NormalizeDateTimeSuffix(inputParameter.Type), StringComparison.Ordinal)))) + inputParameter.Type.IsDateTimeInputType() && + string.Equals(parameter.Name, inputParameter.Name.NormalizeDateTimeSuffix(), StringComparison.Ordinal)))) { var originalName = inputParameter.OriginalName; if (!string.IsNullOrEmpty(originalName)) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs index 2cd28132ecc..e39797743e9 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs @@ -104,7 +104,7 @@ private static IEnumerable DateTimeParameterNameTestCases() InputPrimitiveType.String); yield return new TestCaseData("startTime", dateTime, false, "startOn"); - yield return new TestCaseData("creationTimestamp", dateTime, false, "creationOn"); + yield return new TestCaseData("createdAt", dateTime, false, "createdOn"); yield return new TestCaseData("timestamp", dateTime, false, "on"); yield return new TestCaseData("date", InputPrimitiveType.PlainDate, false, "on"); yield return new TestCaseData("modifiedAt", dateTime.WithNullable(true), false, "modifiedOn"); @@ -114,7 +114,7 @@ private static IEnumerable DateTimeParameterNameTestCases() yield return new TestCaseData("recoveryPointInTime", dateTime, false, "recoveryPointInTime"); yield return new TestCaseData("startTime", InputPrimitiveType.String, false, "startTime"); yield return new TestCaseData("creationTimestamp", InputPrimitiveType.String, false, "creationTimestamp"); - yield return new TestCaseData("creationTimestamp", dateTime, true, "creationTimestamp"); + yield return new TestCaseData("createdAt", dateTime, true, "createdAt"); } private static IEnumerable NotEqualsTestCases() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs index d245dec0734..1f289017aba 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading.Tasks; using Microsoft.TypeSpec.Generator.Input; using Microsoft.TypeSpec.Generator.Input.Extensions; using Microsoft.TypeSpec.Generator.Primitives; @@ -150,32 +151,29 @@ public void TestPropertyNameNormalizesDateTimeSuffix( } [Test] - public void TestPropertyNamePreservesLastContractDateTimeSuffix() + public async Task TestPropertyNamePreservesLastContractDateTimeSuffix() { - var lastContract = new TestTypeProvider(properties: - [ - new PropertyProvider( - description: null, - modifiers: MethodSignatureModifiers.Public, - type: typeof(DateTimeOffset), - name: "StartTime", - body: new AutoPropertyBody(HasSetter: true), - enclosingType: TestTypeProvider.Empty) - ]); - var enclosingType = new TestTypeProviderWithLastContract(lastContract); - var inputProperty = InputFactory.Property( - "StartTime", - new InputDateTimeType( + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var inputModel = InputFactory.Model( + "TestModel", + @namespace: "Test", + properties: + [ + InputFactory.Property( + "StartTime", + new InputDateTimeType( DateTimeKnownEncoding.Rfc3339, "utcDateTime", "TypeSpec.utcDateTime", InputPrimitiveType.String), - isRequired: true); - InputFactory.Model("TestModel", properties: [inputProperty]); + isRequired: true) + ]); - var property = new PropertyProvider(inputProperty, enclosingType); + var modelProvider = new ModelProvider(inputModel); + var actual = new TypeProviderWriter(modelProvider).Write().Content; - Assert.AreEqual("StartTime", property.Name); + Assert.AreEqual(Helpers.GetExpectedFromFile("Expected"), actual); } [TestCaseSource(nameof(CollectionPropertyTestCases))] @@ -260,7 +258,7 @@ private static IEnumerable DateTimePropertyNameTestCases() InputPrimitiveType.String); yield return new TestCaseData("StartTime", dateTime, false, "StartOn"); - yield return new TestCaseData("CreationDateTime", dateTime, false, "CreationOn"); + yield return new TestCaseData("CreatedAt", dateTime, false, "CreatedOn"); yield return new TestCaseData("DeletionTimestamp", dateTime, false, "DeletionOn"); yield return new TestCaseData("ModificationTimeStamp", dateTime, false, "ModificationOn"); yield return new TestCaseData("Timestamp", dateTime, false, "On"); @@ -276,19 +274,6 @@ private static IEnumerable DateTimePropertyNameTestCases() yield return new TestCaseData("CreationTimestamp", dateTime, true, "CreationTimestamp"); } - private sealed class TestTypeProviderWithLastContract : TestTypeProvider - { - private readonly TypeProvider _lastContract; - - public TestTypeProviderWithLastContract(TypeProvider lastContract) - { - _lastContract = lastContract; - } - - private protected override TypeProvider? BuildLastContractView( - string? generatedTypeName = default, - string? generatedTypeNamespace = default) => _lastContract; - } [Test] public void CanUpdatePropertyProvider() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesLastContractDateTimeSuffix(Expected).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesLastContractDateTimeSuffix(Expected).cs new file mode 100644 index 00000000000..2bc79bbeadc --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesLastContractDateTimeSuffix(Expected).cs @@ -0,0 +1,27 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; + +namespace Test +{ + public partial class TestModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public TestModel(global::System.DateTimeOffset startTime) + { + StartTime = startTime; + } + + internal TestModel(global::System.DateTimeOffset startTime, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + StartTime = startTime; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public global::System.DateTimeOffset StartTime { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesLastContractDateTimeSuffix/TestModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesLastContractDateTimeSuffix/TestModel.cs new file mode 100644 index 00000000000..d3860bb862d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesLastContractDateTimeSuffix/TestModel.cs @@ -0,0 +1,7 @@ +namespace Test +{ + public class TestModel + { + public System.DateTimeOffset StartTime { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/RestorePreviousParameterNamesRestoresNormalizedMethodParameterName(Expected).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/RestorePreviousParameterNamesRestoresNormalizedMethodParameterName(Expected).cs new file mode 100644 index 00000000000..88494a19a32 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/TypeProviderTests/RestorePreviousParameterNamesRestoresNormalizedMethodParameterName(Expected).cs @@ -0,0 +1,16 @@ +// + +#nullable disable + +using System; + +namespace Test +{ + public partial class TestClient + { + public string Foo(global::System.DateTimeOffset startTime) + { + return null; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs index b8948ca88de..303f8157a8a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TypeProviderTests.cs @@ -805,7 +805,8 @@ public async Task RestorePreviousParameterNamesRestoresNormalizedMethodParameter BackCompatHelper.RestorePreviousParameterNames(typeProvider, typeProvider.Methods); - Assert.AreEqual("startTime", parameter.Name); + var actual = new TypeProviderWriter(typeProvider).Write().Content; + Assert.AreEqual(Helpers.GetExpectedFromFile("Expected"), actual); } // A reorder combined with a casing-only rename must still restore the published spelling. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/CSharpNameExtensionsTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/CSharpNameExtensionsTests.cs index 25c54e0b0fc..3cf3937eac2 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/CSharpNameExtensionsTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Utilities/CSharpNameExtensionsTests.cs @@ -25,7 +25,13 @@ public void TestNormalizeCSharpUrlSuffix(string name, string expected) [TestCaseSource(nameof(DateTimeNameTestCases))] public void TestNormalizeDateTimeSuffix(string name, InputType type, string expected) { - Assert.AreEqual(expected, name.NormalizeDateTimeSuffix(type)); + Assert.AreEqual(expected, type.IsDateTimeInputType() ? name.NormalizeDateTimeSuffix() : name); + } + + [Test] + public void NormalizeCSharpAcronymsNormalizesDateTimeSuffixInSinglePass() + { + Assert.AreEqual("IPStartOn", "IpStartTime".NormalizeCSharpAcronyms(normalizeDateTimeSuffix: true)); } private static IEnumerable DateTimeNameTestCases() @@ -44,6 +50,12 @@ private static IEnumerable DateTimeNameTestCases() yield return new TestCaseData("pointInTime", dateTime, "pointInTime"); yield return new TestCaseData("recoveryPointInTime", dateTime, "recoveryPointInTime"); yield return new TestCaseData("startTime", InputPrimitiveType.String, "startTime"); + yield return new TestCaseData("createdAt", dateTime, "createdOn"); + yield return new TestCaseData("expiresAt", dateTime, "expiresOn"); + yield return new TestCaseData("deletedTime", dateTime, "deletedOn"); + yield return new TestCaseData("finishedTime", dateTime, "finishedOn"); + yield return new TestCaseData("stateTransitionTime", dateTime, "stateTransitionOn"); + yield return new TestCaseData("notBefore", dateTime, "notBefore"); } } } From ea2410205e6ae978329d42a91d241f636dba959a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:55:58 +0000 Subject: [PATCH 6/8] fix: simplify date naming normalization Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Shared/CSharpNameExtensions.cs | 117 +++++++----------- .../src/Utilities/BackCompatHelper.cs | 11 +- 2 files changed, 50 insertions(+), 78 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs index c9d9a40b103..f1896e15e89 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Shared/CSharpNameExtensions.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text; using Microsoft.TypeSpec.Generator.Input; @@ -11,18 +10,6 @@ namespace Microsoft.TypeSpec.Generator.Utilities { internal static class CSharpNameExtensions { - private const string DateSuffix = "Date"; - private const string DateTimeSuffix = "DateTime"; - private const string FromName = "From"; - private const string LowercaseOnSuffix = "on"; - private const string OnSuffix = "On"; - private const string PointInTimeName = "PointInTime"; - private const string TimeStampSuffix = "TimeStamp"; - private const string TimeSuffix = "Time"; - private const string TimestampSuffix = "Timestamp"; - private const string ToName = "To"; - private const string AtSuffix = "At"; - private static readonly (string Source, string Replacement)[] _acronymRenamingRules = [ ("Ipv4", "IPv4"), @@ -34,19 +21,10 @@ private static readonly (string Source, string Replacement)[] _acronymRenamingRu ("Os", "OS") ]; - private static readonly HashSet _dateTimeNameExclusions = new(StringComparer.OrdinalIgnoreCase) - { - FromName, - ToName, - PointInTimeName - }; - public static string NormalizeCSharpAcronyms(this string name, bool normalizeDateTimeSuffix = false) { - var suffixLength = normalizeDateTimeSuffix && !HasExcludedDateTimeNameComponent(name) - ? GetDateTimeSuffixLength(name) - : 0; - StringBuilder? normalizedName = suffixLength > 0 ? new(name.Length - suffixLength + OnSuffix.Length) : null; + name = normalizeDateTimeSuffix ? name.NormalizeDateTimeSuffix() : name; + StringBuilder? normalizedName = null; int segmentStart = 0; for (int index = 0; index < name.Length - 1; index++) { @@ -77,79 +55,80 @@ public static string NormalizeCSharpAcronyms(this string name, bool normalizeDat return name; } - normalizedName.Append(name, segmentStart, name.Length - suffixLength - segmentStart); - if (suffixLength > 0) - { - normalizedName.Append(name.Length == suffixLength && char.IsLower(name[0]) ? LowercaseOnSuffix : OnSuffix); - } + normalizedName.Append(name, segmentStart, name.Length - segmentStart); return normalizedName.ToString(); } public static string NormalizeDateTimeSuffix(this string name) { - if (HasExcludedDateTimeNameComponent(name)) + if (DateTimeNameRules.HasExcludedComponent(name)) { return name; } - var suffixLength = GetDateTimeSuffixLength(name); + var suffixLength = DateTimeNameRules.GetSuffixLength(name); if (suffixLength == 0) { return name; } var prefix = name[..^suffixLength]; - var onSuffix = prefix.Length == 0 && char.IsLower(name[0]) ? LowercaseOnSuffix : OnSuffix; + var onSuffix = prefix.Length == 0 && char.IsLower(name[0]) + ? DateTimeNameRules.LowercaseOnSuffix + : DateTimeNameRules.OnSuffix; return prefix + onSuffix; } - private static bool HasExcludedDateTimeNameComponent(string name) + private static class DateTimeNameRules { - var lookup = _dateTimeNameExclusions.GetAlternateLookup>(); - return (name.Length >= FromName.Length && lookup.Contains(name.AsSpan(0, FromName.Length))) || - (name.Length >= ToName.Length && lookup.Contains(name.AsSpan(0, ToName.Length))) || - (name.Length >= PointInTimeName.Length && lookup.Contains(name.AsSpan(^PointInTimeName.Length))); - } - - private static int GetDateTimeSuffixLength(string name) - { - if (name.EndsWith(TimestampSuffix, StringComparison.Ordinal) || - name.EndsWith(TimeStampSuffix, StringComparison.Ordinal)) - { - return TimestampSuffix.Length; - } - - if (name.Equals(TimestampSuffix, StringComparison.OrdinalIgnoreCase)) + 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 TimeStampSuffix = "TimeStamp"; + private const string TimeSuffix = "Time"; + private const string TimestampSuffix = "Timestamp"; + private const string ToName = "To"; + + internal static bool HasExcludedComponent(string name) { - return TimestampSuffix.Length; + return name.StartsWith(FromName, StringComparison.OrdinalIgnoreCase) || + name.StartsWith(ToName, StringComparison.OrdinalIgnoreCase) || + name.EndsWith(PointInTimeName, StringComparison.OrdinalIgnoreCase); } - if (name.Length > DateTimeSuffix.Length && name.EndsWith(DateTimeSuffix, StringComparison.Ordinal)) + internal static int GetSuffixLength(string name) { - return DateTimeSuffix.Length; - } + if (name.EndsWith(TimestampSuffix, StringComparison.Ordinal) || + name.EndsWith(TimeStampSuffix, StringComparison.Ordinal) || + name.Equals(TimestampSuffix, StringComparison.OrdinalIgnoreCase)) + { + return TimestampSuffix.Length; + } - if (name.Length > TimeSuffix.Length && name.EndsWith(TimeSuffix, StringComparison.Ordinal)) - { - return TimeSuffix.Length; - } + if (name.Length > DateTimeSuffix.Length && name.EndsWith(DateTimeSuffix, StringComparison.Ordinal)) + { + return DateTimeSuffix.Length; + } - if (name.Equals(DateSuffix, StringComparison.OrdinalIgnoreCase)) - { - return DateSuffix.Length; - } + if (name.Length > TimeSuffix.Length && name.EndsWith(TimeSuffix, StringComparison.Ordinal)) + { + return TimeSuffix.Length; + } - if (name.EndsWith(DateSuffix, StringComparison.Ordinal)) - { - return DateSuffix.Length; - } + if (name.Equals(DateSuffix, StringComparison.OrdinalIgnoreCase) || + name.EndsWith(DateSuffix, StringComparison.Ordinal)) + { + return DateSuffix.Length; + } - if (name.Length > AtSuffix.Length && name.EndsWith(AtSuffix, StringComparison.Ordinal)) - { - return AtSuffix.Length; + return name.Length > AtSuffix.Length && name.EndsWith(AtSuffix, StringComparison.Ordinal) + ? AtSuffix.Length + : 0; } - - return 0; } public static bool IsDateTimeInputType(this InputType inputType) => inputType switch diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs index c30d1aa2e44..3dfb91b1cf7 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs @@ -191,16 +191,9 @@ public static void RestorePreviousParameterNames( var inputParameter = parameter.InputParameter; if (inputParameter is not null && - (string.Equals(parameter.Name, inputParameter.Name, StringComparison.Ordinal) || - (inputParameter is InputMethodParameter { IsExactName: false } && - inputParameter.Type.IsDateTimeInputType() && - string.Equals(parameter.Name, inputParameter.Name.NormalizeDateTimeSuffix(), StringComparison.Ordinal)))) + string.Equals(inputParameter.Name, inputParameter.OriginalName, StringComparison.Ordinal)) { - var originalName = inputParameter.OriginalName; - 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 From b02cbaf6ef6d6f651d2b857c468bd311ce9d0871 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:09:21 +0000 Subject: [PATCH 7/8] fix: refine date naming tests Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Utilities/BackCompatHelper.cs | 3 +- .../test/Providers/ParameterProviderTests.cs | 32 ++++++++++++------- .../test/Providers/PropertyProviderTests.cs | 2 +- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs index 3dfb91b1cf7..b08a6bd9368 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs @@ -190,8 +190,7 @@ public static void RestorePreviousParameterNames( string? preservedName = null; var inputParameter = parameter.InputParameter; - if (inputParameter is not null && - string.Equals(inputParameter.Name, inputParameter.OriginalName, StringComparison.Ordinal)) + if (inputParameter is not null) { preservedName = FindPreviousParameterName(lastContractView, inputParameter.OriginalName, method.Signature.Name); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs index e39797743e9..a3fe1cea310 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ParameterProviderTests.cs @@ -103,18 +103,26 @@ private static IEnumerable DateTimeParameterNameTestCases() "TypeSpec.utcDateTime", InputPrimitiveType.String); - yield return new TestCaseData("startTime", dateTime, false, "startOn"); - yield return new TestCaseData("createdAt", dateTime, false, "createdOn"); - yield return new TestCaseData("timestamp", dateTime, false, "on"); - yield return new TestCaseData("date", InputPrimitiveType.PlainDate, false, "on"); - yield return new TestCaseData("modifiedAt", dateTime.WithNullable(true), false, "modifiedOn"); - yield return new TestCaseData("fromTime", dateTime, false, "fromTime"); - yield return new TestCaseData("toDate", dateTime, false, "toDate"); - yield return new TestCaseData("pointInTime", dateTime, false, "pointInTime"); - yield return new TestCaseData("recoveryPointInTime", dateTime, false, "recoveryPointInTime"); - yield return new TestCaseData("startTime", InputPrimitiveType.String, false, "startTime"); - yield return new TestCaseData("creationTimestamp", InputPrimitiveType.String, false, "creationTimestamp"); - yield return new TestCaseData("createdAt", dateTime, true, "createdAt"); + var testCases = new (string Name, InputType Type, string NormalizedName)[] + { + ("startTime", dateTime, "startOn"), + ("createdAt", dateTime, "createdOn"), + ("timestamp", dateTime, "on"), + ("date", InputPrimitiveType.PlainDate, "on"), + ("modifiedAt", dateTime.WithNullable(true), "modifiedOn"), + ("fromTime", dateTime, "fromTime"), + ("toDate", dateTime, "toDate"), + ("pointInTime", dateTime, "pointInTime"), + ("recoveryPointInTime", dateTime, "recoveryPointInTime"), + ("startTime", InputPrimitiveType.String, "startTime"), + ("creationTimestamp", InputPrimitiveType.String, "creationTimestamp") + }; + + foreach (var testCase in testCases) + { + yield return new TestCaseData(testCase.Name, testCase.Type, false, testCase.NormalizedName); + yield return new TestCaseData(testCase.Name, testCase.Type, true, testCase.Name); + } } private static IEnumerable NotEqualsTestCases() diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs index 1f289017aba..5d755ab0c34 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/PropertyProviderTests.cs @@ -263,7 +263,7 @@ private static IEnumerable DateTimePropertyNameTestCases() yield return new TestCaseData("ModificationTimeStamp", dateTime, false, "ModificationOn"); yield return new TestCaseData("Timestamp", dateTime, false, "On"); yield return new TestCaseData("ExpirationDate", dateTime, false, "ExpirationOn"); - yield return new TestCaseData("RecordedAt", dateTime, false, "RecordedOn"); + yield return new TestCaseData("RecordedAt", InputPrimitiveType.String, false, "RecordedAt"); yield return new TestCaseData("Date", InputPrimitiveType.PlainDate, false, "On"); yield return new TestCaseData("SnapshotTimestamp", dateTime.WithNullable(true), false, "SnapshotOn"); yield return new TestCaseData("FromTime", dateTime, false, "FromTime"); From de464c1e06ca30216d49c598b903100318ffd5ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:42:54 +0000 Subject: [PATCH 8/8] fix: preserve custom back compat parameter names Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Utilities/BackCompatHelper.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs index b08a6bd9368..6798da935bc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs @@ -190,7 +190,11 @@ public static void RestorePreviousParameterNames( string? preservedName = null; var inputParameter = parameter.InputParameter; - if (inputParameter is not null) + if (inputParameter is not null && + (string.Equals(parameter.Name, inputParameter.Name, StringComparison.Ordinal) || + (inputParameter is InputMethodParameter { IsExactName: false } && + inputParameter.Type.IsDateTimeInputType() && + string.Equals(parameter.Name, inputParameter.Name.NormalizeDateTimeSuffix(), StringComparison.Ordinal)))) { preservedName = FindPreviousParameterName(lastContractView, inputParameter.OriginalName, method.Signature.Name); }