Skip to content

Commit

Permalink
initial generation diagnostics infrastructure
Browse files Browse the repository at this point in the history
  • Loading branch information
Mpdreamz committed Nov 8, 2024
1 parent 0844698 commit a14e25b
Show file tree
Hide file tree
Showing 18 changed files with 340 additions and 49 deletions.
2 changes: 1 addition & 1 deletion docs/source/markup/substitutions.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ Here are some variable substitutions:
| Value | Source |
| ------------------- | ------------ |
| {{project}} | conf.py |
| {{frontmatter_key}} | Front Matter |
| {{frontmatter_key}} | Front Matter |
10 changes: 7 additions & 3 deletions src/Elastic.Markdown/BuildContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
using System.IO.Abstractions;
using Elastic.Markdown.Diagnostics;

namespace Elastic.Markdown;

public record BuildContext
{
private readonly string? _urlPathPrefix;
public required IFileSystem ReadFileSystem { get; init; }
public required IFileSystem WriteFileSystem { get; init; }
public required DiagnosticsCollector Collector { get; init; }

public bool Force { get; init; }

public string? UrlPathPrefix
Expand All @@ -16,6 +20,6 @@ public string? UrlPathPrefix
init => _urlPathPrefix = value;
}

public required IFileSystem ReadFileSystem { get; init; }
public required IFileSystem WriteFileSystem { get; init; }
private readonly string? _urlPathPrefix;

}
112 changes: 112 additions & 0 deletions src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System.Threading.Channels;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Elastic.Markdown.Diagnostics;

public class DiagnosticsChannel
{
private readonly Channel<Diagnostic> _channel;
private readonly CancellationTokenSource _ctxSource;
public ChannelReader<Diagnostic> Reader => _channel.Reader;

public CancellationToken CancellationToken => _ctxSource.Token;

public DiagnosticsChannel()
{
var options = new UnboundedChannelOptions { SingleReader = true, SingleWriter = false };
_ctxSource = new CancellationTokenSource();
_channel = Channel.CreateUnbounded<Diagnostic>(options);
}

public void TryComplete(Exception? exception = null)
{
_channel.Writer.TryComplete(exception);
_ctxSource.Cancel();
}

public void Write(Diagnostic diagnostic)
{
var written = _channel.Writer.TryWrite(diagnostic);
if (!written)
{
//TODO
}
}
}


public enum Severity { Error, Warning }

public readonly record struct Diagnostic
{
public Severity Severity { get; init; }
public int Line { get; init; }
public int? Position { get; init; }
public string File { get; init; }
public string Message { get; init; }
}

public interface IDiagnosticsOutput
{
public void Write(Diagnostic diagnostic);
}

public class LogDiagnosticOutput(ILogger logger) : IDiagnosticsOutput
{
public void Write(Diagnostic diagnostic)
{
if (diagnostic.Severity == Severity.Error)
logger.LogError($"{diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})");
else
logger.LogWarning($"{diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})");
}
}


