Skip to content

Commit

Permalink
Improve source generator resiliency when handling conflicting type na…
Browse files Browse the repository at this point in the history
…mes. (#60)

* Improve source generator resiliency when handling conflicting type names.

* Minor fixes.
  • Loading branch information
eiriktsarpalis authored Nov 16, 2024
1 parent eabb24a commit d8c1edf
Show file tree
Hide file tree
Showing 17 changed files with 272 additions and 128 deletions.
42 changes: 27 additions & 15 deletions src/PolyType.SourceGenerator/Helpers/RoslynHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,20 +152,25 @@ public static bool MatchesNamespace(this ISymbol? symbol, ImmutableArray<string>
}

/// <summary>
/// Returns a string representation of the type suitable for use as an identifier in source.
/// Returns a string representation of the type suitable for use as an identifier in source code or file names.
/// </summary>
public static string CreateTypeIdentifier(this ITypeSymbol type)
public static string CreateTypeIdentifier(this ITypeSymbol type, ReadOnlySpan<string> reservedIdentifiers, bool includeNamespaces = false)
{
StringBuilder sb = new();
GenerateCore(type, sb);
return sb.ToString();
string identifier = sb.ToString();

static void GenerateCore(ITypeSymbol type, StringBuilder sb)
// Do not return identifiers that are C# keywords or reserved identifiers.
return IsCSharpKeyword(identifier) || reservedIdentifiers.IndexOf(identifier) >= 0
? "__Type_" + identifier
: identifier;

void GenerateCore(ITypeSymbol type, StringBuilder sb)
{
switch (type)
{
case ITypeParameterSymbol typeParameter:
AppendAsPascalCase(typeParameter.Name);
sb.Append(typeParameter.Name);
break;

case IArrayTypeSymbol arrayType:
Expand All @@ -180,8 +185,13 @@ static void GenerateCore(ITypeSymbol type, StringBuilder sb)
break;

case INamedTypeSymbol namedType:
PrependContainingTypes(namedType);
AppendAsPascalCase(namedType.Name);
if (includeNamespaces)
{
PrependNamespaces(namedType.ContainingNamespace);
PrependContainingTypes(namedType);
}

sb.Append(namedType.Name);

IEnumerable<ITypeSymbol> typeArguments = namedType.IsTupleType
? namedType.TupleElements.Select(e => e.Type)
Expand All @@ -200,6 +210,16 @@ static void GenerateCore(ITypeSymbol type, StringBuilder sb)
throw new InvalidOperationException();
}

void PrependNamespaces(INamespaceSymbol ns)
{
if (ns.ContainingNamespace is { } containingNs)
{
PrependNamespaces(containingNs);
sb.Append(ns.Name);
sb.Append('_');
}
}

void PrependContainingTypes(INamedTypeSymbol namedType)
{
if (namedType.ContainingType is { } parent)
Expand All @@ -209,14 +229,6 @@ void PrependContainingTypes(INamedTypeSymbol namedType)
sb.Append('_');
}
}

void AppendAsPascalCase(string name)
{
// Avoid creating identifiers that are C# keywords
Debug.Assert(name.Length > 0);
sb.Append(char.ToUpperInvariant(name[0]));
sb.Append(name, 1, name.Length - 1);
}
}
}

