Skip to content

Commit d05612c

Browse files
authored
Add support for diagnostic hints and track unused substitution keys (#762)
* Add support for diagnostic hints and track unused substitution keys Introduced a "Hint" severity level to diagnostics for improvement suggestions. Enhanced substitution tracking to collect unused keys and provide hints, optimizing substitution usage. Updated console and GitHub outputs to display diagnostic hints alongside errors and warnings for better feedback. * update hint errate display
1 parent 88ff72b commit d05612c

File tree

10 files changed

+141
-60
lines changed

10 files changed

+141
-60
lines changed

src/Elastic.Documentation.Tooling/Diagnostics/Console/ConsoleDiagnosticsCollector.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ public class ConsoleDiagnosticsCollector(ILoggerFactory loggerFactory, ICoreServ
1616
{
1717
private readonly List<Diagnostic> _errors = [];
1818
private readonly List<Diagnostic> _warnings = [];
19+
private readonly List<Diagnostic> _hints = [];
1920

2021
protected override void HandleItem(Diagnostic diagnostic)
2122
{
22-
if (diagnostic.Severity == Severity.Warning)
23+
if (diagnostic.Severity == Severity.Error)
24+
_errors.Add(diagnostic);
25+
else if (diagnostic.Severity == Severity.Warning)
2326
_warnings.Add(diagnostic);
2427
else
25-
_errors.Add(diagnostic);
28+
_hints.Add(diagnostic);
2629
}
2730

2831
private bool _stopped;
@@ -32,10 +35,10 @@ public override async Task StopAsync(Cancel cancellationToken)
3235
return;
3336
_stopped = true;
3437
var repository = new ErrataFileSourceRepository();
35-
repository.WriteDiagnosticsToConsole(_errors, _warnings);
38+
repository.WriteDiagnosticsToConsole(_errors, _warnings, _hints);
3639

3740
AnsiConsole.WriteLine();
38-
AnsiConsole.Write(new Markup($" [bold red]{Errors} Errors[/] / [bold blue]{Warnings} Warnings[/]"));
41+
AnsiConsole.Write(new Markup($" [bold red]{Errors} Errors[/] / [bold blue]{Warnings} Warnings[/] / [bold yellow]{Hints} Hints[/]"));
3942
AnsiConsole.WriteLine();
4043
AnsiConsole.WriteLine();
4144

src/Elastic.Documentation.Tooling/Diagnostics/Console/ErrataFileSourceRepository.cs

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,26 @@ public bool TryGet(string id, [NotNullWhen(true)] out Source? source)
2323
return true;
2424
}
2525

26-
public void WriteDiagnosticsToConsole(IReadOnlyCollection<Diagnostic> errors, IReadOnlyCollection<Diagnostic> warnings)
26+
public void WriteDiagnosticsToConsole(IReadOnlyCollection<Diagnostic> errors, IReadOnlyCollection<Diagnostic> warnings, List<Diagnostic> hints)
2727
{
2828
var report = new Report(this);
29-
var limitedErrors = errors.Take(100).ToArray();
30-
var limitedWarnings = warnings.Take(100 - limitedErrors.Length);
31-
var limited = limitedWarnings.Concat(limitedErrors).ToArray();
29+
var limited = errors
30+
.Concat(warnings)
31+
.OrderBy(d => d.Severity switch { Severity.Error => 0, Severity.Warning => 1, Severity.Hint => 2, _ => 3 })
32+
.Take(100)
33+
.ToArray();
34+
35+
// show hints if we don't have plenty of errors/warnings to show
36+
if (limited.Length < 100)
37+
limited = limited.Concat(hints).Take(100).ToArray();
3238

3339
foreach (var item in limited)
3440
{
3541
var d = item.Severity switch
3642
{
3743
Severity.Error => Errata.Diagnostic.Error(item.Message),
3844
Severity.Warning => Errata.Diagnostic.Warning(item.Message),
45+
Severity.Hint => Errata.Diagnostic.Info(item.Message),
3946
_ => Errata.Diagnostic.Info(item.Message)
4047
};
4148
if (item is { Line: not null, Column: not null })
@@ -44,19 +51,54 @@ public void WriteDiagnosticsToConsole(IReadOnlyCollection<Diagnostic> errors, IR
4451
d = d.WithLabel(new Label(item.File, location, "")
4552
.WithLength(item.Length == null ? 1 : Math.Clamp(item.Length.Value, 1, item.Length.Value + 3))
4653
.WithPriority(1)
47-
.WithColor(item.Severity == Severity.Error ? Color.Red : Color.Blue));
54+
.WithColor(item.Severity switch
55+
{
56+
Severity.Error => Color.Red,
57+
Severity.Warning => Color.Blue,
58+
Severity.Hint => Color.Yellow,
59+
_ => Color.Blue
60+
}));
4861
}
4962
else
5063
d = d.WithNote(item.File);
5164

65+
if (item.Severity == Severity.Hint)
66+
d = d.WithColor(Color.Yellow).WithCategory("Hint");
67+
5268
_ = report.AddDiagnostic(d);
5369
}
5470

5571
var totalErrorCount = errors.Count + warnings.Count;
5672

5773
AnsiConsole.WriteLine();
5874
if (totalErrorCount <= 0)
75+
{
76+
if (hints.Count > 0)
77+
DisplayHintsOnly(report, hints);
5978
return;
79+
}
80+
DisplayErrorAndWarningSummary(report, totalErrorCount, limited);
81+
}
82+
83+
private static void DisplayHintsOnly(Report report, List<Diagnostic> hints)
84+
{
85+
AnsiConsole.Write(new Markup($" [bold]The following improvement hints found in the documentation[/]"));
86+
AnsiConsole.WriteLine();
87+
AnsiConsole.WriteLine();
88+
// Render the report
89+
report.Render(AnsiConsole.Console);
90+
91+
AnsiConsole.WriteLine();
92+
AnsiConsole.WriteLine();
93+
94+
if (hints.Count >= 100)
95+
AnsiConsole.Write(new Markup($" [bold]Only shown the first [yellow]{100}[/] hints out of [yellow]{hints.Count}[/][/]"));
96+
97+
AnsiConsole.WriteLine();
98+
}
99+
100+
private static void DisplayErrorAndWarningSummary(Report report, int totalErrorCount, Diagnostic[] limited)
101+
{
60102
AnsiConsole.Write(new Markup($" [bold]The following errors and warnings were found in the documentation[/]"));
61103
AnsiConsole.WriteLine();
62104
AnsiConsole.WriteLine();

src/Elastic.Documentation.Tooling/Diagnostics/Console/GithubAnnotationOutput.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,7 @@ public void Write(Diagnostic diagnostic)
2727
githubActions.WriteError(diagnostic.Message, properties);
2828
if (diagnostic.Severity == Severity.Warning)
2929
githubActions.WriteWarning(diagnostic.Message, properties);
30+
if (diagnostic.Severity == Severity.Hint)
31+
githubActions.WriteNotice(diagnostic.Message, properties);
3032
}
3133
}

