Skip to content

Commit

Permalink
Merge pull request #187 from kindermannhubert/null-output
Browse files Browse the repository at this point in the history
Printing of 'null' as output for null returning inputs.
  • Loading branch information
kindermannhubert authored Nov 5, 2022
2 parents dc9da91 + 62c91d6 commit 9ab1823
Show file tree
Hide file tree
Showing 13 changed files with 116 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System.Reflection;
using CSharpRepl.Services;
using CSharpRepl.Services.SyntaxHighlighting;
using Microsoft.CodeAnalysis.Scripting.Hosting;
using PrettyPrompt.Highlighting;
using MemberFilter = Microsoft.CodeAnalysis.Scripting.Hosting.MemberFilter;

namespace Microsoft.CodeAnalysis.CSharp.Scripting.Hosting;

internal class CSharpObjectFormatterImpl : CommonObjectFormatter
{
public FormattedString NullLiteral => PrimitiveFormatter.NullLiteral;

protected override CommonTypeNameFormatter TypeNameFormatter { get; }
protected override CommonPrimitiveFormatter PrimitiveFormatter { get; }
protected override MemberFilter Filter { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public CSharpPrimitiveFormatter(SyntaxHighlighter syntaxHighlighter)
NullLiteral = new FormattedString("null", keywordFormat);
}

protected override FormattedString NullLiteral { get; }
public override FormattedString NullLiteral { get; }

protected override FormattedString FormatLiteral(bool value)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal abstract partial class CommonPrimitiveFormatter
/// <summary>
/// String that describes "null" literal in the language.
/// </summary>
protected abstract FormattedString NullLiteral { get; }
public abstract FormattedString NullLiteral { get; }

protected abstract FormattedString FormatLiteral(bool value);
protected abstract FormattedString FormatLiteral(string value, bool quote, bool escapeNonPrintable, int numberRadix = NumberRadixDecimal);
Expand Down
3 changes: 1 addition & 2 deletions CSharpRepl.Services/Roslyn/PrettyPrinter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ public FormattedString FormatObject(object? obj, bool displayDetails)
{
return obj switch
{
// intercept null, don't print the string "null"
null => null,
null => formatter.NullLiteral,

// when displayDetails is true, don't show the escaped string (i.e. interpret the escape characters, via displaying to console)
string str when displayDetails => str,
Expand Down
2 changes: 1 addition & 1 deletion CSharpRepl.Services/Roslyn/RoslynServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ public RoslynServices(IConsole console, Configuration config, ITraceLogger logge
// the script runner is used to actually execute the scripts, and the workspace manager
// is updated alongside. The workspace is a datamodel used in "editor services" like
// syntax highlighting, autocompletion, and roslyn symbol queries.
this.scriptRunner = new ScriptRunner(compilationOptions, referenceService, console, config);
this.workspaceManager = new WorkspaceManager(compilationOptions, referenceService, logger);
this.scriptRunner = new ScriptRunner(workspaceManager, compilationOptions, referenceService, console, config);
this.disassembler = new Disassembler(compilationOptions, referenceService, scriptRunner);
this.prettyPrinter = new PrettyPrinter(highlighter, config);
Expand Down
4 changes: 2 additions & 2 deletions CSharpRepl.Services/Roslyn/Scripting/EvaluationResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

using Microsoft.CodeAnalysis;
using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;

namespace CSharpRepl.Services.Roslyn.Scripting;

/// <remarks>about as close to a discriminated union as I can get</remarks>
public abstract record EvaluationResult
{
public sealed record Success(string Input, object ReturnValue, IReadOnlyCollection<MetadataReference> References) : EvaluationResult;
public sealed record Success(string Input, Optional<object?> ReturnValue, IReadOnlyCollection<MetadataReference> References) : EvaluationResult;
public sealed record Error(Exception Exception) : EvaluationResult;
public sealed record Cancelled() : EvaluationResult;
}
60 changes: 46 additions & 14 deletions CSharpRepl.Services/Roslyn/Scripting/ScriptRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.Scripting.Hosting;
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using PrettyPrompt.Consoles;
using System.Threading.Tasks;
using CSharpRepl.Services.Dotnet;
using CSharpRepl.Services.Roslyn.MetadataResolvers;
using CSharpRepl.Services.Roslyn.References;
using Microsoft.CodeAnalysis;
using CSharpRepl.Services.Dotnet;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.Scripting.Hosting;
using Microsoft.CodeAnalysis.Text;
using PrettyPrompt.Consoles;

namespace CSharpRepl.Services.Roslyn.Scripting;

Expand All @@ -27,17 +29,20 @@ internal sealed class ScriptRunner
private readonly InteractiveAssemblyLoader assemblyLoader;
private readonly CompositeAlternativeReferenceResolver alternativeReferenceResolver;
private readonly MetadataReferenceResolver metadataResolver;
private readonly WorkspaceManager workspaceManager;
private readonly AssemblyReferenceService referenceAssemblyService;
private ScriptOptions scriptOptions;
private ScriptState<object>? state;

public ScriptRunner(
WorkspaceManager workspaceManager,
CSharpCompilationOptions compilationOptions,
AssemblyReferenceService referenceAssemblyService,
IConsole console,
Configuration configuration)
{
this.console = console;
this.workspaceManager = workspaceManager;
this.referenceAssemblyService = referenceAssemblyService;
this.assemblyLoader = new InteractiveAssemblyLoader(new MetadataShadowCopyProvider());

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

state = await EvaluateStringWithStateAsync(text, state, assemblyLoader, this.scriptOptions, args, cancellationToken).ConfigureAwait(false);
state = await EvaluateStringWithStateAsync(text, state, assemblyLoader, scriptOptions, args, cancellationToken).ConfigureAwait(false);

return state.Exception is null
? CreateSuccessfulResult(text, state)
? await CreateSuccessfulResult(text, state, cancellationToken).ConfigureAwait(false)
: new EvaluationResult.Error(this.state.Exception);
}
catch (Exception oce) when (oce is OperationCanceledException || oce.InnerException is OperationCanceledException)
Expand Down Expand Up @@ -113,27 +118,54 @@ public Compilation CompileTransient(string code, OptimizationLevel optimizationL
);
}

private EvaluationResult.Success CreateSuccessfulResult(string text, ScriptState<object> state)
private async Task<EvaluationResult.Success> CreateSuccessfulResult(string text, ScriptState<object> state, CancellationToken cancellationToken)
{
var hasValueReturningStatement = await HasValueReturningStatement(text, cancellationToken).ConfigureAwait(false);

referenceAssemblyService.AddImplementationAssemblyReferences(state.Script.GetCompilation().References);
var frameworkReferenceAssemblies = referenceAssemblyService.LoadedReferenceAssemblies;
var frameworkImplementationAssemblies = referenceAssemblyService.LoadedImplementationAssemblies;
this.scriptOptions = this.scriptOptions.WithReferences(frameworkImplementationAssemblies);
return new EvaluationResult.Success(text, state.ReturnValue, frameworkImplementationAssemblies.Concat(frameworkReferenceAssemblies).ToList());
var returnValue = hasValueReturningStatement ? new Optional<object?>(state.ReturnValue) : default;
return new EvaluationResult.Success(text, returnValue, frameworkImplementationAssemblies.Concat(frameworkReferenceAssemblies).ToList());
}

private Task<ScriptState<object>> EvaluateStringWithStateAsync(string text, ScriptState<object>? state, InteractiveAssemblyLoader assemblyLoader, ScriptOptions scriptOptions, string[]? args = null, CancellationToken cancellationToken = default)
private async Task<ScriptState<object>> EvaluateStringWithStateAsync(string text, ScriptState<object>? state, InteractiveAssemblyLoader assemblyLoader, ScriptOptions scriptOptions, string[]? args, CancellationToken cancellationToken)
{
return state is null
var scriptTask = state is null
? CSharpScript
.Create(text, scriptOptions, globalsType: typeof(ScriptGlobals), assemblyLoader: assemblyLoader)
.RunAsync(globals: CreateGlobalsObject(args), cancellationToken)
: state
.ContinueWithAsync(text, scriptOptions, cancellationToken);

return await scriptTask.ConfigureAwait(false);
}

private async Task<bool> HasValueReturningStatement(string text, CancellationToken cancellationToken)
{
var sourceText = SourceText.From(text);
var document = workspaceManager.CurrentDocument.WithText(sourceText);
var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
if (tree != null &&
await tree.GetRootAsync(cancellationToken).ConfigureAwait(false) is CompilationUnitSyntax root &&
root.Members.Count > 0 &&
root.Members.Last() is GlobalStatementSyntax { Statement: ExpressionStatementSyntax { SemicolonToken.IsMissing: true } possiblyValueReturningStatement })
{
//now we know the text's last statement does not have semicolon so it can return value
//but the statement's return type still can be void - we need to find out
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
if (semanticModel != null)
{
var typeInfo = semanticModel.GetTypeInfo(possiblyValueReturningStatement.Expression, cancellationToken);
return typeInfo.ConvertedType?.SpecialType != SpecialType.System_Void;
}
}
return false;
}

private ScriptGlobals CreateGlobalsObject(string[]? args)
{
return new ScriptGlobals(console, args ?? Array.Empty<string>());
}
}
}
22 changes: 5 additions & 17 deletions CSharpRepl.Tests/DisassemblerTests.cs
Original file line number Diff line number Diff line change
@@ -1,38 +1,26 @@
using CSharpRepl.Services;
using System;
using System.IO;
using System.Threading.Tasks;
using CSharpRepl.Services;
using CSharpRepl.Services.Disassembly;
using CSharpRepl.Services.Roslyn;
using CSharpRepl.Services.Roslyn.References;
using CSharpRepl.Services.Roslyn.Scripting;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using NSubstitute;
using PrettyPrompt.Consoles;
using System;
using System.IO;
using System.Threading.Tasks;
using Xunit;

namespace CSharpRepl.Tests;

[Collection(nameof(RoslynServices))]
public class DisassemblerTests : IAsyncLifetime
{
private readonly Disassembler disassembler;
private readonly RoslynServices services;

public DisassemblerTests()
{
var options = new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary,
usings: Array.Empty<string>()
);
var console = Substitute.For<IConsole>();
console.BufferWidth.Returns(200);
var config = new Configuration();
var referenceService = new AssemblyReferenceService(config, new TestTraceLogger());
var scriptRunner = new ScriptRunner(options, referenceService, console, config);

this.disassembler = new Disassembler(options, referenceService, scriptRunner);
this.services = new RoslynServices(console, new Configuration(), new TestTraceLogger());
}

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

var result = disassembler.Disassemble(input, debugMode: optimizationLevel == OptimizationLevel.Debug);
var result = services.ConvertToIntermediateLanguage(input, debugMode: optimizationLevel == OptimizationLevel.Debug).Result;
var actualOutput = Assert
.IsType<EvaluationResult.Success>(result)
.ReturnValue
Expand Down
12 changes: 6 additions & 6 deletions CSharpRepl.Tests/EvaluationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public async Task Evaluate_LiteralInteger_ReturnsInteger()
var success = Assert.IsType<EvaluationResult.Success>(result);
Assert.Equal("5", success.Input);

var returnValue = Assert.IsType<int>(success.ReturnValue);
var returnValue = Assert.IsType<int>(success.ReturnValue.Value);
Assert.Equal(5, returnValue);
}

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

