diff --git a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs index bd1eae2fca..6c4c325b50 100644 --- a/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs +++ b/src/Framework/Framework/Compilation/ControlTree/ControlTreeResolverBase.cs @@ -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; diff --git a/src/Framework/Framework/Compilation/DefaultControlBuilderFactory.cs b/src/Framework/Framework/Compilation/DefaultControlBuilderFactory.cs index c184f5a10d..e0f43b53dc 100644 --- a/src/Framework/Framework/Compilation/DefaultControlBuilderFactory.cs +++ b/src/Framework/Framework/Compilation/DefaultControlBuilderFactory.cs @@ -106,16 +106,12 @@ public DefaultControlBuilderFactory(DotvvmConfiguration configuration, IMarkupFi var compilationService = configuration.ServiceProvider.GetService(); 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 { diff --git a/src/Framework/Framework/Compilation/DiagnosticsCompilationTracer.cs b/src/Framework/Framework/Compilation/DiagnosticsCompilationTracer.cs new file mode 100644 index 0000000000..bb86f794d4 --- /dev/null +++ b/src/Framework/Framework/Compilation/DiagnosticsCompilationTracer.cs @@ -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 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 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 switch { + [] => IDiagnosticsCompilationTracer.NopHandle.Instance, + [ var a ] => a, + _ => 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 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(); + } + } + } +} diff --git a/src/Framework/Framework/Compilation/DotHtmlFileInfo.cs b/src/Framework/Framework/Compilation/DotHtmlFileInfo.cs index 6a76358030..c9dee547c3 100644 --- a/src/Framework/Framework/Compilation/DotHtmlFileInfo.cs +++ b/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 { @@ -9,6 +10,9 @@ public sealed class DotHtmlFileInfo public CompilationState Status { get; internal set; } public string? Exception { get; internal set; } + public ImmutableArray Errors { get; internal set; } = ImmutableArray.Empty; + public ImmutableArray Warnings { get; internal set; } = ImmutableArray.Empty; + /// Gets or sets the virtual path to the view. public string VirtualPath { get; } @@ -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 + ) + { + } + } } } diff --git a/src/Framework/Framework/Compilation/DotvvmCompilationDiagnostic.cs b/src/Framework/Framework/Compilation/DotvvmCompilationDiagnostic.cs new file mode 100644 index 0000000000..b729cd2d68 --- /dev/null +++ b/src/Framework/Framework/Compilation/DotvvmCompilationDiagnostic.cs @@ -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 +{ + /// Represents a dothtml compilation error or a warning, along with its location. + public record DotvvmCompilationDiagnostic: IEquatable + { + public DotvvmCompilationDiagnostic( + string message, + DiagnosticSeverity severity, + DotvvmCompilationSourceLocation? location, + IEnumerable? notes = null, + Exception? innerException = null) + { + Message = message; + Severity = severity; + Location = location ?? DotvvmCompilationSourceLocation.Unknown; + Notes = notes?.ToImmutableArray() ?? ImmutableArray.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 Notes { get; init; } + /// 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. + 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? 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().FirstOrDefault()?.Metadata.Type; + + public DotvvmCompilationSourceLocation( + string? fileName, + MarkupFile? markupFile, + IEnumerable? 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 tokens): this(fileName: null, null, tokens) { } + public DotvvmCompilationSourceLocation( + DothtmlNode syntaxNode, IEnumerable? tokens = null) + : this(fileName: null, null, tokens ?? syntaxNode?.Tokens) + { + RelatedSyntaxNode = syntaxNode; + } + public DotvvmCompilationSourceLocation( + ResolvedTreeNode resolvedNode, DothtmlNode? syntaxNode = null, IEnumerable? tokens = null) + : this( + syntaxNode ?? resolvedNode.GetAncestors(true).FirstOrDefault(n => n.DothtmlNode is {})?.DothtmlNode!, + tokens + ) + { + RelatedResolvedNode = resolvedNode; + if (resolvedNode.GetAncestors().OfType().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(); + var ts = Tokens.ToArray(); + var r = new List { 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 + ); + } +} diff --git a/src/Framework/Framework/Compilation/DotvvmCompilationException.cs b/src/Framework/Framework/Compilation/DotvvmCompilationException.cs index c1c4fef1ac..42019fd3ec 100644 --- a/src/Framework/Framework/Compilation/DotvvmCompilationException.cs +++ b/src/Framework/Framework/Compilation/DotvvmCompilationException.cs @@ -3,55 +3,90 @@ using System.IO; using System.Linq; using System.Runtime.Serialization; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Binding.Expressions; +using DotVVM.Framework.Binding.Properties; +using DotVVM.Framework.Compilation.ControlTree.Resolved; using DotVVM.Framework.Compilation.Parser; +using DotVVM.Framework.Compilation.Parser.Dothtml.Parser; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Hosting; +using DotVVM.Framework.ResourceManagement; +using DotVVM.Framework.Runtime; +using Newtonsoft.Json; namespace DotVVM.Framework.Compilation { + /// Represents a failed dotvvm compilation result. The exception contains a list of all errors and warnings (). For the exception message, one error is selected as the "primary", usually it's the first encountered error. [Serializable] - public class DotvvmCompilationException : Exception + public class DotvvmCompilationException : Exception, IDotvvmException { - - public string? FileName { get; set; } + public string? FileName + { + get => CompilationError.Location.FileName; + set => SetFile(value, null); + } + [JsonIgnore] + public MarkupFile? MarkupFile => CompilationError.Location.MarkupFile; + [JsonIgnore] public string? SystemFileName => FileName?.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); - public IEnumerable? Tokens { get; set; } + /// Affected tokens of the primary compilation error. + [JsonIgnore] + public IEnumerable? Tokens => CompilationError.Location?.Tokens; + /// Line number of the primary compilation error. + public int? LineNumber => CompilationError.Location?.LineNumber; + /// Position on the line of the primary compilation error. + [JsonIgnore] + public int? ColumnNumber => CompilationError.Location?.ColumnNumber; - public int? ColumnNumber { get; set; } + /// Text of the affected tokens of the first error. + [JsonIgnore] + public string[] AffectedSpans => CompilationError.Location?.AffectedSpans ?? Array.Empty(); - public int? LineNumber { get; set; } + /// The primary compilation error. + public DotvvmCompilationDiagnostic CompilationError { get; set; } + /// All diagnostics except the primary compilation error. + public List OtherDiagnostics { get; } = new List(); - public string[] AffectedSpans - { - get - { - if (Tokens is null || !Tokens.Any()) return new string[0]; - var ts = Tokens.ToArray(); - var r = new List { 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(); - } - } + [JsonIgnore] + public IEnumerable AllDiagnostics => Enumerable.Concat(new [] { CompilationError }, OtherDiagnostics); + [JsonIgnore] + public IEnumerable AllErrors => Enumerable.Concat(new [] { CompilationError }, OtherDiagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)); + + Exception IDotvvmException.TheException => this; + DotvvmProperty? IDotvvmException.RelatedProperty => this.CompilationError.Location.RelatedProperty; - public DotvvmCompilationException(string message) : base(message) { } + DotvvmBindableObject? IDotvvmException.RelatedControl => null; - public DotvvmCompilationException(string message, Exception? innerException) : base(message, innerException) { } + IBinding? IDotvvmException.RelatedBinding => this.CompilationError.Location.RelatedBinding; + + ResolvedTreeNode? IDotvvmException.RelatedResolvedControl => this.CompilationError.Location.RelatedResolvedNode; + + DothtmlNode? IDotvvmException.RelatedDothtmlNode => this.CompilationError.Location.RelatedSyntaxNode; + + IResource? IDotvvmException.RelatedResource => null; + + DotvvmLocationInfo? IDotvvmException.Location => this.CompilationError.Location.ToRuntimeLocation(); + + public DotvvmCompilationException(string message) : this(message, innerException: null) { } + + public DotvvmCompilationException(string message, Exception? innerException) : base(message, innerException) + { + CompilationError = new DotvvmCompilationDiagnostic(message, DiagnosticSeverity.Error, null, innerException: innerException); + } public DotvvmCompilationException(string message, Exception? innerException, IEnumerable? tokens) : base(message, innerException) { - if (tokens != null) - { - if (!(tokens is IList)) tokens = tokens.ToArray(); - this.Tokens = tokens; - LineNumber = tokens.FirstOrDefault()?.LineNumber; - ColumnNumber = tokens.FirstOrDefault()?.ColumnNumber; - } + var location = tokens is null ? DotvvmCompilationSourceLocation.Unknown : new DotvvmCompilationSourceLocation(tokens); + CompilationError = new DotvvmCompilationDiagnostic(message, DiagnosticSeverity.Error, location, innerException: innerException); + } + + public DotvvmCompilationException(DotvvmCompilationDiagnostic primaryError, IEnumerable allDiagnostics) : base(primaryError.Message, primaryError.InnerException) + { + this.CompilationError = primaryError; + this.OtherDiagnostics = allDiagnostics.Where(d => (object)primaryError != d).ToList(); } public DotvvmCompilationException(string message, IEnumerable? tokens) : this(message, null, tokens) { } @@ -59,6 +94,37 @@ protected DotvvmCompilationException( SerializationInfo info, StreamingContext context) : base(info, context) { + CompilationError = new DotvvmCompilationDiagnostic(this.Message, DiagnosticSeverity.Error, null, innerException: this.InnerException); + } + + /// Creates a compilation error if the provided list of diagnostics contains an error. + public static DotvvmCompilationException? TryCreateFromDiagnostics(IEnumerable diagnostics) + { + // we sort by the end position of the error range to prefer more specific errors in case there is an overlap + // for example, binding have 2 errors, one for the entire binding and a more specific error highlighting the problematic binding token + var sorted = diagnostics.OrderBy(e => (-e.Priority, e.Location.EndLineNumber ?? int.MaxValue, e.Location.EndColumnNumber ?? int.MaxValue)).ToArray(); + if (sorted.FirstOrDefault(e => e.IsError) is {} error) + { + return new DotvvmCompilationException(error, sorted); + } + return null; + } + + public void SetFile(string? fileName, MarkupFile? file) + { + if (fileName == CompilationError.Location.FileName && file == CompilationError.Location.MarkupFile) + return; + + var oldFileName = CompilationError.Location.FileName; + CompilationError = CompilationError with { Location = CompilationError.Location with { FileName = fileName, MarkupFile = file } }; + + // also change other diagnostics, if they were from the same file name + for (int i = 0; i < OtherDiagnostics.Count; i++) + { + var d = OtherDiagnostics[i]; + if (d.Location.FileName == oldFileName) + OtherDiagnostics[i] = d with { Location = d.Location with { FileName = fileName, MarkupFile = file } }; + } } } } diff --git a/src/Framework/Framework/Compilation/DotvvmLocationInfo.cs b/src/Framework/Framework/Compilation/DotvvmLocationInfo.cs index eaa5553572..fac627eac0 100644 --- a/src/Framework/Framework/Compilation/DotvvmLocationInfo.cs +++ b/src/Framework/Framework/Compilation/DotvvmLocationInfo.cs @@ -6,6 +6,7 @@ namespace DotVVM.Framework.Compilation { /// /// Contains debug information about original binding location. + /// Used at runtime, so this object avoids referencing compile-time nodes to allow their garbage collection /// public sealed record DotvvmLocationInfo( string? FileName, diff --git a/src/Framework/Framework/Compilation/DotvvmViewCompilationService.cs b/src/Framework/Framework/Compilation/DotvvmViewCompilationService.cs index a88597dbe0..241f646af9 100644 --- a/src/Framework/Framework/Compilation/DotvvmViewCompilationService.cs +++ b/src/Framework/Framework/Compilation/DotvvmViewCompilationService.cs @@ -1,27 +1,30 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; -using DotVVM.Framework.Compilation.Parser; using DotVVM.Framework.Configuration; -using DotVVM.Framework.Controls.Infrastructure; using Microsoft.Extensions.DependencyInjection; -using DotVVM.Framework.Utils; +using DotVVM.Framework.Hosting; +using DotVVM.Framework.Binding.Properties; namespace DotVVM.Framework.Compilation { public class DotvvmViewCompilationService : IDotvvmViewCompilationService { - private IControlBuilderFactory controlBuilderFactory; + private readonly IControlBuilderFactory controlBuilderFactory; + private readonly CompilationTracer tracer; + private readonly IMarkupFileLoader markupFileLoader; private readonly DotvvmConfiguration dotvvmConfiguration; - public DotvvmViewCompilationService(DotvvmConfiguration dotvvmConfiguration, IControlBuilderFactory controlBuilderFactory) + public DotvvmViewCompilationService(DotvvmConfiguration dotvvmConfiguration, IControlBuilderFactory controlBuilderFactory, CompilationTracer tracer, IMarkupFileLoader markupFileLoader) { this.dotvvmConfiguration = dotvvmConfiguration; this.controlBuilderFactory = controlBuilderFactory; + this.tracer = tracer; + this.markupFileLoader = markupFileLoader; masterPages = new Lazy>(InitMasterPagesCollection); controls = new Lazy>(InitControls); routes = new Lazy>(InitRoutes); @@ -188,22 +191,103 @@ public void RegisterCompiledView(string file, ViewCompiler.ControlBuilderDescrip routes.Value.FirstOrDefault(t => t.VirtualPath == file) ?? controls.Value.FirstOrDefault(t => t.VirtualPath == file) ?? masterPages.Value.GetOrAdd(file, path => new DotHtmlFileInfo(path)); + + var tracerData = this.tracer.CompiledViews.GetValueOrDefault(file); + fileInfo.Exception = null; + + var diagnostics = tracerData?.Diagnostics ?? Enumerable.Empty(); + if (exception is null) { fileInfo.Status = CompilationState.CompletedSuccessfully; - fileInfo.Exception = null; } else { fileInfo.Status = CompilationState.CompilationFailed; fileInfo.Exception = exception.Message; + + if (exception is DotvvmCompilationException compilationException) + { + // overwrite the tracer diagnostics to avoid presenting duplicates + diagnostics = compilationException.AllDiagnostics.Select(x => new DotHtmlFileInfo.CompilationDiagnosticViewModel(x, null)).ToArray(); + + AddSourceLines(diagnostics, compilationException.AllDiagnostics); + } } + fileInfo.Errors = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToImmutableArray(); + fileInfo.Warnings = diagnostics.Where(d => d.Severity == DiagnosticSeverity.Warning).ToImmutableArray(); + if (descriptor?.MasterPage is { FileName: {} masterPagePath }) { masterPages.Value.GetOrAdd(masterPagePath, path => new DotHtmlFileInfo(path)); } } + + private void AddSourceLines(IEnumerable viewModels, IEnumerable originalDiagnostics) + { + var markupFiles = new Dictionary(); + foreach (var d in originalDiagnostics) + { + if (d.Location.FileName is null) + continue; + + if (!markupFiles.TryAdd(d.Location.FileName, d.Location.MarkupFile)) + markupFiles[d.Location.FileName] ??= d.Location.MarkupFile; + } + var sourceCodes = new Dictionary(); + foreach (var fileName in viewModels.Where(vm => vm.FileName is {} && vm.LineNumber is {} && vm.SourceLine is null).Select(vm => vm.FileName).Distinct()) + { + var markupFile = markupFiles.GetValueOrDefault(fileName!) ?? markupFileLoader.GetMarkup(this.dotvvmConfiguration, fileName!); + var sourceCode = markupFile?.ReadContent(); + if (sourceCode is {}) + sourceCodes.Add(fileName!, sourceCode.Split('\n')); + } + foreach (var d in viewModels) + { + if (d.FileName is null || d.LineNumber is not > 0 || d.SourceLine is {}) + continue; + var source = sourceCodes.GetValueOrDefault(d.FileName); + + if (source is null || d.LineNumber!.Value > source.Length) + continue; + + d.SourceLine = source[d.LineNumber.Value - 1]; + } + } + + public class CompilationTracer : IDiagnosticsCompilationTracer + { + internal readonly ConcurrentDictionary CompiledViews = new ConcurrentDictionary(); + public IDiagnosticsCompilationTracer.Handle CompilationStarted(string file, string sourceCode) + { + return new Handle(this, file); + } + + internal sealed class Handle : IDiagnosticsCompilationTracer.Handle, IDisposable + { + private readonly CompilationTracer compilationTracer; + public string File { get; } + public DateTime CompiledAt { get; } = DateTime.UtcNow; + public List Diagnostics = new(); + + public Handle(CompilationTracer compilationTracer, string file) + { + this.compilationTracer = compilationTracer; + this.File = file; + } + + public override void CompilationDiagnostic(DotvvmCompilationDiagnostic diagnostic, string? contextLine) + { + Diagnostics.Add(new (diagnostic, contextLine)); + } + + public void Dispose() + { + compilationTracer.CompiledViews[this.File] = this; + } + } + } } } diff --git a/src/Framework/Framework/Compilation/ErrorCheckingVisitor.cs b/src/Framework/Framework/Compilation/ErrorCheckingVisitor.cs index eb3002182a..3d512b4053 100644 --- a/src/Framework/Framework/Compilation/ErrorCheckingVisitor.cs +++ b/src/Framework/Framework/Compilation/ErrorCheckingVisitor.cs @@ -1,21 +1,95 @@ using System; +using System.Collections.Generic; using System.Linq; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Binding.Properties; +using DotVVM.Framework.Compilation.Binding; using DotVVM.Framework.Compilation.ControlTree.Resolved; +using DotVVM.Framework.Compilation.Parser; +using DotVVM.Framework.Compilation.Parser.Binding.Tokenizer; using DotVVM.Framework.Compilation.Parser.Dothtml.Parser; +using DotVVM.Framework.Compilation.Parser.Dothtml.Tokenizer; +using DotVVM.Framework.Hosting; using DotVVM.Framework.Utils; namespace DotVVM.Framework.Compilation { public class ErrorCheckingVisitor : ResolvedControlTreeVisitor { + public List Diagnostics { get; } = new(); + public string? FileName { get; set; } - public override void VisitControl(ResolvedControl control) + public ErrorCheckingVisitor(string? fileName) { - if (control.DothtmlNode is { HasNodeErrors: true }) + this.FileName = fileName; + } + + private void AddNodeErrors(DothtmlNode node, int priority) + { + if (!node.HasNodeErrors && !node.HasNodeWarnings) + return; + + var location = new DotvvmCompilationSourceLocation(node) { FileName = FileName }; + foreach (var error in node.NodeErrors) + { + Diagnostics.Add(new DotvvmCompilationDiagnostic(error, DiagnosticSeverity.Error, location) { Priority = priority }); + } + foreach (var warning in node.NodeWarnings) + { + Diagnostics.Add(new DotvvmCompilationDiagnostic(warning, DiagnosticSeverity.Warning, location)); + } + } + + DotvvmCompilationSourceLocation? MapBindingLocation(ResolvedBinding binding, DotvvmProperty? relatedProperty, BindingCompilationException error) + { + var tokens = error.Tokens?.ToArray(); + if (tokens is null or []) + return null; + + var valueNode = (binding.BindingNode as DothtmlBindingNode)?.ValueNode; + var valueToken = valueNode?.ValueToken; + + if (valueToken is null) + { + // create anonymous file for the one binding + var file = new MarkupFile("anonymous binding", "anonymos binding", error.Expression ?? ""); + return new DotvvmCompilationSourceLocation(file.FileName, file, tokens) { RelatedSyntaxNode = valueNode ?? binding.DothtmlNode, RelatedResolvedNode = binding, RelatedBinding = binding.Binding, RelatedProperty = relatedProperty }; + } + else { - throw new DotvvmCompilationException(string.Join("\r\n", control.DothtmlNode.NodeErrors), control.DothtmlNode.Tokens); + tokens = tokens.Select(t => t switch { + BindingToken bt => bt.RemapPosition(valueToken), + _ => t // dothtml tokens most likely already have correct position + }).ToArray(); + return new DotvvmCompilationSourceLocation(binding, valueNode, tokens) { RelatedBinding = binding.Binding, RelatedProperty = relatedProperty }; } - base.VisitControl(control); + } + + Dictionary AnnotateBindingExceptionWithLocation(ResolvedBinding binding, DotvvmProperty? relatedProperty, IEnumerable errors) + { + var result = new Dictionary(new ReferenceEqualityComparer()); + void recurse(Exception exception, DotvvmCompilationSourceLocation? location) + { + if (result.ContainsKey(exception)) + return; + + if (exception is BindingCompilationException bce) + location = MapBindingLocation(binding, relatedProperty, bce) ?? location; + + if (location is {}) + result[exception] = location; + + if (exception is AggregateException agg) + foreach (var inner in agg.InnerExceptions) + recurse(inner, location); + else if (exception.InnerException is {}) + recurse(exception.InnerException, location); + } + + foreach (var x in errors) + recurse(x, null); + + return result; } public override void VisitPropertyBinding(ResolvedPropertyBinding propertyBinding) @@ -23,29 +97,75 @@ public override void VisitPropertyBinding(ResolvedPropertyBinding propertyBindin var errors = propertyBinding.Binding.Errors; if (errors.HasErrors) { - // TODO: aggregate all errors from the page - throw new DotvvmCompilationException( + var bindingLocation = new DotvvmCompilationSourceLocation(propertyBinding.Binding, propertyBinding.Binding.BindingNode); + var detailedLocations = AnnotateBindingExceptionWithLocation(propertyBinding.Binding, propertyBinding.Property, errors.Exceptions); + foreach (var error in + from topException in errors.Exceptions + from exception in topException.AllInnerExceptions() + where exception is not AggregateException and not BindingPropertyException { InnerException: {} } and not BindingCompilationException { InnerException: {}, Message: "Binding compilation failed" } + let location = detailedLocations.GetValueOrDefault(exception) + let message = exception.Message + orderby location?.LineNumber ?? int.MaxValue, + location?.ColumnNumber ?? int.MaxValue, + location?.LineErrorLength ?? int.MaxValue, + exception.InnerException is null ? 0 : exception is BindingCompilationException ? -1 : 2 + group (topException, exception, location, message) by message into g + select g.First()) + { + var message = $"{error.exception.GetType().Name}: {error.message}"; + Diagnostics.Add(new DotvvmCompilationDiagnostic( + message, + DiagnosticSeverity.Error, + error.location ?? bindingLocation, + innerException: error.topException + )); + } + // summary error explaining which binding properties are causing the problem + Diagnostics.Add(new DotvvmCompilationDiagnostic( errors.GetErrorMessage(propertyBinding.Binding.Binding), - errors.Exceptions.FirstOrDefault(), - propertyBinding.Binding.BindingNode?.Tokens); + DiagnosticSeverity.Error, + bindingLocation, + innerException: errors.Exceptions.FirstOrDefault() + )); } base.VisitPropertyBinding(propertyBinding); } public override void VisitView(ResolvedTreeRoot view) { - if (view.DothtmlNode.HasNodeErrors) - { - throw new DotvvmCompilationException(string.Join("\r\n", view.DothtmlNode.NodeErrors), view.DothtmlNode.Tokens); - } - foreach (var directive in ((DothtmlRootNode) view.DothtmlNode).Directives) + base.VisitView(view); + } + + public void AddTokenizerErrors(List tokens) + { + foreach (var token in tokens) { - if (directive.HasNodeErrors) + if (token.Error is { IsCritical: var critical }) { - throw new DotvvmCompilationException(string.Join("\r\n", directive.NodeErrors), directive.Tokens); + var location = new DotvvmCompilationSourceLocation(new[] { (token.Error as BeginWithLastTokenOfTypeTokenError)?.LastToken ?? token }); + Diagnostics.Add(new DotvvmCompilationDiagnostic( + token.Error.ErrorMessage, + critical ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning, + location + ) { Priority = 200 }); } } - base.VisitView(view); + } + public void AddSyntaxErrors(DothtmlNode rootNode) + { + foreach (var node in rootNode.EnumerateNodes()) + { + AddNodeErrors(node, priority: 100); + } + } + + public void ThrowOnErrors() + { + var sorted = Diagnostics.OrderBy(e => (-e.Priority, e.Location.LineNumber ?? -1, e.Location.ColumnNumber ?? -1)).ToArray(); + if (sorted.FirstOrDefault(e => e.IsError) is {} error) + { + throw new DotvvmCompilationException(error, sorted); + } } } } diff --git a/src/Framework/Framework/Compilation/Parser/Binding/Tokenizer/BindingToken.cs b/src/Framework/Framework/Compilation/Parser/Binding/Tokenizer/BindingToken.cs index 46bce1f896..7529739f95 100644 --- a/src/Framework/Framework/Compilation/Parser/Binding/Tokenizer/BindingToken.cs +++ b/src/Framework/Framework/Compilation/Parser/Binding/Tokenizer/BindingToken.cs @@ -5,5 +5,16 @@ public class BindingToken : TokenBase public BindingToken(string text, BindingTokenType type, int lineNumber, int columnNumber, int length, int startPosition) : base(text, type, lineNumber, columnNumber, length, startPosition) { } + + public BindingToken RemapPosition(TokenBase parentToken) => + RemapPosition(parentToken.LineNumber, parentToken.ColumnNumber, parentToken.StartPosition); + public BindingToken RemapPosition(int startLine, int startColumn, int startPosition) + { + return new BindingToken(Text, Type, + startLine + this.LineNumber - 1, + this.LineNumber <= 1 ? startColumn + this.ColumnNumber : this.ColumnNumber, + Length, + this.StartPosition + startPosition); + } } } diff --git a/src/Framework/Framework/Compilation/Static/CompilationReport.cs b/src/Framework/Framework/Compilation/Static/CompilationReport.cs deleted file mode 100644 index 0cc58f3051..0000000000 --- a/src/Framework/Framework/Compilation/Static/CompilationReport.cs +++ /dev/null @@ -1,71 +0,0 @@ - -using System.Collections.Generic; - -namespace DotVVM.Framework.Compilation.Static -{ - internal class CompilationReport - { - private const string UnknownError = "An unknown error occurred. This is likely a bug in the compiler."; - - public CompilationReport(string viewPath, int line, int column, string message) - { - ViewPath = viewPath; - Line = line; - Column = column; - Message = message; - } - - public CompilationReport(string viewPath, DotvvmCompilationException exception) - : this( - viewPath: viewPath, - line: exception.LineNumber ?? -1, - column: exception.ColumnNumber ?? -1, - message: !string.IsNullOrEmpty(exception.Message) - ? exception.Message - : exception.InnerException?.ToString() ?? UnknownError) - { - } - - public string Message { get; } - - public int Line { get; } - - public int Column { get; } - - public string ViewPath { get; } - - public static bool operator ==(CompilationReport? left, CompilationReport? right) - { - if (left is null) - { - return right is null; - } - - return left.Equals(right); - } - - public static bool operator !=(CompilationReport? left, CompilationReport? right) - { - return !(left == right); - } - - public override bool Equals(object? obj) - { - return obj is CompilationReport report - && Message == report.Message - && Line == report.Line - && Column == report.Column - && ViewPath == report.ViewPath; - } - - public override int GetHashCode() - { - var hashCode = -712964631; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Message); - hashCode = hashCode * -1521134295 + Line.GetHashCode(); - hashCode = hashCode * -1521134295 + Column.GetHashCode(); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ViewPath); - return hashCode; - } - } -} diff --git a/src/Framework/Framework/Compilation/Static/DefaultCompilationReportLogger.cs b/src/Framework/Framework/Compilation/Static/DefaultCompilationReportLogger.cs index 50422b230a..445d07cca6 100644 --- a/src/Framework/Framework/Compilation/Static/DefaultCompilationReportLogger.cs +++ b/src/Framework/Framework/Compilation/Static/DefaultCompilationReportLogger.cs @@ -7,12 +7,12 @@ namespace DotVVM.Framework.Compilation.Static { internal class DefaultCompilationReportLogger : ICompilationReportLogger { - public void Log(Stream stream, IEnumerable reports) + public void Log(Stream stream, IEnumerable diagnostics) { using var writer = new StreamWriter(stream); - foreach (var report in reports) + foreach (var d in diagnostics) { - writer.WriteLine($"{report.ViewPath}({report.Line},{report.Column}): {report.Message}"); + writer.WriteLine($"{d.Location}: {d.Severity.ToString().ToLowerInvariant()}: {d.Message}"); } } } diff --git a/src/Framework/Framework/Compilation/Static/ICompilationReportLogger.cs b/src/Framework/Framework/Compilation/Static/ICompilationReportLogger.cs index 062f0bf1ff..51302d35c8 100644 --- a/src/Framework/Framework/Compilation/Static/ICompilationReportLogger.cs +++ b/src/Framework/Framework/Compilation/Static/ICompilationReportLogger.cs @@ -6,6 +6,6 @@ namespace DotVVM.Framework.Compilation.Static { internal interface ICompilationReportLogger { - void Log(Stream stream, IEnumerable reports); + void Log(Stream stream, IEnumerable diagnostics); } } diff --git a/src/Framework/Framework/Compilation/Static/StaticViewCompiler.cs b/src/Framework/Framework/Compilation/Static/StaticViewCompiler.cs index c758d26e41..c9a4fe7394 100644 --- a/src/Framework/Framework/Compilation/Static/StaticViewCompiler.cs +++ b/src/Framework/Framework/Compilation/Static/StaticViewCompiler.cs @@ -18,31 +18,31 @@ namespace DotVVM.Framework.Compilation.Static { internal static class StaticViewCompiler { - public static ImmutableArray CompileAll( + public static ImmutableArray CompileAll( Assembly dotvvmProjectAssembly, string dotvvmProjectDir) { var configuration = ConfigurationInitializer.GetConfiguration(dotvvmProjectAssembly, dotvvmProjectDir); - var reportsBuilder = ImmutableArray.CreateBuilder(); + var diagnostics = ImmutableArray.CreateBuilder(); var markupControls = configuration.Markup.Controls.Select(c => c.Src) .Where(p => !string.IsNullOrWhiteSpace(p)) .ToImmutableArray(); foreach (var markupControl in markupControls) { - reportsBuilder.AddRange(CompileNoThrow(configuration, markupControl!)); + diagnostics.AddRange(CompileNoThrow(configuration, markupControl!)); } var views = configuration.RouteTable.Select(r => r.VirtualPath).ToImmutableArray(); foreach(var view in views) { - reportsBuilder.AddRange(CompileNoThrow(configuration, view)); + diagnostics.AddRange(CompileNoThrow(configuration, view)); } - return reportsBuilder.Distinct().ToImmutableArray(); + return diagnostics.Distinct().ToImmutableArray(); } - private static ImmutableArray CompileNoThrow( + private static ImmutableArray CompileNoThrow( DotvvmConfiguration configuration, string viewPath) { @@ -50,7 +50,7 @@ private static ImmutableArray CompileNoThrow( var file = fileLoader.GetMarkup(configuration, viewPath); if (file is null) { - return ImmutableArray.Create(); + return ImmutableArray.Create(); } var sourceCode = file.ReadContent(); @@ -62,14 +62,12 @@ private static ImmutableArray CompileNoThrow( sourceCode: sourceCode, fileName: viewPath); _ = builderFactory(); - // TODO: Reporting compiler errors solely through exceptions is dumb. I have no way of getting all of - // the parser errors at once because they're reported through exceptions one at a time. We need - // to rewrite DefaultViewCompiler and its interface if the static linter/compiler is to be useful. - return ImmutableArray.Create(); + // TODO: get warnings from compilation tracer + return ImmutableArray.Create(); } catch(DotvvmCompilationException e) { - return ImmutableArray.Create(new CompilationReport(viewPath, e)); + return e.AllDiagnostics.ToImmutableArray(); } } } diff --git a/src/Framework/Framework/Compilation/Validation/ControlUsageValidationVisitor.cs b/src/Framework/Framework/Compilation/Validation/ControlUsageValidationVisitor.cs index 20d9d476ef..7e3a2c3c2a 100644 --- a/src/Framework/Framework/Compilation/Validation/ControlUsageValidationVisitor.cs +++ b/src/Framework/Framework/Compilation/Validation/ControlUsageValidationVisitor.cs @@ -3,12 +3,14 @@ using System.Linq; using DotVVM.Framework.Binding.Properties; using DotVVM.Framework.Compilation.ControlTree.Resolved; +using FastExpressionCompiler; namespace DotVVM.Framework.Compilation.Validation { public class ControlUsageValidationVisitor: ResolvedControlTreeVisitor { - public List<(ResolvedControl control, ControlUsageError err)> Errors { get; set; } = new List<(ResolvedControl, ControlUsageError)>(); + public List Errors { get; set; } = new(); + public bool WriteErrorsToNodes { get; set; } = true; readonly IControlUsageValidator validator; public ControlUsageValidationVisitor(IControlUsageValidator validator) { @@ -20,19 +22,32 @@ public override void VisitControl(ResolvedControl control) var err = validator.Validate(control); foreach (var e in err) { - Errors.Add((control, e)); - foreach (var node in e.Nodes) + var location = new DotvvmCompilationSourceLocation(control, e.Nodes.FirstOrDefault(), e.Nodes.SelectMany(n => n.Tokens)); + var msgPrefix = $"{control.Metadata.Type.ToCode(stripNamespace: true)} validation"; + if (location.LineNumber is {}) { - switch (e.Severity) + msgPrefix += $" at line {location.LineNumber}"; + } + Errors.Add(new DotvvmCompilationDiagnostic( + msgPrefix + ": " + e.ErrorMessage, + e.Severity, + location + ) { Priority = -1 }); + if (this.WriteErrorsToNodes) + { + foreach (var node in e.Nodes) { - case DiagnosticSeverity.Error: - node.AddError(e.ErrorMessage); - break; - case DiagnosticSeverity.Warning: - node.AddWarning(e.ErrorMessage); - break; - default: - break; + switch (e.Severity) + { + case DiagnosticSeverity.Error: + node.AddError(e.ErrorMessage); + break; + case DiagnosticSeverity.Warning: + node.AddWarning(e.ErrorMessage); + break; + default: + break; + } } } } @@ -44,15 +59,9 @@ public void VisitAndAssert(ResolvedTreeRoot view) { if (this.Errors.Any()) throw new Exception("The ControlUsageValidationVisitor has already collected some errors."); VisitView(view); - if (this.Errors.FirstOrDefault(e => e.err.Severity == DiagnosticSeverity.Error) is { err: {} } controlUsageError) + if (this.Errors.FirstOrDefault(e => e.Severity == DiagnosticSeverity.Error) is { } controlUsageError) { - var lineNumber = - controlUsageError.control.GetAncestors() - .Select(c => c.DothtmlNode) - .FirstOrDefault(n => n != null) - ?.Tokens.FirstOrDefault()?.LineNumber; - var message = $"Validation error in {controlUsageError.control.Metadata.Type.Name} at line {lineNumber}: {controlUsageError.err.ErrorMessage}"; - throw new DotvvmCompilationException(message, controlUsageError.err.Nodes.SelectMany(n => n.Tokens)); + throw new DotvvmCompilationException(controlUsageError, this.Errors); } } } diff --git a/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompiler.cs b/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompiler.cs index 5c67f77199..a4d703f2c1 100644 --- a/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompiler.cs +++ b/src/Framework/Framework/Compilation/ViewCompiler/DefaultViewCompiler.cs @@ -20,11 +20,13 @@ public class DefaultViewCompiler : IViewCompiler private readonly IBindingCompiler bindingCompiler; private readonly ViewCompilerConfiguration config; private readonly Func controlValidatorFactory; + private readonly CompositeDiagnosticsCompilationTracer tracer; private readonly ILogger? logger; - public DefaultViewCompiler(IOptions config, IControlTreeResolver controlTreeResolver, IBindingCompiler bindingCompiler, Func controlValidatorFactory, ILogger? logger = null) + public DefaultViewCompiler(IOptions config, IControlTreeResolver controlTreeResolver, IBindingCompiler bindingCompiler, Func controlValidatorFactory, CompositeDiagnosticsCompilationTracer tracer, ILogger? logger = null) { this.config = config.Value; + this.tracer = tracer; this.controlTreeResolver = controlTreeResolver; this.bindingCompiler = bindingCompiler; this.controlValidatorFactory = controlValidatorFactory; @@ -36,80 +38,118 @@ public DefaultViewCompiler(IOptions config, IControlT /// protected virtual (ControlBuilderDescriptor, Func>) CompileViewCore(string sourceCode, string fileName) { - // parse the document - var tokenizer = new DothtmlTokenizer(); - tokenizer.Tokenize(sourceCode); - var parser = new DothtmlParser(); - var node = parser.Parse(tokenizer.Tokens); - - var resolvedView = (ResolvedTreeRoot)controlTreeResolver.ResolveTree(node, fileName); - - var descriptor = resolvedView.ControlBuilderDescriptor; + var tracingHandle = tracer.CompilationStarted(fileName, sourceCode); + bool faultBlockHack(Exception e) + { + // avoids rethrowing exception and triggering the debugger by abusing + // the `filter` block to report the error + tracingHandle.Failed(e); + return false; + } + try + { + // parse the document + var tokenizer = new DothtmlTokenizer(); + tokenizer.Tokenize(sourceCode); + var parser = new DothtmlParser(); + var node = parser.Parse(tokenizer.Tokens); + tracingHandle.Parsed(tokenizer.Tokens, node); - return (descriptor, () => { + var resolvedView = (ResolvedTreeRoot)controlTreeResolver.ResolveTree(node, fileName); - var errorCheckingVisitor = new ErrorCheckingVisitor(); - resolvedView.Accept(errorCheckingVisitor); + var descriptor = resolvedView.ControlBuilderDescriptor; - foreach (var token in tokenizer.Tokens) - { - if (token.Error is { IsCritical: true }) + return (descriptor, () => { + try { - throw new DotvvmCompilationException(token.Error.ErrorMessage, new[] { (token.Error as BeginWithLastTokenOfTypeTokenError)?.LastToken ?? token }); - } - } + tracingHandle.Resolved(resolvedView, descriptor); + + // avoid visiting invalid tree, it could trigger crashes in styles + CheckErrors(fileName, sourceCode, tracingHandle, tokenizer.Tokens, node, resolvedView); + + foreach (var visitor in config.TreeVisitors) + { + var v = visitor(); + try + { + resolvedView.Accept(v); + tracingHandle.AfterVisitor(v, resolvedView); + } + finally + { + (v as IDisposable)?.Dispose(); + } + } - foreach (var n in node.EnumerateNodes()) - { - if (n.HasNodeErrors) - { - throw new DotvvmCompilationException(string.Join(", ", n.NodeErrors), n.Tokens); - } - } + var validationVisitor = this.controlValidatorFactory.Invoke(); + validationVisitor.WriteErrorsToNodes = false; + validationVisitor.DefaultVisit(resolvedView); + + // validate tree again for new errors from the visitors and warnings + var diagnostics = CheckErrors(fileName, sourceCode, tracingHandle, tokenizer.Tokens, node, resolvedView, additionalDiagnostics: validationVisitor.Errors); + LogDiagnostics(tracingHandle, diagnostics, fileName, sourceCode); - foreach (var visitor in config.TreeVisitors) - visitor().ApplyAction(resolvedView.Accept).ApplyAction(v => (v as IDisposable)?.Dispose()); + var emitter = new DefaultViewCompilerCodeEmitter(); + var compilingVisitor = new ViewCompilingVisitor(emitter, bindingCompiler); + resolvedView.Accept(compilingVisitor); - var validationVisitor = this.controlValidatorFactory.Invoke(); - validationVisitor.VisitAndAssert(resolvedView); - LogWarnings(resolvedView, sourceCode); + return compilingVisitor.BuildCompiledView; + } + catch (Exception e) when (faultBlockHack(e)) { throw; } + finally + { + (tracingHandle as IDisposable)?.Dispose(); + } + }); + } + catch (Exception e) when (faultBlockHack(e)) { throw; } + } - var emitter = new DefaultViewCompilerCodeEmitter(); - var compilingVisitor = new ViewCompilingVisitor(emitter, bindingCompiler); + private List CheckErrors(string fileName, string sourceCode, IDiagnosticsCompilationTracer.Handle tracingHandle, List tokens, DothtmlNode syntaxTree, ResolvedTreeRoot? resolvedTree, IEnumerable? additionalDiagnostics = null) + { + var errorChecker = new ErrorCheckingVisitor(fileName); + errorChecker.AddTokenizerErrors(tokens); + errorChecker.AddSyntaxErrors(syntaxTree); + resolvedTree?.Accept(errorChecker); - resolvedView.Accept(compilingVisitor); + if (additionalDiagnostics is { }) + { + errorChecker.Diagnostics.AddRange(additionalDiagnostics); + } - return compilingVisitor.BuildCompiledView; - }); + if (DotvvmCompilationException.TryCreateFromDiagnostics(errorChecker.Diagnostics) is {} error) + { + LogDiagnostics(tracingHandle, error.AllDiagnostics, fileName, sourceCode); + throw error; + } + return errorChecker.Diagnostics; } - private void LogWarnings(ResolvedTreeRoot resolvedView, string sourceCode) + private void LogDiagnostics(IDiagnosticsCompilationTracer.Handle tracingHandle, IEnumerable diagnostics, string fileName, string sourceCode) { - string[]? lines = null; - if (logger is null || resolvedView.DothtmlNode is null) return; + var warnings = diagnostics.Where(d => d.IsWarning || d.IsError).ToArray(); + if (warnings.Length == 0) return; + + var lines = sourceCode.Split('\n'); // Currently, all warnings are placed on syntax nodes (even when produced in control tree resolver) - foreach (var node in resolvedView.DothtmlNode.EnumerateNodes()) + foreach (var warning in warnings) { - if (node.HasNodeWarnings) - { - lines ??= sourceCode.Split('\n'); - var nodePosition = node.Tokens.FirstOrDefault(); - var sourceLine = nodePosition is { LineNumber: > 0 } && nodePosition.LineNumber <= lines.Length ? lines[nodePosition.LineNumber - 1] : null; - sourceLine = sourceLine?.TrimEnd(); - var highlightLength = 1; - if (sourceLine is {} && nodePosition is {}) - { - highlightLength = node.Tokens.Where(t => t.LineNumber == nodePosition?.LineNumber).Sum(t => t.Length); - highlightLength = Math.Max(1, Math.Min(highlightLength, sourceLine.Length - nodePosition.ColumnNumber + 1)); - } + var loc = warning.Location; + var sourceLine = loc.LineNumber > 0 && loc.LineNumber <= lines.Length ? lines[loc.LineNumber.Value - 1] : null; + sourceLine = sourceLine?.TrimEnd(); - foreach (var warning in node.NodeWarnings) - { - var logEvent = new CompilationWarning(warning, resolvedView.FileName, nodePosition?.LineNumber, nodePosition?.ColumnNumber, sourceLine, highlightLength); - logger.Log(LogLevel.Warning, 0, logEvent, null, (x, e) => x.ToString()); - } + var highlightLength = 1; + if (sourceLine is {} && loc is { ColumnNumber: {}, LineErrorLength: > 0 }) + { + highlightLength = loc.LineErrorLength; + highlightLength = Math.Max(1, Math.Min(highlightLength, sourceLine.Length - loc.ColumnNumber.Value + 1)); } + + var logEvent = new CompilationWarning(warning.Message, fileName, loc.LineNumber, loc.ColumnNumber, sourceLine, highlightLength); + logger?.Log(warning.IsWarning ? LogLevel.Warning : LogLevel.Error, 0, logEvent, null, (x, e) => x.ToString()); + + tracingHandle.CompilationDiagnostic(warning, sourceLine); } } diff --git a/src/Framework/Framework/Configuration/DotvvmCompilationPageConfiguration.cs b/src/Framework/Framework/Configuration/DotvvmCompilationPageConfiguration.cs index 142c5838a6..a08070bc37 100644 --- a/src/Framework/Framework/Configuration/DotvvmCompilationPageConfiguration.cs +++ b/src/Framework/Framework/Configuration/DotvvmCompilationPageConfiguration.cs @@ -125,6 +125,9 @@ public void Apply(DotvvmConfiguration config) url: Url, virtualPath: "embedded://DotVVM.Framework/Diagnostics/CompilationPage.dothtml"); + config.Markup.AddMarkupControl("dotvvm-internal", "CompilationDiagnostic", "embedded://DotVVM.Framework/Diagnostics/CompilationDiagnostic.dotcontrol"); + config.Markup.AddMarkupControl("dotvvm-internal", "CompilationDiagnosticRows", "embedded://DotVVM.Framework/Diagnostics/CompilationDiagnosticRows.dotcontrol"); + config.Security.RequireSecFetchHeaders.EnableForRoutes(RouteName); } diff --git a/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs b/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs index 80ebd5fada..580b3f6f7c 100644 --- a/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs +++ b/src/Framework/Framework/Controls/DotvvmBindableObjectHelper.cs @@ -485,20 +485,26 @@ static string ValueDebugString(object? value) return toStringed; } - private static (string? prefix, string tagName) FormatControlName(DotvvmBindableObject control, DotvvmConfiguration? config = null) + internal static (string? prefix, string tagName) FormatControlName(DotvvmBindableObject control, DotvvmConfiguration? config) { var type = control.GetType(); if (type == typeof(HtmlGenericControl)) return (null, ((HtmlGenericControl)control).TagName!); + return FormatControlName(type, config); + } + internal static (string? prefix, string tagName) FormatControlName(Type type, DotvvmConfiguration? config) + { var reg = config?.Markup.Controls.FirstOrDefault(c => c.Namespace == type.Namespace && Type.GetType(c.Namespace + "." + type.Name + ", " + c.Assembly) == type) ?? config?.Markup.Controls.FirstOrDefault(c => c.Namespace == type.Namespace) ?? config?.Markup.Controls.FirstOrDefault(c => c.Assembly == type.Assembly.GetName().Name); var ns = reg?.TagPrefix ?? type.Namespace switch { + null => "_", "DotVVM.Framework.Controls" => "dot", "DotVVM.AutoUI.Controls" => "auto", "DotVVM.BusinessPack.Controls" or "DotVVM.BusinessPack.PostBackHandlers" => "bp", "DotVVM.BusinessPack.Controls.FilterOperators" => "op", "DotVVM.BusinessPack.Controls.FilterBuilderFields" => "fp", + var x when x.StartsWith("DotVVM.Contrib.") => "dc", _ => "_" }; var optionsAttribute = type.GetCustomAttribute(); @@ -538,7 +544,7 @@ public static string DebugString(this DotvvmBindableObject control, DotvvmConfig if (ancestor is {} && location.file.Equals(ancestor.TryGetValue(Internal.MarkupFileNameProperty))) { location.line = (int)ancestor.TryGetValue(Internal.MarkupLineNumberProperty)!; - var ancestorName = FormatControlName(ancestor); + var ancestorName = FormatControlName(ancestor, config); location.nearestControlInMarkup = ancestorName.prefix is null ? ancestorName.tagName : $"{ancestorName.prefix}:{ancestorName.tagName}"; } } diff --git a/src/Framework/Framework/Controls/GridViewColumn.cs b/src/Framework/Framework/Controls/GridViewColumn.cs index 7e830c66fc..5c0ab92a03 100644 --- a/src/Framework/Framework/Controls/GridViewColumn.cs +++ b/src/Framework/Framework/Controls/GridViewColumn.cs @@ -10,6 +10,7 @@ using DotVVM.Framework.Compilation.ControlTree.Resolved; using DotVVM.Framework.Compilation.Validation; using Microsoft.Extensions.DependencyInjection; +using DotVVM.Framework.Compilation.Parser.Dothtml.Parser; namespace DotVVM.Framework.Controls { @@ -268,7 +269,9 @@ public static IEnumerable ValidateUsage(ResolvedControl contr { if (control.Properties.ContainsKey(DataContextProperty)) { - yield return new ControlUsageError("Changing the DataContext property on the GridViewColumn is not supported!", control.DothtmlNode); + var node = control.Properties[DataContextProperty].DothtmlNode; + node = (node as DothtmlAttributeNode)?.ValueNode ?? node; + yield return new ControlUsageError("Changing the DataContext property on the GridViewColumn is not supported!", node); } // disallow attached properties on columns diff --git a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs index e4825638b3..641a9a6c3e 100644 --- a/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs +++ b/src/Framework/Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs @@ -71,6 +71,9 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi services.TryAddSingleton(); services.TryAddSingleton>(s => () => ActivatorUtilities.CreateInstance(s)); services.TryAddSingleton(); + services.AddSingleton(); + services.AddSingleton(s => s.GetRequiredService()); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); @@ -130,6 +133,7 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi services.TryAddSingleton(); services.TryAddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Framework/Framework/Diagnostics/CompilationDiagnostic.dotcontrol b/src/Framework/Framework/Diagnostics/CompilationDiagnostic.dotcontrol new file mode 100644 index 0000000000..1c85ddd0ee --- /dev/null +++ b/src/Framework/Framework/Diagnostics/CompilationDiagnostic.dotcontrol @@ -0,0 +1,14 @@ +<%-- @property DotVVM.Framework.Compilation.DotHtmlFileInfo.CompilationDiagnosticViewModel Diagnostic --%> +@viewModel DotVVM.Framework.Compilation.DotHtmlFileInfo.CompilationDiagnosticViewModel + +
+
+
+
{{value: LineNumber}}: {{value: SourceLinePrefix}}{{value: SourceLineHighlight}}{{value: SourceLineSuffix}}
+
+
+
{{value: Severity}}: {{value: Message}}
+ <%--

+ Source File: {{value: FileName}}:{{value: LineNumber}} +

--%> +
diff --git a/src/Framework/Framework/Diagnostics/CompilationDiagnosticRows.dotcontrol b/src/Framework/Framework/Diagnostics/CompilationDiagnosticRows.dotcontrol new file mode 100644 index 0000000000..34cd0cdc8a --- /dev/null +++ b/src/Framework/Framework/Diagnostics/CompilationDiagnosticRows.dotcontrol @@ -0,0 +1,26 @@ +@import DotVVM.Framework.Compilation.DotHtmlFileInfo +@property DotVVM.Framework.Compilation.DotHtmlFileInfo.CompilationDiagnosticViewModel[] Diagnostics +@property int DisplayLimit +@viewModel object +@noWrapperTag + + 0}> + + + + + + + + + _control.DisplayLimit} class=row-continues> + + plus + d.Severity == 'Error')}> + {{value: _control.Diagnostics.Skip(_control.DisplayLimit).Where(d => d.Severity == 'Error').Count()}} more errors and + + + {{value: _control.Diagnostics.Skip(_control.DisplayLimit).Where(d => d.Severity == 'Warning').Count()}} more warnings + + + diff --git a/src/Framework/Framework/Diagnostics/CompilationPage.dothtml b/src/Framework/Framework/Diagnostics/CompilationPage.dothtml index 6270e1b1a7..59fb866c61 100644 --- a/src/Framework/Framework/Diagnostics/CompilationPage.dothtml +++ b/src/Framework/Framework/Diagnostics/CompilationPage.dothtml @@ -1,4 +1,4 @@ -@viewModel DotVVM.Framework.Diagnostics.CompilationPageViewModel +@viewModel DotVVM.Framework.Diagnostics.CompilationPageViewModel @@ -35,9 +35,17 @@ class="nav" Class-active="{value: ActiveTab == 2}" /> + + +
+ +

