Skip to content
Open
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 @@ -90,6 +90,28 @@ internal set
}
}

/// <summary>
/// Safely gets a namespace override from the SourceInputModel if it's available.
/// Returns null if the generator or SourceInputModel is not yet initialized.
/// </summary>
internal static string? GetNamespaceOverride(string typeName)
{
if (_instance == null)
{
return null;
}

try
{
return _instance.SourceInputModel.GetNamespaceOverride(typeName);
}
catch (InvalidOperationException)
{
// SourceInputModel not yet initialized
return null;
}
}

public string LicenseHeader => Configuration.LicenseInfo?.Header ?? string.Empty;
public virtual OutputLibrary OutputLibrary { get; } = new();
public virtual InputLibrary InputLibrary => _inputLibrary;
Expand All @@ -103,7 +125,8 @@ internal set
new CodeGenTypeAttributeDefinition(),
new CodeGenMemberAttributeDefinition(),
new CodeGenSuppressAttributeDefinition(),
new CodeGenSerializationAttributeDefinition()
new CodeGenSerializationAttributeDefinition(),
new CodeGenNamespaceAttributeDefinition()
];

protected internal virtual void Configure()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.TypeSpec.Generator.Expressions;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Statements;
using static Microsoft.TypeSpec.Generator.Snippets.Snippet;

