diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs index 4020a979bb3..0446fcef30d 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/CanonicalTypeProvider.cs @@ -18,6 +18,7 @@ internal class CanonicalTypeProvider : TypeProvider { private readonly TypeProvider _generatedTypeProvider; private readonly Dictionary _specPropertiesMap; + private readonly HashSet _exactSpecPropertyNames; private readonly Dictionary _serializedNameMap; private readonly Dictionary _propertyProviderMap = new(); private readonly HashSet _renamedProperties; @@ -30,14 +31,17 @@ public CanonicalTypeProvider(TypeProvider generatedTypeProvider, InputType? inpu var inputModel = inputType as InputModelType; _specProperties = inputModel?.Properties ?? []; _specPropertiesMap = []; + _exactSpecPropertyNames = []; foreach (var property in _specProperties) { var name = property.IsExactName ? property.Name : property.Name.ToIdentifierName(); _specPropertiesMap.TryAdd(name, property); - if (!property.IsExactName) - { - _specPropertiesMap.TryAdd(name.NormalizeCSharpAcronyms(property.Type.IsDateTimeInputType()), property); - } + _exactSpecPropertyNames.Add(name); + } + foreach (var property in _specProperties.Where(p => !p.IsExactName)) + { + var name = property.Name.ToIdentifierName(); + _specPropertiesMap.TryAdd(name.NormalizeCSharpAcronyms(property.Type.IsDateTimeInputType()), property); } _serializedNameMap = BuildSerializationNameMap(); _renamedProperties = (_generatedTypeProvider.CustomCodeView?.Properties ?? []) @@ -92,9 +96,23 @@ protected internal override MethodProvider[] BuildMethods() protected internal override PropertyProvider[] BuildProperties() { + // Building Properties populates GeneratedPropertiesBySpecName as part of customization filtering. + // Keep these reads together and in this order so the map is never observed mid-build. var generatedProperties = _generatedTypeProvider.Properties; + var generatedPropertiesBySpecName = _generatedTypeProvider.GeneratedPropertiesBySpecName; var customProperties = _generatedTypeProvider.CustomCodeView?.Properties ?? []; + // Exact emitted names take precedence over canonical aliases. A canonical alias from an earlier + // property must not shadow a later property that actually emits that name. + foreach (var generatedProperty in generatedPropertiesBySpecName.Values) + { + if (generatedProperty.InputProperty is InputModelProperty preservedSpecProperty && + _exactSpecPropertyNames.Add(generatedProperty.Name)) + { + _specPropertiesMap[generatedProperty.Name] = preservedSpecProperty; + } + } + // Update the serializedName of generated properties if necessary foreach (var generatedProperty in generatedProperties) { @@ -163,7 +181,9 @@ protected internal override PropertyProvider[] BuildProperties() // Filter out generated properties that have been customized to avoid duplicates. // This is needed because EnsureBuilt caches members without applying customization // filtering, so _generatedTypeProvider.Properties may contain unfiltered results. - var filteredGeneratedProperties = FilterCustomizedProperties(generatedProperties); + var filteredGeneratedProperties = FilterCustomizedProperties( + generatedProperties, + generatedPropertiesBySpecName); if (_specProperties.Count > 0) { @@ -186,9 +206,9 @@ protected internal override PropertyProvider[] BuildProperties() foreach (var prop in customProperties) { - // Check if custom property is in spec - if (_specPropertiesMap.TryGetValue(prop.Name, out var specProp) || - (prop.OriginalName != null && TryGetSpecProperty(prop.OriginalName, out specProp))) + // Check if custom property is in spec. + if ((prop.OriginalName != null && TryGetSpecProperty(prop.OriginalName, out var specProp)) || + _specPropertiesMap.TryGetValue(prop.Name, out specProp)) { inputProperties.Add(specProp); } 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 248f522c13a..758c7dbe09f 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 @@ -103,20 +103,43 @@ private PropertyProvider(InputProperty inputProperty, CSharpType propertyType, T var hasOutputUsage = inputProperty.EnclosingType?.Usage.HasFlag(InputModelTypeUsage.Output) ?? false; Modifiers = IsDiscriminator || (!hasOutputUsage && _isRequiredNonNullableConstant) ? MethodSignatureModifiers.Internal : MethodSignatureModifiers.Public; var identifierName = inputProperty.IsExactName ? inputProperty.Name : inputProperty.Name.ToIdentifierName(); - var lastContractProperties = enclosingType.LastContractView?.Properties; - var legacyName = identifierName == enclosingType.Name - ? $"{identifierName}Property" - : identifierName; - if (!inputProperty.IsExactName && - (lastContractProperties is null || - !lastContractProperties.Any(p => p.Name == legacyName))) + if (!inputProperty.IsExactName) { - identifierName = identifierName - .NormalizeCSharpAcronyms(inputProperty.Type.IsDateTimeInputType()); + var isDateTime = inputProperty.Type.IsDateTimeInputType(); + var canonicalName = identifierName.NormalizeCSharpAcronyms(isDateTime); + var enclosingTypeName = enclosingType.Name; + var lastContractProperties = enclosingType.LastContractView?.Properties + .Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers)) + .ToList(); + + // An exact input-identifier match is authoritative: it is the name this property would have had + // before normalization, so it does not need the disambiguation required by normalized candidates. + // The shipped member may carry the enclosing-type collision suffix, so candidates are compared + // against the collision-adjusted form of each name we are looking for. + var previousProperty = + lastContractProperties?.FirstOrDefault(p => p.Name == AvoidPropertyNameCollision(identifierName, enclosingTypeName)) + ?? lastContractProperties?.FirstOrDefault(p => + p.Name == AvoidPropertyNameCollision(canonicalName, enclosingTypeName) && + !IsClaimedBySiblingProperty(p.Name, inputProperty, enclosingType)); + + if (previousProperty is null && + isDateTime && + !identifierName.EndsWith("On", StringComparison.Ordinal)) + { + // Both the current and previous conventions render date-time names as On. Requiring + // that suffix on the contract name prevents a removed property such as StartDate from being + // mistaken for the historical name of StartTime even though both normalize to StartsOn. + var specStem = identifierName.NormalizeCSharpAcronyms().GetDateTimeStem(); + previousProperty = specStem is null + ? null + : lastContractProperties?.FirstOrDefault(p => + HasDateTimeStem(p.Name, specStem, enclosingTypeName) && + p.Type.WithNullable(false).Equals(Type.WithNullable(false)) && + !IsClaimedBySiblingProperty(p.Name, inputProperty, enclosingType)); + } + identifierName = previousProperty?.Name ?? canonicalName; } - Name = identifierName == enclosingType.Name - ? $"{identifierName}Property" - : identifierName; + Name = AvoidPropertyNameCollision(identifierName, enclosingType.Name); Body = new AutoPropertyBody(propHasSetter, setterModifier, GetPropertyInitializationValue(propertyType, inputProperty)); WireInfo = new PropertyWireInformation(inputProperty); @@ -180,6 +203,95 @@ private void BuildDocs() } } + private static string AvoidPropertyNameCollision(string propertyName, string enclosingTypeName) => + propertyName == enclosingTypeName ? $"{propertyName}Property" : propertyName; + + private static bool HasDateTimeStem(string contractName, string specStem, string enclosingTypeName) + { + if (contractName == AvoidPropertyNameCollision(enclosingTypeName, enclosingTypeName)) + { + contractName = enclosingTypeName; + } + + return contractName.EndsWith("On", StringComparison.Ordinal) && + contractName.GetDateTimeStem() == specStem; + } + + private static bool IsClaimedBySiblingProperty( + string contractName, + InputProperty inputProperty, + TypeProvider enclosingType) + { + var enclosingTypeName = enclosingType.Name; + + foreach (var sibling in inputProperty.EnclosingType?.Properties ?? []) + { + if (ReferenceEquals(sibling, inputProperty)) + { + continue; + } + + if (sibling.IsExactName) + { + if (AvoidPropertyNameCollision(sibling.Name, enclosingTypeName) == contractName) + { + return true; + } + + continue; + } + + // Compare both the raw identifier and the name the sibling actually emits, since acronym + // normalization can move it onto the shipped name even when the identifier does not match. + var siblingIdentifier = sibling.Name.ToIdentifierName(); + var siblingGenerated = siblingIdentifier.NormalizeCSharpAcronyms(sibling.Type.IsDateTimeInputType()); + if (AvoidPropertyNameCollision(siblingIdentifier, enclosingTypeName) == contractName || + AvoidPropertyNameCollision(siblingGenerated, enclosingTypeName) == contractName) + { + return true; + } + } + + // A customization that explicitly targets a different spec property owns its public name outright. + // A raw-name customization is excluded: it has no declared target, so it is intended to replace + // whichever generated property ends up with that name, including a preserved one. Fields are + // included because customization filtering treats a custom field as a claim on a property name. + foreach (var customName in GetExplicitlyRenamedCustomNames(enclosingType)) + { + if (customName.OriginalName == inputProperty.Name || + customName.OriginalName == inputProperty.Name.ToIdentifierName()) + { + continue; + } + + if (customName.Name == contractName) + { + return true; + } + } + + return false; + } + + private static IEnumerable<(string Name, string OriginalName)> GetExplicitlyRenamedCustomNames(TypeProvider enclosingType) + { + foreach (var customProperty in enclosingType.CustomCodeView?.Properties ?? []) + { + if (customProperty.OriginalName is { } propertyOriginalName) + { + yield return (customProperty.Name, propertyOriginalName); + } + } + + foreach (var customField in enclosingType.CustomCodeView?.Fields ?? []) + { + if (customField.OriginalName is { } fieldOriginalName) + { + yield return (customField.Name, fieldOriginalName); + } + } + } + private static bool IsPropertyPrivate(MethodSignatureModifiers modifiers, TypeSignatureModifiers enclosingTypeModifiers) { return (modifiers.HasFlag(MethodSignatureModifiers.Private) && !modifiers.HasFlag(MethodSignatureModifiers.Protected)) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index b78ed2f5757..564e3bbd43f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -25,6 +25,7 @@ public abstract class TypeProvider private Lazy _specView; private Lazy _declaringTypeName; private readonly InputType? _inputType; + private readonly Dictionary _generatedPropertiesBySpecName = new(StringComparer.Ordinal); protected TypeProvider(InputType? inputType = default) { @@ -433,23 +434,82 @@ protected static bool IsResolvableBuildableType(CSharpType type) => protected virtual CSharpType[] GetTypeArguments() => []; - internal PropertyProvider[] FilterCustomizedProperties(IEnumerable specProperties) + internal PropertyProvider[] FilterCustomizedProperties( + IEnumerable specProperties, + IReadOnlyDictionary? knownSpecProperties = null) { - var specPropertiesByName = BuildSpecPropertiesByName(specProperties); + var propertiesToFilter = specProperties as IReadOnlyList ?? [.. specProperties]; + var activeProperties = new HashSet( + propertiesToFilter, + ReferenceEqualityComparer.Instance); + + if (knownSpecProperties is not null) + { + // The supplied map came from the complete generated property set and already resolved exact-name + // precedence. Do not rebuild it from this potentially filtered subset. + foreach (var (name, property) in knownSpecProperties) + { + _generatedPropertiesBySpecName[name] = property; + } + } + else + { + var exactNames = new HashSet(StringComparer.Ordinal); + var normalizedNames = new List<(string Name, PropertyProvider Property)>(); + foreach (var specProperty in propertiesToFilter) + { + var inputProperty = specProperty.InputProperty; + if (inputProperty is null) + { + continue; + } + + if (inputProperty.IsExactName) + { + AddExactName(inputProperty.Name, specProperty); + continue; + } + + var identifierName = inputProperty.Name.ToIdentifierName(); + // The first exact identity in this pass replaces any stale alias, while a later exact + // collision keeps the established first-wins behavior. + AddExactName(inputProperty.Name, specProperty); + AddExactName(identifierName, specProperty); + normalizedNames.Add(( + identifierName.NormalizeCSharpAcronyms(inputProperty.Type.IsDateTimeInputType()), + specProperty)); + } + + // Canonical names are aliases. Add them only after all exact names have been registered so an + // alias such as startTime -> StartsOn cannot shadow a separate startsOn property. + foreach (var (name, property) in normalizedNames) + { + _generatedPropertiesBySpecName.TryAdd(name, property); + } + + void AddExactName(string name, PropertyProvider property) + { + if (exactNames.Add(name)) + { + _generatedPropertiesBySpecName[name] = property; + } + } + } + var properties = new List(); var customProperties = new HashSet(); foreach (var customProperty in BuildAllCustomProperties()) { - AddCustomName(customProperties, customProperty.Name, customProperty.OriginalName, specPropertiesByName); + AddCustomName(customProperties, customProperty.Name, customProperty.OriginalName, _generatedPropertiesBySpecName, activeProperties); } foreach (var customField in BuildAllCustomFields()) { - AddCustomName(customProperties, customField.Name, customField.OriginalName, specPropertiesByName); + AddCustomName(customProperties, customField.Name, customField.OriginalName, _generatedPropertiesBySpecName, activeProperties); } - foreach (var property in specProperties) + foreach (var property in propertiesToFilter) { if (ShouldGenerate(property, customProperties)) { @@ -464,44 +524,40 @@ private static void AddCustomName( HashSet customNames, string name, string? originalName, - IReadOnlyDictionary specPropertiesByName) + IReadOnlyDictionary generatedPropertiesBySpecName, + IReadOnlySet activeProperties) { - customNames.Add(name); if (originalName is null) { + AddCustomName(customNames, name, generatedPropertiesBySpecName, activeProperties); return; } - customNames.Add(originalName); - if (specPropertiesByName.TryGetValue(originalName, out var inputProperty) && !inputProperty.IsExactName) - { - customNames.Add( - originalName - .ToIdentifierName() - .NormalizeCSharpAcronyms(inputProperty.Type.IsDateTimeInputType())); - } + customNames.Add(name); + AddCustomName(customNames, originalName, generatedPropertiesBySpecName, activeProperties); } - private static IReadOnlyDictionary BuildSpecPropertiesByName(IEnumerable specProperties) + private static void AddCustomName( + HashSet customNames, + string name, + IReadOnlyDictionary generatedPropertiesBySpecName, + IReadOnlySet activeProperties) { - var specPropertiesByName = new Dictionary(StringComparer.Ordinal); - - foreach (var specProperty in specProperties) + customNames.Add(name); + if (generatedPropertiesBySpecName.TryGetValue(name, out var generatedProperty) && + activeProperties.Contains(generatedProperty)) { - var inputProperty = specProperty.InputProperty; - if (inputProperty is null) - { - continue; - } - - var identifierName = inputProperty.Name.ToIdentifierName(); - specPropertiesByName.TryAdd(inputProperty.Name, inputProperty); - specPropertiesByName.TryAdd(identifierName, inputProperty); + customNames.Add(generatedProperty.Name); } - - return specPropertiesByName; } + /// + /// Maps the spec names a customization can reference to the property the generator built from that spec + /// property, as captured by before the properties replaced by + /// custom code were filtered out. Callers must build before reading this map. + /// + internal IReadOnlyDictionary GeneratedPropertiesBySpecName => _generatedPropertiesBySpecName; + internal FieldProvider[] FilterCustomizedFields(IEnumerable specFields) { var fields = new List(); @@ -724,6 +780,7 @@ public virtual void Reset() { _methods = null; _properties = null; + _generatedPropertiesBySpecName.Clear(); _fields = null; ResetConstructors(); _implements = null; 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 62d1a2aa806..fe73f4bf19c 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; @@ -77,6 +76,17 @@ public static string NormalizeDateTimeSuffix(this string name) return prefix + onSuffix; } + /// + /// Gets the normalized semantic stem of a date-time name by removing its recognized suffix. + /// + public static string? GetDateTimeStem(this string name) + { + var suffixLength = DateTimeNameRules.GetSuffixLength(name); + return suffixLength == 0 || suffixLength == name.Length + ? null + : DateTimeNameRules.ToVerbForm(name[..^suffixLength]); + } + private static class DateTimeNameRules { private const string AtSuffix = "At"; @@ -95,28 +105,36 @@ private static class DateTimeNameRules 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 _nounToVerbMap = new(StringComparer.OrdinalIgnoreCase) - { - ["Change"] = "Changed", - ["Creation"] = "Created", - ["Deletion"] = "Deleted", - ["End"] = "Ends", - ["Expiration"] = "Expires", - ["Modification"] = "Modified", - ["Start"] = "Starts" - }; + // Complete prefixes that read better as verbs when combined with the "On" suffix. Keep the + // collection ordered so adding overlapping suffixes in the future cannot make compound matching + // depend on dictionary enumeration order. + private static readonly (string Noun, string Verb)[] _nounToVerbRules = + [ + ("Change", "Changed"), + ("Creation", "Created"), + ("Deletion", "Deleted"), + ("End", "Ends"), + ("Expiration", "Expires"), + ("Expire", "Expires"), + ("Modification", "Modified"), + ("Start", "Starts") + ]; internal static string ToVerbForm(string prefix) { - if (_nounToVerbMap.TryGetValue(prefix, out var verb)) + // Resolve all exact rules before considering compound suffixes so an overlapping rule added + // later cannot be preempted by an earlier compound match. + foreach (var (noun, verb) in _nounToVerbRules) { - return char.IsLower(prefix[0]) - ? char.ToLowerInvariant(verb[0]) + verb[1..] - : verb; + if (prefix.Equals(noun, StringComparison.OrdinalIgnoreCase)) + { + return char.IsLower(prefix[0]) + ? char.ToLowerInvariant(verb[0]) + verb[1..] + : verb; + } } - foreach (var (noun, compoundVerb) in _nounToVerbMap) + foreach (var (noun, compoundVerb) in _nounToVerbRules) { if (prefix.Length > noun.Length && prefix.EndsWith(noun, StringComparison.OrdinalIgnoreCase) && diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelCustomizationTests.cs index 5dfa310e73a..cefa2e2685f 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelCustomizationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelCustomizationTests.cs @@ -205,6 +205,415 @@ public async Task CustomCodeReplacesDateNormalizedPropertyUsingRawSpecName() Assert.AreEqual($"{Helpers.GetExpectedFromFile("Expected")}\n", new TypeProviderWriter(modelTypeProvider).Write().Content); } + [Test] + public async Task CustomCodeWithRawDateTimeNameReplacesNormalizedProperty() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var inputModel = InputFactory.Model( + "mockInputModel", + properties: [InputFactory.Property("valueDate", dateTime)]); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + + Assert.AreEqual("ValueDate", modelTypeProvider.CustomCodeView!.Properties.Single().Name); + Assert.AreEqual(0, modelTypeProvider.Properties.Count); + Assert.AreEqual("ValueDate", modelTypeProvider.CanonicalView!.Properties.Single().Name); + Assert.AreEqual($"{Helpers.GetExpectedFromFile("Expected")}\n", new TypeProviderWriter(modelTypeProvider).Write().Content); + } + + [Test] + public async Task CustomCodeWithSpecNameReplacesLastContractPreservedProperty() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var inputModel = InputFactory.Model( + "mockInputModel", + properties: [InputFactory.Property("startTime", dateTime, isRequired: true)]); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Last")); + + var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + + // The spec property is generated as the shipped "StartOn" rather than the canonical "StartsOn", + // so the raw-name customization must suppress "StartOn". + Assert.AreEqual("StartTime", modelTypeProvider.CustomCodeView!.Properties.Single().Name); + Assert.AreEqual(0, modelTypeProvider.Properties.Count); + + var canonicalProperty = modelTypeProvider.CanonicalView!.Properties.Single(); + Assert.AreEqual("StartTime", canonicalProperty.Name); + Assert.AreEqual("startTime", canonicalProperty.WireInfo!.SerializedName); + Assert.AreEqual($"{Helpers.GetExpectedFromFile("Expected")}\n", new TypeProviderWriter(modelTypeProvider).Write().Content); + } + + [Test] + public async Task CustomCodeWithLastContractNamePreservesWireInfo() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var inputModel = InputFactory.Model( + "mockInputModel", + properties: [InputFactory.Property("startTime", dateTime, isRequired: true)]); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Last")); + + var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + + // The customization uses the name preserved from the last contract, which is neither the raw spec + // name nor the canonical name. Suppression succeeds trivially because the custom name matches the + // generated one; what this covers is the reverse lookup, which must still resolve "StartOn" back to + // the "startTime" spec property so the canonical property keeps its wire information. + Assert.AreEqual("StartOn", modelTypeProvider.CustomCodeView!.Properties.Single().Name); + Assert.AreEqual(0, modelTypeProvider.Properties.Count); + + var canonicalProperty = modelTypeProvider.CanonicalView!.Properties.Single(); + Assert.AreEqual("StartOn", canonicalProperty.Name); + Assert.AreEqual("startTime", canonicalProperty.WireInfo!.SerializedName); + Assert.AreEqual($"{Helpers.GetExpectedFromFile("Expected")}\n", new TypeProviderWriter(modelTypeProvider).Write().Content); + } + + [Test] + public async Task CustomCodeRenameWithNameMatchingAnotherSpecPropertyDoesNotSuppressIt() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + + // Two unrelated spec properties. The customization renames "startTime", and the name it renames to + // deliberately collides with the identifier form of the *other* spec property, "valueDate". + // That collision is the point of this test: a custom member's public name must be matched against + // the spec only when it is a raw-name customization with no explicit CodeGenMember target. Without + // that distinction the rename also resolves as a customization of "valueDate" and wrongly suppresses + // its generated property. A non-colliding rename cannot exercise this at all. + var inputModel = InputFactory.Model( + "mockInputModel", + properties: + [ + InputFactory.Property("startTime", dateTime, isRequired: true), + InputFactory.Property("valueDate", dateTime, isRequired: true) + ]); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + + // The customization targets only "startTime", so "valueDate" keeps its generated property. + Assert.AreEqual("ValueDate", modelTypeProvider.CustomCodeView!.Properties.Single().Name); + Assert.That(modelTypeProvider.Properties.Select(p => p.Name), Is.EqualTo(new[] { "ValueOn" })); + + // Downstream construction and serialization consume the canonical view, which must contain both + // the renamed custom member and the untouched generated one. + Assert.That( + modelTypeProvider.CanonicalView!.Properties.Select(p => p.Name), + Is.EquivalentTo(new[] { "ValueDate", "ValueOn" })); + Assert.AreEqual($"{Helpers.GetExpectedFromFile("Expected")}\n", new TypeProviderWriter(modelTypeProvider).Write().Content); + } + + [Test] + public async Task DuplicateExactIdentifierCustomizationKeepsFirstSpecMatch() + { + var inputModel = InputFactory.Model( + "mockInputModel", + properties: + [ + InputFactory.Property("start_time", InputPrimitiveType.String, wireName: "firstWireName"), + InputFactory.Property("startTime", InputPrimitiveType.String, wireName: "secondWireName") + ]); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + var canonicalProperty = modelTypeProvider.CanonicalView!.Properties.Single(); + + // Both spec names collapse to StartTime. Preserve the existing first-wins behavior when resolving + // that exact identifier rather than incidentally changing it while moving aliases to a later pass. + Assert.AreEqual("firstWireName", canonicalProperty.WireInfo!.SerializedName); + } + + [Test] + public async Task DuplicateExactIdentifierCustomizationSuppressesOnlyFirstSpecMatch() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var inputModel = InputFactory.Model( + "mockInputModel", + properties: + [ + InputFactory.Property("start_time", InputPrimitiveType.String, wireName: "firstWireName"), + InputFactory.Property("startTime", dateTime, wireName: "secondWireName") + ]); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync( + method: nameof(DuplicateExactIdentifierCustomizationKeepsFirstSpecMatch))); + + var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + + // StartTime targets the first exact identifier. It must not resolve to the second property's alias + // and suppress that property's distinct StartsOn output as well. + Assert.That(modelTypeProvider.Properties.Select(p => p.Name), Is.EqualTo(new[] { "StartsOn" })); + Assert.That( + modelTypeProvider.CanonicalView!.Properties.Select(p => p.WireInfo!.SerializedName), + Is.EquivalentTo(new[] { "firstWireName", "secondWireName" })); + } + + [Test] + public async Task RefilteringPreservesCustomCodeSpecAssociation() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var inputModel = InputFactory.Model( + "mockInputModel", + properties: [InputFactory.Property("startTime", dateTime, isRequired: true)]); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Last")); + + var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + + // Mirrors CSharpGen.FilterCustomizedMembers, which re-applies filtering by passing the already + // filtered members back through Update. The generated property suppressed by the customization is + // absent from that list, so the spec-name snapshot must survive the second pass. + modelTypeProvider.Update( + modelTypeProvider.Methods, + modelTypeProvider.Constructors, + modelTypeProvider.Properties, + modelTypeProvider.Fields); + + Assert.That( + modelTypeProvider.GeneratedPropertiesBySpecName.Keys, + Is.EquivalentTo(new[] { "startTime", "StartTime", "StartsOn" })); + + var canonicalProperty = modelTypeProvider.CanonicalView!.Properties.Single(); + Assert.AreEqual("StartOn", canonicalProperty.Name); + Assert.AreEqual("startTime", canonicalProperty.WireInfo!.SerializedName); + } + + [Test] + public async Task RefilteringDoesNotUseStaleAliasToSuppressActiveProperty() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var inputModel = InputFactory.Model( + "mockInputModel", + properties: + [ + InputFactory.Property("startTime", dateTime), + InputFactory.Property("other", InputPrimitiveType.String) + ]); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + var activeProperty = modelTypeProvider.GeneratedPropertiesBySpecName["other"]; + activeProperty.Update(name: "StartsOn"); + + // Simulate a visitor removing the customized startTime property and renaming the remaining property + // before CSharpGen re-applies filtering. The stale startTime -> StartsOn alias must not suppress the + // active property because its mapped provider is no longer in this filtering pass. + modelTypeProvider.Update(properties: [activeProperty]); + + Assert.That(modelTypeProvider.Properties.Select(p => p.Name), Is.EqualTo(new[] { "StartsOn" })); + } + + [Test] + public async Task HistoricalNameClaimedByCustomRenameIsNotReused() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + + // The GA contract shipped "StartOn", which "startTime" would normally preserve. Custom code renames + // the unrelated "foo" onto that same name, so it is no longer available. Preserving it anyway would + // emit two "StartOn" members, and the collision is resolved by dropping the generated property, which + // silently loses "startTime" from the output entirely. + var inputModel = InputFactory.Model( + "mockInputModel", + properties: + [ + InputFactory.Property("startTime", dateTime, isRequired: true), + InputFactory.Property("foo", InputPrimitiveType.String, isRequired: true) + ]); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Last")); + + var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + + Assert.AreEqual("StartOn", modelTypeProvider.CustomCodeView!.Properties.Single().Name); + + // "startTime" falls back to the canonical name instead of being dropped. + Assert.That(modelTypeProvider.Properties.Select(p => p.Name), Is.EqualTo(new[] { "StartsOn" })); + Assert.That( + modelTypeProvider.CanonicalView!.Properties.Select(p => p.Name), + Is.EquivalentTo(new[] { "StartsOn", "StartOn" })); + Assert.AreEqual($"{Helpers.GetExpectedFromFile("Expected")}\n", new TypeProviderWriter(modelTypeProvider).Write().Content); + } + + [Test] + public async Task CustomCodeWithCanonicalNameReplacesPreservedProperty() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var inputModel = InputFactory.Model( + "mockInputModel", + properties: [InputFactory.Property("startTime", dateTime, isRequired: true)]); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Last")); + + var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + + // The canonical name stays a supported customization target even though the generated property kept + // the shipped "StartOn", so the generated property must still be replaced. + Assert.AreEqual("MyStart", modelTypeProvider.CustomCodeView!.Properties.Single().Name); + Assert.AreEqual(0, modelTypeProvider.Properties.Count); + } + + [Test] + public async Task ExactSpecNameTakesPrecedenceOverCanonicalAliasForCustomization() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var inputModel = InputFactory.Model( + "mockInputModel", + properties: + [ + InputFactory.Property("startTime", dateTime, wireName: "startTime", isRequired: true), + InputFactory.Property("startsOn", dateTime, wireName: "startsOn", isRequired: true) + ]); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Last")); + + var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + + // The raw StartsOn customization targets the exact "startsOn" spec property. It must not resolve + // through the earlier startTime property's canonical StartsOn alias and suppress the preserved StartOn. + Assert.That(modelTypeProvider.Properties.Select(p => p.Name), Is.EqualTo(new[] { "StartOn" })); + + var canonicalProperties = modelTypeProvider.CanonicalView!.Properties.ToDictionary(p => p.Name); + Assert.That(canonicalProperties.Keys, Is.EquivalentTo(new[] { "StartOn", "StartsOn" })); + Assert.AreEqual("startTime", canonicalProperties["StartOn"].WireInfo!.SerializedName); + Assert.AreEqual("startsOn", canonicalProperties["StartsOn"].WireInfo!.SerializedName); + } + + [Test] + public async Task ExactNameSpecPropertyTakesPrecedenceOverCanonicalAliasForCustomization() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var inputModel = InputFactory.Model( + "mockInputModel", + properties: + [ + InputFactory.Property("startTime", dateTime, wireName: "startTime", isRequired: true), + InputFactory.Property("StartsOn", dateTime, wireName: "startsOn", isRequired: true, isExactName: true) + ]); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync( + method: nameof(ExactSpecNameTakesPrecedenceOverCanonicalAliasForCustomization)), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync( + "Last", + method: nameof(ExactSpecNameTakesPrecedenceOverCanonicalAliasForCustomization))); + + var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + + Assert.That(modelTypeProvider.Properties.Select(p => p.Name), Is.EqualTo(new[] { "StartOn" })); + + var canonicalProperties = modelTypeProvider.CanonicalView!.Properties.ToDictionary(p => p.Name); + Assert.That(canonicalProperties.Keys, Is.EquivalentTo(new[] { "StartOn", "StartsOn" })); + Assert.AreEqual("startTime", canonicalProperties["StartOn"].WireInfo!.SerializedName); + Assert.AreEqual("startsOn", canonicalProperties["StartsOn"].WireInfo!.SerializedName); + } + + [Test] + public async Task HistoricalNameClaimedByRenamedCustomFieldIsNotReused() + { + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var inputModel = InputFactory.Model( + "mockInputModel", + properties: + [ + InputFactory.Property("startTime", dateTime, isRequired: true), + InputFactory.Property("foo", InputPrimitiveType.String, isRequired: true) + ]); + + var mockGenerator = await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync("Last")); + + var modelTypeProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel"); + + // A custom field renamed onto the shipped name claims it just as a custom property would, because + // customization filtering treats custom fields as claims on property names. + Assert.AreEqual("StartOn", modelTypeProvider.CustomCodeView!.Fields.Single().Name); + Assert.That(modelTypeProvider.Properties.Select(p => p.Name), Is.EqualTo(new[] { "StartsOn" })); + } + [Test] public async Task CustomCodeReplacesAcronymNormalizedProperty() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeRenameWithNameMatchingAnotherSpecPropertyDoesNotSuppressIt(Expected).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeRenameWithNameMatchingAnotherSpecPropertyDoesNotSuppressIt(Expected).cs new file mode 100644 index 00000000000..8d284a3be9d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeRenameWithNameMatchingAnotherSpecPropertyDoesNotSuppressIt(Expected).cs @@ -0,0 +1,29 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; + +namespace Sample.Models +{ + public partial class MockInputModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public MockInputModel(global::System.DateTimeOffset valueDate, global::System.DateTimeOffset valueOn) + { + ValueDate = valueDate; + ValueOn = valueOn; + } + + internal MockInputModel(global::System.DateTimeOffset valueDate, global::System.DateTimeOffset valueOn, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + ValueDate = valueDate; + ValueOn = valueOn; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public global::System.DateTimeOffset ValueOn { get; set; } + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeRenameWithNameMatchingAnotherSpecPropertyDoesNotSuppressIt/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeRenameWithNameMatchingAnotherSpecPropertyDoesNotSuppressIt/MockInputModel.cs new file mode 100644 index 00000000000..32a60aae939 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeRenameWithNameMatchingAnotherSpecPropertyDoesNotSuppressIt/MockInputModel.cs @@ -0,0 +1,12 @@ +#nullable disable + +using Microsoft.TypeSpec.Generator.Customizations; + +namespace Sample.Models +{ + public partial class MockInputModel + { + [CodeGenMember("startTime")] + public System.DateTimeOffset ValueDate { get; set; } + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithCanonicalNameReplacesPreservedProperty(Last)/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithCanonicalNameReplacesPreservedProperty(Last)/MockInputModel.cs new file mode 100644 index 00000000000..9b42ee1a608 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithCanonicalNameReplacesPreservedProperty(Last)/MockInputModel.cs @@ -0,0 +1,7 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + public System.DateTimeOffset StartOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithCanonicalNameReplacesPreservedProperty/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithCanonicalNameReplacesPreservedProperty/MockInputModel.cs new file mode 100644 index 00000000000..2c636a40756 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithCanonicalNameReplacesPreservedProperty/MockInputModel.cs @@ -0,0 +1,12 @@ +#nullable disable + +using Microsoft.TypeSpec.Generator.Customizations; + +namespace Sample.Models +{ + public partial class MockInputModel + { + [CodeGenMember("StartsOn")] + public System.DateTimeOffset MyStart { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithLastContractNamePreservesWireInfo(Expected).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithLastContractNamePreservesWireInfo(Expected).cs new file mode 100644 index 00000000000..5727c4114a3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithLastContractNamePreservesWireInfo(Expected).cs @@ -0,0 +1,25 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; + +namespace Sample.Models +{ + public partial class MockInputModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public MockInputModel(global::System.DateTimeOffset startOn) + { + StartOn = startOn; + } + + internal MockInputModel(global::System.DateTimeOffset startOn, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + StartOn = startOn; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithLastContractNamePreservesWireInfo(Last)/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithLastContractNamePreservesWireInfo(Last)/MockInputModel.cs new file mode 100644 index 00000000000..9b42ee1a608 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithLastContractNamePreservesWireInfo(Last)/MockInputModel.cs @@ -0,0 +1,7 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + public System.DateTimeOffset StartOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithLastContractNamePreservesWireInfo/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithLastContractNamePreservesWireInfo/MockInputModel.cs new file mode 100644 index 00000000000..74791e874c7 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithLastContractNamePreservesWireInfo/MockInputModel.cs @@ -0,0 +1,9 @@ +#nullable disable + +namespace Sample.Models +{ + public partial class MockInputModel + { + public System.DateTimeOffset StartOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithRawDateTimeNameReplacesNormalizedProperty(Expected).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithRawDateTimeNameReplacesNormalizedProperty(Expected).cs new file mode 100644 index 00000000000..afbb84eeef4 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithRawDateTimeNameReplacesNormalizedProperty(Expected).cs @@ -0,0 +1,24 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; + +namespace Sample.Models +{ + public partial class MockInputModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public MockInputModel() + { + } + + internal MockInputModel(global::System.DateTimeOffset? valueDate, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + ValueDate = valueDate; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithRawDateTimeNameReplacesNormalizedProperty/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithRawDateTimeNameReplacesNormalizedProperty/MockInputModel.cs new file mode 100644 index 00000000000..9f499b4c322 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithRawDateTimeNameReplacesNormalizedProperty/MockInputModel.cs @@ -0,0 +1,9 @@ +#nullable disable + +namespace Sample.Models +{ + public partial class MockInputModel + { + internal System.DateTimeOffset? ValueDate { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithSpecNameReplacesLastContractPreservedProperty(Expected).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithSpecNameReplacesLastContractPreservedProperty(Expected).cs new file mode 100644 index 00000000000..88487bdaefa --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithSpecNameReplacesLastContractPreservedProperty(Expected).cs @@ -0,0 +1,25 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; + +namespace Sample.Models +{ + public partial class MockInputModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public MockInputModel(global::System.DateTimeOffset startTime) + { + StartTime = startTime; + } + + internal MockInputModel(global::System.DateTimeOffset startTime, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + StartTime = startTime; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithSpecNameReplacesLastContractPreservedProperty(Last)/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithSpecNameReplacesLastContractPreservedProperty(Last)/MockInputModel.cs new file mode 100644 index 00000000000..9b42ee1a608 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithSpecNameReplacesLastContractPreservedProperty(Last)/MockInputModel.cs @@ -0,0 +1,7 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + public System.DateTimeOffset StartOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithSpecNameReplacesLastContractPreservedProperty/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithSpecNameReplacesLastContractPreservedProperty/MockInputModel.cs new file mode 100644 index 00000000000..c0f346fe924 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/CustomCodeWithSpecNameReplacesLastContractPreservedProperty/MockInputModel.cs @@ -0,0 +1,9 @@ +#nullable disable + +namespace Sample.Models +{ + public partial class MockInputModel + { + public System.DateTimeOffset StartTime { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/DuplicateExactIdentifierCustomizationKeepsFirstSpecMatch/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/DuplicateExactIdentifierCustomizationKeepsFirstSpecMatch/MockInputModel.cs new file mode 100644 index 00000000000..e335f6ffad2 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/DuplicateExactIdentifierCustomizationKeepsFirstSpecMatch/MockInputModel.cs @@ -0,0 +1,9 @@ +#nullable disable + +namespace Sample.Models +{ + public partial class MockInputModel + { + public string StartTime { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/ExactSpecNameTakesPrecedenceOverCanonicalAliasForCustomization(Last)/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/ExactSpecNameTakesPrecedenceOverCanonicalAliasForCustomization(Last)/MockInputModel.cs new file mode 100644 index 00000000000..2f6c707eae4 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/ExactSpecNameTakesPrecedenceOverCanonicalAliasForCustomization(Last)/MockInputModel.cs @@ -0,0 +1,9 @@ +#nullable disable + +namespace Sample.Models +{ + public class MockInputModel + { + public System.DateTimeOffset StartOn { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/ExactSpecNameTakesPrecedenceOverCanonicalAliasForCustomization/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/ExactSpecNameTakesPrecedenceOverCanonicalAliasForCustomization/MockInputModel.cs new file mode 100644 index 00000000000..7c75b590f3b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/ExactSpecNameTakesPrecedenceOverCanonicalAliasForCustomization/MockInputModel.cs @@ -0,0 +1,9 @@ +#nullable disable + +namespace Sample.Models +{ + public partial class MockInputModel + { + public System.DateTimeOffset StartsOn { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByCustomRenameIsNotReused(Expected).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByCustomRenameIsNotReused(Expected).cs new file mode 100644 index 00000000000..970d7ec980d --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByCustomRenameIsNotReused(Expected).cs @@ -0,0 +1,32 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; +using Sample; + +namespace Sample.Models +{ + public partial class MockInputModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public MockInputModel(global::System.DateTimeOffset startsOn, string startOn) + { + global::Sample.Argument.AssertNotNull(startOn, nameof(startOn)); + + StartsOn = startsOn; + StartOn = startOn; + } + + internal MockInputModel(global::System.DateTimeOffset startsOn, string startOn, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + StartsOn = startsOn; + StartOn = startOn; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public global::System.DateTimeOffset StartsOn { get; set; } + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByCustomRenameIsNotReused(Last)/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByCustomRenameIsNotReused(Last)/MockInputModel.cs new file mode 100644 index 00000000000..108d6cd5741 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByCustomRenameIsNotReused(Last)/MockInputModel.cs @@ -0,0 +1,7 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + public System.DateTimeOffset StartOn { get; set; } + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByCustomRenameIsNotReused/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByCustomRenameIsNotReused/MockInputModel.cs new file mode 100644 index 00000000000..9fe20434f47 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByCustomRenameIsNotReused/MockInputModel.cs @@ -0,0 +1,12 @@ +#nullable disable + +using Microsoft.TypeSpec.Generator.Customizations; + +namespace Sample.Models +{ + public partial class MockInputModel + { + [CodeGenMember("foo")] + public string StartOn { get; set; } + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByRenamedCustomFieldIsNotReused(Last)/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByRenamedCustomFieldIsNotReused(Last)/MockInputModel.cs new file mode 100644 index 00000000000..9b42ee1a608 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByRenamedCustomFieldIsNotReused(Last)/MockInputModel.cs @@ -0,0 +1,7 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + public System.DateTimeOffset StartOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByRenamedCustomFieldIsNotReused/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByRenamedCustomFieldIsNotReused/MockInputModel.cs new file mode 100644 index 00000000000..3066ed50f2c --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/HistoricalNameClaimedByRenamedCustomFieldIsNotReused/MockInputModel.cs @@ -0,0 +1,12 @@ +#nullable disable + +using Microsoft.TypeSpec.Generator.Customizations; + +namespace Sample.Models +{ + public partial class MockInputModel + { + [CodeGenMember("foo")] + internal string StartOn; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/RefilteringDoesNotUseStaleAliasToSuppressActiveProperty/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/RefilteringDoesNotUseStaleAliasToSuppressActiveProperty/MockInputModel.cs new file mode 100644 index 00000000000..96792e2a466 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/RefilteringDoesNotUseStaleAliasToSuppressActiveProperty/MockInputModel.cs @@ -0,0 +1,9 @@ +#nullable disable + +namespace Sample.Models +{ + public partial class MockInputModel + { + public System.DateTimeOffset? StartTime { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/RefilteringPreservesCustomCodeSpecAssociation(Last)/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/RefilteringPreservesCustomCodeSpecAssociation(Last)/MockInputModel.cs new file mode 100644 index 00000000000..9b42ee1a608 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/RefilteringPreservesCustomCodeSpecAssociation(Last)/MockInputModel.cs @@ -0,0 +1,7 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + public System.DateTimeOffset StartOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/RefilteringPreservesCustomCodeSpecAssociation/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/RefilteringPreservesCustomCodeSpecAssociation/MockInputModel.cs new file mode 100644 index 00000000000..74791e874c7 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelCustomizationTests/RefilteringPreservesCustomCodeSpecAssociation/MockInputModel.cs @@ -0,0 +1,9 @@ +#nullable disable + +namespace Sample.Models +{ + public partial class MockInputModel + { + public System.DateTimeOffset StartOn { get; set; } + } +} 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 0e8d0c8c03f..b07e9428ede 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 @@ -199,6 +199,359 @@ public async Task TestPropertyNamePreservesLastContractDateTimeSuffix() Assert.AreEqual(Helpers.GetExpectedFromFile("Expected"), actual); } + [Test] + public async Task TestPropertyNamePreservesPreviouslyGeneratedDateTimeSuffixFromLastContract() + { + 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) + ]); + + var modelProvider = new ModelProvider(inputModel); + + Assert.AreEqual("StartOn", modelProvider.Properties.Single().Name); + Assert.AreEqual(Helpers.GetExpectedFromFile("Expected"), new TypeProviderWriter(modelProvider).Write().Content); + } + + [Test] + public async Task TestPropertyNamePreservesHistoricalDateTimeNamesFromLastContract() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var historicalModel = InputFactory.Model( + "TestModel", + @namespace: "Test", + properties: + [ + InputFactory.Property("EndTime", dateTime, isRequired: true), + InputFactory.Property("ExpirationTime", dateTime, isRequired: true), + InputFactory.Property("AccessTierChangeTime", dateTime, isRequired: true), + InputFactory.Property("LastSyncTimestamp", dateTime, isRequired: true) + ]); + var canonicalModel = InputFactory.Model( + "CanonicalModel", + @namespace: "Test", + properties: [InputFactory.Property("StartTime", dateTime, isRequired: true)]); + var internalLegacyModel = InputFactory.Model( + "InternalLegacyModel", + @namespace: "Test", + properties: [InputFactory.Property("StartTime", dateTime, isRequired: true)]); + + var historicalProvider = new ModelProvider(historicalModel); + + Assert.That( + historicalProvider.Properties.Select(p => p.Name), + Is.EqualTo(new[] { "EndOn", "ExpireOn", "AccessTierChangeOn", "LastSyncTimestamp" })); + Assert.AreEqual( + Helpers.GetExpectedFromFile("Expected"), + new TypeProviderWriter(historicalProvider).Write().Content); + + // A property absent from the GA contract, and one whose only legacy-looking match is non-public, + // both take the new canonical name. + Assert.That(new ModelProvider(canonicalModel).Properties.Single().Name, Is.EqualTo("StartsOn")); + Assert.That(new ModelProvider(internalLegacyModel).Properties.Single().Name, Is.EqualTo("StartsOn")); + } + + [Test] + public async Task TestPropertyNamePreservesDateTimeNameFromProjectedProviderLastContract() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var timestampProperty = InputFactory.Property("lastKeyRotationTimestamp", dateTime, isRequired: true); + var renamedProperty = InputFactory.Property("startTime", dateTime, isRequired: true); + InputFactory.Model( + "DiskEncryptionSetProperties", + @namespace: "Test", + properties: [timestampProperty, renamedProperty]); + + // The wire model that declares the properties is not part of the shipped surface. The properties are + // projected onto the resource data provider, so the last contract must be consulted through the + // provider that actually declares the public members. + var projectedProvider = new TestTypeProvider(name: "DiskEncryptionSetData", ns: "Test"); + var wireProvider = new TestTypeProvider(name: "DiskEncryptionSetProperties", ns: "Test"); + + projectedProvider.Update(properties: + [ + new PropertyProvider(timestampProperty, projectedProvider), + new PropertyProvider(renamedProperty, projectedProvider) + ]); + + Assert.That( + projectedProvider.Properties.Select(p => p.Name), + Is.EqualTo(new[] { "LastKeyRotationTimestamp", "StartOn" })); + Assert.AreEqual( + Helpers.GetExpectedFromFile("Expected"), + new TypeProviderWriter(projectedProvider).Write().Content); + + // The same spec properties on the wire provider, which has no shipped contract of its own, take the + // new canonical names. + Assert.That( + new[] { timestampProperty, renamedProperty }.Select(p => new PropertyProvider(p, wireProvider).Name), + Is.EqualTo(new[] { "LastKeyRotationOn", "StartsOn" })); + } + + [Test] + public async Task TestPropertyNameDoesNotReuseHistoricalNameClaimedByAnotherProperty() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + + // The GA contract shipped a string "StartOn". The current spec keeps "startOn" and adds a new + // date-time "startTime", whose canonical name normalizes onto the same historical "StartOn". + // The historical name is already spoken for, so only the retained property may claim it. + var inputModel = InputFactory.Model( + "TestModel", + @namespace: "Test", + properties: + [ + InputFactory.Property("startOn", InputPrimitiveType.String, isRequired: true), + InputFactory.Property("startTime", dateTime, isRequired: true) + ]); + + var modelProvider = new ModelProvider(inputModel); + + Assert.That(modelProvider.Properties.Select(p => p.Name), Is.EqualTo(new[] { "StartOn", "StartsOn" })); + Assert.AreEqual( + Helpers.GetExpectedFromFile("Expected"), + new TypeProviderWriter(modelProvider).Write().Content); + } + + [Test] + public async Task TestPropertyNamePreservesHistoricalFirstAndLastDateTimeNames() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var firstModel = InputFactory.Model( + "FirstModel", + @namespace: "Test", + properties: [InputFactory.Property("firstTime", dateTime, isRequired: true)]); + var lastModel = InputFactory.Model( + "LastModel", + @namespace: "Test", + properties: [InputFactory.Property("lastDate", dateTime, isRequired: true)]); + + // First and Last are excluded from canonical normalization, but their GA On names still + // represent the same properties and must remain preserved. + Assert.AreEqual("FirstOn", new ModelProvider(firstModel).Properties.Single().Name); + Assert.AreEqual("LastOn", new ModelProvider(lastModel).Properties.Single().Name); + } + + [Test] + public async Task TestPropertyNameDoesNotReuseUnrelatedHistoricalDateTimeName() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + // StartDate is a removed GA property, not the previous generated name of the new StartTime property. + // Although both normalize to StartsOn, the new property must not take the unrelated GA name. + var inputModel = InputFactory.Model( + "TestModel", + @namespace: "Test", + properties: + [ + InputFactory.Property( + "startTime", + new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String), + isRequired: true) + ]); + + Assert.AreEqual("StartsOn", new ModelProvider(inputModel).Properties.Single().Name); + } + + [Test] + public async Task TestPropertyNamePrefersExactCanonicalNameFromLastContract() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + // The contract shipped both the historical "StartOn" and the canonical "StartsOn". The exact + // canonical match wins, so the result does not depend on their declaration order. + var inputModel = InputFactory.Model( + "TestModel", + @namespace: "Test", + properties: + [ + InputFactory.Property( + "startTime", + new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String), + isRequired: true) + ]); + + Assert.AreEqual("StartsOn", new ModelProvider(inputModel).Properties.Single().Name); + } + + [Test] + public async Task TestPropertyNameDoesNotReuseCanonicalNameClaimedBySibling() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var dateTime = new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String); + var inputModel = InputFactory.Model( + "TestModel", + @namespace: "Test", + properties: + [ + InputFactory.Property("startTime", dateTime, isRequired: true), + InputFactory.Property("startsOn", dateTime, isRequired: true) + ]); + + // The sibling owns the exact canonical StartsOn name, so startTime preserves the still-available + // historical StartOn name rather than producing a duplicate StartsOn declaration. + Assert.That( + new ModelProvider(inputModel).Properties.Select(p => p.Name), + Is.EqualTo(new[] { "StartOn", "StartsOn" })); + } + + [Test] + public async Task TestDateTimeNameAlreadyEndingOnDoesNotUseHistoricalStemMatch() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var inputModel = InputFactory.Model( + "TestModel", + @namespace: "Test", + properties: + [ + InputFactory.Property( + "startsOn", + new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String), + isRequired: true) + ]); + + // A spec name already in On form has no earlier spelling to recover. The GA StartOn member + // belongs to a different property and must not be selected merely because its stem also normalizes. + Assert.AreEqual("StartsOn", new ModelProvider(inputModel).Properties.Single().Name); + } + + [Test] + public async Task TestPropertyNameIgnoresIncompatiblyTypedHistoricalName() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + // The contract's "StartOn" is a string, so it is a different property that merely normalizes onto + // the same canonical name. Claiming it would also let ModelProvider restore the shipped string type + // onto this date-time property. + var inputModel = InputFactory.Model( + "TestModel", + @namespace: "Test", + properties: + [ + InputFactory.Property( + "startTime", + new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String), + isRequired: true) + ]); + + var property = new ModelProvider(inputModel).Properties.Single(); + + Assert.AreEqual("StartsOn", property.Name); + Assert.AreEqual(new CSharpType(typeof(DateTimeOffset)), property.Type); + } + + [Test] + public async Task TestPropertyNameDoesNotReuseHistoricalNameClaimedByNormalizedSibling() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + // The retained "ipStartOn" only claims the shipped "IPStartOn" after acronym normalization, so the + // claim has to be detected on the generated name rather than the raw identifier. + var inputModel = InputFactory.Model( + "TestModel", + @namespace: "Test", + properties: + [ + InputFactory.Property("ipStartOn", InputPrimitiveType.String, isRequired: true), + InputFactory.Property( + "ipStartTime", + new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String), + isRequired: true) + ]); + + Assert.That( + new ModelProvider(inputModel).Properties.Select(p => p.Name), + Is.EqualTo(new[] { "IPStartOn", "IPStartsOn" })); + } + + [Test] + public async Task TestPropertyNamePreservesCollisionSuffixedHistoricalName() + { + await MockHelpers.LoadMockGeneratorAsync(lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + // The model is named "StartOn", so the shipped member carries the enclosing-type collision suffix + // and is named "StartOnProperty". That suffix has to be stripped before the historical name is + // matched against the canonical one. + var inputModel = InputFactory.Model( + "StartOn", + @namespace: "Test", + properties: + [ + InputFactory.Property( + "startTime", + new InputDateTimeType( + DateTimeKnownEncoding.Rfc3339, + "utcDateTime", + "TypeSpec.utcDateTime", + InputPrimitiveType.String), + isRequired: true) + ]); + + Assert.AreEqual("StartOnProperty", new ModelProvider(inputModel).Properties.Single().Name); + } + [TestCaseSource(nameof(CollectionPropertyTestCases))] public void CollectionProperty(CSharpType coreType, InputModelProperty collectionProperty, CSharpType expectedType) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestDateTimeNameAlreadyEndingOnDoesNotUseHistoricalStemMatch/TestModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestDateTimeNameAlreadyEndingOnDoesNotUseHistoricalStemMatch/TestModel.cs new file mode 100644 index 00000000000..16bb8a8f091 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestDateTimeNameAlreadyEndingOnDoesNotUseHistoricalStemMatch/TestModel.cs @@ -0,0 +1,7 @@ +namespace Test +{ + public class TestModel + { + public System.DateTimeOffset StartOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseCanonicalNameClaimedBySibling/TestModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseCanonicalNameClaimedBySibling/TestModel.cs new file mode 100644 index 00000000000..54273576a50 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseCanonicalNameClaimedBySibling/TestModel.cs @@ -0,0 +1,8 @@ +namespace Test +{ + public class TestModel + { + public System.DateTimeOffset StartsOn { get; set; } + public System.DateTimeOffset StartOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseHistoricalNameClaimedByAnotherProperty(Expected).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseHistoricalNameClaimedByAnotherProperty(Expected).cs new file mode 100644 index 00000000000..17c032a605a --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseHistoricalNameClaimedByAnotherProperty(Expected).cs @@ -0,0 +1,34 @@ +// + +#nullable disable + +using System; +using System.Collections.Generic; +using Sample; + +namespace Test +{ + public partial class TestModel + { + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public TestModel(string startOn, global::System.DateTimeOffset startsOn) + { + global::Sample.Argument.AssertNotNull(startOn, nameof(startOn)); + + StartOn = startOn; + StartsOn = startsOn; + } + + internal TestModel(string startOn, global::System.DateTimeOffset startsOn, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + StartOn = startOn; + StartsOn = startsOn; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public string StartOn { get; set; } + + public global::System.DateTimeOffset StartsOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseHistoricalNameClaimedByAnotherProperty/TestModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseHistoricalNameClaimedByAnotherProperty/TestModel.cs new file mode 100644 index 00000000000..ca157485ed3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseHistoricalNameClaimedByAnotherProperty/TestModel.cs @@ -0,0 +1,7 @@ +namespace Test +{ + public class TestModel + { + public string StartOn { get; set; } + } +} \ No newline at end of file diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseHistoricalNameClaimedByNormalizedSibling/TestModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseHistoricalNameClaimedByNormalizedSibling/TestModel.cs new file mode 100644 index 00000000000..5a891a93556 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseHistoricalNameClaimedByNormalizedSibling/TestModel.cs @@ -0,0 +1,7 @@ +namespace Test +{ + public class TestModel + { + public System.DateTimeOffset IPStartOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseUnrelatedHistoricalDateTimeName/TestModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseUnrelatedHistoricalDateTimeName/TestModel.cs new file mode 100644 index 00000000000..b17cea051a3 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameDoesNotReuseUnrelatedHistoricalDateTimeName/TestModel.cs @@ -0,0 +1,7 @@ +namespace Test +{ + public class TestModel + { + public System.DateTimeOffset StartDate { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameIgnoresIncompatiblyTypedHistoricalName/TestModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameIgnoresIncompatiblyTypedHistoricalName/TestModel.cs new file mode 100644 index 00000000000..a98d045aef9 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNameIgnoresIncompatiblyTypedHistoricalName/TestModel.cs @@ -0,0 +1,7 @@ +namespace Test +{ + public class TestModel + { + public string StartOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePrefersExactCanonicalNameFromLastContract/TestModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePrefersExactCanonicalNameFromLastContract/TestModel.cs new file mode 100644 index 00000000000..7ef464e2788 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePrefersExactCanonicalNameFromLastContract/TestModel.cs @@ -0,0 +1,8 @@ +namespace Test +{ + public class TestModel + { + public System.DateTimeOffset StartOn { get; set; } + public System.DateTimeOffset StartsOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesCollisionSuffixedHistoricalName/TestModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesCollisionSuffixedHistoricalName/TestModel.cs new file mode 100644 index 00000000000..717cf8ea3d7 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesCollisionSuffixedHistoricalName/TestModel.cs @@ -0,0 +1,7 @@ +namespace Test +{ + public class StartOn + { + public System.DateTimeOffset StartOnProperty { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesDateTimeNameFromProjectedProviderLastContract(Expected).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesDateTimeNameFromProjectedProviderLastContract(Expected).cs new file mode 100644 index 00000000000..a2f6727a9fc --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesDateTimeNameFromProjectedProviderLastContract(Expected).cs @@ -0,0 +1,15 @@ +// + +#nullable disable + +using System; + +namespace Test +{ + public partial class DiskEncryptionSetData + { + public global::System.DateTimeOffset LastKeyRotationTimestamp { get; set; } + + public global::System.DateTimeOffset StartOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesDateTimeNameFromProjectedProviderLastContract/DiskEncryptionSetData.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesDateTimeNameFromProjectedProviderLastContract/DiskEncryptionSetData.cs new file mode 100644 index 00000000000..a0953deaa1e --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesDateTimeNameFromProjectedProviderLastContract/DiskEncryptionSetData.cs @@ -0,0 +1,12 @@ +namespace Test +{ + public class DiskEncryptionSetData + { + public System.DateTimeOffset LastKeyRotationTimestamp { get; set; } + public System.DateTimeOffset StartOn { get; set; } + } + + public class DiskEncryptionSetProperties + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesHistoricalDateTimeNamesFromLastContract(Expected).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesHistoricalDateTimeNamesFromLastContract(Expected).cs new file mode 100644 index 00000000000..6ef57a5ade9 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesHistoricalDateTimeNamesFromLastContract(Expected).cs @@ -0,0 +1,39 @@ +// + +#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 endOn, global::System.DateTimeOffset expireOn, global::System.DateTimeOffset accessTierChangeOn, global::System.DateTimeOffset lastSyncTimestamp) + { + EndOn = endOn; + ExpireOn = expireOn; + AccessTierChangeOn = accessTierChangeOn; + LastSyncTimestamp = lastSyncTimestamp; + } + + internal TestModel(global::System.DateTimeOffset endOn, global::System.DateTimeOffset expireOn, global::System.DateTimeOffset accessTierChangeOn, global::System.DateTimeOffset lastSyncTimestamp, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + EndOn = endOn; + ExpireOn = expireOn; + AccessTierChangeOn = accessTierChangeOn; + LastSyncTimestamp = lastSyncTimestamp; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public global::System.DateTimeOffset EndOn { get; set; } + + public global::System.DateTimeOffset ExpireOn { get; set; } + + public global::System.DateTimeOffset AccessTierChangeOn { get; set; } + + public global::System.DateTimeOffset LastSyncTimestamp { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesHistoricalDateTimeNamesFromLastContract/TestModels.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesHistoricalDateTimeNamesFromLastContract/TestModels.cs new file mode 100644 index 00000000000..e3593ac0652 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesHistoricalDateTimeNamesFromLastContract/TestModels.cs @@ -0,0 +1,20 @@ +namespace Test +{ + public class TestModel + { + public System.DateTimeOffset EndOn { get; set; } + public System.DateTimeOffset ExpireOn { get; set; } + public System.DateTimeOffset AccessTierChangeOn { get; set; } + public System.DateTimeOffset LastSyncTimestamp { get; set; } + } + + public class CanonicalModel + { + public System.DateTimeOffset StartsOn { get; set; } + } + + public class InternalLegacyModel + { + internal System.DateTimeOffset StartOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesHistoricalFirstAndLastDateTimeNames/TestModels.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesHistoricalFirstAndLastDateTimeNames/TestModels.cs new file mode 100644 index 00000000000..4efb8499186 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesHistoricalFirstAndLastDateTimeNames/TestModels.cs @@ -0,0 +1,12 @@ +namespace Test +{ + public class FirstModel + { + public System.DateTimeOffset FirstOn { get; set; } + } + + public class LastModel + { + public System.DateTimeOffset LastOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesPreviouslyGeneratedDateTimeSuffixFromLastContract(Expected).cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesPreviouslyGeneratedDateTimeSuffixFromLastContract(Expected).cs new file mode 100644 index 00000000000..fcfbe26e8bf --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesPreviouslyGeneratedDateTimeSuffixFromLastContract(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 startOn) + { + StartOn = startOn; + } + + internal TestModel(global::System.DateTimeOffset startOn, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties) + { + StartOn = startOn; + _additionalBinaryDataProperties = additionalBinaryDataProperties; + } + + public global::System.DateTimeOffset StartOn { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesPreviouslyGeneratedDateTimeSuffixFromLastContract/TestModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesPreviouslyGeneratedDateTimeSuffixFromLastContract/TestModel.cs new file mode 100644 index 00000000000..16bb8a8f091 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/TestData/PropertyProviderTests/TestPropertyNamePreservesPreviouslyGeneratedDateTimeSuffixFromLastContract/TestModel.cs @@ -0,0 +1,7 @@ +namespace Test +{ + public class TestModel + { + public System.DateTimeOffset StartOn { get; set; } + } +} 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 b321248a4e6..0f8676baaf1 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 @@ -78,6 +78,8 @@ private static IEnumerable DateTimeNameTestCases() yield return new TestCaseData("creationTime", dateTime, "createdOn"); yield return new TestCaseData("ExpirationDate", dateTime, "ExpiresOn"); yield return new TestCaseData("expirationDate", dateTime, "expiresOn"); + yield return new TestCaseData("ExpireOn", dateTime, "ExpiresOn"); + yield return new TestCaseData("expireOn", dateTime, "expiresOn"); yield return new TestCaseData("ExpirationDateTime", dateTime, "ExpiresOn"); yield return new TestCaseData("expirationDateTime", dateTime, "expiresOn"); yield return new TestCaseData("modelExpirationDate", dateTime, "modelExpiresOn");