diff --git a/src/Compilers/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.csproj b/src/Compilers/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.csproj index 9dccec2deeff3..29a1ac30d47ca 100644 --- a/src/Compilers/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.csproj +++ b/src/Compilers/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.csproj @@ -84,6 +84,7 @@ + diff --git a/src/Compilers/CSharp/Portable/Symbols/AbstractTypeMap.cs b/src/Compilers/CSharp/Portable/Symbols/AbstractTypeMap.cs index ee43c93bf9e79..1614226389f90 100644 --- a/src/Compilers/CSharp/Portable/Symbols/AbstractTypeMap.cs +++ b/src/Compilers/CSharp/Portable/Symbols/AbstractTypeMap.cs @@ -58,29 +58,34 @@ internal NamedTypeSymbol SubstituteNamedType(NamedTypeSymbol previous) NamedTypeSymbol newConstructedFrom = SubstituteTypeDeclaration(oldConstructedFrom); ImmutableArray oldTypeArguments = previous.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics; - bool changed = !ReferenceEquals(oldConstructedFrom, newConstructedFrom); - var newTypeArguments = ArrayBuilder.GetInstance(oldTypeArguments.Length); + ArrayBuilder newTypeArguments = null; for (int i = 0; i < oldTypeArguments.Length; i++) { var oldArgument = oldTypeArguments[i]; var newArgument = oldArgument.SubstituteType(this); - if (!changed && !oldArgument.IsSameAs(newArgument)) + if (newTypeArguments is null) { - changed = true; + if (oldArgument.IsSameAs(newArgument)) + { + continue; + } + + newTypeArguments = ArrayBuilder.GetInstance(oldTypeArguments.Length); + newTypeArguments.AddRange(oldTypeArguments, i); } newTypeArguments.Add(newArgument); } - if (!changed) + if (newTypeArguments is null && ReferenceEquals(oldConstructedFrom, newConstructedFrom)) { - newTypeArguments.Free(); return previous; } - return newConstructedFrom.ConstructIfGeneric(newTypeArguments.ToImmutableAndFree()).WithTupleDataFrom(previous); + var substitutedArguments = newTypeArguments is null ? oldTypeArguments : newTypeArguments.ToImmutableAndFree(); + return newConstructedFrom.ConstructIfGeneric(substitutedArguments).WithTupleDataFrom(previous); } /// diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/TypeMapTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/TypeMapTests.cs index 221ac874926f2..0b5b9ff2dadf1 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/TypeMapTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/TypeMapTests.cs @@ -11,13 +11,201 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; +using Roslyn.Test.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.CSharp.UnitTests { public class TypeMapTests : CSharpTestBase { + [Theory] + [InlineData("C")] + [InlineData("C")] + [InlineData("C")] + [InlineData("C")] + public void SubstituteNamedType_NoChange(string type) + { + var (map, previous, _) = CreateSubstitution(type, type); + + Assert.Same(previous, map.SubstituteNamedType(previous)); + } + + [Theory] + [InlineData("C", "C")] + [InlineData("C", "C")] + [InlineData("C", "C")] + [InlineData("C", "C")] + [InlineData("C", "C")] + [InlineData("C>", "C>")] + [InlineData("Outer.C>", "Outer.C>")] + public void SubstituteNamedType_ChangedArguments(string type, string substitutedType) + { + var (map, previous, expected) = CreateSubstitution(type, substitutedType); + + var actual = map.SubstituteNamedType(previous); + + Assert.NotSame(previous, actual); + Assert.True(TypeSymbol.Equals(expected, actual, TypeCompareKind.ConsiderEverything)); + Assert.Same(previous.OriginalDefinition, actual.OriginalDefinition); + Assert.True(TypeSymbol.Equals(actual, map.SubstituteNamedType(actual), TypeCompareKind.ConsiderEverything)); + } + + [Theory] + [InlineData("Outer.C", "Outer.C")] + [InlineData("Outer.C", "Outer.C")] + [InlineData("Outer.C", "Outer.C")] + [InlineData("Outer.C", "Outer.C")] + public void SubstituteNamedType_ContainingTypeOnly(string type, string substitutedType) + { + var (map, previous, expected) = CreateSubstitution(type, substitutedType); + + var actual = map.SubstituteNamedType(previous); + + Assert.NotSame(previous, actual); + Assert.True(TypeSymbol.Equals(expected, actual, TypeCompareKind.ConsiderEverything)); + Assert.Same(previous.OriginalDefinition, actual.OriginalDefinition); + Assert.Equal(SpecialType.System_Int32, actual.ContainingType.TypeArguments().Single().SpecialType); + var oldArguments = previous.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics; + var newArguments = actual.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics; + Assert.Equal(oldArguments.Length, newArguments.Length); + for (int i = 0; i < oldArguments.Length; i++) + { + Assert.True(oldArguments[i].Equals(newArguments[i], TypeCompareKind.ConsiderEverything)); + } + } + + private static (TypeMap map, NamedTypeSymbol previous, NamedTypeSymbol expected) CreateSubstitution(string type, string substitutedType) + { + var compilation = CreateCompilation($$""" + public class C { } + public class C { } + public class C { } + public class C { } + public class Outer + { + public class C { } + public class C { } + public class C { } + public class C { } + } + public class Context + { + public {{type}} Previous { get; set; } + public {{substitutedType}} Expected { get; set; } + } + """); + compilation.VerifyEmitDiagnostics(); + var context = compilation.GetTypeByMetadataName("Context`1"); + var map = new TypeMap(context.TypeParameters, + ImmutableArray.Create(TypeWithAnnotations.Create(compilation.GetSpecialType(SpecialType.System_Int32)))); + var previous = (NamedTypeSymbol)((PropertySymbol)context.GetMembers("Previous").Single()).Type; + var expected = (NamedTypeSymbol)((PropertySymbol)context.GetMembers("Expected").Single()).Type; + return (map, previous, expected); + } + + [Fact] + public void SubstituteNamedType_TupleNamesAndNullableArguments() + { + var compilation = CreateCompilation(""" + #nullable enable + public class C { } + public class Context where T : class + { + public C second)> Previous => throw null!; + public C second)> Expected => throw null!; + } + """, targetFramework: TargetFramework.NetCoreApp); + compilation.VerifyEmitDiagnostics(); + var context = compilation.GetTypeByMetadataName("Context`1"); + var previous = (NamedTypeSymbol)((PropertySymbol)context.GetMembers("Previous").Single()).Type; + var expected = (NamedTypeSymbol)((PropertySymbol)context.GetMembers("Expected").Single()).Type; + var map = new TypeMap(context.TypeParameters, + ImmutableArray.Create(TypeWithAnnotations.Create(compilation.GetSpecialType(SpecialType.System_Object), NullableAnnotation.NotAnnotated))); + + var actual = map.SubstituteNamedType(previous); + + Assert.True(TypeSymbol.Equals(expected, actual, TypeCompareKind.ConsiderEverything)); + var arguments = actual.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics; + Assert.Equal(NullableAnnotation.Annotated, arguments[0].NullableAnnotation); + var tuple = (NamedTypeSymbol)arguments[1].Type; + Assert.Equal(new[] { "first", "second" }, tuple.TupleElementNames); + Assert.Equal(NullableAnnotation.Annotated, tuple.TupleElements[0].TypeWithAnnotations.NullableAnnotation); + Assert.Equal(SpecialType.System_Object, tuple.TupleElements[0].Type.SpecialType); + } + + [Fact] + public void SubstituteNamedType_NullabilityOnly() + { + var compilation = CreateCompilation(""" + #nullable enable + public class C { } + public class Context where T : class + { + public C Previous => throw null!; + public C Expected => throw null!; + } + """); + compilation.VerifyEmitDiagnostics(); + var context = compilation.GetTypeByMetadataName("Context`1"); + var previous = (NamedTypeSymbol)((PropertySymbol)context.GetMembers("Previous").Single()).Type; + var expected = (NamedTypeSymbol)((PropertySymbol)context.GetMembers("Expected").Single()).Type; + var map = new TypeMap(context.TypeParameters, + ImmutableArray.Create(TypeWithAnnotations.Create(context.TypeParameters.Single(), NullableAnnotation.Annotated))); + + var actual = map.SubstituteNamedType(previous); + + Assert.NotSame(previous, actual); + Assert.True(TypeSymbol.Equals(expected, actual, TypeCompareKind.ConsiderEverything)); + var oldArgument = previous.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[1]; + var newArgument = actual.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[1]; + Assert.Same(oldArgument.Type, newArgument.Type); + Assert.Equal(NullableAnnotation.NotAnnotated, oldArgument.NullableAnnotation); + Assert.Equal(NullableAnnotation.Annotated, newArgument.NullableAnnotation); + Assert.False(oldArgument.IsSameAs(newArgument)); + } + + [Fact] + public void SubstituteNamedType_CustomModifierOnly() + { + var compilation = CreateCompilation(""" + public class C { } + public class Modifier { } + """); + compilation.VerifyEmitDiagnostics(); + var definition = compilation.GetTypeByMetadataName("C`2"); + var modifier = compilation.GetTypeByMetadataName("Modifier`1"); + var intType = compilation.GetSpecialType(SpecialType.System_Int32); + var stringType = compilation.GetSpecialType(SpecialType.System_String); + var previous = definition.Construct(ImmutableArray.Create( + TypeWithAnnotations.Create(stringType, NullableAnnotation.Annotated), + TypeWithAnnotations.Create(intType, customModifiers: ImmutableArray.Create( + CSharpCustomModifier.CreateOptional(modifier), + CSharpCustomModifier.CreateRequired(stringType))))); + var map = new TypeMap(modifier.TypeParameters, ImmutableArray.Create(TypeWithAnnotations.Create(intType))); + + var actual = map.SubstituteNamedType(previous); + + Assert.NotSame(previous, actual); + var arguments = actual.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics; + Assert.Same(stringType, arguments[0].Type); + Assert.Equal(NullableAnnotation.Annotated, arguments[0].NullableAnnotation); + Assert.Same(intType, arguments[1].Type); + Assert.Collection(arguments[1].CustomModifiers, + m => + { + Assert.True(m.IsOptional); + Assert.True(TypeSymbol.Equals(modifier.Construct(intType), ((CSharpCustomModifier)m).ModifierSymbol, TypeCompareKind.ConsiderEverything)); + }, + m => + { + Assert.False(m.IsOptional); + Assert.Same(stringType, ((CSharpCustomModifier)m).ModifierSymbol); + }); + Assert.False(previous.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[1].IsSameAs(arguments[1])); + } + // take a type of the form Something and return the type X. private TypeSymbol TypeArg(TypeSymbol t) { diff --git a/src/Compilers/Core/Portable/Microsoft.CodeAnalysis.csproj b/src/Compilers/Core/Portable/Microsoft.CodeAnalysis.csproj index c4b23686d08f1..761608d78563d 100644 --- a/src/Compilers/Core/Portable/Microsoft.CodeAnalysis.csproj +++ b/src/Compilers/Core/Portable/Microsoft.CodeAnalysis.csproj @@ -102,6 +102,7 @@ + diff --git a/src/Tools/Benchmarks/GenericTypeSubstitutionBenchmarks.cs b/src/Tools/Benchmarks/GenericTypeSubstitutionBenchmarks.cs new file mode 100644 index 0000000000000..25245b9cf0e61 --- /dev/null +++ b/src/Tools/Benchmarks/GenericTypeSubstitutionBenchmarks.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Linq; +using Basic.Reference.Assemblies; +using BenchmarkDotNet.Attributes; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Symbols; +using Microsoft.CodeAnalysis.Text; + +namespace Benchmarks; + +public enum GenericTypeSubstitutionCase +{ + NoChange0, + NoChange1, + NoChange2, + NoChange8, + FirstChanged1, + FirstChanged2, + FirstChanged8, + LastChanged2, + LastChanged8, + ContainingTypeChanged0, + ContainingTypeChanged1, + ContainingTypeChanged2, + ContainingTypeChanged8, + NestedArgumentChanged, +} + +[MemoryDiagnoser] +public class GenericTypeSubstitutionBenchmarks +{ + private TypeMap _map = null!; + private NamedTypeSymbol _type = null!; + + [ParamsAllValues] + public GenericTypeSubstitutionCase Case { get; set; } + + [GlobalSetup] + public void Setup() + { + var arity = Case switch + { + GenericTypeSubstitutionCase.NoChange0 or GenericTypeSubstitutionCase.ContainingTypeChanged0 => 0, + GenericTypeSubstitutionCase.NoChange1 or GenericTypeSubstitutionCase.FirstChanged1 or + GenericTypeSubstitutionCase.ContainingTypeChanged1 => 1, + GenericTypeSubstitutionCase.NoChange2 or GenericTypeSubstitutionCase.FirstChanged2 or + GenericTypeSubstitutionCase.LastChanged2 or GenericTypeSubstitutionCase.ContainingTypeChanged2 or + GenericTypeSubstitutionCase.NestedArgumentChanged => 2, + GenericTypeSubstitutionCase.NoChange8 or GenericTypeSubstitutionCase.FirstChanged8 or + GenericTypeSubstitutionCase.LastChanged8 or GenericTypeSubstitutionCase.ContainingTypeChanged8 => 8, + _ => throw new InvalidOperationException(), + }; + var typeParameters = arity == 0 + ? "" + : "<" + string.Join(", ", Enumerable.Range(0, arity).Select(i => $"T{i}")) + ">"; + var source = $$""" + public class Parameters { } + public class G{{typeParameters}} { } + public class Outer + { + public class Inner{{typeParameters}} { } + } + """; + var compilation = CSharpCompilation.Create( + nameof(GenericTypeSubstitutionBenchmarks), + [CSharpSyntaxTree.ParseText(SourceText.From(source))], + Net90.References.All, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + var errors = compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).ToArray(); + if (errors.Length != 0) + { + throw new InvalidOperationException(string.Join(Environment.NewLine, errors.Select(d => d.ToString()))); + } + + var parameter = compilation.GlobalNamespace.GetTypeMembers("Parameters").Single().TypeParameters.Single(); + var intType = compilation.GetSpecialType(SpecialType.System_Int32); + var stringType = compilation.GetSpecialType(SpecialType.System_String); + _map = new TypeMap( + ImmutableArray.Create(parameter), + ImmutableArray.Create(TypeWithAnnotations.Create(intType))); + + var definition = compilation.GlobalNamespace.GetTypeMembers("G").Single(); + if (Case is GenericTypeSubstitutionCase.ContainingTypeChanged0 or + GenericTypeSubstitutionCase.ContainingTypeChanged1 or + GenericTypeSubstitutionCase.ContainingTypeChanged2 or + GenericTypeSubstitutionCase.ContainingTypeChanged8) + { + definition = compilation.GlobalNamespace.GetTypeMembers("Outer").Single() + .Construct(parameter).GetTypeMembers("Inner").Single(); + } + + var arguments = Enumerable.Repeat(stringType, arity).ToArray(); + switch (Case) + { + case GenericTypeSubstitutionCase.FirstChanged1: + case GenericTypeSubstitutionCase.FirstChanged2: + case GenericTypeSubstitutionCase.FirstChanged8: + arguments[0] = parameter; + break; + case GenericTypeSubstitutionCase.LastChanged2: + case GenericTypeSubstitutionCase.LastChanged8: + arguments[arity - 1] = parameter; + break; + case GenericTypeSubstitutionCase.NestedArgumentChanged: + arguments[0] = definition.Construct(parameter, stringType); + break; + } + + _type = arity == 0 ? definition : definition.Construct(arguments); + } + + [Benchmark] + public object Substitute() => _map.SubstituteNamedType(_type); +}