Skip to content

Commit a14e25b

Browse files
committed
initial generation diagnostics infrastructure
1 parent 0844698 commit a14e25b

File tree

18 files changed

+340
-49
lines changed

18 files changed

+340
-49
lines changed

docs/source/markup/substitutions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ Here are some variable substitutions:
99
| Value | Source |
1010
| ------------------- | ------------ |
1111
| {{project}} | conf.py |
12-
| {{frontmatter_key}} | Front Matter |
12+
| {{frontmatter_key}} | Front Matter |

src/Elastic.Markdown/BuildContext.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44
using System.IO.Abstractions;
5+
using Elastic.Markdown.Diagnostics;
56

67
namespace Elastic.Markdown;
78

89
public record BuildContext
910
{
10-
private readonly string? _urlPathPrefix;
11+
public required IFileSystem ReadFileSystem { get; init; }
12+
public required IFileSystem WriteFileSystem { get; init; }
13+
public required DiagnosticsCollector Collector { get; init; }
14+
1115
public bool Force { get; init; }
1216

1317
public string? UrlPathPrefix
@@ -16,6 +20,6 @@ public string? UrlPathPrefix
1620
init => _urlPathPrefix = value;
1721
}
1822

19-
public required IFileSystem ReadFileSystem { get; init; }
20-
public required IFileSystem WriteFileSystem { get; init; }
23+
private readonly string? _urlPathPrefix;
24+
2125
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using System.Threading.Channels;
2+
using Microsoft.Extensions.Hosting;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace Elastic.Markdown.Diagnostics;
6+
7+
public class DiagnosticsChannel
8+
{
9+
private readonly Channel<Diagnostic> _channel;
10+
private readonly CancellationTokenSource _ctxSource;
11+
public ChannelReader<Diagnostic> Reader => _channel.Reader;
12+
13+
public CancellationToken CancellationToken => _ctxSource.Token;
14+
15+
public DiagnosticsChannel()
16+
{
17+
var options = new UnboundedChannelOptions { SingleReader = true, SingleWriter = false };
18+
_ctxSource = new CancellationTokenSource();
19+
_channel = Channel.CreateUnbounded<Diagnostic>(options);
20+
}
21+
22+
public void TryComplete(Exception? exception = null)
23+
{
24+
_channel.Writer.TryComplete(exception);
25+
_ctxSource.Cancel();
26+
}
27+
28+
public void Write(Diagnostic diagnostic)
29+
{
30+
var written = _channel.Writer.TryWrite(diagnostic);
31+
if (!written)
32+
{
33+
//TODO
34+
}
35+
}
36+
}
37+
38+
39+
public enum Severity { Error, Warning }
40+
41+
public readonly record struct Diagnostic
42+
{
43+
public Severity Severity { get; init; }
44+
public int Line { get; init; }
45+
public int? Position { get; init; }
46+
public string File { get; init; }
47+
public string Message { get; init; }
48+
}
49+
50+
public interface IDiagnosticsOutput
51+
{
52+
public void Write(Diagnostic diagnostic);
53+
}
54+
55+
public class LogDiagnosticOutput(ILogger logger) : IDiagnosticsOutput
56+
{
57+
public void Write(Diagnostic diagnostic)
58+
{
59+
if (diagnostic.Severity == Severity.Error)
60+
logger.LogError($"{diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})");
61+
else
62+
logger.LogWarning($"{diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})");
63+
}
64+
}
65+
66+
67+
public class DiagnosticsCollector(ILoggerFactory loggerFactory, IReadOnlyCollection<IDiagnosticsOutput> outputs)
68+
: IHostedService
69+
{
70+
private readonly IReadOnlyCollection<IDiagnosticsOutput> _outputs =
71+
[new LogDiagnosticOutput(loggerFactory.CreateLogger<LogDiagnosticOutput>()), ..outputs];
72+
73+
public DiagnosticsChannel Channel { get; } = new();
74+
75+
private long _errors;
76+
private long _warnings;
77+
public long Warnings => _warnings;
78+
public long Errors => _errors;
79+
80+
public async Task StartAsync(Cancel ctx)
81+
{
82+
while (!Channel.CancellationToken.IsCancellationRequested)
83+
{
84+
while (await Channel.Reader.WaitToReadAsync(Channel.CancellationToken))
85+
Drain();
86+
}
87+
Drain();
88+
89+
void Drain()
90+
{
91+
while (Channel.Reader.TryRead(out var item))
92+
{
93+
IncrementSeverityCount(item);
94+
HandleItem(item);
95+
foreach (var output in _outputs)
96+
output.Write(item);
97+
}
98+
}
99+
}
100+
101+
private void IncrementSeverityCount(Diagnostic item)
102+
{
103+
if (item.Severity == Severity.Error)
104+
Interlocked.Increment(ref _errors);
105+
else if (item.Severity == Severity.Warning)
106+
Interlocked.Increment(ref _warnings);
107+
}
108+
109+
protected virtual void HandleItem(Diagnostic diagnostic) {}
110+
111+
public virtual Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
112+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Elastic.Markdown.Myst;
2+
using Markdig.Helpers;
3+
using Markdig.Parsers;
4+
5+
namespace Elastic.Markdown.Diagnostics;
6+
7+
public static class ProcessorDiagnosticExtensions
8+
{
9+
public static void EmitError(this InlineProcessor processor, int line, int position, string message)
10+
{
11+
var d = new Diagnostic
12+
{
13+
Severity = Severity.Error,
14+
File = processor.GetContext().Path.FullName,
15+
Position = position,
16+
Line = line,
17+
Message = message
18+
};
19+
processor.GetBuildContext().Collector.Channel.Write(d);
20+
}
21+
}

src/Elastic.Markdown/DocumentationGenerator.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44
using System.IO.Abstractions;
5+
using System.Security.Cryptography;
56
using System.Text.Json;
67
using System.Text.Json.Serialization;
78
using Elastic.Markdown.IO;
@@ -102,6 +103,9 @@ public async Task GenerateAll(Cancel ctx)
102103

103104

104105
var handledItems = 0;
106+
107+
var collectTask = Task.Run(async () => await Context.Collector.StartAsync(ctx), ctx);
108+
105109
await Parallel.ForEachAsync(DocumentationSet.Files, ctx, async (file, token) =>
106110
{
107111
if (file.SourceFile.LastWriteTimeUtc <= outputSeenChanges)
@@ -122,19 +126,29 @@ await Parallel.ForEachAsync(DocumentationSet.Files, ctx, async (file, token) =>
122126
if (item % 1_000 == 0)
123127
_logger.LogInformation($"Handled {handledItems} files");
124128
});
129+
Context.Collector.Channel.TryComplete();
130+
131+
await GenerateDocumentationState(ctx);
132+
133+
await collectTask;
134+
await Context.Collector.StopAsync(ctx);
135+
125136

126137
IFileInfo OutputFile(string relativePath)
127138
{
128139
var outputFile = _writeFileSystem.FileInfo.New(Path.Combine(DocumentationSet.OutputPath.FullName, relativePath));
129140
return outputFile;
130141
}
131142

143+
}
144+
145+
private async Task GenerateDocumentationState(Cancel ctx)
146+
{
132147
var stateFile = DocumentationSet.OutputStateFile;
133148
_logger.LogInformation($"Writing documentation state {DocumentationSet.LastWrite} to {stateFile.FullName}");
134149
var state = new OutputState { LastSeenChanges = DocumentationSet.LastWrite };
135150
var bytes = JsonSerializer.SerializeToUtf8Bytes(state, SourceGenerationContext.Default.OutputState);
136151
await DocumentationSet.OutputPath.FileSystem.File.WriteAllBytesAsync(stateFile.FullName, bytes, ctx);
137-
138152
}
139153

140154
private async Task CopyFileFsAware(DocumentationFile file, IFileInfo outputFile, Cancel ctx)

src/Elastic.Markdown/IO/DocumentationSet.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Globalization;
55
using System.IO.Abstractions;
66
using System.Text.Json;
7+
using Elastic.Markdown.Diagnostics;
78
using Elastic.Markdown.Myst;
89

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

2021
private MarkdownParser MarkdownParser { get; }
2122

22-
public DocumentationSet(IFileSystem fileSystem) : this(null, null, new BuildContext
23-
{
24-
ReadFileSystem = fileSystem,
25-
WriteFileSystem = fileSystem
26-
}) { }
23+
public DocumentationSet(BuildContext context) : this(null, null, context) { }
2724

2825
public DocumentationSet(IDirectoryInfo? sourcePath, IDirectoryInfo? outputPath, BuildContext context)
2926
{

src/Elastic.Markdown/Myst/MarkdownParser.cs

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,6 @@
1212

1313
namespace Elastic.Markdown.Myst;
1414

15-
16-
public class ParserContext : MarkdownParserContext
17-
{
18-
public ParserContext(MarkdownParser markdownParser,
19-
IFileInfo path,
20-
YamlFrontMatter? frontMatter,
21-
BuildContext context)
22-
{
23-
Parser = markdownParser;
24-
Path = path;
25-
FrontMatter = frontMatter;
26-
Build = context;
27-
28-
if (frontMatter?.Properties is { } props)
29-
{
30-
foreach (var (key, value) in props)
31-
Properties[key] = value;
32-
}
33-
}
34-
35-
public MarkdownParser Parser { get; }
36-
public IFileInfo Path { get; }
37-
public YamlFrontMatter? FrontMatter { get; }
38-
public BuildContext Build { get; }
39-
}
40-
4115
public class MarkdownParser(IDirectoryInfo sourcePath, BuildContext context)
4216
{
4317
public IDirectoryInfo SourcePath { get; } = sourcePath;
@@ -46,6 +20,7 @@ public class MarkdownParser(IDirectoryInfo sourcePath, BuildContext context)
4620
public MarkdownPipeline Pipeline =>
4721
new MarkdownPipelineBuilder()
4822
.EnableTrackTrivia()
23+
.UsePreciseSourceLocation()
4924
.UseGenericAttributes()
5025
.UseEmphasisExtras(EmphasisExtraOptions.Default)
5126
.UseSoftlineBreakAsHardlineBreak()
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.IO.Abstractions;
2+
using Markdig;
3+
using Markdig.Parsers;
4+
5+
namespace Elastic.Markdown.Myst;
6+
7+
public static class ParserContextExtensions
8+
{
9+
public static BuildContext GetBuildContext(this InlineProcessor processor) =>
10+
processor.GetContext().Build;
11+
12+
public static BuildContext GetBuildContext(this BlockProcessor processor) =>
13+
processor.GetContext().Build;
14+
15+
public static ParserContext GetContext(this InlineProcessor processor) =>
16+
processor.Context as ParserContext
17+
?? throw new InvalidOperationException($"Provided context is not a {nameof(ParserContext)}");
18+
19+
public static ParserContext GetContext(this BlockProcessor processor) =>
20+
processor.Context as ParserContext
21+
?? throw new InvalidOperationException($"Provided context is not a {nameof(ParserContext)}");
22+
}
23+
24+
public class ParserContext : MarkdownParserContext
25+
{
26+
public ParserContext(MarkdownParser markdownParser,
27+
IFileInfo path,
28+
YamlFrontMatter? frontMatter,
29+
BuildContext context)
30+
{
31+
Parser = markdownParser;
32+
Path = path;
33+
FrontMatter = frontMatter;
34+
Build = context;
35+
36+
if (frontMatter?.Properties is { } props)
37+
{
38+
foreach (var (key, value) in props)
39+
Properties[key] = value;
40+
}
41+
}
42+
43+
public MarkdownParser Parser { get; }
44+
public IFileInfo Path { get; }
45+
public YamlFrontMatter? FrontMatter { get; }
46+
public BuildContext Build { get; }
47+
}

src/Elastic.Markdown/Myst/Substitution/SubstitutionParser.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics;
66
using System.Net.Mime;
77
using System.Runtime.CompilerServices;
8+
using Elastic.Markdown.Diagnostics;
89
using Markdig.Helpers;
910
using Markdig.Parsers;
1011
using Markdig.Renderers;
@@ -153,6 +154,8 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
153154
Column = column,
154155
DelimiterCount = openSticks
155156
};
157+
if (!found)
158+
processor.EmitError(line + 1, column + 3 , $"Substitution key {{{key}}} is undefined");
156159

157160
if (processor.TrackTrivia)
158161
{

src/docs-builder/Cli/Commands.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
using System.IO.Abstractions;
55
using Actions.Core.Services;
66
using ConsoleAppFramework;
7+
using Documentation.Builder.Diagnostics;
78
using Documentation.Builder.Http;
89
using Elastic.Markdown;
10+
using Elastic.Markdown.Diagnostics;
911
using Microsoft.Extensions.Logging;
1012

1113
namespace Documentation.Builder.Cli;
@@ -38,7 +40,7 @@ public async Task Serve(string? path = null, Cancel ctx = default)
3840
[Command("generate")]
3941
[ConsoleAppFilter<StopwatchFilter>]
4042
[ConsoleAppFilter<CatchExceptionFilter>]
41-
public async Task Generate(
43+
public async Task<int> Generate(
4244
string? path = null,
4345
string? output = null,
4446
string? pathPrefix = null,
@@ -53,10 +55,12 @@ public async Task Generate(
5355
UrlPathPrefix = pathPrefix,
5456
Force = force ?? false,
5557
ReadFileSystem = fileSystem,
56-
WriteFileSystem = fileSystem
58+
WriteFileSystem = fileSystem,
59+
Collector = new ConsoleDiagnosticsCollector(logger)
5760
};
5861
var generator = DocumentationGenerator.Create(path, output, context, logger);
5962
await generator.GenerateAll(ctx);
63+
return context.Collector.Errors > 1 ? 1 : 0;
6064
}
6165

6266
/// <summary>
@@ -70,7 +74,7 @@ public async Task Generate(
7074
[Command("")]
7175
[ConsoleAppFilter<StopwatchFilter>]
7276
[ConsoleAppFilter<CatchExceptionFilter>]
73-
public async Task GenerateDefault(
77+
public async Task<int> GenerateDefault(
7478
string? path = null,
7579
string? output = null,
7680
string? pathPrefix = null,

0 commit comments

Comments
 (0)