Skip to content

Commit 62c91d6

Browse files
Printing of 'null' as output for null returning inputs. Printing nothing for void returning or no-output inputs.
1 parent 8855650 commit 62c91d6

File tree

13 files changed

+116
-51
lines changed

13 files changed

+116
-51
lines changed

CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis.CSharp.Scripting.Hosting/CSharpObjectFormatterImpl.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
#nullable disable
6-
75
using System.Reflection;
86
using CSharpRepl.Services;
97
using CSharpRepl.Services.SyntaxHighlighting;
108
using Microsoft.CodeAnalysis.Scripting.Hosting;
9+
using PrettyPrompt.Highlighting;
1110
using MemberFilter = Microsoft.CodeAnalysis.Scripting.Hosting.MemberFilter;
1211

1312
namespace Microsoft.CodeAnalysis.CSharp.Scripting.Hosting;
1413

1514
internal class CSharpObjectFormatterImpl : CommonObjectFormatter
1615
{
16+
public FormattedString NullLiteral => PrimitiveFormatter.NullLiteral;
17+
1718
protected override CommonTypeNameFormatter TypeNameFormatter { get; }
1819
protected override CommonPrimitiveFormatter PrimitiveFormatter { get; }
1920
protected override MemberFilter Filter { get; }

CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis.CSharp.Scripting.Hosting/CSharpPrimitiveFormatter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public CSharpPrimitiveFormatter(SyntaxHighlighter syntaxHighlighter)
2626
NullLiteral = new FormattedString("null", keywordFormat);
2727
}
2828

29-
protected override FormattedString NullLiteral { get; }
29+
public override FormattedString NullLiteral { get; }
3030

