From 860bb180d1a888ff322df724e1736bc4dec98a80 Mon Sep 17 00:00:00 2001 From: Dapeng Zhang Date: Thu, 5 Sep 2024 05:24:41 +0800 Subject: [PATCH] implement the feature of changing the name of a model (#4285) Fixes #4256 Fixes #4261 In order to get our mocking system working properly and make everything aligned, I changed the `SourceInputModel` to public and added it to the `CodeModelPlugin` so that our plugin writers could override something on it to do some advanced stuffs --- .../test/TestHelpers/MockHelpers.cs | 5 + .../src/CSharpGen.cs | 24 +---- .../src/CodeModelPlugin.cs | 11 +- .../src/Configuration.cs | 10 ++ .../PostProcessing/GeneratedCodeWorkspace.cs | 19 +++- .../src/Providers/NamedTypeSymbolProvider.cs | 5 +- .../src/Providers/TypeProvider.cs | 15 ++- .../src/SourceInput/ClientSourceInput.cs | 9 -- .../src/SourceInput/CompilationCustomCode.cs | 21 ---- .../src/SourceInput/ModelTypeMapping.cs | 96 ----------------- .../src/SourceInput/SourceInputModel.cs | 101 +++++------------- .../test/CSharpGenTests.cs | 41 ------- .../test/ConfigurationTests.cs | 5 +- .../test/Helpers.cs | 43 +++++++- .../ModelProviders/ModelCustomizationTests.cs | 35 ++++++ .../TestCustomization_CanChangeModelName.cs | 25 +++++ .../test/TestHelpers/MockHelpers.cs | 18 +++- 17 files changed, 201 insertions(+), 282 deletions(-) delete mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ClientSourceInput.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/CompilationCustomCode.cs delete mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ModelTypeMapping.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/ModelProviders/ModelCustomizationTests.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/TestData/ModelCustomizationTests/TestCustomization_CanChangeModelName.cs diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/TestHelpers/MockHelpers.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/TestHelpers/MockHelpers.cs index 6edf309846..88c97e6d76 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/TestHelpers/MockHelpers.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp.ClientModel/test/TestHelpers/MockHelpers.cs @@ -5,10 +5,12 @@ using System.Collections.Generic; using System.IO; using System.Reflection; +using Microsoft.CodeAnalysis; using Microsoft.Generator.CSharp.ClientModel.Providers; using Microsoft.Generator.CSharp.Input; using Microsoft.Generator.CSharp.Primitives; using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.SourceInput; using Moq; using Moq.Protected; @@ -96,6 +98,9 @@ public static Mock LoadMockPlugin( mockPluginInstance.Setup(p => p.InputLibrary).Returns(createInputLibrary); } + var sourceInputModel = new Mock(() => new SourceInputModel(null)) { CallBase = true }; + mockPluginInstance.Setup(p => p.SourceInputModel).Returns(sourceInputModel.Object); + codeModelInstance!.SetValue(null, mockPluginInstance.Object); clientModelInstance!.SetValue(null, mockPluginInstance.Object); mockPluginInstance.Object.Configure(); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CSharpGen.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CSharpGen.cs index 4a7af1883a..d43e575aa6 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CSharpGen.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CSharpGen.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Generator.CSharp.Primitives; +using Microsoft.Generator.CSharp.SourceInput; namespace Microsoft.Generator.CSharp { @@ -14,7 +15,6 @@ internal sealed class CSharpGen { private const string ConfigurationFileName = "Configuration.json"; private const string CodeModelFileName = "tspCodeModel.json"; - private const string GeneratedFolderName = "Generated"; private static readonly string[] _filesToKeep = [ConfigurationFileName, CodeModelFileName]; @@ -25,10 +25,11 @@ public async Task ExecuteAsync() { GeneratedCodeWorkspace.Initialize(); var outputPath = CodeModelPlugin.Instance.Configuration.OutputDirectory; - var generatedSourceOutputPath = ParseGeneratedSourceOutputPath(outputPath); - var generatedTestOutputPath = Path.Combine(outputPath, "..", "..", "tests", GeneratedFolderName); + var generatedSourceOutputPath = CodeModelPlugin.Instance.Configuration.ProjectGeneratedDirectory; + var generatedTestOutputPath = CodeModelPlugin.Instance.Configuration.TestGeneratedDirectory; GeneratedCodeWorkspace workspace = await GeneratedCodeWorkspace.Create(); + await CodeModelPlugin.Instance.InitializeSourceInputModelAsync(); var output = CodeModelPlugin.Instance.OutputLibrary; Directory.CreateDirectory(Path.Combine(generatedSourceOutputPath, "Models")); @@ -82,23 +83,6 @@ public async Task ExecuteAsync() } } - /// - /// Parses and updates the output path for the generated code. - /// - /// The output path. - /// The parsed output path string. - internal static string ParseGeneratedSourceOutputPath(string outputPath) - { - if (!outputPath.EndsWith("src", StringComparison.Ordinal) && !outputPath.EndsWith("src/", StringComparison.Ordinal)) - { - outputPath = Path.Combine(outputPath, "src"); - } - - outputPath = Path.Combine(outputPath, GeneratedFolderName); - - return outputPath; - } - /// /// Clears the output directory specified by . If is not null, /// the specified files in the output directory will not be deleted. diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CodeModelPlugin.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CodeModelPlugin.cs index ed7509847c..2bc8d5ca19 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CodeModelPlugin.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/CodeModelPlugin.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; +using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.Generator.CSharp.Input; using Microsoft.Generator.CSharp.Primitives; using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.SourceInput; namespace Microsoft.Generator.CSharp { @@ -53,11 +55,11 @@ protected CodeModelPlugin() } internal bool IsNewProject { get; set; } - private Lazy _inputLibrary; // Extensibility points to be implemented by a plugin public virtual TypeFactory TypeFactory { get; } + public virtual SourceInputModel SourceInputModel => _sourceInputModel ?? throw new InvalidOperationException($"SourceInputModel has not been initialized yet"); public virtual string LicenseString => string.Empty; public virtual OutputLibrary OutputLibrary { get; } = new(); public virtual InputLibrary InputLibrary => _inputLibrary.Value; @@ -72,5 +74,12 @@ public virtual void AddVisitor(LibraryVisitor visitor) { _visitors.Add(visitor); } + + private SourceInputModel? _sourceInputModel; + internal async Task InitializeSourceInputModelAsync() + { + GeneratedCodeWorkspace existingCode = GeneratedCodeWorkspace.CreateExistingCodeProject(Instance.Configuration.ProjectDirectory, Instance.Configuration.ProjectGeneratedDirectory); + _sourceInputModel = new SourceInputModel(await existingCode.GetCompilationAsync()); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Configuration.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Configuration.cs index 29803f205f..c5cad927a6 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Configuration.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Configuration.cs @@ -20,6 +20,7 @@ public class Configuration "Enum", ]; + private const string GeneratedFolderName = "Generated"; private const string ConfigurationFileName = "Configuration.json"; // for mocking @@ -144,6 +145,15 @@ private static class Options private string? _projectDirectory; internal string ProjectDirectory => _projectDirectory ??= Path.Combine(OutputDirectory, "src"); + private string? _testProjectDirectory; + internal string TestProjectDirectory => _testProjectDirectory ??= Path.Combine(OutputDirectory, "tests"); + + private string? _projectGeneratedDirectory; + internal string ProjectGeneratedDirectory => _projectGeneratedDirectory ??= Path.Combine(ProjectDirectory, GeneratedFolderName); + + private string? _testGeneratedDirectory; + internal string TestGeneratedDirectory => _testGeneratedDirectory ??= Path.Combine(TestProjectDirectory, GeneratedFolderName); + internal string LibraryName { get; } /// diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/PostProcessing/GeneratedCodeWorkspace.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/PostProcessing/GeneratedCodeWorkspace.cs index 63feb7fbc0..08b34a424d 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/PostProcessing/GeneratedCodeWorkspace.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/PostProcessing/GeneratedCodeWorkspace.cs @@ -47,6 +47,14 @@ public static void Initialize() _cachedProject = Task.Run(CreateGeneratedCodeProject); } + internal async Task GetCompilationAsync() + { + var compilation = await _project.GetCompilationAsync(); + Debug.Assert(compilation is CSharpCompilation); + + return (CSharpCompilation)compilation; + } + public void AddPlainFiles(string name, string content) { PlainFiles.Add(name, content); @@ -140,17 +148,18 @@ internal static async Task Create() return new GeneratedCodeWorkspace(generatedCodeProject); } - public static GeneratedCodeWorkspace CreateExistingCodeProject(string outputDirectory) + internal static GeneratedCodeWorkspace CreateExistingCodeProject(string projectDirectory, string generatedDirectory) { var workspace = new AdhocWorkspace(); var newOptionSet = workspace.Options.WithChangedOption(FormattingOptions.NewLine, LanguageNames.CSharp, _newLine); workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(newOptionSet)); Project project = workspace.AddProject("ExistingCode", LanguageNames.CSharp); - if (Path.IsPathRooted(outputDirectory)) + if (Path.IsPathRooted(projectDirectory)) { - outputDirectory = Path.GetFullPath(outputDirectory); - project = AddDirectory(project, outputDirectory, null); + projectDirectory = Path.GetFullPath(projectDirectory); + + project = AddDirectory(project, projectDirectory, skipPredicate: sourceFile => sourceFile.StartsWith(generatedDirectory)); } project = project @@ -161,7 +170,7 @@ public static GeneratedCodeWorkspace CreateExistingCodeProject(string outputDire return new GeneratedCodeWorkspace(project); } - public static async Task CreatePreviousContractFromDll(string xmlDocumentationpath, string dllPath) + internal static async Task CreatePreviousContractFromDll(string xmlDocumentationpath, string dllPath) { var workspace = new AdhocWorkspace(); Project project = workspace.AddProject("PreviousContract", LanguageNames.CSharp); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs index 90948de243..666337b445 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/NamedTypeSymbolProvider.cs @@ -6,13 +6,12 @@ using System.Linq; using System.Xml.Linq; using Microsoft.CodeAnalysis; -using Microsoft.Generator.CSharp.Expressions; using Microsoft.Generator.CSharp.Primitives; using Microsoft.Generator.CSharp.Statements; namespace Microsoft.Generator.CSharp.Providers { - public class NamedTypeSymbolProvider : TypeProvider + internal sealed class NamedTypeSymbolProvider : TypeProvider { private INamedTypeSymbol _namedTypeSymbol; @@ -21,6 +20,8 @@ public NamedTypeSymbolProvider(INamedTypeSymbol namedTypeSymbol) _namedTypeSymbol = namedTypeSymbol; } + private protected sealed override TypeProvider? GetCustomCodeView() => null; + protected override string BuildRelativeFilePath() => throw new InvalidOperationException("This type should not be writting in generation"); protected override string BuildName() => _namedTypeSymbol.Name; diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs index 77df97d634..e73b1f0acd 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/Providers/TypeProvider.cs @@ -12,6 +12,17 @@ namespace Microsoft.Generator.CSharp.Providers { public abstract class TypeProvider { + private Lazy _customCodeView; + protected TypeProvider() + { + _customCodeView = new(GetCustomCodeView); + } + + private protected virtual TypeProvider? GetCustomCodeView() + => CodeModelPlugin.Instance.SourceInputModel.FindForType(GetNamespace(), BuildName()); + + public TypeProvider? CustomCodeView => _customCodeView.Value; + protected string? _deprecated; /// @@ -22,7 +33,7 @@ public abstract class TypeProvider private string? _relativeFilePath; - public string Name => _name ??= BuildName(); + public string Name => _name ??= CustomCodeView?.Name ?? BuildName(); private string? _name; @@ -45,7 +56,7 @@ public string? Deprecated private CSharpType? _type; public CSharpType Type => _type ??= new( this, - GetNamespace(), + CustomCodeView?.GetNamespace() ?? GetNamespace(), GetTypeArguments(), GetBaseType()); diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ClientSourceInput.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ClientSourceInput.cs deleted file mode 100644 index 481c8f75d3..0000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ClientSourceInput.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.CodeAnalysis; - -namespace Microsoft.Generator.CSharp.SourceInput -{ - internal record ClientSourceInput(INamedTypeSymbol? ParentClientType); -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/CompilationCustomCode.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/CompilationCustomCode.cs deleted file mode 100644 index 6933eb569a..0000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/CompilationCustomCode.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; -using Microsoft.CodeAnalysis; -using Microsoft.Generator.CSharp.Primitives; - -namespace Microsoft.Generator.CSharp.SourceInput -{ - internal abstract class CompilationCustomCode - { - protected Compilation _compilation; - - public CompilationCustomCode(Compilation compilation) - { - _compilation = compilation; - } - - internal abstract IMethodSymbol? FindMethod(string namespaceName, string typeName, string methodName, IEnumerable parameters); - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ModelTypeMapping.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ModelTypeMapping.cs deleted file mode 100644 index dbf8f4abef..0000000000 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/ModelTypeMapping.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System.Collections.Generic; -using Microsoft.CodeAnalysis; - -namespace Microsoft.Generator.CSharp.SourceInput -{ - internal class ModelTypeMapping - { - private readonly Dictionary _propertyMappings; - private readonly Dictionary _codeGenMemberMappings; - private readonly Dictionary _typeSerializationMappings; - - public string[]? Usages { get; } - public string[]? Formats { get; } - - public ModelTypeMapping(CodeGenAttributes codeGenAttributes, INamedTypeSymbol existingType) - { - _propertyMappings = new(); - _codeGenMemberMappings = new(); - _typeSerializationMappings = new(); - - foreach (ISymbol member in GetMembers(existingType)) - { - // If member is defined in both base and derived class, use derived one - if (ShouldIncludeMember(member) && !_propertyMappings.ContainsKey(member.Name)) - { - _propertyMappings[member.Name] = member; - } - - foreach (var attributeData in member.GetAttributes()) - { - // handle CodeGenMember attribute - if (codeGenAttributes.TryGetCodeGenMemberAttributeValue(attributeData, out var schemaMemberName)) - { - _codeGenMemberMappings[schemaMemberName] = member; - } - } - } - - foreach (var attributeData in existingType.GetAttributes()) - { - // handle CodeGenModel attribute - if (codeGenAttributes.TryGetCodeGenModelAttributeValue(attributeData, out var usage, out var formats)) - { - Usages = usage; - Formats = formats; - } - - // handle CodeGenSerialization attribute - if (codeGenAttributes.TryGetCodeGenSerializationAttributeValue(attributeData, out var propertyName, out var serializationNames, out var serializationHook, out var deserializationHook, out var bicepSerializationHook) && !_typeSerializationMappings.ContainsKey(propertyName)) - { - _typeSerializationMappings.Add(propertyName, new(propertyName, serializationNames, serializationHook, deserializationHook, bicepSerializationHook)); - } - } - } - - private static bool ShouldIncludeMember(ISymbol member) - { - // here we exclude those "CompilerGenerated" members, such as the backing field of a property which is also a field. - return !member.IsImplicitlyDeclared && member is IPropertySymbol or IFieldSymbol; - } - - public ISymbol? GetMemberByOriginalName(string name) - => _codeGenMemberMappings.TryGetValue(name, out var renamedSymbol) ? - renamedSymbol : - _propertyMappings.TryGetValue(name, out var memberSymbol) ? memberSymbol : null; - - internal SourcePropertySerializationMapping? GetForMemberSerialization(string name) - => _typeSerializationMappings.TryGetValue(name, out var serialization) ? serialization : null; - - public IEnumerable GetPropertiesWithSerialization() - { - // only the property with CodeGenSerialization attribute will be emitted into the serialization code. - foreach (var (propertyName, symbol) in _propertyMappings) - { - if (_typeSerializationMappings.ContainsKey(propertyName)) - yield return symbol; - } - } - - private static IEnumerable GetMembers(INamedTypeSymbol? typeSymbol) - { - while (typeSymbol != null) - { - foreach (var symbol in typeSymbol.GetMembers()) - { - yield return symbol; - } - - typeSymbol = typeSymbol.BaseType; - } - } - } -} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourceInputModel.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourceInputModel.cs index b53510a622..97f3227a7e 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourceInputModel.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/src/SourceInput/SourceInputModel.cs @@ -9,38 +9,38 @@ using System.Threading.Tasks; using Microsoft.Build.Construction; using Microsoft.CodeAnalysis; -using Microsoft.Generator.CSharp.Customization; -using Microsoft.Generator.CSharp.Primitives; using Microsoft.Generator.CSharp.Providers; using NuGet.Configuration; namespace Microsoft.Generator.CSharp.SourceInput { - internal sealed class SourceInputModel + public class SourceInputModel { - private static SourceInputModel? _instance; - public static SourceInputModel Instance => _instance ?? throw new InvalidOperationException("SourceInputModel has not been initialized"); - - public static void Initialize(Compilation customization, CompilationCustomCode? existingCompilation = null) - { - _instance = new SourceInputModel(customization, existingCompilation); - } - - private readonly CompilationCustomCode? _existingCompilation; private readonly CodeGenAttributes _codeGenAttributes; - private readonly Dictionary _nameMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - public Compilation Customization { get; } - public Compilation? PreviousContract { get; } + public Compilation? Customization { get; } + private Lazy _previousContract; + public Compilation? PreviousContract => _previousContract.Value; + + private readonly Lazy> _nameMap; - private SourceInputModel(Compilation customization, CompilationCustomCode? existingCompilation = null) + public SourceInputModel(Compilation? customization) { Customization = customization; - PreviousContract = LoadBaselineContract().GetAwaiter().GetResult(); - _existingCompilation = existingCompilation; + _previousContract = new(() => LoadBaselineContract().GetAwaiter().GetResult()); _codeGenAttributes = new CodeGenAttributes(); + _nameMap = new(PopulateNameMap); + } + + private IReadOnlyDictionary PopulateNameMap() + { + var nameMap = new Dictionary(); + if (Customization == null) + { + return nameMap; + } IAssemblySymbol assembly = Customization.Assembly; foreach (IModuleSymbol module in assembly.Modules) @@ -49,75 +49,28 @@ private SourceInputModel(Compilation customization, CompilationCustomCode? exist { if (type is INamedTypeSymbol namedTypeSymbol && TryGetName(type, out var schemaName)) { - _nameMap.Add(schemaName, namedTypeSymbol); + nameMap.Add(schemaName, namedTypeSymbol); } } } - } - public IReadOnlyList? GetServiceVersionOverrides() - { - var osvAttributeType = Customization.GetTypeByMetadataName(typeof(CodeGenOverrideServiceVersionsAttribute).FullName!)!; - var osvAttribute = Customization.Assembly.GetAttributes() - .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, osvAttributeType)); - - return osvAttribute?.ConstructorArguments[0].Values.Select(v => v.Value).OfType().ToList(); + return nameMap; } - internal ModelTypeMapping? CreateForModel(INamedTypeSymbol? symbol) + public TypeProvider? FindForType(string ns, string name) { - if (symbol == null) + if (Customization == null) + { return null; - - return new ModelTypeMapping(_codeGenAttributes, symbol); - } - - internal IMethodSymbol? FindMethod(string namespaceName, string typeName, string methodName, IEnumerable parameters) - { - return _existingCompilation?.FindMethod(namespaceName, typeName, methodName, parameters); - } - - public INamedTypeSymbol? FindForType(string name) - { - var ns = CodeModelPlugin.Instance.Configuration.ModelNamespace; + } var fullyQualifiedMetadataName = $"{ns}.{name}"; - if (!_nameMap.TryGetValue(name, out var type) && - !_nameMap.TryGetValue(fullyQualifiedMetadataName, out type)) + if (!_nameMap.Value.TryGetValue(name, out var type) && + !_nameMap.Value.TryGetValue(fullyQualifiedMetadataName, out type)) { type = Customization.Assembly.GetTypeByMetadataName(fullyQualifiedMetadataName); } - return type; - } - - internal bool TryGetClientSourceInput(INamedTypeSymbol type, [NotNullWhen(true)] out ClientSourceInput? clientSourceInput) - { - foreach (var attribute in type.GetAttributes()) - { - var attributeType = attribute.AttributeClass; - while (attributeType != null) - { - if (attributeType.Name == CodeGenAttributes.CodeGenClientAttributeName) - { - INamedTypeSymbol? parentClientType = null; - foreach ((var argumentName, TypedConstant constant) in attribute.NamedArguments) - { - if (argumentName == nameof(CodeGenClientAttribute.ParentClient)) - { - parentClientType = (INamedTypeSymbol?)constant.Value; - } - } - - clientSourceInput = new ClientSourceInput(parentClientType); - return true; - } - - attributeType = attributeType.BaseType; - } - } - - clientSourceInput = null; - return false; + return type != null ? new NamedTypeSymbolProvider(type) : null; } private bool TryGetName(ISymbol symbol, [NotNullWhen(true)] out string? name) diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpGenTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpGenTests.cs index 5af406bb08..10d7e42572 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpGenTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/CSharpGenTests.cs @@ -10,33 +10,6 @@ namespace Microsoft.Generator.CSharp.Tests { public class CSharpGenTests { - // Validates that the output path is parsed correctly when provided - [Test] - public void TestGetOutputPath_OutputPathProvided() - { - var outputPath = "./outputDir"; - var parsedOutputPath = CSharpGen.ParseGeneratedSourceOutputPath(outputPath); - var expectedPath = Path.Combine(outputPath, "src", "Generated"); - var areEqual = string.Equals(expectedPath, parsedOutputPath, StringComparison.OrdinalIgnoreCase); - - Assert.IsTrue(areEqual); - - // append 'src' to the output path and validate that it is not appended again - TestOutputPathAppended(outputPath, expectedPath); - } - - // Validates that the output path is parsed correctly when an empty string is provided - [Test] - public void TestGetConfigurationInputFilePath_DefaultPath() - { - var outputPath = ""; - var parsedOutputPath = CSharpGen.ParseGeneratedSourceOutputPath(outputPath); - var expectedPath = Path.Combine("src", "Generated"); - var areEqual = string.Equals(expectedPath, parsedOutputPath, StringComparison.OrdinalIgnoreCase); - - Assert.IsTrue(areEqual); - } - // Validates that a valid plugin implementation is accepted [Test] public void TestCSharpGen_ValidPlugin() @@ -61,19 +34,5 @@ public void VisitorsAreVisited() mockPlugin.Verify(m => m.Visitors, Times.Once); mockVisitor.Verify(m => m.Visit(mockPlugin.Object.OutputLibrary), Times.Once); } - - private void TestOutputPathAppended(string outputPath, string expectedPath) - { - var srcPath = "src"; - - outputPath = Path.Combine(outputPath, srcPath); - - - var parsedOutputPath = CSharpGen.ParseGeneratedSourceOutputPath(outputPath); - - var areEqual = string.Equals(expectedPath, parsedOutputPath, StringComparison.OrdinalIgnoreCase); - - Assert.IsTrue(areEqual); - } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ConfigurationTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ConfigurationTests.cs index 78db6d6963..978e77b5f1 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ConfigurationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/ConfigurationTests.cs @@ -49,10 +49,9 @@ public void TestInitialize_NoFileFound() } // Validates that the output folder is parsed correctly from the configuration - [TestCaseSource("ParseConfigOutputFolderTestCases")] + [TestCaseSource(nameof(ParseConfigOutputFolderTestCases))] public void TestParseConfig_OutputFolder(string mockJson, bool throwsError) { - var expected = Path.GetFullPath(MockHelpers.TestHelpersFolder); if (throwsError) @@ -64,6 +63,8 @@ public void TestParseConfig_OutputFolder(string mockJson, bool throwsError) var configuration = Configuration.Load(MockHelpers.TestHelpersFolder, mockJson); Assert.AreEqual(expected, configuration.OutputDirectory); + Assert.AreEqual(Path.Combine(expected, "src", "Generated"), configuration.ProjectGeneratedDirectory); + Assert.AreEqual(Path.Combine(expected, "tests", "Generated"), configuration.TestGeneratedDirectory); } // Validates that the LibraryName field is parsed correctly from the configuration diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Helpers.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Helpers.cs index caffdebfad..869a0f2aee 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Helpers.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Helpers.cs @@ -1,8 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Diagnostics; using System.IO; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis; namespace Microsoft.Generator.CSharp.Tests { @@ -11,15 +14,47 @@ internal static class Helpers private static readonly string _assemblyLocation = Path.GetDirectoryName(typeof(Helpers).Assembly.Location)!; public static string GetExpectedFromFile(string? parameters = null) + { + return File.ReadAllText(GetAssetFilePath(parameters)); + } + + private static string GetAssetFilePath(string? parameters = null) { var stackTrace = new StackTrace(); - var stackFrame = stackTrace.GetFrame(1); - var method = stackFrame!.GetMethod(); + var stackFrame = GetRealMethodInvocation(stackTrace); + var method = stackFrame.GetMethod(); var callingClass = method!.DeclaringType; var nsSplit = callingClass!.Namespace!.Split('.'); - var ns = nsSplit[nsSplit.Length - 1]; + var ns = nsSplit[^1]; var paramString = parameters is null ? string.Empty : $"({parameters})"; - return File.ReadAllText(Path.Combine(_assemblyLocation, ns, "TestData", callingClass.Name, $"{method.Name}{paramString}.cs")); + return Path.Combine(_assemblyLocation, ns, "TestData", callingClass.Name, $"{method.Name}{paramString}.cs"); + } + + private static StackFrame GetRealMethodInvocation(StackTrace stackTrace) + { + int i = 1; + while (i < stackTrace.FrameCount) + { + var frame = stackTrace.GetFrame(i); + if (frame!.GetMethod()!.DeclaringType != typeof(Helpers)) + { + return frame; + } + i++; + } + + throw new InvalidOperationException($"There is no method invocation outside the {typeof(Helpers)} class in the stack trace"); + } + + public static Compilation GetCompilationFromFile(string? parameters = null) + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(File.ReadAllText(GetAssetFilePath(parameters))); + CSharpCompilation compilation = CSharpCompilation.Create("ExistingCode") + .WithOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication)) + .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) + .AddSyntaxTrees(syntaxTree); + + return compilation; } } } diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/ModelProviders/ModelCustomizationTests.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/ModelProviders/ModelCustomizationTests.cs new file mode 100644 index 0000000000..a1dab37ccc --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/ModelProviders/ModelCustomizationTests.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Generator.CSharp.Input; +using Microsoft.Generator.CSharp.Providers; +using Microsoft.Generator.CSharp.Tests.Common; +using NUnit.Framework; + +namespace Microsoft.Generator.CSharp.Tests.Providers // the namespace here is crucial to get correct test data file. +{ + public class ModelCustomizationTests + { + // Validates that the property body's setter is correctly set based on the property type + [TestCase] + public void TestCustomization_CanChangeModelName() + { + MockHelpers.LoadMockPlugin(customization: Helpers.GetCompilationFromFile()); + + var props = new[] + { + InputFactory.Property("prop1", InputFactory.Array(InputPrimitiveType.String)) + }; + + var inputModel = InputFactory.Model("mockInputModel", properties: props); + var modelTypeProvider = new ModelProvider(inputModel); + var customCodeView = modelTypeProvider.CustomCodeView; + + Assert.IsNotNull(customCodeView); + Assert.AreEqual("CustomizedModel", modelTypeProvider.Type.Name); + Assert.AreEqual("NewNamespace.Models", modelTypeProvider.Type.Namespace); + Assert.AreEqual(customCodeView?.Name, modelTypeProvider.Type.Name); + Assert.AreEqual(customCodeView?.Type.Namespace, modelTypeProvider.Type.Namespace); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/TestData/ModelCustomizationTests/TestCustomization_CanChangeModelName.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/TestData/ModelCustomizationTests/TestCustomization_CanChangeModelName.cs new file mode 100644 index 0000000000..dcfbbbf3b9 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/Providers/TestData/ModelCustomizationTests/TestCustomization_CanChangeModelName.cs @@ -0,0 +1,25 @@ +#nullable disable + +using System; + +namespace NewNamespace +{ + // TODO: if we decide to use the public APIs, we do not have to define this attribute here. Tracking: https://github.com/Azure/autorest.csharp/issues/4551 + [AttributeUsage(AttributeTargets.Class)] + internal class CodeGenTypeAttribute : Attribute + { + public string OriginalName { get; } + + public CodeGenTypeAttribute(string originalName) + { + OriginalName = originalName; + } + } +} +namespace NewNamespace.Models +{ + [CodeGenType("MockInputModel")] + public partial class CustomizedModel + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TestHelpers/MockHelpers.cs b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TestHelpers/MockHelpers.cs index 126c470c68..c15c61ab03 100644 --- a/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TestHelpers/MockHelpers.cs +++ b/packages/http-client-csharp/generator/Microsoft.Generator.CSharp/test/TestHelpers/MockHelpers.cs @@ -1,14 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System.IO; using System; -using System.Collections.Generic; +using System.IO; +using Microsoft.CodeAnalysis; using Microsoft.Generator.CSharp.Input; using Microsoft.Generator.CSharp.Primitives; -using Moq.Protected; -using Moq; +using Microsoft.Generator.CSharp.SourceInput; using Microsoft.Generator.CSharp.Tests.Common; +using Moq; +using Moq.Protected; namespace Microsoft.Generator.CSharp.Tests { @@ -20,7 +21,8 @@ public static Mock LoadMockPlugin( Func? createCSharpTypeCore = null, Func? createOutputLibrary = null, string? configuration = null, - InputModelType[]? inputModelTypes = null) + InputModelType[]? inputModelTypes = null, + Compilation? customization = null) { var configFilePath = Path.Combine(AppContext.BaseDirectory, TestHelpersFolder); // initialize the singleton instance of the plugin @@ -47,7 +49,13 @@ public static Mock LoadMockPlugin( mockPlugin.SetupGet(p => p.TypeFactory).Returns(mockTypeFactory.Object); + var sourceInputModel = new Mock(() => new SourceInputModel(customization)) { CallBase = true }; + + mockPlugin.Setup(p => p.SourceInputModel).Returns(sourceInputModel.Object); + CodeModelPlugin.Instance = mockPlugin.Object; + + return mockPlugin; } }