diff --git a/src/ILInspector.CSharp.Tests/CSharpDeclarationWriterTests.cs b/src/ILInspector.CSharp.Tests/CSharpDeclarationWriterTests.cs index 255da97dda..aed59eb6cd 100644 --- a/src/ILInspector.CSharp.Tests/CSharpDeclarationWriterTests.cs +++ b/src/ILInspector.CSharp.Tests/CSharpDeclarationWriterTests.cs @@ -428,7 +428,7 @@ public void MethodDeclaration_DoesNotShortenTypeNamesInsideParameterAttributeStr [ new ApiParameter { - Attributes = ["System.Diagnostics.CodeAnalysis.StringSyntax(\"System.String\")"], + Attributes = ["System.Diagnostics.CodeAnalysis.StringSyntax(\"System.String (External.Kind)1\")"], Type = "string", Name = "pattern" } @@ -449,7 +449,7 @@ public void MethodDeclaration_DoesNotShortenTypeNamesInsideParameterAttributeStr """ using System.Diagnostics.CodeAnalysis; - public void Validate([StringSyntax("System.String")] string pattern); + public void Validate([StringSyntax("System.String (External.Kind)1")] string pattern); """, rendered.Source); } diff --git a/src/ILInspector.CSharp.Tests/CSharpFormatterTests.cs b/src/ILInspector.CSharp.Tests/CSharpFormatterTests.cs index d7d21121fc..26faa9e891 100644 --- a/src/ILInspector.CSharp.Tests/CSharpFormatterTests.cs +++ b/src/ILInspector.CSharp.Tests/CSharpFormatterTests.cs @@ -78,6 +78,78 @@ public class Widget Assert.Empty(declaration.Diagnostics); } + [Fact] + public void FormatsPrimaryConstructorParametersInTypeUnit() + { + var type = new ApiType + { + Namespace = "Samples", + Name = "Worker", + Kind = "class" + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ShortWithUsings + }); + + var declaration = formatter.FormatTypeUnit( + type, + members: null, + primaryConstructorParameters: + [ + new ApiParameter + { + Type = "System.String", + Name = "message", + Attributes = + [ + "Attributes.Other.Marker(typeof(External.Value))" + ] + } + ]); + + Assert.Contains( + "public class Worker([Marker(typeof(External.Value))] String message)", + declaration.Text, + StringComparison.Ordinal); + Assert.Contains("Attributes.Other", declaration.Usings); + } + + [Fact] + public void PrimaryConstructorAttributeSuffixShadowKeepsQualifiedName() + { + var type = new ApiType + { + Namespace = "Samples", + Name = "Worker", + Kind = "class" + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ShortWithUsings, + AdditionalRootShadowingNames = ["MarkerAttribute"] + }); + + var declaration = formatter.FormatTypeUnit( + type, + members: null, + primaryConstructorParameters: + [ + new ApiParameter + { + Type = "int", + Name = "value", + Attributes = ["External.Marker"] + } + ]); + + Assert.Contains( + "public class Worker([External.Marker] int value)", + declaration.Text, + StringComparison.Ordinal); + Assert.DoesNotContain("External", declaration.Usings); + } + [Theory] [InlineData(CSharpTypeNamePolicy.Qualified, "public System.Threading.Tasks.Task Run()", false)] [InlineData(CSharpTypeNamePolicy.ShortWithUsings, "public Task Run()", true)] @@ -165,6 +237,517 @@ public FieldWriter(TextWriter writer, IFieldFormatter formatter, MarkoutWriterOp value => value.StartsWith("using ", StringComparison.Ordinal)); } + [Fact] + public void ShortWithUsingsDerivesAlongsideCallerImports() + { + var type = new ApiType { Namespace = "Samples", Name = "Worker", Kind = "class" }; + var member = new ApiMember + { + Name = "CreateTimer", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "System.Windows.Forms.Timer", + MemberName = "CreateTimer" + } + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ShortWithUsings, + Usings = ["System.Threading"] + }); + + var declaration = formatter.FormatMemberUnit(type, member); + + Assert.Contains( + "public Timer CreateTimer()", + declaration.Text, + StringComparison.Ordinal); + Assert.Equal(["System.Windows.Forms"], declaration.Usings); + } + + [Fact] + public void UnqualifiedTypePreventsCollidingImport() + { + var type = new ApiType { Namespace = "App", Name = "Client", Kind = "class" }; + var member = new ApiMember + { + Name = "Get", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "Lib.Node", + MemberName = "Get", + Parameters = [new ApiParameter { Type = "Node", Name = "ambient" }] + } + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ShortWithUsings + }); + + var declaration = formatter.FormatMemberUnit(type, member); + + Assert.Contains( + "public Lib.Node Get(Node ambient)", + declaration.Text, + StringComparison.Ordinal); + Assert.Empty(declaration.Usings); + } + + [Theory] + [InlineData(CSharpTypeNamePolicy.ShortWithUsings)] + [InlineData(CSharpTypeNamePolicy.ContextualShort)] + public void DeclaredTypeNameKeepsCrossNamespaceReferenceQualified( + CSharpTypeNamePolicy policy) + { + var type = new ApiType { Namespace = "Samples", Name = "Task", Kind = "class" }; + var member = new ApiMember + { + Name = "Run", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "System.Threading.Tasks.Task", + MemberName = "Run" + } + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = policy, + Usings = policy == CSharpTypeNamePolicy.ContextualShort + ? ["System.Threading.Tasks"] + : [] + }); + + var declaration = formatter.FormatMemberUnit(type, member); + + Assert.Contains( + "public System.Threading.Tasks.Task Run()", + declaration.Text, + StringComparison.Ordinal); + Assert.Empty(declaration.Usings); + } + + [Fact] + public void NamespaceSegmentKeepsSameNamedReferenceQualified() + { + var type = new ApiType { Namespace = "Samples.Models", Name = "Worker", Kind = "class" }; + var member = new ApiMember + { + Name = "Get", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "External.Models", + MemberName = "Get" + } + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ShortWithUsings, + ContainingNamespace = type.Namespace + }); + + var declaration = formatter.FormatMemberUnit(type, member); + + Assert.Contains( + "public External.Models Get()", + declaration.Text, + StringComparison.Ordinal); + Assert.Empty(declaration.Usings); + } + + [Fact] + public void KnownNamespaceRootKeepsSameNamedReferenceQualified() + { + var type = new ApiType + { + Namespace = "Samples", + Name = "Worker", + Kind = "class", + Members = + [ + new ApiMember + { + Name = "GetWidget", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "Alpha.Beta.Widget", + MemberName = "GetWidget" + } + }, + new ApiMember + { + Name = "GetAlpha", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "Zeta.Alpha", + MemberName = "GetAlpha" + } + } + ] + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ShortWithUsings, + ContainingNamespace = type.Namespace + }); + + var declaration = formatter.FormatTypeUnit(type, type.Members); + + Assert.Contains("public Widget GetWidget();", declaration.Text, StringComparison.Ordinal); + Assert.Contains("public Zeta.Alpha GetAlpha();", declaration.Text, StringComparison.Ordinal); + Assert.Equal(["Alpha.Beta"], declaration.Usings); + } + + [Fact] + public void EnclosingNamespaceChildKeepsSameNamedReferenceQualified() + { + var type = new ApiType + { + Namespace = "Alpha.Beta", + Name = "Worker", + Kind = "class", + Members = + [ + new ApiMember + { + Name = "GetThing", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "Alpha.Gamma.Thing", + MemberName = "GetThing" + } + }, + new ApiMember + { + Name = "GetGamma", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "Other.Gamma", + MemberName = "GetGamma" + } + } + ] + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ShortWithUsings, + ContainingNamespace = type.Namespace + }); + + var declaration = formatter.FormatTypeUnit(type, type.Members); + + Assert.Contains("public Thing GetThing();", declaration.Text, StringComparison.Ordinal); + Assert.Contains("public Other.Gamma GetGamma();", declaration.Text, StringComparison.Ordinal); + Assert.Equal(["Alpha.Gamma"], declaration.Usings); + } + + [Fact] + public void UnrelatedNamespaceChildDoesNotShadowSameNamedReference() + { + var type = new ApiType + { + Namespace = "Alpha.Beta", + Name = "Worker", + Kind = "class", + Members = + [ + new ApiMember + { + Name = "GetThing", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "Zeta.Delta.Thing", + MemberName = "GetThing" + } + }, + new ApiMember + { + Name = "GetDelta", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "Other.Delta", + MemberName = "GetDelta" + } + } + ] + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ShortWithUsings, + ContainingNamespace = type.Namespace + }); + + var declaration = formatter.FormatTypeUnit(type, type.Members); + + Assert.Contains("public Thing GetThing();", declaration.Text, StringComparison.Ordinal); + Assert.Contains("public Delta GetDelta();", declaration.Text, StringComparison.Ordinal); + Assert.Equal( + ["Other", "Zeta.Delta"], + declaration.Usings.Order(StringComparer.Ordinal)); + } + + [Fact] + public void ContainingNamespaceChildShadowedRootUsesGlobalAlias() + { + var type = new ApiType { Namespace = "Alpha.System", Name = "Worker", Kind = "class" }; + var member = new ApiMember + { + Name = "GetUri", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "System.Uri", + MemberName = "GetUri" + } + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified, + ContainingNamespace = type.Namespace + }); + + var declaration = formatter.FormatMemberUnit(type, member); + + Assert.Contains( + "public global::System.Uri GetUri()", + declaration.Text, + StringComparison.Ordinal); + } + + [Fact] + public void ContainingNamespaceRootDoesNotRequireGlobalAlias() + { + var type = new ApiType { Namespace = "System.Example", Name = "Worker", Kind = "class" }; + var member = new ApiMember + { + Name = "GetUri", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "System.Uri", + MemberName = "GetUri" + } + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified, + ContainingNamespace = type.Namespace + }); + + var declaration = formatter.FormatMemberUnit(type, member); + + Assert.Contains("public System.Uri GetUri()", declaration.Text, StringComparison.Ordinal); + Assert.DoesNotContain("global::System.Uri", declaration.Text, StringComparison.Ordinal); + } + + [Fact] + public void QualifiedPolicyUsesGlobalAliasWhenNamespaceRootIsShadowed() + { + var type = new ApiType + { + Namespace = "Samples", + Name = "Worker`1", + Kind = "class", + TypeParameters = [new TypeParameter { Name = "System" }] + }; + var member = new ApiMember + { + Name = "Run", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "System.Threading.Tasks.Task", + MemberName = "Run" + } + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + var declaration = formatter.FormatMemberUnit(type, member); + + Assert.Contains( + "public global::System.Threading.Tasks.Task Run()", + declaration.Text, + StringComparison.Ordinal); + } + + [Fact] + public void QualifiedPolicyEscapesShadowedKeywordNamespaceRoot() + { + var type = new ApiType + { + Namespace = "Samples", + Name = "Worker`1", + Kind = "class", + TypeParameters = [new TypeParameter { Name = "event" }] + }; + var member = new ApiMember + { + Name = "GetWidget", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "event.Models.Widget", + MemberName = "GetWidget" + } + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + var declaration = formatter.FormatMemberUnit(type, member); + + Assert.Contains( + "public global::@event.Models.Widget GetWidget()", + declaration.Text, + StringComparison.Ordinal); + Assert.DoesNotContain("@global::", declaration.Text, StringComparison.Ordinal); + } + + [Fact] + public void QualifiedPolicyEscapesKeywordRootInsideExistingGlobalAlias() + { + var type = new ApiType + { + Namespace = "Samples", + Name = "Worker", + Kind = "class" + }; + var member = new ApiMember + { + Name = "GetWidget", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "global::event.Models.Widget", + MemberName = "GetWidget" + } + }; + + var declaration = new CSharpFormatter().FormatMemberUnit(type, member); + + Assert.Contains( + "public global::@event.Models.Widget GetWidget()", + declaration.Text, + StringComparison.Ordinal); + Assert.DoesNotContain("global::global::", declaration.Text, StringComparison.Ordinal); + } + + [Theory] + [InlineData(CSharpTypeNamePolicy.Qualified, "public @event.Models.Widget GetWidget()", false)] + [InlineData(CSharpTypeNamePolicy.ShortWithUsings, "public Widget GetWidget()", true)] + [InlineData(CSharpTypeNamePolicy.ContextualShort, "public Widget GetWidget()", false)] + public void KeywordNamespaceRootMatchesTypeNamePlan( + CSharpTypeNamePolicy policy, + string expectedDeclaration, + bool importNamespace) + { + var type = new ApiType + { + Namespace = "Samples", + Name = "Worker", + Kind = "class" + }; + var member = new ApiMember + { + Name = "GetWidget", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "event.Models.Widget", + MemberName = "GetWidget" + } + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = policy, + Usings = policy == CSharpTypeNamePolicy.ContextualShort + ? ["event.Models"] + : [] + }); + + var declaration = formatter.FormatMemberUnit(type, member); + + Assert.Contains(expectedDeclaration, declaration.Text, StringComparison.Ordinal); + if (importNamespace) + Assert.Contains("using @event.Models;", declaration.Text, StringComparison.Ordinal); + } + + [Fact] + public void QualifiedPolicyEscapesKeywordTypeWithShadowedRoot() + { + var type = new ApiType + { + Namespace = "Samples", + Name = "Worker`1", + Kind = "class", + TypeParameters = [new TypeParameter { Name = "Alpha" }] + }; + var member = new ApiMember + { + Name = "GetEvent", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "Alpha.event", + MemberName = "GetEvent" + } + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + var declaration = formatter.FormatMemberUnit(type, member); + + Assert.Contains( + "public global::Alpha.@event GetEvent()", + declaration.Text, + StringComparison.Ordinal); + } + + [Fact] + public void ShortPolicyEscapesKeywordType() + { + var type = new ApiType + { + Namespace = "Samples", + Name = "Worker", + Kind = "class" + }; + var member = new ApiMember + { + Name = "GetEvent", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "Alpha.event", + MemberName = "GetEvent" + } + }; + var formatter = new CSharpFormatter(new CSharpFormatOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ShortWithUsings + }); + + var declaration = formatter.FormatMemberUnit(type, member); + + Assert.Contains("Alpha", declaration.Usings); + Assert.Contains("public @event GetEvent()", declaration.Text, StringComparison.Ordinal); + } + [Fact] public void FormatsParameterListsWithAttributesDefaultsAndEscapedKeywords() { @@ -230,6 +813,47 @@ public void FormatsDelegateWithStructuredAccessibility() new CSharpFormatter().FormatDelegate(type, type.Members.Single())); } + [Fact] + public void FormatsDelegateWithContainedHostileName() + { + var type = new ApiType + { + Namespace = "Samples", + Name = "Call\nback", + Kind = "delegate", + Accessibility = "private" + }; + var invoke = new ApiMember + { + Name = "Invoke", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "void", + MemberName = "Invoke" + } + }; + + Assert.Equal( + "private delegate void Call_back();", + new CSharpFormatter().FormatDelegate(type, invoke)); + } + + [Fact] + public void FormatsNestedTypeNameWithoutFoldingItsNestingSeparator() + { + var type = new ApiType + { + Namespace = "Samples", + Name = "Outer.Inner", + Kind = "class" + }; + + Assert.Equal( + "public class Outer.Inner", + new CSharpFormatter().FormatTypeDeclaration(type)); + } + [Fact] public void KnownIdentifierEscapingIsIdempotent() { diff --git a/src/ILInspector.CSharp.Tests/CSharpTypePrinterTests.cs b/src/ILInspector.CSharp.Tests/CSharpTypePrinterTests.cs index 435f1e9b15..bfc4641441 100644 --- a/src/ILInspector.CSharp.Tests/CSharpTypePrinterTests.cs +++ b/src/ILInspector.CSharp.Tests/CSharpTypePrinterTests.cs @@ -951,6 +951,86 @@ public void DerivedUsingShortensCrossNamespaceReference() Assert.Contains("public Task Run();", result.Units[0].Source, StringComparison.Ordinal); } + [Fact] + public void DerivationUsesOnlySelectedMembers() + { + var type = CreateEmptyType("Samples", "Worker"); + var selected = CreateMethod("Open"); + selected.SignatureModel!.ReturnType = "System.IO.Stream"; + var omitted = CreateMethod("CreateTimer"); + omitted.SignatureModel!.ReturnType = "System.Windows.Forms.Timer"; + type.Members.Add(selected); + type.Members.Add(omitted); + + var result = _printer.Print(new CSharpTypePrintRequest(type, members: [selected])); + + Assert.Equal(["System.IO"], result.Usings); + Assert.Contains("public Stream Open();", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain("System.Windows.Forms", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void ConfiguredNamespaceShortensPrimaryConstructorParameter() + { + var type = CreateEmptyType("Samples", "Worker"); + + var result = _printer.Print( + new CSharpTypePrintRequest( + type, + primaryConstructorParameters: + [ + new ApiParameter { Type = "System.IO.TextWriter", Name = "writer" } + ]), + new CSharpTypePrintOptions { Usings = ["System.IO"] }); + + Assert.Equal(["System.IO"], result.Usings); + Assert.Contains("public class Worker(TextWriter writer)", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void PrimaryConstructorNestedTypeDoesNotInventNamespace() + { + var type = CreateEmptyType("Samples", "Worker"); + + var result = _printer.Print(new CSharpTypePrintRequest( + type, + primaryConstructorParameters: + [ + new ApiParameter + { + Type = "System.Environment.SpecialFolder", + Name = "folder" + } + ])); + + Assert.Empty(result.Usings); + Assert.Contains( + "public class Worker(System.Environment.SpecialFolder folder)", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void PrimaryConstructorTypeStaysQualifiedWithDistinctConfiguredUsing() + { + var type = CreateEmptyType("Samples", "Worker"); + + var result = _printer.Print( + new CSharpTypePrintRequest( + type, + primaryConstructorParameters: + [ + new ApiParameter { Type = "Lib.Exception", Name = "exception" } + ]), + new CSharpTypePrintOptions { Usings = ["System"] }); + + Assert.Equal(["System"], result.Usings); + Assert.Contains( + "public class Worker(Lib.Exception exception)", + result.Source, + StringComparison.Ordinal); + } + [Fact] public void FullMemberUsesBareTypesBackedByNamespaceSet() { @@ -1187,6 +1267,160 @@ public void CrossTypeAmbiguousSimpleNameStaysQualifiedAcrossUnit() Assert.Contains("public MyNamespace.String N1();", result.Source, StringComparison.Ordinal); } + [Fact] + public void CollisionDoesNotBlockUnrelatedSameNamespaceShortening() + { + var type = CreateEmptyType("Alpha", "Widget"); + type.Members.Add(new ApiMember + { + Name = "GetPanel", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "Alpha.Panel", + MemberName = "GetPanel" + } + }); + type.Members.Add(new ApiMember + { + Name = "Pair", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "Alpha.Button", + MemberName = "Pair", + Parameters = + [ + new ApiParameter { Type = "Beta.Button", Name = "other" }, + new ApiParameter { Type = "Alpha.Panel", Name = "panel" } + ] + } + }); + + var result = _printer.Print(new CSharpTypePrintRequest(type)); + + Assert.Contains("public Panel GetPanel();", result.Source, StringComparison.Ordinal); + Assert.Contains( + "public Button Pair(Beta.Button other, Panel panel);", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void NestedDeclaredTypeShadowsSameNamespaceTopLevelReference() + { + var outer = CreateEmptyType("Alpha", "Widget"); + var member = CreateMethod("GetButton"); + member.SignatureModel!.ReturnType = "Alpha.Button"; + outer.Members.Add(member); + var nested = CreateEmptyType("Alpha", "Button"); + + var result = _printer.Print(new CSharpTypePrintRequest( + outer, + nestedTypes: [new CSharpTypePrintRequest(nested)])); + + Assert.Contains( + "public Alpha.Button GetButton();", + result.Source, + StringComparison.Ordinal); + Assert.Contains("public class Button", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void EnclosingTypeParameterShadowsSameNamespaceReferencesInNestedType() + { + var outer = CreateEmptyType("Alpha", "Widget`1"); + outer.TypeParameters = [new TypeParameter { Name = "T" }]; + var nested = CreateEmptyType("Alpha", "Inner"); + var member = CreateMethod("Get"); + member.SignatureModel!.ReturnType = "Alpha.T"; + nested.Members.Add(member); + + var result = _printer.Print(new CSharpTypePrintRequest( + outer, + nestedTypes: + [ + new CSharpTypePrintRequest( + nested, + primaryConstructorParameters: + [ + new ApiParameter { Type = "Alpha.T", Name = "value" } + ]) + ])); + + Assert.Contains( + "public class Inner(Alpha.T value)", + result.Source, + StringComparison.Ordinal); + Assert.Contains("public Alpha.T Get();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void ContextualShortFiltersCollidingCallerImportsAcrossUnit() + { + var first = CreateEmptyType("Samples", "First"); + first.Members.Add(new ApiMember + { + Name = "Get", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "Alpha.Widget", + MemberName = "Get" + } + }); + var second = CreateEmptyType("Samples", "Second"); + second.Members.Add(new ApiMember + { + Name = "Get", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "Beta.Widget", + MemberName = "Get" + } + }); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(first), new CSharpTypePrintRequest(second)], + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ContextualShort, + Usings = ["Alpha", "Beta"] + }); + + Assert.Contains("using Alpha;", result.Source, StringComparison.Ordinal); + Assert.Contains("using Beta;", result.Source, StringComparison.Ordinal); + Assert.Contains("public Alpha.Widget Get();", result.Source, StringComparison.Ordinal); + Assert.Contains("public Beta.Widget Get();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void ShortWithUsingsDerivesAlongsideCallerImports() + { + var type = CreateEmptyType("Samples", "Worker"); + var member = CreateMethod("CreateTimer"); + member.SignatureModel!.ReturnType = "System.Windows.Forms.Timer"; + type.Members.Add(member); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ShortWithUsings, + Usings = ["System.Threading"] + }); + + Assert.Equal( + ["System.Threading", "System.Windows.Forms"], + result.Usings.Order(StringComparer.Ordinal)); + Assert.Contains("using System.Windows.Forms;", result.Source, StringComparison.Ordinal); + Assert.Contains( + "public Timer CreateTimer();", + result.Units[0].Source, + StringComparison.Ordinal); + } + [Fact] public void RawSignatureMethodTypeParameterShadowsReferenceAndStaysQualified() { @@ -1268,7 +1502,7 @@ public void AttributeArgumentEnumAccessDoesNotDeriveTypeAsNamespace() Kind = "method", SignatureModel = new ApiSignature { - ReturnType = "int", + ReturnType = "System.Runtime.InteropServices.UnmanagedType", MemberName = "Encode", Parameters = [ @@ -1296,401 +1530,2138 @@ public void AttributeArgumentEnumAccessDoesNotDeriveTypeAsNamespace() "[MarshalAs(System.Runtime.InteropServices.UnmanagedType.I4)]", result.Units[0].Source, StringComparison.Ordinal); + Assert.DoesNotContain( + "[MarshalAs(UnmanagedType.I4)]", + result.Units[0].Source, + StringComparison.Ordinal); + Assert.Contains("public UnmanagedType Encode(", result.Source, StringComparison.Ordinal); } [Fact] - public void GenericTypeParameterShadowsReferenceAndStaysQualified() + public void DottedAttributeValueSharingDeclaredTypeRootUsesGlobalNamespace() { - // The type parameter `Task` shadows any same-named type reference within the - // type body, so importing System.Threading.Tasks and shortening the return type - // to `Task` would rebind it to the parameter. It must stay fully qualified. - var type = CreateEmptyType("Samples", "Box`1"); - type.TypeParameters = [new TypeParameter { Name = "Task" }]; - var member = CreateMethod("Run"); - member.SignatureModel!.ReturnType = "System.Threading.Tasks.Task"; + var type = CreateEmptyType("App", "Samples"); + var member = CreateMethod("GetColor"); + member.SignatureModel!.ReturnType = "int"; + member.Attributes = + [ + "System.ComponentModel.DefaultValue(Samples.Models.Color.Red)", + "System.ComponentModel.Description(\"Items[0]\")" + ]; type.Members.Add(member); - var result = _printer.Print(new CSharpTypePrintRequest(type)); + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified, + IncludeCustomAttributes = true + }); - Assert.DoesNotContain("using System.Threading.Tasks;", result.Source, StringComparison.Ordinal); Assert.Contains( - "public System.Threading.Tasks.Task Run();", - result.Units[0].Source, + "[System.ComponentModel.DefaultValue(global::Samples.Models.Color.Red)]", + result.Source, + StringComparison.Ordinal); + Assert.Contains( + "public int GetColor();", + result.Source, StringComparison.Ordinal); } [Fact] - public void MethodTypeParameterShadowsReferenceAndStaysQualified() + public void DeclaredNestedPathInOtherNamespaceDoesNotCaptureAttributeValue() { - // A method type parameter named `Task` shadows the same-named type reference; - // the namespace must not be imported. - var type = CreateEmptyType("Samples", "Worker"); - var member = CreateMethod("Run"); - member.SignatureModel!.ReturnType = "System.Threading.Tasks.Task"; - member.SignatureModel!.TypeParameters = [new TypeParameter { Name = "Task" }]; - type.Members.Add(member); + var otherContainer = CreateEmptyType("Other", "Container"); + var kind = CreateEmptyType("Other", "Kind"); + var appContainer = CreateEmptyType("App", "Container"); + appContainer.Attributes = ["Ext.Opt(Container.Kind.Fast)"]; - var result = _printer.Print(new CSharpTypePrintRequest(type)); + var result = _printer.PrintBatch( + [ + new CSharpTypePrintRequest( + otherContainer, + nestedTypes: [new CSharpTypePrintRequest(kind)]), + new CSharpTypePrintRequest(appContainer) + ], + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified, + IncludeCustomAttributes = true + }); - Assert.DoesNotContain("using System.Threading.Tasks;", result.Source, StringComparison.Ordinal); Assert.Contains( - "System.Threading.Tasks.Task Run", - result.Units[0].Source, + "[Ext.Opt(global::Container.Kind.Fast)]", + result.Source, StringComparison.Ordinal); } [Fact] - public void NestedTypeReferencedAsNamespaceIsNotImportedWhenEnclosingTypeIsReferenced() + public void ImportedDeclaredNestedPathKeepsRelativeAttributeValue() { - // `System.Environment.SpecialFolder` is a nested type but arrives as a flat - // dotted string, so the last-dot split derives namespace `System.Environment` - // — which is actually a type. Emitting `using System.Environment;` is illegal. - // When the enclosing type `System.Environment` is itself referenced in the - // unit, its full name shows up as a derived namespace and must be excluded, so - // the nested reference stays fully qualified. - var type = CreateEmptyType("App", "Consumer"); - var enclosing = CreateMethod("GetEnv"); - enclosing.SignatureModel!.ReturnType = "System.Environment"; - var nested = CreateMethod("GetFolder"); - nested.SignatureModel!.ReturnType = "System.Environment.SpecialFolder"; - type.Members.Add(enclosing); - type.Members.Add(nested); + var foo = CreateEmptyType("A", "Foo"); + var options = CreateEmptyType("A", "Options"); + var consumer = CreateEmptyType("App", "Consumer"); + consumer.Attributes = ["Ext.Opt(Foo.Options.Fast)"]; + var method = CreateMethod("GetFoo"); + method.SignatureModel!.ReturnType = "A.Foo"; + consumer.Members.Add(method); - var result = _printer.Print(new CSharpTypePrintRequest(type)); + var result = _printer.PrintBatch( + [ + new CSharpTypePrintRequest( + foo, + nestedTypes: [new CSharpTypePrintRequest(options)]), + new CSharpTypePrintRequest(consumer) + ], + new CSharpTypePrintOptions { IncludeCustomAttributes = true }); - Assert.DoesNotContain("using System.Environment;", result.Source, StringComparison.Ordinal); - Assert.Contains( - "System.Environment.SpecialFolder GetFolder();", - result.Units[0].Source, - StringComparison.Ordinal); + Assert.Contains("using A;", result.Source, StringComparison.Ordinal); + Assert.Contains("[Ext.Opt(Foo.Options.Fast)]", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain("global::Foo.Options.Fast", result.Source, StringComparison.Ordinal); } - [Theory] - [InlineData(CSharpTypeNamePolicy.ShortWithUsings)] - [InlineData(CSharpTypeNamePolicy.ContextualShort)] - public void UsingsSuppressedKeepsReferencesQualified(CSharpTypeNamePolicy policy) + [Fact] + public void AttributeValueDoesNotDriveImportedDeclaredNestedPathUsing() { - // With IncludeUsings=false the composed Source omits using directives, so - // shortening a cross-namespace reference would leave it unresolvable. - var type = CreateEmptyType("Samples", "Worker"); - var member = CreateMethod("Run"); - member.SignatureModel!.ReturnType = "System.Threading.Tasks.Task"; - type.Members.Add(member); + var foo = CreateEmptyType("A", "Foo"); + var options = CreateEmptyType("A", "Options"); + var consumer = CreateEmptyType("App", "Consumer"); + consumer.Attributes = ["Ext.Opt(Foo.Options.Fast)"]; + + var result = _printer.PrintBatch( + [ + new CSharpTypePrintRequest( + foo, + nestedTypes: [new CSharpTypePrintRequest(options)]), + new CSharpTypePrintRequest(consumer) + ], + new CSharpTypePrintOptions { IncludeCustomAttributes = true }); + + Assert.DoesNotContain("using A;", result.Source, StringComparison.Ordinal); + Assert.Contains("[Ext.Opt(Foo.Options.Fast)]", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void AmbiguousImportedNestedPathIsNotPreservedAsRelative() + { + var aFoo = CreateEmptyType("A", "Foo"); + var aOptions = CreateEmptyType("A", "Options"); + var bFoo = CreateEmptyType("B", "Foo"); + var bOptions = CreateEmptyType("B", "Options"); + var consumer = CreateEmptyType("App", "Consumer"); + consumer.Attributes = ["Ext.Opt(Foo.Options.Fast)"]; + var getLeft = CreateMethod("GetLeft"); + getLeft.SignatureModel!.ReturnType = "A.Left"; + var getRight = CreateMethod("GetRight"); + getRight.SignatureModel!.ReturnType = "B.Right"; + consumer.Members.Add(getLeft); + consumer.Members.Add(getRight); + + var result = _printer.PrintBatch( + [ + new CSharpTypePrintRequest( + aFoo, + nestedTypes: [new CSharpTypePrintRequest(aOptions)]), + new CSharpTypePrintRequest( + bFoo, + nestedTypes: [new CSharpTypePrintRequest(bOptions)]), + new CSharpTypePrintRequest(consumer) + ], + new CSharpTypePrintOptions { IncludeCustomAttributes = true }); + + Assert.Contains("using A;", result.Source, StringComparison.Ordinal); + Assert.Contains("using B;", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain("[Ext.Opt(Foo.Options.Fast)]", result.Source, StringComparison.Ordinal); + Assert.Contains("[Ext.Opt(global::Foo.Options.Fast)]", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void DeclaredNestedPathDoesNotCaptureKnownNamespaceReference() + { + var system = CreateEmptyType("App", "System"); + var uri = CreateEmptyType("App", "Uri"); + system.Attributes = ["Ext.Opt(System.Uri.SchemeDelimiter)"]; + var member = CreateMethod("Create"); + member.SignatureModel!.ReturnType = "System.Text.StringBuilder"; + system.Members.Add(member); var result = _printer.Print( - new CSharpTypePrintRequest(type), + new CSharpTypePrintRequest( + system, + nestedTypes: [new CSharpTypePrintRequest(uri)]), new CSharpTypePrintOptions { - TypeNamePolicy = policy, - Usings = ["System.Threading.Tasks"], - IncludeUsings = false + TypeNamePolicy = CSharpTypeNamePolicy.Qualified, + IncludeCustomAttributes = true }); - Assert.DoesNotContain("using System.Threading.Tasks;", result.Source, StringComparison.Ordinal); - Assert.Empty(result.Usings); Assert.Contains( - "public System.Threading.Tasks.Task Run();", - result.Units[0].Source, + "[Ext.Opt(global::System.Uri.SchemeDelimiter)]", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void KeywordRootInDottedAttributeValueIsEscapedWithoutShortening() + { + var type = CreateEmptyType("Samples", "Worker"); + var member = CreateMethod("GetColor"); + member.SignatureModel!.ReturnType = "int"; + member.Attributes = + [ + "System.ComponentModel.DefaultValue(event.Models.Color.Red)" + ]; + type.Members.Add(member); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ShortWithUsings, + IncludeCustomAttributes = true + }); + + Assert.Contains( + "@event.Models.Color.Red", + result.Source, + StringComparison.Ordinal); + Assert.DoesNotContain("DefaultValue(Color.Red)", result.Source, StringComparison.Ordinal); + Assert.Contains("public int GetColor();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void DottedAttributeValueRootedAtGlobalTypeDoesNotAbortBatch() + { + var host = CreateEmptyType("", "Host"); + var worker = CreateEmptyType("Samples", "Worker"); + var member = CreateMethod("Get"); + member.SignatureModel!.ReturnType = "int"; + member.Attributes = ["Ext.Opt(Host.Options.Fast)"]; + worker.Members.Add(member); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(host), new CSharpTypePrintRequest(worker)], + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified, + IncludeCustomAttributes = true + }); + + Assert.Contains("[Ext.Opt(global::Host.Options.Fast)]", result.Source, StringComparison.Ordinal); + Assert.Empty(result.Diagnostics); + } + + [Fact] + public void TypeAndDelegateAttributeValuesKeepDeclaredTypeRoots() + { + var samples = CreateEmptyType("App", "Samples"); + samples.Attributes = ["Ext.Opt(Samples.Options.Fast)"]; + var options = CreateEmptyType("App", "Options"); + var handler = CreateEmptyType("App", "Handler"); + handler.Kind = "delegate"; + handler.Attributes = ["Ext.Opt(Samples.Options.Fast)"]; + var invoke = CreateMethod("Invoke"); + invoke.SignatureModel!.ReturnType = "void"; + handler.Members.Add(invoke); + + var result = _printer.PrintBatch( + [ + new CSharpTypePrintRequest( + samples, + nestedTypes: [new CSharpTypePrintRequest(options)]), + new CSharpTypePrintRequest(handler) + ], + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified, + IncludeCustomAttributes = true + }); + + Assert.Equal( + 2, + result.Source.Split( + "[Ext.Opt(Samples.Options.Fast)]", + StringSplitOptions.None).Length - 1); + Assert.DoesNotContain("global::Samples.Options.Fast", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void DottedAttributeValueDoesNotInventShadowingNamespace() + { + var type = CreateEmptyType("Lib.Sub", "Widget"); + var member = CreateMethod("Get"); + member.SignatureModel!.ReturnType = "Foo.Deep"; + member.Attributes = ["Ext.Opt(Lib.Sub.Deep.Const.Field)"]; + type.Members.Add(member); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ShortWithUsings, + IncludeCustomAttributes = true + }); + + Assert.Contains("using Foo;", result.Source, StringComparison.Ordinal); + Assert.Contains("public Deep Get();", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain("shadowed by a namespace", string.Join('\n', result.Diagnostics), StringComparison.Ordinal); + } + + [Fact] + public void RawStringAttributeValueIsNotRewritten() + { + var type = CreateEmptyType("App", "Consumer"); + var method = CreateMethod("Get"); + method.SignatureModel!.ReturnType = "N.Type"; + method.SignatureModel.Parameters.Add(new ApiParameter + { + Type = "string", + Name = "value", + Attributes = ["Ext.Note(\"\"\"\"N.Type\"\"\"\")"] + }); + type.Members.Add(method); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions { IncludeCustomAttributes = true }); + + Assert.Contains("\"\"\"\"N.Type\"\"\"\"", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain( + "[Ext.Note(\"\"\"\"Type\"\"\"\")]", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void SameNamespaceRootShadowRequalifiesDottedAttributeValue() + { + var widget = CreateEmptyType("Lib.Sub", "Widget"); + var member = CreateMethod("GetThing"); + member.SignatureModel!.ReturnType = "Lib.Sub.Thing"; + member.Attributes = ["Ext.Opt(Lib.Sub.Thing.Value)"]; + widget.Members.Add(member); + var lib = CreateEmptyType("Lib.Sub", "Lib"); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(widget), new CSharpTypePrintRequest(lib)], + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ShortWithUsings, + IncludeCustomAttributes = true + }); + + Assert.Contains("[Ext.Opt(global::Lib.Sub.Thing.Value)]", result.Source, StringComparison.Ordinal); + Assert.Contains("public Thing GetThing();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void RawKeywordSegmentsArePlannedAcrossDeclarationSurfaces() + { + var type = CreateEmptyType("Samples", "Widget"); + type.BaseType = "Lib.event.Base"; + type.Interfaces.Add("Lib.event.IThing"); + type.Attributes = ["Lib.event.Marker"]; + var invoke = CreateMethod("Invoke"); + invoke.SignatureModel!.ReturnType = "Lib.event.Color"; + var handler = CreateEmptyType("Samples", "Handler"); + handler.Kind = "delegate"; + handler.Members.Add(invoke); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(type), new CSharpTypePrintRequest(handler)], + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified, + IncludeCustomAttributes = true + }); + + Assert.Contains("[Lib.@event.Marker]", result.Source, StringComparison.Ordinal); + Assert.Contains( + "public class Widget : Lib.@event.Base, Lib.@event.IThing", + result.Source, + StringComparison.Ordinal); + Assert.Contains( + "public delegate Lib.@event.Color Handler();", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void GenericTypeParameterShadowsReferenceAndStaysQualified() + { + // The type parameter `Task` shadows any same-named type reference within the + // type body, so importing System.Threading.Tasks and shortening the return type + // to `Task` would rebind it to the parameter. It must stay fully qualified. + var type = CreateEmptyType("Samples", "Box`1"); + type.TypeParameters = [new TypeParameter { Name = "Task" }]; + var member = CreateMethod("Run"); + member.SignatureModel!.ReturnType = "System.Threading.Tasks.Task"; + type.Members.Add(member); + + var result = _printer.Print(new CSharpTypePrintRequest(type)); + + Assert.DoesNotContain("using System.Threading.Tasks;", result.Source, StringComparison.Ordinal); + Assert.Contains( + "public System.Threading.Tasks.Task Run();", + result.Units[0].Source, + StringComparison.Ordinal); + } + + [Fact] + public void MethodTypeParameterShadowsReferenceAndStaysQualified() + { + // A method type parameter named `Task` shadows the same-named type reference; + // the namespace must not be imported. + var type = CreateEmptyType("Samples", "Worker"); + var member = CreateMethod("Run"); + member.SignatureModel!.ReturnType = "System.Threading.Tasks.Task"; + member.SignatureModel!.TypeParameters = [new TypeParameter { Name = "Task" }]; + type.Members.Add(member); + + var result = _printer.Print(new CSharpTypePrintRequest(type)); + + Assert.DoesNotContain("using System.Threading.Tasks;", result.Source, StringComparison.Ordinal); + Assert.Contains( + "System.Threading.Tasks.Task Run", + result.Units[0].Source, + StringComparison.Ordinal); + } + + [Fact] + public void NestedTypeReferencedAsNamespaceIsNotImportedWhenEnclosingTypeIsReferenced() + { + // `System.Environment.SpecialFolder` is a nested type but arrives as a flat + // dotted string, so the last-dot split derives namespace `System.Environment` + // — which is actually a type. Emitting `using System.Environment;` is illegal. + // When the enclosing type `System.Environment` is itself referenced in the + // unit, its full name shows up as a derived namespace and must be excluded, so + // the nested reference stays fully qualified. + var type = CreateEmptyType("App", "Consumer"); + var enclosing = CreateMethod("GetEnv"); + enclosing.SignatureModel!.ReturnType = "System.Environment"; + var nested = CreateMethod("GetFolder"); + nested.SignatureModel!.ReturnType = "System.Environment.SpecialFolder"; + type.Members.Add(enclosing); + type.Members.Add(nested); + + var result = _printer.Print(new CSharpTypePrintRequest(type)); + + Assert.DoesNotContain("using System.Environment;", result.Source, StringComparison.Ordinal); + Assert.Contains( + "System.Environment.SpecialFolder GetFolder();", + result.Units[0].Source, + StringComparison.Ordinal); + } + + [Fact] + public void ReferenceCollidingWithNamespaceSegmentStaysQualified() + { + var type = CreateEmptyType("Samples.Models", "Worker"); + var member = CreateMethod("Get"); + member.SignatureModel!.ReturnType = "External.Models"; + type.Members.Add(member); + + var result = _printer.Print(new CSharpTypePrintRequest(type)); + + Assert.DoesNotContain("using External;", result.Source, StringComparison.Ordinal); + Assert.Contains( + "public External.Models Get();", + result.Units[0].Source, + StringComparison.Ordinal); + } + + [Fact] + public void ReferenceCollidingWithDerivedNamespaceRootStaysQualified() + { + var type = CreateEmptyType("Samples", "Worker"); + var widget = CreateMethod("GetWidget"); + widget.SignatureModel!.ReturnType = "Alpha.Beta.Widget"; + var alpha = CreateMethod("GetAlpha"); + alpha.SignatureModel!.ReturnType = "Zeta.Alpha"; + type.Members.Add(widget); + type.Members.Add(alpha); + + var result = _printer.Print(new CSharpTypePrintRequest(type)); + + Assert.Equal(["Alpha.Beta"], result.Usings); + Assert.Contains("public Widget GetWidget();", result.Source, StringComparison.Ordinal); + Assert.Contains("public Zeta.Alpha GetAlpha();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void ReferenceCollidingWithCallerNamespaceRootStaysQualified() + { + var type = CreateEmptyType("Samples", "Worker"); + var alpha = CreateMethod("GetAlpha"); + alpha.SignatureModel!.ReturnType = "Zeta.Alpha"; + type.Members.Add(alpha); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions + { + Usings = ["Alpha.Beta"] + }); + + Assert.Equal(["Alpha.Beta"], result.Usings); + Assert.DoesNotContain("using Zeta;", result.Source, StringComparison.Ordinal); + Assert.Contains("public Zeta.Alpha GetAlpha();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void ReferenceCollidingWithEnclosingNamespaceChildStaysQualified() + { + var type = CreateEmptyType("Alpha.Beta", "Worker"); + var thing = CreateMethod("GetThing"); + thing.SignatureModel!.ReturnType = "Alpha.Gamma.Thing"; + var gamma = CreateMethod("GetGamma"); + gamma.SignatureModel!.ReturnType = "Other.Gamma"; + type.Members.Add(thing); + type.Members.Add(gamma); + + var result = _printer.Print(new CSharpTypePrintRequest(type)); + + Assert.Equal(["Alpha.Gamma"], result.Usings); + Assert.Contains("public Thing GetThing();", result.Source, StringComparison.Ordinal); + Assert.Contains("public Other.Gamma GetGamma();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void CallerNamespaceChildShadowsSameNamedReference() + { + var type = CreateEmptyType("Alpha.Beta", "Worker"); + var gamma = CreateMethod("GetGamma"); + gamma.SignatureModel!.ReturnType = "Other.Gamma"; + type.Members.Add(gamma); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ContextualShort, + Usings = ["Alpha.Gamma", "Other"] + }); + + Assert.Equal( + ["Alpha.Gamma", "Other"], + result.Usings.Order(StringComparer.Ordinal)); + Assert.Contains("public Other.Gamma GetGamma();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void UnrelatedNamespaceChildDoesNotShadowSameNamedReference() + { + var type = CreateEmptyType("Alpha.Beta", "Worker"); + var thing = CreateMethod("GetThing"); + thing.SignatureModel!.ReturnType = "Zeta.Delta.Thing"; + var delta = CreateMethod("GetDelta"); + delta.SignatureModel!.ReturnType = "Other.Delta"; + type.Members.Add(thing); + type.Members.Add(delta); + + var result = _printer.Print(new CSharpTypePrintRequest(type)); + + Assert.Equal( + ["Other", "Zeta.Delta"], + result.Usings.Order(StringComparer.Ordinal)); + Assert.Contains("public Thing GetThing();", result.Source, StringComparison.Ordinal); + Assert.Contains("public Delta GetDelta();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void SameNamespaceTypeMatchingRootUsesShortName() + { + var type = CreateEmptyType("Alpha.Beta", "Worker"); + var alpha = CreateMethod("GetAlpha"); + alpha.SignatureModel!.ReturnType = "Alpha.Beta.Alpha"; + type.Members.Add(alpha); + + var result = _printer.Print(new CSharpTypePrintRequest(type)); + + Assert.Contains("public Alpha GetAlpha();", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain("Alpha.Beta.Alpha", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void ContainingNamespaceChildShadowedRootUsesGlobalAlias() + { + var type = CreateEmptyType("Alpha.System", "Worker"); + var uri = CreateMethod("GetUri"); + uri.SignatureModel!.ReturnType = "System.Uri"; + type.Members.Add(uri); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + Assert.Contains( + "public global::System.Uri GetUri();", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void ContainingNamespaceRootDoesNotRequireGlobalAlias() + { + var type = CreateEmptyType("System.Example", "Worker"); + var uri = CreateMethod("GetUri"); + uri.SignatureModel!.ReturnType = "System.Uri"; + type.Members.Add(uri); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + Assert.Contains("public System.Uri GetUri();", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain("global::System.Uri", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void SiblingMemberNamespaceEvidenceTriggersGlobalAlias() + { + var type = CreateEmptyType("Alpha.Beta", "Worker"); + var thing = CreateMethod("GetThing"); + thing.SignatureModel!.ReturnType = "Alpha.System.Thing"; + var uri = CreateMethod("GetUri"); + uri.SignatureModel!.ReturnType = "System.Uri"; + type.Members.Add(thing); + type.Members.Add(uri); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + Assert.Contains("public Alpha.System.Thing GetThing();", result.Source, StringComparison.Ordinal); + Assert.Contains("public global::System.Uri GetUri();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void BaseTypeNamespaceEvidenceTriggersGlobalAlias() + { + var type = CreateEmptyType("Alpha.Beta", "Worker"); + type.BaseType = "Alpha.System.Base"; + var uri = CreateMethod("GetUri"); + uri.SignatureModel!.ReturnType = "System.Uri"; + type.Members.Add(uri); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + Assert.Contains(": Alpha.System.Base", result.Source, StringComparison.Ordinal); + Assert.Contains("public global::System.Uri GetUri();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void EnclosingTypeNameTriggersGlobalAliasInNestedType() + { + var outer = CreateEmptyType("Samples", "Beta"); + var nested = CreateEmptyType("Samples", "Inner"); + var widget = CreateMethod("GetWidget"); + widget.SignatureModel!.ReturnType = "Beta.Models.Widget"; + nested.Members.Add(widget); + + var result = _printer.Print(new CSharpTypePrintRequest( + outer, + nestedTypes: [new CSharpTypePrintRequest(nested)])); + + Assert.Contains( + "public global::Beta.Models.Widget GetWidget();", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void TopLevelSiblingTypeNameTriggersGlobalAlias() + { + var worker = CreateEmptyType("Samples", "Worker"); + var uri = CreateMethod("GetUri"); + uri.SignatureModel!.ReturnType = "System.Uri"; + worker.Members.Add(uri); + var system = CreateEmptyType("Samples", "System"); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(worker), new CSharpTypePrintRequest(system)], + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + Assert.Contains("public global::System.Uri GetUri();", result.Source, StringComparison.Ordinal); + Assert.Contains("public class System", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void AncestorNamespaceTypeNameTriggersGlobalAlias() + { + var system = CreateEmptyType("Alpha", "System"); + var worker = CreateEmptyType("Alpha.Beta", "Worker"); + var uri = CreateMethod("GetUri"); + uri.SignatureModel!.ReturnType = "System.Uri"; + worker.Members.Add(uri); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(system), new CSharpTypePrintRequest(worker)], + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + Assert.Contains("public class System", result.Source, StringComparison.Ordinal); + Assert.Contains("public global::System.Uri GetUri();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void GlobalNamespaceTypeConflictingWithNamespaceRootReportsDiagnostic() + { + var system = CreateEmptyType("", "System"); + var worker = CreateEmptyType("Samples", "Worker"); + var uri = CreateMethod("GetUri"); + uri.SignatureModel!.ReturnType = "System.Uri"; + worker.Members.Add(uri); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(system), new CSharpTypePrintRequest(worker)], + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + Assert.Contains( + "public global::System.Uri GetUri();", + result.Source, + StringComparison.Ordinal); + Assert.Contains( + result.Diagnostics, + diagnostic => diagnostic.Message.Contains("conflicts with global type 'System'", StringComparison.Ordinal)); + } + + [Fact] + public void GlobalNamespaceTypeConflictRecognizesNestedNamespaceRoot() + { + var system = CreateEmptyType("", "System"); + var worker = CreateEmptyType("Samples", "Worker"); + var method = CreateMethod("GetItems"); + method.SignatureModel!.ReturnType = "System.Collections.Generic.List"; + worker.Members.Add(method); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(system), new CSharpTypePrintRequest(worker)], + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + Assert.Contains( + "global::System.Collections.Generic.List", + result.Source, + StringComparison.Ordinal); + Assert.Contains( + result.Diagnostics, + diagnostic => diagnostic.Message.Contains("conflicts with global type 'System'", StringComparison.Ordinal)); + } + + [Fact] + public void DelegateWithGlobalNamespaceRootConflictReportsDiagnostic() + { + var system = CreateEmptyType("", "System"); + var handler = CreateEmptyType("Samples", "Handler"); + handler.Kind = "delegate"; + var invoke = CreateMethod("Invoke"); + invoke.SignatureModel!.ReturnType = "System.Uri"; + handler.Members.Add(invoke); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(system), new CSharpTypePrintRequest(handler)], + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + Assert.Contains( + "public delegate global::System.Uri Handler();", + result.Source, + StringComparison.Ordinal); + Assert.Contains( + result.Diagnostics, + diagnostic => diagnostic.TypeName == "Samples.Handler" + && diagnostic.Message.Contains("conflicts with global type 'System'", StringComparison.Ordinal)); + } + + [Fact] + public void GlobalTypeCanReferenceItsDeclaredNestedType() + { + var host = CreateEmptyType("", "Host"); + var classify = CreateMethod("Classify"); + classify.SignatureModel!.ReturnType = "Host.Kind"; + host.Members.Add(classify); + var kind = CreateEmptyType("", "Kind"); + kind.Kind = "enum"; + + var result = _printer.Print(new CSharpTypePrintRequest( + host, + nestedTypes: [new CSharpTypePrintRequest(kind)])); + + Assert.Contains("public global::Host.Kind Classify();", result.Source, StringComparison.Ordinal); + Assert.Contains("public enum Kind", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void SiblingTypeReferenceRemainsShort() + { + var widget = CreateEmptyType("Alpha", "Widget"); + var panel = CreateEmptyType("Alpha", "Panel"); + var getPanel = CreateMethod("GetPanel"); + getPanel.SignatureModel!.ReturnType = "Alpha.Panel"; + widget.Members.Add(getPanel); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(widget), new CSharpTypePrintRequest(panel)]); + + Assert.Contains("public Panel GetPanel();", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain("public Alpha.Panel GetPanel();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void ContextualShortUsesSafeImportThatIsAlsoADeclaringNamespace() + { + var thing = CreateEmptyType("Alpha", "Thing"); + var worker = CreateEmptyType("Beta", "Worker"); + var getThing = CreateMethod("GetThing"); + getThing.SignatureModel!.ReturnType = "Alpha.Thing"; + worker.Members.Add(getThing); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(thing), new CSharpTypePrintRequest(worker)], + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ContextualShort, + Usings = ["Alpha"] + }); + + Assert.Contains("using Alpha;", result.Source, StringComparison.Ordinal); + Assert.Contains("public Thing GetThing();", result.Source, StringComparison.Ordinal); + } + + [Theory] + [InlineData(CSharpTypeNamePolicy.ShortWithUsings)] + [InlineData(CSharpTypeNamePolicy.ContextualShort)] + public void ImportedDeclaredTypeCannotCaptureQualifiedNamespaceRoot( + CSharpTypeNamePolicy policy) + { + var importedSystem = CreateEmptyType("Imported", "System"); + var importedWidget = CreateEmptyType("Imported", "Widget"); + var consumer = CreateEmptyType("Samples", "Consumer"); + var getWidget = CreateMethod("GetWidget"); + getWidget.SignatureModel!.ReturnType = "Imported.Widget"; + var getUri = CreateMethod("GetUri"); + getUri.SignatureModel!.ReturnType = "System.Uri"; + consumer.Members.Add(getWidget); + consumer.Members.Add(getUri); + + var result = _printer.PrintBatch( + [ + new CSharpTypePrintRequest(importedSystem), + new CSharpTypePrintRequest(importedWidget), + new CSharpTypePrintRequest(consumer) + ], + new CSharpTypePrintOptions + { + TypeNamePolicy = policy, + Usings = policy == CSharpTypeNamePolicy.ContextualShort + ? ["Imported"] + : [] + }); + + Assert.Contains("using Imported;", result.Source, StringComparison.Ordinal); + Assert.Contains("public Widget GetWidget();", result.Source, StringComparison.Ordinal); + Assert.Contains("public global::System.Uri GetUri();", result.Source, StringComparison.Ordinal); + } + + [Theory] + [InlineData("Alpha", "Beta")] + [InlineData("A", "A.B")] + public void ShortWithUsingsImportsOtherDeclaringNamespace( + string consumerNamespace, + string dependencyNamespace) + { + var worker = CreateEmptyType(consumerNamespace, "Worker"); + var getThing = CreateMethod("GetThing"); + getThing.SignatureModel!.ReturnType = $"{dependencyNamespace}.Thing"; + worker.Members.Add(getThing); + var thing = CreateEmptyType(dependencyNamespace, "Thing"); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(worker), new CSharpTypePrintRequest(thing)]); + + Assert.Contains($"using {dependencyNamespace};", result.Source, StringComparison.Ordinal); + Assert.Contains("public Thing GetThing();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void ConfiguredUsingKeepsOtherDeclaredNamespaceReferenceQualified() + { + var exception = CreateEmptyType("Lib", "Exception"); + var consumer = CreateEmptyType("App", "Consumer"); + var getException = CreateMethod("GetException"); + getException.SignatureModel!.ReturnType = "Lib.Exception"; + consumer.Members.Add(getException); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(exception), new CSharpTypePrintRequest(consumer)], + new CSharpTypePrintOptions { Usings = ["System"] }); + + Assert.Equal(["System"], result.Usings); + Assert.DoesNotContain("using Lib;", result.Source, StringComparison.Ordinal); + Assert.Contains( + "public Lib.Exception GetException();", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void DerivedUsingKeepsOtherDeclaredNamespaceReferenceQualified() + { + var marker = CreateEmptyType("Lib", "Marker"); + var consumer = CreateEmptyType("App", "Consumer"); + var getMarker = CreateMethod("GetMarker"); + getMarker.SignatureModel!.ReturnType = "Lib.Marker"; + getMarker.SignatureModel.Parameters = + [ + new ApiParameter { Type = "Other.Value", Name = "value" } + ]; + consumer.Members.Add(getMarker); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(marker), new CSharpTypePrintRequest(consumer)]); + + Assert.Equal(["Other"], result.Usings); + Assert.DoesNotContain("using Lib;", result.Source, StringComparison.Ordinal); + Assert.Contains( + "public Lib.Marker GetMarker(Value value);", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void GlobalTypeReferencePreventsCollidingDeclaredNamespaceImport() + { + var node = CreateEmptyType("Lib", "Node"); + var client = CreateEmptyType("App", "Client"); + var getNode = CreateMethod("GetNode"); + getNode.SignatureModel!.ReturnType = "Lib.Node"; + getNode.SignatureModel.Parameters = + [ + new ApiParameter { Type = "Node", Name = "ambient" } + ]; + client.Members.Add(getNode); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(node), new CSharpTypePrintRequest(client)]); + + Assert.DoesNotContain("using Lib;", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain("Lib", result.Usings); + Assert.Contains( + "public Lib.Node GetNode(Node ambient);", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void DeclaredSimpleNameCollisionKeepsReferencedDeclarationQualified() + { + var user = CreateEmptyType("App", "User"); + user.Members.Add(new ApiMember + { + Name = "Value", + Kind = "field", + ReturnType = "N.Sub.Marker" + }); + + var result = _printer.PrintBatch( + [ + new CSharpTypePrintRequest(user), + new CSharpTypePrintRequest(CreateEmptyType("App", "Marker")), + new CSharpTypePrintRequest(CreateEmptyType("N.Sub", "Marker")) + ]); + + Assert.DoesNotContain("using N.Sub;", result.Source, StringComparison.Ordinal); + Assert.Contains("public N.Sub.Marker Value;", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void GenericDeclaredSimpleNameCollisionKeepsReferenceQualified() + { + var user = CreateEmptyType("App", "User"); + var getMarker = CreateMethod("GetMarker"); + getMarker.SignatureModel!.ReturnType = "N.Sub.Marker"; + user.Members.Add(getMarker); + var genericMarker = CreateEmptyType("N.Sub", "Marker`1"); + genericMarker.TypeParameters = [new TypeParameter { Name = "T" }]; + + var result = _printer.PrintBatch( + [ + new CSharpTypePrintRequest(user), + new CSharpTypePrintRequest(CreateEmptyType("App", "Marker")), + new CSharpTypePrintRequest(genericMarker) + ]); + + Assert.DoesNotContain("using N.Sub;", result.Source, StringComparison.Ordinal); + Assert.Contains( + "public N.Sub.Marker GetMarker();", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void NestedDeclarationDoesNotAuthorizeContainingTypeUsing() + { + var user = CreateEmptyType("App", "User"); + user.Members.Add(new ApiMember + { + Name = "Value", + Kind = "field", + ReturnType = "N.Container.Marker" + }); + var container = CreateEmptyType("N", "Container"); + var marker = CreateEmptyType("N", "Marker"); + + var result = _printer.PrintBatch( + [ + new CSharpTypePrintRequest(user), + new CSharpTypePrintRequest( + container, + nestedTypes: [new CSharpTypePrintRequest(marker)]) + ]); + + Assert.DoesNotContain("using N.Container;", result.Source, StringComparison.Ordinal); + Assert.Contains( + "public N.Container.Marker Value;", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void QualifiedPolicyPlansTypeAndMemberAttributes() + { + var system = CreateEmptyType("Samples", "System"); + var worker = CreateEmptyType("Samples", "Worker"); + worker.Attributes = ["System.ObsoleteAttribute"]; + var run = CreateMethod("Run"); + run.Attributes = ["System.ObsoleteAttribute"]; + worker.Members.Add(run); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(system), new CSharpTypePrintRequest(worker)], + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified, + IncludeCustomAttributes = true + }); + + Assert.Equal( + 2, + result.Source.Split("[global::System.ObsoleteAttribute]", StringSplitOptions.None).Length - 1); + Assert.DoesNotContain("[System.ObsoleteAttribute]", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void QualifiedPolicyPlansTypeBearingAttributeArgumentsAndReturnAttributes() + { + var type = CreateEmptyType("Samples", "External`1"); + type.Attributes = + [ + "Other.Marker(typeof(External.Value), (External.Kind)1)", + "Other.KeywordMarker(typeof(Alpha.@event), (Alpha.@event)1)" + ]; + type.TypeParameters = [new TypeParameter { Name = "Alpha" }]; + var method = CreateMethod("Get"); + method.Kind = "property"; + method.SignatureModel!.ReturnAttributes = ["External.ReturnMarker"]; + method.SignatureModel.Accessors = + [ + new ApiAccessor + { + Kind = "get", + ReturnAttributes = ["External.AccessorMarker"] + } + ]; + type.Members.Add(method); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified, + IncludeCustomAttributes = true + }); + + Assert.Contains( + "[Other.Marker(typeof(global::External.Value), (global::External.Kind)1)]", + result.Source, + StringComparison.Ordinal); + Assert.Contains( + "[Other.KeywordMarker(typeof(global::Alpha.@event), (global::Alpha.@event)1)]", + result.Source, + StringComparison.Ordinal); + Assert.Contains( + "[return: global::External.ReturnMarker]", + result.Source, + StringComparison.Ordinal); + Assert.Contains( + "[return: global::External.AccessorMarker]", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void ParenthesizedAttributeValueFollowedByBinaryOperatorIsNotACast() + { + var constants = CreateEmptyType("App", "Constants"); + var consumer = CreateEmptyType("App", "Consumer"); + consumer.Attributes = ["App.Probe((Constants.Value) + 1)"]; + + var result = _printer.PrintBatch( + [ + new CSharpTypePrintRequest(constants), + new CSharpTypePrintRequest(consumer) + ], + new CSharpTypePrintOptions { IncludeCustomAttributes = true }); + + Assert.Contains( + "[App.Probe((Constants.Value) + 1)]", + result.Source, + StringComparison.Ordinal); + Assert.DoesNotContain("global::Constants.Value", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void AttributeValueCanUseTypeDeclaredInAncestorNamespace() + { + var foo = CreateEmptyType("A", "Foo"); + var options = CreateEmptyType("A", "Options"); + options.Kind = "enum"; + options.Members = + [ + new ApiMember + { + Name = "Fast", + Kind = "field", + ReturnType = "A.Foo.Options" + } + ]; + var consumer = CreateEmptyType("A.B", "Consumer"); + consumer.Attributes = ["Marker(Foo.Options.Fast)"]; + + var result = _printer.PrintBatch( + [ + new CSharpTypePrintRequest( + foo, + nestedTypes: [new CSharpTypePrintRequest(options)]), + new CSharpTypePrintRequest(consumer) + ], + new CSharpTypePrintOptions { IncludeCustomAttributes = true }); + + Assert.Contains( + "[Marker(Foo.Options.Fast)]", + result.Source, + StringComparison.Ordinal); + Assert.DoesNotContain("global::Foo.Options.Fast", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void QualifiedPolicyPlansDelegateReferences() + { + var type = new ApiType + { + Namespace = "Alpha.System", + Name = "Callback", + Kind = "delegate", + Members = + [ + new ApiMember + { + Name = "Invoke", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "System.Uri", + MemberName = "Invoke" + } + } + ] + }; + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + Assert.Contains( + "public delegate global::System.Uri Callback();", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void PrimaryConstructorShadowingProducesDiagnostic() + { + var type = CreateEmptyType("Samples", "Worker`1"); + type.TypeParameters = [new TypeParameter { Name = "Task" }]; + + var result = _printer.Print(new CSharpTypePrintRequest( + type, + primaryConstructorParameters: + [ + new ApiParameter + { + Type = "System.Threading.Tasks.Task", + Name = "task" + } + ])); + + Assert.Contains( + "public class Worker(System.Threading.Tasks.Task task)", + result.Source, + StringComparison.Ordinal); + var diagnostic = Assert.Single(result.Diagnostics); + Assert.Contains("Task", diagnostic.Message, StringComparison.Ordinal); + Assert.Contains("shadowed", diagnostic.Message, StringComparison.Ordinal); + } + + [Fact] + public void LexicallyShadowedQualifiedRootUsesGlobalAlias() + { + var type = CreateEmptyType("Samples", "Worker`2"); + type.TypeParameters = + [ + new TypeParameter { Name = "Alpha" }, + new TypeParameter { Name = "Thing" } + ]; + var thing = CreateMethod("GetThing"); + thing.SignatureModel!.ReturnType = "Alpha.Beta.Thing"; + type.Members.Add(thing); + + var result = _printer.Print(new CSharpTypePrintRequest(type)); + + Assert.DoesNotContain("using Alpha.Beta;", result.Source, StringComparison.Ordinal); + Assert.Contains( + "public global::Alpha.Beta.Thing GetThing();", + result.Source, + StringComparison.Ordinal); + } + + [Theory] + [InlineData(CSharpTypeNamePolicy.ShortWithUsings)] + [InlineData(CSharpTypeNamePolicy.ContextualShort)] + public void UsingsSuppressedKeepsReferencesQualified(CSharpTypeNamePolicy policy) + { + // With IncludeUsings=false the composed Source omits using directives, so + // shortening a cross-namespace reference would leave it unresolvable. + var type = CreateEmptyType("Samples", "Worker"); + var member = CreateMethod("Run"); + member.SignatureModel!.ReturnType = "System.Threading.Tasks.Task"; + type.Members.Add(member); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions + { + TypeNamePolicy = policy, + Usings = ["System.Threading.Tasks"], + IncludeUsings = false + }); + + Assert.DoesNotContain("using System.Threading.Tasks;", result.Source, StringComparison.Ordinal); + Assert.Empty(result.Usings); + Assert.Contains( + "public System.Threading.Tasks.Task Run();", + result.Units[0].Source, + StringComparison.Ordinal); + } + + [Theory] + [InlineData(CSharpTypeNamePolicy.Qualified, "System.Threading.Tasks.Task", false)] + [InlineData(CSharpTypeNamePolicy.ShortWithUsings, "Task", true)] + [InlineData(CSharpTypeNamePolicy.ContextualShort, "Task", true)] + public void TypeNamePolicyAppliesToCompleteMemberWithBodyComposition( + CSharpTypeNamePolicy policy, + string expectedReturnType, + bool expectsImport) + { + var type = CreateEmptyType("Samples", "Worker"); + var member = CreateMethod("Run"); + member.SignatureModel!.ReturnType = "System.Threading.Tasks.Task"; + type.Members.Add(member); + + var result = _printer.Print( + new CSharpTypePrintRequest( + type, + memberPolicyOverrides: + [ + new CSharpMemberPolicy( + member, + CSharpBodyPolicy.Full, + new CSharpBlockBody("return default!;")) + ]), + new CSharpTypePrintOptions + { + TypeNamePolicy = policy, + Usings = policy == CSharpTypeNamePolicy.ContextualShort + ? ["System.Threading.Tasks"] + : [] + }); + + Assert.Contains($"public {expectedReturnType} Run()", result.Source, StringComparison.Ordinal); + Assert.Contains("return default!;", result.Source, StringComparison.Ordinal); + Assert.Equal(expectsImport, result.Usings.Contains("System.Threading.Tasks")); + } + + [Fact] + public void ResultEqualityIncludesUsingSet() + { + var request = new CSharpTypePrintRequest(CreateEmptyType("Samples", "Worker")); + var alpha = _printer.Print( + request, + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ContextualShort, + Usings = ["Alpha"] + }); + var beta = _printer.Print( + request, + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.ContextualShort, + Usings = ["Beta"] + }); + + Assert.Equal(alpha.Units, beta.Units); + Assert.Equal(alpha.Diagnostics, beta.Diagnostics); + Assert.NotEqual(alpha, beta); + } + + [Fact] + public void ResultEqualityNormalizesUsingSetComparers() + { + var insensitive = new CSharpTypePrintResult( + [], + ImmutableSortedSet.Create(StringComparer.OrdinalIgnoreCase, "Alpha"), + [], + () => ""); + var ordinal = new CSharpTypePrintResult( + [], + ImmutableSortedSet.Create(StringComparer.Ordinal, "alpha"), + [], + () => ""); + + Assert.False(insensitive.Equals(ordinal)); + Assert.False(ordinal.Equals(insensitive)); + Assert.Same(StringComparer.Ordinal, insensitive.Usings.KeyComparer); + } + + [Theory] + [InlineData(CSharpBodyPolicy.Full)] + [InlineData(CSharpBodyPolicy.Stub)] + public void AbstractMembersRejectImplementationPolicies(CSharpBodyPolicy bodyPolicy) + { + var member = CreateMethod("Run"); + member.IsAbstract = true; + var type = CreateEmptyType("Samples", "Widget"); + type.IsAbstract = true; + type.Members.Add(member); + var body = bodyPolicy == CSharpBodyPolicy.Full + ? new CSharpBlockBody("return;") + : null; + + var exception = Assert.Throws(() => _printer.Print( + new CSharpTypePrintRequest( + type, + memberPolicyOverrides: [new CSharpMemberPolicy(member, bodyPolicy, body)]))); + + Assert.Contains("must use skeleton body policy", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void StubPropertyRequiresExplicitAccessorBodyShape() + { + var property = new ApiMember + { + Name = "Value", + Kind = "property", + SignatureModel = new ApiSignature + { + ReturnType = "int", + MemberName = "Value", + Accessors = [new ApiAccessor { Kind = "get" }] + } + }; + var type = CreateEmptyType("Samples", "Widget"); + type.Members.Add(property); + + var exception = Assert.Throws( + () => _printer.Print(new CSharpTypePrintRequest(type, CSharpBodyPolicy.Stub))); + + Assert.Contains("requires an explicit accessor body shape", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void PrimaryConstructorTypeRequiresExplicitConstructorInitializer() + { + var constructor = new ApiMember + { + Name = ".ctor", + Kind = "constructor", + SignatureModel = new ApiSignature() + }; + var type = CreateEmptyType("Samples", "Widget"); + type.Members.Add(constructor); + + var exception = Assert.Throws(() => _printer.Print( + new CSharpTypePrintRequest( + type, + memberPolicyOverrides: [new CSharpMemberPolicy(constructor, CSharpBodyPolicy.Stub)], + primaryConstructorParameters: [new ApiParameter { Type = "int", Name = "value" }]))); + + Assert.Contains("requires an explicit constructor initializer", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void PropertyBodySpecifiesIndependentAccessorShapes() + { + var property = new ApiMember + { + Name = "Value", + Kind = "property", + SignatureModel = new ApiSignature + { + ReturnType = "int", + MemberName = "Value", + Accessors = + [ + new ApiAccessor { Kind = "get", ReturnAttributes = ["Marker"] }, + new ApiAccessor { Kind = "set" } + ] + } + }; + var type = CreateEmptyType("Samples", "Widget"); + type.Members.Add(property); + var request = new CSharpTypePrintRequest( + type, + memberPolicyOverrides: + [ + new CSharpMemberPolicy( + property, + CSharpBodyPolicy.Full, + new CSharpPropertyBody( + CSharpAccessorBody.Block("return 42;"), + CSharpAccessorBody.Throw)) + ]); + + var result = _printer.Print(request); + + Assert.Contains( + """ + public int Value + { + [return: Marker] get + { + return 42; + } + set + { + throw null; + } + } + """, + result.Units[0].Source, StringComparison.Ordinal); } - [Theory] - [InlineData(CSharpTypeNamePolicy.Qualified, "System.Threading.Tasks.Task", false)] - [InlineData(CSharpTypeNamePolicy.ShortWithUsings, "Task", true)] - [InlineData(CSharpTypeNamePolicy.ContextualShort, "Task", true)] - public void TypeNamePolicyAppliesToCompleteMemberWithBodyComposition( - CSharpTypeNamePolicy policy, - string expectedReturnType, - bool expectsImport) + [Fact] + public void FullPropertyAndEventBodiesPlanAccessorReturnAttributes() + { + var property = new ApiMember + { + Name = "Value", + Kind = "property", + SignatureModel = new ApiSignature + { + ReturnType = "int", + MemberName = "Value", + Accessors = + [ + new ApiAccessor + { + Kind = "get", + ReturnAttributes = ["External.GetterMarker"] + } + ] + } + }; + var @event = new ApiMember + { + Name = "Changed", + Kind = "event", + SignatureModel = new ApiSignature + { + ReturnType = "System.EventHandler", + MemberName = "Changed", + Accessors = + [ + new ApiAccessor + { + Kind = "add", + ReturnAttributes = ["External.AddMarker"] + }, + new ApiAccessor + { + Kind = "remove", + ReturnAttributes = ["External.RemoveMarker"] + } + ] + } + }; + var type = CreateEmptyType("Samples", "External`1"); + type.TypeParameters = [new TypeParameter { Name = "void" }]; + type.Members.Add(property); + type.Members.Add(@event); + + var result = _printer.Print( + new CSharpTypePrintRequest( + type, + memberPolicyOverrides: + [ + new CSharpMemberPolicy( + property, + CSharpBodyPolicy.Full, + new CSharpPropertyBody(CSharpAccessorBody.Block("return 42;"), null)), + new CSharpMemberPolicy( + @event, + CSharpBodyPolicy.Full, + new CSharpEventBody( + CSharpAccessorBody.Block("_changed += value;"), + CSharpAccessorBody.Block("_changed -= value;"))) + ]), + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + Assert.Contains( + "[return: global::External.GetterMarker] get", + result.Source, + StringComparison.Ordinal); + Assert.Contains( + "[return: global::External.AddMarker] add", + result.Source, + StringComparison.Ordinal); + Assert.Contains( + "[return: global::External.RemoveMarker] remove", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void ExplicitInterfacePropertyPreservesQualifiedNameAndOmitsAccessibility() + { + var property = new ApiMember + { + Name = "Samples.IValue.Value", + Kind = "explicit-interface-implementation", + SignatureModel = new ApiSignature + { + ReturnType = "int", + MemberName = "Samples.IValue.Value", + Accessors = [new ApiAccessor { Kind = "get" }] + } + }; + var type = CreateEmptyType("Samples", "Widget"); + type.Interfaces.Add("Samples.IValue"); + type.Members.Add(property); + var request = new CSharpTypePrintRequest( + type, + memberPolicyOverrides: + [ + new CSharpMemberPolicy( + property, + CSharpBodyPolicy.Full, + new CSharpPropertyBody( + CSharpAccessorBody.Block("return 42;"), + null)) + ]); + + var result = _printer.Print(request); + + Assert.Contains( + """ + int Samples.IValue.Value + { + get + { + return 42; + } + } + """, + result.Units[0].Source, + StringComparison.Ordinal); + Assert.DoesNotContain( + "public int Samples.IValue.Value", + result.Units[0].Source, + StringComparison.Ordinal); + } + + [Fact] + public void ExplicitInterfaceIndexerPreservesQualifierAndOmitsAccessibility() + { + var indexer = new ApiMember + { + Name = "Samples.IValues.Item", + Kind = "explicit-interface-implementation", + SignatureModel = new ApiSignature + { + ReturnType = "int", + MemberName = "this[]", + Parameters = [new ApiParameter { Type = "int", Name = "index" }], + Accessors = [new ApiAccessor { Kind = "get" }] + } + }; + var type = CreateEmptyType("Samples", "Widget"); + type.Interfaces.Add("Samples.IValues"); + type.Members.Add(indexer); + var request = new CSharpTypePrintRequest( + type, + memberPolicyOverrides: + [ + new CSharpMemberPolicy( + indexer, + CSharpBodyPolicy.Full, + new CSharpPropertyBody( + CSharpAccessorBody.Block("return index;"), + null)) + ]); + + var result = _printer.Print(request); + + Assert.Contains( + """ + int Samples.IValues.this[int index] + { + get + { + return index; + } + } + """, + result.Units[0].Source, + StringComparison.Ordinal); + Assert.DoesNotContain( + "public int Samples.IValues.this", + result.Units[0].Source, + StringComparison.Ordinal); + } + + [Fact] + public void ExplicitInterfaceQualifierRespectsLexicalShadowing() { - var type = CreateEmptyType("Samples", "Worker"); - var member = CreateMethod("Run"); - member.SignatureModel!.ReturnType = "System.Threading.Tasks.Task"; - type.Members.Add(member); + var property = new ApiMember + { + Name = "Samples.IValue.Value", + Kind = "explicit-interface-implementation", + SignatureModel = new ApiSignature + { + ReturnType = "int", + MemberName = "Samples.IValue.Value", + Accessors = [new ApiAccessor { Kind = "get" }] + } + }; + var type = CreateEmptyType("Samples", "Widget`1"); + type.MetadataName = "Widget`1"; + type.TypeParameters = [new TypeParameter { Name = "Samples" }]; + type.Interfaces.Add("Samples.IValue"); + type.Members.Add(property); var result = _printer.Print( - new CSharpTypePrintRequest( - type, - memberPolicyOverrides: - [ - new CSharpMemberPolicy( - member, - CSharpBodyPolicy.Full, - new CSharpBlockBody("return default!;")) - ]), + new CSharpTypePrintRequest(type), new CSharpTypePrintOptions { - TypeNamePolicy = policy, - Usings = policy == CSharpTypeNamePolicy.ContextualShort - ? ["System.Threading.Tasks"] - : [] + TypeNamePolicy = CSharpTypeNamePolicy.Qualified }); - Assert.Contains($"public {expectedReturnType} Run()", result.Source, StringComparison.Ordinal); - Assert.Contains("return default!;", result.Source, StringComparison.Ordinal); - Assert.Equal(expectsImport, result.Usings.Contains("System.Threading.Tasks")); + Assert.Contains( + "int global::Samples.IValue.Value", + result.Source, + StringComparison.Ordinal); } [Fact] - public void ResultEqualityIncludesUsingSet() + public void SiblingMemberTypeReferenceContributesRootShadowing() { - var request = new CSharpTypePrintRequest(CreateEmptyType("Samples", "Worker")); - var alpha = _printer.Print( - request, + var type = CreateEmptyType("Contoso.Data", "Store"); + var getJson = CreateMethod("GetJson"); + getJson.SignatureModel!.ReturnType = "Contoso.Data.Json"; + var getNode = CreateMethod("GetNode"); + getNode.SignatureModel!.ReturnType = "Json.Node"; + type.Members.Add(getJson); + type.Members.Add(getNode); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), new CSharpTypePrintOptions { - TypeNamePolicy = CSharpTypeNamePolicy.ContextualShort, - Usings = ["Alpha"] + TypeNamePolicy = CSharpTypeNamePolicy.Qualified, + IncludeUsings = false }); - var beta = _printer.Print( - request, + + Assert.Contains( + "public global::Json.Node GetNode();", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void AncestorNamespaceTypeReferenceContributesRootShadowing() + { + var type = CreateEmptyType("Contoso.Data.Serialization", "Store"); + var convert = CreateMethod("Convert"); + convert.SignatureModel!.ReturnType = "Json.Node"; + convert.SignatureModel.Parameters.Add(new ApiParameter + { + Type = "Contoso.Data.Json", + Name = "value" + }); + type.Members.Add(convert); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), new CSharpTypePrintOptions { - TypeNamePolicy = CSharpTypeNamePolicy.ContextualShort, - Usings = ["Beta"] + TypeNamePolicy = CSharpTypeNamePolicy.Qualified, + IncludeUsings = false }); - Assert.Equal(alpha.Units, beta.Units); - Assert.Equal(alpha.Diagnostics, beta.Diagnostics); - Assert.NotEqual(alpha, beta); + Assert.Contains( + "public global::Json.Node Convert(Contoso.Data.Json value);", + result.Source, + StringComparison.Ordinal); } [Fact] - public void ResultEqualityNormalizesUsingSetComparers() + public void GenericInterfaceReferencesArePlannedByTypeComponent() { - var insensitive = new CSharpTypePrintResult( - [], - ImmutableSortedSet.Create(StringComparer.OrdinalIgnoreCase, "Alpha"), - [], - () => ""); - var ordinal = new CSharpTypePrintResult( - [], - ImmutableSortedSet.Create(StringComparer.Ordinal, "alpha"), - [], - () => ""); + var type = CreateEmptyType("App", "Host"); + type.Interfaces.Add("A.IFoo"); - Assert.False(insensitive.Equals(ordinal)); - Assert.False(ordinal.Equals(insensitive)); - Assert.Same(StringComparer.Ordinal, insensitive.Usings.KeyComparer); + var result = _printer.Print(new CSharpTypePrintRequest(type)); + + Assert.Contains("using A;", result.Source, StringComparison.Ordinal); + Assert.Contains("using B;", result.Source, StringComparison.Ordinal); + Assert.Contains("public class Host : IFoo", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain("using A.IFoo.Middle.Inner"); - var exception = Assert.Throws(() => _printer.Print( + var result = _printer.Print(new CSharpTypePrintRequest(type)); + + Assert.Contains("using N;", result.Source, StringComparison.Ordinal); + Assert.Contains( + "public class Host : Outer.Middle.Inner", + result.Source, + StringComparison.Ordinal); + Assert.DoesNotContain("using Middle;", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void PrimaryConstructorAttributeEvidenceContributesToMemberPlanning() + { + var type = CreateEmptyType("Contoso.Data", "Store"); + var method = CreateMethod("GetNode"); + method.SignatureModel!.ReturnType = "Json.Node"; + type.Members.Add(method); + var parameter = new ApiParameter + { + Type = "int", + Name = "value", + Attributes = ["Marker(typeof(Contoso.Data.Json))"] + }; + + var result = _printer.Print( new CSharpTypePrintRequest( type, - memberPolicyOverrides: [new CSharpMemberPolicy(member, bodyPolicy, body)]))); + primaryConstructorParameters: [parameter]), + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); - Assert.Contains("must use skeleton body policy", exception.Message, StringComparison.Ordinal); + Assert.Contains( + "public global::Json.Node GetNode();", + result.Source, + StringComparison.Ordinal); } [Fact] - public void StubPropertyRequiresExplicitAccessorBodyShape() + public void PrimaryConstructorAttributeNameRemainsQualified() { - var property = new ApiMember + var type = CreateEmptyType("Samples", "Host"); + var method = CreateMethod("Get"); + method.SignatureModel!.ReturnType = "B.Marker"; + type.Members.Add(method); + var parameter = new ApiParameter { - Name = "Value", - Kind = "property", - SignatureModel = new ApiSignature - { - ReturnType = "int", - MemberName = "Value", - Accessors = [new ApiAccessor { Kind = "get" }] - } + Type = "int", + Name = "value", + Attributes = ["External.Marker"] }; - var type = CreateEmptyType("Samples", "Widget"); - type.Members.Add(property); - var exception = Assert.Throws( - () => _printer.Print(new CSharpTypePrintRequest(type, CSharpBodyPolicy.Stub))); + var result = _printer.Print(new CSharpTypePrintRequest( + type, + primaryConstructorParameters: [parameter])); - Assert.Contains("requires an explicit accessor body shape", exception.Message, StringComparison.Ordinal); + Assert.Contains( + "public class Host([External.Marker] int value)", + result.Source, + StringComparison.Ordinal); + Assert.DoesNotContain("using External;", result.Source, StringComparison.Ordinal); + Assert.Contains("using B;", result.Source, StringComparison.Ordinal); + Assert.Contains("public Marker Get();", result.Source, StringComparison.Ordinal); + Assert.Empty(result.Diagnostics); } [Fact] - public void PrimaryConstructorTypeRequiresExplicitConstructorInitializer() + public void UnitWideAttributeSuffixCollisionPreventsUnsafeImports() { - var constructor = new ApiMember + var host = CreateEmptyType("App", "Host"); + var method = CreateMethod("GetWidget"); + method.SignatureModel!.ReturnType = "External.Widget"; + host.Members.Add(method); + var parameter = new ApiParameter { - Name = ".ctor", - Kind = "constructor", - SignatureModel = new ApiSignature() + Type = "int", + Name = "value", + Attributes = ["External.Marker"] }; - var type = CreateEmptyType("Samples", "Widget"); - type.Members.Add(constructor); - var exception = Assert.Throws(() => _printer.Print( - new CSharpTypePrintRequest( - type, - memberPolicyOverrides: [new CSharpMemberPolicy(constructor, CSharpBodyPolicy.Stub)], - primaryConstructorParameters: [new ApiParameter { Type = "int", Name = "value" }]))); + var invoke = CreateMethod("Invoke"); + invoke.SignatureModel!.ReturnType = "Collision.MarkerAttribute"; + var handler = CreateEmptyType("App", "Handler"); + handler.Kind = "delegate"; + handler.Members.Add(invoke); - Assert.Contains("requires an explicit constructor initializer", exception.Message, StringComparison.Ordinal); + var result = _printer.PrintBatch( + [ + new CSharpTypePrintRequest( + host, + primaryConstructorParameters: [parameter]), + new CSharpTypePrintRequest(handler) + ]); + + Assert.DoesNotContain("using External;", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain("using Collision;", result.Source, StringComparison.Ordinal); + Assert.Contains("[External.Marker] int value", result.Source, StringComparison.Ordinal); + Assert.Contains( + "delegate Collision.MarkerAttribute Handler();", + result.Source, + StringComparison.Ordinal); } [Fact] - public void PropertyBodySpecifiesIndependentAccessorShapes() + public void SameNamespaceAttributeSuffixCollisionPreventsUnsafeImport() { - var property = new ApiMember + var host = CreateEmptyType("App", "Host"); + var method = CreateMethod("GetWidget"); + method.SignatureModel!.ReturnType = "External.Widget"; + host.Members.Add(method); + var parameter = new ApiParameter { - Name = "Value", - Kind = "property", - SignatureModel = new ApiSignature - { - ReturnType = "int", - MemberName = "Value", - Accessors = - [ - new ApiAccessor { Kind = "get", ReturnAttributes = ["Marker"] }, - new ApiAccessor { Kind = "set" } - ] - } + Type = "int", + Name = "value", + Attributes = ["App.Marker"] }; - var type = CreateEmptyType("Samples", "Widget"); - type.Members.Add(property); - var request = new CSharpTypePrintRequest( - type, - memberPolicyOverrides: + + var invoke = CreateMethod("Invoke"); + invoke.SignatureModel!.ReturnType = "Collision.MarkerAttribute"; + var handler = CreateEmptyType("App", "Handler"); + handler.Kind = "delegate"; + handler.Members.Add(invoke); + + var result = _printer.PrintBatch( [ - new CSharpMemberPolicy( - property, - CSharpBodyPolicy.Full, - new CSharpPropertyBody( - CSharpAccessorBody.Block("return 42;"), - CSharpAccessorBody.Throw)) + new CSharpTypePrintRequest( + host, + primaryConstructorParameters: [parameter]), + new CSharpTypePrintRequest(handler) ]); - var result = _printer.Print(request); - + Assert.Contains("using External;", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain("using Collision;", result.Source, StringComparison.Ordinal); + Assert.Contains("[Marker] int value", result.Source, StringComparison.Ordinal); Assert.Contains( - """ - public int Value - { - [return: Marker] get - { - return 42; - } - set - { - throw null; - } - } - """, - result.Units[0].Source, + "delegate Collision.MarkerAttribute Handler();", + result.Source, StringComparison.Ordinal); } [Fact] - public void ExplicitInterfacePropertyPreservesQualifiedNameAndOmitsAccessibility() + public void UnitWidePrimaryAttributeSuffixCollisionIsSymmetric() + { + var host = CreateEmptyType("App", "Host"); + var getLeft = CreateMethod("GetLeft"); + getLeft.SignatureModel!.ReturnType = "A.Left"; + host.Members.Add(getLeft); + var hostParameter = new ApiParameter + { + Type = "int", + Name = "value", + Attributes = ["A.Marker"] + }; + + var worker = CreateEmptyType("App", "Worker"); + var getRight = CreateMethod("GetRight"); + getRight.SignatureModel!.ReturnType = "B.Right"; + worker.Members.Add(getRight); + var workerParameter = new ApiParameter + { + Type = "int", + Name = "value", + Attributes = ["B.MarkerAttribute"] + }; + + var result = _printer.PrintBatch( + [ + new CSharpTypePrintRequest( + host, + primaryConstructorParameters: [hostParameter]), + new CSharpTypePrintRequest( + worker, + primaryConstructorParameters: [workerParameter]) + ]); + + Assert.DoesNotContain("using A;", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain("using B;", result.Source, StringComparison.Ordinal); + Assert.Contains("[A.Marker] int value", result.Source, StringComparison.Ordinal); + Assert.Contains("[B.MarkerAttribute] int value", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void ExplicitInterfaceDualProvenanceRemainsQualified() { var property = new ApiMember { - Name = "Samples.IValue.Value", + Name = "Contracts.IValue.Value", Kind = "explicit-interface-implementation", SignatureModel = new ApiSignature { - ReturnType = "int", - MemberName = "Samples.IValue.Value", + ReturnType = "Contracts.IValue", + MemberName = "Contracts.IValue.Value", Accessors = [new ApiAccessor { Kind = "get" }] } }; - var type = CreateEmptyType("Samples", "Widget"); - type.Interfaces.Add("Samples.IValue"); + var type = CreateEmptyType("App", "Widget"); + type.Interfaces.Add("Contracts.IValue"); type.Members.Add(property); - var request = new CSharpTypePrintRequest( - type, - memberPolicyOverrides: - [ - new CSharpMemberPolicy( - property, - CSharpBodyPolicy.Full, - new CSharpPropertyBody( - CSharpAccessorBody.Block("return 42;"), - null)) - ]); - var result = _printer.Print(request); + var result = _printer.Print(new CSharpTypePrintRequest(type)); Assert.Contains( - """ - int Samples.IValue.Value - { - get - { - return 42; - } - } - """, - result.Units[0].Source, + "Contracts.IValue Contracts.IValue.Value", + result.Source, StringComparison.Ordinal); Assert.DoesNotContain( - "public int Samples.IValue.Value", - result.Units[0].Source, + "IValue IValue.Value", + result.Source, StringComparison.Ordinal); } [Fact] - public void ExplicitInterfaceIndexerPreservesQualifierAndOmitsAccessibility() + public void ExplicitInterfaceCollisionKeepsBothReferencesQualified() { - var indexer = new ApiMember + var property = new ApiMember { - Name = "Samples.IValues.Item", + Name = "Contracts.IValue.Value", Kind = "explicit-interface-implementation", SignatureModel = new ApiSignature { - ReturnType = "int", - MemberName = "this[]", - Parameters = [new ApiParameter { Type = "int", Name = "index" }], + ReturnType = "Other.IValue", + MemberName = "Contracts.IValue.Value", Accessors = [new ApiAccessor { Kind = "get" }] } }; - var type = CreateEmptyType("Samples", "Widget"); - type.Interfaces.Add("Samples.IValues"); - type.Members.Add(indexer); - var request = new CSharpTypePrintRequest( - type, - memberPolicyOverrides: - [ - new CSharpMemberPolicy( - indexer, - CSharpBodyPolicy.Full, - new CSharpPropertyBody( - CSharpAccessorBody.Block("return index;"), - null)) - ]); + var sibling = CreateMethod("GetThing"); + sibling.SignatureModel!.ReturnType = "Contracts.Thing"; + var type = CreateEmptyType("App", "Widget"); + type.Interfaces.Add("Contracts.IValue"); + type.Members.Add(property); + type.Members.Add(sibling); - var result = _printer.Print(request); + var result = _printer.Print(new CSharpTypePrintRequest(type)); + Assert.DoesNotContain("using Contracts;", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain("using Other;", result.Source, StringComparison.Ordinal); Assert.Contains( - """ - int Samples.IValues.this[int index] - { - get - { - return index; - } - } - """, - result.Units[0].Source, + "Other.IValue Contracts.IValue.Value", + result.Source, StringComparison.Ordinal); - Assert.DoesNotContain( - "public int Samples.IValues.this", - result.Units[0].Source, + } + + [Fact] + public void KeptQualifiedReferenceIsNotRewrittenByShorterPrefix() + { + var type = CreateEmptyType("App", "Widget"); + var method = CreateMethod("Get"); + method.SignatureModel!.ReturnType = "A.B"; + method.SignatureModel.Parameters.Add(new ApiParameter + { + Type = "A.B.C", + Name = "value" + }); + type.Members.Add(method); + + var result = _printer.Print(new CSharpTypePrintRequest(type)); + + Assert.Contains( + "public B Get(A.B.C value);", + result.Source, + StringComparison.Ordinal); + Assert.DoesNotContain("public B Get(B.C value);", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void HiddenCustomAttributesStillContributeBindingEvidence() + { + var type = CreateEmptyType("App", "Widget"); + type.Attributes = ["App.Foo.MarkerAttribute"]; + var method = CreateMethod("Get"); + method.SignatureModel!.ReturnType = "Foo.Bar.Baz"; + type.Members.Add(method); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions + { + IncludeCustomAttributes = false, + IncludeUsings = false + }); + + Assert.DoesNotContain("MarkerAttribute", result.Source, StringComparison.Ordinal); + Assert.Contains( + "public global::Foo.Bar.Baz Get();", + result.Source, StringComparison.Ordinal); } + [Fact] + public void HiddenCustomAttributeDoesNotReportAnEmittedRootConflict() + { + var root = CreateEmptyType("", "Foo"); + var type = CreateEmptyType("App", "Widget"); + type.Attributes = ["Foo.Bar.Marker"]; + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(root), new CSharpTypePrintRequest(type)], + new CSharpTypePrintOptions { IncludeCustomAttributes = false }); + + Assert.DoesNotContain("Marker", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain( + result.Diagnostics, + diagnostic => diagnostic.Message.Contains( + "Type name 'Foo.Bar.Marker' conflicts with global type 'Foo'", + StringComparison.Ordinal)); + } + + [Fact] + public void ImportedHiddenAttributeNamespacePreventsConflictingSignatureShortening() + { + var type = CreateEmptyType("App", "Widget"); + var getFoo = CreateMethod("GetFoo"); + getFoo.SignatureModel!.ReturnType = "A.Foo"; + getFoo.Attributes = ["B.Foo"]; + var getOther = CreateMethod("GetOther"); + getOther.SignatureModel!.ReturnType = "B.Other"; + type.Members.Add(getFoo); + type.Members.Add(getOther); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions { IncludeCustomAttributes = false }); + + Assert.DoesNotContain("using A;", result.Source, StringComparison.Ordinal); + Assert.Contains("using B;", result.Source, StringComparison.Ordinal); + Assert.Contains("public A.Foo GetFoo();", result.Source, StringComparison.Ordinal); + Assert.Contains("public Other GetOther();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void AttributeTypeIsNotImportedAsNestedTypeNamespace() + { + var type = CreateEmptyType("App", "Widget"); + type.Attributes = ["N.Outer"]; + var method = CreateMethod("Get"); + method.SignatureModel!.ReturnType = "N.Outer.Inner"; + type.Members.Add(method); + + var result = _printer.Print( + new CSharpTypePrintRequest(type), + new CSharpTypePrintOptions { IncludeCustomAttributes = false }); + + Assert.DoesNotContain("using N.Outer;", result.Source, StringComparison.Ordinal); + Assert.Contains("public N.Outer.Inner Get();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void GlobalTypeConflictingWithNamespaceDeclarationReportsDiagnostic() + { + var root = CreateEmptyType("", "Foo"); + var namespaced = CreateEmptyType("Foo.Bar", "Worker"); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(root), new CSharpTypePrintRequest(namespaced)]); + + Assert.Contains( + result.Diagnostics, + diagnostic => diagnostic.Message.Contains( + "Namespace root 'Foo' conflicts with global type 'Foo'", + StringComparison.Ordinal)); + } + + [Fact] + public void GlobalTypeConflictingWithUsingReportsDiagnostic() + { + var system = CreateEmptyType("", "System"); + + var result = _printer.Print( + new CSharpTypePrintRequest(system), + new CSharpTypePrintOptions + { + Usings = ["System.Text"] + }); + + Assert.Contains( + result.Diagnostics, + diagnostic => diagnostic.Message.Contains( + "Namespace root 'System' conflicts with global type 'System'", + StringComparison.Ordinal)); + } + [Fact] public void NestedTypesAndPrimaryConstructorsRenderInFileScopedNamespaceUnits() { @@ -1799,6 +3770,179 @@ public void EnumAndDelegateRequestsUseTheirLanguageDeclarations() StringComparison.Ordinal); } + [Fact] + public void DelegateReturnAttributesAreRenderedAndPlanned() + { + var external = CreateEmptyType("", "External"); + var invoke = CreateMethod("Invoke"); + invoke.SignatureModel!.ReturnType = "bool"; + invoke.SignatureModel.ReturnAttributes = ["External.Marker"]; + var delegateType = CreateEmptyType("Samples", "Predicate"); + delegateType.Kind = "delegate"; + delegateType.Members.Add(invoke); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(external), new CSharpTypePrintRequest(delegateType)], + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified + }); + + Assert.Contains( + "[return: global::External.Marker]\n public delegate bool Predicate();", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void ConfiguredNamespaceShortensDelegateSignatureTypes() + { + var invoke = CreateMethod("Invoke"); + invoke.SignatureModel!.ReturnType = "External.Result"; + invoke.SignatureModel.Parameters.Add(new ApiParameter + { + Type = "External.Input", + Name = "value" + }); + var delegateType = CreateEmptyType("Samples", "Handler"); + delegateType.Kind = "delegate"; + delegateType.Members.Add(invoke); + + var result = _printer.Print( + new CSharpTypePrintRequest(delegateType), + new CSharpTypePrintOptions { Usings = ["External"] }); + + Assert.Contains("using External;", result.Source, StringComparison.Ordinal); + Assert.Contains( + "public delegate Result Handler(Input value);", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void DelegateConstraintStaysQualifiedWithDistinctConfiguredUsing() + { + var invoke = CreateMethod("Invoke"); + var delegateType = CreateEmptyType("Samples", "Handler`1"); + delegateType.Kind = "delegate"; + delegateType.Members.Add(invoke); + delegateType.TypeParameters = + [ + new TypeParameter + { + Name = "T", + Constraints = ["Lib.Exception"] + } + ]; + + var result = _printer.Print( + new CSharpTypePrintRequest(delegateType), + new CSharpTypePrintOptions { Usings = ["System"] }); + + Assert.Equal(["System"], result.Usings); + Assert.Contains( + "public delegate void Handler() where T : Lib.Exception;", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void DelegateSignatureTypeStaysQualifiedWithDistinctConfiguredUsing() + { + var invoke = CreateMethod("Invoke"); + invoke.SignatureModel!.ReturnType = "Lib.Exception"; + var delegateType = CreateEmptyType("Samples", "Callback"); + delegateType.Kind = "delegate"; + delegateType.Members.Add(invoke); + + var result = _printer.Print( + new CSharpTypePrintRequest(delegateType), + new CSharpTypePrintOptions { Usings = ["System"] }); + + Assert.Equal(["System"], result.Usings); + Assert.Contains( + "public delegate Lib.Exception Callback();", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void DelegateSignatureTypesStayQualifiedAcrossDistinctDerivedUsings() + { + var invoke = CreateMethod("Invoke"); + invoke.SignatureModel!.ReturnType = "Alpha.Result"; + invoke.SignatureModel.Parameters.Add(new ApiParameter + { + Type = "Beta.Input", + Name = "value" + }); + var delegateType = CreateEmptyType("Samples", "Handler"); + delegateType.Kind = "delegate"; + delegateType.Members.Add(invoke); + + var result = _printer.Print(new CSharpTypePrintRequest(delegateType)); + + Assert.Empty(result.Usings); + Assert.Contains( + "public delegate Alpha.Result Handler(Beta.Input value);", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void DelegateParameterAttributesRemainQualified() + { + var invoke = CreateMethod("Invoke"); + invoke.SignatureModel!.ReturnType = "Contracts.Marker"; + invoke.SignatureModel.Parameters.Add(new ApiParameter + { + Type = "string", + Name = "value", + Attributes = ["Attributes.Marker"] + }); + var delegateType = CreateEmptyType("Samples", "Handler"); + delegateType.Kind = "delegate"; + delegateType.Members.Add(invoke); + + var result = _printer.Print(new CSharpTypePrintRequest(delegateType)); + + Assert.Empty(result.Usings); + Assert.Contains( + "public delegate Contracts.Marker Handler([Attributes.Marker] string value);", + result.Source, + StringComparison.Ordinal); + } + + [Fact] + public void DelegateAttributeDoesNotEraseSameTypeSignatureEvidence() + { + var invoke = CreateMethod("Invoke"); + invoke.SignatureModel!.ReturnType = "A.Foo"; + invoke.SignatureModel.Parameters.Add(new ApiParameter + { + Type = "B.Foo", + Name = "value", + Attributes = ["B.Foo"] + }); + invoke.SignatureModel.Parameters.Add(new ApiParameter + { + Type = "B.Other", + Name = "other" + }); + var delegateType = CreateEmptyType("Samples", "Handler"); + delegateType.Kind = "delegate"; + delegateType.Members.Add(invoke); + + var result = _printer.Print(new CSharpTypePrintRequest(delegateType)); + + Assert.DoesNotContain("using A;", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain("using B;", result.Source, StringComparison.Ordinal); + Assert.Contains( + "public delegate A.Foo Handler([B.Foo] B.Foo value, B.Other other);", + result.Source, + StringComparison.Ordinal); + } + [Fact] public void NestedTypeFailsWithoutItsDeclaringType() { @@ -2117,6 +4261,104 @@ public void SourceEmitsPragmaAndAssemblyAndModuleAttributesWhenRequested() StringComparison.Ordinal); } + [Fact] + public void GlobalAttributesAreEscapedAndDiagnoseGlobalRootConflicts() + { + var result = _printer.Print( + new CSharpTypePrintRequest(CreateEmptyType("", "System")), + new CSharpTypePrintOptions + { + TypeNamePolicy = CSharpTypeNamePolicy.Qualified, + AssemblyAttributes = ["System.CLSCompliantAttribute(true)"], + ModuleAttributes = ["event.Marker"] + }); + + Assert.Contains( + "[assembly: global::System.CLSCompliantAttribute(true)]", + result.Source, + StringComparison.Ordinal); + Assert.Contains("[module: @event.Marker]", result.Source, StringComparison.Ordinal); + Assert.Contains( + result.Diagnostics, + diagnostic => diagnostic.TypeName == "" + && diagnostic.Message.Contains("conflicts with global type 'System'", StringComparison.Ordinal)); + } + + [Fact] + public void SynthesizedObsoleteAttributeCannotBindToSiblingType() + { + var obsolete = CreateEmptyType("Samples", "Obsolete"); + var widget = CreateEmptyType("Samples", "Widget"); + var member = CreateMethod("Get"); + member.SignatureModel!.ReturnType = "int"; + member.IsObsolete = true; + widget.Members.Add(member); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(obsolete), new CSharpTypePrintRequest(widget)]); + + Assert.Contains("[System.Obsolete] public int Get();", result.Source, StringComparison.Ordinal); + } + + [Fact] + public void SynthesizedObsoleteReportsGlobalSystemConflict() + { + var system = CreateEmptyType("", "System"); + var widget = CreateEmptyType("Samples", "Widget"); + var member = CreateMethod("Get"); + member.SignatureModel!.ReturnType = "int"; + member.IsObsolete = true; + widget.Members.Add(member); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(system), new CSharpTypePrintRequest(widget)]); + + Assert.Contains("[global::System.Obsolete]", result.Source, StringComparison.Ordinal); + Assert.Contains( + result.Diagnostics, + diagnostic => diagnostic.Message.Contains( + "conflicts with global type 'System'", + StringComparison.Ordinal)); + } + + [Fact] + public void GenericGlobalTypeDoesNotConflictWithNamespaceRoot() + { + var generic = CreateEmptyType("", "Foo`1"); + generic.TypeParameters = [new TypeParameter { Name = "T" }]; + var namespaced = CreateEmptyType("Foo.Bar", "Worker"); + + var result = _printer.PrintBatch( + [new CSharpTypePrintRequest(generic), new CSharpTypePrintRequest(namespaced)]); + + Assert.DoesNotContain( + result.Diagnostics, + diagnostic => diagnostic.Message.Contains( + "Namespace root 'Foo' conflicts with global type 'Foo'", + StringComparison.Ordinal)); + } + + [Fact] + public void GlobalNestedTypeReferenceDoesNotReportNamespaceRootConflict() + { + var host = CreateEmptyType("", "Host"); + var kind = CreateEmptyType("", "Kind"); + var method = CreateMethod("GetKind"); + method.SignatureModel!.ReturnType = "Host.Kind"; + host.Members.Add(method); + + var result = _printer.Print(new CSharpTypePrintRequest( + host, + nestedTypes: [new CSharpTypePrintRequest(kind)])); + + Assert.Contains("public global::Host.Kind GetKind();", result.Source, StringComparison.Ordinal); + Assert.DoesNotContain( + result.Diagnostics, + diagnostic => diagnostic.Message.Contains( + "Type name 'Host.Kind' conflicts with global type 'Host'", + StringComparison.Ordinal)); + } + [Fact] public void SourceEscapesDeduplicatesAndSortsEmittedUsings() { diff --git a/src/ILInspector.CSharp/CSharpDeclarationWriter.cs b/src/ILInspector.CSharp/CSharpDeclarationWriter.cs index 1782b60e32..37c49c08fb 100644 --- a/src/ILInspector.CSharp/CSharpDeclarationWriter.cs +++ b/src/ILInspector.CSharp/CSharpDeclarationWriter.cs @@ -24,6 +24,13 @@ internal sealed record CSharpDeclarationOptions public CSharpTypeNameMode TypeNameMode { get; init; } = CSharpTypeNameMode.Qualified; public string? ContainingNamespace { get; init; } public IReadOnlyCollection Usings { get; init; } = []; + // Fields avoid shifting the MethodDef tokens pinned by AuthoredCorpusHarnessProcessTests. + public IReadOnlyCollection AdditionalShadowingNames = []; + public IReadOnlyCollection AdditionalRootShadowingNames = []; + public IReadOnlyCollection AdditionalUnresolvableRootNames = []; + public IReadOnlyCollection AdditionalDeclaredTypeFullNames = []; + public IReadOnlyCollection AdditionalImportedDeclaredTypeFullNames = []; + public IReadOnlyCollection AdditionalKnownNamespaces = []; public CSharpNamespaceMode NamespaceMode { get; init; } = CSharpNamespaceMode.Omit; public bool AbbreviateSignature { get; init; } public bool TerminateMemberDeclaration { get; init; } @@ -62,8 +69,33 @@ public static CSharpRenderedDeclaration RenderMemberUnit( IReadOnlyList? methodParameters = null) { options ??= new CSharpDeclarationOptions(); - var references = CollectMemberTypeReferences(member); - var plan = TypeNamePlan.Create(references, options); + var attributeReferences = CollectAttributeTypeReferences(member.Attributes, member).ToHashSet(StringComparer.Ordinal); + var attributeValueReferences = CollectAttributeValueTypeReferences(member.Attributes, member) + .ToHashSet(StringComparer.Ordinal); + var qualificationOnlyAttributeReferences = CollectQualificationOnlyAttributeTypeReferences( + member.Attributes, + member) + .Concat(CollectExplicitInterfaceTypeReferences(member)) + .ToHashSet(StringComparer.Ordinal); + var synthesizedAttributeReferences = member.IsObsolete && options.IncludeObsoleteAttribute + ? new HashSet(["System.Obsolete"], StringComparer.Ordinal) + : new HashSet(StringComparer.Ordinal); + var memberReferences = CollectMemberTypeReferences(member).ToHashSet(StringComparer.Ordinal); + var explicitInterfaceReferences = CollectExplicitInterfaceTypeReferences(member) + .ToHashSet(StringComparer.Ordinal); + var references = memberReferences + .Concat(attributeReferences) + .Concat(explicitInterfaceReferences) + .Concat(synthesizedAttributeReferences); + var plan = TypeNamePlan.Create( + references, + options, + CollectShadowingNames(type, [member]), + CSharpFormatter.StripArity(type.Name), + qualificationOnlyAttributeReferences, + memberReferences.Except(explicitInterfaceReferences).ToHashSet(StringComparer.Ordinal), + attributeValueReferences, + synthesizedAttributeReferences); var declaration = RenderMemberDeclarationCore(type, member, options, methodParameters); declaration = plan.Apply(declaration); @@ -81,8 +113,33 @@ public static string RenderMemberDeclaration( IReadOnlyList? methodParameters = null) { options ??= new CSharpDeclarationOptions(); - var references = CollectMemberTypeReferences(member); - var plan = TypeNamePlan.Create(references, options); + var attributeReferences = CollectAttributeTypeReferences(member.Attributes, member).ToHashSet(StringComparer.Ordinal); + var attributeValueReferences = CollectAttributeValueTypeReferences(member.Attributes, member) + .ToHashSet(StringComparer.Ordinal); + var qualificationOnlyAttributeReferences = CollectQualificationOnlyAttributeTypeReferences( + member.Attributes, + member) + .Concat(CollectExplicitInterfaceTypeReferences(member)) + .ToHashSet(StringComparer.Ordinal); + var synthesizedAttributeReferences = member.IsObsolete && options.IncludeObsoleteAttribute + ? new HashSet(["System.Obsolete"], StringComparer.Ordinal) + : new HashSet(StringComparer.Ordinal); + var memberReferences = CollectMemberTypeReferences(member).ToHashSet(StringComparer.Ordinal); + var explicitInterfaceReferences = CollectExplicitInterfaceTypeReferences(member) + .ToHashSet(StringComparer.Ordinal); + var references = memberReferences + .Concat(attributeReferences) + .Concat(explicitInterfaceReferences) + .Concat(synthesizedAttributeReferences); + var plan = TypeNamePlan.Create( + references, + options, + CollectShadowingNames(type, [member]), + CSharpFormatter.StripArity(type.Name), + qualificationOnlyAttributeReferences, + memberReferences.Except(explicitInterfaceReferences).ToHashSet(StringComparer.Ordinal), + attributeValueReferences, + synthesizedAttributeReferences); var declaration = RenderMemberDeclarationCore(type, member, options, methodParameters); declaration = plan.Apply(declaration); return options.TerminateMemberDeclaration && NeedsTerminator(declaration) @@ -93,15 +150,67 @@ public static string RenderMemberDeclaration( public static CSharpRenderedDeclaration RenderTypeUnit( ApiType type, IEnumerable? members = null, - CSharpDeclarationOptions? options = null) + CSharpDeclarationOptions? options = null, + IReadOnlyList? primaryConstructorParameters = null) { options ??= new CSharpDeclarationOptions { NamespaceMode = CSharpNamespaceMode.FileScoped }; var memberList = members?.ToList() ?? type.Members; - var references = CollectTypeReferences(type) - .Concat(memberList.SelectMany(CollectMemberTypeReferences)); - var plan = TypeNamePlan.Create(references, options); - - List lines = [plan.Apply(RenderTypeDeclarationCore(type, options))]; + var parameters = primaryConstructorParameters ?? []; + var attributeReferences = CollectAttributeTypeReferences(type.Attributes) + .Concat(memberList.SelectMany(member => CollectAttributeTypeReferences(member.Attributes, member))) + .ToHashSet(StringComparer.Ordinal); + var attributeValueReferences = CollectAttributeValueTypeReferences(type.Attributes) + .Concat(memberList.SelectMany(member => + CollectAttributeValueTypeReferences(member.Attributes, member))) + .Concat(parameters.SelectMany(parameter => + CollectAttributeValueTypeReferences(parameter.Attributes))) + .ToHashSet(StringComparer.Ordinal); + var qualificationOnlyAttributeReferences = CollectDeclaredAttributeTypeReferences(type.Attributes) + .Concat(memberList.SelectMany(member => + CollectQualificationOnlyAttributeTypeReferences(member.Attributes, member))) + .Concat(parameters.SelectMany(parameter => + CollectAttributeArgumentTypeReferences(parameter.Attributes))) + .Concat(memberList.SelectMany(CollectExplicitInterfaceTypeReferences)) + .ToHashSet(StringComparer.Ordinal); + var synthesizedAttributeReferences = memberList + .Where(member => member.IsObsolete && options.IncludeObsoleteAttribute) + .Select(_ => "System.Obsolete") + .ToHashSet(StringComparer.Ordinal); + var primaryParameterAttributeReferences = parameters + .SelectMany(parameter => CollectAttributeTypeReferences(parameter.Attributes)) + .ToHashSet(StringComparer.Ordinal); + var primaryParameterDeclaredAttributeReferences = parameters + .SelectMany(parameter => CollectDeclaredAttributeTypeReferences(parameter.Attributes)) + .ToHashSet(StringComparer.Ordinal); + var shortenableReferences = CollectTypeReferences(type) + .Concat(memberList.SelectMany(member => CollectMemberTypeReferences(member))) + .Concat(parameters.SelectMany(parameter => + ExtractTypeNames(parameter.Type))) + .ToHashSet(StringComparer.Ordinal); + shortenableReferences.ExceptWith(memberList.SelectMany(CollectExplicitInterfaceTypeReferences)); + var references = shortenableReferences + .Concat(parameters.SelectMany(CollectParameterTypeReferences)) + .Concat(attributeReferences) + .Concat(primaryParameterAttributeReferences) + .Concat(memberList.SelectMany(CollectExplicitInterfaceTypeReferences)) + .Concat(synthesizedAttributeReferences); + var plan = TypeNamePlan.Create( + references, + options, + CollectShadowingNames(type, memberList), + CSharpFormatter.StripArity(type.Name), + qualificationOnlyAttributeReferences, + shortenableReferences, + attributeValueReferences, + synthesizedAttributeReferences, + primaryParameterDeclaredAttributeReferences); + + string typeDeclaration = AddPrimaryConstructorParameters( + type, + RenderTypeDeclarationCore(type, options), + options, + parameters); + List lines = [plan.Apply(typeDeclaration)]; lines.Add("{"); foreach (var member in memberList) { @@ -125,11 +234,118 @@ static string IndentEveryLine(string text, string pad) ? string.Join('\n', text.Split('\n').Select(line => line.Length == 0 ? line : pad + line)) : pad + text; - public static string RenderTypeDeclaration(ApiType type, CSharpDeclarationOptions? options = null) + public static string RenderTypeDeclaration( + ApiType type, + CSharpDeclarationOptions? options = null, + IReadOnlyList? primaryConstructorParameters = null, + ApiMember? delegateInvoke = null) { options ??= new CSharpDeclarationOptions(); - var plan = TypeNamePlan.Create(CollectTypeReferences(type), options); - return plan.Apply(RenderTypeDeclarationCore(type, options)); + var parameters = primaryConstructorParameters ?? []; + if (delegateInvoke is not null) + { + if (delegateInvoke.SignatureModel is not { } signature) + { + throw new NotSupportedException( + $"Delegate '{type.FullName}' requires a structured Invoke signature."); + } + + var delegateAttributeReferences = CollectAttributeTypeReferences(type.Attributes) + .Concat(CollectAttributeTypeReferences(delegateInvoke.Attributes, delegateInvoke)) + .ToHashSet(StringComparer.Ordinal); + var delegateAttributeValueReferences = CollectAttributeValueTypeReferences(type.Attributes) + .Concat(CollectAttributeValueTypeReferences(delegateInvoke.Attributes, delegateInvoke)) + .ToHashSet(StringComparer.Ordinal); + var delegateSignatureReferences = CollectTypeReferences(type) + .Concat(CollectMemberTypeReferences(delegateInvoke)) + .ToHashSet(StringComparer.Ordinal); + var delegateQualificationOnlyReferences = CollectDeclaredAttributeTypeReferences(type.Attributes) + .Concat(CollectDeclaredAttributeTypeReferences(delegateInvoke.Attributes, delegateInvoke)) + .ToHashSet(StringComparer.Ordinal); + delegateSignatureReferences.ExceptWith(delegateQualificationOnlyReferences); + var references = delegateSignatureReferences + .Concat(delegateQualificationOnlyReferences) + .Concat(delegateAttributeReferences) + .ToList(); + var delegatePlan = TypeNamePlan.Create( + references, + options, + CollectShadowingNames(type, [delegateInvoke]), + CSharpFormatter.StripArity(type.Name), + delegateQualificationOnlyReferences, + delegateSignatureReferences, + valueReferences: delegateAttributeValueReferences); + string attributes = options.IncludeCustomAttributes && type.Attributes.Count > 0 + ? string.Join("\n", type.Attributes.Select(attribute => $"[{attribute}]")) + "\n" + : ""; + string unsafeText = delegateInvoke.IsUnsafe ? " unsafe" : ""; + string parameterList = CSharpFormatter.FormatParameterList(signature.Parameters); + string returnAttributes = signature.ReturnAttributes.Count > 0 + ? $"[return: {string.Join(", ", signature.ReturnAttributes)}]\n" + : ""; + string delegateDeclaration = + $"{attributes}{returnAttributes}{TypeAccessibility(type)}{unsafeText} delegate {signature.ReturnType ?? "void"} {FormatTypeDisplayName(type.Name, type.TypeParameters)}{parameterList}"; + delegateDeclaration = AppendTypeParameterConstraints(delegateDeclaration, type.TypeParameters); + return delegatePlan.Apply(delegateDeclaration + ";"); + } + + var attributeReferences = CollectAttributeTypeReferences(type.Attributes).ToHashSet(StringComparer.Ordinal); + var attributeValueReferences = CollectAttributeValueTypeReferences(type.Attributes) + .Concat(parameters.SelectMany(parameter => + CollectAttributeValueTypeReferences(parameter.Attributes))) + .ToHashSet(StringComparer.Ordinal); + var parameterAttributeReferences = parameters + .SelectMany(parameter => CollectAttributeTypeReferences(parameter.Attributes)) + .ToHashSet(StringComparer.Ordinal); + var qualificationOnlyAttributeReferences = CollectDeclaredAttributeTypeReferences(type.Attributes) + .Concat(parameters.SelectMany(parameter => + CollectAttributeArgumentTypeReferences(parameter.Attributes))) + .ToHashSet(StringComparer.Ordinal); + var shortenableReferences = CollectTypeReferences(type) + .Concat(parameters.SelectMany(parameter => + ExtractTypeNames(parameter.Type))) + .ToHashSet(StringComparer.Ordinal); + var primaryParameterDeclaredAttributeReferences = parameters + .SelectMany(parameter => CollectDeclaredAttributeTypeReferences(parameter.Attributes)) + .ToHashSet(StringComparer.Ordinal); + var plan = TypeNamePlan.Create( + shortenableReferences + .Concat(parameters.SelectMany(CollectParameterTypeReferences)) + .Concat(attributeReferences) + .Concat(parameterAttributeReferences), + options, + CollectShadowingNames(type, []), + CSharpFormatter.StripArity(type.Name), + qualificationOnlyAttributeReferences, + shortenableReferences, + attributeValueReferences, + attributeNameReferences: primaryParameterDeclaredAttributeReferences); + string declaration = AddPrimaryConstructorParameters( + type, + RenderTypeDeclarationCore(type, options), + options, + parameters); + return plan.Apply(declaration); + } + + internal static ( + IReadOnlyList Attributes, + IReadOnlyList Diagnostics) RenderAttributeBodies( + IReadOnlyList attributes, + CSharpDeclarationOptions options) + { + var references = CollectAttributeTypeReferences(attributes).ToHashSet(StringComparer.Ordinal); + var valueReferences = CollectAttributeValueTypeReferences(attributes).ToHashSet(StringComparer.Ordinal); + var qualificationOnlyReferences = CollectDeclaredAttributeTypeReferences(attributes) + .ToHashSet(StringComparer.Ordinal); + var plan = TypeNamePlan.Create( + references, + options, + new HashSet(StringComparer.Ordinal), + "", + qualificationOnlyReferences, + valueReferences: valueReferences); + return (attributes.Select(plan.Apply).ToArray(), plan.Diagnostics); } /// @@ -138,108 +354,569 @@ public static string RenderTypeDeclaration(ApiType type, CSharpDeclarationOption /// (including any nested types the caller flattens in). /// A namespace is included only when every simple type name it contributes is /// unambiguous across the whole unit: the simple name maps to a single full name - /// and does not clash with a type declared in the unit. Importing such a - /// namespace and shortening those references therefore cannot introduce an - /// ambiguous or shadowed reference. References whose simple name is ambiguous - /// stay fully qualified and their namespaces are excluded. + /// and is not shadowed by a declaration or visible namespace. Importing such a + /// namespace and shortening those references therefore cannot rebind them. + /// Ambiguous or shadowed references stay qualified and their namespaces are + /// excluded. /// public static IReadOnlyList DeriveContextualUsings(IReadOnlyCollection types) { ArgumentNullException.ThrowIfNull(types); + return DeriveTypeNameContext(types.Select(type => ( + Type: type, + Members: (IEnumerable)type.Members, + AdditionalParameters: Enumerable.Empty(), + DeclaredTypeFullName: string.IsNullOrWhiteSpace(type.Namespace) + ? CSharpFormatter.StripArity(type.Name) + : $"{type.Namespace}.{CSharpFormatter.StripArity(type.Name)}", + CanImportDeclaringNamespace: true))) + .SafeUsings; + } - var typeRefs = types - .SelectMany(type => CollectTypeReferences(type) - .Concat(type.Members.SelectMany(CollectMemberTypeReferences))) + internal static ( + IReadOnlyList SafeUsings, + IReadOnlyList KnownNamespaces, + IReadOnlyList<(string Namespace, string SimpleName)> ReferencedTypeNames) DeriveTypeNameContext( + IEnumerable<( + ApiType Type, + IEnumerable Members, + IEnumerable AdditionalParameters, + string DeclaredTypeFullName, + bool CanImportDeclaringNamespace)> scopes, + IEnumerable? contextualNamespaces = null, + IEnumerable? additionalAttributes = null) + { + var contextualNamespaceList = (contextualNamespaces ?? []).ToList(); + var scopeList = scopes + .Select(scope => ( + scope.Type, + Members: scope.Members.ToList(), + AdditionalParameters: scope.AdditionalParameters.ToList(), + scope.DeclaredTypeFullName, + scope.CanImportDeclaringNamespace)) + .ToList(); + var typeRefs = scopeList + .SelectMany(scope => CollectTypeReferences(scope.Type) + .Concat(scope.Members.SelectMany(member => + CollectMemberTypeReferences( + member, + includeParameterAttributes: scope.Type.Kind != "delegate"))) + .Concat(scope.AdditionalParameters.SelectMany(parameter => + ExtractTypeNames(parameter.Type)))) .Select(TypeRef.TryCreate) .Where(r => r is not null) .Select(r => r!) .DistinctBy(r => r.FullName, StringComparer.Ordinal) .ToList(); - - var declaredSimpleNames = types - .Select(type => CSharpFormatter.StripArity(type.Name)) + var attributeTypeRefs = scopeList + .SelectMany(scope => CollectDeclaredAttributeTypeReferences(scope.Type.Attributes) + .Concat(scope.Members.SelectMany(member => + CollectDeclaredAttributeTypeReferences(member.Attributes, member))) + .Concat(scope.Members.SelectMany(CollectExplicitInterfaceTypeReferences)) + .Concat(scope.AdditionalParameters.SelectMany(parameter => + CollectDeclaredAttributeTypeReferences(parameter.Attributes))) + .Concat(scope.Members + .Where(member => member.IsObsolete) + .Select(_ => "System.Obsolete"))) + .Concat(CollectDeclaredAttributeTypeReferences(additionalAttributes ?? [])) + .Select(TypeRef.TryCreate) + .Where(r => r is not null) + .Select(r => r!) + .DistinctBy(r => r.FullName, StringComparer.Ordinal) + .ToList(); + var primaryAttributeTypeRefs = scopeList + .SelectMany(scope => scope.AdditionalParameters.SelectMany(parameter => + CollectDeclaredAttributeTypeReferences(parameter.Attributes))) + .Select(TypeRef.TryCreate) + .Where(r => r is not null) + .Select(r => r!) + .DistinctBy(r => r.FullName, StringComparer.Ordinal) + .ToList(); + // Dotted signature text does not distinguish namespaces from enclosing types. + // Newly planned delegate and primary-constructor surfaces need independent + // namespace evidence before they can introduce a using. + var existingSurfaceTypeFullNames = scopeList + .SelectMany(scope => + scope.Type.Kind == "delegate" + ? [] + : CollectTypeReferences(scope.Type) + .Concat(scope.Members.SelectMany(member => + CollectMemberTypeReferences( + member, + includeParameterAttributes: true)))) + .Select(TypeRef.TryCreate) + .Where(reference => reference is not null) + .Select(reference => reference!.FullName) .ToHashSet(StringComparer.Ordinal); - - // Generic type/method parameters shadow same-named type references within - // their scope: importing a namespace and shortening a reference to a simple - // name that matches an in-scope type parameter would rebind it to the - // parameter. Exclude those namespaces so such references stay qualified. - foreach (var type in types) - { - foreach (var typeParameter in type.TypeParameters) - declaredSimpleNames.Add(typeParameter.Name); - foreach (var member in type.Members) + var exclusiveImportTypeFullNames = scopeList + .SelectMany(scope => + (scope.Type.Kind == "delegate" + ? CollectTypeReferences(scope.Type) + .Concat(scope.Members.SelectMany(member => + CollectMemberTypeReferences( + member, + includeParameterAttributes: false))) + : []) + .Concat(scope.AdditionalParameters.SelectMany(parameter => + ExtractTypeNames(parameter.Type)))) + .Select(TypeRef.TryCreate) + .Where(reference => reference is not null) + .Select(reference => reference!.FullName) + .ToHashSet(StringComparer.Ordinal); + exclusiveImportTypeFullNames.ExceptWith(existingSurfaceTypeFullNames); + + var knownNamespaces = typeRefs + .Select(typeRef => typeRef.Namespace) + .Concat(attributeTypeRefs.Select(typeRef => typeRef.Namespace)) + .Concat(contextualNamespaceList) + .Concat(scopeList.Select(scope => scope.Type.Namespace)) + .Where(ns => !string.IsNullOrWhiteSpace(ns)) + .Select(ns => ns!) + .Distinct(StringComparer.Ordinal) + .ToList(); + var declaredTypeNames = new HashSet(StringComparer.Ordinal); + var uniquelyImportableDeclaredTypeFullNames = scopeList + .GroupBy( + scope => CSharpFormatter.StripArity(scope.Type.Name), + StringComparer.Ordinal) + .Where(group => { - if (member.SignatureModel is { } signature) - { - foreach (var typeParameter in signature.TypeParameters) - declaredSimpleNames.Add(typeParameter.Name); - } - - // Members whose signature failed structured decoding fall back to the - // raw signature string, whose generic method parameters are not in - // SignatureModel. Parse them so they still shadow same-named references. - foreach (var name in RawSignatureGenericParameterNames(member)) - declaredSimpleNames.Add(name); + var identities = group + .Select(scope => scope.DeclaredTypeFullName) + .Distinct(StringComparer.Ordinal) + .ToList(); + return identities.Count == 1 + && group.All(scope => scope.CanImportDeclaringNamespace); + }) + .Select(group => group.First().DeclaredTypeFullName) + .ToHashSet(StringComparer.Ordinal); + var shadowingNames = new HashSet(StringComparer.Ordinal); + var rootShadowingNames = new HashSet(StringComparer.Ordinal); + foreach (var scope in scopeList) + { + string declaredTypeName = CSharpFormatter.StripArity(scope.Type.Name); + declaredTypeNames.Add(declaredTypeName); + rootShadowingNames.Add(declaredTypeName); + foreach (var knownNamespace in knownNamespaces) + { + AddVisibleNamespaceNames( + shadowingNames, + scope.Type.Namespace, + knownNamespace, + rootShadowingNames); } + var lexicalShadowingNames = CollectShadowingNames(scope.Type, scope.Members); + shadowingNames.UnionWith(lexicalShadowingNames); + rootShadowingNames.UnionWith(lexicalShadowingNames); } var usings = new SortedSet(StringComparer.Ordinal); - var collidingSimpleNames = typeRefs - .GroupBy(r => r.SimpleName, StringComparer.Ordinal) - .Where(g => g.Select(r => r.FullName).Distinct(StringComparer.Ordinal).Count() > 1) - .Select(g => g.Key) + var potentiallyImportedNamespaces = typeRefs + .Select(typeRef => typeRef.Namespace) + .Concat(contextualNamespaceList) + .Concat(scopeList + .Select(scope => scope.Type.Namespace) + .Where(ns => !string.IsNullOrWhiteSpace(ns)) + .Select(ns => ns!)) .ToHashSet(StringComparer.Ordinal); + var collidingSimpleNames = CollidingSimpleNames(typeRefs); + var unsafePrimaryAttributeNamespaces = new HashSet(StringComparer.Ordinal); + foreach (var attributeTypeRef in attributeTypeRefs) + { + if (!potentiallyImportedNamespaces.Contains(attributeTypeRef.Namespace)) + continue; - // A nested type referenced as a type (e.g. `System.Environment.SpecialFolder`) - // arrives here as a flat dotted string, indistinguishable from a - // namespace-qualified reference: TypeRef.TryCreate splits at the last dot and - // derives namespace `System.Environment`, which is actually a type. Emitting - // `using System.Environment;` is illegal (CS0138). When the enclosing type is - // itself referenced in the unit we can detect this — its full name appears as a - // derived namespace — and exclude that namespace. (The isolated case, where the - // enclosing type is never referenced on its own, is not detectable from the - // flattened string alone; a full fix needs nested-type identity from the - // metadata layer. The failure mode is safe-visible: the reference stays - // qualified and, for a spurious using, RTS records a RecompileFail rather than - // miscompiling.) - var referencedFullNames = typeRefs - .Select(r => r.FullName) - .ToHashSet(StringComparer.Ordinal); + var collidingTypeNames = new HashSet(StringComparer.Ordinal) + { + attributeTypeRef.SimpleName + }; + if (!attributeTypeRef.SimpleName.EndsWith("Attribute", StringComparison.Ordinal)) + collidingTypeNames.Add($"{attributeTypeRef.SimpleName}Attribute"); + bool collides = typeRefs.Any(typeRef => + typeRef.FullName != attributeTypeRef.FullName + && collidingTypeNames.Contains(typeRef.SimpleName)); + if (collides) + { + collidingSimpleNames.UnionWith(collidingTypeNames); + if (primaryAttributeTypeRefs.Any(primary => + primary.FullName == attributeTypeRef.FullName)) + { + unsafePrimaryAttributeNamespaces.Add(attributeTypeRef.Namespace); + } + } + } + foreach (var attributeTypeRef in primaryAttributeTypeRefs) + { + if (!potentiallyImportedNamespaces.Contains(attributeTypeRef.Namespace)) + continue; - // A namespace contributes a simple name for every reference it owns. Per-type - // shortening keys off namespace membership, so importing a namespace shortens - // every reference it owns. If any of those simple names is ambiguous unit-wide - // or shadowed by a declared type or type parameter, importing the namespace is - // unsafe: the shortened reference would become ambiguous or rebind. Exclude the - // whole namespace so every reference it owns stays fully qualified. - var unsafeNamespaces = typeRefs - .Where(r => collidingSimpleNames.Contains(r.SimpleName) || declaredSimpleNames.Contains(r.SimpleName)) - .Select(r => r.Namespace) + var lookupNames = AttributeLookupNames(attributeTypeRef); + bool collides = attributeTypeRefs.Any(other => + other.FullName != attributeTypeRef.FullName + && potentiallyImportedNamespaces.Contains(other.Namespace) + && lookupNames.Overlaps(AttributeLookupNames(other))); + if (collides) + unsafePrimaryAttributeNamespaces.Add(attributeTypeRef.Namespace); + } + var unsafeNamespaces = UnsafeNamespaces( + typeRefs, + shadowingNames, + collidingSimpleNames, + rootShadowingNames, + declaredTypeNames, + uniquelyImportableDeclaredTypeFullNames, + typeRefs.Concat(attributeTypeRefs).ToList()); + unsafeNamespaces.UnionWith(unsafePrimaryAttributeNamespaces); + var establishedNamespaces = contextualNamespaceList + .Concat(scopeList + .Select(scope => scope.Type.Namespace) + .Where(ns => !string.IsNullOrWhiteSpace(ns)) + .Select(ns => ns!)) + .Concat(typeRefs + .Where(typeRef => + !exclusiveImportTypeFullNames.Contains(typeRef.FullName)) + .Select(typeRef => typeRef.Namespace)) .ToHashSet(StringComparer.Ordinal); + var exclusiveImportNamespaces = new HashSet(StringComparer.Ordinal); foreach (var group in typeRefs.GroupBy(r => r.SimpleName, StringComparer.Ordinal)) { if (collidingSimpleNames.Contains(group.Key)) continue; - if (declaredSimpleNames.Contains(group.Key)) + if (shadowingNames.Contains(group.Key)) + continue; + bool importsDeclaredType = declaredTypeNames.Contains(group.Key); + if (importsDeclaredType + && !uniquelyImportableDeclaredTypeFullNames.Contains(group.First().FullName)) continue; var ns = group.First().Namespace; + if (ns.Length == 0) + continue; if (unsafeNamespaces.Contains(ns)) continue; - if (referencedFullNames.Contains(ns)) + bool requiresEstablishedNamespace = + exclusiveImportTypeFullNames.Contains(group.First().FullName); + if (requiresEstablishedNamespace + && !establishedNamespaces.Contains(ns)) + { continue; + } usings.Add(ns); + if (importsDeclaredType + || requiresEstablishedNamespace) + { + exclusiveImportNamespaces.Add(ns); + } + } + + var effectiveImportedNamespaces = usings + .Concat(contextualNamespaceList) + .Where(ns => !string.IsNullOrWhiteSpace(ns)) + .ToHashSet(StringComparer.Ordinal); + foreach (var exclusiveNamespace in exclusiveImportNamespaces) + { + if (effectiveImportedNamespaces.Any(ns => + !string.Equals(ns, exclusiveNamespace, StringComparison.Ordinal))) + { + usings.Remove(exclusiveNamespace); + } + } + + var referencedTypeNames = typeRefs + .Concat(attributeTypeRefs) + .Select(typeRef => (typeRef.Namespace, typeRef.SimpleName)) + .Distinct() + .ToList(); + return (usings.ToList(), knownNamespaces, referencedTypeNames); + + static HashSet AttributeLookupNames(TypeRef typeRef) + { + var names = new HashSet(StringComparer.Ordinal) + { + typeRef.SimpleName + }; + if (!typeRef.SimpleName.EndsWith("Attribute", StringComparison.Ordinal)) + names.Add($"{typeRef.SimpleName}Attribute"); + return names; } + } - return usings.ToList(); + static IEnumerable CollectParameterTypeReferences(ApiParameter parameter) + { + if (!string.IsNullOrWhiteSpace(parameter.Type)) + foreach (var reference in ExtractTypeNames(parameter.Type)) + yield return reference; + foreach (var attribute in parameter.Attributes) + foreach (var reference in ExtractTypeNames(StripAttributeArguments(attribute))) + yield return reference; + } + + static IEnumerable CollectAttributeTypeReferences( + IEnumerable attributes, + ApiMember? member = null) + => CollectDeclaredAttributeTypeReferences(attributes, member) + .Concat(CollectAttributeValueTypeReferences(attributes, member)); + + static IEnumerable CollectDeclaredAttributeTypeReferences( + IEnumerable attributes, + ApiMember? member = null) + { + foreach (var attribute in AttributeTexts(attributes, member)) + { + foreach (var reference in ExtractTypeNames(StripAttributeArguments(attribute))) + yield return reference; + foreach (var reference in CollectAttributeArgumentTypeReferences([attribute])) + yield return reference; + } + } + + static IEnumerable CollectAttributeArgumentTypeReferences( + IEnumerable attributes, + ApiMember? member = null) + { + foreach (var attribute in AttributeTexts(attributes, member)) + { + int firstArgumentList = attribute.IndexOf('(', StringComparison.Ordinal); + for (var index = 0; index < attribute.Length; index++) + { + if (IsStringLiteralStart(attribute, index)) + { + index = SkipStringLiteral(attribute, index) - 1; + continue; + } + if (attribute[index] == '\'') + { + index = SkipCharLiteral(attribute, index) - 1; + continue; + } + + bool isTypeOf = attribute.AsSpan(index).StartsWith("typeof(", StringComparison.Ordinal); + bool isNestedCast = attribute[index] == '(' + && index > firstArgumentList; + if (!isTypeOf && !isNestedCast) + continue; + + int open = isTypeOf ? index + "typeof".Length : index; + int close = attribute.IndexOf(')', open + 1); + if (close < 0) + break; + if (isNestedCast) + { + int next = close + 1; + while (next < attribute.Length && char.IsWhiteSpace(attribute[next])) + next++; + // A following + or - is ambiguous with a parenthesized value + // expression, so preserve it rather than inventing type evidence. + if (next >= attribute.Length + || !char.IsAsciiDigit(attribute[next])) + { + continue; + } + } + + foreach (var reference in ExtractTypeNames(attribute[(open + 1)..close])) + yield return reference; + index = close; + } + } + } + + static IEnumerable CollectAttributeValueTypeReferences( + IEnumerable attributes, + ApiMember? member = null) + { + foreach (var attribute in AttributeTexts(attributes, member)) + { + int firstArgumentList = attribute.IndexOf('(', StringComparison.Ordinal); + if (firstArgumentList < 0) + continue; + var recognizedReferences = CollectAttributeArgumentTypeReferences([attribute]) + .ToHashSet(StringComparer.Ordinal); + foreach (var valueExpression in ExtractTypeNames( + attribute[(firstArgumentList + 1)..])) + { + if (recognizedReferences.Contains(valueExpression)) + continue; + int memberSeparator = valueExpression.LastIndexOf('.'); + if (memberSeparator <= 0) + continue; + string declaringType = valueExpression[..memberSeparator]; + if (declaringType.Contains('.', StringComparison.Ordinal)) + yield return declaringType; + } + } + } + + static IEnumerable CollectQualificationOnlyAttributeTypeReferences( + IEnumerable memberAttributes, + ApiMember member) + { + foreach (var reference in CollectDeclaredAttributeTypeReferences(memberAttributes)) + yield return reference; + if (member.SignatureModel is not { } signature) + yield break; + + foreach (var reference in CollectDeclaredAttributeTypeReferences(signature.ReturnAttributes)) + yield return reference; + foreach (var accessor in signature.Accessors) + foreach (var reference in CollectDeclaredAttributeTypeReferences(accessor.ReturnAttributes)) + yield return reference; + foreach (var parameter in signature.Parameters) + foreach (var reference in CollectAttributeArgumentTypeReferences(parameter.Attributes)) + yield return reference; + } + + static IEnumerable AttributeTexts( + IEnumerable attributes, + ApiMember? member) + { + foreach (var attribute in attributes) + yield return attribute; + if (member?.SignatureModel is not { } signature) + yield break; + + foreach (var attribute in signature.ReturnAttributes) + yield return attribute; + foreach (var parameter in signature.Parameters) + foreach (var attribute in parameter.Attributes) + yield return attribute; + foreach (var accessor in signature.Accessors) + foreach (var attribute in accessor.ReturnAttributes) + yield return attribute; + } + + static string AddPrimaryConstructorParameters( + ApiType type, + string declaration, + CSharpDeclarationOptions options, + IReadOnlyList parameters) + { + if (parameters.Count == 0) + return declaration; + string declarationWithoutAttributes = RenderTypeDeclarationCore( + type, + options with { IncludeCustomAttributes = false }); + if (!declaration.EndsWith(declarationWithoutAttributes, StringComparison.Ordinal)) + { + throw new InvalidOperationException( + $"C# type declaration for '{type.FullName}' has an unexpected attribute prefix."); + } + + string parameterList = CSharpFormatter.FormatParameterList(parameters); + int constraints = declarationWithoutAttributes.IndexOf(" where ", StringComparison.Ordinal); + string head = constraints >= 0 + ? declarationWithoutAttributes[..constraints] + : declarationWithoutAttributes; + string tail = constraints >= 0 ? declarationWithoutAttributes[constraints..] : ""; + int inheritance = head.IndexOf(" : ", StringComparison.Ordinal); + string withParameters = inheritance >= 0 + ? head[..inheritance] + parameterList + head[inheritance..] + tail + : $"{head}{parameterList}{tail}"; + return declaration[..^declarationWithoutAttributes.Length] + withParameters; + } + + static HashSet CollectShadowingNames( + ApiType type, + IEnumerable members) + { + var names = new HashSet(StringComparer.Ordinal); + foreach (var typeParameter in type.TypeParameters) + names.Add(typeParameter.Name); + foreach (var member in members) + { + if (member.SignatureModel is { } signature) + foreach (var typeParameter in signature.TypeParameters) + names.Add(typeParameter.Name); + + foreach (var name in RawSignatureGenericParameterNames(member)) + names.Add(name); + } + return names; + } + + static void AddVisibleNamespaceNames( + HashSet names, + string? containingNamespace, + string? knownNamespace, + HashSet? shadowedGlobalRoots = null) + { + if (string.IsNullOrWhiteSpace(knownNamespace)) + return; + var knownSegments = knownNamespace.Split( + '.', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (knownSegments.Length == 0) + return; + names.Add(knownSegments[0]); + + if (string.IsNullOrWhiteSpace(containingNamespace)) + return; + var containingSegments = containingNamespace.Split( + '.', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + int sharedLength = Math.Min(containingSegments.Length, knownSegments.Length - 1); + for (var i = 0; i < sharedLength; i++) + { + if (!string.Equals( + containingSegments[i], + knownSegments[i], + StringComparison.Ordinal)) + { + break; + } + names.Add(knownSegments[i + 1]); + shadowedGlobalRoots?.Add(knownSegments[i + 1]); + } + } + + static HashSet CollidingSimpleNames(IReadOnlyList typeRefs) + => typeRefs + .GroupBy(r => r.SimpleName, StringComparer.Ordinal) + .Where(g => g.Select(r => r.FullName).Distinct(StringComparer.Ordinal).Count() > 1) + .Select(g => g.Key) + .ToHashSet(StringComparer.Ordinal); + + static string NamespaceRoot(string ns) + { + int separator = ns.IndexOf('.'); + return separator < 0 ? ns : ns[..separator]; + } + + static HashSet UnsafeNamespaces( + IReadOnlyList typeRefs, + IReadOnlySet shadowingNames, + IReadOnlySet collidingSimpleNames, + IReadOnlySet rootShadowingNames, + IReadOnlySet? declaredTypeNames = null, + IReadOnlySet? uniquelyImportableDeclaredTypeFullNames = null, + IReadOnlyList? bindingEvidence = null) + { + declaredTypeNames ??= new HashSet(StringComparer.Ordinal); + uniquelyImportableDeclaredTypeFullNames ??= new HashSet(StringComparer.Ordinal); + var unsafeNamespaces = typeRefs + .Where(r => collidingSimpleNames.Contains(r.SimpleName) + || shadowingNames.Contains(r.SimpleName) + || (declaredTypeNames.Contains(r.SimpleName) + && !uniquelyImportableDeclaredTypeFullNames.Contains(r.FullName)) + || rootShadowingNames.Contains(NamespaceRoot(r.Namespace))) + .Select(r => r.Namespace) + .ToHashSet(StringComparer.Ordinal); + + var referencedFullNames = (bindingEvidence ?? typeRefs) + .Select(r => r.FullName) + .ToHashSet(StringComparer.Ordinal); + unsafeNamespaces.UnionWith(typeRefs + .Where(r => referencedFullNames.Contains(r.Namespace)) + .Select(r => r.Namespace)); + + return unsafeNamespaces; } static string ComposeUnit(IReadOnlyList bodyLines, IReadOnlyList usings, CSharpDeclarationOptions options) { var sb = new StringBuilder(); foreach (var ns in usings) - sb.AppendLf($"using {ns};"); + sb.AppendLf($"using {EscapeNamespace(ns)};"); if (usings.Count > 0) sb.AppendLf(); @@ -247,7 +924,7 @@ static string ComposeUnit(IReadOnlyList bodyLines, IReadOnlyList if (options.NamespaceMode == CSharpNamespaceMode.FileScoped && !string.IsNullOrWhiteSpace(options.ContainingNamespace)) { - sb.AppendLf($"namespace {options.ContainingNamespace};"); + sb.AppendLf($"namespace {EscapeNamespace(options.ContainingNamespace)};"); sb.AppendLf(); } @@ -489,29 +1166,52 @@ static string RenderMemberDeclarationCore( static IEnumerable CollectTypeReferences(ApiType type) { if (type.BaseType is { Length: > 0 }) - yield return type.BaseType; + { + foreach (var reference in ExtractTypeNames(type.BaseType)) + yield return reference; + } foreach (var iface in type.Interfaces) - yield return iface; + { + foreach (var reference in ExtractTypeNames(iface)) + yield return reference; + } foreach (var typeParameter in type.TypeParameters) { foreach (var constraint in typeParameter.Constraints) { - foreach (var reference in ExtractQualifiedTypeNames(constraint)) + foreach (var reference in ExtractTypeNames(constraint)) yield return reference; } } } - static IEnumerable CollectMemberTypeReferences(ApiMember member) + static IEnumerable CollectMemberTypeReferences( + ApiMember member, + bool includeParameterAttributes = true) { - foreach (var expression in MemberTypeExpressions(member)) + foreach (var expression in MemberTypeExpressions(member, includeParameterAttributes)) { - foreach (var reference in ExtractQualifiedTypeNames(expression)) + foreach (var reference in ExtractTypeNames(expression)) yield return reference; } } - static IEnumerable MemberTypeExpressions(ApiMember member) + static IEnumerable CollectExplicitInterfaceTypeReferences(ApiMember member) + { + if (member.Kind != "explicit-interface-implementation") + yield break; + + int memberSeparator = member.Name.LastIndexOf('.'); + if (memberSeparator > 0) + { + foreach (var reference in ExtractTypeNames(member.Name[..memberSeparator])) + yield return reference; + } + } + + static IEnumerable MemberTypeExpressions( + ApiMember member, + bool includeParameterAttributes) { if (!string.IsNullOrWhiteSpace(member.ReturnType)) yield return member.ReturnType!; @@ -523,12 +1223,15 @@ static IEnumerable MemberTypeExpressions(ApiMember member) { if (!string.IsNullOrWhiteSpace(parameter.Type)) yield return parameter.Type; - foreach (var attribute in parameter.Attributes) - yield return StripAttributeArguments(attribute); + if (includeParameterAttributes) + { + foreach (var attribute in parameter.Attributes) + yield return StripAttributeArguments(attribute); + } } foreach (var typeParameter in signatureModel.TypeParameters) foreach (var constraint in typeParameter.Constraints) - foreach (var reference in ExtractQualifiedTypeNames(constraint)) + foreach (var reference in ExtractTypeNames(constraint)) yield return reference; } @@ -753,14 +1456,20 @@ static string StripAttributeArguments(string attribute) return paren < 0 ? attribute : attribute[..paren]; } - static IEnumerable ExtractQualifiedTypeNames(string expression) + static IEnumerable ExtractTypeNames(string expression) { foreach (var token in DottedIdentifierTokens(expression)) { - if (token.Contains('.', StringComparison.Ordinal) - && !token.StartsWith("global.", StringComparison.Ordinal) - && !token.StartsWith("global::", StringComparison.Ordinal)) - yield return token; + if (token.StartsWith("global.", StringComparison.Ordinal) + || token.StartsWith("global::", StringComparison.Ordinal)) + { + continue; + } + + string normalized = token.StartsWith('@') ? token[1..] : token; + yield return normalized + .Replace(".@", ".", StringComparison.Ordinal) + .Replace("+@", "+", StringComparison.Ordinal); } } @@ -778,19 +1487,66 @@ static IEnumerable DottedIdentifierTokens(string text) i = SkipCharLiteral(text, i); continue; } - if (!IsIdentifierStart(text[i])) + if (!IsIdentifierStart(text[i]) + && (text[i] != '@' + || i + 1 >= text.Length + || !IsIdentifierStart(text[i + 1]))) { i++; continue; } - var start = i++; - while (i < text.Length && (IsIdentifierPart(text[i]) || text[i] is '.' or '+')) - i++; - yield return text[start..i].TrimEnd('.'); + var token = new StringBuilder(); + bool yieldedConstructedRoot = false; + while (i < text.Length) + { + int segmentStart = i++; + while (i < text.Length && IsIdentifierPart(text[i])) + i++; + token.Append(text.AsSpan(segmentStart, i - segmentStart)); + + if (i < text.Length && text[i] == '<') + { + if (!yieldedConstructedRoot) + { + yield return token.ToString(); + yieldedConstructedRoot = true; + } + int close = MatchingAngleBracket(text, i); + if (close < 0) + break; + foreach (string nested in DottedIdentifierTokens(text[(i + 1)..close])) + yield return nested; + i = close + 1; + } + + if (i + 1 >= text.Length + || text[i] is not ('.' or '+') + || (!IsIdentifierStart(text[i + 1]) && text[i + 1] != '@')) + { + break; + } + + token.Append(text[i++]); + } + if (!yieldedConstructedRoot) + yield return token.ToString(); } } + static int MatchingAngleBracket(string text, int open) + { + int depth = 0; + for (int i = open; i < text.Length; i++) + { + if (text[i] == '<') + depth++; + else if (text[i] == '>' && --depth == 0) + return i; + } + return -1; + } + /// /// Appends a method's where clauses. An override and an explicit /// interface implementation inherit their constraints and mostly may not restate @@ -1282,7 +2038,7 @@ static string FormatTypeDisplayName(string name, IReadOnlyList ty var tick = name.IndexOf('`'); if (tick >= 0) name = name[..tick]; - name = EscapeQualifiedIdentifier(name); + name = ContainQualifiedName(name); if (typeParameters.Count > 0) name += $"<{string.Join(", ", typeParameters.Select(TypeParameterDisplayName))}>"; return name; @@ -1317,8 +2073,8 @@ static string TypeParameterDisplayName(TypeParameter typeParameter) static string FormatObsoleteAttribute(string? message) => string.IsNullOrWhiteSpace(message) - ? "[Obsolete]" - : $"[Obsolete(\"{EscapeCSharpString(message)}\")]"; + ? "[System.Obsolete]" + : $"[System.Obsolete(\"{EscapeCSharpString(message)}\")]"; // The Obsolete message is attacker-controlled attribute text rendered inside a // C# string literal. Escaping only the classic C-escapes leaves vertical tabs, @@ -2071,165 +2827,461 @@ static int SkipInterpolationHole(string text, int open, int depth) } sealed record TypeNamePlan( - IReadOnlyDictionary Replacements, + IReadOnlyList> Replacements, ImmutableSortedSet GeneratedUsings, - IReadOnlyList Diagnostics) + List Diagnostics) { public string Apply(string text) { - foreach (var (qualified, replacement) in Replacements.OrderByDescending(kvp => kvp.Key.Length)) - text = ReplaceIdentifierToken(text, qualified, replacement); - return text; + var sb = new StringBuilder(text.Length); + for (var i = 0; i < text.Length;) + { + if (IsStringLiteralStart(text, i)) + { + var end = SkipStringLiteral(text, i); + sb.Append(text.AsSpan(i, end - i)); + i = end; + continue; + } + if (text[i] == '\'') + { + var end = SkipCharLiteral(text, i); + sb.Append(text.AsSpan(i, end - i)); + i = end; + continue; + } + + bool matched = false; + foreach (var (token, replacements) in Replacements) + { + if (i + token.Length > text.Length + || !text.AsSpan(i, token.Length).SequenceEqual(token) + || !IsStartBoundary(text, i - 1) + || !IsEndBoundary(text, i + token.Length)) + { + continue; + } + + if (IsWithinGlobalAlias(text, i)) + { + string qualified = replacements.Qualified; + sb.Append(qualified.StartsWith("global::", StringComparison.Ordinal) + ? qualified["global::".Length..] + : qualified); + AddDiagnostic(replacements.Diagnostic); + } + else + { + bool preserveQualification = IsAttributeValuePrefix( + text, + i, + i + token.Length); + string replacement = preserveQualification + ? replacements.Qualified + : replacements.Shortened ?? replacements.Qualified; + sb.Append(replacement); + if (replacement == replacements.Qualified) + AddDiagnostic(replacements.Diagnostic); + } + i += token.Length; + matched = true; + break; + } + + if (!matched) + sb.Append(text[i++]); + } + + return sb.ToString(); + + void AddDiagnostic(string? diagnostic) + { + if (diagnostic is not null + && !Diagnostics.Contains(diagnostic, StringComparer.Ordinal)) + { + Diagnostics.Add(diagnostic); + } + } } - public static TypeNamePlan Create(IEnumerable references, CSharpDeclarationOptions options) + public static TypeNamePlan Create( + IEnumerable references, + CSharpDeclarationOptions options, + IReadOnlySet shadowingNames, + string declaredTypeName, + IReadOnlySet? qualificationOnlyReferences = null, + IReadOnlySet? shortenableReferences = null, + IReadOnlySet? valueReferences = null, + IReadOnlySet? preferredSimpleNameReferences = null, + IReadOnlySet? attributeNameReferences = null) { - if (options.TypeNameMode == CSharpTypeNameMode.Qualified) - return new TypeNamePlan( - new Dictionary(), - ImmutableSortedSet.Create(StringComparer.Ordinal), - []); - + var qualificationOnlyFullNames = qualificationOnlyReferences? + .Select(TypeRef.TryCreate) + .Where(reference => reference is not null) + .Select(reference => reference!.FullName) + .ToHashSet(StringComparer.Ordinal) + ?? new HashSet(StringComparer.Ordinal); + if (shortenableReferences is not null) + { + qualificationOnlyFullNames.ExceptWith(shortenableReferences + .Select(TypeRef.TryCreate) + .Where(reference => reference is not null) + .Select(reference => reference!.FullName)); + } + var valueFullNames = valueReferences? + .Select(TypeRef.TryCreate) + .Where(reference => reference is not null) + .Select(reference => reference!.FullName) + .ToHashSet(StringComparer.Ordinal) + ?? new HashSet(StringComparer.Ordinal); + var valueOnlyFullNames = valueFullNames.ToHashSet(StringComparer.Ordinal); + valueOnlyFullNames.ExceptWith(qualificationOnlyFullNames); + if (shortenableReferences is not null) + { + valueOnlyFullNames.ExceptWith(shortenableReferences + .Select(TypeRef.TryCreate) + .Where(reference => reference is not null) + .Select(reference => reference!.FullName)); + } + var preferredSimpleNameFullNames = preferredSimpleNameReferences? + .Select(TypeRef.TryCreate) + .Where(reference => reference is not null) + .Select(reference => reference!.FullName) + .ToHashSet(StringComparer.Ordinal) + ?? new HashSet(StringComparer.Ordinal); + var attributeNameFullNames = attributeNameReferences? + .Select(TypeRef.TryCreate) + .Where(reference => reference is not null) + .Select(reference => reference!.FullName) + .ToHashSet(StringComparer.Ordinal) + ?? new HashSet(StringComparer.Ordinal); var typeRefs = references .Select(TypeRef.TryCreate) .Where(r => r is not null) .Select(r => r!) .DistinctBy(r => r.FullName, StringComparer.Ordinal) .ToList(); + var bindingTypeRefs = typeRefs + .Where(reference => !valueOnlyFullNames.Contains(reference.FullName)) + .ToList(); - var collisions = typeRefs - .GroupBy(r => r.SimpleName, StringComparer.Ordinal) - .Where(g => g.Select(r => r.FullName).Distinct(StringComparer.Ordinal).Count() > 1) - .ToDictionary(g => g.Key, g => g.ToList(), StringComparer.Ordinal); - + var lexicalShadowingNames = shadowingNames.ToHashSet(StringComparer.Ordinal); + lexicalShadowingNames.UnionWith(options.AdditionalShadowingNames); + var namespaceShadowingNames = new HashSet(StringComparer.Ordinal); + var namespaceRootShadowingNames = new HashSet(StringComparer.Ordinal); + foreach (var typeRef in bindingTypeRefs) + { + AddVisibleNamespaceNames( + namespaceShadowingNames, + options.ContainingNamespace, + typeRef.Namespace, + namespaceRootShadowingNames); + } + foreach (var ns in options.Usings) + { + AddVisibleNamespaceNames( + namespaceShadowingNames, + options.ContainingNamespace, + ns, + namespaceRootShadowingNames); + } + foreach (var ns in options.AdditionalKnownNamespaces) + { + AddVisibleNamespaceNames( + namespaceShadowingNames, + options.ContainingNamespace, + ns, + namespaceRootShadowingNames); + } + AddVisibleNamespaceNames( + namespaceShadowingNames, + options.ContainingNamespace, + options.ContainingNamespace, + namespaceRootShadowingNames); + if (bindingTypeRefs.Any(r => string.Equals(r.SimpleName, declaredTypeName, StringComparison.Ordinal) + && !string.Equals(r.Namespace, options.ContainingNamespace, StringComparison.Ordinal))) + { + lexicalShadowingNames.Add(declaredTypeName); + } + var rootShadowingNames = lexicalShadowingNames.ToHashSet(StringComparer.Ordinal); + rootShadowingNames.UnionWith(options.AdditionalRootShadowingNames); + rootShadowingNames.Add(declaredTypeName); + rootShadowingNames.UnionWith(namespaceRootShadowingNames); + rootShadowingNames.UnionWith(bindingTypeRefs + .Where(typeRef => + { + string containingNamespace = options.ContainingNamespace ?? ""; + return typeRef.Namespace.Length <= containingNamespace.Length + && containingNamespace.StartsWith(typeRef.Namespace, StringComparison.Ordinal) + && (typeRef.Namespace.Length == containingNamespace.Length + || typeRef.Namespace.Length == 0 + || containingNamespace[typeRef.Namespace.Length] == '.'); + }) + .Select(typeRef => typeRef.SimpleName)); + + var shorteningTypeRefs = bindingTypeRefs + .Where(reference => !qualificationOnlyFullNames.Contains(reference.FullName)) + .ToList(); var contextualUsings = options.Usings.ToHashSet(StringComparer.Ordinal); + var potentiallyImportedNamespaces = contextualUsings.ToHashSet(StringComparer.Ordinal); + if (!string.IsNullOrWhiteSpace(options.ContainingNamespace)) + potentiallyImportedNamespaces.Add(options.ContainingNamespace); + if (options.TypeNameMode == CSharpTypeNameMode.ShortWithUsings) + potentiallyImportedNamespaces.UnionWith(shorteningTypeRefs.Select(reference => reference.Namespace)); + var collisionEvidence = bindingTypeRefs + .Where(reference => reference.Namespace.Length == 0 + || potentiallyImportedNamespaces.Contains(reference.Namespace)) + .ToList(); + var collisions = CollidingSimpleNames(collisionEvidence); + var allShadowingNames = lexicalShadowingNames + .Concat(namespaceShadowingNames) + .ToHashSet(StringComparer.Ordinal); + var unsafeNamespaces = UnsafeNamespaces( + shorteningTypeRefs, + allShadowingNames, + collisions, + rootShadowingNames, + bindingEvidence: bindingTypeRefs); var generatedUsings = new SortedSet(StringComparer.Ordinal); var diagnostics = new List(); - var replacements = new Dictionary(StringComparer.Ordinal); + var replacements = new Dictionary( + StringComparer.Ordinal); + void ReplaceQualifiedName( + TypeRef typeRef, + string qualifiedReplacement, + string? shortenedReplacement = null, + string? diagnostic = null) + { + var plan = (qualifiedReplacement, shortenedReplacement, diagnostic); + Add(typeRef.FullName); + Add(EscapeQualifiedKeywordSegments(typeRef.FullName)); + Add(EscapeNamespace(typeRef.FullName)); + + void Add(string key) + { + replacements[key] = plan; + } + } + string ResolvableQualifiedName(TypeRef typeRef) + { + string root = NamespaceRoot(typeRef.Namespace); + string escapedFullName = EscapeNamespace(typeRef.FullName); + return rootShadowingNames.Contains(root) + ? $"global::{escapedFullName}" + : escapedFullName; + } + string? UnresolvableRootDiagnostic(TypeRef typeRef) + { + string root = NamespaceRoot(typeRef.Namespace); + return options.AdditionalUnresolvableRootNames.Contains(root) + && !options.AdditionalDeclaredTypeFullNames.Contains(typeRef.FullName) + && IsKnownNamespaceRoot(root) + ? $"Type name '{typeRef.FullName}' conflicts with global type '{root}'; emitted the only available global-qualified spelling." + : null; + } + void KeepResolvableQualified(TypeRef typeRef) + { + ReplaceQualifiedName( + typeRef, + ResolvableQualifiedName(typeRef), + diagnostic: UnresolvableRootDiagnostic(typeRef)); + } + void KeepAttributeValueQualified(TypeRef typeRef) + { + string root = NamespaceRoot(typeRef.Namespace); + string qualified = (options.AdditionalDeclaredTypeFullNames.Contains(typeRef.FullName) + || options.AdditionalImportedDeclaredTypeFullNames.Contains(typeRef.FullName)) + && !IsKnownNamespaceRoot(root) + ? EscapeNamespace(typeRef.FullName) + : ResolvableQualifiedName(typeRef); + ReplaceQualifiedName( + typeRef, + qualified, + diagnostic: UnresolvableRootDiagnostic(typeRef)); + } + bool IsKnownNamespaceRoot(string root) + => options.AdditionalKnownNamespaces.Any(@namespace => + string.Equals(@namespace, root, StringComparison.Ordinal) + || @namespace.StartsWith($"{root}.", StringComparison.Ordinal)); foreach (var typeRef in typeRefs) { - if (collisions.ContainsKey(typeRef.SimpleName)) + if (typeRef.Namespace.Length == 0) + continue; + if (valueOnlyFullNames.Contains(typeRef.FullName)) { - diagnostics.Add($"Type name '{typeRef.SimpleName}' is ambiguous; kept '{typeRef.FullName}' qualified."); + KeepAttributeValueQualified(typeRef); continue; } - + if (qualificationOnlyFullNames.Contains(typeRef.FullName)) + { + KeepResolvableQualified(typeRef); + continue; + } + if (options.TypeNameMode == CSharpTypeNameMode.Qualified + && !preferredSimpleNameFullNames.Contains(typeRef.FullName)) + { + KeepResolvableQualified(typeRef); + continue; + } + if (attributeNameFullNames.Contains(typeRef.FullName) + && !typeRef.SimpleName.EndsWith("Attribute", StringComparison.Ordinal)) + { + string suffixedName = $"{typeRef.SimpleName}Attribute"; + bool suffixCanBind = rootShadowingNames.Contains(suffixedName) + || allShadowingNames.Contains(suffixedName) + || collisionEvidence.Any(reference => + reference.FullName != typeRef.FullName + && reference.SimpleName == suffixedName); + if (suffixCanBind) + { + diagnostics.Add( + $"Attribute name '{typeRef.SimpleName}' can bind to '{suffixedName}'; kept '{typeRef.FullName}' qualified."); + KeepResolvableQualified(typeRef); + continue; + } + } var isSameNamespace = !string.IsNullOrWhiteSpace(options.ContainingNamespace) && string.Equals(typeRef.Namespace, options.ContainingNamespace, StringComparison.Ordinal); + if (!isSameNamespace && collisions.Contains(typeRef.SimpleName)) + { + diagnostics.Add($"Type name '{typeRef.SimpleName}' is ambiguous; kept '{typeRef.FullName}' qualified."); + KeepResolvableQualified(typeRef); + continue; + } + if (lexicalShadowingNames.Contains(typeRef.SimpleName)) + { + diagnostics.Add($"Type name '{typeRef.SimpleName}' is shadowed in this declaration; kept '{typeRef.FullName}' qualified."); + KeepResolvableQualified(typeRef); + continue; + } + if (!isSameNamespace && namespaceShadowingNames.Contains(typeRef.SimpleName)) + { + diagnostics.Add($"Type name '{typeRef.SimpleName}' is shadowed by a namespace in this declaration; kept '{typeRef.FullName}' qualified."); + KeepResolvableQualified(typeRef); + continue; + } + if (!isSameNamespace && unsafeNamespaces.Contains(typeRef.Namespace)) + { + diagnostics.Add($"Namespace '{typeRef.Namespace}' contains an ambiguous or shadowed type name; kept '{typeRef.FullName}' qualified."); + KeepResolvableQualified(typeRef); + continue; + } var isInContext = isSameNamespace || contextualUsings.Contains(typeRef.Namespace); if (options.TypeNameMode == CSharpTypeNameMode.ContextualShort && !isInContext) + { + KeepResolvableQualified(typeRef); continue; + } - replacements[typeRef.FullName] = typeRef.SimpleName; + ReplaceQualifiedName( + typeRef, + valueFullNames.Contains(typeRef.FullName) + ? ResolvableQualifiedName(typeRef) + : EscapeNamespace(typeRef.FullName), + EscapeIdentifier(typeRef.SimpleName), + UnresolvableRootDiagnostic(typeRef)); if (options.TypeNameMode == CSharpTypeNameMode.ShortWithUsings && !isSameNamespace) generatedUsings.Add(typeRef.Namespace); } return new TypeNamePlan( - replacements, + replacements.OrderByDescending(kvp => kvp.Key.Length).ToArray(), generatedUsings.ToImmutableSortedSet(StringComparer.Ordinal), diagnostics); } - static string ReplaceIdentifierToken(string text, string token, string replacement) + static bool IsWithinGlobalAlias(string text, int index) { - var sb = new StringBuilder(text.Length); - for (var i = 0; i < text.Length;) + var start = index; + while (start > 0 + && (IsIdentifierPart(text[start - 1]) || text[start - 1] is '.' or '+')) { - if (IsStringLiteralStart(text, i)) - { - var end = SkipStringLiteral(text, i); - sb.Append(text.AsSpan(i, end - i)); - i = end; - continue; - } - if (text[i] == '\'') - { - var end = SkipCharLiteral(text, i); - sb.Append(text.AsSpan(i, end - i)); - i = end; - continue; - } - if (i + token.Length <= text.Length - && text.AsSpan(i, token.Length).SequenceEqual(token) - && IsStartBoundary(text, i - 1) - && IsEndBoundary(text, i + token.Length)) - { - sb.Append(replacement); - i += token.Length; - continue; - } - - sb.Append(text[i++]); + start--; } - - return sb.ToString(); + return start >= "global::".Length + && text.AsSpan(start - "global::".Length, "global::".Length) + .SequenceEqual("global::"); } static bool IsStartBoundary(string text, int index) => index < 0 || index >= text.Length - || (!IsIdentifierPart(text[index]) && text[index] is not '.' and not '+'); + || (!IsIdentifierPart(text[index]) && text[index] is not '.' and not '+' and not '@'); static bool IsEndBoundary(string text, int index) => index < 0 || index >= text.Length || (!IsIdentifierPart(text[index]) && text[index] != '+'); - } - - static bool IsStringLiteralStart(string text, int index) - => text[index] == '"' - || (text[index] == '@' && index + 1 < text.Length && text[index + 1] == '"') - || (text[index] == '$' && index + 1 < text.Length && text[index + 1] == '"') - || (text[index] == '$' && index + 2 < text.Length && text[index + 1] == '@' && text[index + 2] == '"') - || (text[index] == '@' && index + 2 < text.Length && text[index + 1] == '$' && text[index + 2] == '"'); - static int SkipStringLiteral(string text, int start) - { - var i = start; - var verbatim = false; - if (text[i] == '$') + static bool IsAttributeValuePrefix(string text, int start, int end) { - i++; - if (i < text.Length && text[i] == '@') + if (end >= text.Length || text[end] != '.') + return false; + + int parenthesisDepth = 0; + var bracketParenthesisDepths = new Stack(); + for (int index = 0; index < start;) { - verbatim = true; - i++; + if (IsStringLiteralStart(text, index)) + { + index = SkipStringLiteral(text, index); + continue; + } + if (text[index] == '\'') + { + index = SkipCharLiteral(text, index); + continue; + } + + switch (text[index]) + { + case '[': + bracketParenthesisDepths.Push(parenthesisDepth); + break; + case ']': + if (bracketParenthesisDepths.Count > 0) + bracketParenthesisDepths.Pop(); + break; + case '(': + parenthesisDepth++; + break; + case ')': + if (parenthesisDepth > 0) + parenthesisDepth--; + break; + } + index++; } + + return bracketParenthesisDepths.Count > 0 + && parenthesisDepth > bracketParenthesisDepths.Peek(); } - else if (text[i] == '@') + } + + static bool IsStringLiteralStart(string text, int index) + { + if (text[index] == '"') + return true; + if (text[index] is not ('@' or '$')) + return false; + do { - i++; - if (i < text.Length && text[i] == '$') - i++; - verbatim = true; + index++; } + while (index < text.Length && text[index] is '@' or '$'); + return index < text.Length && text[index] == '"'; + } + static int SkipStringLiteral(string text, int start) + { + var i = start; + while (i < text.Length && text[i] is '@' or '$') + i++; if (i >= text.Length || text[i] != '"') return start + 1; - - i++; - while (i < text.Length) - { - if (text[i] == '"' && verbatim && i + 1 < text.Length && text[i + 1] == '"') - { - i += 2; - continue; - } - if (text[i] == '"') - return i + 1; - if (text[i] == '\\' && !verbatim && i + 1 < text.Length) - { - i += 2; - continue; - } - - i++; - } - - return text.Length; + return Math.Min(SkipLiteral(text, i) + 1, text.Length); } static int SkipCharLiteral(string text, int start) @@ -2251,6 +3303,8 @@ static int SkipCharLiteral(string text, int start) return text.Length; } + // An empty namespace records unqualified type-position evidence. It participates + // in collision analysis but never produces a replacement or using directive. sealed record TypeRef(string FullName, string Namespace, string SimpleName) { public static TypeRef? TryCreate(string value) @@ -2259,12 +3313,14 @@ sealed record TypeRef(string FullName, string Namespace, string SimpleName) if (value.Length == 0) return null; var lastDot = value.LastIndexOf('.'); - if (lastDot <= 0 || lastDot == value.Length - 1) + if (lastDot == value.Length - 1) + return null; + if (lastDot < 0) + return new TypeRef(value, "", StripArity(value)); + if (lastDot == 0) return null; var ns = value[..lastDot]; var simple = StripArity(value[(lastDot + 1)..]); - if (CSharpKeywords.RequiresDeclarationEscape(simple)) - return null; return new TypeRef(value, ns, simple); } diff --git a/src/ILInspector.CSharp/CSharpFormatter.cs b/src/ILInspector.CSharp/CSharpFormatter.cs index 128033e6d0..b9d4350f46 100644 --- a/src/ILInspector.CSharp/CSharpFormatter.cs +++ b/src/ILInspector.CSharp/CSharpFormatter.cs @@ -34,6 +34,12 @@ public sealed record CSharpFormatOptions public CSharpTypeNamePolicy TypeNamePolicy { get; init; } = CSharpTypeNamePolicy.Qualified; public string? ContainingNamespace { get; init; } public IReadOnlyCollection Usings { get; init; } = []; + internal IReadOnlyCollection AdditionalShadowingNames { get; init; } = []; + internal IReadOnlyCollection AdditionalRootShadowingNames { get; init; } = []; + internal IReadOnlyCollection AdditionalUnresolvableRootNames { get; init; } = []; + internal IReadOnlyCollection AdditionalDeclaredTypeFullNames { get; init; } = []; + internal IReadOnlyCollection AdditionalImportedDeclaredTypeFullNames { get; init; } = []; + internal IReadOnlyCollection AdditionalKnownNamespaces { get; init; } = []; public CSharpNamespacePolicy NamespacePolicy { get; init; } = CSharpNamespacePolicy.Omit; public bool AbbreviateSignature { get; init; } public bool TerminateMemberDeclaration { get; init; } @@ -128,24 +134,10 @@ public string FormatTypeDeclaration( IReadOnlyList? primaryConstructorParameters = null) { ArgumentNullException.ThrowIfNull(type); - string declaration = CSharpDeclarationWriter.RenderTypeDeclaration(type, _declarationOptions); - if (primaryConstructorParameters is not { Count: > 0 }) - return declaration; - - string declarationWithoutAttributes = CSharpDeclarationWriter.RenderTypeDeclaration( + return CSharpDeclarationWriter.RenderTypeDeclaration( type, - _declarationOptions with { IncludeCustomAttributes = false }); - if (!declaration.EndsWith(declarationWithoutAttributes, StringComparison.Ordinal)) - { - throw new InvalidOperationException( - $"C# type declaration for '{type.FullName}' has an unexpected attribute prefix."); - } - - string attributePrefix = declaration[..^declarationWithoutAttributes.Length]; - return attributePrefix - + AddPrimaryConstructorParameters( - declarationWithoutAttributes, - primaryConstructorParameters); + _declarationOptions, + primaryConstructorParameters); } public string FormatDelegate(ApiType type, ApiMember invoke) @@ -154,43 +146,34 @@ public string FormatDelegate(ApiType type, ApiMember invoke) ArgumentNullException.ThrowIfNull(invoke); if (type.Kind != "delegate") throw new ArgumentException($"Type '{type.FullName}' is not a delegate.", nameof(type)); - if (invoke.SignatureModel is not { } signature) + if (invoke.SignatureModel is null) { throw new NotSupportedException( $"Delegate '{type.FullName}' requires a structured Invoke signature."); } - string attributes = _declarationOptions.IncludeCustomAttributes && type.Attributes.Count > 0 - ? string.Join("\n", type.Attributes.Select(attribute => $"[{attribute}]")) + "\n" - : ""; - string unsafeText = invoke.IsUnsafe ? " unsafe" : ""; - string parameters = FormatParameterList(signature.Parameters); - string declaration = - $"{attributes}{CSharpDeclarationWriter.TypeAccessibility(type)}{unsafeText} delegate {signature.ReturnType ?? "void"} {FormatTypeName(type, includeVariance: true)}{parameters}"; - foreach (var typeParameter in type.TypeParameters) - { - if (typeParameter.Constraints.Count > 0) - { - declaration += - $" where {CSharpIdentifier.ContainIdentifierForDeclaration(typeParameter.Name)} : " - + CSharpDeclarationWriter.FormatConstraintList( - typeParameter, - type.TypeParameters.Select(parameter => parameter.Name)); - } - } - - return declaration + ";"; + return CSharpDeclarationWriter.RenderTypeDeclaration( + type, + _declarationOptions, + delegateInvoke: invoke); } public CSharpFormattedDeclaration FormatTypeUnit( ApiType type, IEnumerable? members = null) + => FormatTypeUnit(type, members, primaryConstructorParameters: null); + + internal CSharpFormattedDeclaration FormatTypeUnit( + ApiType type, + IEnumerable? members, + IReadOnlyList? primaryConstructorParameters) { ArgumentNullException.ThrowIfNull(type); return ToFormattedDeclaration(CSharpDeclarationWriter.RenderTypeUnit( type, members, - _declarationOptions)); + _declarationOptions, + primaryConstructorParameters)); } public static string EscapeIdentifier(string identifier) @@ -520,6 +503,12 @@ static CSharpDeclarationOptions ToDeclarationOptions( }, ContainingNamespace = options.ContainingNamespace, Usings = usings, + AdditionalShadowingNames = options.AdditionalShadowingNames, + AdditionalRootShadowingNames = options.AdditionalRootShadowingNames, + AdditionalUnresolvableRootNames = options.AdditionalUnresolvableRootNames, + AdditionalDeclaredTypeFullNames = options.AdditionalDeclaredTypeFullNames, + AdditionalImportedDeclaredTypeFullNames = options.AdditionalImportedDeclaredTypeFullNames, + AdditionalKnownNamespaces = options.AdditionalKnownNamespaces, NamespaceMode = options.NamespacePolicy switch { CSharpNamespacePolicy.Omit => CSharpNamespaceMode.Omit, @@ -544,17 +533,4 @@ static string FormatTypeParameter(TypeParameter parameter, bool includeVariance) ? $"{variance} {CSharpIdentifier.ContainIdentifierForDeclaration(parameter.Name)}" : CSharpIdentifier.ContainIdentifierForDeclaration(parameter.Name); - static string AddPrimaryConstructorParameters( - string declaration, - IReadOnlyList parameters) - { - string parameterList = FormatParameterList(parameters); - int constraints = declaration.IndexOf(" where ", StringComparison.Ordinal); - string head = constraints >= 0 ? declaration[..constraints] : declaration; - string tail = constraints >= 0 ? declaration[constraints..] : ""; - int inheritance = head.IndexOf(" : ", StringComparison.Ordinal); - return inheritance >= 0 - ? head[..inheritance] + parameterList + head[inheritance..] + tail - : $"{head}{parameterList}{tail}"; - } } diff --git a/src/ILInspector.CSharp/CSharpTypePrinter.cs b/src/ILInspector.CSharp/CSharpTypePrinter.cs index fdb435740e..dcb34f21f5 100644 --- a/src/ILInspector.CSharp/CSharpTypePrinter.cs +++ b/src/ILInspector.CSharp/CSharpTypePrinter.cs @@ -57,29 +57,122 @@ public CSharpTypePrintResult PrintBatch( nameof(requests))); } - var derivedUsings = ComputeDerivedUsings(preparedTypes, options); - IReadOnlyList contextualUsings = options.TypeNamePolicy switch - { - CSharpTypeNamePolicy.Qualified => [], - CSharpTypeNamePolicy.ShortWithUsings => derivedUsings, - CSharpTypeNamePolicy.ContextualShort when options.IncludeUsings => configuredUsings, - CSharpTypeNamePolicy.ContextualShort => [], - _ => throw new InvalidOperationException() - }; + var typeNameContext = ComputeTypeNameContext(preparedTypes, options); + var safeUsings = typeNameContext.SafeUsings; + var derivedUsings = options.TypeNamePolicy == CSharpTypeNamePolicy.ShortWithUsings + ? safeUsings + : []; + var contextualUsings = TypeNameContext(options, configuredUsings, safeUsings); var effectiveUsings = options.IncludeUsings ? configuredUsings .Concat(derivedUsings) .ToImmutableSortedSet(StringComparer.Ordinal) : ImmutableSortedSet.Create(StringComparer.Ordinal); - + var declaredTypeFullNamesByNamespace = + new Dictionary>(StringComparer.Ordinal); + foreach (var group in preparedTypes.GroupBy(type => type.Namespace, StringComparer.Ordinal)) + { + var declaredTypeFullNames = ImmutableHashSet.CreateBuilder(StringComparer.Ordinal); + var pendingTypes = new Stack<(PreparedType Type, string? Parent)>( + group.Select(type => (type, (string?)null))); + while (pendingTypes.TryPop(out var pending)) + { + string name = CSharpFormatter.StripArity(pending.Type.Type.Name); + string fullName = pending.Parent is null ? name : $"{pending.Parent}.{name}"; + declaredTypeFullNames.Add(fullName); + foreach (var nested in pending.Type.NestedTypes) + pendingTypes.Push((nested, fullName)); + } + declaredTypeFullNamesByNamespace.Add(group.Key, declaredTypeFullNames.ToImmutable()); + } + var importedDeclaredTypeFullNames = declaredTypeFullNamesByNamespace + .Where(entry => effectiveUsings.Contains(entry.Key)) + .SelectMany(entry => entry.Value.Select(path => (Namespace: entry.Key, Path: path))) + .GroupBy(entry => entry.Path, StringComparer.Ordinal) + .Where(group => group.Select(entry => entry.Namespace).Distinct(StringComparer.Ordinal).Count() == 1) + .Select(group => group.Key) + .ToImmutableHashSet(StringComparer.Ordinal); + var importedDeclaredTypeNames = preparedTypes + .Where(type => effectiveUsings.Contains(type.Namespace)) + .Select(type => CSharpFormatter.StripArity(type.Type.Name)) + .ToImmutableHashSet(StringComparer.Ordinal); + var globalDeclaredTypeNames = preparedTypes + .Where(type => type.Namespace.Length == 0 + && type.Type.TypeParameters.Count == 0) + .Select(type => CSharpFormatter.StripArity(type.Type.Name)) + .ToImmutableHashSet(StringComparer.Ordinal); + foreach (string globalTypeName in globalDeclaredTypeNames) + { + bool conflictsWithNamespace = preparedTypes.Any(type => + NamespaceRoot(type.Namespace) == globalTypeName); + bool conflictsWithUsing = effectiveUsings.Any(@namespace => + NamespaceRoot(@namespace) == globalTypeName); + if (conflictsWithNamespace || conflictsWithUsing) + { + diagnostics.Add(new CSharpTypePrintDiagnostic( + globalTypeName, + $"Namespace root '{globalTypeName}' conflicts with global type '{globalTypeName}'; emitted namespace or using directives cannot bind that root as a namespace.")); + } + } + var globalAttributeOptions = new CSharpDeclarationOptions + { + TypeNameMode = options.TypeNamePolicy == CSharpTypeNamePolicy.Qualified + ? CSharpTypeNameMode.Qualified + : CSharpTypeNameMode.ContextualShort, + Usings = contextualUsings, + AdditionalRootShadowingNames = globalDeclaredTypeNames, + AdditionalUnresolvableRootNames = globalDeclaredTypeNames, + AdditionalKnownNamespaces = typeNameContext.KnownNamespaces + }; + var plannedAssemblyAttributes = CSharpDeclarationWriter.RenderAttributeBodies( + options.AssemblyAttributes, + globalAttributeOptions); + var plannedModuleAttributes = CSharpDeclarationWriter.RenderAttributeBodies( + options.ModuleAttributes, + globalAttributeOptions); + diagnostics.AddRange(plannedAssemblyAttributes.Diagnostics.Select( + diagnostic => new CSharpTypePrintDiagnostic("", diagnostic))); + diagnostics.AddRange(plannedModuleAttributes.Diagnostics.Select( + diagnostic => new CSharpTypePrintDiagnostic("", diagnostic))); var units = ImmutableArray.CreateBuilder(); var renderedUnits = ImmutableArray.CreateBuilder(); foreach (var group in preparedTypes.GroupBy(type => type.Namespace, StringComparer.Ordinal)) { + var groupedTypes = group.ToList(); + var declaredTypeFullNameSet = declaredTypeFullNamesByNamespace + .Where(entry => string.Equals(entry.Key, group.Key, StringComparison.Ordinal) + || IsAncestorNamespace(entry.Key, group.Key)) + .SelectMany(entry => entry.Value) + .ToImmutableHashSet(StringComparer.Ordinal); var containingNamespace = group.Key.Length == 0 ? null : group.Key; bool useBlockScopedNamespace = containingNamespace is not null && !useFileScopedNamespace; + var ancestorTypeNames = preparedTypes + .Where(candidate => IsAncestorNamespace(candidate.Namespace, group.Key)) + .Select(candidate => CSharpFormatter.StripArity(candidate.Type.Name)) + .ToImmutableHashSet(StringComparer.Ordinal); + var referencedAncestorTypeNames = typeNameContext.ReferencedTypeNames + .Where(reference => string.Equals(reference.Namespace, group.Key, StringComparison.Ordinal) + || IsAncestorNamespace(reference.Namespace, group.Key)) + .Select(reference => reference.SimpleName); var rendered = Join( - group.Select(type => RenderType(type, indent: 0, options, contextualUsings, diagnostics)), + groupedTypes.Select(type => RenderType( + type, + indent: 0, + options, + contextualUsings, + inheritedShadowingNames: ImmutableHashSet.Empty, + inheritedRootShadowingNames: groupedTypes + .Where(sibling => !ReferenceEquals(sibling, type)) + .Select(sibling => CSharpFormatter.StripArity(sibling.Type.Name)) + .Concat(ancestorTypeNames) + .Concat(referencedAncestorTypeNames) + .Concat(importedDeclaredTypeNames) + .ToImmutableHashSet(StringComparer.Ordinal), + globalDeclaredTypeNames, + declaredTypeFullNameSet, + importedDeclaredTypeFullNames, + typeNameContext.KnownNamespaces, + diagnostics)), "\n\n"); if (containingNamespace is not null) { @@ -100,46 +193,93 @@ public CSharpTypePrintResult PrintBatch( return new CSharpTypePrintResult( unitList, effectiveUsings, - diagnostics.ToImmutable(), - () => ComposeSource(renderedUnitList, effectiveUsings, options)); + diagnostics.Distinct().ToImmutableArray(), + () => ComposeSource( + renderedUnitList, + effectiveUsings, + plannedAssemblyAttributes.Attributes, + plannedModuleAttributes.Attributes, + options)); } /// - /// Derives the collision-safe namespaces to shorten against, excluding the - /// unit's own declaring namespaces (their references are already shortened by - /// the same-namespace rule, so importing them would be redundant). + /// Derives collision-safe namespaces and the namespace identities known to the + /// complete output unit. /// - static IReadOnlyList ComputeDerivedUsings( + static ( + IReadOnlyList SafeUsings, + IReadOnlyList KnownNamespaces, + IReadOnlyList<(string Namespace, string SimpleName)> ReferencedTypeNames) ComputeTypeNameContext( IReadOnlyList preparedTypes, CSharpTypePrintOptions options) { - // Shortening is only sound when the enabling `using` directives are - // actually emitted. When usings are suppressed, keep references qualified - // so the composed source stays compilable. - if (options.TypeNamePolicy != CSharpTypeNamePolicy.ShortWithUsings - || !options.IncludeUsings) - return []; - - var allTypes = new List(); - var declaringNamespaces = new HashSet(StringComparer.Ordinal); - void Flatten(PreparedType prepared) - { - allTypes.Add(prepared.Type); - declaringNamespaces.Add(prepared.Namespace); + var scopes = new List<( + ApiType Type, + IEnumerable Members, + IEnumerable AdditionalParameters, + string DeclaredTypeFullName, + bool CanImportDeclaringNamespace)>(); + void Flatten(PreparedType prepared, string? parentPath) + { + string name = CSharpFormatter.StripArity(prepared.Type.Name); + string path = parentPath is null ? name : $"{parentPath}.{name}"; + string fullName = prepared.Namespace.Length == 0 + ? path + : $"{prepared.Namespace}.{path}"; + scopes.Add(( + prepared.Type, + prepared.Members.Select(member => member.Member), + prepared.PrimaryConstructorParameters, + fullName, + parentPath is null)); foreach (var nested in prepared.NestedTypes) - Flatten(nested); + Flatten(nested, path); } foreach (var prepared in preparedTypes) - Flatten(prepared); + Flatten(prepared, parentPath: null); - return CSharpFormatter.DeriveContextualUsings(allTypes) - .Where(ns => !declaringNamespaces.Contains(ns)) - .ToArray(); + return CSharpDeclarationWriter.DeriveTypeNameContext( + scopes, + options.Usings, + options.AssemblyAttributes.Concat(options.ModuleAttributes)); + } + + static string NamespaceRoot(string @namespace) + { + int separator = @namespace.IndexOf('.'); + return separator < 0 ? @namespace : @namespace[..separator]; } + static bool IsAncestorNamespace(string candidate, string descendant) + => candidate.Length < descendant.Length + && descendant.StartsWith(candidate, StringComparison.Ordinal) + && (candidate.Length == 0 || descendant[candidate.Length] == '.'); + + static IReadOnlyList TypeNameContext( + CSharpTypePrintOptions options, + IReadOnlyList configuredUsings, + IReadOnlyList safeUsings) + => options.TypeNamePolicy switch + { + CSharpTypeNamePolicy.Qualified => [], + CSharpTypeNamePolicy.ShortWithUsings => + options.IncludeUsings + ? safeUsings + : [], + CSharpTypeNamePolicy.ContextualShort => + options.IncludeUsings + ? configuredUsings + .Where(safeUsings.Contains) + .ToArray() + : [], + _ => throw new InvalidOperationException() + }; + static CSharpSourceArtifact ComposeSource( ImmutableArray units, IReadOnlyCollection usings, + IReadOnlyList assemblyAttributes, + IReadOnlyList moduleAttributes, CSharpTypePrintOptions options) { var sb = new System.Text.StringBuilder(); @@ -147,9 +287,9 @@ static CSharpSourceArtifact ComposeSource( string? bodyIndent = null; if (options.EmitPragmaWarningDisable) sb.AppendLf("#pragma warning disable"); - foreach (var attribute in options.AssemblyAttributes) + foreach (var attribute in assemblyAttributes) sb.AppendLf($"[assembly: {attribute}]"); - foreach (var attribute in options.ModuleAttributes) + foreach (var attribute in moduleAttributes) sb.AppendLf($"[module: {attribute}]"); if (options.IncludeUsings) { @@ -316,26 +456,60 @@ static RenderedFragment RenderType( int indent, CSharpTypePrintOptions options, IReadOnlyList contextualUsings, + IReadOnlySet inheritedShadowingNames, + IReadOnlySet inheritedRootShadowingNames, + IReadOnlySet unresolvableRootNames, + IReadOnlySet declaredTypeFullNames, + IReadOnlySet importedDeclaredTypeFullNames, + IReadOnlyCollection knownNamespaces, ImmutableArray.Builder diagnostics) { - var formatter = DeclarationFormatter(prepared.Namespace, options, contextualUsings); - if (prepared.Type.Kind == "delegate") - return new RenderedFragment(RenderDelegate(prepared, formatter, indent)); - - var propertyFormatter = DeclarationFormatter( + var inScopeShadowingNames = inheritedShadowingNames.ToHashSet(StringComparer.Ordinal); + inScopeShadowingNames.UnionWith(prepared.Type.TypeParameters.Select( + parameter => parameter.Name)); + inScopeShadowingNames.UnionWith(prepared.NestedTypes.Select( + nested => CSharpFormatter.StripArity(nested.Type.Name))); + var formatter = DeclarationFormatter( prepared.Namespace, options, contextualUsings, - omitPropertyAccessors: true); + inScopeShadowingNames, + inheritedRootShadowingNames, + unresolvableRootNames, + declaredTypeFullNames, + importedDeclaredTypeFullNames, + knownNamespaces); var diagnosticPass = DeclarationFormatter( prepared.Namespace, options, contextualUsings, + inScopeShadowingNames, + inheritedRootShadowingNames, + unresolvableRootNames, + declaredTypeFullNames, + importedDeclaredTypeFullNames, + knownNamespaces, terminateMemberDeclaration: true) - .FormatTypeUnit(prepared.Type, prepared.Type.Members); + .FormatTypeUnit( + prepared.Type, + prepared.Members.Select(member => member.Member), + prepared.PrimaryConstructorParameters); diagnostics.AddRange(diagnosticPass.Diagnostics.Select( diagnostic => new CSharpTypePrintDiagnostic(prepared.Type.FullName, diagnostic))); + if (prepared.Type.Kind == "delegate") + return new RenderedFragment(RenderDelegate(prepared, formatter, indent)); + var propertyFormatter = DeclarationFormatter( + prepared.Namespace, + options, + contextualUsings, + inScopeShadowingNames, + inheritedRootShadowingNames, + unresolvableRootNames, + declaredTypeFullNames, + importedDeclaredTypeFullNames, + knownNamespaces, + omitPropertyAccessors: true); string pad = new(' ', indent * 4); string declaration = formatter.FormatTypeDeclaration( prepared.Type, @@ -357,7 +531,23 @@ static RenderedFragment RenderType( foreach (var member in prepared.Members) fragments.Add(RenderMember(prepared, member, formatter, propertyFormatter, indent + 1)); foreach (var nested in prepared.NestedTypes) - fragments.Add(RenderType(nested, indent + 1, options, contextualUsings, diagnostics)); + { + var nestedShadowingNames = inScopeShadowingNames.ToHashSet(StringComparer.Ordinal); + var nestedRootShadowingNames = inheritedRootShadowingNames.ToHashSet(StringComparer.Ordinal); + nestedRootShadowingNames.Add(CSharpFormatter.StripArity(prepared.Type.Name)); + fragments.Add(RenderType( + nested, + indent + 1, + options, + contextualUsings, + nestedShadowingNames, + nestedRootShadowingNames, + unresolvableRootNames, + declaredTypeFullNames, + importedDeclaredTypeFullNames, + knownNamespaces, + diagnostics)); + } } fragments.Add(new RenderedFragment($"{pad}}}")); return Join(fragments, "\n"); @@ -444,9 +634,13 @@ static RenderedFragment RenderProperty( { var accessors = new List(); if (body.Getter is not null) - accessors.Add(AccessorHead(member.Member, "get") + ";"); + accessors.Add(AccessorHead(type.Type, member.Member, "get", formatter) + ";"); if (body.Setter is not null) - accessors.Add(AccessorHead(member.Member, SetterKeyword(member.Member)) + ";"); + accessors.Add(AccessorHead( + type.Type, + member.Member, + SetterKeyword(member.Member), + formatter) + ";"); return new RenderedFragment( $"{PadDeclaration(declaration, pad)} {{ {string.Join(" ", accessors)} }}"); } @@ -457,11 +651,20 @@ static RenderedFragment RenderProperty( new($"{pad}{{") }; if (body.Getter is not null) - fragments.Add(RenderAccessor(member.Member, "get", body.Getter, indent + 1)); + { + fragments.Add( + RenderAccessor(type.Type, member.Member, "get", body.Getter, formatter, indent + 1)); + } if (body.Setter is not null) { fragments.Add( - RenderAccessor(member.Member, SetterKeyword(member.Member), body.Setter, indent + 1)); + RenderAccessor( + type.Type, + member.Member, + SetterKeyword(member.Member), + body.Setter, + formatter, + indent + 1)); } fragments.Add(new RenderedFragment($"{pad}}}")); return Join(fragments, "\n"); @@ -498,21 +701,23 @@ static RenderedFragment RenderEvent( { new(PadDeclaration(declaration, pad)), new($"{pad}{{"), - RenderAccessor(member.Member, "add", body.Adder, indent + 1), - RenderAccessor(member.Member, "remove", body.Remover, indent + 1), + RenderAccessor(type.Type, member.Member, "add", body.Adder, formatter, indent + 1), + RenderAccessor(type.Type, member.Member, "remove", body.Remover, formatter, indent + 1), new($"{pad}}}") }; return Join(fragments, "\n"); } static RenderedFragment RenderAccessor( + ApiType declaringType, ApiMember member, string kind, CSharpAccessorBody body, + CSharpFormatter formatter, int indent) { string pad = new(' ', indent * 4); - string head = AccessorHead(member, kind); + string head = AccessorHead(declaringType, member, kind, formatter); if (body.Kind == CSharpAccessorBodyKind.Auto) return new RenderedFragment($"{pad}{head};"); @@ -523,13 +728,38 @@ static RenderedFragment RenderAccessor( return block.Wrap($"{pad}{head}\n", ""); } - static string AccessorHead(ApiMember member, string kind) + static string AccessorHead( + ApiType declaringType, + ApiMember member, + string kind, + CSharpFormatter formatter) { var accessor = member.SignatureModel?.Accessors .FirstOrDefault(candidate => candidate.Kind == kind); var parts = new List(); if (accessor?.ReturnAttributes is { Count: > 0 } returnAttributes) - parts.Add($"[return: {string.Join(", ", returnAttributes)}]"); + { + var attributeProbe = new ApiMember + { + Name = "__AccessorAttributeProbe", + Kind = "method", + SignatureModel = new ApiSignature + { + ReturnType = "void", + MemberName = "__AccessorAttributeProbe", + ReturnAttributes = returnAttributes + } + }; + string formattedProbe = formatter.FormatMember(declaringType, attributeProbe); + attributeProbe.SignatureModel.ReturnAttributes = []; + string formattedDeclaration = formatter.FormatMember(declaringType, attributeProbe); + if (!formattedProbe.EndsWith(formattedDeclaration, StringComparison.Ordinal)) + { + throw new InvalidOperationException( + $"C# accessor '{member.Name}.{kind}' return attributes were not rendered."); + } + parts.Add(formattedProbe[..^formattedDeclaration.Length].TrimEnd()); + } if (!string.IsNullOrWhiteSpace(accessor?.Accessibility)) parts.Add(accessor.Accessibility!); parts.Add(kind); @@ -556,6 +786,12 @@ static CSharpFormatter DeclarationFormatter( string containingNamespace, CSharpTypePrintOptions options, IReadOnlyList contextualUsings, + IReadOnlyCollection additionalShadowingNames, + IReadOnlyCollection additionalRootShadowingNames, + IReadOnlyCollection additionalUnresolvableRootNames, + IReadOnlyCollection additionalDeclaredTypeFullNames, + IReadOnlyCollection additionalImportedDeclaredTypeFullNames, + IReadOnlyCollection additionalKnownNamespaces, bool omitPropertyAccessors = false, bool terminateMemberDeclaration = false) => new(new CSharpFormatOptions @@ -565,6 +801,12 @@ static CSharpFormatter DeclarationFormatter( : CSharpTypeNamePolicy.ContextualShort, ContainingNamespace = containingNamespace.Length == 0 ? null : containingNamespace, Usings = contextualUsings, + AdditionalShadowingNames = additionalShadowingNames, + AdditionalRootShadowingNames = additionalRootShadowingNames, + AdditionalUnresolvableRootNames = additionalUnresolvableRootNames, + AdditionalDeclaredTypeFullNames = additionalDeclaredTypeFullNames, + AdditionalImportedDeclaredTypeFullNames = additionalImportedDeclaredTypeFullNames, + AdditionalKnownNamespaces = additionalKnownNamespaces, NamespacePolicy = CSharpNamespacePolicy.Omit, IncludeCustomAttributes = options.IncludeCustomAttributes, OmitPropertyAccessors = omitPropertyAccessors, diff --git a/src/ILInspector.Decompiler.Tests/ReturnToSenderPrototypeTests.cs b/src/ILInspector.Decompiler.Tests/ReturnToSenderPrototypeTests.cs index f00e5ee43e..0faf666aed 100644 --- a/src/ILInspector.Decompiler.Tests/ReturnToSenderPrototypeTests.cs +++ b/src/ILInspector.Decompiler.Tests/ReturnToSenderPrototypeTests.cs @@ -6763,6 +6763,46 @@ public void CSharpTypePrinter_PrimaryConstructorParametersPrecedeGenericConstrai Assert.Contains("public class Class1(string message) where T : class", Assert.Single(result.Units).Source); } + [Fact] + public void CompileBackTargets_LexicallyShadowedNamespaceRootUsesGlobalAlias() + { + var assemblyPath = CompileFixture(""" + namespace Alpha.Beta + { + public class Thing + { + } + } + + public class Worker + { + public global::Alpha.Beta.Thing GetThing() + { + return null; + } + } + """); + try + { + var result = Assert.Single(ReturnToSender.CompileBackTargets( + assemblyPath, + [new ReturnToSender.RequestedTarget("Worker`2", "GetThing", 0)])); + + Assert.True( + result.Status == FidelityCheck.CompileBackStatus.Exact, + $"{result.Status}: {result.Detail}{Environment.NewLine}{result.Source}"); + Assert.False(result.UsedCompileBackFloor, result.Detail); + Assert.Contains( + "public global::Alpha.Beta.Thing GetThing()", + result.Source, + StringComparison.Ordinal); + } + finally + { + DeleteFixture(assemblyPath); + } + } + [Fact] public void CompileBackTargets_RoundTripsAutoPropertySetter() { diff --git a/tools/DecompilerHarness/ReturnToSenderTypePlanner.cs b/tools/DecompilerHarness/ReturnToSenderTypePlanner.cs index 3ea184b471..48dd93e874 100644 --- a/tools/DecompilerHarness/ReturnToSenderTypePlanner.cs +++ b/tools/DecompilerHarness/ReturnToSenderTypePlanner.cs @@ -604,7 +604,7 @@ static CompileBackSourceResult ApplyFullBodies( Diagnostics = diagnostics, }; evidence = rows; - return new CompileBackSourceResult(plan, ComposeCompilationUnit(plan)); + return ComposeCompilationUnit(plan); CSharpTypePrintRequest Enrich(CSharpTypePrintRequest request) { @@ -1190,7 +1190,7 @@ public static CompileBackSourceResult ComposePropertyGetter( production.Requirements, declarations, diagnostics); - return new CompileBackSourceResult(plan, ComposeCompilationUnit(plan)); + return ComposeCompilationUnit(plan); } static void AddRequiredMembers( @@ -2625,7 +2625,7 @@ public static CompileBackSourceResult ComposePropertySetter( production.Requirements, declarations, diagnostics); - return new CompileBackSourceResult(plan, ComposeCompilationUnit(plan)); + return ComposeCompilationUnit(plan); } public static CompileBackSourceResult ComposeEventAccessor( @@ -2754,7 +2754,7 @@ [new CompileBackFact("metadata", "target-event-accessor", reader.GetString(acces production.Requirements, production.Requests, diagnostics); - return new CompileBackSourceResult(plan, ComposeCompilationUnit(plan)); + return ComposeCompilationUnit(plan); } internal static CompileBackSourceResult ComposeMethod( @@ -3039,11 +3039,13 @@ chainParameterTypes is { } chainParams production.Requirements, declarations, diagnostics); - return new CompileBackSourceResult(plan, ComposeCompilationUnit(plan)); + return ComposeCompilationUnit(plan); } - static CSharpSourceArtifact ComposeCompilationUnit(CompileBackReconstructionPlan plan) - => new CSharpTypePrinter().PrintBatch( + static CompileBackSourceResult ComposeCompilationUnit(CompileBackReconstructionPlan plan) + { + const string typeNamePlanningLayer = "type name planning"; + var rendered = new CSharpTypePrinter().PrintBatch( plan.PrintRequests, new CSharpTypePrintOptions { @@ -3052,7 +3054,23 @@ static CSharpSourceArtifact ComposeCompilationUnit(CompileBackReconstructionPlan AssemblyAttributes = plan.Module.AssemblyAttributes.Select(attribute => attribute.Text).ToArray(), ModuleAttributes = plan.Module.ModuleAttributes.Select(attribute => attribute.Text).ToArray(), Usings = plan.Module.Usings, - }).SourceArtifact; + }); + var enrichedPlan = plan with + { + Diagnostics = plan.Diagnostics + .Where(diagnostic => diagnostic.Layer != typeNamePlanningLayer) + .Concat(rendered.Diagnostics + .Where(diagnostic => diagnostic.Message.Contains( + "conflicts with global type '", + StringComparison.Ordinal)) + .Select(diagnostic => new CompileBackPlanningDiagnostic( + typeNamePlanningLayer, + "unresolvable namespace root", + $"{diagnostic.TypeName}: {diagnostic.Message}"))) + .ToArray() + }; + return new CompileBackSourceResult(enrichedPlan, rendered.SourceArtifact); + } static ApiMember ToApiMember(CompileBackMemberRequirement member) { diff --git a/tools/DecompilerHarness/corpus/two-row-authored-corpus.jsonl b/tools/DecompilerHarness/corpus/two-row-authored-corpus.jsonl index b5c6a7f643..4bd43be0e8 100644 --- a/tools/DecompilerHarness/corpus/two-row-authored-corpus.jsonl +++ b/tools/DecompilerHarness/corpus/two-row-authored-corpus.jsonl @@ -1,2 +1,2 @@ {"assembly":"ILInspector.CSharp","assemblyVersion":"1.0.0.0","tfm":"release","type":"ILInspector.CSharp.CSharpDeclarationWriter","method":"RenderMemberUnit","overload":0,"signature":"\u00600(ApiType,ApiMember,CSharpDeclarationOptions,IReadOnlyList\u003Cstring\u003E)","metadataToken":100663442,"parameterCount":4,"ilSize":105,"sourceUrl":"https://raw.githubusercontent.com/richlander/dotnet-inspect/3818808646ce072da59faee897f83f1966061a44/src/ILInspector.CSharp/CSharpDeclarationWriter.cs","checksumAlgorithm":"SHA256","checksum":"97A66F7359EB8307DEBCCC7F0BD0D9B6E3D496206758EF4A061113F9675F27E2","authoredBody":"options ??= new CSharpDeclarationOptions();\n\n var references = CollectMemberTypeReferences(member);\n\n var plan = TypeNamePlan.Create(references, options);\n\n var declaration = RenderMemberDeclarationCore(type, member, options, methodParameters);\n\n declaration = plan.Apply(declaration);\n\n\n if (options.TerminateMemberDeclaration \u0026\u0026 NeedsTerminator(declaration))\n declaration \u002B= \u0022;\u0022;\n\n\n var source = ComposeUnit([declaration], plan.GeneratedUsings, options);\n\n return new CSharpRenderedDeclaration(source, plan.GeneratedUsings, plan.Diagnostics);"} -{"assembly":"ILInspector.CSharp","assemblyVersion":"1.0.0.0","tfm":"release","type":"ILInspector.CSharp.CSharpFormatter","method":"FormatMember","overload":0,"signature":"\u00600(ApiType,ApiMember,IReadOnlyList\u003Cstring\u003E)","metadataToken":100663583,"parameterCount":3,"ilSize":37,"sourceUrl":"https://raw.githubusercontent.com/richlander/dotnet-inspect/3818808646ce072da59faee897f83f1966061a44/src/ILInspector.CSharp/CSharpFormatter.cs","checksumAlgorithm":"SHA256","checksum":"1766A183E738F46A36DB8DF4A4502B23B9AEADCB3734638A5779CB8878829E84","authoredBody":"ArgumentNullException.ThrowIfNull(type);\n\n ArgumentNullException.ThrowIfNull(member);\n\n return CSharpDeclarationWriter.RenderMemberDeclaration(\n type,\n member,\n _declarationOptions,\n methodParameters);"} +{"assembly":"ILInspector.CSharp","assemblyVersion":"1.0.0.0","tfm":"release","type":"ILInspector.CSharp.CSharpFormatter","method":"FormatMember","overload":0,"signature":"\u00600(ApiType,ApiMember,IReadOnlyList\u003Cstring\u003E)","metadataToken":100663613,"parameterCount":3,"ilSize":37,"sourceUrl":"https://raw.githubusercontent.com/richlander/dotnet-inspect/3818808646ce072da59faee897f83f1966061a44/src/ILInspector.CSharp/CSharpFormatter.cs","checksumAlgorithm":"SHA256","checksum":"1766A183E738F46A36DB8DF4A4502B23B9AEADCB3734638A5779CB8878829E84","authoredBody":"ArgumentNullException.ThrowIfNull(type);\n\n ArgumentNullException.ThrowIfNull(member);\n\n return CSharpDeclarationWriter.RenderMemberDeclaration(\n type,\n member,\n _declarationOptions,\n methodParameters);"}