Skip to content

Commit

Permalink
Merge pull request #1762 from riganti/compilation-page-warnings
Browse files Browse the repository at this point in the history
Refactor compilation diagnostics, add them to compilation status page
  • Loading branch information
exyi committed Feb 8, 2024
2 parents 53c3da6 + f056894 commit 7faf5a0
Show file tree
Hide file tree
Showing 43 changed files with 1,118 additions and 310 deletions.
Expand Up @@ -180,11 +180,12 @@ protected virtual void ResolveRootContent(DothtmlRootNode root, IAbstractControl
}
catch (DotvvmCompilationException ex)
{
if (ex.Tokens == null)
if (ex.Tokens.IsEmpty)
{
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
Expand Up @@ -106,17 +106,22 @@ public DefaultControlBuilderFactory(DotvvmConfiguration configuration, IMarkupFi
var compilationService = configuration.ServiceProvider.GetService<IDotvvmViewCompilationService>();
void editCompilationException(DotvvmCompilationException ex)
{
if (ex.FileName == null)
if (ex.FileName is null || ex.FileName == file.FullPath || ex.FileName == file.FileName)
{
ex.FileName = file.FullPath;
ex.SetFile(file.FullPath, file);
}
else if (!Path.IsPathRooted(ex.FileName))
else if (ex.MarkupFile is null)
{
ex.FileName = Path.Combine(
file.FullPath.Remove(file.FullPath.Length - file.FileName.Length),
ex.FileName);
// try to load the markup file of this error
try
{
var exceptionFile = GetMarkupFile(ex.FileName);
ex.SetFile(exceptionFile.file.FullPath, exceptionFile.file);
}
catch { }
}
}

try
{
var sw = ValueStopwatch.StartNew();
Expand Down
106 changes: 106 additions & 0 deletions src/Framework/Framework/Compilation/DiagnosticsCompilationTracer.cs
@@ -0,0 +1,106 @@
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
{
/// <summary> Instrumets DotVVM view compilation, traced events are defined in <see cref="Handle" />.
/// The tracers are found using IServiceProvider, to register your tracer, add it to DI with <c>service.AddSingleton&lt;IDiagnosticsCompilationTracer, MyTracer>()</c> </summary>
public interface IDiagnosticsCompilationTracer
{
Handle CompilationStarted(string file, string sourceCode);
/// <summary> Traces compilation of a single file, created in the <see cref="CompilationStarted(string, string)"/> method. Note that the class can also implement <see cref="IDisposable" />. </summary>
abstract class Handle
{
/// <summary> Called after the DotHTML file is parsed and syntax tree is created. Called even when there are errors. </summary>
public virtual void Parsed(List<DothtmlToken> tokens, DothtmlRootNode syntaxTree) { }
/// <summary> Called after the entire tree has resolved types - controls have assigned type, attributes have assigned DotvvmProperty, bindings are compiled, ... </summary>
public virtual void Resolved(ResolvedTreeRoot tree, ControlBuilderDescriptor descriptor) { }
/// <summary> After initial resolving, the tree is post-processed using a number of visitors (<see cref="DataContextPropertyAssigningVisitor"/>, <see cref="Styles.StylingVisitor" />, <see cref="LiteralOptimizationVisitor" />, ...). After each visitor processing, this method is called. </summary>
public virtual void AfterVisitor(ResolvedControlTreeVisitor visitor, ResolvedTreeRoot tree) { }
/// <summary> For each compilation diagnostic (warning/error), this method is called. </summary>
/// <param name="contextLine"> The line of code where the error occured. </param>
public virtual void CompilationDiagnostic(DotvvmCompilationDiagnostic diagnostic, string? contextLine) { }
/// <summary> Called if the compilation fails for any reason. Normally, <paramref name="exception"/> will be of type <see cref="DotvvmCompilationDiagnostic" /> </summary>
public virtual void Failed(Exception exception) { }
}
/// <summary> Singleton tracing handle which does nothing. </summary>
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
@@ -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
)
{
}
}
}
}
188 changes: 188 additions & 0 deletions src/Framework/Framework/Compilation/DotvvmCompilationDiagnostic.cs
@@ -0,0 +1,188 @@
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 ImmutableArray<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)
{
this.Tokens = tokens?.ToImmutableArray() ?? ImmutableArray<TokenBase>.Empty;
if (this.Tokens.Length > 0)
{
lineNumber ??= this.Tokens[0].LineNumber;
columnNumber ??= this.Tokens[0].ColumnNumber;
lineErrorLength ??= this.Tokens.Where(t => t.LineNumber == lineNumber).Select(t => (int?)(t.ColumnNumber + t.Length)).LastOrDefault() - columnNumber;
}

this.MarkupFile = markupFile;
this.FileName = fileName ?? markupFile?.FileName;
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.IsEmpty && LineNumber is null && ColumnNumber is null;

/// <summary> Text of the affected tokens. Consecutive tokens are concatenated - usually, this returns a single element array. </summary>
public string[] AffectedSpans
{
get
{
if (Tokens.IsEmpty)
return Array.Empty<string>();
var spans = new List<string> { Tokens[0].Text };
for (int i = 1; i < Tokens.Length; i++)
{
if (Tokens[i].StartPosition == Tokens[i - 1].EndPosition)
spans[spans.Count - 1] += Tokens[i].Text;
else
spans.Add(Tokens[i].Text);
}
return spans.ToArray();
}
}

/// <summary> Ranges of the affected tokens (in UTF-16 codepoint positions). Consecutive rangess are merged - usually, this returns a single element array. </summary>
public (int start, int end)[] AffectedRanges
{
get
{
if (Tokens.IsEmpty)
return Array.Empty<(int, int)>();
var ranges = new (int start, int end)[Tokens.Length];
ranges[0] = (Tokens[0].StartPosition, Tokens[0].EndPosition);
int ri = 0;
for (int i = 1; i < Tokens.Length; i++)
{
if (Tokens[i].StartPosition == Tokens[i - 1].EndPosition)
ranges[i].end = Tokens[i].EndPosition;
else
{
ri += 1;
ranges[ri] = (Tokens[i].StartPosition, Tokens[i].EndPosition);
}
}
return ranges.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
);
}
}

0 comments on commit 7faf5a0

Please sign in to comment.