@@ -46,7 +54,16 @@ + Class-success="{value: Status == 'CompletedSuccessfully' && Warnings.Length == 0}" /> + + + + 0} + Diagnostics={value: Enumerable.Concat(Errors, Warnings).ToArray()} + DisplayLimit={value: _root.DefaultShownDiagnosticLimit} /> + + + Class-success="{value: Status == 'CompletedSuccessfully' && Warnings.Length == 0}" /> + + + 0} + Diagnostics={value: Enumerable.Concat(Errors, Warnings).ToArray()} + DisplayLimit={value: _root.DefaultShownDiagnosticLimit} /> + + + Class-success="{value: Status == 'CompletedSuccessfully' && Warnings.Length == 0}" /> + + + 0} + Diagnostics={value: Enumerable.Concat(Errors, Warnings).ToArray()} + DisplayLimit={value: _root.DefaultShownDiagnosticLimit} /> + + @@ -143,8 +176,8 @@ -
-

Errors

+
+

{{value: ActiveTab == 3 ? "Warnings" : "Errors"}}

m.Status == 'None') || Controls.AsEnumerable().Any(m => m.Status == 'None') || Routes.AsEnumerable().Any(m => m.Status == 'None')} style="color: var(--error-dark-color)"> @@ -158,8 +191,8 @@ Status Actions - r.Status == 'CompilationFailed')} WrapperTagName=tbody > - + r.Status == 'CompilationFailed' || r.Warnings.Length > 0 && ActiveTab == 3)} WrapperTagName=tbody > + Route {RouteName}"}> @@ -175,9 +208,13 @@ + 0} + Diagnostics={value: Enumerable.Concat(Errors, Warnings).ToArray()} + DisplayLimit={value: _root.DefaultShownDiagnosticLimit} /> - r.Status == 'CompilationFailed')} WrapperTagName=tbody > - + r.Status == 'CompilationFailed' || r.Warnings.Length > 0 && ActiveTab == 3)} WrapperTagName=tbody > + Control {{value: $"{TagPrefix}:{TagName}"}} @@ -188,9 +225,13 @@ + 0} + Diagnostics={value: Enumerable.Concat(Errors, Warnings).ToArray()} + DisplayLimit={value: _root.DefaultShownDiagnosticLimit} /> - r.Status == 'CompilationFailed')} WrapperTagName=tbody > - + r.Status == 'CompilationFailed' || r.Warnings.Length > 0 && ActiveTab == 3)} WrapperTagName=tbody > + Master page {{value: VirtualPath}} @@ -199,6 +240,10 @@ + 0} + Diagnostics={value: Enumerable.Concat(Errors, Warnings).ToArray()} + DisplayLimit={value: _root.DefaultShownDiagnosticLimit} /> diff --git a/src/Framework/Framework/Diagnostics/CompilationPageViewModel.cs b/src/Framework/Framework/Diagnostics/CompilationPageViewModel.cs index c6007f36fc..7ff2b4121d 100644 --- a/src/Framework/Framework/Diagnostics/CompilationPageViewModel.cs +++ b/src/Framework/Framework/Diagnostics/CompilationPageViewModel.cs @@ -17,6 +17,9 @@ public class CompilationPageViewModel : DotvvmViewModelBase public int ActiveTab { get; set; } = 0; public string PathBase => Context.TranslateVirtualPath("~/"); + public bool ShowInlineDiagnostics { get; set; } = true; + public int DefaultShownDiagnosticLimit { get; set; } = 8; + public CompilationPageViewModel(IDotvvmViewCompilationService viewCompilationService) { this.viewCompilationService = viewCompilationService; diff --git a/src/Framework/Framework/DotVVM.Framework.csproj b/src/Framework/Framework/DotVVM.Framework.csproj index 4993bcc7ee..9bafd8d706 100644 --- a/src/Framework/Framework/DotVVM.Framework.csproj +++ b/src/Framework/Framework/DotVVM.Framework.csproj @@ -36,6 +36,8 @@ + + diff --git a/src/Framework/Framework/Hosting/ErrorPages/DotvvmMarkupErrorSection.cs b/src/Framework/Framework/Hosting/ErrorPages/DotvvmMarkupErrorSection.cs index 7f9e9f5a76..cc10850a28 100644 --- a/src/Framework/Framework/Hosting/ErrorPages/DotvvmMarkupErrorSection.cs +++ b/src/Framework/Framework/Hosting/ErrorPages/DotvvmMarkupErrorSection.cs @@ -71,11 +71,15 @@ protected virtual void WriteException(IErrorWriter w, Exception exc) private SourceModel? ExtractSourceFromDotvvmCompilationException(DotvvmCompilationException compilationException) { - if (compilationException.Tokens != null && compilationException.Tokens.Any()) + if (compilationException.LineNumber is {} || compilationException.FileName is {}) { - var errorColumn = compilationException.Tokens.FirstOrDefault()?.ColumnNumber ?? 0; - var errorLength = compilationException.Tokens.Where(t => t.LineNumber == compilationException.LineNumber).Sum(t => t.Length); - if (compilationException.FileName != null) + var errorColumn = compilationException.ColumnNumber ?? 0; + var errorLength = compilationException.CompilationError.Location.LineErrorLength; + if (compilationException.MarkupFile is {}) + return ErrorFormatter.LoadSourcePiece(compilationException.MarkupFile, compilationException.LineNumber ?? 0, + errorColumn: errorColumn, + errorLength: errorLength); + else if (compilationException.FileName != null) return ErrorFormatter.LoadSourcePiece(compilationException.FileName, compilationException.LineNumber ?? 0, errorColumn: errorColumn, errorLength: errorLength); @@ -85,10 +89,6 @@ protected virtual void WriteException(IErrorWriter w, Exception exc) return CreateAnonymousLine(line, lineNumber: compilationException.Tokens.FirstOrDefault()?.LineNumber ?? 0); } } - else if (compilationException.FileName != null) - { - return ErrorFormatter.LoadSourcePiece(compilationException.FileName, compilationException.LineNumber ?? 0, errorColumn: compilationException.ColumnNumber ?? 0, errorLength: compilationException.ColumnNumber != null ? 1 : 0); - } return null; } diff --git a/src/Framework/Framework/Hosting/ErrorPages/ErrorFormatter.cs b/src/Framework/Framework/Hosting/ErrorPages/ErrorFormatter.cs index f9daa39e1c..810d184e45 100644 --- a/src/Framework/Framework/Hosting/ErrorPages/ErrorFormatter.cs +++ b/src/Framework/Framework/Hosting/ErrorPages/ErrorFormatter.cs @@ -278,16 +278,33 @@ public static SourceModel LoadSourcePiece(string? fileName, int lineNumber, { try { - var lines = File.ReadAllLines(fileName); - if (lineNumber >= 0) - { - result.CurrentLine = lines[Math.Max(0, lineNumber - 1)]; - result.PreLines = lines.Skip(lineNumber - additionalLineCount) - .TakeWhile(l => l != result.CurrentLine).ToArray(); - } - else additionalLineCount = 30; - result.PostLines = lines.Skip(lineNumber).Take(additionalLineCount).ToArray(); - return result; + return SourcePieceFromSource(fileName, File.ReadAllText(fileName), lineNumber, additionalLineCount, errorColumn, errorLength); + } + catch + { + result.LoadFailed = true; + } + } + return result; + } + + public static SourceModel LoadSourcePiece(MarkupFile? file, int lineNumber, + int additionalLineCount = 7, + int errorColumn = 0, + int errorLength = 0) + { + var result = new SourceModel { + FileName = file?.FileName, + LineNumber = lineNumber, + ErrorColumn = errorColumn, + ErrorLength = errorLength + }; + + if (file != null) + { + try + { + return SourcePieceFromSource(file.FileName, file.ReadContent(), lineNumber, additionalLineCount, errorColumn, errorLength); } catch { @@ -297,6 +314,30 @@ public static SourceModel LoadSourcePiece(string? fileName, int lineNumber, return result; } + + public static SourceModel SourcePieceFromSource(string? fileName, string sourceCode, int lineNumber, + int additionalLineCount = 7, + int errorColumn = 0, + int errorLength = 0) + { + var result = new SourceModel { + FileName = fileName, + LineNumber = lineNumber, + ErrorColumn = errorColumn, + ErrorLength = errorLength + }; + var lines = sourceCode.Split('\n'); + if (lineNumber >= 0) + { + result.CurrentLine = lines[Math.Max(0, lineNumber - 1)]; + result.PreLines = lines.Skip(lineNumber - additionalLineCount) + .TakeWhile(l => l != result.CurrentLine).ToArray(); + } + else additionalLineCount = 30; + result.PostLines = lines.Skip(lineNumber).Take(additionalLineCount).ToArray(); + return result; + } + public List> Formatters = new(); public string ErrorHtml(Exception exception, IHttpContext context) diff --git a/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs b/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs index 4d30b52d1a..6b52195d8e 100644 --- a/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs +++ b/src/Framework/Framework/Hosting/ErrorPages/ErrorPageTemplate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using System.Collections; @@ -107,7 +107,7 @@ public string TransformText()