src/Elastic.Documentation.Tooling/Diagnostics/Log.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,19 @@ public void Write(Diagnostic diagnostic)
1616
{
1717
if (diagnostic.Severity == Severity.Error)
1818
logger.LogError("{Message}", diagnostic.Message);
19-
else
19+
else if (diagnostic.Severity == Severity.Warning)
2020
logger.LogWarning("{Message}", diagnostic.Message);
21+
else
22+
logger.LogInformation("{Message}", diagnostic.Message);
2123
}
2224
else
2325
{
2426
if (diagnostic.Severity == Severity.Error)
2527
logger.LogError("{Message} ({File}:{Line})", diagnostic.Message, diagnostic.File, diagnostic.Line ?? 0);
28+
else if (diagnostic.Severity == Severity.Warning)
29+
logger.LogWarning("{Message}", diagnostic.Message);
2630
else
27-
logger.LogWarning("{Message} ({File}:{Line})", diagnostic.Message, diagnostic.File, diagnostic.Line ?? 0);
31+
logger.LogInformation("{Message} ({File}:{Line})", diagnostic.Message, diagnostic.File, diagnostic.Line ?? 0);
2832
}
2933
}
3034
}

src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@
88

99
namespace Elastic.Markdown.Diagnostics;
1010

11+
public enum Severity
12+
{
13+
Error,
14+
Warning,
15+
Hint
16+
}
17+
18+
public readonly record struct Diagnostic
19+
{
20+
public Severity Severity { get; init; }
21+
public int? Line { get; init; }
22+
public int? Column { get; init; }
23+
public int? Length { get; init; }
24+
public string File { get; init; }
25+
public string Message { get; init; }
26+
}
27+
1128
public sealed class DiagnosticsChannel : IDisposable
1229
{
1330
private readonly Channel<Diagnostic> _channel;
@@ -18,7 +35,11 @@ public sealed class DiagnosticsChannel : IDisposable
1835

1936
public DiagnosticsChannel()
2037
{
21-
var options = new UnboundedChannelOptions { SingleReader = true, SingleWriter = false };
38+
var options = new UnboundedChannelOptions
39+
{
40+
SingleReader = true,
41+
SingleWriter = false
42+
};
2243
_ctxSource = new CancellationTokenSource();
2344
_channel = Channel.CreateUnbounded<Diagnostic>(options);
2445
}
@@ -43,18 +64,6 @@ public void Write(Diagnostic diagnostic)
4364
public void Dispose() => _ctxSource.Dispose();
4465
}
4566