namespace Microsoft.TypeSpec.Generator.Providers
{
internal class CodeGenNamespaceAttributeDefinition : TypeProvider
{
protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "Internal", $"{Name}.cs");

protected override string BuildName() => "CodeGenNamespaceAttribute";

protected override string BuildNamespace() => CodeModelGenerator.CustomizationAttributeNamespace;

private protected sealed override NamedTypeSymbolProvider? BuildCustomCodeView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null;
private protected sealed override NamedTypeSymbolProvider? BuildLastContractView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null;

protected override TypeSignatureModifiers BuildDeclarationModifiers() =>
TypeSignatureModifiers.Internal | TypeSignatureModifiers.Class;

protected internal override CSharpType[] BuildImplements() => [typeof(Attribute)];

protected override IReadOnlyList<AttributeStatement> BuildAttributes()
{
return [new AttributeStatement(typeof(AttributeUsageAttribute),
[FrameworkEnumValue(AttributeTargets.Assembly)],
[
new KeyValuePair<string, ValueExpression>("AllowMultiple", True)
])];
}

protected internal override PropertyProvider[] BuildProperties() =>
[
new PropertyProvider(
$"Gets the original name of the type whose namespace should be changed.",
MethodSignatureModifiers.Public,
typeof(string),
"TypeName",
new AutoPropertyBody(false),
this),
new PropertyProvider(
$"Gets the new namespace for the type.",
MethodSignatureModifiers.Public,
typeof(string),
"Namespace",
new AutoPropertyBody(false),
this)
];

protected internal override ConstructorProvider[] BuildConstructors()
{
var typeNameParameter = new ParameterProvider("typeName", $"The original name of the type.", typeof(string));
var namespaceParameter = new ParameterProvider("namespace", $"The new namespace for the type.", typeof(string));

return
[
new ConstructorProvider(
new ConstructorSignature(
Type,
null,
MethodSignatureModifiers.Public,
[typeNameParameter, namespaceParameter]),
new[]
{
This.Property("TypeName").Assign(typeNameParameter).Terminate(),
This.Property("Namespace").Assign(namespaceParameter).Terminate()
},
this)
];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ protected TypeProvider() : this(null)

private protected virtual TypeProvider? BuildLastContractView(string? generatedTypeName = null, string? generatedTypeNamespace = null)
=> CodeModelGenerator.Instance.SourceInputModel.FindForTypeInLastContract(
generatedTypeNamespace ?? CustomCodeView?.Type.Namespace ?? BuildNamespace(),
generatedTypeNamespace ?? CustomCodeView?.Type.Namespace ?? CodeModelGenerator.GetNamespaceOverride(BuildName()) ?? BuildNamespace(),
generatedTypeName ?? CustomCodeView?.Name ?? BuildName(),
DeclaringTypeProvider?.Type.Name);

Expand Down Expand Up @@ -134,7 +134,7 @@ public string? Deprecated
public CSharpType Type => _type ??=
new(
CustomCodeView?.Name ?? BuildName(),
CustomCodeView?.Type.Namespace ?? BuildNamespace(),
GetResolvedNamespace(),
this is EnumProvider ||
DeclarationModifiers.HasFlag(TypeSignatureModifiers.Struct) ||
DeclarationModifiers.HasFlag(TypeSignatureModifiers.Enum),
Expand All @@ -149,6 +149,26 @@ this is EnumProvider ||
protected virtual bool GetIsEnum() => false;
public bool IsEnum => GetIsEnum();

/// <summary>
/// Resolves the namespace for this type, checking custom code view first,
/// then assembly-level CodeGenNamespace overrides, then the default namespace.
/// </summary>
private string GetResolvedNamespace()
{
if (CustomCodeView?.Type.Namespace is { } customNs)
{
return customNs;
}

var overrideNs = CodeModelGenerator.GetNamespaceOverride(BuildName());
if (overrideNs != null)
{
return overrideNs;
}

return BuildNamespace();
}

protected virtual string BuildNamespace() => CodeModelGenerator.Instance.TypeFactory.PrimaryNamespace;

private TypeSignatureModifiers? _declarationModifiers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public static class CodeGenAttributes

public const string CodeGenSerializationAttributeName = "CodeGenSerializationAttribute";

public const string CodeGenNamespaceAttributeName = "CodeGenNamespaceAttribute";

private const string SerializationName = "SerializationName";

private const string SerializationValueHook = "SerializationValueHook";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ public class SourceInputModel
public Compilation? LastContract { get; }

private readonly Lazy<IReadOnlyDictionary<string, INamedTypeSymbol>> _nameMap;
private readonly Lazy<IReadOnlyDictionary<string, string>> _namespaceOverrides;

public SourceInputModel(Compilation? customization, Compilation? lastContract)
{
Customization = customization;
LastContract = lastContract;

_nameMap = new(PopulateNameMap);
_namespaceOverrides = new(PopulateNamespaceOverrides);
}

private IReadOnlyDictionary<string, INamedTypeSymbol> PopulateNameMap()
Expand Down Expand Up @@ -52,6 +54,14 @@ private IReadOnlyDictionary<string, INamedTypeSymbol> PopulateNameMap()
return FindTypeInCustomization(Customization, ns, name, false, declaringTypeName);
}

/// <summary>
/// Gets the namespace override for a type if one was specified via [assembly: CodeGenNamespace] in custom code.
/// </summary>
public string? GetNamespaceOverride(string typeName)
{
return _namespaceOverrides.Value.TryGetValue(typeName, out var ns) ? ns : null;
}

public TypeProvider? FindForTypeInLastContract(string ns, string name, string? declaringTypeName = null)
{
return FindTypeInCompilation(LastContract, ns, name, true, declaringTypeName, includeInternal: false);
Expand Down Expand Up @@ -113,6 +123,28 @@ private static string GetFullyQualifiedMetadataName(string ns, string name, stri
return type != null ? new NamedTypeSymbolProvider(type, compilation) : null;
}

private IReadOnlyDictionary<string, string> PopulateNamespaceOverrides()
{
var overrides = new Dictionary<string, string>();
if (Customization == null)
{
return overrides;
}

foreach (var attribute in Customization.Assembly.GetAttributes())
{
if (attribute.AttributeClass?.Name == CodeGenAttributes.CodeGenNamespaceAttributeName
&& attribute.ConstructorArguments.Length >= 2
&& attribute.ConstructorArguments[0].Value is string typeName
&& attribute.ConstructorArguments[1].Value is string @namespace)
{
overrides[typeName] = @namespace;
}
}

return overrides;
}

private bool TryGetName(ISymbol symbol, [NotNullWhen(true)] out string? name)
{
name = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,13 @@ public void CodeGenTypeAttributeEmitted()
var codeGenTypeAttribute = new CodeGenTypeAttribute("PropertyName");
Assert.AreEqual("PropertyName", codeGenTypeAttribute.OriginalName);
}

[Test]
public void CodeGenNamespaceAttributeEmitted()
{
var codeGenNamespaceAttribute = new CodeGenNamespaceAttribute("EnumTypeName", "New.Namespace.Models");
Assert.AreEqual("EnumTypeName", codeGenNamespaceAttribute.TypeName);
Assert.AreEqual("New.Namespace.Models", codeGenNamespaceAttribute.Namespace);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Linq;
using System.Threading.Tasks;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Providers;
using Microsoft.TypeSpec.Generator.Tests.Common;
Expand Down Expand Up @@ -29,5 +31,117 @@ public async Task CanCustomizeExtensibleEnumAccessibility()
Assert.IsTrue(enumType.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public));
Assert.IsFalse(enumType.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Internal));
}

[Test]
public async Task CanChangeFixedEnumNamespace()
{
await MockHelpers.LoadMockGeneratorAsync(compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var input = InputFactory.Int32Enum("mockInputEnum",
[
("One", 1),
("Two", 2)
],
isExtensible: false);

var enumType = EnumProvider.Create(input);
Assert.IsNotNull(enumType);
Assert.IsNull(enumType.CustomCodeView, "CustomCodeView should be null since no custom type is defined");
Assert.AreEqual("NewNamespace.Models", enumType.Type.Namespace);
Assert.AreEqual("MockInputEnum", enumType.Type.Name);
Assert.IsTrue(enumType is FixedEnumProvider, "Enum should remain a FixedEnumProvider");
Assert.AreEqual(2, enumType.EnumValues.Count);
Assert.AreEqual(2, enumType.Fields.Count);
}

[Test]
public async Task CanChangeExtensibleEnumNamespace()
{
await MockHelpers.LoadMockGeneratorAsync(compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var input = InputFactory.Int32Enum("mockInputEnum",
[
("One", 1),
("Two", 2)
],
isExtensible: true);

var enumType = EnumProvider.Create(input);
Assert.IsNotNull(enumType);
Assert.IsNull(enumType.CustomCodeView, "CustomCodeView should be null since no custom type is defined");
Assert.AreEqual("NewNamespace.Models", enumType.Type.Namespace);
Assert.AreEqual("MockInputEnum", enumType.Type.Name);
Assert.IsTrue(enumType is ExtensibleEnumProvider, "Enum should remain an ExtensibleEnumProvider");
Assert.AreEqual(2, enumType.EnumValues.Count);
}

[Test]
public async Task CanChangeFixedEnumNamespacePreservesMembers()
{
await MockHelpers.LoadMockGeneratorAsync(compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var input = InputFactory.Int32Enum("mockInputEnum",
[
("Red", 1),
("Green", 2),
("Blue", 3)
],
isExtensible: false);

var enumType = EnumProvider.Create(input);
Assert.IsNotNull(enumType);
Assert.AreEqual("NewNamespace.Models", enumType.Type.Namespace);

// Verify all enum members are preserved
Assert.AreEqual(3, enumType.EnumValues.Count);
Assert.AreEqual("Red", enumType.EnumValues[0].Name);
Assert.AreEqual("Green", enumType.EnumValues[1].Name);
Assert.AreEqual("Blue", enumType.EnumValues[2].Name);
// Verify fields match enum values
Assert.AreEqual(3, enumType.Fields.Count);
}

[Test]
public async Task CanChangeFixedEnumNamespaceWithCodeGenTypeAndCodeGenNamespace()
{
// When both [CodeGenType] and [assembly: CodeGenNamespace] target the same type,
// CodeGenType (CustomCodeView) takes precedence.
await MockHelpers.LoadMockGeneratorAsync(compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var input = InputFactory.Int32Enum("mockInputEnum",
[
("One", 1),
("Two", 2)
],
isExtensible: false);

var enumType = EnumProvider.Create(input);
Assert.IsNotNull(enumType);
Assert.IsNotNull(enumType.CustomCodeView, "CustomCodeView should exist from [CodeGenType]");
// CodeGenType's namespace wins over CodeGenNamespace
Assert.AreEqual("CustomCodeView.Models", enumType.Type.Namespace);
Assert.AreEqual("RenamedEnum", enumType.Type.Name);
}

[Test]
public async Task CodeGenNamespaceIgnoredForNonMatchingType()
{
// [assembly: CodeGenNamespace("NonExistentType", ...)] should have no effect on other types.
await MockHelpers.LoadMockGeneratorAsync(compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var input = InputFactory.Int32Enum("mockInputEnum",
[
("One", 1),
("Two", 2)
],
isExtensible: false);

var enumType = EnumProvider.Create(input);
Assert.IsNotNull(enumType);
Assert.IsNull(enumType.CustomCodeView);
// Should use the default namespace, not the assembly attribute's namespace
Assert.AreEqual("Sample.Models", enumType.Type.Namespace);
Assert.AreEqual("MockInputEnum", enumType.Type.Name);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using Microsoft.TypeSpec.Generator.Customizations;

[assembly: CodeGenNamespace("MockInputEnum", "NewNamespace.Models")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using Microsoft.TypeSpec.Generator.Customizations;

[assembly: CodeGenNamespace("MockInputEnum", "NewNamespace.Models")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using Microsoft.TypeSpec.Generator.Customizations;

[assembly: CodeGenNamespace("MockInputEnum", "NewNamespace.Models")]
Loading
Loading