Skip to content

Commit

Permalink
Add support for emiting warning / errors from cecilifier ( #292)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianoc committed Aug 25, 2024
1 parent 4243be5 commit c032c71
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 66 deletions.
68 changes: 68 additions & 0 deletions Cecilifier.Core.Tests/Tests/Unit/CecilifierContextTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Linq;
using Cecilifier.Core.Misc;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using NUnit.Framework;

namespace Cecilifier.Core.Tests.Tests.Unit;

[TestFixture]
public class CecilifierContextTests
{
[OneTimeSetUp]
public void SetUpFixture()
{
var syntaxTree = CSharpSyntaxTree.ParseText("class C {}");
var comp = CSharpCompilation.Create("", new[] { syntaxTree });
_semanticModel = comp.GetSemanticModel(syntaxTree);
}


[Test]
public void WarningDiagnosticsAreEmitted()
{
var cecilifierContext = new CecilifierContext(_semanticModel, new CecilifierOptions(), 0);
cecilifierContext.EmitWarning("Simple Warning");

var found = cecilifierContext.Diagnostics.Where(d => d.Message.Contains("Simple Warning")).ToList();
Assert.That(found, Is.Not.Null);
Assert.That(found.Count, Is.EqualTo(1));
Assert.That(found[0].Kind, Is.EqualTo(DiagnosticKind.Warning));
}

[Test]
public void ErrorDiagnosticsAreEmitted()
{
var cecilifierContext = new CecilifierContext(_semanticModel, new CecilifierOptions(), 0);
cecilifierContext.EmitError("Simple Error");

var found = cecilifierContext.Diagnostics.Where(d => d.Message.Contains("Simple Error")).ToList();
Assert.That(found, Is.Not.Null);
Assert.That(found.Count, Is.EqualTo(1));
Assert.That(found[0].Kind, Is.EqualTo(DiagnosticKind.Error));
}

[Test]
public void DiagnosticsEmitsEquivalentPreprocessorDirectives()
{
var cecilifierContext = new CecilifierContext(_semanticModel, new CecilifierOptions(), 0);
cecilifierContext.EmitError("Simple Error");
cecilifierContext.EmitWarning("Simple Warning");

Assert.That(cecilifierContext.Output, Contains.Substring("#error Simple Error"));
Assert.That(cecilifierContext.Output, Contains.Substring("#warning Simple Warning"));
}

[Test]
public void NewLinesInDiagnosticsEmitsMultiplePreprocessorDirectives()
{
var cecilifierContext = new CecilifierContext(_semanticModel, new CecilifierOptions(), 0);
cecilifierContext.EmitWarning("Warning with\nmultiple\nlines");

Assert.That(cecilifierContext.Output, Contains.Substring("#warning Warning with"));
Assert.That(cecilifierContext.Output, Contains.Substring("#warning multiple"));
Assert.That(cecilifierContext.Output, Contains.Substring("#warning lines"));
}

private SemanticModel _semanticModel;
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,20 @@
using System.IO;
using System.Text;
using Cecilifier.Core.Misc;
using NUnit.Framework;

namespace Cecilifier.Core.Tests.Integration
namespace Cecilifier.Core.Tests.Tests.Unit
{
/// <summary>
/// These tests are meant to document the list of known unsupported features and also to ensure that these are reported if used.
/// </summary>
[TestFixture]
public class UnsupportedFeaturesTestCase
public class UnsupportedFeaturesTestCase : CecilifierUnitTestBase
{
[TestCase("yield return 1", TestName = "YieldReturn")]
[TestCase("yield break", TestName = "YieldBreak")]
public void EnumeratorBlocks(string statement)
{
var code = new MemoryStream(Encoding.ASCII.GetBytes($"class Test {{ System.Collections.IEnumerable Do() {{ {statement}; }} }} "));
using (var stream = Cecilifier.Process(code, new CecilifierOptions { References = ReferencedAssemblies.GetTrustedAssembliesPath() }).GeneratedCode)
{
var cecilifiedCode = stream.ReadToEnd();
Assert.That(cecilifiedCode, Does.Match("Syntax 'Yield(Return|Break)Statement' is not supported"));
}
var result = RunCecilifier($"class Test {{ System.Collections.IEnumerable Do() {{ {statement}; }} }} ");
var cecilifiedCode = result.GeneratedCode.ReadToEnd();
Assert.That(cecilifiedCode, Does.Match("Syntax 'Yield(Return|Break)Statement' is not supported"));
}

[TestCase("var (a,b)")]
Expand All @@ -40,12 +34,9 @@ public void TestStatements(string statement, string expectedInError)

private static void AssertUnsupportedFeature(string codeString, string expectedMessage)
{
var code = new MemoryStream(Encoding.ASCII.GetBytes(codeString));
using (var stream = Cecilifier.Process(code, new CecilifierOptions { References = ReferencedAssemblies.GetTrustedAssembliesPath() }).GeneratedCode)
{
var cecilifiedCode = stream.ReadToEnd();
Assert.That(cecilifiedCode, Contains.Substring(expectedMessage));
}
var result = RunCecilifier(codeString);
var cecilifiedCode = result.GeneratedCode.ReadToEnd();
Assert.That(cecilifiedCode, Contains.Substring(expectedMessage));
}

[Test]
Expand Down
2 changes: 1 addition & 1 deletion Cecilifier.Core/AST/ExpressionVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ internal static bool Visit(IVisitorContext ctx, string ilVar, SyntaxNode node)

var ev = new ExpressionVisitor(ctx, ilVar);
ev.Visit(node);

return ev.skipLeftSideVisitingInAssignment;
}

Expand Down
4 changes: 4 additions & 0 deletions Cecilifier.Core/AST/IVisitorContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ internal interface IVisitorContext
{
ServiceCollection Services { get; }

void EmitWarning(string message, SyntaxNode node = null);

void EmitError(string message, SyntaxNode node = null);

INameStrategy Naming { get; }

SemanticModel SemanticModel { get; }
Expand Down
1 change: 1 addition & 0 deletions Cecilifier.Core/AST/SyntaxWalkerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,7 @@ protected static void EnsureForwardedMethod(IVisitorContext context, IMethodSymb

protected void LogUnsupportedSyntax(SyntaxNode node)
{
Context.EmitWarning($"Syntax {node.Kind()} ({node.HumanReadableSummary()}) is not supported.\nGenerated code may not compile, or if it compiles, produce invalid results.", node);
var lineSpan = node.GetLocation().GetLineSpan();
AddCecilExpression($"/* Syntax '{node.Kind()}' is not supported in {lineSpan.Path} ({lineSpan.Span.Start.Line + 1},{lineSpan.Span.Start.Character + 1}):\n------\n{node}\n----*/");
}
Expand Down
8 changes: 5 additions & 3 deletions Cecilifier.Core/Cecilifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static CecilifierResult Process(Stream content, CecilifierOptions options

var errors = comp.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Select(d => (CompilationError) d)
.Select(CecilifierDiagnostic.FromCompiler)
.ToArray();
if (errors.Length > 0)
{
Expand All @@ -51,7 +51,7 @@ public static CecilifierResult Process(Stream content, CecilifierOptions options
//new SyntaxTreeDump("TREE: ", root);

var mainTypeName = visitor.MainType != null ? visitor.MainType.Identifier.Text : "Cecilified";
return new CecilifierResult(new StringReader(ctx.Output.AsCecilApplication(mainTypeName, visitor.MainMethodDefinitionVariable)), mainTypeName, ctx.Mappings);
return new CecilifierResult(new StringReader(ctx.Output.AsCecilApplication(mainTypeName, visitor.MainMethodDefinitionVariable)), mainTypeName, ctx.Mappings, ctx.Diagnostics);
}

private static OutputKind OutputKindFor(SyntaxTree syntaxTree)
Expand All @@ -73,15 +73,17 @@ public class CecilifierOptions

public struct CecilifierResult
{
public CecilifierResult(StringReader generatedCode, string mainTypeName, IList<Mapping> mappings)
public CecilifierResult(StringReader generatedCode, string mainTypeName, IList<Mapping> mappings, IList<CecilifierDiagnostic> diagnostics = null)
{
GeneratedCode = generatedCode;
MainTypeName = mainTypeName;
Mappings = mappings;
Diagnostics = diagnostics ?? [];
}

public StringReader GeneratedCode { get; }
public string MainTypeName { get; }
public IList<Mapping> Mappings { get; }
public IList<CecilifierDiagnostic> Diagnostics { get; }
}
}
19 changes: 19 additions & 0 deletions Cecilifier.Core/Misc/CecilifierContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public CecilifierContext(SemanticModel semanticModel, CecilifierOptions options,
roslynTypeSystem = new RoslynTypeSystem(this);
TypeResolver = new TypeResolverImpl(this);
Mappings = new List<Mapping>();
Diagnostics = [];
CecilifiedLineNumber = startingLine;
startLineNumber = startingLine;

Expand Down Expand Up @@ -62,6 +63,24 @@ public string Output

public IList<Mapping> Mappings { get; }

public IList<CecilifierDiagnostic> Diagnostics { get; }

public void EmitWarning(string message, SyntaxNode node = null) => EmitDiagnostic(message, node, DiagnosticKind.Warning);
public void EmitError(string message, SyntaxNode node = null) => EmitDiagnostic(message, node, DiagnosticKind.Error);

private void EmitDiagnostic(string message, SyntaxNode node, DiagnosticKind diagnosticKind)
{
Diagnostics.Add(CecilifierDiagnostic.FromAstNode(node, diagnosticKind, message));

var diagnosticKindString = diagnosticKind == DiagnosticKind.Warning ? "warning" : "error";
var lines = message.Split('\n');
foreach (var line in lines)
{
WriteCecilExpression($"#{diagnosticKindString} {line}");
WriteNewLine();
}
}

public IMethodSymbol GetDeclaredSymbol(BaseMethodDeclarationSyntax methodDeclaration)
{
return (IMethodSymbol) SemanticModel.GetDeclaredSymbol(methodDeclaration);
Expand Down
41 changes: 41 additions & 0 deletions Cecilifier.Core/Misc/CecilifierDiagnostic.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using Microsoft.CodeAnalysis;

namespace Cecilifier.Core.Misc;

public enum DiagnosticKind
{
Information,
Warning,
Error
}

public record struct SourceLineInformation(int StartLineNumber, int StartColumn, int EndLineNumber, int EndColumn);

public record struct CecilifierDiagnostic(DiagnosticKind Kind, string Message, SourceLineInformation LineInformation)
{
public static CecilifierDiagnostic FromCompiler(Diagnostic diagnostic)
{
var lineSpan = diagnostic.Location.GetLineSpan();
return new CecilifierDiagnostic(
diagnostic.Severity switch
{
DiagnosticSeverity.Error => DiagnosticKind.Error,
DiagnosticSeverity.Hidden => DiagnosticKind.Information,
DiagnosticSeverity.Info => DiagnosticKind.Information,
DiagnosticSeverity.Warning => DiagnosticKind.Warning,
_ => throw new ArgumentOutOfRangeException()
},
diagnostic.GetMessage(),
new SourceLineInformation(lineSpan.Span.Start.Line + 1, lineSpan.Span.Start.Character + 1, lineSpan.Span.End.Line + 1,lineSpan.Span.End.Character + 1));
}

public static CecilifierDiagnostic FromAstNode(SyntaxNode node, DiagnosticKind diagnosticKind, string message)
{
var lineSpan = node != null ? node.GetLocation().GetLineSpan() : new FileLinePositionSpan();
return new CecilifierDiagnostic(
diagnosticKind,
message,
new SourceLineInformation(lineSpan.Span.Start.Line + 1, lineSpan.Span.Start.Character + 1, lineSpan.Span.End.Line + 1,lineSpan.Span.End.Character + 1));
}
}
16 changes: 0 additions & 16 deletions Cecilifier.Core/Misc/CompilationError.cs

This file was deleted.

8 changes: 4 additions & 4 deletions Cecilifier.Core/SyntaxErrorException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ namespace Cecilifier.Core
{
public class SyntaxErrorException : Exception
{
public CompilationError[] Errors { get; }
public CecilifierDiagnostic[] Diagnostics { get; }

public SyntaxErrorException(CompilationError[] errors)
public SyntaxErrorException(CecilifierDiagnostic[] diagnostics)
{
Errors = errors;
Diagnostics = diagnostics;
}

public override string Message => ToString();

public override string ToString()
{
return string.Join('\n', Errors.Select(err => err.Message));
return string.Join('\n', Diagnostics.Select(err => err.Message));
}
}
}
19 changes: 19 additions & 0 deletions Cecilifier.Web/CecilifiedWebResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Cecilifier.Core.Mappings;
using Cecilifier.Core.Misc;

namespace Cecilifier.Web;

public class CecilifiedWebResult
{
[JsonPropertyName("status")] public int Status { get; set; }
[JsonPropertyName("data")] public string Data { get; set; }
[JsonPropertyName("counter")] public int Counter { get; set; }
[JsonPropertyName("clientsCounter")] public uint Clients { get; set; }
[JsonPropertyName("maximumUnique")] public uint MaximumUnique { get; set; }
[JsonPropertyName("kind")] public char Kind { get; set; }
[JsonPropertyName("mappings")] public IList<Mapping> Mappings { get; set; }
[JsonPropertyName("mainTypeName")] public string MainTypeName { get; set; }
[JsonPropertyName("diagnostics")] public IList<CecilifierDiagnostic> Diagnostics { get; set; }
}
24 changes: 6 additions & 18 deletions Cecilifier.Web/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
using System.Resources;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
Expand All @@ -30,18 +29,6 @@

namespace Cecilifier.Web
{
public class CecilifiedWebResult
{
[JsonPropertyName("status")] public int Status { get; set; }
[JsonPropertyName("cecilifiedCode")] public string CecilifiedCode { get; set; }
[JsonPropertyName("counter")] public int Counter { get; set; }
[JsonPropertyName("clientsCounter")] public uint Clients { get; set; }
[JsonPropertyName("maximumUnique")] public uint MaximumUnique { get; set; }
[JsonPropertyName("kind")] public char Kind { get; set; }
[JsonPropertyName("mappings")] public IList<Mapping> Mappings { get; set; }
[JsonPropertyName("mainTypeName")] public string MainTypeName { get; set; }
}

public class Startup
{
private const string ProjectContents = """
Expand Down Expand Up @@ -158,7 +145,7 @@ async Task SendStatisticsAsync(WebSocket webSocket)
var cecilifiedWebResult = new CecilifiedWebResult
{
Status = 4,
CecilifiedCode = String.Empty,
Data = String.Empty,
Counter = CecilifierApplication.Count,
MaximumUnique = CecilifierApplication.MaximumUnique,
Clients = CecilifierApplication.UniqueClients,
Expand Down Expand Up @@ -260,11 +247,11 @@ private async Task ProcessWebSocketAsync(WebSocket webSocket, int remoteIpAddres
catch (SyntaxErrorException ex)
{
var source = includeSourceInErrorReports ? codeSnippet : string.Empty;
await SendMessageWithCodeToChatAsync("Syntax Error", string.Join('\n', ex.Errors.Select(err => err.Message)), "15746887", source);
await SendMessageWithCodeToChatAsync("Syntax Error", string.Join('\n', ex.Diagnostics.Select(err => err.Message)), "15746887", source);

var jsonOptions = new JsonSerializerOptions(JsonSerializerOptions.Default);
jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
var serializedErrors = JsonSerializer.Serialize(ex.Errors, jsonOptions);
var serializedErrors = JsonSerializer.Serialize(ex.Diagnostics, jsonOptions);
var dataToReturn = Encoding.UTF8.GetBytes($"{{ \"status\" : 1, \"error\": \"Code contains syntax errors\", \"errors\": {serializedErrors} }}").AsMemory();
await webSocket.SendAsync(dataToReturn, WebSocketMessageType.Text, true, CancellationToken.None);
}
Expand Down Expand Up @@ -338,18 +325,19 @@ Memory<byte> ZipProject(params (string fileName, string contents)[] files)
}
}

private static byte[] JsonSerializedBytes(string cecilifiedCode, char kind, CecilifierResult cecilifierResult)
private static byte[] JsonSerializedBytes(string data, char kind, CecilifierResult cecilifierResult)
{
var cecilifiedWebResult = new CecilifiedWebResult
{
Status = 0,
CecilifiedCode = cecilifiedCode,
Data = data,
Counter = CecilifierApplication.Count,
MaximumUnique = CecilifierApplication.MaximumUnique,
Clients = CecilifierApplication.UniqueClients,
Kind = kind,
MainTypeName = cecilifierResult.MainTypeName,
Mappings = cecilifierResult.Mappings.OrderBy(x => x.Cecilified.Length).ToArray(),
Diagnostics = cecilifierResult.Diagnostics
};

return JsonSerializer.SerializeToUtf8Bytes(cecilifiedWebResult);
Expand Down
Loading

0 comments on commit c032c71

Please sign in to comment.