-
Notifications
You must be signed in to change notification settings - Fork 97
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor compilation diagnostics, add them to compilation status page
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
Showing
43 changed files
with
1,095 additions
and
307 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
src/Framework/Framework/Compilation/DiagnosticsCompilationTracer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
189 changes: 189 additions & 0 deletions
189
src/Framework/Framework/Compilation/DotvvmCompilationDiagnostic.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
} | ||
} |
Oops, something went wrong.