Skip to content

Commit

Permalink
Refactor compilation diagnostics, add them to compilation status page
Browse files Browse the repository at this point in the history
This is a large commit of various interdependent changes with
the simple goal of adding warnings to the compilation status page.

* Non-critical errors from tokens are now reported as warnings.
   Before, they were ignored completely.
* Added IDiagnosticsCompilationTracer interface
   - it allows watching the compilation process, for example
     observing changes made by various visitors or
     in this case getting the warnings when the compilation
     otherwise succeeds
* Added DotvvmCompilationDiagnostic and
   DotvvmCompilationSourceLocation records
   - it designed to be a generic class for warnings and errors from
     the DotHTML compilation. It contains references to the syntax
     and resolved trees, so it should not be stored long-term, but
     provides all the possible detail about each reported error/warning.
* BindingCompilationException is refactored to use the
   DotvvmCompilationDiagnostics and support any number of them.
   - plus, IDotvvmException compatibility is added
   - the exception will present as the "first" / primary error for better compatibility
* ErrorCheckingVisitor is now smarter about binding errors,
   the BindingToken ranges are mapped into the DothtmlToken
   so we can underline only the affected part of the binding.
   This is somewhat important for displaying diagnostics
   of multiline bindings in the compilation page.
   Multiple errors can be collected from the error.
* ViewCompilationService now collects the warnings and errors
   from the tracer and DotvvmCompilationException
* And finally, the compilation displays up to 8 errors and warnings
   encountered during the page compilation under each row.
   Warnings tab was also added which shows only the views with at
   least one warning/error
  • Loading branch information