var assignment = Assert.IsType<EvaluationResult.Success>(variableAssignment);
var usage = Assert.IsType<EvaluationResult.Success>(variableUsage);
Assert.Null(assignment.ReturnValue);
Assert.Null(assignment.ReturnValue.Value);
Assert.Equal("Hello Mundo", usage.ReturnValue);
}

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

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

Assert.Null(installationResult.ReturnValue);
Assert.NotNull(usageResult.ReturnValue);
Assert.Null(installationResult.ReturnValue.Value);
Assert.NotNull(usageResult.ReturnValue.Value);
Assert.Contains("Adding references for 'Microsoft.CodeAnalysis.CSharp.3.11.0'", ProgramTests.RemoveFormatting(stdout.ToString()));
}

Expand Down Expand Up @@ -173,7 +173,7 @@ public async Task Evaluate_ResolveCorrectRuntimeVersionOfReferencedAssembly()
Assert.IsType<EvaluationResult.Success>(referenceResult);
Assert.IsType<EvaluationResult.Success>(importResult);

var referencedSystemManagementPath = (string)((EvaluationResult.Success)importResult).ReturnValue;
var referencedSystemManagementPath = (string)((EvaluationResult.Success)importResult).ReturnValue.Value;
referencedSystemManagementPath = referencedSystemManagementPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
var winRuntimeSelected = referencedSystemManagementPath.Contains(Path.Combine("runtimes", "win", "lib"), StringComparison.OrdinalIgnoreCase);
var isWin = Environment.OSVersion.Platform == PlatformID.Win32NT;
Expand Down
8 changes: 6 additions & 2 deletions CSharpRepl.Tests/PrettyPrinterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,18 @@ public void FormatObject_ObjectInput_PrintsOutput(object obj, bool showDetails,

public static IEnumerable<object[]> FormatObjectInputs = new[]
{
new object[] { null, false, null },
new object[] { null, true, null },
new object[] { null, false, "null" },
new object[] { null, true, "null" },

new object[] { @"""hello world""", false, @"""\""hello world\"""""},
new object[] { @"""hello world""", true, @"""hello world"""},

new object[] { "a\nb", false, @"""a\nb"""},
new object[] { "a\nb", true, "a\nb"},

new object[] { new[] { 1, 2, 3 }, false, "int[3] { 1, 2, 3 }"},
new object[] { new[] { 1, 2, 3 }, true, $"int[3] {"{"}{NewLine} 1,{NewLine} 2,{NewLine} 3{NewLine}{"}"}{NewLine}"},

new object[] { Encoding.UTF8, true, "System.Text.UTF8Encoding+UTF8EncodingSealed"},
};
}
34 changes: 34 additions & 0 deletions CSharpRepl.Tests/RoslynServicesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using CSharpRepl.PrettyPromptConfig;
using CSharpRepl.Services;
using CSharpRepl.Services.Roslyn;
using CSharpRepl.Services.Roslyn.Scripting;
using NSubstitute;
using PrettyPrompt;
using PrettyPrompt.Consoles;
Expand Down Expand Up @@ -77,6 +78,39 @@ public async Task AutoFormat(string text, int caret, string expectedText, int ex
Assert.Equal(expectedText, formattedText.Replace("\r", ""));
Assert.Equal(expectedCaret, formattedCaret);
}

[Theory]
[InlineData("_ = 1", true, 1)]
[InlineData("_ = 1;", false, null)]

[InlineData("object o; o = null", true, null)]
[InlineData("object o; o = null;", false, null)]

[InlineData("int i = 1;", false, null)]

[InlineData("\"abc\".ToString()", true, "abc")]
[InlineData("\"abc\".ToString();", false, null)]

[InlineData("object o = null; o?.ToString()", true, null)]
[InlineData("object o = null; o?.ToString();", false, null)]

[InlineData("Console.WriteLine()", false, null)]
[InlineData("Console.WriteLine();", false, null)]
public async Task NullOutput_Versus_NoOutput(string text, bool hasOutput, object? expectedOutput)
{
var result = (EvaluationResult.Success)await services.EvaluateAsync(text);

Assert.Equal(hasOutput, result.ReturnValue.HasValue);

if (hasOutput)
{
Assert.Equal(expectedOutput, result.ReturnValue.Value);
}
else
{
Assert.Null(expectedOutput);
}
}
}

