Skip to content

Commit ee11a02

Browse files
committed
Hints and suppresshints
1 parent 060520f commit ee11a02

File tree

11 files changed

+1575
-31
lines changed

11 files changed

+1575
-31
lines changed

src/Elastic.Documentation.Configuration/ConfigurationFileProvider.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.IO.Abstractions;
66
using System.Text.RegularExpressions;
77
using Elastic.Documentation.Configuration.Assembler;
8+
using Elastic.Documentation.Configuration.Converters;
89
using Elastic.Documentation.Configuration.DocSet;
910
using Elastic.Documentation.Configuration.Serialization;
1011
using Microsoft.Extensions.DependencyInjection;
@@ -22,6 +23,7 @@ public partial class ConfigurationFileProvider
2223

2324
public static IDeserializer Deserializer { get; } = new StaticDeserializerBuilder(new YamlStaticContext())
2425
.WithNamingConvention(UnderscoredNamingConvention.Instance)
26+
.WithTypeConverter(new HintTypeSetConverter())
2527
.WithTypeConverter(new TocItemCollectionYamlConverter())
2628
.WithTypeConverter(new TocItemYamlConverter())
2729
.WithTypeConverter(new SiteTableOfContentsCollectionYamlConverter())
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Documentation.Diagnostics;
6+
using YamlDotNet.Core;
7+
using YamlDotNet.Core.Events;
8+
using YamlDotNet.Serialization;
9+
10+
namespace Elastic.Documentation.Configuration.Converters;
11+
12+
/// <summary>
13+
/// YAML converter for deserializing a list of strings into a HashSet of HintType enums.
14+
/// </summary>
15+
public class HintTypeSetConverter : IYamlTypeConverter
16+
{
17+
public bool Accepts(Type type) => type == typeof(HashSet<HintType>);
18+
19+
public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
20+
{
21+
var result = new HashSet<HintType>();
22+
23+
// Handle null/empty case
24+
if (parser.Current is not SequenceStart)
25+
{
26+
_ = parser.MoveNext();
27+
return result;
28+
}
29+
30+
_ = parser.MoveNext(); // Skip SequenceStart
31+
32+
while (parser.Current is not SequenceEnd)
33+
{
34+
if (parser.Current is Scalar scalar)
35+
{
36+
var value = scalar.Value;
37+
if (!string.IsNullOrWhiteSpace(value) &&
38+
Enum.TryParse<HintType>(value, ignoreCase: true, out var hintType))
39+
{
40+
_ = result.Add(hintType);
41+
}
42+
}
43+
_ = parser.MoveNext();
44+
}
45+
46+
_ = parser.MoveNext(); // Skip SequenceEnd
47+
48+
return result;
49+
}
50+
51+
public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer)
52+
{
53+
if (value is not HashSet<HintType> set)
54+
{
55+
emitter.Emit(new SequenceStart(null, null, false, SequenceStyle.Block));
56+
emitter.Emit(new SequenceEnd());
57+
return;
58+
}
59+
60+
emitter.Emit(new SequenceStart(null, null, false, SequenceStyle.Block));
61+
foreach (var hint in set)
62+
{
63+
emitter.Emit(new Scalar(hint.ToString()));
64+
}
65+
emitter.Emit(new SequenceEnd());
66+
}
67+
}

src/Elastic.Documentation.Configuration/DocSet/DocumentationSetFile.cs

Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.IO.Abstractions;
66
using Elastic.Documentation.Configuration.Products;
77
using Elastic.Documentation.Diagnostics;
8+
using Elastic.Documentation.Extensions;
89
using YamlDotNet.Core;
910
using YamlDotNet.Core.Events;
1011
using YamlDotNet.Serialization;
@@ -20,6 +21,13 @@ public class TableOfContentsFile
2021
[YamlMember(Alias = "toc")]
2122
public TableOfContents TableOfContents { get; set; } = [];
2223

