Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ protected override void CompareModels(XmlAdvancedModel model, XmlAdvancedModel m
Assert.AreEqual(model.Metadata.Count, model2.Metadata.Count);

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

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

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

// Verify enums
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ internal virtual void XmlModelWriteCore(global::System.Xml.XmlWriter writer, glo
throw new global::System.FormatException($"The model {nameof(global::Sample.Models.TestXmlModel)} does not support writing '{format}' format.");
}

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

Assert.IsTrue(methodBody.Contains("timestamp = child.GetDateTimeOffset(\"O\")"),
$"DateTimeOffset property should use child.GetDateTimeOffset(\"O\") with RFC3339 format. Actual:\n{methodBody}");
Assert.IsTrue(methodBody.Contains("GetDateTimeOffset(\"O\")"),
$"DateTimeOffset property should use RFC3339 format. Actual:\n{methodBody}");
}

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ public sealed class ParameterProvider : IEquatable<ParameterProvider>
public ParameterProvider(InputParameter inputParameter)
{
InputParameter = inputParameter;
Name = inputParameter.Name;
Name = inputParameter is InputMethodParameter && !inputParameter.IsExactName
&& inputParameter.Type.IsDateTimeInputType()
? inputParameter.Name.NormalizeDateTimeSuffix()
: inputParameter.Name;
Comment on lines +66 to +69

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot validate this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated and fixed: normalized non-exact method parameters now retain the original-name mapping during back-compat restoration. Added a regression test in 875ac14.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wouldn't a simpler solution would be to simply have the back compat validate both the parameter name or the InputParameter.Name on the param?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parameter.InputParameter is the same input instance used here, so comparing its name would be tautological. The condition retains the check against the normalized generated name to avoid overriding visitor-customized parameter names. aa00bd3

Description = DocHelpers.GetFormattableDescription(inputParameter.Summary, inputParameter.Doc) ?? FormattableStringHelpers.Empty;
var type = CodeModelGenerator.Instance.TypeFactory.CreateCSharpType(inputParameter.Type) ?? throw new InvalidOperationException($"Failed to create CSharpType for {inputParameter.Type}");
if (!inputParameter.IsRequired)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ private PropertyProvider(InputProperty inputProperty, CSharpType propertyType, T
(lastContractProperties is null ||
!lastContractProperties.Any(p => p.Name == legacyName)))
{
identifierName = identifierName.NormalizeCSharpAcronyms();
identifierName = identifierName
.NormalizeCSharpAcronyms(inputProperty.Type.IsDateTimeInputType());
}
Name = identifierName == enclosingType.Name
? $"{identifierName}Property"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@
// Licensed under the MIT License.
Comment thread
jorgerangel-msft marked this conversation as resolved.

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

namespace Microsoft.TypeSpec.Generator.Utilities
{
internal static class CSharpNameExtensions
{
private const string DateSuffix = "Date";
private const string DateTimeSuffix = "DateTime";
private const string FromName = "From";
private const string LowercaseOnSuffix = "on";
private const string OnSuffix = "On";
private const string PointInTimeName = "PointInTime";
private const string TimeStampSuffix = "TimeStamp";
private const string TimeSuffix = "Time";
private const string TimestampSuffix = "Timestamp";
private const string ToName = "To";
private const string AtSuffix = "At";

private static readonly (string Source, string Replacement)[] _acronymRenamingRules =
[
("Ipv4", "IPv4"),
Expand All @@ -20,9 +34,19 @@ private static readonly (string Source, string Replacement)[] _acronymRenamingRu
("Os", "OS")
Comment thread
jorgerangel-msft marked this conversation as resolved.
];

public static string NormalizeCSharpAcronyms(this string name)
private static readonly HashSet<string> _dateTimeNameExclusions = new(StringComparer.OrdinalIgnoreCase)
{
FromName,
ToName,
PointInTimeName
};

public static string NormalizeCSharpAcronyms(this string name, bool normalizeDateTimeSuffix = false)
{
StringBuilder? normalizedName = null;
var suffixLength = normalizeDateTimeSuffix && !HasExcludedDateTimeNameComponent(name)
? GetDateTimeSuffixLength(name)
: 0;
StringBuilder? normalizedName = suffixLength > 0 ? new(name.Length - suffixLength + OnSuffix.Length) : null;
int segmentStart = 0;
for (int index = 0; index < name.Length - 1; index++)
{
Expand Down Expand Up @@ -53,10 +77,88 @@ public static string NormalizeCSharpAcronyms(this string name)
return name;
}

normalizedName.Append(name, segmentStart, name.Length - segmentStart);
normalizedName.Append(name, segmentStart, name.Length - suffixLength - segmentStart);
if (suffixLength > 0)
{
normalizedName.Append(name.Length == suffixLength && char.IsLower(name[0]) ? LowercaseOnSuffix : OnSuffix);
}
return normalizedName.ToString();
}

public static string NormalizeDateTimeSuffix(this string name)
{
if (HasExcludedDateTimeNameComponent(name))
{
return name;
}

var suffixLength = GetDateTimeSuffixLength(name);
if (suffixLength == 0)
{
return name;
}

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

private static bool HasExcludedDateTimeNameComponent(string name)
{
var lookup = _dateTimeNameExclusions.GetAlternateLookup<ReadOnlySpan<char>>();
return (name.Length >= FromName.Length && lookup.Contains(name.AsSpan(0, FromName.Length))) ||
(name.Length >= ToName.Length && lookup.Contains(name.AsSpan(0, ToName.Length))) ||
(name.Length >= PointInTimeName.Length && lookup.Contains(name.AsSpan(^PointInTimeName.Length)));
}
Comment on lines +106 to +112

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot we have "pointinttime" declared in _dateTimeNameExclusions and also here. If we need to reference it in more than once place, we should create it as a const and do the same for the other exclusions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted the exclusions, including PointInTime, into shared constants. aa00bd3


private static int GetDateTimeSuffixLength(string name)
{
if (name.EndsWith(TimestampSuffix, StringComparison.Ordinal) ||
name.EndsWith(TimeStampSuffix, StringComparison.Ordinal))
{
return TimestampSuffix.Length;
}

if (name.Equals(TimestampSuffix, StringComparison.OrdinalIgnoreCase))
{
return TimestampSuffix.Length;
}

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

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

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

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

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

return 0;
}
Comment on lines +114 to +153

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot we should create const values for these strings.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted the date-time suffix and replacement strings into constants. aa00bd3


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

var inputParameter = parameter.InputParameter;
if (inputParameter is not null && string.Equals(parameter.Name, inputParameter.Name, StringComparison.Ordinal))
if (inputParameter is not null &&
(string.Equals(parameter.Name, inputParameter.Name, StringComparison.Ordinal) ||
(inputParameter is InputMethodParameter { IsExactName: false } &&
inputParameter.Type.IsDateTimeInputType() &&
string.Equals(parameter.Name, inputParameter.Name.NormalizeDateTimeSuffix(), StringComparison.Ordinal))))
{
var originalName = inputParameter.OriginalName;
if (!string.IsNullOrEmpty(originalName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,56 @@ public void ValidateArrayHandling()
Assert.IsTrue(parameter.ToPublicInputParameter().Type.Equals(typeof(IEnumerable<string>)));
}

[TestCaseSource(nameof(DateTimeParameterNameTestCases))]
public void MethodParameterNameNormalizesDateTimeSuffix(
string inputName,
InputType inputType,
bool isExactName,
string expectedName)
{
MockHelpers.LoadMockGenerator();
var inputParameter = InputFactory.MethodParameter(
inputName,
inputType,
isRequired: true,
isExactName: isExactName);

var parameter = CodeModelGenerator.Instance.TypeFactory.CreateParameter(inputParameter);

Assert.IsNotNull(parameter);
Assert.AreEqual(expectedName, parameter!.Name);
Assert.AreEqual(inputName, parameter.WireInfo.SerializedName);
}

private static IEnumerable<InputType> ValueInputTypes()
{
yield return InputPrimitiveType.Int32;
yield return InputPrimitiveType.Float32;
yield return InputFactory.Int32Enum("inputEnum", [("foo", 1)], isExtensible: true);
}

private static IEnumerable<TestCaseData> DateTimeParameterNameTestCases()
{
var dateTime = new InputDateTimeType(
DateTimeKnownEncoding.Rfc3339,
"utcDateTime",
"TypeSpec.utcDateTime",
InputPrimitiveType.String);

yield return new TestCaseData("startTime", dateTime, false, "startOn");
yield return new TestCaseData("createdAt", dateTime, false, "createdOn");
yield return new TestCaseData("timestamp", dateTime, false, "on");
yield return new TestCaseData("date", InputPrimitiveType.PlainDate, false, "on");
yield return new TestCaseData("modifiedAt", dateTime.WithNullable(true), false, "modifiedOn");
yield return new TestCaseData("fromTime", dateTime, false, "fromTime");
yield return new TestCaseData("toDate", dateTime, false, "toDate");
yield return new TestCaseData("pointInTime", dateTime, false, "pointInTime");
yield return new TestCaseData("recoveryPointInTime", dateTime, false, "recoveryPointInTime");
yield return new TestCaseData("startTime", InputPrimitiveType.String, false, "startTime");
yield return new TestCaseData("creationTimestamp", InputPrimitiveType.String, false, "creationTimestamp");
yield return new TestCaseData("createdAt", dateTime, true, "createdAt");
}

private static IEnumerable<TestCaseData> NotEqualsTestCases()
{
yield return new TestCaseData(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Input.Extensions;
using Microsoft.TypeSpec.Generator.Primitives;
Expand Down Expand Up @@ -129,6 +130,52 @@ public void TestPropertyNameNormalizesAcronymCasing(string inputName, bool isExa
Assert.AreEqual(expectedName, property.Name);
}

[TestCaseSource(nameof(DateTimePropertyNameTestCases))]
public void TestPropertyNameNormalizesDateTimeSuffix(
string inputName,
InputType inputType,
bool isExactName,
string expectedName)
{
var inputProperty = InputFactory.Property(
inputName,
inputType,
isRequired: true,
isExactName: isExactName);
InputFactory.Model("TestModel", properties: [inputProperty]);

var property = new PropertyProvider(inputProperty, new TestTypeProvider());

Assert.AreEqual(expectedName, property.Name);
Assert.AreEqual(inputName.ToVariableName(), property.WireInfo?.SerializedName);
}

[Test]
public async Task TestPropertyNamePreservesLastContractDateTimeSuffix()
{
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);
var actual = new TypeProviderWriter(modelProvider).Write().Content;

Assert.AreEqual(Helpers.GetExpectedFromFile("Expected"), actual);
}

[TestCaseSource(nameof(CollectionPropertyTestCases))]
public void CollectionProperty(CSharpType coreType, InputModelProperty collectionProperty, CSharpType expectedType)
{
Expand Down Expand Up @@ -202,6 +249,32 @@ public void TestPropertyNameConflictsWithTypeNameAfterAcronymNormalization()
Assert.AreEqual("IPAddressProperty", property.Name);
}

private static IEnumerable<TestCaseData> DateTimePropertyNameTestCases()
{
var dateTime = new InputDateTimeType(
DateTimeKnownEncoding.Rfc3339,
"utcDateTime",
"TypeSpec.utcDateTime",
InputPrimitiveType.String);

yield return new TestCaseData("StartTime", dateTime, false, "StartOn");
yield return new TestCaseData("CreatedAt", dateTime, false, "CreatedOn");
yield return new TestCaseData("DeletionTimestamp", dateTime, false, "DeletionOn");
yield return new TestCaseData("ModificationTimeStamp", dateTime, false, "ModificationOn");
yield return new TestCaseData("Timestamp", dateTime, false, "On");
yield return new TestCaseData("ExpirationDate", dateTime, false, "ExpirationOn");
yield return new TestCaseData("RecordedAt", dateTime, false, "RecordedOn");
yield return new TestCaseData("Date", InputPrimitiveType.PlainDate, false, "On");
yield return new TestCaseData("SnapshotTimestamp", dateTime.WithNullable(true), false, "SnapshotOn");
yield return new TestCaseData("FromTime", dateTime, false, "FromTime");
yield return new TestCaseData("ToDate", dateTime, false, "ToDate");
yield return new TestCaseData("RecoveryPointInTime", dateTime, false, "RecoveryPointInTime");
yield return new TestCaseData("StartTime", InputPrimitiveType.String, false, "StartTime");
yield return new TestCaseData("CreationTimestamp", InputPrimitiveType.String, false, "CreationTimestamp");
yield return new TestCaseData("CreationTimestamp", dateTime, true, "CreationTimestamp");
}


[Test]
public void CanUpdatePropertyProvider()
{
Expand Down
Loading