46-
public enum Severity { Error, Warning }
47-
48-
public readonly record struct Diagnostic
49-
{
50-
public Severity Severity { get; init; }
51-
public int? Line { get; init; }
52-
public int? Column { get; init; }
53-
public int? Length { get; init; }
54-
public string File { get; init; }
55-
public string Message { get; init; }
56-
}
57-
5867
public interface IDiagnosticsOutput
5968
{
6069
void Write(Diagnostic diagnostic);
@@ -67,13 +76,17 @@ public class DiagnosticsCollector(IReadOnlyCollection<IDiagnosticsOutput> output
6776

6877
private int _errors;
6978
private int _warnings;
79+
private int _hints;
7080
public int Warnings => _warnings;
7181
public int Errors => _errors;
82+
public int Hints => _hints;
7283

7384
private Task? _started;
7485

7586
public HashSet<string> OffendingFiles { get; } = [];
7687

88+
public HashSet<string> InUseSubstitutionKeys { get; } = [];
89+
7790
public ConcurrentBag<string> CrossLinks { get; } = [];
7891

7992
public Task StartAsync(Cancel cancellationToken)
@@ -119,6 +132,8 @@ private void IncrementSeverityCount(Diagnostic item)
119132
_ = Interlocked.Increment(ref _errors);
120133
else if (item.Severity == Severity.Warning)
121134
_ = Interlocked.Increment(ref _warnings);
135+
else if (item.Severity == Severity.Hint)
136+
_ = Interlocked.Increment(ref _hints);
122137
}
123138

124139
protected virtual void HandleItem(Diagnostic diagnostic) { }
@@ -132,35 +147,33 @@ public virtual async Task StopAsync(Cancel cancellationToken)
132147

133148
public void EmitCrossLink(string link) => CrossLinks.Add(link);
134149

135-
public void EmitError(string file, string message, Exception? e = null)
136-
{
137-
var d = new Diagnostic
150+
private void Emit(Severity severity, string file, string message) =>
151+
Channel.Write(new Diagnostic
138152
{
139-
Severity = Severity.Error,
153+
Severity = severity,
140154
File = file,
141155
Message = message
142-
+ (e != null ? Environment.NewLine + e : string.Empty)
143-
+ (e?.InnerException != null ? Environment.NewLine + e.InnerException : string.Empty),
156+
});
144157

145-
};
146-
Channel.Write(d);
147-
}
148-
149-
public void EmitWarning(string file, string message)
158+
public void EmitError(string file, string message, Exception? e = null)
150159
{
151-
var d = new Diagnostic
152-
{
153-
Severity = Severity.Warning,
154-
File = file,
155-
Message = message,
156-
};
157-
Channel.Write(d);
160+
message = message
161+
+ (e != null ? Environment.NewLine + e : string.Empty)
162+
+ (e?.InnerException != null ? Environment.NewLine + e.InnerException : string.Empty);
163+
Emit(Severity.Error, file, message);
158164
}
159165

166+
public void EmitWarning(string file, string message) => Emit(Severity.Warning, file, message);
167+
168+
public void EmitHint(string file, string message) => Emit(Severity.Hint, file, message);
169+
160170
public async ValueTask DisposeAsync()
161171
{
162172
Channel.TryComplete();
163173
await StopAsync(CancellationToken.None);
164174
GC.SuppressFinalize(this);
165175
}
176+
177+
public void CollectUsedSubstitutionKey(ReadOnlySpan<char> key) =>
178+
_ = InUseSubstitutionKeys.Add(key.ToString());
166179
}

src/Elastic.Markdown/DocumentationGenerator.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ public async Task GenerateAll(Cancel ctx)
8787

8888
await ProcessDocumentationFiles(offendingFiles, outputSeenChanges, ctx);
8989

90+
HintUnusedSubstitutionKeys();
91+
9092
await ExtractEmbeddedStaticResources(ctx);
9193

9294
_logger.LogInformation($"Completing diagnostics channel");
@@ -135,6 +137,24 @@ await Parallel.ForEachAsync(DocumentationSet.Files, ctx, async (file, token) =>
135137
_logger.LogInformation("-> Processed {ProcessedFileCount}/{TotalFileCount} files", processedFileCount, totalFileCount);
136138
}
137139