Expand Down
1 change: 0 additions & 1 deletion src/PolyType.SourceGenerator/Model/TypeId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ namespace PolyType.SourceGenerator.Model;
public readonly struct TypeId : IEquatable<TypeId>
{
public required string FullyQualifiedName { get; init; }
public required string TypeIdentifier { get; init; }
public required bool IsValueType { get; init; }
public required SpecialType SpecialType { get; init; }

Expand Down
5 changes: 5 additions & 0 deletions src/PolyType.SourceGenerator/Model/TypeShapeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ namespace PolyType.SourceGenerator.Model;
public abstract record TypeShapeModel
{
public required TypeId Type { get; init; }

/// <summary>
/// A unique identifier deriving from the type name that can be used as a valid member identifier.
/// </summary>
public required string SourceIdentifier { get; init; }
}
9 changes: 8 additions & 1 deletion src/PolyType.SourceGenerator/Parser/Parser.ModelMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,28 @@ namespace PolyType.SourceGenerator;

public sealed partial class Parser
{
private TypeShapeModel MapModel(TypeId typeId, TypeDataModel model)
private TypeShapeModel MapModel(TypeDataModel model, TypeId typeId, string sourceIdentifier)
{
return model switch
{
EnumDataModel enumModel => new EnumShapeModel
{
Type = typeId,
SourceIdentifier = sourceIdentifier,
UnderlyingType = CreateTypeId(enumModel.UnderlyingType),
},

NullableDataModel nullableModel => new NullableShapeModel
{
Type = typeId,
SourceIdentifier = sourceIdentifier,
ElementType = CreateTypeId(nullableModel.ElementType),
},

EnumerableDataModel enumerableModel => new EnumerableShapeModel
{
Type = typeId,
SourceIdentifier = sourceIdentifier,
ElementType = CreateTypeId(enumerableModel.ElementType),
ConstructionStrategy = enumerableModel.ConstructionStrategy switch
{
Expand Down Expand Up @@ -64,6 +67,7 @@ enumerableModel.ConstructionStrategy is CollectionModelConstructionStrategy.List
DictionaryDataModel dictionaryModel => new DictionaryShapeModel
{
Type = typeId,
SourceIdentifier = sourceIdentifier,
KeyType = CreateTypeId(dictionaryModel.KeyType),
ValueType = CreateTypeId(dictionaryModel.ValueType),
ConstructionStrategy = dictionaryModel.ConstructionStrategy switch
Expand Down Expand Up @@ -101,6 +105,7 @@ dictionaryModel.ConstructionStrategy is CollectionModelConstructionStrategy.Dict
ObjectDataModel objectModel => new ObjectShapeModel
{
Type = typeId,
SourceIdentifier = sourceIdentifier,
Constructor = objectModel.Constructors
.Select(c => MapConstructor(objectModel, typeId, c))
.FirstOrDefault(),
Expand All @@ -118,6 +123,7 @@ dictionaryModel.ConstructionStrategy is CollectionModelConstructionStrategy.Dict
TupleDataModel tupleModel => new ObjectShapeModel
{
Type = typeId,
SourceIdentifier = sourceIdentifier,
Constructor = MapTupleConstructor(typeId, tupleModel),
Properties = tupleModel.Elements
.Select((e, i) => MapProperty(model.Type, typeId, e, tupleElementIndex: i, isClassTupleType: !tupleModel.IsValueTuple))
Expand All @@ -131,6 +137,7 @@ dictionaryModel.ConstructionStrategy is CollectionModelConstructionStrategy.Dict
_ => new ObjectShapeModel
{
Type = typeId,
SourceIdentifier = sourceIdentifier,
Constructor = null,
Properties = [],
IsValueTupleType = false,
Expand Down
76 changes: 59 additions & 17 deletions src/PolyType.SourceGenerator/Parser/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,27 +128,19 @@ private Parser(ISymbol generationScope, PolyTypeKnownSymbols knownSymbols, Cance

Parser parser = new(knownSymbols.Compilation.Assembly, knownSymbols, cancellationToken);
ImmutableEquatableArray<TypeDeclarationModel> generateShapeTypes = parser.IncludeTypesUsingGenerateShapeAttributes(generateShapeDeclarations);
return parser.ExportTypeShapeProviderModel(s_globalImplicitProviderDeclaration, generateShapeTypes);
return parser.ExportTypeShapeProviderModel(s_globalProviderDeclaration, generateShapeTypes);
}

private TypeShapeProviderModel ExportTypeShapeProviderModel(TypeDeclarationModel providerDeclaration, ImmutableEquatableArray<TypeDeclarationModel> generateShapeTypes)
{
Dictionary<TypeId, TypeShapeModel> generatedModels = new(GeneratedModels.Count);
foreach (KeyValuePair<ITypeSymbol, TypeDataModel> entry in GeneratedModels)
{
TypeId typeId = CreateTypeId(entry.Value.Type);
if (generatedModels.ContainsKey(typeId))
{
ReportDiagnostic(TypeNameConflict, location: null, typeId.FullyQualifiedName);
}

generatedModels[typeId] = MapModel(typeId, entry.Value);
}

return new TypeShapeProviderModel
{
ProviderDeclaration = providerDeclaration,
ProvidedTypes = generatedModels.ToImmutableEquatableDictionary(),
ProvidedTypes = GetGeneratedTypesAndIdentifiers()
.ToImmutableEquatableDictionary(
keySelector: kvp => kvp.Key,
valueSelector: kvp => MapModel(kvp.Value.Model, kvp.Value.TypeId, kvp.Value.SourceIdentifier)),

AnnotatedTypes = generateShapeTypes,
Diagnostics = Diagnostics.ToImmutableEquatableSet(),
};
Expand Down Expand Up @@ -269,12 +261,63 @@ static string FormatTypeDeclarationHeader(BaseTypeDeclarationSyntax typeDeclarat
}
}

private Dictionary<TypeId, (TypeDataModel Model, TypeId TypeId, string SourceIdentifier)> GetGeneratedTypesAndIdentifiers()
{
Dictionary<TypeId, (TypeDataModel Model, TypeId TypeId, string SourceIdentifier)> results = new(GeneratedModels.Count);
Dictionary<string, TypeId?> shortIdentifiers = new(GeneratedModels.Count);
ReadOnlySpan<string> reservedIdentifiers = SourceFormatter.ReservedIdentifiers;

foreach (KeyValuePair<ITypeSymbol, TypeDataModel> entry in GeneratedModels)
{
TypeId typeId = CreateTypeId(entry.Value.Type);
if (results.ContainsKey(typeId))
{
// We can't have duplicate types with the same fully qualified name.
ReportDiagnostic(TypeNameConflict, location: null, typeId.FullyQualifiedName);
continue;
}

// Generate a property name for the type. Start with a short-form name that
// doesn't include namespaces or containing types. If there is a conflict,
// we will update the identifiers to incorporate fully qualified names.
// Fully qualified names should not have conflicts since we've already checked

string sourceIdentifier = entry.Value.Type.CreateTypeIdentifier(reservedIdentifiers, includeNamespaces: false);
if (!shortIdentifiers.TryGetValue(sourceIdentifier, out TypeId? conflictingIdentifier))
{
// This is the first occurrence of the short-form identifier.
// Add to the index including the typeId in case of a later conflict.
shortIdentifiers.Add(sourceIdentifier, typeId);
}
else
{
// We have a conflict, update the identifiers of both types to long-form.
if (conflictingIdentifier is { } cId)
{
// Update the identifier of the conflicting type since it hasn't been already.
var conflictingResults = results[cId];
conflictingResults.SourceIdentifier = conflictingResults.Model.Type.CreateTypeIdentifier(reservedIdentifiers, includeNamespaces: true);
results[cId] = conflictingResults;

// Mark the short-form identifier as updated.
shortIdentifiers[sourceIdentifier] = null;
}

// Update the identifier of the current type and store the new key.
sourceIdentifier = entry.Value.Type.CreateTypeIdentifier(reservedIdentifiers, includeNamespaces: true);
}

results.Add(typeId, (entry.Value, typeId, sourceIdentifier));
}

return results;
}

private static TypeId CreateTypeId(ITypeSymbol type)
{
return new TypeId
{
FullyQualifiedName = type.GetFullyQualifiedName(),
TypeIdentifier = type.CreateTypeIdentifier(),
IsValueType = type.IsValueType,
SpecialType = type.OriginalDefinition.SpecialType,
};
Expand All @@ -290,12 +333,11 @@ private static TypeId CreateTypeId(ITypeSymbol type)
return null;
}

private static readonly TypeDeclarationModel s_globalImplicitProviderDeclaration = new()
private static readonly TypeDeclarationModel s_globalProviderDeclaration = new()
{
Id = new()
{
FullyQualifiedName = "global::PolyType.SourceGenerator.ShapeProvider",
TypeIdentifier = "GenerateShapeProvider",
IsValueType = false,
SpecialType = SpecialType.None,
},
Expand Down
2 changes: 1 addition & 1 deletion src/PolyType.SourceGenerator/PolyTypeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ private void GenerateSource(SourceProductionContext context, TypeShapeProviderMo
context.ReportDiagnostic(diagnostic.CreateDiagnostic());
}

SourceFormatter.FormatProvider(context, provider);
SourceFormatter.GenerateSourceFiles(context, provider);
}

public Action<TypeShapeProviderModel>? OnGeneratingSource { get; init; }
Expand Down
Loading

0 comments on commit d8c1edf

Please sign in to comment.