diff --git a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/CompilationContext.cs b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/CompilationContext.cs index 8cf1dcf27ab90..f54617764836b 100644 --- a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/CompilationContext.cs +++ b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/CompilationContext.cs @@ -60,11 +60,17 @@ internal CompilationContext( // added (anonymous types, for instance). Debug.Assert(Compilation != compilation); + // Binder.IsInScopeOfAssociatedSyntaxTree() expects a non-null AssociatedFileIdentifier when + // looking up file-local types. If there is no document name, use an invalid FilePathChecksumOpt. + FileIdentifier fileIdentifier = methodDebugInfo.ContainingDocumentName is { } documentName + ? FileIdentifier.Create(documentName) + : new FileIdentifier { EncoderFallbackErrorMessage = null, FilePathChecksumOpt = ImmutableArray.Empty, DisplayFilePath = string.Empty }; + NamespaceBinder = CreateBinderChain( Compilation, currentFrame.ContainingNamespace, methodDebugInfo.ImportRecordGroups, - methodDebugInfo.ContainingDocumentName is { } documentName ? FileIdentifier.Create(documentName) : null); + fileIdentifier: fileIdentifier); if (_methodNotType) { diff --git a/src/ExpressionEvaluator/CSharp/Test/ExpressionCompiler/CompileExpressionsTests.cs b/src/ExpressionEvaluator/CSharp/Test/ExpressionCompiler/CompileExpressionsTests.cs index 193c1a64371a3..db0a911f072d2 100644 --- a/src/ExpressionEvaluator/CSharp/Test/ExpressionCompiler/CompileExpressionsTests.cs +++ b/src/ExpressionEvaluator/CSharp/Test/ExpressionCompiler/CompileExpressionsTests.cs @@ -8,8 +8,12 @@ using System.Text; using Microsoft.CodeAnalysis.CodeGen; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.ExpressionEvaluator; using Microsoft.CodeAnalysis.ExpressionEvaluator.UnitTests; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.VisualStudio.Debugger.Evaluation; using Roslyn.Test.Utilities; using Xunit; @@ -735,16 +739,24 @@ public static void F() "C.X", out var error, testData); - Assert.NotNull(result.Assembly); - Assert.Null(error); - testData.GetMethodData("<>x.<>m0").VerifyIL(""" - { - // Code size 6 (0x6) - .maxstack 1 - IL_0000: ldsfld "int C.X" - IL_0005: ret - } - """); + if (runtime.DebugFormat == DebugInformationFormat.Pdb) + { + Assert.Null(result); + Assert.Equal("error CS0103: The name 'C' does not exist in the current context", error); + } + else + { + Assert.NotNull(result.Assembly); + Assert.Null(error); + testData.GetMethodData("<>x.<>m0").VerifyIL(""" + { + // Code size 6 (0x6) + .maxstack 1 + IL_0000: ldsfld "int C.X" + IL_0005: ret + } + """); + } }); } @@ -789,16 +801,24 @@ public static void F() "C.X", out var error, testData); - Assert.Null(error); - Assert.NotNull(result.Assembly); - testData.GetMethodData("<>x.<>m0").VerifyIL(""" + if (runtime.DebugFormat == DebugInformationFormat.Pdb) { - // Code size 6 (0x6) - .maxstack 1 - IL_0000: ldsfld "int C.X" - IL_0005: ret + Assert.Null(result); + Assert.Equal("error CS0103: The name 'C' does not exist in the current context", error); + } + else + { + Assert.Null(error); + Assert.NotNull(result.Assembly); + testData.GetMethodData("<>x.<>m0").VerifyIL(""" + { + // Code size 6 (0x6) + .maxstack 1 + IL_0000: ldsfld "int C.X" + IL_0005: ret + } + """); } - """); }); } @@ -849,16 +869,24 @@ public class Inner "Outer.Inner.X", out var error, testData); - Assert.Null(error); - Assert.NotNull(result.Assembly); - testData.GetMethodData("<>x.<>m0").VerifyIL(""" - { - // Code size 6 (0x6) - .maxstack 1 - IL_0000: ldsfld "int Outer.Inner.X" - IL_0005: ret - } - """); + if (runtime.DebugFormat == DebugInformationFormat.Pdb) + { + Assert.Null(result); + Assert.Equal("error CS0103: The name 'Outer' does not exist in the current context", error); + } + else + { + Assert.Null(error); + Assert.NotNull(result.Assembly); + testData.GetMethodData("<>x.<>m0").VerifyIL(""" + { + // Code size 6 (0x6) + .maxstack 1 + IL_0000: ldsfld "int Outer.Inner.X" + IL_0005: ret + } + """); + } }); } @@ -903,6 +931,211 @@ public static void F() }); } + [Fact] + public void FileLocalType_05() + { + var source = +@"file class C +{ +} + +class Program +{ + static void Main() + { + } +}"; + var comp = CreateCompilation(SyntaxFactory.ParseSyntaxTree(source, options: TestOptions.Regular11, path: "path/to/MyFile.cs", Encoding.Default), options: TestOptions.DebugDll); + WithRuntimeInstance( + comp, + references: null, + includeLocalSignatures: true, + includeIntrinsicAssembly: false, + validator: runtime => + { + var context = CreateMethodContext(runtime, "Program.Main"); + var testData = new CompilationTestData(); + var result = context.CompileExpression( + "new C()", + out var error, + testData); + if (runtime.DebugFormat == DebugInformationFormat.Pdb) + { + Assert.Null(result); + Assert.Equal("error CS0246: The type or namespace name 'C' could not be found (are you missing a using directive or an assembly reference?)", error); + } + else + { + Assert.NotNull(result.Assembly); + Assert.Null(error); + testData.GetMethodData("<>x.<>m0").VerifyIL(""" + { + // Code size 6 (0x6) + .maxstack 1 + IL_0000: newobj "C..ctor()" + IL_0005: ret + } + """); + } + }); + } + + [Fact] + public void FileLocalType_06() + { + var source = +@"file class C +{ + int F = 1; + void M() + { + } +}"; + var comp = CreateCompilation(SyntaxFactory.ParseSyntaxTree(source, options: TestOptions.Regular11, path: "path/to/MyFile.cs", Encoding.Default), options: TestOptions.DebugDll); + WithRuntimeInstance( + comp, + references: null, + includeLocalSignatures: true, + includeIntrinsicAssembly: false, + validator: runtime => + { + var context = CreateMethodContext(runtime, "C.M"); + var testData = new CompilationTestData(); + var result = context.CompileExpression( + "F", + out var error, + testData); + if (runtime.DebugFormat == DebugInformationFormat.Pdb) + { + Assert.Null(result); + Assert.Equal("error CS0103: The name 'F' does not exist in the current context", error); + } + else + { + Assert.NotNull(result.Assembly); + Assert.Null(error); + testData.GetMethodData("<>x.<>m0").VerifyIL(""" + { + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: ldfld "int C.F" + IL_0006: ret + } + """); + } + }); + } + + [WorkItem(66109, "https://github.com/dotnet/roslyn/issues/66109")] + [WorkItem(64098, "https://github.com/dotnet/roslyn/issues/64098")] + [Fact] + public void FileLocalType_07() + { + var sourceA = """ + file class A + { + public int F1() => 1; + public int F2() => 2; + } + class Program + { + static void M1() + { + A x = new A(); + } + static void M2() + { + #line 100 "B.cs" + A y = new A(); + #line 200 "C.cs" + A z = new A(); + } + } + """; + var sourceB = """ + class B + { + } + """; + var sourceC = """ + class C + { + } + """; + var comp = CreateCompilation( + new[] + { + SyntaxFactory.ParseSyntaxTree(sourceA, path: "path/to/A.cs", encoding: Encoding.Default), + SyntaxFactory.ParseSyntaxTree(sourceB, path: "path/to/B.cs", encoding: Encoding.Default), + SyntaxFactory.ParseSyntaxTree(sourceC, path: "path/to/C.cs", encoding: Encoding.Default), + }, + options: TestOptions.DebugDll); + + WithRuntimeInstance( + comp, + references: null, + includeLocalSignatures: true, + includeIntrinsicAssembly: false, + validator: runtime => + { + var context = CreateMethodContext(runtime, "Program.M1"); + var testData = new CompilationTestData(); + ResultProperties resultProperties; + string error; + ImmutableArray missingAssemblyIdentities; + var result = context.CompileExpression( + "x.F1() + (new A()).F2()", + DkmEvaluationFlags.TreatAsExpression, + NoAliases, + DebuggerDiagnosticFormatter.Instance, + out resultProperties, + out error, + out missingAssemblyIdentities, + EnsureEnglishUICulture.PreferredOrNull, + testData); + if (runtime.DebugFormat == DebugInformationFormat.Pdb) + { + Assert.Null(result); + Assert.Equal("error CS1061: 'A' does not contain a definition for 'F1' and no accessible extension method 'F1' accepting a first argument of type 'A' could be found (are you missing a using directive or an assembly reference?)", error); + } + else + { + Assert.NotNull(result.Assembly); + Assert.Null(error); + testData.GetMethodData("<>x.<>m0").VerifyIL(""" + { + // Code size 18 (0x12) + .maxstack 2 + .locals init (A V_0) //x + IL_0000: ldloc.0 + IL_0001: callvirt "int A.F1()" + IL_0006: newobj "A..ctor()" + IL_000b: call "int A.F2()" + IL_0010: add + IL_0011: ret + } + """); + } + + // https://github.com/dotnet/roslyn/issues/64098: EE doesn't handle methods that span multiple documents correctly + context = CreateMethodContext(runtime, "Program.M2"); + testData = new CompilationTestData(); + result = context.CompileExpression( + "y.F1() + (new A()).F2()", + DkmEvaluationFlags.TreatAsExpression, + NoAliases, + DebuggerDiagnosticFormatter.Instance, + out resultProperties, + out error, + out missingAssemblyIdentities, + EnsureEnglishUICulture.PreferredOrNull, + testData); + Assert.Null(result); + Assert.Equal("error CS1061: 'A' does not contain a definition for 'F1' and no accessible extension method 'F1' accepting a first argument of type 'A' could be found (are you missing a using directive or an assembly reference?)", error); + }); + } + [Fact] public void IllFormedFilePath_01() { @@ -944,5 +1177,365 @@ .maxstack 1 """); }); } + + [WorkItem(66109, "https://github.com/dotnet/roslyn/issues/66109")] + [Fact] + public void SequencePointsMultipleDocuments_01() + { + var sourceA = +@"partial class Program +{ + private int x = 1; + private void F() + { + } +}"; + var sourceB = +@"partial class Program +{ + private int y = 2; + public Program() + { + F(); + int z = x + y; + } +}"; + var comp = CreateCompilation( + new[] + { + SyntaxFactory.ParseSyntaxTree(sourceA, path: "A.cs", encoding: Encoding.Default), + SyntaxFactory.ParseSyntaxTree(sourceB, path: "B.cs", encoding: Encoding.Default) + }, + options: TestOptions.DebugDll); + + comp.VerifyPdb(""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """, + format: Microsoft.CodeAnalysis.Emit.DebugInformationFormat.Pdb); + comp.VerifyPdb(""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """, + format: Microsoft.CodeAnalysis.Emit.DebugInformationFormat.PortablePdb); + + WithRuntimeInstance( + comp, + references: null, + includeLocalSignatures: true, + includeIntrinsicAssembly: false, + validator: runtime => + { + GetContextState(runtime, "Program..ctor", out var blocks, out var moduleVersionId, out var symReader, out var methodToken, out var localSignatureToken); + + var appDomain = new AppDomain(); + uint ilOffset = ExpressionCompilerTestHelpers.GetOffset(methodToken, symReader); + var context = CreateMethodContext( + appDomain, + blocks, + symReader, + moduleVersionId, + methodToken: methodToken, + methodVersion: 1, + ilOffset: 0x15, // offset matches startOffset of "z" scope + localSignatureToken: localSignatureToken, + kind: MakeAssemblyReferencesKind.AllAssemblies); + var testData = new CompilationTestData(); + var result = context.CompileExpression( + "z", + out var error, + testData); + Assert.Null(error); + Assert.NotNull(result.Assembly); + testData.GetMethodData("<>x.<>m0").VerifyIL(""" + { + // Code size 2 (0x2) + .maxstack 1 + .locals init (int V_0) //z + IL_0000: ldloc.0 + IL_0001: ret + } + """); + + testData = new CompilationTestData(); + var locals = ArrayBuilder.GetInstance(); + string typeName; + var assembly = context.CompileGetLocals(locals, argumentsOnly: false, typeName: out typeName, testData: testData); + Assert.Equal(2, locals.Count); + VerifyLocal(testData, typeName, locals[0], "<>m0", "this", expectedILOpt: """ + { + // Code size 2 (0x2) + .maxstack 1 + .locals init (int V_0) //z + IL_0000: ldarg.0 + IL_0001: ret + } + """); + VerifyLocal(testData, typeName, locals[1], "<>m1", "z", expectedILOpt: """ + { + // Code size 2 (0x2) + .maxstack 1 + .locals init (int V_0) //z + IL_0000: ldloc.0 + IL_0001: ret + } + """); + locals.Free(); + }); + } + + [WorkItem(66109, "https://github.com/dotnet/roslyn/issues/66109")] + [Fact] + public void SequencePointsMultipleDocuments_02() + { + var sourceA = """ + class A + { + static void Main() + { + int x = 1; + #line 100 "B.cs" + int y = 2; + #line 200 "C.cs" + int z = 3; + } + } + """; + var sourceB = """ + class B + { + } + """; + var sourceC = """ + class C + { + } + """; + var comp = CreateCompilation( + new[] + { + SyntaxFactory.ParseSyntaxTree(sourceA, path: "A.cs", encoding: Encoding.Default), + SyntaxFactory.ParseSyntaxTree(sourceB, path: "B.cs", encoding: Encoding.Default), + SyntaxFactory.ParseSyntaxTree(sourceC, path: "C.cs", encoding: Encoding.Default), + }, + options: TestOptions.DebugDll); + + comp.VerifyPdb(""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """, + format: Microsoft.CodeAnalysis.Emit.DebugInformationFormat.Pdb); + comp.VerifyPdb(""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """, + format: Microsoft.CodeAnalysis.Emit.DebugInformationFormat.PortablePdb); + + WithRuntimeInstance( + comp, + references: null, + includeLocalSignatures: true, + includeIntrinsicAssembly: false, + validator: runtime => + { + var context = CreateMethodContext(runtime, "A.Main"); + var testData = new CompilationTestData(); + var result = context.CompileExpression( + "x + y + z", + out var error, + testData); + Assert.Null(error); + Assert.NotNull(result.Assembly); + testData.GetMethodData("<>x.<>m0").VerifyIL(""" + { + // Code size 6 (0x6) + .maxstack 2 + .locals init (int V_0, //x + int V_1, //y + int V_2) //z + IL_0000: ldloc.0 + IL_0001: ldloc.1 + IL_0002: add + IL_0003: ldloc.2 + IL_0004: add + IL_0005: ret + } + """); + + testData = new CompilationTestData(); + var locals = ArrayBuilder.GetInstance(); + string typeName; + var assembly = context.CompileGetLocals(locals, argumentsOnly: false, typeName: out typeName, testData: testData); + Assert.Equal(3, locals.Count); + VerifyLocal(testData, typeName, locals[0], "<>m0", "x", expectedILOpt: """ + { + // Code size 2 (0x2) + .maxstack 1 + .locals init (int V_0, //x + int V_1, //y + int V_2) //z + IL_0000: ldloc.0 + IL_0001: ret + } + """); + VerifyLocal(testData, typeName, locals[1], "<>m1", "y", expectedILOpt: """ + { + // Code size 2 (0x2) + .maxstack 1 + .locals init (int V_0, //x + int V_1, //y + int V_2) //z + IL_0000: ldloc.1 + IL_0001: ret + } + """); + VerifyLocal(testData, typeName, locals[2], "<>m2", "z", expectedILOpt: """ + { + // Code size 2 (0x2) + .maxstack 1 + .locals init (int V_0, //x + int V_1, //y + int V_2) //z + IL_0000: ldloc.2 + IL_0001: ret + } + """); + locals.Free(); + }); + } } } diff --git a/src/ExpressionEvaluator/Core/Source/ExpressionCompiler/PDB/MethodDebugInfo.Native.cs b/src/ExpressionEvaluator/Core/Source/ExpressionCompiler/PDB/MethodDebugInfo.Native.cs index 5fbdff49cfe5c..2176c12f4c293 100644 --- a/src/ExpressionEvaluator/Core/Source/ExpressionCompiler/PDB/MethodDebugInfo.Native.cs +++ b/src/ExpressionEvaluator/Core/Source/ExpressionCompiler/PDB/MethodDebugInfo.Native.cs @@ -59,7 +59,7 @@ public override int GetHashCode() public static unsafe MethodDebugInfo ReadMethodDebugInfo( ISymUnmanagedReader3? symReader, - EESymbolProvider? symbolProvider, // TODO: only null in DTEE case where we looking for default namesapace + EESymbolProvider? symbolProvider, // TODO: only null in DTEE case where we looking for default namespace int methodToken, int methodVersion, int ilOffset, @@ -170,20 +170,9 @@ public static unsafe MethodDebugInfo ReadMethodDebugI var reuseSpan = GetReuseSpan(allScopes, ilOffset, isVisualBasicMethod); - string? name = null; - if (symReader.GetMethod(methodToken) is ISymEncUnmanagedMethod and ISymUnmanagedMethod methodInfo) - { - // We need a receiver of type `ISymUnmanagedMethod` to call the extension `GetDocumentsForMethod()` here. - // We also need to ensure that the receiver implements `ISymEncUnmanagedMethod` to prevent the extension from throwing. - var doc = methodInfo.GetDocumentsForMethod() switch - { - [var singleDocument] => singleDocument, - var documents => throw ExceptionUtilities.UnexpectedValue(documents) - }; - - name = doc.GetName(); - } - + // containingDocumentName is not set since ISymUnmanagedMethod.GetDocumentsForMethod() + // may fail (see https://github.com/dotnet/roslyn/issues/66260). The result is that + // symbols from file-local types will not bind successfully in the EE. return new MethodDebugInfo( hoistedLocalScopeRecords, importRecordGroups, @@ -194,7 +183,7 @@ public static unsafe MethodDebugInfo ReadMethodDebugI containingScopes.GetLocalNames(), constantsBuilder.ToImmutableAndFree(), reuseSpan, - name); + containingDocumentName: null); } catch (InvalidOperationException) { diff --git a/src/ExpressionEvaluator/Core/Source/ExpressionCompiler/PDB/MethodDebugInfo.Portable.cs b/src/ExpressionEvaluator/Core/Source/ExpressionCompiler/PDB/MethodDebugInfo.Portable.cs index be7c1028b25fb..7d9e683140711 100644 --- a/src/ExpressionEvaluator/Core/Source/ExpressionCompiler/PDB/MethodDebugInfo.Portable.cs +++ b/src/ExpressionEvaluator/Core/Source/ExpressionCompiler/PDB/MethodDebugInfo.Portable.cs @@ -33,7 +33,7 @@ public static MethodDebugInfo ReadFromPortable( var methodHandle = GetDeltaRelativeMethodDefinitionHandle(reader, methodToken); - // TODO: only null in DTEE case where we looking for default namesapace + // TODO: only null in DTEE case where we looking for default namespace if (symbolProvider != null) { ReadLocalScopeInformation( @@ -64,8 +64,12 @@ public static MethodDebugInfo ReadFromPortable( ReadMethodCustomDebugInformation(reader, methodHandle, out var hoistedLocalScopes, out var defaultNamespace); var documentHandle = reader.GetMethodDebugInformation(methodHandle).Document; - var document = reader.GetDocument(documentHandle); - var documentName = reader.GetString(document.Name); + string? documentName = null; + if (!documentHandle.IsNil) + { + var document = reader.GetDocument(documentHandle); + documentName = reader.GetString(document.Name); + } return new MethodDebugInfo( hoistedLocalScopes,