diff --git a/CodeConverter/CSharp/CommonConversions.cs b/CodeConverter/CSharp/CommonConversions.cs index 6647937f..8af2c712 100644 --- a/CodeConverter/CSharp/CommonConversions.cs +++ b/CodeConverter/CSharp/CommonConversions.cs @@ -664,11 +664,28 @@ public bool HasOutAttribute(VBSyntax.AttributeListSyntax a) public bool IsExtensionAttribute(VBSyntax.AttributeSyntax a) { + if (a.SyntaxTree != SemanticModel.SyntaxTree) + return AttributeNameMatches(a, "Extension"); return (SemanticModel.GetTypeInfo(a).ConvertedType?.GetFullMetadataName()) ?.Equals(ExtensionAttributeType.FullName, StringComparison.Ordinal) == true; } - public bool IsOutAttribute(VBSyntax.AttributeSyntax a) => SemanticModel.GetTypeInfo(a).ConvertedType.IsOutAttribute(); + public bool IsOutAttribute(VBSyntax.AttributeSyntax a) + { + if (a.SyntaxTree != SemanticModel.SyntaxTree) + return AttributeNameMatches(a, "Out"); + return SemanticModel.GetTypeInfo(a).ConvertedType.IsOutAttribute(); + } + + // SemanticModel.GetTypeInfo throws when the node is not in its syntax tree; fall back to name matching. + private static bool AttributeNameMatches(VBSyntax.AttributeSyntax a, string shortName) + { + var name = a.Name.ToString(); + return name.Equals(shortName, StringComparison.Ordinal) || + name.Equals(shortName + "Attribute", StringComparison.Ordinal) || + name.EndsWith("." + shortName, StringComparison.Ordinal) || + name.EndsWith("." + shortName + "Attribute", StringComparison.Ordinal); + } public ISymbol GetCsOriginalSymbolOrNull(ISymbol symbol) { diff --git a/Tests/CSharp/ExpressionTests/ByRefTests.cs b/Tests/CSharp/ExpressionTests/ByRefTests.cs index a5badfd3..2d0be02d 100644 --- a/Tests/CSharp/ExpressionTests/ByRefTests.cs +++ b/Tests/CSharp/ExpressionTests/ByRefTests.cs @@ -1,4 +1,6 @@ using System.Threading.Tasks; +using ICSharpCode.CodeConverter.Common; +using ICSharpCode.CodeConverter.CSharp; using ICSharpCode.CodeConverter.Tests.TestRunners; using Xunit; @@ -914,4 +916,49 @@ public static void LogAndReset(ref int arg) }"); } + [Fact] + public async Task Issue1225_OutAttributeOnParameterFromOtherFileDoesNotCrashAsync() + { + // Issue #1225: IsOutAttribute called SemanticModel.GetTypeInfo(attribute) on an attribute node + // that belongs to a different syntax tree (parameter declared in a separate source file). + // This caused ArgumentException "Knoten ist nicht innerhalb Syntaxbaum" / + // "Node is not within syntax tree". The fix falls back to name-based checking + // when the attribute's syntax tree differs from the current semantic model's tree. + var serviceFileContent = @"Imports System.Runtime.InteropServices +Public Class LicenseService + Public Shared ReadOnly Property Instance As LicenseService + Get + Return New LicenseService() + End Get + End Property + + Public Function GetLicenseMaybe( ByRef licenseName As String) As Boolean + licenseName = Nothing + Return False + End Function +End Class"; + + var callerFileContent = @"Public Class Caller + Public Sub Test(licenseName As String) + Dim res = LicenseService.Instance.GetLicenseMaybe(licenseName) + End Sub +End Class"; + + var options = new TextConversionOptions(DefaultReferences.With()) { ShowCompilationErrors = true }; + var languageConversion = new VBToCSConversion { ConversionOptions = options }; + var serviceTree = languageConversion.CreateTree(serviceFileContent); + var callerTree = languageConversion.CreateTree(callerFileContent); + + // Add both files to the same project so that the VB compilation sees both + var serviceDoc = await languageConversion.CreateProjectDocumentFromTreeAsync(serviceTree, options.References); + var callerDoc = serviceDoc.Project.AddDocumentFromTree(callerTree); + + // Convert only the caller document; its parameter info comes from the service file's syntax tree + var result = await ProjectConversion.ConvertSingleAsync(callerDoc, options); + var output = (result.ConvertedCode ?? "") + (result.GetExceptionsAsString() ?? ""); + + Assert.DoesNotContain("#error", output); + Assert.Contains("GetLicenseMaybe", output); + } + } \ No newline at end of file