[Collection(nameof(RoslynServices))]
Expand Down
2 changes: 1 addition & 1 deletion CSharpRepl/CSharpReplPromptCallbacks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ protected override Task<bool> ConfirmCompletionCommit(string text, int caret, Ke
switch (result)
{
case EvaluationResult.Success success:
var ilCode = success.ReturnValue.ToString()!;
var ilCode = success.ReturnValue.ToString();
var output = Prompt.RenderAnsiOutput(ilCode, Array.Empty<FormatSpan>(), console.BufferWidth);
return new KeyPressCallbackResult(text, output);
case EvaluationResult.Error err:
Expand Down
11 changes: 9 additions & 2 deletions CSharpRepl/ReadEvalPrintLoop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,15 @@ private static async Task PrintAsync(RoslynServices roslyn, IConsole console, Ev
switch (result)
{
case EvaluationResult.Success ok:
var formatted = await roslyn.PrettyPrintAsync(ok?.ReturnValue, displayDetails);
console.WriteLine(formatted);
if (ok.ReturnValue.HasValue)
{
var formatted = await roslyn.PrettyPrintAsync(ok.ReturnValue.Value, displayDetails);
console.WriteLine(formatted);
}
else
{
console.WriteLine("");
}
break;
case EvaluationResult.Error err:
var formattedError = await roslyn.PrettyPrintAsync(err.Exception, displayDetails);
Expand Down

0 comments on commit 9ab1823

Please sign in to comment.