exyi committed Jan 17, 2024
1 parent 817d6bd commit bccb5e6
Show file tree
Hide file tree
Showing 43 changed files with 1,096 additions and 308 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,12 @@ protected virtual void ResolveRootContent(DothtmlRootNode root, IAbstractControl
}
catch (DotvvmCompilationException ex)
{
if (ex.Tokens == null)
if (ex.Tokens is null)
{
ex.Tokens = node.Tokens;
ex.ColumnNumber = node.Tokens.First().ColumnNumber;
ex.LineNumber = node.Tokens.First().LineNumber;
var oldLoc = ex.CompilationError.Location;
ex.CompilationError = ex.CompilationError with {
Location = new(oldLoc.FileName, oldLoc.MarkupFile, node.Tokens)
};
}
if (!LogError(ex, node))
throw;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,12 @@ public DefaultControlBuilderFactory(DotvvmConfiguration configuration, IMarkupFi
var compilationService = configuration.ServiceProvider.GetService<IDotvvmViewCompilationService>();
void editCompilationException(DotvvmCompilationException ex)
{
if (ex.FileName == null)
var fileName = ex.FileName ?? file.FullPath;
if (!Path.IsPathRooted(fileName) && Path.IsPathRooted(file.FullPath))
{
ex.FileName = file.FullPath;
}
else if (!Path.IsPathRooted(ex.FileName))
{
ex.FileName = Path.Combine(
file.FullPath.Remove(file.FullPath.Length - file.FileName.Length),
ex.FileName);
fileName = Path.Combine(file.FullPath.Remove(file.FullPath.Length - file.FileName.Length), fileName);
}
ex.SetFile(fileName, file);
}
try
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DotVVM.Framework.Compilation.ControlTree.Resolved;
using DotVVM.Framework.Compilation.Parser.Dothtml.Parser;
using DotVVM.Framework.Compilation.Parser.Dothtml.Tokenizer;
using DotVVM.Framework.Compilation.ViewCompiler;

namespace DotVVM.Framework.Compilation
{

public interface IDiagnosticsCompilationTracer
{
Handle CompilationStarted(string file, string sourceCode);
abstract class Handle
{
public virtual void Parsed(List<DothtmlToken> tokens, DothtmlRootNode syntaxTree) { }
public virtual void Resolved(ResolvedTreeRoot tree, ControlBuilderDescriptor descriptor) { }
public virtual void AfterVisitor(ResolvedControlTreeVisitor visitor, ResolvedTreeRoot tree) { }
public virtual void CompilationDiagnostic(DotvvmCompilationDiagnostic diagnostic, string? contextLine) { }
public virtual void Failed(Exception exception) { }
}
sealed class NopHandle: Handle
{
private NopHandle() { }
public static readonly NopHandle Instance = new NopHandle();
}
}

public sealed class CompositeDiagnosticsCompilationTracer : IDiagnosticsCompilationTracer
{
readonly IDiagnosticsCompilationTracer[] tracers;

public CompositeDiagnosticsCompilationTracer(IEnumerable<IDiagnosticsCompilationTracer> tracers)
{
this.tracers = tracers.ToArray();
}

public IDiagnosticsCompilationTracer.Handle CompilationStarted(string file, string sourceCode)
{
var handles = this.tracers
.Select(t => t.CompilationStarted(file, sourceCode))
.Where(t => t != IDiagnosticsCompilationTracer.NopHandle.Instance)
.ToArray();


return handles.Length switch {
0 => IDiagnosticsCompilationTracer.NopHandle.Instance,
1 => handles[0],
_ => new Handle(handles)
};
}

sealed class Handle : IDiagnosticsCompilationTracer.Handle, IDisposable
{
private IDiagnosticsCompilationTracer.Handle[] handles;

public Handle(IDiagnosticsCompilationTracer.Handle[] handles)
{
this.handles = handles;
}

public override void AfterVisitor(ResolvedControlTreeVisitor visitor, ResolvedTreeRoot tree)
{
foreach (var h in handles)
h.AfterVisitor(visitor, tree);
}
public override void CompilationDiagnostic(DotvvmCompilationDiagnostic warning, string? contextLine)
{
foreach (var h in handles)
h.CompilationDiagnostic(warning, contextLine);
}


public override void Failed(Exception exception)
{
foreach (var h in handles)
h.Failed(exception);
}
public override void Parsed(List<DothtmlToken> tokens, DothtmlRootNode syntaxTree)
{
foreach (var h in handles)
h.Parsed(tokens, syntaxTree);
}
public override void Resolved(ResolvedTreeRoot tree, ControlBuilderDescriptor descriptor)
{
foreach (var h in handles)
h.Resolved(tree, descriptor);
}
public void Dispose()
{
foreach (var h in handles)
(h as IDisposable)?.Dispose();
}
}
}
}
37 changes: 37 additions & 0 deletions src/Framework/Framework/Compilation/DotHtmlFileInfo.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using DotVVM.Framework.Binding.Properties;

namespace DotVVM.Framework.Compilation
{
Expand All @@ -9,6 +10,9 @@ public sealed class DotHtmlFileInfo
public CompilationState Status { get; internal set; }
public string? Exception { get; internal set; }

public ImmutableArray<CompilationDiagnosticViewModel> Errors { get; internal set; } = ImmutableArray<CompilationDiagnosticViewModel>.Empty;
public ImmutableArray<CompilationDiagnosticViewModel> Warnings { get; internal set; } = ImmutableArray<CompilationDiagnosticViewModel>.Empty;

/// <summary>Gets or sets the virtual path to the view.</summary>
public string VirtualPath { get; }

Expand Down Expand Up @@ -46,5 +50,38 @@ private static bool IsDothtmlFile(string virtualPath)
virtualPath.IndexOf(".dotlayout", StringComparison.OrdinalIgnoreCase) > -1
);
}

public sealed record CompilationDiagnosticViewModel(
DiagnosticSeverity Severity,
string Message,
string? FileName,
int? LineNumber,
int? ColumnNumber,
string? SourceLine,
int? HighlightLength
)
{
public string? SourceLine { get; set; } = SourceLine;
public string? SourceLinePrefix => SourceLine?.Remove(ColumnNumber ?? 0);
public string? SourceLineHighlight =>
HighlightLength is {} len ? SourceLine?.Substring(ColumnNumber ?? 0, len)
: SourceLine?.Substring(ColumnNumber ?? 0);
public string? SourceLineSuffix =>
(ColumnNumber + HighlightLength) is int startIndex ? SourceLine?.Substring(startIndex) : null;


public CompilationDiagnosticViewModel(DotvvmCompilationDiagnostic diagnostic, string? contextLine)
: this(
diagnostic.Severity,
diagnostic.Message,
diagnostic.Location.FileName,
diagnostic.Location.LineNumber,
diagnostic.Location.ColumnNumber,
contextLine,
diagnostic.Location.LineErrorLength
)
{
}
}
}
}
189 changes: 189 additions & 0 deletions src/Framework/Framework/Compilation/DotvvmCompilationDiagnostic.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using DotVVM.Framework.Binding.Properties;
using DotVVM.Framework.Compilation.Parser;
using DotVVM.Framework.Hosting;
using System.Linq;
using DotVVM.Framework.Compilation.Parser.Dothtml.Parser;
using System;
using DotVVM.Framework.Compilation.ControlTree.Resolved;
using DotVVM.Framework.Binding;
using Newtonsoft.Json;
using DotVVM.Framework.Binding.Expressions;