140+
private void HintUnusedSubstitutionKeys()
141+
{
142+
var definedKeys = new HashSet<string>(Context.Configuration.Substitutions.Keys.ToArray());
143+
var keysNotInUse = definedKeys.Except(Context.Collector.InUseSubstitutionKeys).ToArray();
144+
// If we have less than 20 unused keys emit them separately
145+
// Otherwise emit one hint with all of them for brevity
146+
if (keysNotInUse.Length >= 20)
147+
{
148+
var keys = string.Join(", ", keysNotInUse);
149+
Context.Collector.EmitHint(Context.ConfigurationPath.FullName, $"The following keys: '{keys}' are not used in any file");
150+
}
151+
else
152+
{
153+
foreach (var key in keysNotInUse)
154+
Context.Collector.EmitHint(Context.ConfigurationPath.FullName, $"Substitution key '{key}' is not used in any file");
155+
}
156+
}
157+
138158
private async Task ExtractEmbeddedStaticResources(Cancel ctx)
139159
{
140160
_logger.LogInformation($"Copying static files to output directory");

src/Elastic.Markdown/Helpers/Interpolation.cs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System.Diagnostics.CodeAnalysis;
66
using System.Text.RegularExpressions;
7+
using Elastic.Markdown.Diagnostics;
78
using Elastic.Markdown.Myst;
89

910
namespace Elastic.Markdown.Helpers;
@@ -22,32 +23,26 @@ ParserContext context
2223
)
2324
{
2425
var span = input.AsSpan();
25-
return span.ReplaceSubstitutions([context.Substitutions, context.ContextSubstitutions], out var replacement)
26+
return span.ReplaceSubstitutions([context.Substitutions, context.ContextSubstitutions], context.Build.Collector, out var replacement)
2627
? replacement : input;
2728
}
2829

29-
30-
public static bool ReplaceSubstitutions(
31-
this ReadOnlySpan<char> span,
32-
ParserContext context,
33-
[NotNullWhen(true)] out string? replacement
34-
) =>
35-
span.ReplaceSubstitutions([context.Substitutions, context.ContextSubstitutions], out replacement);
36-
3730
public static bool ReplaceSubstitutions(
3831
this ReadOnlySpan<char> span,
3932
IReadOnlyDictionary<string, string>? properties,
33+
DiagnosticsCollector? collector,
4034
[NotNullWhen(true)] out string? replacement
4135
)
4236
{
4337
replacement = null;
4438
return properties is not null && properties.Count != 0 &&
45-
span.IndexOf("}}") >= 0 && span.ReplaceSubstitutions([properties], out replacement);
39+
span.IndexOf("}}") >= 0 && span.ReplaceSubstitutions([properties], collector, out replacement);
4640
}
4741

48-
public static bool ReplaceSubstitutions(
42+
private static bool ReplaceSubstitutions(
4943
this ReadOnlySpan<char> span,
5044
IReadOnlyDictionary<string, string>[] properties,
45+
DiagnosticsCollector? collector,
5146
[NotNullWhen(true)] out string? replacement
5247
)
5348
{
@@ -78,6 +73,8 @@ public static bool ReplaceSubstitutions(
7873
if (!lookup.TryGetValue(key, out var value))
7974
continue;
8075

76+
collector?.CollectUsedSubstitutionKey(key);
77+
8178
replacement ??= span.ToString();
8279
replacement = replacement.Replace(spanMatch.ToString(), value);
8380
replaced = true;

src/Elastic.Markdown/IO/MarkdownFile.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ protected void ReadDocumentInstructions(MarkdownDocument document)
210210

211211
if (!string.IsNullOrEmpty(NavigationTitle))
212212
{
213-
if (NavigationTitle.AsSpan().ReplaceSubstitutions(subs, out var replacement))
213+
if (NavigationTitle.AsSpan().ReplaceSubstitutions(subs, Collector, out var replacement))
214214
NavigationTitle = replacement;
215215
}
216216

@@ -219,7 +219,7 @@ protected void ReadDocumentInstructions(MarkdownDocument document)
219219
Title = RelativePath;
220220
Collector.EmitWarning(FilePath, "Document has no title, using file name as title.");
221221
}
222-
else if (Title.AsSpan().ReplaceSubstitutions(subs, out var replacement))
222+
else if (Title.AsSpan().ReplaceSubstitutions(subs, Collector, out var replacement))
223223
Title = replacement;
224224

225225
var toc = GetAnchors(_set, MarkdownParser, YamlFrontMatter, document, subs, out var anchors);
@@ -272,7 +272,7 @@ public static List<PageTocItem> GetAnchors(
272272
.Concat(includedTocs)
273273
.Select(toc => subs.Count == 0
274274
? toc
275-
: toc.Heading.AsSpan().ReplaceSubstitutions(subs, out var r)
275+
: toc.Heading.AsSpan().ReplaceSubstitutions(subs, set.Build.Collector, out var r)
276276
? toc with { Heading = r }
277277
: toc)
278278
.ToList();

0 commit comments

Comments
 (0)