Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MeshWeaver.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
<Project Path="src/MeshWeaver.Blazor.Portal/MeshWeaver.Blazor.Portal.csproj" />
<Project Path="src/MeshWeaver.Blazor/MeshWeaver.Blazor.csproj" />
<Project Path="src/MeshWeaver.Cli/MeshWeaver.Cli.csproj" />
<Project Path="src/MeshWeaver.Compiler/MeshWeaver.Compiler.csproj" />
<Project Path="src/MeshWeaver.Connection.Orleans/MeshWeaver.Connection.Orleans.csproj" />
<Project Path="src/MeshWeaver.ContentCollections/MeshWeaver.ContentCollections.csproj" />
<Project Path="src/MeshWeaver.ContentCollections.Indexing/MeshWeaver.ContentCollections.Indexing.csproj" />
Expand Down
1 change: 1 addition & 0 deletions memex/Memex.Portal.Monolith/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;

using MeshWeaver.Compiler;
var builder = WebApplication.CreateBuilder(args);

// Configure Memex services (pattern from MeshWeaver.Portal's ConfigureWebPortalServices)
Expand Down
1 change: 1 addition & 0 deletions memex/aspire/Memex.Portal.Distributed/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using Orleans.Configuration;
using Orleans.Hosting;

using MeshWeaver.Compiler;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddServerSideBlazor().AddCircuitOptions(o => o.DetailedErrors = true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;

namespace MeshWeaver.Graph.Configuration;
namespace MeshWeaver.Compiler;

/// <summary>
/// DI helpers for registering <see cref="IAssemblyStore"/> implementations. Registration
Expand Down
26 changes: 26 additions & 0 deletions src/MeshWeaver.Compiler/CodeConventions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace MeshWeaver.Compiler;

/// <summary>
/// The Code-node naming conventions the compile toolchain keys on. Defined here — below the
/// graph — because <see cref="CodeQueryResolver"/>'s default
/// queries and path matching are part of the compile toolchain's identity boundary;
/// <c>MeshWeaver.Graph</c>'s <c>CodeNodeType</c> aliases these same constants for the mesh-side
/// registration, so the two can never drift.
/// </summary>
public static class CodeConventions
{
/// <summary>The NodeType value used to identify code nodes.</summary>
public const string CodeNodeType = "Code";

/// <summary>
/// The sub-namespace for source code files. Code nodes live under
/// <c>{NodeTypePath}/Source/</c> alongside (not inside) their parent NodeType.
/// </summary>
public const string SourceSubNamespace = "Source";

/// <summary>
/// The sub-namespace for test code files. Tests live under <c>{NodeTypePath}/Test/</c>
/// alongside (not inside) their parent NodeType.
/// </summary>
public const string TestSubNamespace = "Test";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using System.Text.RegularExpressions;
using MeshWeaver.Mesh;

namespace MeshWeaver.Graph.Configuration;
namespace MeshWeaver.Compiler;

/// <summary>
/// Shared query-expansion helpers used by both the compiler (to find which Code
Expand All @@ -11,7 +11,7 @@ namespace MeshWeaver.Graph.Configuration;
/// user sees in the UI is guaranteed to match the files the runtime actually
/// pulls into the NodeType's assembly.
/// <para>
/// Rules (mirrored from <see cref="NodeTypeDefinition.Sources"/>):
/// Rules (mirrored from <c>NodeTypeDefinition.Sources</c>):
/// </para>
/// <list type="bullet">
/// <item>An optional <c>name=</c> prefix (e.g. <c>"shared=@Lib/Common"</c>)
Expand All @@ -34,19 +34,19 @@ namespace MeshWeaver.Graph.Configuration;
public static class CodeQueryResolver
{
/// <summary>
/// Default Source query when a NodeType doesn't declare <see cref="NodeTypeDefinition.Sources"/>.
/// Default Source query when a NodeType doesn't declare <c>NodeTypeDefinition.Sources</c>.
/// Resolves to <c>{NodeTypePath}/Source</c> subtree.
/// </summary>
public static readonly IReadOnlyList<string> DefaultSources =
[$"namespace:{CodeNodeType.SourceSubNamespace} scope:subtree"];
[$"namespace:{CodeConventions.SourceSubNamespace} scope:subtree"];

/// <summary>
/// Default Test query when a NodeType doesn't declare <see cref="NodeTypeDefinition.Tests"/>.
/// Default Test query when a NodeType doesn't declare <c>NodeTypeDefinition.Tests</c>.
/// Resolves to <c>{NodeTypePath}/Test</c> subtree. Mirrors <see cref="DefaultSources"/>
/// so the side-menu Sources/Tests split matches the convention.
/// </summary>
public static readonly IReadOnlyList<string> DefaultTests =
[$"namespace:{CodeNodeType.TestSubNamespace} scope:subtree"];
[$"namespace:{CodeConventions.TestSubNamespace} scope:subtree"];

/// <summary>Group name for unnamed Source query entries.</summary>
public const string DefaultSourceGroupName = "src";
Expand Down Expand Up @@ -81,8 +81,8 @@ public static (string? Name, string Query) ParseName(string rawEntry)
}

/// <summary>
/// Expands one raw query entry from <see cref="NodeTypeDefinition.Sources"/> or
/// <see cref="NodeTypeDefinition.Tests"/> into one-or-more concrete mesh query
/// Expands one raw query entry from <c>NodeTypeDefinition.Sources</c> or
/// <c>NodeTypeDefinition.Tests</c> into one-or-more concrete mesh query
/// strings ready for <c>IMeshService.Query&lt;MeshNode&gt;</c>. An optional
/// <c>name=</c> prefix is stripped first — the compiler ignores grouping.
/// </summary>
Expand Down Expand Up @@ -146,7 +146,7 @@ private static string RebaseRelativeNamespace(string query, string selfPath)
private static string WithCodeTypeFilter(string query) =>
query.Contains("nodeType:", System.StringComparison.OrdinalIgnoreCase)
? query
: $"{query} nodeType:{CodeNodeType.NodeType}";
: $"{query} nodeType:{CodeConventions.CodeNodeType}";

/// <summary>
/// Groups the raw Sources/Tests entries by their <c>name=</c> prefix (falling back
Expand Down
37 changes: 37 additions & 0 deletions src/MeshWeaver.Compiler/CompilationException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Moved verbatim from MeshNodeCompilationService.cs in MeshWeaver.Graph (#1707). The namespace
// stays MeshWeaver.Graph.Configuration and MeshWeaver.Graph type-forwards it, so modules compiled
// against earlier releases keep binding.
namespace MeshWeaver.Compiler;

/// <summary>
/// Exception thrown when compilation fails.
/// </summary>
public class CompilationException : Exception
{
/// <summary>The mesh path of the node whose compilation failed.</summary>
public string NodePath { get; }

/// <summary>
/// Initializes a new instance of the exception for a failed compilation.
/// </summary>
/// <param name="nodePath">The mesh path of the node whose compilation failed.</param>
/// <param name="message">The error message describing the failure.</param>
public CompilationException(string nodePath, string message)
: base(message)
{
NodePath = nodePath;
}

/// <summary>
/// Initializes a new instance of the exception for a failed compilation, wrapping an
/// underlying cause.
/// </summary>
/// <param name="nodePath">The mesh path of the node whose compilation failed.</param>
/// <param name="message">The error message describing the failure.</param>
/// <param name="innerException">The underlying exception that caused the failure.</param>
public CompilationException(string nodePath, string message, Exception innerException)
: base(message, innerException)
{
NodePath = nodePath;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace MeshWeaver.Graph.Configuration;
namespace MeshWeaver.Compiler;

/// <summary>
/// Everything language services (hover, completion, diagnostics, speculative compile) need
Expand Down
137 changes: 137 additions & 0 deletions src/MeshWeaver.Compiler/CompileDiagnostics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Lsp = MeshWeaver.Mesh.Services.LanguageServer;

namespace MeshWeaver.Compiler;

/// <summary>
/// Diagnostics shaping for dynamic NodeType compiles: the flattened failure summary, the
/// structured per-source-file re-derivation, and the source-discovery report. Pure over
/// <see cref="CompilationInputs"/> / Roslyn diagnostics; the scheduling and the write-backs stay
/// with the caller in MeshWeaver.Graph.
/// </summary>
public static class CompileDiagnostics
{
/// <summary>
/// Formats a failed Roslyn <c>Emit</c>'s diagnostics into a complete, never-empty error
/// message — each line carries the diagnostic <c>CS####</c> id, severity, source line and
/// message. Falls back to Warning-severity diagnostics when there are no Errors, and to an
/// explanatory sentence when Emit failed with NO diagnostics at all (typically a missing
/// source file or a configuration lambda referencing a type that was never compiled). The
/// previous <c>Where(Severity == Error).Select(GetMessage)</c> produced a bare
/// "Compilation failed for 'X':" whenever the failure carried no Error-severity diagnostic.
/// </summary>
internal static string FormatCompileFailure(string nodePath, IEnumerable<Diagnostic> diagnostics)
{
var joined = string.Join('\n', diagnostics
.Where(d => d.Severity is DiagnosticSeverity.Error or DiagnosticSeverity.Warning)
.OrderByDescending(d => d.Severity)
.Select(d =>
{
var loc = d.Location.IsInSource
? $" (line {d.Location.GetLineSpan().StartLinePosition.Line + 1})"
: "";
return $"{d.Id} {d.Severity}{loc}: {d.GetMessage()}";
}));
return !string.IsNullOrEmpty(joined)
? $"Compilation failed for '{nodePath}':\n{joined}"
: $"Compilation failed for '{nodePath}': Roslyn emit failed but produced no error/warning "
+ "diagnostics — this usually means a source file was not found, or the configuration "
+ "lambda references a type that was never compiled (see the source-discovery report below).";
}

/// <summary>
/// Sentinel FilePath for the generated skeleton tree — must match the one the LSP uses
/// so skeleton-internal diagnostics (framework noise the user can't act on) are filtered out.
/// </summary>
internal const string SkeletonDiagnosticsPath = "__skeleton__.cs";

/// <summary>
/// Re-derives a failed compile's diagnostics in their structured, per-source-file form by
/// assembling ONE LSP-style compilation (skeleton tree + one tree per src/test Code node,
/// each carrying the MeshNode path as its <c>FilePath</c>) — exactly the model
/// <see cref="SpeculativeCompilation"/> / the language service use, so a diagnostic's
/// <see cref="Lsp.SourceLocation.SourcePath"/> is the Code node path. This is what lets the
/// GUI mark each error at its exact line/column in a Monaco editor and link to the source.
/// Synchronous, CPU-bound — the caller schedules it off the hub.
/// </summary>
internal static IReadOnlyList<Lsp.DiagnosticInfo> DiagnoseInputs(CompilationInputs inputs)
{
var trees = new List<SyntaxTree>(inputs.Sources.Length + 1)
{
CSharpSyntaxTree.ParseText(
Microsoft.CodeAnalysis.Text.SourceText.From(inputs.SkeletonSource),
inputs.ParseOptions, path: SkeletonDiagnosticsPath),
};
foreach (var (path, code) in inputs.Sources)
trees.Add(CSharpSyntaxTree.ParseText(
Microsoft.CodeAnalysis.Text.SourceText.From(code), inputs.ParseOptions, path: path));

// Structured failure diagnostics are best-effort: pass no generator candidates so
// RunSourceGenerators is a no-op here (the authoritative flat summary from the production
// compile already reflects generation). Avoids loading any generator on every failed compile.
var compilation = GeneratorPipeline.RunSourceGenerators(
CSharpCompilation.Create(inputs.AssemblyName, trees, inputs.References, inputs.CompilationOptions),
Array.Empty<string>(), Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance, CancellationToken.None);

var diags = compilation.GetDiagnostics();
if (diags.IsDefaultOrEmpty) return Array.Empty<Lsp.DiagnosticInfo>();

var result = new List<Lsp.DiagnosticInfo>(diags.Length);
foreach (var d in diags)
{
if (d.Severity is not (DiagnosticSeverity.Error or DiagnosticSeverity.Warning)) continue;
// Skeleton-internal diagnostics are framework noise the user can't act on.
if (d.Location.SourceTree?.FilePath == SkeletonDiagnosticsPath) continue;
result.Add(ToDiagnosticInfo(d));
}
// Errors first, then by file then position — stable order for the GUI.
return result
.OrderByDescending(d => d.Severity)
.ThenBy(d => d.Location?.SourcePath, StringComparer.Ordinal)
.ThenBy(d => d.Location?.Range.Start.Line ?? 0)
.ToList();
}

internal static Lsp.DiagnosticInfo ToDiagnosticInfo(Diagnostic d)
{
Lsp.SourceLocation? location = null;
if (d.Location.IsInSource && d.Location.SourceTree?.FilePath is { Length: > 0 } path)
{
var span = d.Location.GetLineSpan();
location = new Lsp.SourceLocation(
path,
new Lsp.SourceRange(
new Lsp.SourcePosition(span.StartLinePosition.Line, span.StartLinePosition.Character),
new Lsp.SourcePosition(span.EndLinePosition.Line, span.EndLinePosition.Character)));
}
return new Lsp.DiagnosticInfo(d.Id, MapDiagnosticSeverity(d.Severity), d.GetMessage(), location);
}

internal static Lsp.DiagnosticSeverity MapDiagnosticSeverity(DiagnosticSeverity s) => s switch
{
DiagnosticSeverity.Hidden => Lsp.DiagnosticSeverity.Hidden,
DiagnosticSeverity.Info => Lsp.DiagnosticSeverity.Info,
DiagnosticSeverity.Warning => Lsp.DiagnosticSeverity.Warning,
DiagnosticSeverity.Error => Lsp.DiagnosticSeverity.Error,
_ => Lsp.DiagnosticSeverity.Info,
};

/// <summary>Formats the source-discovery report appended to every compile failure: every
/// executed query and every matched Code path.</summary>
internal static string BuildSourceDiscoveryReport(
IReadOnlyList<string> executedQueries, IReadOnlyList<string> matchedCodePaths)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine($"Executed source queries ({executedQueries.Count}):");
foreach (var q in executedQueries)
sb.AppendLine($" - {q}");
sb.AppendLine($"Matched Code nodes ({matchedCodePaths.Count}):");
if (matchedCodePaths.Count == 0)
sb.AppendLine(" (none) — the configuration lambda cannot reference types because no source files were included. Check that your Source Code nodes exist and that the NodeType's `sources` list points at them.");
else
foreach (var p in matchedCodePaths)
sb.AppendLine($" - {p}");
return sb.ToString();
}
}
Loading
Loading