24+
/// <summary>
25+
/// Set of diagnostic hint types to suppress. Deserialized directly from YAML list of strings.
26+
/// Valid values: "DeepLinkingVirtualFile", "FolderFileNameMismatch"
27+
/// </summary>
28+
[YamlMember(Alias = "suppress")]
29+
public HashSet<HintType> SuppressDiagnostics { get; set; } = [];
30+
2331
public static TableOfContentsFile Deserialize(string json) =>
2432
ConfigurationFileProvider.Deserializer.Deserialize<TableOfContentsFile>(json);
2533
}
@@ -93,8 +101,8 @@ public static DocumentationSetFile LoadAndResolve(IDiagnosticsCollector collecto
93101
{
94102
fileSystem ??= sourceDirectory.FileSystem;
95103
var docSet = Deserialize(yaml);
96-
var docsetPath = fileSystem.Path.Combine(sourceDirectory.FullName, "docset.yml");
97-
docSet.TableOfContents = ResolveTableOfContents(collector, docSet.TableOfContents, sourceDirectory, fileSystem, parentPath: "", context: docsetPath);
104+
var docsetPath = fileSystem.Path.Combine(sourceDirectory.FullName, "docset.yml").OptionalWindowsReplace();
105+
docSet.TableOfContents = ResolveTableOfContents(collector, docSet.TableOfContents, sourceDirectory, fileSystem, parentPath: "", context: docsetPath, docSet.SuppressDiagnostics);
98106
return docSet;
99107
}
100108

@@ -111,7 +119,8 @@ private static TableOfContents ResolveTableOfContents(
111119
IDirectoryInfo baseDirectory,
112120
IFileSystem fileSystem,
113121
string parentPath,
114-
string context
122+
string context,
123+
HashSet<HintType>? suppressDiagnostics = null
115124
)
116125
{
117126
var resolved = new TableOfContents();
@@ -120,9 +129,9 @@ string context
120129
{
121130
var resolvedItem = item switch
122131
{
123-
IsolatedTableOfContentsRef tocRef => ResolveIsolatedToc(collector, tocRef, baseDirectory, fileSystem, parentPath, context),
124-
FileRef fileRef => ResolveFileRef(collector, fileRef, baseDirectory, fileSystem, parentPath, context),
125-
FolderRef folderRef => ResolveFolderRef(collector, folderRef, baseDirectory, fileSystem, parentPath, context),
132+
IsolatedTableOfContentsRef tocRef => ResolveIsolatedToc(collector, tocRef, baseDirectory, fileSystem, parentPath, context, suppressDiagnostics),
133+
FileRef fileRef => ResolveFileRef(collector, fileRef, baseDirectory, fileSystem, parentPath, context, suppressDiagnostics),
134+
FolderRef folderRef => ResolveFolderRef(collector, folderRef, baseDirectory, fileSystem, parentPath, context, suppressDiagnostics),
126135
CrossLinkRef crossLink => ResolveCrossLinkRef(collector, crossLink, baseDirectory, fileSystem, parentPath, context),
127136
_ => null
128137
};
@@ -139,14 +148,17 @@ string context
139148
/// Validates that the TOC has no children in parent YAML and that toc.yml exists.
140149
/// The TOC's path is set to the full path (including parent path) for consistency with files and folders.
141150
/// </summary>
151+
#pragma warning disable IDE0060 // Remove unused parameter - suppressDiagnostics is for consistency, nested TOCs use their own suppression config
142152
private static ITableOfContentsItem? ResolveIsolatedToc(
143153
IDiagnosticsCollector collector,
144154
IsolatedTableOfContentsRef tocRef,
145155
IDirectoryInfo baseDirectory,
146156
IFileSystem fileSystem,
147157
string parentPath,
148-
string parentContext
158+
string parentContext,
159+
HashSet<HintType>? suppressDiagnostics = null
149160
)
161+
#pragma warning restore IDE0060
150162
{
151163
// TOC paths containing '/' are treated as relative to the context file's directory (full paths).
152164
// Simple TOC names (no '/') are resolved relative to the parent path in the navigation hierarchy.
@@ -190,12 +202,31 @@ string parentContext
190202
}
191203

192204
var tocYaml = fileSystem.File.ReadAllText(tocFilePath);
193-
var tocFile = TableOfContentsFile.Deserialize(tocYaml);
205+
var nestedTocFile = TableOfContentsFile.Deserialize(tocYaml);
206+
207+
// this is temporary after this lands in main we can update these files to include
208+
// suppress:
209+
// - DeepLinkingVirtualFile
210+
string[] skip = [
211+
"docs-content/solutions/toc.yml",
212+
"docs-content/manage-data/toc.yml",
213+
"docs-content/explore-analyze/toc.yml",
214+
"docs-content/deploy-manage/toc.yml",
215+
"docs-content/troubleshoot/toc.yml",
216+
"docs-content/troubleshoot/ingest/opentelemetry/toc.yml",
217+
"docs-content/reference/security/toc.yml"
218+
];
219+
220+
var path = tocFilePath.OptionalWindowsReplace();
221+
// Hardcode suppression for known problematic files
222+
if (skip.Any(f => path.Contains(f, StringComparison.OrdinalIgnoreCase)))
223+
_ = nestedTocFile.SuppressDiagnostics.Add(HintType.DeepLinkingVirtualFile);
224+
194225

195226
// Recursively resolve children with the FULL TOC path as the parent path
196227
// This ensures all file paths within the TOC include the TOC directory path
197228
// The context for children is the toc.yml file that defines them
198-
var resolvedChildren = ResolveTableOfContents(collector, tocFile.TableOfContents, baseDirectory, fileSystem, fullTocPath, tocFilePath);
229+
var resolvedChildren = ResolveTableOfContents(collector, nestedTocFile.TableOfContents, baseDirectory, fileSystem, fullTocPath, tocFilePath, nestedTocFile.SuppressDiagnostics);
199230

200231
// Validate: TOC must have at least one child
201232
if (resolvedChildren.Count == 0)
@@ -218,7 +249,8 @@ private static ITableOfContentsItem ResolveFileRef(
218249
IDirectoryInfo baseDirectory,
219250
IFileSystem fileSystem,
220251
string parentPath,
221-
string context)
252+
string context,
253+
HashSet<HintType>? suppressDiagnostics = null)
222254
{
223255
var fullPath = string.IsNullOrEmpty(parentPath) ? fileRef.Path : $"{parentPath}/{fileRef.Path}";
224256

@@ -241,18 +273,22 @@ private static ITableOfContentsItem ResolveFileRef(
241273
// Only check if we're in a folder context (parentPath is not empty)
242274
if (!string.IsNullOrEmpty(parentPath) && fileName != "index.md")
243275
{
244-
// Extract just the folder name from parentPath (in case it's nested like "guides/getting-started")
245-
var folderName = parentPath.Contains('/') ? parentPath.Split('/')[^1] : parentPath;
276+
// Check if this hint type should be suppressed
277+
if (!suppressDiagnostics.ShouldSuppress(HintType.FolderFileNameMismatch))
278+
{
279+
// Extract just the folder name from parentPath (in case it's nested like "guides/getting-started")
280+
var folderName = parentPath.Contains('/') ? parentPath.Split('/')[^1] : parentPath;
246281

247-
// Normalize for comparison: remove hyphens, underscores, and lowercase
248-
// This allows "getting-started" to match "GettingStarted" or "getting_started"
249-
var normalizedFile = fileWithoutExtension.Replace("-", "", StringComparison.Ordinal).Replace("_", "", StringComparison.Ordinal).ToLowerInvariant();
250-
var normalizedFolder = folderName.Replace("-", "", StringComparison.Ordinal).Replace("_", "", StringComparison.Ordinal).ToLowerInvariant();
282+
// Normalize for comparison: remove hyphens, underscores, and lowercase
283+
// This allows "getting-started" to match "GettingStarted" or "getting_started"
284+
var normalizedFile = fileWithoutExtension.Replace("-", "", StringComparison.Ordinal).Replace("_", "", StringComparison.Ordinal).ToLowerInvariant();
285+
var normalizedFolder = folderName.Replace("-", "", StringComparison.Ordinal).Replace("_", "", StringComparison.Ordinal).ToLowerInvariant();
251286

252-
if (!normalizedFile.Equals(normalizedFolder, StringComparison.Ordinal))
253-
{
254-
collector.EmitHint(context,
255-
$"File name '{fileName}' does not match folder name '{folderName}'. Best practice is to name the file the same as the folder (e.g., 'folder: {folderName}, file: {folderName}.md').");
287+
if (!normalizedFile.Equals(normalizedFolder, StringComparison.Ordinal))
288+
{
289+
collector.EmitHint(context,
290+
$"File name '{fileName}' does not match folder name '{folderName}'. Best practice is to name the file the same as the folder (e.g., 'folder: {folderName}, file: {folderName}.md').");
291+
}
256292
}
257293
}
258294
}
@@ -272,8 +308,12 @@ private static ITableOfContentsItem ResolveFileRef(
272308
// This suggests using 'folder' instead of 'file' would be better
273309
if (fileRef.Path.Contains('/') && fileRef.Children.Count > 0 && fileRef is not FolderIndexFileRef)
274310
{
275-
collector.EmitHint(context,
276-
$"File '{fileRef.Path}' uses deep-linking with children. Consider using 'folder' instead of 'file' for better navigation structure. Virtual files are primarily intended to group sibling files together.");
311+
// Check if this hint type should be suppressed
312+
if (!suppressDiagnostics.ShouldSuppress(HintType.DeepLinkingVirtualFile))
313+
{
314+
collector.EmitHint(context,
315+
$"File '{fileRef.Path}' uses deep-linking with children. Consider using 'folder' instead of 'file' for better navigation structure. Virtual files are primarily intended to group sibling files together.");
316+
}
277317
}
278318

279319
// Children of a file should be resolved in the same directory as the parent file.
@@ -304,7 +344,7 @@ private static ITableOfContentsItem ResolveFileRef(
304344
parentPathForChildren = parentPath;
305345
}
306346

307-
var resolvedChildren = ResolveTableOfContents(collector, fileRef.Children, baseDirectory, fileSystem, parentPathForChildren, context);
347+
var resolvedChildren = ResolveTableOfContents(collector, fileRef.Children, baseDirectory, fileSystem, parentPathForChildren, context, suppressDiagnostics);
308348

309349
// Preserve the specific type when creating the resolved reference
310350
return fileRef switch
@@ -325,7 +365,8 @@ private static ITableOfContentsItem ResolveFolderRef(
325365
IDirectoryInfo baseDirectory,
326366
IFileSystem fileSystem,
327367
string parentPath,
328-
string context)
368+
string context,
369+
HashSet<HintType>? suppressDiagnostics = null)
329370
{
330371
// Folder paths containing '/' are treated as relative to the context file's directory (full paths).
331372
// Simple folder names (no '/') are resolved relative to the parent path in the navigation hierarchy.
@@ -351,7 +392,7 @@ private static ITableOfContentsItem ResolveFolderRef(
351392
// If children are explicitly defined, resolve them
352393
if (folderRef.Children.Count > 0)
353394
{
354-
var resolvedChildren = ResolveTableOfContents(collector, folderRef.Children, baseDirectory, fileSystem, fullPath, context);
395+
var resolvedChildren = ResolveTableOfContents(collector, folderRef.Children, baseDirectory, fileSystem, fullPath, context, suppressDiagnostics);
355396
return new FolderRef(fullPath, resolvedChildren, context);
356397
}
357398

0 commit comments

Comments
 (0)