Skip to content

Commit

Permalink
Create valid property identifiers when generating OData client (#333)
Browse files Browse the repository at this point in the history
  • Loading branch information
gregwinterstein authored Apr 7, 2024
1 parent d80f705 commit ee15aee
Show file tree
Hide file tree
Showing 8 changed files with 629 additions and 68 deletions.
203 changes: 188 additions & 15 deletions src/Microsoft.OData.CodeGen/Templates/ODataT4CodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace Microsoft.OData.CodeGen.Templates
{
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
Expand All @@ -18,6 +19,7 @@ namespace Microsoft.OData.CodeGen.Templates
using System.Net;
using System.Security;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using Microsoft.OData.Edm.Csdl;
Expand Down Expand Up @@ -2466,7 +2468,16 @@ internal void WritePropertiesForSingleType(IEnumerable<IEdmProperty> properties)
foreach (IEdmProperty property in properties.Where(i => i.PropertyKind == EdmPropertyKind.Navigation))
{
string propertyType;
string propertyName = this.context.EnableNamingAlias ? Customization.CustomizeNaming(property.Name) : property.Name;
string propertyName;
if (IdentifierMappings.TryGetValue(property.Name, out var mappedPropertyName))
{
propertyName = mappedPropertyName;
}
else
{
propertyName = this.context.EnableNamingAlias ? Customization.CustomizeNaming(property.Name) : property.Name;
}

if (property.Type is Microsoft.OData.Edm.EdmCollectionTypeReference)
{
propertyType = GetSourceOrReturnTypeName(property.Type);
Expand Down Expand Up @@ -2495,6 +2506,7 @@ internal void WriteEntityType(IEdmEntityType entityType, Dictionary<IEdmStructur
IEdmEntityType current = entityType;
while (current != null)
{
this.SetPropertyIdentifierMappings(entityType.Name, entityType);
this.WritePropertiesForSingleType(current.DeclaredProperties);
current = (IEdmEntityType)current.BaseType;
}
Expand Down Expand Up @@ -2534,7 +2546,6 @@ internal void WriteEntityType(IEdmEntityType entityType, Dictionary<IEdmStructur

this.WriteObsoleteAttribute(GetRevisionAnnotations(entityType), /* isClass */ true);
this.WriteStructurdTypeDeclaration(entityType, this.BaseEntityType);
this.SetPropertyIdentifierMappingsIfNameConflicts(entityType.Name, entityType);
this.WriteTypeStaticCreateMethod(entityType.Name, entityType);
this.WritePropertiesForStructuredType(entityType);

Expand All @@ -2552,7 +2563,7 @@ internal void WriteComplexType(IEdmComplexType complexType, Dictionary<IEdmStruc
{
this.WriteSummaryCommentForStructuredType(this.context.EnableNamingAlias ? Customization.CustomizeNaming(complexType.Name) : complexType.Name, GetDescriptionAnnotation(complexType)?.Value);
this.WriteStructurdTypeDeclaration(complexType, string.Empty);
this.SetPropertyIdentifierMappingsIfNameConflicts(complexType.Name, complexType);
this.SetPropertyIdentifierMappings(complexType.Name, complexType);
this.WriteTypeStaticCreateMethod(complexType.Name, complexType);
this.WritePropertiesForStructuredType(complexType);

Expand Down Expand Up @@ -2908,6 +2919,170 @@ internal void SetPropertyIdentifierMappingsIfNameConflicts(string typeName, IEdm
}
}

/// <summary>
/// Set all property mappings for <paramref name="structuredType"/>
/// </summary>
/// <param name="typeName">Name of type used to verify conflicts against <paramref name="structuredType"/></param>
/// <param name="structuredType">Type object to map properties</param>
internal void SetPropertyIdentifierMappings(string typeName, IEdmStructuredType structuredType)
{
this.SetPropertyIdentifierMappingsIfNameConflicts(typeName, structuredType);
this.SetPropertyIdentifierMappingsIfBaseConflicts(structuredType);
this.SetPropertyIdentifierMappingsIfInvalidIdentifier(structuredType);
}

/// <summary>
/// List of base class properties that should not be used in generated classes
/// </summary>
private static readonly IEnumerable<string> BaseClassProperties = typeof(Client.DataServiceQuerySingle<object>).GetProperties().Select(prop => prop.Name);

/// <summary>
/// Map identifiers that conflict with base class identifiers to TypeNameIdentiferName
/// and if still non-unique, add incrementing integer suffix
/// </summary>
/// <param name="structuredType">Object containing DeclaredProperties to map</param>
internal void SetPropertyIdentifierMappingsIfBaseConflicts(IEdmStructuredType structuredType)
{
Func<string, string> customizePropertyName = (name) => { return this.context.EnableNamingAlias ? Customization.CustomizeNaming(name) : name; };

// PropertyName in VB is case-insensitive.
bool isLanguageCaseSensitive = this.context.TargetLanguage == LanguageOption.CSharp;

var baseProperties = BaseClassProperties;
if (!isLanguageCaseSensitive)
{
baseProperties = BaseClassProperties.Select(prop => prop.ToUpperInvariant());
}

var propertiesToRename = structuredType.DeclaredProperties.Where(prop => baseProperties.Contains((isLanguageCaseSensitive ? customizePropertyName(prop.Name) : prop.Name.ToUpperInvariant())));
if (propertiesToRename.Any())
{
UniqueIdentifierService uniqueIdentifierService =
new UniqueIdentifierService(structuredType.DeclaredProperties.Select(prop => prop.Name), isLanguageCaseSensitive);

var typeName = customizePropertyName(((IEdmNamedElement)structuredType).Name);
foreach (var property in propertiesToRename)
{
var renamedPropertyName = uniqueIdentifierService.GetUniqueIdentifier(typeName + Customization.CustomizeNaming(property.Name));
if (IdentifierMappings.ContainsKey(property.Name))
{
IdentifierMappings[property.Name] = renamedPropertyName;
}
else
{
IdentifierMappings.Add(property.Name, renamedPropertyName);
}
}
}
}

/// <summary>
/// CSharp code provider instance
/// </summary>
private static readonly CodeDomProvider CSharpProvider = new Microsoft.CSharp.CSharpCodeProvider();

/// <summary>
/// VB code provider instance
/// </summary>
private static readonly CodeDomProvider VBProvider = new Microsoft.VisualBasic.VBCodeProvider();

/// <summary>
/// Map invalid identifiers to valid names to avoid generating code that does not compile
/// </summary>
/// <param name="structuredType">Object containing properties to map</param>
internal void SetPropertyIdentifierMappingsIfInvalidIdentifier(IEdmStructuredType structuredType)
{
bool isLanguageCaseSensitive = this.context.TargetLanguage == LanguageOption.CSharp;
UniqueIdentifierService uniqueIdentifierService =
new UniqueIdentifierService(IdentifierMappings.Select(mapping => mapping.Value), isLanguageCaseSensitive);

Func<string, string> customizePropertyName = (name) => { return this.context.EnableNamingAlias ? Customization.CustomizeNaming(name) : name; };
var codeDomProvider = (this.context.TargetLanguage == LanguageOption.CSharp ? CSharpProvider : VBProvider);
var propertiesToRename = structuredType.DeclaredProperties.Where(prop => !codeDomProvider.IsValidIdentifier(prop.Name)
&& !LanguageKeywords.Contains(prop.Name));

foreach (var property in propertiesToRename)
{
var customizedPropertyName = customizePropertyName(property.Name);
var validName = uniqueIdentifierService.GetUniqueIdentifier(GetValidIdentifier(customizedPropertyName));
if (IdentifierMappings.ContainsKey(property.Name))
{
if (!codeDomProvider.IsValidIdentifier(IdentifierMappings[property.Name]))
{
IdentifierMappings[property.Name] = validName;
}
}
else
{
IdentifierMappings.Add(property.Name, validName);
}
}
}

/// <summary>
/// Regex match any non-identifier characters, used to generate valid identifiers<br />
/// See C# definitions: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#643-identifiers <br />
/// See VB definitions: https://learn.microsoft.com/en-us/dotnet/visual-basic/reference/language-specification/lexical-grammar#identifiers <br />
/// See Unicode character class definitions: https://www.unicode.org/reports/tr18/#General_Category_Property
/// </summary>
private static readonly Regex RegexMatchNonIdentifier = new Regex(@"[^\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\p{Cf}]", RegexOptions.Compiled);

/// <summary>
/// Default prefix character used to prefix non-alpha characters in identifiers
/// </summary>
private const char DefaultPrefixCharacter = '_';

/// <summary>
/// Get a valid C#/VB identifier for <paramref name="name"/> based on C#/VB identifier specification<br />
/// - Removes characters not permitted in C#/VB identifiers<br />
/// - Prefixes <paramref name="name"/>with underscore (_) if it doesn't start with a letter
/// </summary>
/// <remarks>
/// C# and VB use very similar rules for identifier names<br />
/// See C# definitions: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#643-identifiers <br />
/// See VB definitions: https://learn.microsoft.com/en-us/dotnet/visual-basic/reference/language-specification/lexical-grammar#identifiers
/// </remarks>
/// <param name="name">Identifier to validate and modify</param>
/// <returns>Modified <paramref name="name"/> or <paramref name="name"/> unchanged if no changes are needed</returns>
private string GetValidIdentifier(string name)
{
var codeDomProvider = (this.context.TargetLanguage == LanguageOption.CSharp ? CSharpProvider : VBProvider);
if (codeDomProvider.IsValidIdentifier(name))
{
return name;
}

var segments = RegexMatchNonIdentifier.Split(name);
var validName = new StringBuilder();
var isFirst = true;
foreach (var segment in segments.Where(token => !string.IsNullOrWhiteSpace(token)))
{
if (isFirst && !this.context.EnableNamingAlias)
{
validName.Append(segment);
}
else
{
var titleCaseSegment = Customization.CustomizeNaming(segment);
validName.Append(titleCaseSegment);
}

isFirst = false;
}

if (validName.Length < 1)
{
throw new Exception($"Failed to make valid identifier for '{name}'");
}

if (!char.IsLetter(validName[0]) && validName[0] != DefaultPrefixCharacter)
{
validName.Insert(0, DefaultPrefixCharacter);
}

return validName.ToString();
}

internal void WriteTypeStaticCreateMethod(string typeName, IEdmStructuredType structuredType)
{
Debug.Assert(structuredType != null, "structuredType != null");
Expand Down Expand Up @@ -3063,7 +3238,7 @@ internal void WritePropertiesForStructuredType(IEdmStructuredType structuredType
});
}

// Private name should not confict with field name
// Private name should not conflict with field name
UniqueIdentifierService uniqueIdentifierService = new UniqueIdentifierService(propertyInfos.Select(_ => _.FixedPropertyName),
this.context.TargetLanguage == LanguageOption.CSharp);

Expand Down Expand Up @@ -4901,13 +5076,13 @@ internal override void WriteGeneratedEdmModel(string escapedEdmxString)

this.Write(@", out edmModel, out errors))
{
global::System.Text.StringBuilder errorMessages = new global::System.Text.StringBuilder();
foreach (var error in errors)
{
errorMessages.Append(error.ErrorMessage);
errorMessages.Append(""; "");
}
throw new global::System.InvalidOperationException(errorMessages.ToString());
global::System.Text.StringBuilder errorMessages = new global::System.Text.StringBuilder();
foreach (var error in errors)
{
errorMessages.Append(error.ErrorMessage);
errorMessages.Append(""; "");
}
throw new global::System.InvalidOperationException(errorMessages.ToString());
}
return edmModel;
Expand Down Expand Up @@ -5206,7 +5381,7 @@ internal override void WritePropertyForStructuredType(PropertyOptions propertyOp

this.Write(this.ToStringHelper.ToStringWithCulture(T4Version));

this.Write("\")]\r\n\r\n");
this.Write("\")]\r\n");


if (this.context.EnableNamingAlias || IdentifierMappings.ContainsKey(propertyOptions.OriginalPropertyName))
Expand Down Expand Up @@ -5617,9 +5792,7 @@ internal override void WriteBoundFunctionInEntityTypeReturnSingleResult(bool hid

this.Write(" public virtual ");

this.Write(this.ToStringHelper.ToStringWithCulture(hideBaseMethod ? this.OverloadsModifier : string.Empty));

this.Write(" ");
this.Write(this.ToStringHelper.ToStringWithCulture(hideBaseMethod ? $"{this.OverloadsModifier} " : string.Empty));

this.Write(this.ToStringHelper.ToStringWithCulture(isReturnEntity ? returnTypeNameWithSingleSuffix : string.Format(CultureInfo.InvariantCulture, this.DataServiceQuerySingleStructureTemplate, returnTypeName)));

Expand Down
Loading

0 comments on commit ee15aee

Please sign in to comment.