namespace DotVVM.Framework.Compilation
{
/// <summary> Represents a dothtml compilation error or a warning, along with its location. </summary>
public record DotvvmCompilationDiagnostic: IEquatable<DotvvmCompilationDiagnostic>
{
public DotvvmCompilationDiagnostic(
string message,
DiagnosticSeverity severity,
DotvvmCompilationSourceLocation? location,
IEnumerable<DotvvmCompilationDiagnostic>? notes = null,
Exception? innerException = null)
{
Message = message;
Severity = severity;
Location = location ?? DotvvmCompilationSourceLocation.Unknown;
Notes = notes?.ToImmutableArray() ?? ImmutableArray<DotvvmCompilationDiagnostic>.Empty;
InnerException = innerException;
}

public string Message { get; init; }
public Exception? InnerException { get; init; }
public DiagnosticSeverity Severity { get; init; }
public DotvvmCompilationSourceLocation Location { get; init; }
public ImmutableArray<DotvvmCompilationDiagnostic> Notes { get; init; }
/// <summary> Errors with lower number are preferred when selecting the primary fault to the user. When equal, errors are sorted based on the location. 0 is default for semantic errors, 100 for parser errors and 200 for tokenizer errors. </summary>
public int Priority { get; init; }

public bool IsError => Severity == DiagnosticSeverity.Error;
public bool IsWarning => Severity == DiagnosticSeverity.Warning;

public override string ToString() =>
$"{Severity}: {Message}\n at {Location?.ToString() ?? "unknown location"}";
}

public sealed record DotvvmCompilationSourceLocation
{
public string? FileName { get; init; }
[JsonIgnore]
public MarkupFile? MarkupFile { get; init; }
[JsonIgnore]
public IEnumerable<TokenBase>? Tokens { get; init; }
public int? LineNumber { get; init; }
public int? ColumnNumber { get; init; }
public int LineErrorLength { get; init; }
[JsonIgnore]
public DothtmlNode? RelatedSyntaxNode { get; init; }
[JsonIgnore]
public ResolvedTreeNode? RelatedResolvedNode { get; init; }
public DotvvmProperty? RelatedProperty { get; init; }
public IBinding? RelatedBinding { get; init; }

public Type? RelatedControlType => this.RelatedResolvedNode?.GetAncestors(true).OfType<ResolvedControl>().FirstOrDefault()?.Metadata.Type;

public DotvvmCompilationSourceLocation(
string? fileName,
MarkupFile? markupFile,
IEnumerable<TokenBase>? tokens,
int? lineNumber = null,
int? columnNumber = null,
int? lineErrorLength = null)
{
if (tokens is {})
{
tokens = tokens.ToArray();
lineNumber ??= tokens.FirstOrDefault()?.LineNumber;
columnNumber ??= tokens.FirstOrDefault()?.ColumnNumber;
lineErrorLength ??= tokens.Where(t => t.LineNumber == lineNumber).Select(t => (int?)(t.ColumnNumber + t.Length)).LastOrDefault() - columnNumber;
}

this.MarkupFile = markupFile;
this.FileName = fileName ?? markupFile?.FileName;
this.Tokens = tokens;
this.LineNumber = lineNumber;
this.ColumnNumber = columnNumber;
this.LineErrorLength = lineErrorLength ?? 0;
}

public DotvvmCompilationSourceLocation(
IEnumerable<TokenBase> tokens): this(fileName: null, null, tokens) { }
public DotvvmCompilationSourceLocation(
DothtmlNode syntaxNode, IEnumerable<TokenBase>? tokens = null)
: this(fileName: null, null, tokens ?? syntaxNode?.Tokens)
{
RelatedSyntaxNode = syntaxNode;
}
public DotvvmCompilationSourceLocation(
ResolvedTreeNode resolvedNode, DothtmlNode? syntaxNode = null, IEnumerable<TokenBase>? tokens = null)
: this(
syntaxNode ?? resolvedNode.GetAncestors(true).FirstOrDefault(n => n.DothtmlNode is {})?.DothtmlNode!,
tokens
)
{
RelatedResolvedNode = resolvedNode;
if (resolvedNode.GetAncestors().OfType<ResolvedPropertySetter>().FirstOrDefault() is {} property)
RelatedProperty = property.Property;
}

public static readonly DotvvmCompilationSourceLocation Unknown = new(fileName: null, null, null);
public bool IsUnknown => FileName is null && MarkupFile is null && Tokens is null && LineNumber is null && ColumnNumber is null;

public string[] AffectedSpans
{
get
{
if (Tokens is null || !Tokens.Any())
return Array.Empty<string>();
var ts = Tokens.ToArray();
var r = new List<string> { ts[0].Text };
for (int i = 1; i < ts.Length; i++)
{
if (ts[i].StartPosition == ts[i - 1].EndPosition)
r[r.Count - 1] += ts[i].Text;
else
r.Add(ts[i].Text);
}
return r.ToArray();
}
}

public (int start, int end)[] AffectedRanges
{
get
{
if (Tokens is null || !Tokens.Any())
return Array.Empty<(int, int)>();
var ts = Tokens.ToArray();
var r = new (int start, int end)[ts.Length];
r[0] = (ts[0].StartPosition, ts[0].EndPosition);
int ri = 0;
for (int i = 1; i < ts.Length; i++)
{
if (ts[i].StartPosition == ts[i - 1].EndPosition)
r[i].end = ts[i].EndPosition;
else
{
ri += 1;
r[ri] = (ts[i].StartPosition, ts[i].EndPosition);
}
}
return r.AsSpan(0, ri + 1).ToArray();
}
}

public int? EndLineNumber => Tokens?.LastOrDefault()?.LineNumber ?? LineNumber;
public int? EndColumnNumber => (Tokens?.LastOrDefault()?.ColumnNumber + Tokens?.LastOrDefault()?.Length) ?? ColumnNumber;

public override string ToString()
{
if (IsUnknown)
return "Unknown location";
else if (FileName is {} && LineNumber is {})
{
// MSBuild-style file location
return $"{FileName}({LineNumber}{(ColumnNumber is {} ? "," + ColumnNumber : "")})";
}
else
{
// only position, plus add the affected spans
var location =
LineNumber is {} && ColumnNumber is {} ? $"{LineNumber},{ColumnNumber}: " :
LineNumber is {} ? $"{LineNumber}: " :
"";
return $"{location}{string.Join("; ", AffectedSpans)}";
}
}

public DotvvmLocationInfo ToRuntimeLocation() =>
new DotvvmLocationInfo(
this.FileName,
this.AffectedRanges,
this.LineNumber,
this.RelatedControlType,
this.RelatedProperty
);
}
}
Loading

0 comments on commit bccb5e6

Please sign in to comment.