3131
protected override FormattedString FormatLiteral(bool value)
3232
{

CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis.Scripting.Hosting/CommonPrimitiveFormatter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal abstract partial class CommonPrimitiveFormatter
1717
/// <summary>
1818
/// String that describes "null" literal in the language.
1919
/// </summary>
20-
protected abstract FormattedString NullLiteral { get; }
20+
public abstract FormattedString NullLiteral { get; }
2121

2222
protected abstract FormattedString FormatLiteral(bool value);
2323
protected abstract FormattedString FormatLiteral(string value, bool quote, bool escapeNonPrintable, int numberRadix = NumberRadixDecimal);

CSharpRepl.Services/Roslyn/PrettyPrinter.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ public FormattedString FormatObject(object? obj, bool displayDetails)
3535
{
3636
return obj switch
3737
{
38-
// intercept null, don't print the string "null"
39-
null => null,
38+
null => formatter.NullLiteral,
4039

4140
// when displayDetails is true, don't show the escaped string (i.e. interpret the escape characters, via displaying to console)
4241
string str when displayDetails => str,

CSharpRepl.Services/Roslyn/RoslynServices.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ public RoslynServices(IConsole console, Configuration config, ITraceLogger logge
7979
// the script runner is used to actually execute the scripts, and the workspace manager
8080
// is updated alongside. The workspace is a datamodel used in "editor services" like
8181
// syntax highlighting, autocompletion, and roslyn symbol queries.
82-
this.scriptRunner = new ScriptRunner(compilationOptions, referenceService, console, config);
8382
this.workspaceManager = new WorkspaceManager(compilationOptions, referenceService, logger);
83+
this.scriptRunner = new ScriptRunner(workspaceManager, compilationOptions, referenceService, console, config);
8484

8585
this.disassembler = new Disassembler(compilationOptions, referenceService, scriptRunner);
8686
this.prettyPrinter = new PrettyPrinter(highlighter, config);

CSharpRepl.Services/Roslyn/Scripting/EvaluationResult.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5-
using Microsoft.CodeAnalysis;
65
using System;
76
using System.Collections.Generic;
7+
using Microsoft.CodeAnalysis;
88

99
namespace CSharpRepl.Services.Roslyn.Scripting;
1010

1111
/// <remarks>about as close to a discriminated union as I can get</remarks>
1212
public abstract record EvaluationResult
1313
{
14-
public sealed record Success(string Input, object ReturnValue, IReadOnlyCollection<MetadataReference> References) : EvaluationResult;
14+
public sealed record Success(string Input, Optional<object?> ReturnValue, IReadOnlyCollection<MetadataReference> References) : EvaluationResult;
1515
public sealed record Error(Exception Exception) : EvaluationResult;
1616
public sealed record Cancelled() : EvaluationResult;
1717
}

CSharpRepl.Services/Roslyn/Scripting/ScriptRunner.cs

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5-
using Microsoft.CodeAnalysis.CSharp;
6-
using Microsoft.CodeAnalysis.CSharp.Scripting;
7-
using Microsoft.CodeAnalysis.Scripting;
8-
using Microsoft.CodeAnalysis.Scripting.Hosting;
95
using System;
106
using System.Linq;
11-
using System.Threading.Tasks;
127
using System.Threading;
13-
using PrettyPrompt.Consoles;
8+
using System.Threading.Tasks;
9+
using CSharpRepl.Services.Dotnet;
1410
using CSharpRepl.Services.Roslyn.MetadataResolvers;
1511
using CSharpRepl.Services.Roslyn.References;
1612
using Microsoft.CodeAnalysis;
17-
using CSharpRepl.Services.Dotnet;
13+
using Microsoft.CodeAnalysis.CSharp;
14+
using Microsoft.CodeAnalysis.CSharp.Scripting;
15+
using Microsoft.CodeAnalysis.CSharp.Syntax;
16+
using Microsoft.CodeAnalysis.Scripting;
17+
using Microsoft.CodeAnalysis.Scripting.Hosting;
18+
using Microsoft.CodeAnalysis.Text;
19+
using PrettyPrompt.Consoles;
1820

1921
namespace CSharpRepl.Services.Roslyn.Scripting;
2022

@@ -27,17 +29,20 @@ internal sealed class ScriptRunner
2729
private readonly InteractiveAssemblyLoader assemblyLoader;
2830
private readonly CompositeAlternativeReferenceResolver alternativeReferenceResolver;
2931
private readonly MetadataReferenceResolver metadataResolver;
32+
private readonly WorkspaceManager workspaceManager;
3033
private readonly AssemblyReferenceService referenceAssemblyService;
3134
private ScriptOptions scriptOptions;
3235
private ScriptState<object>? state;
3336

3437
public ScriptRunner(
38+
WorkspaceManager workspaceManager,
3539
CSharpCompilationOptions compilationOptions,
3640
AssemblyReferenceService referenceAssemblyService,
3741
IConsole console,
3842
Configuration configuration)
3943
{
4044
this.console = console;
45+
this.workspaceManager = workspaceManager;
4146
this.referenceAssemblyService = referenceAssemblyService;
4247
this.assemblyLoader = new InteractiveAssemblyLoader(new MetadataShadowCopyProvider());
4348

@@ -79,10 +84,10 @@ public async Task<EvaluationResult> RunCompilation(string text, string[]? args =
7984
var usings = referenceAssemblyService.GetUsings(text);
8085
referenceAssemblyService.TrackUsings(usings);
8186

82-
state = await EvaluateStringWithStateAsync(text, state, assemblyLoader, this.scriptOptions, args, cancellationToken).ConfigureAwait(false);
87+
state = await EvaluateStringWithStateAsync(text, state, assemblyLoader, scriptOptions, args, cancellationToken).ConfigureAwait(false);
8388

8489
return state.Exception is null
85-
? CreateSuccessfulResult(text, state)
90+
? await CreateSuccessfulResult(text, state, cancellationToken).ConfigureAwait(false)
8691
: new EvaluationResult.Error(this.state.Exception);
8792
}
8893
catch (Exception oce) when (oce is OperationCanceledException || oce.InnerException is OperationCanceledException)
@@ -113,27 +118,54 @@ public Compilation CompileTransient(string code, OptimizationLevel optimizationL
113118
);
114119
}
115120

116-
private EvaluationResult.Success CreateSuccessfulResult(string text, ScriptState<object> state)
121+
private async Task<EvaluationResult.Success> CreateSuccessfulResult(string text, ScriptState<object> state, CancellationToken cancellationToken)
117122
{
123+
var hasValueReturningStatement = await HasValueReturningStatement(text, cancellationToken).ConfigureAwait(false);
124+
118125
referenceAssemblyService.AddImplementationAssemblyReferences(state.Script.GetCompilation().References);
119126
var frameworkReferenceAssemblies = referenceAssemblyService.LoadedReferenceAssemblies;
120127
var frameworkImplementationAssemblies = referenceAssemblyService.LoadedImplementationAssemblies;
121128
this.scriptOptions = this.scriptOptions.WithReferences(frameworkImplementationAssemblies);
122-
return new EvaluationResult.Success(text, state.ReturnValue, frameworkImplementationAssemblies.Concat(frameworkReferenceAssemblies).ToList());
129+
var returnValue = hasValueReturningStatement ? new Optional<object?>(state.ReturnValue) : default;
130+
return new EvaluationResult.Success(text, returnValue, frameworkImplementationAssemblies.Concat(frameworkReferenceAssemblies).ToList());
123131
}
124132

125-
private Task<ScriptState<object>> EvaluateStringWithStateAsync(string text, ScriptState<object>? state, InteractiveAssemblyLoader assemblyLoader, ScriptOptions scriptOptions, string[]? args = null, CancellationToken cancellationToken = default)
133+
private async Task<ScriptState<object>> EvaluateStringWithStateAsync(string text, ScriptState<object>? state, InteractiveAssemblyLoader assemblyLoader, ScriptOptions scriptOptions, string[]? args, CancellationToken cancellationToken)
126134
{
127-
return state is null
135+
var scriptTask = state is null
128136
? CSharpScript
129137
.Create(text, scriptOptions, globalsType: typeof(ScriptGlobals), assemblyLoader: assemblyLoader)
130138
.RunAsync(globals: CreateGlobalsObject(args), cancellationToken)
131139
: state
132140
.ContinueWithAsync(text, scriptOptions, cancellationToken);
141+
142+
return await scriptTask.ConfigureAwait(false);
143+
}
144+
145+
private async Task<bool> HasValueReturningStatement(string text, CancellationToken cancellationToken)
146+
{
147+
var sourceText = SourceText.From(text);
148+
var document = workspaceManager.CurrentDocument.WithText(sourceText);
149+
var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
150+
if (tree != null &&
151+
await tree.GetRootAsync(cancellationToken).ConfigureAwait(false) is CompilationUnitSyntax root &&
152+
root.Members.Count > 0 &&
153+
root.Members.Last() is GlobalStatementSyntax { Statement: ExpressionStatementSyntax { SemicolonToken.IsMissing: true } possiblyValueReturningStatement })
154+
{
155+
//now we know the text's last statement does not have semicolon so it can return value
156+
//but the statement's return type still can be void - we need to find out
157+
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
158+
if (semanticModel != null)
159+
{
160+
var typeInfo = semanticModel.GetTypeInfo(possiblyValueReturningStatement.Expression, cancellationToken);
161+
return typeInfo.ConvertedType?.SpecialType != SpecialType.System_Void;
162+
}
163+
}
164+
return false;
133165
}
134166

135167
private ScriptGlobals CreateGlobalsObject(string[]? args)
136168
{
137169
return new ScriptGlobals(console, args ?? Array.Empty<string>());
138170
}
139-
}
171+
}

CSharpRepl.Tests/DisassemblerTests.cs

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,26 @@
1-
using CSharpRepl.Services;
1+
using System;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
using CSharpRepl.Services;
25
using CSharpRepl.Services.Disassembly;
36
using CSharpRepl.Services.Roslyn;
4-
using CSharpRepl.Services.Roslyn.References;
57
using CSharpRepl.Services.Roslyn.Scripting;
68
using Microsoft.CodeAnalysis;
7-
using Microsoft.CodeAnalysis.CSharp;
89
using NSubstitute;
910
using PrettyPrompt.Consoles;
10-
using System;
11-
using System.IO;
12-
using System.Threading.Tasks;
1311
using Xunit;
1412

1513
namespace CSharpRepl.Tests;
1614

1715
[Collection(nameof(RoslynServices))]
1816
public class DisassemblerTests : IAsyncLifetime
1917
{
20-
private readonly Disassembler disassembler;
2118
private readonly RoslynServices services;
2219

2320
public DisassemblerTests()
2421
{
25-
var options = new CSharpCompilationOptions(
26-
OutputKind.DynamicallyLinkedLibrary,
27-
usings: Array.Empty<string>()
28-
);
2922
var console = Substitute.For<IConsole>();
3023
console.BufferWidth.Returns(200);
31-
var config = new Configuration();
32-
var referenceService = new AssemblyReferenceService(config, new TestTraceLogger());
33-
var scriptRunner = new ScriptRunner(options, referenceService, console, config);
34-
35-
this.disassembler = new Disassembler(options, referenceService, scriptRunner);
3624
this.services = new RoslynServices(console, new Configuration(), new TestTraceLogger());
3725
}
3826

@@ -49,7 +37,7 @@ public void Disassemble_InputCSharp_OutputIL(OptimizationLevel optimizationLevel
4937
var input = File.ReadAllText($"./Data/Disassembly/{testCase}.Input.txt").Replace("\r\n", "\n");
5038
var expectedOutput = File.ReadAllText($"./Data/Disassembly/{testCase}.Output.{optimizationLevel}.il").Replace("\r\n", "\n");
5139

52-
var result = disassembler.Disassemble(input, debugMode: optimizationLevel == OptimizationLevel.Debug);
40+
var result = services.ConvertToIntermediateLanguage(input, debugMode: optimizationLevel == OptimizationLevel.Debug).Result;
5341
var actualOutput = Assert
5442
.IsType<EvaluationResult.Success>(result)
5543
.ReturnValue

CSharpRepl.Tests/EvaluationTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public async Task Evaluate_LiteralInteger_ReturnsInteger()
4040
var success = Assert.IsType<EvaluationResult.Success>(result);
4141
Assert.Equal("5", success.Input);
4242

43-
var returnValue = Assert.IsType<int>(success.ReturnValue);
43+
var returnValue = Assert.IsType<int>(success.ReturnValue.Value);
4444
Assert.Equal(5, returnValue);
4545
}
4646

@@ -52,7 +52,7 @@ public async Task Evaluate_Variable_ReturnsValue()
5252

5353
var assignment = Assert.IsType<EvaluationResult.Success>(variableAssignment);
5454
var usage = Assert.IsType<EvaluationResult.Success>(variableUsage);
55-
Assert.Null(assignment.ReturnValue);
55+
Assert.Null(assignment.ReturnValue.Value);
5656
Assert.Equal("Hello Mundo", usage.ReturnValue);
5757
}
5858

@@ -65,7 +65,7 @@ public async Task Evaluate_NugetPackage_InstallsPackage()
6565
var installationResult = Assert.IsType<EvaluationResult.Success>(installation);
6666
var usageResult = Assert.IsType<EvaluationResult.Success>(usage);
6767

68-
Assert.Null(installationResult.ReturnValue);
68+
Assert.Null(installationResult.ReturnValue.Value);
6969
Assert.Contains(installationResult.References, r => r.Display.EndsWith("Newtonsoft.Json.dll"));
7070
Assert.Contains("Adding references for 'Newtonsoft.Json", ProgramTests.RemoveFormatting(stdout.ToString()));
7171
Assert.Equal(@"{""Foo"":""bar""}", usageResult.ReturnValue);
@@ -80,8 +80,8 @@ public async Task Evaluate_NugetPackageVersioned_InstallsPackageVersion()
8080
var installationResult = Assert.IsType<EvaluationResult.Success>(installation);
8181
var usageResult = Assert.IsType<EvaluationResult.Success>(usage);
8282

83-
Assert.Null(installationResult.ReturnValue);
84-
Assert.NotNull(usageResult.ReturnValue);
83+
Assert.Null(installationResult.ReturnValue.Value);
84+
Assert.NotNull(usageResult.ReturnValue.Value);
8585
Assert.Contains("Adding references for 'Microsoft.CodeAnalysis.CSharp.3.11.0'", ProgramTests.RemoveFormatting(stdout.ToString()));
8686
}
8787

@@ -173,7 +173,7 @@ public async Task Evaluate_ResolveCorrectRuntimeVersionOfReferencedAssembly()
173173
Assert.IsType<EvaluationResult.Success>(referenceResult);
174174
Assert.IsType<EvaluationResult.Success>(importResult);
175175

176-
var referencedSystemManagementPath = (string)((EvaluationResult.Success)importResult).ReturnValue;
176+
var referencedSystemManagementPath = (string)((EvaluationResult.Success)importResult).ReturnValue.Value;
177177
referencedSystemManagementPath = referencedSystemManagementPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
178178
var winRuntimeSelected = referencedSystemManagementPath.Contains(Path.Combine("runtimes", "win", "lib"), StringComparison.OrdinalIgnoreCase);
179179
var isWin = Environment.OSVersion.Platform == PlatformID.Win32NT;

CSharpRepl.Tests/PrettyPrinterTests.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,18 @@ public void FormatObject_ObjectInput_PrintsOutput(object obj, bool showDetails,
2929

3030
public static IEnumerable<object[]> FormatObjectInputs = new[]
3131
{
32-
new object[] { null, false, null },
33-
new object[] { null, true, null },
32+
new object[] { null, false, "null" },
33+
new object[] { null, true, "null" },
34+
3435
new object[] { @"""hello world""", false, @"""\""hello world\"""""},
3536
new object[] { @"""hello world""", true, @"""hello world"""},
37+
3638
new object[] { "a\nb", false, @"""a\nb"""},
3739
new object[] { "a\nb", true, "a\nb"},
40+
3841
new object[] { new[] { 1, 2, 3 }, false, "int[3] { 1, 2, 3 }"},
3942
new object[] { new[] { 1, 2, 3 }, true, $"int[3] {"{"}{NewLine} 1,{NewLine} 2,{NewLine} 3{NewLine}{"}"}{NewLine}"},
43+
4044
new object[] { Encoding.UTF8, true, "System.Text.UTF8Encoding+UTF8EncodingSealed"},
4145
};
4246
}

CSharpRepl.Tests/RoslynServicesTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using CSharpRepl.PrettyPromptConfig;
88
using CSharpRepl.Services;
99
using CSharpRepl.Services.Roslyn;
10+
using CSharpRepl.Services.Roslyn.Scripting;
1011
using NSubstitute;
1112
using PrettyPrompt;
1213
using PrettyPrompt.Consoles;
@@ -77,6 +78,39 @@ public async Task AutoFormat(string text, int caret, string expectedText, int ex
7778
Assert.Equal(expectedText, formattedText.Replace("\r", ""));
7879
Assert.Equal(expectedCaret, formattedCaret);
7980
}
81+
82+
[Theory]
83+
[InlineData("_ = 1", true, 1)]
84+
[InlineData("_ = 1;", false, null)]
85+
86+
[InlineData("object o; o = null", true, null)]
87+
[InlineData("object o; o = null;", false, null)]
88+
89+
[InlineData("int i = 1;", false, null)]
90+
91+
[InlineData("\"abc\".ToString()", true, "abc")]
92+
[InlineData("\"abc\".ToString();", false, null)]
93+
94+
[InlineData("object o = null; o?.ToString()", true, null)]
95+
[InlineData("object o = null; o?.ToString();", false, null)]
96+
97+
[InlineData("Console.WriteLine()", false, null)]
98+
[InlineData("Console.WriteLine();", false, null)]
99+
public async Task NullOutput_Versus_NoOutput(string text, bool hasOutput, object? expectedOutput)
100+
{
101+
var result = (EvaluationResult.Success)await services.EvaluateAsync(text);
102+
103+
Assert.Equal(hasOutput, result.ReturnValue.HasValue);
104+
105+
if (hasOutput)
106+
{
107+
Assert.Equal(expectedOutput, result.ReturnValue.Value);
108+
}
109+
else
110+
{
111+
Assert.Null(expectedOutput);
112+
}
113+
}
80114
}
81115

82116
[Collection(nameof(RoslynServices))]

CSharpRepl/CSharpReplPromptCallbacks.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ protected override Task<bool> ConfirmCompletionCommit(string text, int caret, Ke
188188
switch (result)
189189
{
190190
case EvaluationResult.Success success:
191-
var ilCode = success.ReturnValue.ToString()!;
191+
var ilCode = success.ReturnValue.ToString();
192192
var output = Prompt.RenderAnsiOutput(ilCode, Array.Empty<FormatSpan>(), console.BufferWidth);
193193
return new KeyPressCallbackResult(text, output);
194194
case EvaluationResult.Error err:

CSharpRepl/ReadEvalPrintLoop.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,15 @@ private static async Task PrintAsync(RoslynServices roslyn, IConsole console, Ev
114114
switch (result)
115115
{
116116
case EvaluationResult.Success ok:
117-
var formatted = await roslyn.PrettyPrintAsync(ok?.ReturnValue, displayDetails);
118-
console.WriteLine(formatted);
117+
if (ok.ReturnValue.HasValue)
118+
{
119+
var formatted = await roslyn.PrettyPrintAsync(ok.ReturnValue.Value, displayDetails);
120+
console.WriteLine(formatted);
121+
}
122+
else
123+
{
124+
console.WriteLine("");
125+
}
119126
break;
120127
case EvaluationResult.Error err:
121128
var formattedError = await roslyn.PrettyPrintAsync(err.Exception, displayDetails);

0 commit comments

Comments
 (0)