public class DiagnosticsCollector(ILoggerFactory loggerFactory, IReadOnlyCollection<IDiagnosticsOutput> outputs)
: IHostedService
{
private readonly IReadOnlyCollection<IDiagnosticsOutput> _outputs =
[new LogDiagnosticOutput(loggerFactory.CreateLogger<LogDiagnosticOutput>()), ..outputs];

public DiagnosticsChannel Channel { get; } = new();

private long _errors;
private long _warnings;
public long Warnings => _warnings;
public long Errors => _errors;

public async Task StartAsync(Cancel ctx)
{
while (!Channel.CancellationToken.IsCancellationRequested)
{
while (await Channel.Reader.WaitToReadAsync(Channel.CancellationToken))
Drain();
}
Drain();

void Drain()
{
while (Channel.Reader.TryRead(out var item))
{
IncrementSeverityCount(item);
HandleItem(item);
foreach (var output in _outputs)
output.Write(item);
}
}
}

private void IncrementSeverityCount(Diagnostic item)
{
if (item.Severity == Severity.Error)
Interlocked.Increment(ref _errors);
else if (item.Severity == Severity.Warning)
Interlocked.Increment(ref _warnings);
}

protected virtual void HandleItem(Diagnostic diagnostic) {}

public virtual Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
21 changes: 21 additions & 0 deletions src/Elastic.Markdown/Diagnostics/ProcessorDiagnosticExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Elastic.Markdown.Myst;
using Markdig.Helpers;
using Markdig.Parsers;

namespace Elastic.Markdown.Diagnostics;

public static class ProcessorDiagnosticExtensions
{
public static void EmitError(this InlineProcessor processor, int line, int position, string message)
{
var d = new Diagnostic
{
Severity = Severity.Error,
File = processor.GetContext().Path.FullName,
Position = position,
Line = line,
Message = message
};
processor.GetBuildContext().Collector.Channel.Write(d);
}
}
16 changes: 15 additions & 1 deletion src/Elastic.Markdown/DocumentationGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
using System.IO.Abstractions;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using Elastic.Markdown.IO;
Expand Down Expand Up @@ -102,6 +103,9 @@ public async Task GenerateAll(Cancel ctx)


var handledItems = 0;

var collectTask = Task.Run(async () => await Context.Collector.StartAsync(ctx), ctx);

await Parallel.ForEachAsync(DocumentationSet.Files, ctx, async (file, token) =>
{
if (file.SourceFile.LastWriteTimeUtc <= outputSeenChanges)
Expand All @@ -122,19 +126,29 @@ await Parallel.ForEachAsync(DocumentationSet.Files, ctx, async (file, token) =>
if (item % 1_000 == 0)
_logger.LogInformation($"Handled {handledItems} files");
});
Context.Collector.Channel.TryComplete();

await GenerateDocumentationState(ctx);

await collectTask;
await Context.Collector.StopAsync(ctx);


IFileInfo OutputFile(string relativePath)
{
var outputFile = _writeFileSystem.FileInfo.New(Path.Combine(DocumentationSet.OutputPath.FullName, relativePath));
return outputFile;
}

}

private async Task GenerateDocumentationState(Cancel ctx)
{
var stateFile = DocumentationSet.OutputStateFile;
_logger.LogInformation($"Writing documentation state {DocumentationSet.LastWrite} to {stateFile.FullName}");
var state = new OutputState { LastSeenChanges = DocumentationSet.LastWrite };
var bytes = JsonSerializer.SerializeToUtf8Bytes(state, SourceGenerationContext.Default.OutputState);
await DocumentationSet.OutputPath.FileSystem.File.WriteAllBytesAsync(stateFile.FullName, bytes, ctx);

}

private async Task CopyFileFsAware(DocumentationFile file, IFileInfo outputFile, Cancel ctx)
Expand Down
7 changes: 2 additions & 5 deletions src/Elastic.Markdown/IO/DocumentationSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Globalization;
using System.IO.Abstractions;
using System.Text.Json;
using Elastic.Markdown.Diagnostics;
using Elastic.Markdown.Myst;

namespace Elastic.Markdown.IO;
Expand All @@ -19,11 +20,7 @@ public class DocumentationSet

private MarkdownParser MarkdownParser { get; }

public DocumentationSet(IFileSystem fileSystem) : this(null, null, new BuildContext
{
ReadFileSystem = fileSystem,
WriteFileSystem = fileSystem
}) { }
public DocumentationSet(BuildContext context) : this(null, null, context) { }

public DocumentationSet(IDirectoryInfo? sourcePath, IDirectoryInfo? outputPath, BuildContext context)
{
Expand Down
27 changes: 1 addition & 26 deletions src/Elastic.Markdown/Myst/MarkdownParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,6 @@

namespace Elastic.Markdown.Myst;


public class ParserContext : MarkdownParserContext
{
public ParserContext(MarkdownParser markdownParser,
IFileInfo path,
YamlFrontMatter? frontMatter,
BuildContext context)
{
Parser = markdownParser;
Path = path;
FrontMatter = frontMatter;
Build = context;

if (frontMatter?.Properties is { } props)
{
foreach (var (key, value) in props)
Properties[key] = value;
}
}

public MarkdownParser Parser { get; }
public IFileInfo Path { get; }
public YamlFrontMatter? FrontMatter { get; }
public BuildContext Build { get; }
}

public class MarkdownParser(IDirectoryInfo sourcePath, BuildContext context)
{
public IDirectoryInfo SourcePath { get; } = sourcePath;
Expand All @@ -46,6 +20,7 @@ public class MarkdownParser(IDirectoryInfo sourcePath, BuildContext context)
public MarkdownPipeline Pipeline =>
new MarkdownPipelineBuilder()
.EnableTrackTrivia()
.UsePreciseSourceLocation()
.UseGenericAttributes()
.UseEmphasisExtras(EmphasisExtraOptions.Default)
.UseSoftlineBreakAsHardlineBreak()
Expand Down
47 changes: 47 additions & 0 deletions src/Elastic.Markdown/Myst/ParserContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.IO.Abstractions;
using Markdig;
using Markdig.Parsers;

namespace Elastic.Markdown.Myst;

public static class ParserContextExtensions
{
public static BuildContext GetBuildContext(this InlineProcessor processor) =>
processor.GetContext().Build;

public static BuildContext GetBuildContext(this BlockProcessor processor) =>
processor.GetContext().Build;

public static ParserContext GetContext(this InlineProcessor processor) =>
processor.Context as ParserContext
?? throw new InvalidOperationException($"Provided context is not a {nameof(ParserContext)}");

public static ParserContext GetContext(this BlockProcessor processor) =>
processor.Context as ParserContext
?? throw new InvalidOperationException($"Provided context is not a {nameof(ParserContext)}");
}

public class ParserContext : MarkdownParserContext
{
public ParserContext(MarkdownParser markdownParser,
IFileInfo path,
YamlFrontMatter? frontMatter,
BuildContext context)
{
Parser = markdownParser;
Path = path;
FrontMatter = frontMatter;
Build = context;

if (frontMatter?.Properties is { } props)
{
foreach (var (key, value) in props)
Properties[key] = value;
}
}

public MarkdownParser Parser { get; }
public IFileInfo Path { get; }
public YamlFrontMatter? FrontMatter { get; }
public BuildContext Build { get; }
}
3 changes: 3 additions & 0 deletions src/Elastic.Markdown/Myst/Substitution/SubstitutionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics;
using System.Net.Mime;
using System.Runtime.CompilerServices;
using Elastic.Markdown.Diagnostics;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Renderers;
Expand Down Expand Up @@ -153,6 +154,8 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
Column = column,
DelimiterCount = openSticks
};
if (!found)
processor.EmitError(line + 1, column + 3 , $"Substitution key {{{key}}} is undefined");

if (processor.TrackTrivia)
{
Expand Down
10 changes: 7 additions & 3 deletions src/docs-builder/Cli/Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
using System.IO.Abstractions;
using Actions.Core.Services;
using ConsoleAppFramework;
using Documentation.Builder.Diagnostics;
using Documentation.Builder.Http;
using Elastic.Markdown;
using Elastic.Markdown.Diagnostics;
using Microsoft.Extensions.Logging;

namespace Documentation.Builder.Cli;
Expand Down Expand Up @@ -38,7 +40,7 @@ public async Task Serve(string? path = null, Cancel ctx = default)
[Command("generate")]
[ConsoleAppFilter<StopwatchFilter>]
[ConsoleAppFilter<CatchExceptionFilter>]
public async Task Generate(
public async Task<int> Generate(
string? path = null,
string? output = null,
string? pathPrefix = null,
Expand All @@ -53,10 +55,12 @@ public async Task Generate(
UrlPathPrefix = pathPrefix,
Force = force ?? false,
ReadFileSystem = fileSystem,
WriteFileSystem = fileSystem
WriteFileSystem = fileSystem,
Collector = new ConsoleDiagnosticsCollector(logger)
};
var generator = DocumentationGenerator.Create(path, output, context, logger);
await generator.GenerateAll(ctx);
return context.Collector.Errors > 1 ? 1 : 0;
}

/// <summary>
Expand All @@ -70,7 +74,7 @@ public async Task Generate(
[Command("")]
[ConsoleAppFilter<StopwatchFilter>]
[ConsoleAppFilter<CatchExceptionFilter>]
public async Task GenerateDefault(
public async Task<int> GenerateDefault(
string? path = null,
string? output = null,
string? pathPrefix = null,
Expand Down
Loading

0 comments on commit a14e25b

Please sign in to comment.