-

Server Error, HTTP {ErrorCode}: {WebUtility.HtmlEncode(ErrorDescription)}

+

Server Error, HTTP {ErrorCode}: {WebUtility.HtmlEncode(ErrorDescription)}

{WebUtility.HtmlEncode(Summary)}

@@ -164,13 +164,14 @@ public void ObjectBrowser(object? obj) ReferenceLoopHandling = ReferenceLoopHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore, Converters = { - new ReflectionTypeJsonConverter(), - new ReflectionAssemblyJsonConverter(), - new DotvvmTypeDescriptorJsonConverter(), - new Controls.DotvvmControlDebugJsonConverter(), - new IgnoreStuffJsonConverter(), - new BindingDebugJsonConverter() - }, + new ReflectionTypeJsonConverter(), + new ReflectionAssemblyJsonConverter(), + new DotvvmTypeDescriptorJsonConverter(), + new Controls.DotvvmControlDebugJsonConverter(), + new IgnoreStuffJsonConverter(), + new BindingDebugJsonConverter(), + new DotvvmPropertyJsonConverter() + }, // suppress any errors that occur during serialization (getters may throw exception, ...) Error = (sender, args) => { args.ErrorContext.Handled = true; diff --git a/src/Framework/Framework/ResourceManagement/ReflectionAssemblyJsonConverter.cs b/src/Framework/Framework/ResourceManagement/ReflectionAssemblyJsonConverter.cs index 18d89d6cac..77e8f75862 100644 --- a/src/Framework/Framework/ResourceManagement/ReflectionAssemblyJsonConverter.cs +++ b/src/Framework/Framework/ResourceManagement/ReflectionAssemblyJsonConverter.cs @@ -85,4 +85,22 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer writer.WriteValue($"{t.FullName}, {assembly}"); } } + + public class DotvvmPropertyJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => + typeof(IPropertyDescriptor).IsAssignableFrom(objectType) || typeof(IPropertyGroupDescriptor).IsAssignableFrom(objectType); + public override bool CanRead => false; + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => + throw new NotImplementedException(); + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + return; + } + writer.WriteValue(value.ToString()); + } + } } diff --git a/src/Framework/Framework/Resources/Styles/DotVVM.Internal.css b/src/Framework/Framework/Resources/Styles/DotVVM.Internal.css index 37e5de9539..5c6e903380 100644 --- a/src/Framework/Framework/Resources/Styles/DotVVM.Internal.css +++ b/src/Framework/Framework/Resources/Styles/DotVVM.Internal.css @@ -42,7 +42,7 @@ html { /* Variables */ :root { - --heading-color: #a82f23; + --heading-color: #333333; --nav-color: #2980b9; --activate-color: #de1212; --idle-color: #bcbcbc; @@ -50,6 +50,8 @@ html { --hint-color: #bbbbbb; --error-color: #de1212; --error-dark-color: #a82f23; + --warning-color: #ffa322; + --warning-dark-color: #940c00; --success-color: green; @@ -101,6 +103,14 @@ h3 { color: var(--hint-color) } +.error-text { + color: var(--error-dark-color) +} + +.warning-text { + color: var(--warning-dark-color) +} + /* Tables */ table { @@ -131,16 +141,19 @@ table { table th, table td { - border-right: 0.1rem var(--hint-color) solid; - border-bottom: 0.1rem var(--hint-color) solid; - border-left: 0.1rem transparent solid; - border-top: 0.1rem transparent solid; + border-right: none; + border-bottom: none; + border-left: none; + border-top: 0.1rem var(--hint-color) solid; box-sizing: border-box; } + table tr:first-child th, table tr:first-child td { + border-top: none; + } - table th:last-child, - table td:last-child { - border-right: 0.1rem transparent solid; + table tr.row-continues td { + /* border-top: 0.1rem var(--hint-color) dashed; */ + border-top: none; } table th.explosive, @@ -154,6 +167,11 @@ table { width: 1px; } + table td.center, + table th.center { + text-align: center; + } + table tr.failure { background-color: var(--error-dark-color); color: white; @@ -172,7 +190,7 @@ table { /* Source code */ .source .source-errorLine { - color: #a82f23; + color: var(--error-dark-color); } .errorUnderline { @@ -182,6 +200,17 @@ table { padding: 0.2rem; } +.source .source-warningLine { + color: var(--warning-dark-color); +} + +.warningUnderline { + background-color: color-mix(in srgb, var(--warning-color) 20%, white 80%); + border: 0.1rem solid color-mix(in srgb, var(--warning-color) 50%, white 50%); + color: var(--warning-dark-color); + padding: 0.2rem; +} + .code { font-family: var(--font-monospace); } diff --git a/src/Framework/Framework/Utils/ReferenceEqualityComparer.cs b/src/Framework/Framework/Utils/ReferenceEqualityComparer.cs new file mode 100644 index 0000000000..8d83b2f6fe --- /dev/null +++ b/src/Framework/Framework/Utils/ReferenceEqualityComparer.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace DotVVM.Framework.Utils +{ + internal class ReferenceEqualityComparer : IEqualityComparer + where T : class + { + public bool Equals(T? x, T? y) => ReferenceEquals(x, y); + + public int GetHashCode(T obj) => obj?.GetHashCode() ?? 0; + } +} diff --git a/src/Samples/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs index e2cd4f4635..04dac2a63f 100644 --- a/src/Samples/Common/DotvvmStartup.cs +++ b/src/Samples/Common/DotvvmStartup.cs @@ -95,6 +95,7 @@ public void Configure(DotvvmConfiguration config, string applicationPath) ), allowGeneric: true, allowMultipleMethods: true); config.Diagnostics.CompilationPage.IsApiEnabled = true; + config.Diagnostics.CompilationPage.IsEnabled = true; config.Diagnostics.CompilationPage.ShouldCompileAllOnLoad = false; config.AssertConfigurationIsValid(); diff --git a/src/Samples/Common/Views/FeatureSamples/LambdaExpressions/LambdaExpressions.dothtml b/src/Samples/Common/Views/FeatureSamples/LambdaExpressions/LambdaExpressions.dothtml index 3662c4f15e..5ce3e7979a 100644 --- a/src/Samples/Common/Views/FeatureSamples/LambdaExpressions/LambdaExpressions.dothtml +++ b/src/Samples/Common/Views/FeatureSamples/LambdaExpressions/LambdaExpressions.dothtml @@ -36,7 +36,7 @@ --%> <%--Click="{command: SetResult(Array.Where((int item) => item % 2 == 0))}" />--%> - Click="{command SetResult(Array.Where(item => item % 2 == 0))}" /> + Click="{command: SetResult(Array.Where(item => item % 2 == 0))}" /> --%> <%--Click="{command: SetResult(Array.Where((int item) => item % 2 == 1))}" />--%> diff --git a/src/Samples/Tests/Tests/ErrorsTests.cs b/src/Samples/Tests/Tests/ErrorsTests.cs index 4057b6e87b..2eba2ef2b5 100644 --- a/src/Samples/Tests/Tests/ErrorsTests.cs +++ b/src/Samples/Tests/Tests/ErrorsTests.cs @@ -454,7 +454,7 @@ public void Error_RouteLinkInvalidRouteName() browser.NavigateToUrl(SamplesRouteUrls.Errors_InvalidRouteName); AssertUI.TextEquals(browser.First("exceptionType", By.ClassName), "DotVVM.Framework.Compilation.DotvvmCompilationException"); - AssertUI.TextEquals(browser.First(".exceptionMessage"), "Validation error in RouteLink at line 18: RouteName \"NonExistingRouteName\" does not exist.", + AssertUI.TextEquals(browser.First(".exceptionMessage"), "RouteLink validation at line 18: RouteName \"NonExistingRouteName\" does not exist.", failureMessage: "Exception should contain information about the undefined route name"); }); } diff --git a/src/Tests/ControlTests/GridViewTests.cs b/src/Tests/ControlTests/GridViewTests.cs index 263fa71c4e..251657e712 100644 --- a/src/Tests/ControlTests/GridViewTests.cs +++ b/src/Tests/ControlTests/GridViewTests.cs @@ -97,7 +97,7 @@ public async Task GridViewColumn_Usage_DataContext() ")); - Assert.IsTrue(exception.Message.Contains("Changing the DataContext property on the GridViewColumn is not supported!")); + StringAssert.Contains(exception.Message, "Changing the DataContext property on the GridViewColumn is not supported!"); } [TestMethod] diff --git a/src/Tests/ControlTests/ViewModulesServerSideTests.cs b/src/Tests/ControlTests/ViewModulesServerSideTests.cs index 1b7616967e..f6d8ce5f9f 100644 --- a/src/Tests/ControlTests/ViewModulesServerSideTests.cs +++ b/src/Tests/ControlTests/ViewModulesServerSideTests.cs @@ -41,14 +41,14 @@ public async Task NamedCommandWithoutViewModule_StaticCommand() { var r = await Assert.ThrowsExceptionAsync(() => cth.RunPage(typeof(object), @" ")); - Assert.AreEqual("Validation error in NamedCommand at line 7: The NamedCommand control can be used only in pages or controls that have the @js directive.", r.Message); + Assert.AreEqual("NamedCommand validation at line 7: The NamedCommand control can be used only in pages or controls that have the @js directive.", r.Message); } [TestMethod] public async Task NamedCommandWithoutViewModule_Command() { var r = await Assert.ThrowsExceptionAsync(() => cth.RunPage(typeof(object), @" ")); - Assert.AreEqual("Validation error in NamedCommand at line 7: The NamedCommand control can be used only in pages or controls that have the @js directive.", r.Message); + Assert.AreEqual("NamedCommand validation at line 7: The NamedCommand control can be used only in pages or controls that have the @js directive.", r.Message); } [TestMethod] diff --git a/src/Tests/ControlTests/testoutputs/DotvvmErrorTests.AuthView_InvalidWrapperTagUsage.txt b/src/Tests/ControlTests/testoutputs/DotvvmErrorTests.AuthView_InvalidWrapperTagUsage.txt index 48e545c6a8..f7273e8eaa 100644 --- a/src/Tests/ControlTests/testoutputs/DotvvmErrorTests.AuthView_InvalidWrapperTagUsage.txt +++ b/src/Tests/ControlTests/testoutputs/DotvvmErrorTests.AuthView_InvalidWrapperTagUsage.txt @@ -1,2 +1,2 @@ -DotvvmCompilationException occurred: Validation error in AuthenticatedView at line 6: The WrapperTagName property cannot be set when RenderWrapperTag is false! - at void DotVVM.Framework.Compilation.Validation.ControlUsageValidationVisitor.VisitAndAssert(ResolvedTreeRoot view) +DotvvmCompilationException occurred: AuthenticatedView validation at line 6: The WrapperTagName property cannot be set when RenderWrapperTag is false! + at List DotVVM.Framework.Compilation.ViewCompiler.DefaultViewCompiler.CheckErrors(string fileName, string sourceCode, Handle tracingHandle, List tokens, DothtmlNode syntaxTree, ResolvedTreeRoot resolvedTree, IEnumerable additionalDiagnostics) diff --git a/src/Tests/ControlTests/testoutputs/DotvvmErrorTests.HtmlLiteral_InvalidWrapperTagUsage.txt b/src/Tests/ControlTests/testoutputs/DotvvmErrorTests.HtmlLiteral_InvalidWrapperTagUsage.txt index 3889337588..7612335a5a 100644 --- a/src/Tests/ControlTests/testoutputs/DotvvmErrorTests.HtmlLiteral_InvalidWrapperTagUsage.txt +++ b/src/Tests/ControlTests/testoutputs/DotvvmErrorTests.HtmlLiteral_InvalidWrapperTagUsage.txt @@ -1,2 +1,2 @@ -DotvvmCompilationException occurred: Validation error in HtmlLiteral at line 6: The WrapperTagName property cannot be set when RenderWrapperTag is false! - at void DotVVM.Framework.Compilation.Validation.ControlUsageValidationVisitor.VisitAndAssert(ResolvedTreeRoot view) +DotvvmCompilationException occurred: HtmlLiteral validation at line 6: The WrapperTagName property cannot be set when RenderWrapperTag is false! + at List DotVVM.Framework.Compilation.ViewCompiler.DefaultViewCompiler.CheckErrors(string fileName, string sourceCode, Handle tracingHandle, List tokens, DothtmlNode syntaxTree, ResolvedTreeRoot resolvedTree, IEnumerable additionalDiagnostics) diff --git a/src/Tests/Runtime/DefaultViewCompilerTests.cs b/src/Tests/Runtime/DefaultViewCompilerTests.cs index bc469dd2cf..29fd2c9285 100644 --- a/src/Tests/Runtime/DefaultViewCompilerTests.cs +++ b/src/Tests/Runtime/DefaultViewCompilerTests.cs @@ -432,8 +432,8 @@ @viewModel object var ex = Assert.ThrowsException(() => { CompileMarkup(markup); }); - Assert.IsTrue(ex.ToString().Contains("DotVVM.Framework.Binding.Properties.DataSourceLengthBinding")); - Assert.IsTrue(ex.ToString().Contains("Cannot find collection length from binding '_this'")); + Assert.IsTrue(ex.AllErrors.Any(e => e.Message.Contains("DotVVM.Framework.Binding.Properties.DataSourceLengthBinding"))); + StringAssert.Contains(ex.ToString(), "Cannot find collection length from binding '_this'"); } [TestMethod] diff --git a/src/Tests/Runtime/DotvvmControlErrorsTests.cs b/src/Tests/Runtime/DotvvmControlErrorsTests.cs index d08499c92e..980f27e2bd 100644 --- a/src/Tests/Runtime/DotvvmControlErrorsTests.cs +++ b/src/Tests/Runtime/DotvvmControlErrorsTests.cs @@ -94,7 +94,7 @@ public void Button_NonExistingCommand_ThrowsException() var dotvvmBuilder = CreateControlRenderer(control, new object()); var exc = Assert.ThrowsException(() => dotvvmBuilder()); - StringAssert.Contains(exc.Message, "Could not initialize binding"); + StringAssert.Contains(exc.Message, "Could not resolve identifier 'NonExistingCommand'"); } [TestMethod] @@ -104,7 +104,7 @@ public void CheckBox_NonExistingViewModelProperty_ThrowsException() var dotvvmBuilder = CreateControlRenderer(control, new object()); var exc = Assert.ThrowsException(() => dotvvmBuilder()); - StringAssert.Contains(exc.Message, "Could not initialize binding"); + StringAssert.Contains(exc.Message, "Could not resolve identifier 'InvalidPropertyName'"); } [TestMethod]