diff --git a/.editorconfig b/.editorconfig index d88b172aa..2e852032b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -143,6 +143,9 @@ csharp_style_expression_bodied_operators = true:suggestion csharp_style_expression_bodied_properties = true:suggestion csharp_style_expression_bodied_indexers = true:suggestion csharp_style_expression_bodied_accessors = true:suggestion +csharp_style_expression_bodied_local_functions = when_on_single_line:error +dotnet_style_prefer_conditional_expression_over_return = false + # Suggest more modern language features when available csharp_style_pattern_matching_over_is_with_cast_check = true:error @@ -197,12 +200,37 @@ resharper_redundant_case_label_highlighting=do_not_show resharper_redundant_argument_default_value_highlighting=do_not_show resharper_explicit_caller_info_argument_highlighting=hint +csharp_style_namespace_declarations = file_scoped + +dotnet_analyzer_diagnostic.severity = warning +dotnet_analyzer_diagnostic.category-Style.severity = warning + +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1848 +dotnet_diagnostic.CA1848.severity = suggestion +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2201 +dotnet_diagnostic.CA2201.severity = none + +# disable for default arm on switches, IDE0072 still covers missing enum members +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0010 +dotnet_diagnostic.IDE0010.severity = none + +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/IDE0200 +dotnet_diagnostic.IDE0200.severity = none + +# TODO enable GenerateDocumentationFile to properly document our code +dotnet_diagnostic.IDE0005.severity = none + +dotnet_diagnostic.IDE0001.severity = none + dotnet_diagnostic.IDE0057.severity = none [DocumentationWebHost.cs] dotnet_diagnostic.IL3050.severity = none dotnet_diagnostic.IL2026.severity = none +[tests/**/*.cs] +dotnet_diagnostic.IDE0058.severity = none + [*.{sh,bat,ps1}] trim_trailing_whitespace=true diff --git a/Directory.Build.props b/Directory.Build.props index 7cbc3b842..a49f44bc9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -21,6 +21,7 @@ enable enable + true diff --git a/src/Directory.Build.props b/src/Directory.Build.props index ac45217ec..0f3e8d750 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -6,8 +6,8 @@ all low true - true + true diff --git a/src/Elastic.Markdown.Refactor/Move.cs b/src/Elastic.Markdown.Refactor/Move.cs index 92d5909b4..59ca49584 100644 --- a/src/Elastic.Markdown.Refactor/Move.cs +++ b/src/Elastic.Markdown.Refactor/Move.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using Elastic.Markdown.IO; using Microsoft.Extensions.Logging; +using static System.StringComparison; namespace Elastic.Markdown.Refactor; @@ -13,9 +14,8 @@ public record ChangeSet(IFileInfo From, IFileInfo To); public record Change(IFileInfo Source, string OriginalContent, string NewContent); public record LinkModification(string OldLink, string NewLink, string SourceFile, int LineNumber, int ColumnNumber); -public class Move(IFileSystem readFileSystem, IFileSystem writeFileSystem, DocumentationSet documentationSet, ILoggerFactory loggerFactory) +public partial class Move(IFileSystem readFileSystem, IFileSystem writeFileSystem, DocumentationSet documentationSet, ILoggerFactory loggerFactory) { - private const string ChangeFormatString = "Change \e[31m{0}\e[0m to \e[32m{1}\e[0m at \e[34m{2}:{3}:{4}\e[0m"; private readonly ILogger _logger = loggerFactory.CreateLogger(); private readonly Dictionary> _changes = []; @@ -35,7 +35,7 @@ public async Task Execute(string source, string target, bool isDryRun, Canc foreach (var (fromFile, toFile) in fromFiles.Zip(toFiles)) { var changeSet = new ChangeSet(fromFile, toFile); - _logger.LogInformation($"Requested to move from '{fromFile}' to '{toFile}"); + _logger.LogInformation("Requested to move from '{FromFile}' to '{ToFile}'", fromFile, toFile); await SetupChanges(changeSet, ctx); } @@ -49,7 +49,7 @@ private async Task SetupChanges(ChangeSet changeSet, Cancel ctx) var sourceContent = await readFileSystem.File.ReadAllTextAsync(sourcePath, ctx); - var markdownLinkRegex = new Regex(@"\[([^\]]*)\]\(((?:\.{0,2}\/)?[^:)]+\.md(?:#[^)]*)?)\)", RegexOptions.Compiled); + var markdownLinkRegex = MarkdownLinkRegex(); var change = Regex.Replace(sourceContent, markdownLinkRegex.ToString(), match => { @@ -64,13 +64,12 @@ private async Task SetupChanges(ChangeSet changeSet, Cancel ctx) var fullPath = Path.GetFullPath(Path.Combine(sourceDirectory, originalPath)); var relativePath = Path.GetRelativePath(targetDirectory, fullPath); - if (originalPath.StartsWith("./") && !relativePath.StartsWith("./")) - newPath = "./" + relativePath; - else - newPath = relativePath; + newPath = originalPath.StartsWith("./", OrdinalIgnoreCase) && !relativePath.StartsWith("./", OrdinalIgnoreCase) + ? "./" + relativePath + : relativePath; } var newLink = $"[{match.Groups[1].Value}]({newPath})"; - var lineNumber = sourceContent.Substring(0, match.Index).Count(c => c == '\n') + 1; + var lineNumber = sourceContent[..match.Index].Count(c => c == '\n') + 1; var columnNumber = match.Index - sourceContent.LastIndexOf('\n', match.Index); if (!_linkModifications.ContainsKey(changeSet)) _linkModifications[changeSet] = []; @@ -104,14 +103,14 @@ private async Task MoveAndRewriteLinks(bool isDryRun, Cancel ctx) { foreach (var (oldLink, newLink, sourceFile, lineNumber, columnNumber) in linkModifications) { - _logger.LogInformation(string.Format( - ChangeFormatString, + _logger.LogInformation( + "Change \e[31m{OldLink}\e[0m to \e[32m{NewLink}\e[0m at \e[34m{SourceFile}:{LineNumber}:{Column}\e[0m", oldLink, newLink, sourceFile == changeSet.From.FullName && !isDryRun ? changeSet.To.FullName : sourceFile, lineNumber, columnNumber - )); + ); } } @@ -125,13 +124,13 @@ private async Task MoveAndRewriteLinks(bool isDryRun, Cancel ctx) foreach (var (filePath, _, newContent) in changes) { if (!filePath.Directory!.Exists) - writeFileSystem.Directory.CreateDirectory(filePath.Directory.FullName); + _ = writeFileSystem.Directory.CreateDirectory(filePath.Directory.FullName); await writeFileSystem.File.WriteAllTextAsync(filePath.FullName, newContent, ctx); } var targetDirectory = Path.GetDirectoryName(changeSet.To.FullName); - readFileSystem.Directory.CreateDirectory(targetDirectory!); + _ = readFileSystem.Directory.CreateDirectory(targetDirectory!); readFileSystem.File.Move(changeSet.From.FullName, changeSet.To.FullName); } } @@ -172,15 +171,16 @@ private bool ValidateInputs(string source, string target, out IFileInfo[] fromFi //from does not exist at all if (!fromFile.Exists && !fromDirectory.Exists) { - _logger.LogError(!string.IsNullOrEmpty(fromFile.Extension) - ? $"Source file '{fromFile}' does not exist" - : $"Source directory '{fromDirectory}' does not exist"); + if (!string.IsNullOrEmpty(fromFile.Extension)) + _logger.LogError("Source file '{File}' does not exist", fromFile); + else + _logger.LogError("Source directory '{Directory}' does not exist", fromDirectory); return false; } //moving file if (fromFile.Exists) { - if (!fromFile.Extension.Equals(".md", StringComparison.OrdinalIgnoreCase)) + if (!fromFile.Extension.Equals(".md", OrdinalIgnoreCase)) { _logger.LogError("Source path must be a markdown file. Directory paths are not supported yet"); return false; @@ -190,14 +190,14 @@ private bool ValidateInputs(string source, string target, out IFileInfo[] fromFi if (toFile.Extension == string.Empty) toFile = readFileSystem.FileInfo.New(Path.Combine(toDirectory.FullName, fromFile.Name)); - if (!toFile.Extension.Equals(".md", StringComparison.OrdinalIgnoreCase)) + if (!toFile.Extension.Equals(".md", OrdinalIgnoreCase)) { - _logger.LogError($"Target path '{toFile.FullName}' must be a markdown file."); + _logger.LogError("Target path '{FullName}' must be a markdown file.", toFile.FullName); return false; } if (toFile.Exists) { - _logger.LogError($"Target file {target} already exists"); + _logger.LogError("Target file {Target} already exists", target); return false; } fromFiles = [fromFile]; @@ -208,22 +208,22 @@ private bool ValidateInputs(string source, string target, out IFileInfo[] fromFi { if (toDirectory.Exists) { - _logger.LogError($"Target directory '{toDirectory.FullName}' already exists."); + _logger.LogError("Target directory '{FullName}' already exists.", toDirectory.FullName); return false; } - if (toDirectory.FullName.StartsWith(fromDirectory.FullName)) + if (toDirectory.FullName.StartsWith(fromDirectory.FullName, OrdinalIgnoreCase)) { - _logger.LogError($"Can not move source directory '{toDirectory.FullName}' to a {toFile.FullName}"); + _logger.LogError("Can not move source directory '{SourceDirectory}' to a '{TargetFile}'", toDirectory.FullName, toFile.FullName); return false; } fromFiles = fromDirectory.GetFiles("*.md", SearchOption.AllDirectories); - toFiles = fromFiles.Select(f => + toFiles = [.. fromFiles.Select(f => { var relative = Path.GetRelativePath(fromDirectory.FullName, f.FullName); return readFileSystem.FileInfo.New(Path.Combine(toDirectory.FullName, relative)); - }).ToArray(); + })]; } return true; @@ -293,12 +293,12 @@ private string ReplaceLinks( else { var relativeTarget = Path.GetRelativePath(Path.GetDirectoryName(value.FilePath)!, target); - newLink = originalPath.StartsWith("./") && !relativeTarget.StartsWith("./") + newLink = originalPath.StartsWith("./", OrdinalIgnoreCase) && !relativeTarget.StartsWith("./", OrdinalIgnoreCase) ? $"[{match.Groups[1].Value}](./{relativeTarget}{anchor})" : $"[{match.Groups[1].Value}]({relativeTarget}{anchor})"; } - var lineNumber = content.Substring(0, match.Index).Count(c => c == '\n') + 1; + var lineNumber = content[..match.Index].Count(c => c == '\n') + 1; var columnNumber = match.Index - content.LastIndexOf('\n', match.Index); if (!_linkModifications.ContainsKey(changeSet)) _linkModifications[changeSet] = []; @@ -311,4 +311,7 @@ private string ReplaceLinks( )); return newLink; }); + + [GeneratedRegex(@"\[([^\]]*)\]\(((?:\.{0,2}\/)?[^:)]+\.md(?:#[^)]*)?)\)", RegexOptions.Compiled)] + private static partial Regex MarkdownLinkRegex(); } diff --git a/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs b/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs index d7e70716f..8d9fe6a16 100644 --- a/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs +++ b/src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs @@ -51,7 +51,7 @@ public class CrossLinkResolver(ConfigurationFile configuration, ILoggerFactory l private readonly string[] _links = configuration.CrossLinkRepositories; private FrozenDictionary _linkReferences = new Dictionary().ToFrozenDictionary(); private readonly ILogger _logger = logger.CreateLogger(nameof(CrossLinkResolver)); - private readonly HashSet _declaredRepositories = new(); + private readonly HashSet _declaredRepositories = []; public static LinkReference Deserialize(string json) => JsonSerializer.Deserialize(json, SourceGenerationContext.Default.LinkReference)!; @@ -62,11 +62,11 @@ public async Task FetchLinks() var dictionary = new Dictionary(); foreach (var link in _links) { - _declaredRepositories.Add(link); + _ = _declaredRepositories.Add(link); try { var url = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{link}/main/links.json"; - _logger.LogInformation($"Fetching {url}"); + _logger.LogInformation("Fetching {Url}", url); var json = await client.GetStringAsync(url); var linkReference = Deserialize(json); dictionary.Add(link, linkReference); @@ -172,7 +172,7 @@ private static string ToTargetUrlPath(string lookupPath) //https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/cloud-account/change-your-password var path = lookupPath.Replace(".md", ""); if (path.EndsWith("/index")) - path = path.Substring(0, path.Length - 6); + path = path[..^6]; if (path == "index") path = string.Empty; return path; diff --git a/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs b/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs index 00aca67fb..f30eb760c 100644 --- a/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs +++ b/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs @@ -8,13 +8,13 @@ namespace Elastic.Markdown.Diagnostics; -public class DiagnosticsChannel +public sealed class DiagnosticsChannel : IDisposable { private readonly Channel _channel; private readonly CancellationTokenSource _ctxSource; public ChannelReader Reader => _channel.Reader; - public CancellationToken CancellationToken => _ctxSource.Token; + public Cancel CancellationToken => _ctxSource.Token; public DiagnosticsChannel() { @@ -25,11 +25,11 @@ public DiagnosticsChannel() public void TryComplete(Exception? exception = null) { - _channel.Writer.TryComplete(exception); + _ = _channel.Writer.TryComplete(exception); _ctxSource.Cancel(); } - public ValueTask WaitToWrite() => _channel.Writer.WaitToWriteAsync(); + public ValueTask WaitToWrite(Cancel ctx) => _channel.Writer.WaitToWriteAsync(ctx); public void Write(Diagnostic diagnostic) { @@ -39,6 +39,8 @@ public void Write(Diagnostic diagnostic) //TODO } } + + public void Dispose() => _ctxSource.Dispose(); } public enum Severity { Error, Warning } @@ -70,17 +72,17 @@ public class DiagnosticsCollector(IReadOnlyCollection output private Task? _started; - public HashSet OffendingFiles { get; } = new(); + public HashSet OffendingFiles { get; } = []; - public ConcurrentBag CrossLinks { get; } = new(); + public ConcurrentBag CrossLinks { get; } = []; - public Task StartAsync(Cancel ctx) + public Task StartAsync(Cancel cancellationToken) { if (_started is not null) return _started; _started = Task.Run(async () => { - await Channel.WaitToWrite(); + _ = await Channel.WaitToWrite(cancellationToken); while (!Channel.CancellationToken.IsCancellationRequested) { try @@ -95,7 +97,7 @@ public Task StartAsync(Cancel ctx) } Drain(); - }, ctx); + }, cancellationToken); return _started; void Drain() @@ -104,7 +106,7 @@ void Drain() { IncrementSeverityCount(item); HandleItem(item); - OffendingFiles.Add(item.File); + _ = OffendingFiles.Add(item.File); foreach (var output in outputs) output.Write(item); } @@ -114,14 +116,14 @@ void Drain() private void IncrementSeverityCount(Diagnostic item) { if (item.Severity == Severity.Error) - Interlocked.Increment(ref _errors); + _ = Interlocked.Increment(ref _errors); else if (item.Severity == Severity.Warning) - Interlocked.Increment(ref _warnings); + _ = Interlocked.Increment(ref _warnings); } protected virtual void HandleItem(Diagnostic diagnostic) { } - public virtual async Task StopAsync(CancellationToken cancellationToken) + public virtual async Task StopAsync(Cancel cancellationToken) { if (_started is not null) await _started; diff --git a/src/Elastic.Markdown/DocumentationGenerator.cs b/src/Elastic.Markdown/DocumentationGenerator.cs index ad1c56cb8..1ea016b07 100644 --- a/src/Elastic.Markdown/DocumentationGenerator.cs +++ b/src/Elastic.Markdown/DocumentationGenerator.cs @@ -38,9 +38,9 @@ ILoggerFactory logger Resolver = docSet.LinkResolver; HtmlWriter = new HtmlWriter(DocumentationSet, _writeFileSystem); - _logger.LogInformation($"Created documentation set for: {DocumentationSet.Name}"); - _logger.LogInformation($"Source directory: {docSet.SourcePath} Exists: {docSet.SourcePath.Exists}"); - _logger.LogInformation($"Output directory: {docSet.OutputPath} Exists: {docSet.OutputPath.Exists}"); + _logger.LogInformation("Created documentation set for: {DocumentationSetName}", DocumentationSet.Name); + _logger.LogInformation("Source directory: {SourcePath} Exists: {SourcePathExists}", docSet.SourcePath, docSet.SourcePath.Exists); + _logger.LogInformation("Output directory: {OutputPath} Exists: {OutputPathExists}", docSet.OutputPath, docSet.OutputPath.Exists); } public GenerationState? GetPreviousGenerationState() @@ -120,9 +120,9 @@ await Parallel.ForEachAsync(DocumentationSet.Files, ctx, async (file, token) => } if (processedFiles % 100 == 0) - _logger.LogInformation($"-> Processed {processedFiles}/{totalFileCount} files"); + _logger.LogInformation("-> Processed {ProcessedFiles}/{TotalFileCount} files", processedFiles, totalFileCount); }); - _logger.LogInformation($"-> Processed {processedFileCount}/{totalFileCount} files"); + _logger.LogInformation("-> Processed {ProcessedFileCount}/{TotalFileCount} files", processedFileCount, totalFileCount); } private async Task ExtractEmbeddedStaticResources(Cancel ctx) @@ -144,21 +144,21 @@ private async Task ExtractEmbeddedStaticResources(Cancel ctx) outputFile.Directory.Create(); await using var stream = outputFile.OpenWrite(); await resourceStream.CopyToAsync(stream, ctx); - _logger.LogInformation($"Copied static embedded resource {path}"); + _logger.LogInformation("Copied static embedded resource {Path}", path); } } - private async Task ProcessFile(HashSet offendingFiles, DocumentationFile file, DateTimeOffset outputSeenChanges, CancellationToken token) + private async Task ProcessFile(HashSet offendingFiles, DocumentationFile file, DateTimeOffset outputSeenChanges, Cancel token) { if (!Context.Force) { if (offendingFiles.Contains(file.SourceFile.FullName)) - _logger.LogInformation($"Re-evaluating {file.SourceFile.FullName}"); + _logger.LogInformation("Re-evaluating {FileName}", file.SourceFile.FullName); else if (file.SourceFile.LastWriteTimeUtc <= outputSeenChanges) return; } - _logger.LogTrace($"--> {file.SourceFile.FullName}"); + _logger.LogTrace("--> {FileFullPath}", file.SourceFile.FullName); var outputFile = OutputFile(file.RelativePath); if (file is MarkdownFile markdown) await HtmlWriter.WriteAsync(outputFile, markdown, token); @@ -179,33 +179,33 @@ private IFileInfo OutputFile(string relativePath) private bool CompilationNotNeeded(GenerationState? generationState, out HashSet offendingFiles, out DateTimeOffset outputSeenChanges) { - offendingFiles = new HashSet(generationState?.InvalidFiles ?? []); + offendingFiles = [.. generationState?.InvalidFiles ?? []]; outputSeenChanges = generationState?.LastSeenChanges ?? DateTimeOffset.MinValue; if (generationState == null) return false; if (Context.Force) { - _logger.LogInformation($"Full compilation: --force was specified"); + _logger.LogInformation("Full compilation: --force was specified"); return false; } if (Context.Git != generationState.Git) { - _logger.LogInformation($"Full compilation: current git context: {Context.Git} differs from previous git context: {generationState.Git}"); + _logger.LogInformation("Full compilation: current git context: {CurrentGitContext} differs from previous git context: {PreviousGitContext}", Context.Git, generationState.Git); return false; } if (offendingFiles.Count > 0) { - _logger.LogInformation($"Incremental compilation. since: {DocumentationSet.LastWrite}"); - _logger.LogInformation($"Incremental compilation. {offendingFiles.Count} files with errors/warnings"); + _logger.LogInformation("Incremental compilation. since: {LastWrite}", DocumentationSet.LastWrite); + _logger.LogInformation("Incremental compilation. {FileCount} files with errors/warnings", offendingFiles.Count); } else if (DocumentationSet.LastWrite > outputSeenChanges) - _logger.LogInformation($"Incremental compilation. since: {generationState.LastSeenChanges}"); + _logger.LogInformation("Incremental compilation. since: {LastSeenChanges}", generationState.LastSeenChanges); else if (DocumentationSet.LastWrite <= outputSeenChanges) { - _logger.LogInformation($"No compilation: no changes since last observed: {generationState.LastSeenChanges} " - + "Pass --force to force a full regeneration"); + _logger.LogInformation("No compilation: no changes since last observed: {LastSeenChanges}. " + + "Pass --force to force a full regeneration", generationState.LastSeenChanges); return true; } @@ -224,7 +224,7 @@ private async Task GenerateLinkReference(Cancel ctx) private async Task GenerateDocumentationState(Cancel ctx) { var stateFile = DocumentationSet.OutputStateFile; - _logger.LogInformation($"Writing documentation state {DocumentationSet.LastWrite} to {stateFile.FullName}"); + _logger.LogInformation("Writing documentation state {LastWrite} to {StateFileName}", DocumentationSet.LastWrite, stateFile.FullName); var badFiles = Context.Collector.OffendingFiles.ToArray(); var state = new GenerationState { diff --git a/src/Elastic.Markdown/Helpers/Interpolation.cs b/src/Elastic.Markdown/Helpers/Interpolation.cs index e6b42e196..c55eeba22 100644 --- a/src/Elastic.Markdown/Helpers/Interpolation.cs +++ b/src/Elastic.Markdown/Helpers/Interpolation.cs @@ -22,9 +22,8 @@ ParserContext context ) { var span = input.AsSpan(); - if (span.ReplaceSubstitutions([context.Substitutions, context.ContextSubstitutions], out var replacement)) - return replacement; - return input; + return span.ReplaceSubstitutions([context.Substitutions, context.ContextSubstitutions], out var replacement) + ? replacement : input; } @@ -42,13 +41,8 @@ public static bool ReplaceSubstitutions( ) { replacement = null; - if (properties is null || properties.Count == 0) - return false; - - if (span.IndexOf("}}") < 0) - return false; - - return span.ReplaceSubstitutions([properties], out replacement); + return properties is not null && properties.Count != 0 && + span.IndexOf("}}") >= 0 && span.ReplaceSubstitutions([properties], out replacement); } public static bool ReplaceSubstitutions( diff --git a/src/Elastic.Markdown/Helpers/Markdown.cs b/src/Elastic.Markdown/Helpers/Markdown.cs index 9ab56ea8e..c12c8039f 100644 --- a/src/Elastic.Markdown/Helpers/Markdown.cs +++ b/src/Elastic.Markdown/Helpers/Markdown.cs @@ -9,7 +9,7 @@ public static class MarkdownStringExtensions public static string StripMarkdown(this string markdown) { using var writer = new StringWriter(); - Markdig.Markdown.ToPlainText(markdown, writer); + _ = Markdig.Markdown.ToPlainText(markdown, writer); return writer.ToString().TrimEnd('\n'); } } diff --git a/src/Elastic.Markdown/Helpers/SemVersion.cs b/src/Elastic.Markdown/Helpers/SemVersion.cs index e365257e7..bcbd98ac5 100644 --- a/src/Elastic.Markdown/Helpers/SemVersion.cs +++ b/src/Elastic.Markdown/Helpers/SemVersion.cs @@ -11,13 +11,13 @@ namespace Elastic.Markdown.Helpers; /// /// A semver2 compatible version. /// -public class SemVersion : +public partial class SemVersion : IEquatable, IComparable, IComparable { // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string - private static readonly Regex Regex = new(@"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"); + private static readonly Regex Regex = MyRegex(); /// /// The major version part. @@ -122,7 +122,7 @@ public static explicit operator SemVersion(string b) /// /// /// - public static bool operator >(SemVersion left, SemVersion right) => (left.CompareTo(right) > 0); + public static bool operator >(SemVersion left, SemVersion right) => left.CompareTo(right) > 0; /// /// @@ -130,7 +130,7 @@ public static explicit operator SemVersion(string b) /// /// /// - public static bool operator >=(SemVersion left, SemVersion right) => (left == right) || (left > right); + public static bool operator >=(SemVersion left, SemVersion right) => left == right || left > right; /// /// @@ -138,7 +138,7 @@ public static explicit operator SemVersion(string b) /// /// /// - public static bool operator <(SemVersion left, SemVersion right) => (left.CompareTo(right) < 0); + public static bool operator <(SemVersion left, SemVersion right) => left.CompareTo(right) < 0; /// /// @@ -146,7 +146,7 @@ public static explicit operator SemVersion(string b) /// /// /// - public static bool operator <=(SemVersion left, SemVersion right) => (left == right) || (left < right); + public static bool operator <=(SemVersion left, SemVersion right) => left == right || left < right; /// /// Tries to initialize a new instance from the given string. @@ -195,9 +195,9 @@ public SemVersion Update(int? major = null, int? minor = null, int? patch = null /// /// The to compare to. /// 0 if both versions are equal, a positive number, if the other version is lower or a negative number if the other version is higher. - public int CompareByPrecedence(SemVersion? other) + private int CompareByPrecedence(SemVersion? other) { - if (ReferenceEquals(other, null)) + if (other is null) return 1; var result = Major.CompareTo(other.Major); @@ -213,53 +213,28 @@ public int CompareByPrecedence(SemVersion? other) return result; result = CompareComponent(Prerelease, other.Prerelease, true); - if (result != 0) - return result; - - return CompareComponent(Prerelease, other.Metadata, true); + return result != 0 ? result : CompareComponent(Prerelease, other.Metadata, true); } /// - public int CompareTo(SemVersion? other) - { - if (ReferenceEquals(other, null)) - return 1; - - return CompareByPrecedence(other); - } + public int CompareTo(SemVersion? other) => other is null ? 1 : CompareByPrecedence(other); /// public int CompareTo(object? obj) => CompareTo(obj as SemVersion); /// - public bool Equals(SemVersion? other) - { - if (ReferenceEquals(null, other)) - return false; - - if (ReferenceEquals(this, other)) - return true; - - return (Major == other.Major) && (Minor == other.Minor) && (Patch == other.Patch) && - (Prerelease == other.Prerelease) && (Metadata == other.Metadata); - } + public bool Equals(SemVersion? other) => + other is not null && ( + ReferenceEquals(this, other) + || (Major == other.Major && Minor == other.Minor && Patch == other.Patch + && Prerelease == other.Prerelease && Metadata == other.Metadata) + ); /// - public override bool Equals(object? obj) => ReferenceEquals(this, obj) || obj is SemVersion other && Equals(other); + public override bool Equals(object? obj) => ReferenceEquals(this, obj) || (obj is SemVersion other && Equals(other)); /// - public override int GetHashCode() - { - unchecked - { - var hashCode = Major; - hashCode = (hashCode * 397) ^ Minor; - hashCode = (hashCode * 397) ^ Patch; - hashCode = (hashCode * 397) ^ Prerelease.GetHashCode(); - hashCode = (hashCode * 397) ^ Metadata.GetHashCode(); - return hashCode; - } - } + public override int GetHashCode() => HashCode.Combine(Major, Minor, Patch, Prerelease, Metadata); /// public override string ToString() @@ -318,5 +293,8 @@ private static int CompareComponent(string a, string b, bool lower = false) return aComps.Length.CompareTo(bComps.Length); } + + [GeneratedRegex(@"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$")] + private static partial Regex MyRegex(); } diff --git a/src/Elastic.Markdown/Helpers/SlugExtensions.cs b/src/Elastic.Markdown/Helpers/SlugExtensions.cs index dd1c0e20f..85816fc16 100644 --- a/src/Elastic.Markdown/Helpers/SlugExtensions.cs +++ b/src/Elastic.Markdown/Helpers/SlugExtensions.cs @@ -8,9 +8,8 @@ namespace Elastic.Markdown.Helpers; public static class SlugExtensions { - private static readonly SlugHelper _slugHelper = new(); + private static readonly SlugHelper Instance = new(); - - public static string Slugify(this string? text) => _slugHelper.GenerateSlug(text); + public static string Slugify(this string? text) => Instance.GenerateSlug(text); } diff --git a/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs b/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs index 963d560b5..2081ac061 100644 --- a/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs +++ b/src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs @@ -69,12 +69,10 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon Project = ReadString(entry); break; case "exclude": - Exclude = ReadStringArray(entry) - .Select(Glob.Parse) - .ToArray(); + Exclude = [.. ReadStringArray(entry).Select(Glob.Parse)]; break; case "cross_links": - CrossLinkRepositories = ReadStringArray(entry).ToArray(); + CrossLinkRepositories = [.. ReadStringArray(entry)]; break; case "subs": _substitutions = ReadDictionary(entry); @@ -104,7 +102,7 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon EmitError("Could not load docset.yml", e); } - Globs = ImplicitFolders.Select(f => Glob.Parse($"{f}/*.md")).ToArray(); + Globs = [.. ImplicitFolders.Select(f => Glob.Parse($"{f}/*.md"))]; } private List ReadChildren(KeyValuePair entry, string parentPath) @@ -166,7 +164,7 @@ private List ReadChildren(KeyValuePair entry, stri if (toc is not null) { foreach (var f in toc.Files) - Files.Add(f); + _ = Files.Add(f); return [new FolderReference($"{parentPath}".TrimStart('/'), folderFound, toc.TableOfContents)]; } @@ -177,7 +175,7 @@ private List ReadChildren(KeyValuePair entry, stri if (folder is not null) { if (children is null) - ImplicitFolders.Add(parentPath.TrimStart('/')); + _ = ImplicitFolders.Add(parentPath.TrimStart('/')); return [new FolderReference($"{parentPath}".TrimStart('/'), folderFound, children ?? [])]; } @@ -242,7 +240,7 @@ private Dictionary ReadDictionary(KeyValuePair ReadDictionary(KeyValuePair entry) + private static string[] ReadStringArray(KeyValuePair entry) { var values = new List(); if (entry.Value is not YamlSequenceNode sequence) - return values.ToArray(); + return [.. values]; foreach (var entryValue in sequence.Children.OfType()) { @@ -307,7 +305,7 @@ private string[] ReadStringArray(KeyValuePair entry) values.Add(entryValue.Value); } - return values.ToArray(); + return [.. values]; } private void EmitError(string message, YamlNode? node) => diff --git a/src/Elastic.Markdown/IO/Discovery/GitCheckoutInformation.cs b/src/Elastic.Markdown/IO/Discovery/GitCheckoutInformation.cs index 5a17e4ffa..e61a3d495 100644 --- a/src/Elastic.Markdown/IO/Discovery/GitCheckoutInformation.cs +++ b/src/Elastic.Markdown/IO/Discovery/GitCheckoutInformation.cs @@ -96,9 +96,9 @@ public static GitCheckoutInformation Create(IFileSystem fileSystem) string? Read(string path) { var gitPath = Git(path).FullName; - if (!fileSystem.File.Exists(gitPath)) - return null; - return fileSystem.File.ReadAllText(gitPath).Trim(Environment.NewLine.ToCharArray()); + return !fileSystem.File.Exists(gitPath) + ? null + : fileSystem.File.ReadAllText(gitPath).Trim(Environment.NewLine.ToCharArray()); } string BranchTrackingRemote(string b, IniFile c) diff --git a/src/Elastic.Markdown/IO/DocumentationSet.cs b/src/Elastic.Markdown/IO/DocumentationSet.cs index 135d66064..8e9c90c4d 100644 --- a/src/Elastic.Markdown/IO/DocumentationSet.cs +++ b/src/Elastic.Markdown/IO/DocumentationSet.cs @@ -48,7 +48,7 @@ public DocumentationSet(BuildContext context, ILoggerFactory logger, ICrossLinkR OutputStateFile = OutputPath.FileSystem.FileInfo.New(Path.Combine(OutputPath.FullName, ".doc.state")); LinkReferenceFile = OutputPath.FileSystem.FileInfo.New(Path.Combine(OutputPath.FullName, "links.json")); - Files = context.ReadFileSystem.Directory + Files = [.. context.ReadFileSystem.Directory .EnumerateFiles(SourcePath.FullName, "*.*", SearchOption.AllDirectories) .Select(f => context.ReadFileSystem.FileInfo.New(f)) .Select(file => file.Extension switch @@ -60,8 +60,7 @@ public DocumentationSet(BuildContext context, ILoggerFactory logger, ICrossLinkR ".png" => new ImageFile(file, SourcePath), ".md" => CreateMarkDownFile(file, context), _ => new StaticFile(file, SourcePath) - }) - .ToList(); + })]; LastWrite = Files.Max(f => f.SourceFile.LastWriteTimeUtc); @@ -146,11 +145,11 @@ private DocumentationFile CreateMarkDownFile(IFileInfo file, BuildContext contex return new MarkdownFile(file, SourcePath, MarkdownParser, context); // we ignore files in folders that start with an underscore - if (relativePath.IndexOf("_snippets", StringComparison.Ordinal) >= 0) + if (relativePath.Contains("_snippets")) return new SnippetFile(file, SourcePath); // we ignore files in folders that start with an underscore - if (relativePath.IndexOf("/_", StringComparison.Ordinal) > 0 || relativePath.StartsWith("_")) + if (relativePath.IndexOf("/_", StringComparison.Ordinal) > 0 || relativePath.StartsWith('_')) return new ExcludedFile(file, SourcePath); context.EmitError(Configuration.SourceFile, $"Not linked in toc: {relativePath}"); diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index fa7e719e1..12a30cf73 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -92,7 +92,7 @@ public MarkdownFile[] YieldParents() parents.Add(parent.Index); parent = parent?.Parent; } while (parent != null); - return parents.ToArray(); + return [.. parents]; } public string[] YieldParentGroups() { @@ -119,7 +119,7 @@ public async Task MinimalParseAsync(Cancel ctx) public async Task ParseFullAsync(Cancel ctx) { if (!_instructionsParsed) - await MinimalParseAsync(ctx); + _ = await MinimalParseAsync(ctx); var document = await MarkdownParser.ParseAsync(SourceFile, YamlFrontMatter, ctx); return document; @@ -190,7 +190,7 @@ private void ReadDocumentInstructions(MarkdownDocument document) .ToArray(); foreach (var label in anchors) - _anchors.Add(label); + _ = _anchors.Add(label); _instructionsParsed = true; } @@ -232,12 +232,12 @@ private YamlFrontMatter ReadYamlFrontMatter(string raw) } - public string CreateHtml(MarkdownDocument document) + public static string CreateHtml(MarkdownDocument document) { //we manually render title and optionally append an applies block embedded in yaml front matter. var h1 = document.Descendants().FirstOrDefault(h => h.Level == 1); if (h1 is not null) - document.Remove(h1); + _ = document.Remove(h1); return document.ToHtml(MarkdownParser.Pipeline); } } diff --git a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs index bb067ea69..4f74e769b 100644 --- a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs +++ b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information using Elastic.Markdown.Diagnostics; -using Elastic.Markdown.Helpers; using Elastic.Markdown.IO.Configuration; namespace Elastic.Markdown.IO.Navigation; @@ -71,7 +70,7 @@ public DocumentationGroup( NavigationItems = navigationItems; if (Index is not null) - FilesInOrder = FilesInOrder.Except([Index]).ToList(); + FilesInOrder = [.. FilesInOrder.Except([Index])]; OwnFiles = [.. FilesInOrder]; } @@ -142,12 +141,11 @@ public DocumentationGroup( else if (tocItem is FolderReference folder) { var children = folder.Children; - if (children.Count == 0 - && folderLookup.TryGetValue(folder.Path, out var documentationFiles)) + if (children.Count == 0 && folderLookup.TryGetValue(folder.Path, out var documentationFiles)) { - children = documentationFiles + children = [.. documentationFiles .Select(d => new FileReference(d.RelativePath, true, false, [])) - .ToArray(); + ]; } var group = new DocumentationGroup(context, children, lookup, folderLookup, ref fileIndex, depth + 1) diff --git a/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs b/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs index 2e80ef7f7..a865df0f2 100644 --- a/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs +++ b/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs @@ -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.Diagnostics.CodeAnalysis; using Elastic.Markdown.Diagnostics; using Elastic.Markdown.Slices.Directives; using Markdig.Helpers; @@ -16,13 +17,14 @@ public class EnhancedCodeBlockHtmlRenderer : HtmlObjectRenderer(RazorSlice slice, HtmlRenderer renderer, EnhancedCodeBlock block) { var html = slice.RenderAsync().GetAwaiter().GetResult(); var blocks = html.Split("[CONTENT]", 2, StringSplitOptions.RemoveEmptyEntries); - renderer.Write(blocks[0]); + _ = renderer.Write(blocks[0]); RenderCodeBlockLines(renderer, block); - renderer.Write(blocks[1]); + _ = renderer.Write(blocks[1]); } /// @@ -46,27 +48,27 @@ private static void RenderCodeBlockLines(HtmlRenderer renderer, EnhancedCodeBloc if (!hasCode) { - renderer.Write($""); + _ = renderer.Write($""); hasCode = true; } RenderCodeBlockLine(renderer, block, slice, i); } if (hasCode) - renderer.Write($""); + _ = renderer.Write($""); } private static void RenderCodeBlockLine(HtmlRenderer renderer, EnhancedCodeBlock block, StringSlice slice, int lineNumber) { - renderer.WriteEscape(slice); + _ = renderer.WriteEscape(slice); RenderCallouts(renderer, block, lineNumber); - renderer.WriteLine(); + _ = renderer.WriteLine(); } private static void RenderCallouts(HtmlRenderer renderer, EnhancedCodeBlock block, int lineNumber) { var callOuts = FindCallouts(block.CallOuts, lineNumber + 1); foreach (var callOut in callOuts) - renderer.Write($"{callOut.Index}"); + _ = renderer.Write($"{callOut.Index}"); } private static IEnumerable FindCallouts( @@ -148,48 +150,49 @@ protected override void Write(HtmlRenderer renderer, EnhancedCodeBlock block) } else if (siblingBlock is ListBlock listBlock) { - block.Parent.Remove(listBlock); - renderer.WriteLine("
    "); + _ = block.Parent.Remove(listBlock); + _ = renderer.WriteLine("
      "); foreach (var child in listBlock) { var listItem = (ListItemBlock)child; var previousImplicit = renderer.ImplicitParagraph; renderer.ImplicitParagraph = !listBlock.IsLoose; - renderer.EnsureLine(); + _ = renderer.EnsureLine(); if (renderer.EnableHtmlForBlock) { - renderer.Write("'); + _ = renderer.Write("'); } renderer.WriteChildren(listItem); if (renderer.EnableHtmlForBlock) - renderer.WriteLine(""); + _ = renderer.WriteLine(""); - renderer.EnsureLine(); + _ = renderer.EnsureLine(); renderer.ImplicitParagraph = previousImplicit; } - renderer.WriteLine("
    "); + _ = renderer.WriteLine("
"); } } } else if (block.InlineAnnotations) { - renderer.WriteLine("
    "); + _ = renderer.WriteLine("
      "); foreach (var c in block.UniqueCallOuts) { - renderer.WriteLine("
    1. "); - renderer.WriteLine(c.Text); - renderer.WriteLine("
    2. "); + _ = renderer.WriteLine("
    3. "); + _ = renderer.WriteLine(c.Text); + _ = renderer.WriteLine("
    4. "); } - renderer.WriteLine("
    "); + _ = renderer.WriteLine("
"); } } + [SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly")] private static void RenderAppliesToHtml(HtmlRenderer renderer, AppliesToDirective appliesToDirective) { var appliesTo = appliesToDirective.AppliesTo; @@ -197,6 +200,6 @@ private static void RenderAppliesToHtml(HtmlRenderer renderer, AppliesToDirectiv if (appliesTo is null || appliesTo == FrontMatter.ApplicableTo.All) return; var html = slice2.RenderAsync().GetAwaiter().GetResult(); - renderer.Write(html); + _ = renderer.Write(html); } } diff --git a/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockParser.cs b/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockParser.cs index 7b6fe182a..cb69b373d 100644 --- a/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockParser.cs +++ b/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockParser.cs @@ -77,7 +77,7 @@ public override bool Close(BlockProcessor processor, Block block) throw new Exception("Expected parser context to be of type ParserContext"); codeBlock.Language = ( - (codeBlock.Info?.IndexOf("{") ?? -1) != -1 + (codeBlock.Info?.IndexOf('{') ?? -1) != -1 ? codeBlock.Arguments : codeBlock.Info ) ?? "unknown"; @@ -273,7 +273,7 @@ private static List ParseClassicCallOuts(ValueMatch match, ref ReadOnly foreach (var individualStartIndex in allStartIndices) { callOutIndex++; - var endIndex = span.Slice(match.Index + individualStartIndex).IndexOf('>') + 1; + var endIndex = span[(match.Index + individualStartIndex)..].IndexOf('>') + 1; var callout = span.Slice(match.Index + individualStartIndex, endIndex); if (int.TryParse(callout.Trim(['<', '>']), out var index)) { diff --git a/src/Elastic.Markdown/Myst/Comments/CommentBlockParser.cs b/src/Elastic.Markdown/Myst/Comments/CommentBlockParser.cs index 5660cf8c5..5c5701095 100644 --- a/src/Elastic.Markdown/Myst/Comments/CommentBlockParser.cs +++ b/src/Elastic.Markdown/Myst/Comments/CommentBlockParser.cs @@ -79,7 +79,7 @@ public override BlockState TryOpen(BlockProcessor processor) if (leadingCount > 0 && leadingCount <= MaxLeadingCount && (c.IsSpaceOrTab() || c == '\0')) { if (processor.TrackTrivia && c.IsSpaceOrTab()) - processor.NextChar(); + _ = processor.NextChar(); // Move to the content var headingBlock = new CommentBlock(this) diff --git a/src/Elastic.Markdown/Myst/Comments/CommentMarkdownExtension.cs b/src/Elastic.Markdown/Myst/Comments/CommentMarkdownExtension.cs index 6d27ffcae..61eb14cf3 100644 --- a/src/Elastic.Markdown/Myst/Comments/CommentMarkdownExtension.cs +++ b/src/Elastic.Markdown/Myst/Comments/CommentMarkdownExtension.cs @@ -21,12 +21,12 @@ public class CommentMarkdownExtension : IMarkdownExtension public void Setup(MarkdownPipelineBuilder pipeline) { if (!pipeline.BlockParsers.Contains()) - pipeline.BlockParsers.InsertBefore(new CommentBlockParser()); + _ = pipeline.BlockParsers.InsertBefore(new CommentBlockParser()); } public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) { if (!renderer.ObjectRenderers.Contains()) - renderer.ObjectRenderers.InsertBefore(new CommentRenderer()); + _ = renderer.ObjectRenderers.InsertBefore(new CommentRenderer()); } } diff --git a/src/Elastic.Markdown/Myst/Directives/AdmonitionBlock.cs b/src/Elastic.Markdown/Myst/Directives/AdmonitionBlock.cs index 89c61ab85..25e29ef67 100644 --- a/src/Elastic.Markdown/Myst/Directives/AdmonitionBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/AdmonitionBlock.cs @@ -10,25 +10,23 @@ public class DropdownBlock(DirectiveBlockParser parser, ParserContext context) : public class AdmonitionBlock : DirectiveBlock { - private readonly string _admonition; - public AdmonitionBlock(DirectiveBlockParser parser, string admonition, ParserContext context) : base(parser, context) { - _admonition = admonition; - if (_admonition is "admonition") + Admonition = admonition; + if (Admonition is "admonition") Classes = "plain"; var t = Admonition; var title = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(t); Title = title; - } - public string Admonition => _admonition; + public string Admonition { get; } public override string Directive => Admonition; public string? Classes { get; protected set; } + public bool? DropdownOpen { get; private set; } public string Title { get; private set; } @@ -40,7 +38,7 @@ public override void FinalizeAndValidate(ParserContext context) if (DropdownOpen.HasValue) Classes = "dropdown"; - if (_admonition is "admonition" or "dropdown" && !string.IsNullOrEmpty(Arguments)) + if (Admonition is "admonition" or "dropdown" && !string.IsNullOrEmpty(Arguments)) Title = Arguments; else if (!string.IsNullOrEmpty(Arguments)) Title += $" {Arguments}"; diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveBlock.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveBlock.cs index 8bbcfc0a2..ca142dcc8 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveBlock.cs @@ -1,6 +1,7 @@ // Licensed to Elasticsearch B.V under one or more agreements. // 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 + // Copyright (c) Alexandre Mutel. All rights reserved. // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. @@ -96,7 +97,7 @@ public abstract class DirectiveBlock( internal void AddProperty(string key, string value) { - _properties ??= new Dictionary(); + _properties ??= []; _properties[key] = value; } diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs index 0682980b6..3866931cf 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cs @@ -1,9 +1,6 @@ // Licensed to Elasticsearch B.V under one or more agreements. // 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 -// Copyright (c) Alexandre Mutel. All rights reserved. -// This file is licensed under the BSD-Clause 2 license. -// See the license.txt file in the project root for more information. using System.Collections.Frozen; using Markdig.Parsers; diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs index 982f2846c..420fff70f 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs @@ -1,10 +1,8 @@ // Licensed to Elasticsearch B.V under one or more agreements. // 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 -// Copyright (c) Alexandre Mutel. All rights reserved. -// This file is licensed under the BSD-Clause 2 license. -// See the license.txt file in the project root for more information. +using System.Diagnostics.CodeAnalysis; using Elastic.Markdown.Diagnostics; using Elastic.Markdown.Myst.Settings; using Elastic.Markdown.Myst.Substitution; @@ -27,7 +25,7 @@ public class DirectiveHtmlRenderer : HtmlObjectRenderer { protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlock) { - renderer.EnsureLine(); + _ = renderer.EnsureLine(); switch (directiveBlock) { @@ -80,7 +78,7 @@ protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlo } } - private void WriteImage(HtmlRenderer renderer, ImageBlock block) + private static void WriteImage(HtmlRenderer renderer, ImageBlock block) { var imageUrl = block.ImageUrl; if (!string.IsNullOrEmpty(block.ImageUrl)) @@ -114,7 +112,7 @@ private void WriteImage(HtmlRenderer renderer, ImageBlock block) RenderRazorSlice(slice, renderer, block); } - private void WriteFigure(HtmlRenderer renderer, ImageBlock block) + private static void WriteFigure(HtmlRenderer renderer, ImageBlock block) { var imageUrl = block.ImageUrl != null && (block.ImageUrl.StartsWith("/_static") || block.ImageUrl.StartsWith("_static")) @@ -134,10 +132,10 @@ private void WriteFigure(HtmlRenderer renderer, ImageBlock block) RenderRazorSlice(slice, renderer, block); } - private void WriteChildren(HtmlRenderer renderer, DirectiveBlock directiveBlock) => + private static void WriteChildren(HtmlRenderer renderer, DirectiveBlock directiveBlock) => renderer.WriteChildren(directiveBlock); - private void WriteVersion(HtmlRenderer renderer, VersionBlock block) + private static void WriteVersion(HtmlRenderer renderer, VersionBlock block) { var slice = Slices.Directives.Version.Create(new VersionViewModel { @@ -148,7 +146,7 @@ private void WriteVersion(HtmlRenderer renderer, VersionBlock block) RenderRazorSlice(slice, renderer, block); } - private void WriteAdmonition(HtmlRenderer renderer, AdmonitionBlock block) + private static void WriteAdmonition(HtmlRenderer renderer, AdmonitionBlock block) { var slice = Admonition.Create(new AdmonitionViewModel { @@ -161,7 +159,7 @@ private void WriteAdmonition(HtmlRenderer renderer, AdmonitionBlock block) RenderRazorSlice(slice, renderer, block); } - private void WriteDropdown(HtmlRenderer renderer, DropdownBlock block) + private static void WriteDropdown(HtmlRenderer renderer, DropdownBlock block) { var slice = Dropdown.Create(new AdmonitionViewModel { @@ -174,13 +172,13 @@ private void WriteDropdown(HtmlRenderer renderer, DropdownBlock block) RenderRazorSlice(slice, renderer, block); } - private void WriteTabSet(HtmlRenderer renderer, TabSetBlock block) + private static void WriteTabSet(HtmlRenderer renderer, TabSetBlock block) { var slice = TabSet.Create(new TabSetViewModel()); RenderRazorSlice(slice, renderer, block); } - private void WriteTabItem(HtmlRenderer renderer, TabItemBlock block) + private static void WriteTabItem(HtmlRenderer renderer, TabItemBlock block) { var slice = TabItem.Create(new TabItemViewModel { @@ -193,13 +191,13 @@ private void WriteTabItem(HtmlRenderer renderer, TabItemBlock block) RenderRazorSlice(slice, renderer, block); } - private void WriteMermaid(HtmlRenderer renderer, MermaidBlock block) + private static void WriteMermaid(HtmlRenderer renderer, MermaidBlock block) { var slice = Mermaid.Create(new MermaidViewModel()); RenderRazorSliceRawContent(slice, renderer, block); } - private void WriteLiteralIncludeBlock(HtmlRenderer renderer, IncludeBlock block) + private static void WriteLiteralIncludeBlock(HtmlRenderer renderer, IncludeBlock block) { if (!block.Found || block.IncludePath is null) return; @@ -207,7 +205,7 @@ private void WriteLiteralIncludeBlock(HtmlRenderer renderer, IncludeBlock block) var file = block.FileSystem.FileInfo.New(block.IncludePath); var content = block.FileSystem.File.ReadAllText(file.FullName); if (string.IsNullOrEmpty(block.Language)) - renderer.Write(content); + _ = renderer.Write(content); else { var slice = Code.Create(new CodeViewModel @@ -221,7 +219,7 @@ private void WriteLiteralIncludeBlock(HtmlRenderer renderer, IncludeBlock block) } } - private void WriteIncludeBlock(HtmlRenderer renderer, IncludeBlock block) + private static void WriteIncludeBlock(HtmlRenderer renderer, IncludeBlock block) { if (!block.Found || block.IncludePath is null) return; @@ -232,12 +230,12 @@ private void WriteIncludeBlock(HtmlRenderer renderer, IncludeBlock block) var file = block.FileSystem.FileInfo.New(block.IncludePath); var document = parser.ParseAsync(file, block.FrontMatter, default).GetAwaiter().GetResult(); var html = document.ToHtml(MarkdownParser.Pipeline); - renderer.Write(html); + _ = renderer.Write(html); //var slice = Include.Create(new IncludeViewModel { Html = html }); //RenderRazorSlice(slice, renderer, block); } - private void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock block) + private static void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock block) { if (!block.Found || block.IncludePath is null) return; @@ -249,11 +247,11 @@ private void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock block) var file = block.FileSystem.FileInfo.New(block.IncludePath); - SettingsCollection? settings; + YamlSettings? settings; try { var yaml = file.FileSystem.File.ReadAllText(file.FullName); - settings = YamlSerialization.Deserialize(yaml); + settings = YamlSerialization.Deserialize(yaml); } catch (YamlException e) { @@ -279,60 +277,65 @@ private void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock block) RenderRazorSliceNoContent(slice, renderer); } + [SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly")] private static void RenderRazorSlice(RazorSlice slice, HtmlRenderer renderer, string contents) { var html = slice.RenderAsync().GetAwaiter().GetResult(); var blocks = html.Split("[CONTENT]", 2, StringSplitOptions.RemoveEmptyEntries); - renderer.Write(blocks[0]); - renderer.Write(contents); - renderer.Write(blocks[1]); + _ = renderer + .Write(blocks[0]) + .Write(contents) + .Write(blocks[1]); } + [SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly")] private static void RenderRazorSlice(RazorSlice slice, HtmlRenderer renderer, DirectiveBlock obj) { var html = slice.RenderAsync().GetAwaiter().GetResult(); var blocks = html.Split("[CONTENT]", 2, StringSplitOptions.RemoveEmptyEntries); - renderer.Write(blocks[0]); + _ = renderer.Write(blocks[0]); renderer.WriteChildren(obj); - renderer.Write(blocks[1]); + _ = renderer.Write(blocks[1]); } + [SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly")] private static void RenderRazorSliceNoContent(RazorSlice slice, HtmlRenderer renderer) { var html = slice.RenderAsync().GetAwaiter().GetResult(); - renderer.Write(html); + _ = renderer.Write(html); } + [SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly")] private static void RenderRazorSliceRawContent(RazorSlice slice, HtmlRenderer renderer, DirectiveBlock obj) { var html = slice.RenderAsync().GetAwaiter().GetResult(); var blocks = html.Split("[CONTENT]", 2, StringSplitOptions.RemoveEmptyEntries); - renderer.Write(blocks[0]); + _ = renderer.Write(blocks[0]); foreach (var o in obj) Render(o); - renderer.Write(blocks[1]); + _ = renderer.Write(blocks[1]); void RenderLeaf(LeafBlock p) { - renderer.WriteLeafRawLines(p, true, false, false); + _ = renderer.WriteLeafRawLines(p, true, false, false); renderer.EnableHtmlForInline = false; foreach (var oo in p.Inline ?? []) { if (oo is SubstitutionLeaf sl) - renderer.Write(sl.Replacement); + _ = renderer.Write(sl.Replacement); else if (oo is LiteralInline li) renderer.Write(li); else if (oo is LineBreakInline) - renderer.WriteLine(); + _ = renderer.WriteLine(); else if (oo is Role r) { - renderer.Write(new string(r.DelimiterChar, r.DelimiterCount)); + _ = renderer.Write(new string(r.DelimiterChar, r.DelimiterCount)); renderer.WriteChildren(r); } else - renderer.Write($"(LeafBlock: {oo.GetType().Name}"); + _ = renderer.Write($"(LeafBlock: {oo.GetType().Name}"); } renderer.EnableHtmlForInline = true; @@ -346,13 +349,13 @@ void RenderListBlock(ListBlock l) RenderLeaf(lbi); else if (bb is ListItemBlock ll) { - renderer.Write(ll.TriviaBefore); - renderer.Write("-"); + _ = renderer.Write(ll.TriviaBefore); + _ = renderer.Write("-"); foreach (var lll in ll) Render(lll); } else - renderer.Write($"(ListBlock: {l.GetType().Name}"); + _ = renderer.Write($"(ListBlock: {l.GetType().Name}"); } } @@ -363,7 +366,7 @@ void Render(Block o) else if (o is ListBlock l) RenderListBlock(l); else - renderer.Write($"(Block: {o.GetType().Name}"); + _ = renderer.Write($"(Block: {o.GetType().Name}"); } } } diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs index a5c231006..ffbd4473a 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveMarkdownExtension.cs @@ -1,9 +1,6 @@ // Licensed to Elasticsearch B.V under one or more agreements. // 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 -// Copyright (c) Alexandre Mutel. All rights reserved. -// This file is licensed under the BSD-Clause 2 license. -// See the license.txt file in the project root for more information. using Markdig; using Markdig.Parsers; @@ -33,9 +30,9 @@ public void Setup(MarkdownPipelineBuilder pipeline) if (!pipeline.BlockParsers.Contains()) { // Insert the parser before any other parsers - pipeline.BlockParsers.InsertBefore(new DirectiveBlockParser()); + _ = pipeline.BlockParsers.InsertBefore(new DirectiveBlockParser()); } - pipeline.BlockParsers.Replace(new DirectiveParagraphParser()); + _ = pipeline.BlockParsers.Replace(new DirectiveParagraphParser()); // Plug the inline parser for CustomContainerInline var inlineParser = pipeline.InlineParsers.Find(); @@ -43,12 +40,10 @@ public void Setup(MarkdownPipelineBuilder pipeline) { inlineParser.EmphasisDescriptors.Add(new EmphasisDescriptor(':', 2, 2, true)); inlineParser.TryCreateEmphasisInlineList.Add((emphasisChar, delimiterCount) => - { - if (delimiterCount == 2 && emphasisChar == ':') - return new Role { DelimiterChar = ':', DelimiterCount = 2 }; - - return null; - }); + delimiterCount != 2 || emphasisChar != ':' + ? null + : (Markdig.Syntax.Inlines.EmphasisInline)new Role { DelimiterChar = ':', DelimiterCount = 2 } + ); } } @@ -57,9 +52,9 @@ public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) if (!renderer.ObjectRenderers.Contains()) { // Must be inserted before CodeBlockRenderer - renderer.ObjectRenderers.InsertBefore(new DirectiveHtmlRenderer()); + _ = renderer.ObjectRenderers.InsertBefore(new DirectiveHtmlRenderer()); } - renderer.ObjectRenderers.Replace(new SectionedHeadingRenderer()); + _ = renderer.ObjectRenderers.Replace(new SectionedHeadingRenderer()); } } diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveParagraphParser.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveParagraphParser.cs index ae387c574..20af269cc 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveParagraphParser.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveParagraphParser.cs @@ -32,9 +32,8 @@ public override BlockState TryContinue(BlockProcessor processor, Block block) return base.TryContinue(processor, block); // TODO only parse this if no content proceeds it (and not in a code fence) - if (line.StartsWith(":")) - return BlockState.BreakDiscard; - - return base.TryContinue(processor, block); + return line.StartsWith(':') + ? BlockState.BreakDiscard + : base.TryContinue(processor, block); } } diff --git a/src/Elastic.Markdown/Myst/Directives/Role.cs b/src/Elastic.Markdown/Myst/Directives/Role.cs index 1c7ff9226..d56b561da 100644 --- a/src/Elastic.Markdown/Myst/Directives/Role.cs +++ b/src/Elastic.Markdown/Myst/Directives/Role.cs @@ -1,9 +1,6 @@ // Licensed to Elasticsearch B.V under one or more agreements. // 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 -// Copyright (c) Alexandre Mutel. All rights reserved. -// This file is licensed under the BSD-Clause 2 license. -// See the license.txt file in the project root for more information. using Markdig.Syntax.Inlines; @@ -16,6 +13,4 @@ namespace Elastic.Markdown.Myst.Directives; ///
/// /// -public class Role : EmphasisInline -{ -} +public class Role : EmphasisInline; diff --git a/src/Elastic.Markdown/Myst/FrontMatter/Applicability.cs b/src/Elastic.Markdown/Myst/FrontMatter/Applicability.cs index 1b6a7972d..05b520098 100644 --- a/src/Elastic.Markdown/Myst/FrontMatter/Applicability.cs +++ b/src/Elastic.Markdown/Myst/FrontMatter/Applicability.cs @@ -11,13 +11,13 @@ namespace Elastic.Markdown.Myst.FrontMatter; [YamlSerializable] -public record ApplicabilityOverTime : IReadOnlyCollection +public record AppliesCollection : IReadOnlyCollection { - private readonly IReadOnlyCollection _items; - public ApplicabilityOverTime(Applicability[] items) => _items = items; + private readonly Applicability[] _items; + public AppliesCollection(Applicability[] items) => _items = items; // [version] - public static bool TryParse(string? value, out ApplicabilityOverTime? availability) + public static bool TryParse(string? value, out AppliesCollection? availability) { availability = null; if (string.IsNullOrWhiteSpace(value) || string.Equals(value.Trim(), "all", StringComparison.InvariantCultureIgnoreCase)) @@ -37,11 +37,11 @@ public static bool TryParse(string? value, out ApplicabilityOverTime? availabili if (applications.Count == 0) return false; - availability = new ApplicabilityOverTime(applications.ToArray()); + availability = new AppliesCollection([.. applications]); return true; } - public virtual bool Equals(ApplicabilityOverTime? other) + public virtual bool Equals(AppliesCollection? other) { if ((object)this == other) return true; @@ -57,18 +57,18 @@ public override int GetHashCode() { var comparer = StructuralComparisons.StructuralEqualityComparer; return - EqualityComparer.Default.GetHashCode(EqualityContract) * -1521134295 + (EqualityComparer.Default.GetHashCode(EqualityContract) * -1521134295) + comparer.GetHashCode(_items); } - public static explicit operator ApplicabilityOverTime(string b) + public static explicit operator AppliesCollection(string b) { var productAvailability = TryParse(b, out var version) ? version : null; return productAvailability ?? throw new ArgumentException($"'{b}' is not a valid applicability string array."); } - public static ApplicabilityOverTime GenerallyAvailable { get; } + public static AppliesCollection GenerallyAvailable { get; } = new([Applicability.GenerallyAvailable]); public override string ToString() @@ -77,15 +77,15 @@ public override string ToString() return "all"; var sb = new StringBuilder(); foreach (var item in _items) - sb.Append(item).Append(", "); + _ = sb.Append(item).Append(", "); return sb.ToString(); } - public IEnumerator GetEnumerator() => _items.GetEnumerator(); + public IEnumerator GetEnumerator() => ((IEnumerable)_items).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public int Count => _items.Count; + public int Count => _items.Length; } [YamlSerializable] @@ -117,9 +117,9 @@ public override string ToString() ProductLifecycle.GenerallyAvailable => "ga", _ => throw new ArgumentOutOfRangeException() }; - sb.Append(lifecycle); + _ = sb.Append(lifecycle); if (Version is not null && Version != AllVersions.Instance) - sb.Append(" ").Append(Version); + _ = sb.Append(' ').Append(Version); return sb.ToString(); } @@ -156,7 +156,7 @@ public static bool TryParse(string? value, [NotNullWhen(true)] out Applicability "discontinued" => ProductLifecycle.Discontinued, "unavailable" => ProductLifecycle.Unavailable, "ga" => ProductLifecycle.GenerallyAvailable, - _ => throw new ArgumentOutOfRangeException(nameof(tokens), tokens, $"Unknown product lifecycle: {tokens[0]}") + _ => throw new Exception($"Unknown product lifecycle: {tokens[0]}") }; var version = tokens.Length < 2 diff --git a/src/Elastic.Markdown/Myst/FrontMatter/ApplicableTo.cs b/src/Elastic.Markdown/Myst/FrontMatter/ApplicableTo.cs index ef618ccad..cfe57d0fa 100644 --- a/src/Elastic.Markdown/Myst/FrontMatter/ApplicableTo.cs +++ b/src/Elastic.Markdown/Myst/FrontMatter/ApplicableTo.cs @@ -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.Collections; using System.Diagnostics.CodeAnalysis; using YamlDotNet.Core; using YamlDotNet.Core.Events; @@ -9,11 +10,29 @@ namespace Elastic.Markdown.Myst.FrontMatter; +public class WarningCollection : IEquatable, IReadOnlyCollection +{ + private readonly List _list = []; + + public WarningCollection(IEnumerable warnings) => _list.AddRange(warnings); + + public bool Equals(WarningCollection? other) => other != null && _list.SequenceEqual(other._list); + + public IEnumerator GetEnumerator() => _list.GetEnumerator(); + + public override bool Equals(object? obj) => Equals(obj as WarningCollection); + + public override int GetHashCode() => _list.GetHashCode(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => _list.Count; +} + [YamlSerializable] public record ApplicableTo { [YamlMember(Alias = "stack")] - public ApplicabilityOverTime? Stack { get; set; } + public AppliesCollection? Stack { get; set; } [YamlMember(Alias = "deployment")] public DeploymentApplicability? Deployment { get; set; } @@ -22,16 +41,16 @@ public record ApplicableTo public ServerlessProjectApplicability? Serverless { get; set; } [YamlMember(Alias = "product")] - public ApplicabilityOverTime? Product { get; set; } + public AppliesCollection? Product { get; set; } - public string[]? Warnings { get; set; } + internal WarningCollection? Warnings { get; set; } public static ApplicableTo All { get; } = new() { - Stack = ApplicabilityOverTime.GenerallyAvailable, + Stack = AppliesCollection.GenerallyAvailable, Serverless = ServerlessProjectApplicability.All, Deployment = DeploymentApplicability.All, - Product = ApplicabilityOverTime.GenerallyAvailable + Product = AppliesCollection.GenerallyAvailable }; } @@ -39,23 +58,23 @@ public record ApplicableTo public record DeploymentApplicability { [YamlMember(Alias = "self")] - public ApplicabilityOverTime? Self { get; set; } + public AppliesCollection? Self { get; set; } [YamlMember(Alias = "ece")] - public ApplicabilityOverTime? Ece { get; set; } + public AppliesCollection? Ece { get; set; } [YamlMember(Alias = "eck")] - public ApplicabilityOverTime? Eck { get; set; } + public AppliesCollection? Eck { get; set; } [YamlMember(Alias = "ess")] - public ApplicabilityOverTime? Ess { get; set; } + public AppliesCollection? Ess { get; set; } public static DeploymentApplicability All { get; } = new() { - Ece = ApplicabilityOverTime.GenerallyAvailable, - Eck = ApplicabilityOverTime.GenerallyAvailable, - Ess = ApplicabilityOverTime.GenerallyAvailable, - Self = ApplicabilityOverTime.GenerallyAvailable + Ece = AppliesCollection.GenerallyAvailable, + Eck = AppliesCollection.GenerallyAvailable, + Ess = AppliesCollection.GenerallyAvailable, + Self = AppliesCollection.GenerallyAvailable }; } @@ -63,27 +82,27 @@ public record DeploymentApplicability public record ServerlessProjectApplicability { [YamlMember(Alias = "elasticsearch")] - public ApplicabilityOverTime? Elasticsearch { get; set; } + public AppliesCollection? Elasticsearch { get; set; } [YamlMember(Alias = "observability")] - public ApplicabilityOverTime? Observability { get; set; } + public AppliesCollection? Observability { get; set; } [YamlMember(Alias = "security")] - public ApplicabilityOverTime? Security { get; set; } + public AppliesCollection? Security { get; set; } /// /// Returns if all projects share the same applicability /// - public ApplicabilityOverTime? AllProjects => + public AppliesCollection? AllProjects => Elasticsearch == Observability && Observability == Security ? Elasticsearch : null; public static ServerlessProjectApplicability All { get; } = new() { - Elasticsearch = ApplicabilityOverTime.GenerallyAvailable, - Observability = ApplicabilityOverTime.GenerallyAvailable, - Security = ApplicabilityOverTime.GenerallyAvailable + Elasticsearch = AppliesCollection.GenerallyAvailable, + Observability = AppliesCollection.GenerallyAvailable, + Security = AppliesCollection.GenerallyAvailable }; } @@ -115,7 +134,7 @@ public class ApplicableToConverter : IYamlTypeConverter var warnings = new List(); var keys = dictionary.Keys.OfType().ToArray(); - var oldStyleKeys = keys.Where(k => k.StartsWith(":")).ToList(); + var oldStyleKeys = keys.Where(k => k.StartsWith(':')).ToList(); if (oldStyleKeys.Count > 0) warnings.Add($"Applies block does not use valid yaml keys: {string.Join(", ", oldStyleKeys)}"); var unknownKeys = keys.Except(KnownKeys).Except(oldStyleKeys).ToList(); @@ -138,7 +157,7 @@ public class ApplicableToConverter : IYamlTypeConverter applicableTo.Serverless = serverless; if (warnings.Count > 0) - applicableTo.Warnings = warnings.ToArray(); + applicableTo.Warnings = new WarningCollection(warnings); return applicableTo; } @@ -147,11 +166,11 @@ private static void AssignDeploymentType(Dictionary dictionary, if (!dictionary.TryGetValue("deployment", out var deploymentType)) return; - if (deploymentType is null || deploymentType is string s && string.IsNullOrWhiteSpace(s)) + if (deploymentType is null || (deploymentType is string s && string.IsNullOrWhiteSpace(s))) applicableTo.Deployment = DeploymentApplicability.All; else if (deploymentType is string deploymentTypeString) { - var av = ApplicabilityOverTime.TryParse(deploymentTypeString, out var a) ? a : null; + var av = AppliesCollection.TryParse(deploymentTypeString, out var a) ? a : null; applicableTo.Deployment = new DeploymentApplicability { Ece = av, @@ -209,11 +228,11 @@ private static void AssignServerless(Dictionary dictionary, App if (!dictionary.TryGetValue("serverless", out var serverless)) return; - if (serverless is null || serverless is string s && string.IsNullOrWhiteSpace(s)) + if (serverless is null || (serverless is string s && string.IsNullOrWhiteSpace(s))) applicableTo.Serverless = ServerlessProjectApplicability.All; else if (serverless is string serverlessString) { - var av = ApplicabilityOverTime.TryParse(serverlessString, out var a) ? a : null; + var av = AppliesCollection.TryParse(serverlessString, out var a) ? a : null; applicableTo.Serverless = new ServerlessProjectApplicability { Elasticsearch = av, @@ -259,16 +278,16 @@ private static bool TryGetProjectApplicability( return true; } - private static bool TryGetApplicabilityOverTime(Dictionary dictionary, string key, out ApplicabilityOverTime? availability) + private static bool TryGetApplicabilityOverTime(Dictionary dictionary, string key, out AppliesCollection? availability) { availability = null; if (!dictionary.TryGetValue(key, out var target)) return false; - if (target is null || target is string s && string.IsNullOrWhiteSpace(s)) - availability = ApplicabilityOverTime.GenerallyAvailable; + if (target is null || (target is string s && string.IsNullOrWhiteSpace(s))) + availability = AppliesCollection.GenerallyAvailable; else if (target is string stackString) - availability = ApplicabilityOverTime.TryParse(stackString, out var a) ? a : null; + availability = AppliesCollection.TryParse(stackString, out var a) ? a : null; return availability is not null; } diff --git a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs index efb3ec12d..39bb47097 100644 --- a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs +++ b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs @@ -36,7 +36,7 @@ public void Setup(MarkdownPipelineBuilder pipeline) => public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) { } } -internal partial class LinkRegexExtensions +internal sealed partial class LinkRegexExtensions { [GeneratedRegex(@"\s\=(?\d+%?)(?:x(?\d+%?))?$", RegexOptions.IgnoreCase, "en-US")] public static partial Regex MatchTitleStylingInstructions(); @@ -66,7 +66,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) } - private void ParseStylingInstructions(LinkInline link) + private static void ParseStylingInstructions(LinkInline link) { if (string.IsNullOrWhiteSpace(link.Title) || link.Title.IndexOf('=') < 0) return; @@ -76,12 +76,12 @@ private void ParseStylingInstructions(LinkInline link) return; var width = matches.Groups["width"].Value; - if (!width.EndsWith("%")) + if (!width.EndsWith('%')) width += "px"; var height = matches.Groups["height"].Value; if (string.IsNullOrEmpty(height)) height = width; - else if (!height.EndsWith("%")) + else if (!height.EndsWith('%')) height += "px"; var title = link.Title[..matches.Index]; @@ -94,7 +94,7 @@ private void ParseStylingInstructions(LinkInline link) private static bool IsInCommentBlock(LinkInline link) => link.Parent?.ParentBlock is CommentBlock; - private void ValidateAndProcessLink(LinkInline link, InlineProcessor processor, ParserContext context) + private static void ValidateAndProcessLink(LinkInline link, InlineProcessor processor, ParserContext context) { var url = link.Url; @@ -115,7 +115,7 @@ private void ValidateAndProcessLink(LinkInline link, InlineProcessor processor, ProcessInternalLink(link, processor, context); } - private bool ValidateBasicUrl(LinkInline link, InlineProcessor processor, string? url) + private static bool ValidateBasicUrl(LinkInline link, InlineProcessor processor, string? url) { if (string.IsNullOrEmpty(url)) { @@ -134,7 +134,7 @@ private bool ValidateBasicUrl(LinkInline link, InlineProcessor processor, string return true; } - private bool ValidateExternalUri(LinkInline link, InlineProcessor processor, Uri? uri) + private static bool ValidateExternalUri(LinkInline link, InlineProcessor processor, Uri? uri) { if (uri == null) return false; @@ -219,7 +219,7 @@ private static void ProcessLinkText(InlineProcessor processor, LinkInline link, } if (link.FirstChild == null && !string.IsNullOrEmpty(title)) - link.AppendChild(new LiteralInline(title)); + _ = link.AppendChild(new LiteralInline(title)); } private static IFileInfo ResolveFile(ParserContext context, string url) => diff --git a/src/Elastic.Markdown/Myst/InlineParsers/HeadingBlockWithSlugParser.cs b/src/Elastic.Markdown/Myst/InlineParsers/HeadingBlockWithSlugParser.cs index 9dfb43f82..1641b33df 100644 --- a/src/Elastic.Markdown/Myst/InlineParsers/HeadingBlockWithSlugParser.cs +++ b/src/Elastic.Markdown/Myst/InlineParsers/HeadingBlockWithSlugParser.cs @@ -45,7 +45,7 @@ public override bool Close(BlockProcessor processor, Block block) foreach (var match in splits) { - var header = text.Slice(0, match.Index); + var header = text[..match.Index]; var anchor = text.Slice(match.Index, match.Length); var newSlice = new StringSlice(header.ToString()); diff --git a/src/Elastic.Markdown/Myst/MarkdownParser.cs b/src/Elastic.Markdown/Myst/MarkdownParser.cs index 302f5e776..0882bcc73 100644 --- a/src/Elastic.Markdown/Myst/MarkdownParser.cs +++ b/src/Elastic.Markdown/Myst/MarkdownParser.cs @@ -35,34 +35,35 @@ ICrossLinkResolver linksResolver private ICrossLinkResolver LinksResolver { get; } = linksResolver; // ReSharper disable once InconsistentNaming - private static MarkdownPipeline? _minimalPipeline; - public static MarkdownPipeline MinimalPipeline + private static MarkdownPipeline? MinimalPipelineCached; + + private static MarkdownPipeline MinimalPipeline { get { - if (_minimalPipeline is not null) - return _minimalPipeline; + if (MinimalPipelineCached is not null) + return MinimalPipelineCached; var builder = new MarkdownPipelineBuilder() .UseYamlFrontMatter() .UseInlineAnchors() .UseHeadingsWithSlugs() .UseDirectives(); - builder.BlockParsers.TryRemove(); - _minimalPipeline = builder.Build(); - return _minimalPipeline; + _ = builder.BlockParsers.TryRemove(); + MinimalPipelineCached = builder.Build(); + return MinimalPipelineCached; } } // ReSharper disable once InconsistentNaming - private static MarkdownPipeline? _pipeline; + private static MarkdownPipeline? PipelineCached; public static MarkdownPipeline Pipeline { get { - if (_pipeline is not null) - return _pipeline; + if (PipelineCached is not null) + return PipelineCached; var builder = new MarkdownPipelineBuilder() .UseInlineAnchors() @@ -81,9 +82,9 @@ public static MarkdownPipeline Pipeline .UseEnhancedCodeBlocks() .DisableHtml() .UseHardBreaks(); - builder.BlockParsers.TryRemove(); - _pipeline = builder.Build(); - return _pipeline; + _ = builder.BlockParsers.TryRemove(); + PipelineCached = builder.Build(); + return PipelineCached; } } @@ -108,7 +109,7 @@ public Task ParseAsync(IFileInfo path, YamlFrontMatter? matter return ParseAsync(path, context, Pipeline, ctx); } - private async Task ParseAsync( + private static async Task ParseAsync( IFileInfo path, MarkdownParserContext context, MarkdownPipeline pipeline, diff --git a/src/Elastic.Markdown/Myst/SectionedHeadingRenderer.cs b/src/Elastic.Markdown/Myst/SectionedHeadingRenderer.cs index f95cce848..ae4a4e4e0 100644 --- a/src/Elastic.Markdown/Myst/SectionedHeadingRenderer.cs +++ b/src/Elastic.Markdown/Myst/SectionedHeadingRenderer.cs @@ -33,25 +33,25 @@ protected override void Write(HtmlRenderer renderer, HeadingBlock obj) var anchor = obj.GetData("anchor") as string; var slugTarget = (anchor ?? header) ?? string.Empty; - if (slugTarget.IndexOf('$') >= 0) + if (slugTarget.Contains('$')) slugTarget = HeadingAnchorParser.InlineAnchors().Replace(slugTarget, ""); var slug = slugTarget.Slugify(); - renderer.Write(@"
"); - renderer.Write('<'); - renderer.Write(headingText); - renderer.WriteAttributes(obj); - renderer.Write('>'); - renderer.Write($""""""); - renderer.WriteLeafInline(obj); - renderer.Write(""); - renderer.Write("'); - renderer.Write("
"); - renderer.EnsureLine(); + _ = renderer.Write(@"
") + .Write('<') + .Write(headingText) + .WriteAttributes(obj) + .Write('>') + .Write($"""""") + .WriteLeafInline(obj) + .Write("") + .Write("') + .Write("
") + .EnsureLine(); } } diff --git a/src/Elastic.Markdown/Myst/Settings/StructuredSettings.cs b/src/Elastic.Markdown/Myst/Settings/StructuredSettings.cs index c3faa0b15..afa3dbbc7 100644 --- a/src/Elastic.Markdown/Myst/Settings/StructuredSettings.cs +++ b/src/Elastic.Markdown/Myst/Settings/StructuredSettings.cs @@ -8,7 +8,7 @@ namespace Elastic.Markdown.Myst.Settings; [YamlSerializable] -public record SettingsCollection +public record YamlSettings { [YamlMember(Alias = "product")] public string? Product { get; set; } diff --git a/src/Elastic.Markdown/Myst/Substitution/SubstitutionParser.cs b/src/Elastic.Markdown/Myst/Substitution/SubstitutionParser.cs index 8eff207d8..cb123aadb 100644 --- a/src/Elastic.Markdown/Myst/Substitution/SubstitutionParser.cs +++ b/src/Elastic.Markdown/Myst/Substitution/SubstitutionParser.cs @@ -53,7 +53,7 @@ public LazySubstring(string text, int offset, int length) Length = length; } - public ReadOnlySpan AsSpan() => _text.AsSpan(Offset, Length); + public readonly ReadOnlySpan AsSpan() => _text.AsSpan(Offset, Length); public override string ToString() { @@ -123,7 +123,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) if (closeSticks != 2) return false; - var rawContent = slice.AsSpan().Slice(0, slice.Length - span.Length); + var rawContent = slice.AsSpan()[..(slice.Length - span.Length)]; var content = new LazySubstring(slice.Text, slice.Start, rawContent.Length); diff --git a/src/Elastic.Markdown/Myst/YamlSerialization.cs b/src/Elastic.Markdown/Myst/YamlSerialization.cs index 06d0c47ad..b343f38ff 100644 --- a/src/Elastic.Markdown/Myst/YamlSerialization.cs +++ b/src/Elastic.Markdown/Myst/YamlSerialization.cs @@ -30,9 +30,9 @@ public static T Deserialize(string yaml) } [YamlStaticContext] -[YamlSerializable(typeof(SettingsCollection))] +[YamlSerializable(typeof(YamlSettings))] [YamlSerializable(typeof(SettingsGrouping))] -[YamlSerializable(typeof(SettingsCollection))] +[YamlSerializable(typeof(YamlSettings))] [YamlSerializable(typeof(SettingsGrouping))] [YamlSerializable(typeof(Setting))] [YamlSerializable(typeof(AllowedValue))] diff --git a/src/Elastic.Markdown/Slices/Directives/ApplicableTo.cshtml b/src/Elastic.Markdown/Slices/Directives/ApplicableTo.cshtml index b25a0e7a8..3eb1181b7 100644 --- a/src/Elastic.Markdown/Slices/Directives/ApplicableTo.cshtml +++ b/src/Elastic.Markdown/Slices/Directives/ApplicableTo.cshtml @@ -82,7 +82,7 @@ } } - private IHtmlContent RenderProduct(string name, ApplicabilityOverTime applications) + private IHtmlContent RenderProduct(string name, AppliesCollection applications) { foreach (var applicability in applications) { diff --git a/src/Elastic.Markdown/Slices/Directives/_ViewModels.cs b/src/Elastic.Markdown/Slices/Directives/_ViewModels.cs index 6960dccf9..51b42782c 100644 --- a/src/Elastic.Markdown/Slices/Directives/_ViewModels.cs +++ b/src/Elastic.Markdown/Slices/Directives/_ViewModels.cs @@ -62,9 +62,9 @@ public string Style { var sb = new StringBuilder(); if (Height != null) - sb.Append($"height: {Height};"); + _ = sb.Append($"height: {Height};"); if (Width != null) - sb.Append($"width: {Width};"); + _ = sb.Append($"width: {Width};"); return sb.ToString(); } } @@ -73,7 +73,7 @@ public string Style public class SettingsViewModel { - public required SettingsCollection SettingsCollection { get; init; } + public required YamlSettings SettingsCollection { get; init; } public required Func RenderMarkdown { get; init; } } diff --git a/src/Elastic.Markdown/Slices/HtmlWriter.cs b/src/Elastic.Markdown/Slices/HtmlWriter.cs index 71851cfdc..e09dd2924 100644 --- a/src/Elastic.Markdown/Slices/HtmlWriter.cs +++ b/src/Elastic.Markdown/Slices/HtmlWriter.cs @@ -17,7 +17,7 @@ public HtmlWriter(DocumentationSet documentationSet, IFileSystem writeFileSystem { _writeFileSystem = writeFileSystem; var services = new ServiceCollection(); - services.AddLogging(); + _ = services.AddLogging(); ServiceProvider = services.BuildServiceProvider(); LoggerFactory = ServiceProvider.GetRequiredService(); @@ -43,7 +43,7 @@ private async Task RenderNavigation(MarkdownFile markdown, Cancel ctx = public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = default) { var document = await markdown.ParseFullAsync(ctx); - var html = markdown.CreateHtml(document); + var html = MarkdownFile.CreateHtml(document); await DocumentationSet.Tree.Resolve(ctx); _renderedNavigation ??= await RenderNavigation(markdown, ctx); @@ -60,7 +60,7 @@ public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = defau Title = markdown.Title ?? "[TITLE NOT SET]", TitleRaw = markdown.TitleRaw ?? "[TITLE NOT SET]", MarkdownHtml = html, - PageTocItems = markdown.TableOfContents.Values.ToList(), + PageTocItems = [.. markdown.TableOfContents.Values], Tree = DocumentationSet.Tree, CurrentDocument = markdown, PreviousDocument = previous, @@ -90,7 +90,7 @@ public async Task WriteAsync(IFileInfo outputFile, MarkdownFile markdown, Cancel : Path.Combine(outputFile.Directory.FullName, Path.GetFileNameWithoutExtension(outputFile.Name)); if (dir is not null && !_writeFileSystem.Directory.Exists(dir)) - _writeFileSystem.Directory.CreateDirectory(dir); + _ = _writeFileSystem.Directory.CreateDirectory(dir); path = dir is null ? Path.GetFileNameWithoutExtension(outputFile.Name) + ".html" diff --git a/src/Elastic.Markdown/Slices/Layout/_TocTreeNav.cshtml b/src/Elastic.Markdown/Slices/Layout/_TocTreeNav.cshtml index 512c14695..ead5995ed 100644 --- a/src/Elastic.Markdown/Slices/Layout/_TocTreeNav.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_TocTreeNav.cshtml @@ -1,4 +1,3 @@ -@using Elastic.Markdown.Helpers @using Elastic.Markdown.IO.Navigation @inherits RazorSlice @foreach (var item in Model.SubTree.NavigationItems) diff --git a/src/Elastic.Markdown/SourceGenerationContext.cs b/src/Elastic.Markdown/SourceGenerationContext.cs index 7c0c77ec6..8e1769b96 100644 --- a/src/Elastic.Markdown/SourceGenerationContext.cs +++ b/src/Elastic.Markdown/SourceGenerationContext.cs @@ -17,4 +17,4 @@ namespace Elastic.Markdown; [JsonSerializable(typeof(GitCheckoutInformation))] [JsonSerializable(typeof(LinkIndex))] [JsonSerializable(typeof(LinkIndexEntry))] -internal partial class SourceGenerationContext : JsonSerializerContext; +internal sealed partial class SourceGenerationContext : JsonSerializerContext; diff --git a/src/docs-assembler/AssemblyConfiguration.cs b/src/docs-assembler/AssemblyConfiguration.cs index 8664e79f3..aa6227204 100644 --- a/src/docs-assembler/AssemblyConfiguration.cs +++ b/src/docs-assembler/AssemblyConfiguration.cs @@ -35,7 +35,7 @@ public static AssemblyConfiguration Deserialize(string yaml) } [YamlMember(Alias = "repos")] - public Dictionary Repositories { get; set; } = new(); + public Dictionary Repositories { get; set; } = []; } public record Repository diff --git a/src/docs-assembler/Cli/Filters.cs b/src/docs-assembler/Cli/Filters.cs index cdf3c07fb..2d5427320 100644 --- a/src/docs-assembler/Cli/Filters.cs +++ b/src/docs-assembler/Cli/Filters.cs @@ -7,7 +7,7 @@ namespace Documentation.Assembler.Cli; -internal class StopwatchFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +internal sealed class StopwatchFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) { public override async Task InvokeAsync(ConsoleAppContext context, Cancel ctx) { @@ -29,7 +29,7 @@ public override async Task InvokeAsync(ConsoleAppContext context, Cancel ctx) } } -internal class CatchExceptionFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +internal sealed class CatchExceptionFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) { public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) { diff --git a/src/docs-assembler/Cli/LinkCommands.cs b/src/docs-assembler/Cli/LinkCommands.cs index 8ec294b27..5bca2c38c 100644 --- a/src/docs-assembler/Cli/LinkCommands.cs +++ b/src/docs-assembler/Cli/LinkCommands.cs @@ -11,13 +11,15 @@ namespace Documentation.Assembler.Cli; -internal class LinkCommands(ILoggerFactory logger) +internal sealed class LinkCommands(ILoggerFactory logger) { private void AssignOutputLogger() { var log = logger.CreateLogger(); +#pragma warning disable CA2254 ConsoleApp.Log = msg => log.LogInformation(msg); ConsoleApp.LogError = msg => log.LogError(msg); +#pragma warning restore CA2254 } /// @@ -38,7 +40,7 @@ public async Task CreateLinkIndex(Cancel ctx = default) Console.WriteLine("--------------------------------------"); - var linkIndex = new LinkIndex { Repositories = new Dictionary>() }; + var linkIndex = new LinkIndex { Repositories = [] }; try { ListObjectsV2Response response; diff --git a/src/docs-assembler/Cli/RepositoryCommands.cs b/src/docs-assembler/Cli/RepositoryCommands.cs index 50fcbe2c8..72c147cb1 100644 --- a/src/docs-assembler/Cli/RepositoryCommands.cs +++ b/src/docs-assembler/Cli/RepositoryCommands.cs @@ -21,13 +21,15 @@ public void Handle(LineOut lineOut) => lineOut.CharsOrString( public void Handle(Exception e) { } } -internal class RepositoryCommands(ILoggerFactory logger) +internal sealed class RepositoryCommands(ILoggerFactory logger) { private void AssignOutputLogger() { var log = logger.CreateLogger(); +#pragma warning disable CA2254 ConsoleApp.Log = msg => log.LogInformation(msg); ConsoleApp.LogError = msg => log.LogError(msg); +#pragma warning restore CA2254 } // would love to use libgit2 so there is no git dependency but @@ -53,14 +55,14 @@ await Task.Run(() => var checkoutFolder = Path.Combine(Paths.Root.FullName, $".artifacts/assembly/{name}"); var sw = Stopwatch.StartNew(); - dict.AddOrUpdate(name, sw, (_, _) => sw); + _ = dict.AddOrUpdate(name, sw, (_, _) => sw); Console.WriteLine($"Checkout: {name}\t{repository}\t{checkoutFolder}"); var branch = repository.Branch ?? "main"; var args = new StartArguments( "git", "clone", repository.Origin, checkoutFolder, "--depth", "1" , "--single-branch", "--branch", branch ); - Proc.StartRedirected(args, new ConsoleLineHandler(name)); + _ = Proc.StartRedirected(args, new ConsoleLineHandler(name)); sw.Stop(); }, c); }).ConfigureAwait(false); @@ -70,9 +72,8 @@ await Task.Run(() => } /// List all checked out repositories - /// [Command("list")] - public async Task ListRepositories(Cancel ctx = default) + public async Task ListRepositories() { AssignOutputLogger(); var assemblyPath = Path.Combine(Paths.Root.FullName, $".artifacts/assembly"); diff --git a/src/docs-assembler/Program.cs b/src/docs-assembler/Program.cs index 43f975580..021cd6315 100644 --- a/src/docs-assembler/Program.cs +++ b/src/docs-assembler/Program.cs @@ -12,18 +12,17 @@ var services = new ServiceCollection(); services.AddGitHubActionsCore(); -services.AddLogging(x => -{ - x.ClearProviders(); - x.SetMinimumLevel(LogLevel.Information); - x.AddSimpleConsole(c => +services.AddLogging(x => x + .ClearProviders() + .SetMinimumLevel(LogLevel.Information) + .AddSimpleConsole(c => { c.SingleLine = true; c.IncludeScopes = true; c.UseUtcTimestamp = true; c.TimestampFormat = Environment.UserInteractive ? ":: " : "[yyyy-MM-ddTHH:mm:ss] "; - }); -}); + }) +); services.AddSingleton(); services.AddSingleton(); diff --git a/src/docs-builder/Cli/CheckForUpdatesFilter.cs b/src/docs-builder/Cli/CheckForUpdatesFilter.cs index aa9c8d4ef..aaf5728bd 100644 --- a/src/docs-builder/Cli/CheckForUpdatesFilter.cs +++ b/src/docs-builder/Cli/CheckForUpdatesFilter.cs @@ -8,7 +8,7 @@ namespace Documentation.Builder.Cli; -internal class CheckForUpdatesFilter : ConsoleAppFilter +internal sealed class CheckForUpdatesFilter : ConsoleAppFilter { private readonly FileInfo _stateFile; @@ -32,7 +32,7 @@ public override async Task InvokeAsync(ConsoleAppContext context, Cancel ctx) CompareWithAssemblyVersion(latestVersionUrl); } - private void CompareWithAssemblyVersion(Uri latestVersionUrl) + private static void CompareWithAssemblyVersion(Uri latestVersionUrl) { var versionPath = latestVersionUrl.AbsolutePath.Split('/').Last(); if (!SemVersion.TryParse(versionPath, out var latestVersion)) @@ -61,7 +61,7 @@ private void CompareWithAssemblyVersion(Uri latestVersionUrl) ConsoleApp.LogError($"Unable to parse current version from docs-builder binary"); } - private async ValueTask GetLatestVersion(CancellationToken ctx) + private async ValueTask GetLatestVersion(Cancel ctx) { if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) return null; @@ -83,7 +83,7 @@ private void CompareWithAssemblyVersion(Uri latestVersionUrl) { // ensure the 'elastic' folder exists. if (!Directory.Exists(_stateFile.Directory.FullName)) - Directory.CreateDirectory(_stateFile.Directory.FullName); + _ = Directory.CreateDirectory(_stateFile.Directory.FullName); await File.WriteAllTextAsync(_stateFile.FullName, redirectUrl.ToString(), ctx); } return redirectUrl; diff --git a/src/docs-builder/Cli/Commands.cs b/src/docs-builder/Cli/Commands.cs index 5fdb6b3ad..5c32681e4 100644 --- a/src/docs-builder/Cli/Commands.cs +++ b/src/docs-builder/Cli/Commands.cs @@ -13,13 +13,15 @@ namespace Documentation.Builder.Cli; -internal class Commands(ILoggerFactory logger, ICoreService githubActionsService) +internal sealed class Commands(ILoggerFactory logger, ICoreService githubActionsService) { private void AssignOutputLogger() { var log = logger.CreateLogger(); +#pragma warning disable CA2254 ConsoleApp.Log = msg => log.LogInformation(msg); ConsoleApp.LogError = msg => log.LogError(msg); +#pragma warning restore CA2254 } /// diff --git a/src/docs-builder/Cli/Filters.cs b/src/docs-builder/Cli/Filters.cs index aee8d557e..0f3de93ca 100644 --- a/src/docs-builder/Cli/Filters.cs +++ b/src/docs-builder/Cli/Filters.cs @@ -6,7 +6,7 @@ namespace Documentation.Builder.Cli; -internal class StopwatchFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +internal sealed class StopwatchFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) { public override async Task InvokeAsync(ConsoleAppContext context, Cancel ctx) { @@ -28,9 +28,9 @@ public override async Task InvokeAsync(ConsoleAppContext context, Cancel ctx) } } -internal class CatchExceptionFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +internal sealed class CatchExceptionFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + public override async Task InvokeAsync(ConsoleAppContext context, Cancel cancellationToken) { try { diff --git a/src/docs-builder/Diagnostics/Console/ConsoleDiagnosticsCollector.cs b/src/docs-builder/Diagnostics/Console/ConsoleDiagnosticsCollector.cs index 1ac64c0d2..f4747454c 100644 --- a/src/docs-builder/Diagnostics/Console/ConsoleDiagnosticsCollector.cs +++ b/src/docs-builder/Diagnostics/Console/ConsoleDiagnosticsCollector.cs @@ -14,8 +14,8 @@ public class ConsoleDiagnosticsCollector(ILoggerFactory loggerFactory, ICoreServ : DiagnosticsCollector([new Log(loggerFactory.CreateLogger()), new GithubAnnotationOutput(githubActions)] ) { - private readonly List _errors = new(); - private readonly List _warnings = new(); + private readonly List _errors = []; + private readonly List _warnings = []; protected override void HandleItem(Diagnostic diagnostic) { @@ -25,7 +25,7 @@ protected override void HandleItem(Diagnostic diagnostic) _errors.Add(diagnostic); } - public override async Task StopAsync(Cancel ctx) + public override async Task StopAsync(Cancel cancellationToken) { var repository = new ErrataFileSourceRepository(); repository.WriteDiagnosticsToConsole(_errors, _warnings); diff --git a/src/docs-builder/Diagnostics/Console/ErrataFileSourceRepository.cs b/src/docs-builder/Diagnostics/Console/ErrataFileSourceRepository.cs index 746aede44..b9d874353 100644 --- a/src/docs-builder/Diagnostics/Console/ErrataFileSourceRepository.cs +++ b/src/docs-builder/Diagnostics/Console/ErrataFileSourceRepository.cs @@ -14,6 +14,7 @@ namespace Documentation.Builder.Diagnostics.Console; public class ErrataFileSourceRepository : ISourceRepository { + [SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly")] public bool TryGet(string id, [NotNullWhen(true)] out Source? source) { using var reader = new Utf8StreamReader(id); @@ -48,7 +49,7 @@ public void WriteDiagnosticsToConsole(IReadOnlyCollection errors, IR else d = d.WithNote(item.File); - report.AddDiagnostic(d); + _ = report.AddDiagnostic(d); } // Render the report diff --git a/src/docs-builder/Diagnostics/LiveMode/LiveModeDiagnosticsCollector.cs b/src/docs-builder/Diagnostics/LiveMode/LiveModeDiagnosticsCollector.cs index 3cc9e302d..63e5ba851 100644 --- a/src/docs-builder/Diagnostics/LiveMode/LiveModeDiagnosticsCollector.cs +++ b/src/docs-builder/Diagnostics/LiveMode/LiveModeDiagnosticsCollector.cs @@ -13,5 +13,5 @@ public class LiveModeDiagnosticsCollector(ILoggerFactory loggerFactory) { protected override void HandleItem(Diagnostic diagnostic) { } - public override async Task StopAsync(Cancel ctx) => await Task.CompletedTask; + public override async Task StopAsync(Cancel cancellationToken) => await Task.CompletedTask; } diff --git a/src/docs-builder/Diagnostics/Log.cs b/src/docs-builder/Diagnostics/Log.cs index 5803d2613..8148f3c28 100644 --- a/src/docs-builder/Diagnostics/Log.cs +++ b/src/docs-builder/Diagnostics/Log.cs @@ -5,8 +5,7 @@ using Elastic.Markdown.Diagnostics; using Microsoft.Extensions.Logging; -// ReSharper disable once CheckNamespace -namespace Documentation.Builder; +namespace Documentation.Builder.Diagnostics; // named Log for terseness on console output public class Log(ILogger logger) : IDiagnosticsOutput @@ -14,9 +13,9 @@ public class Log(ILogger logger) : IDiagnosticsOutput public void Write(Diagnostic diagnostic) { if (diagnostic.Severity == Severity.Error) - logger.LogError($"{diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})"); + logger.LogError("{Message} ({File}:{Line})", diagnostic.Message, diagnostic.File, diagnostic.Line); else - logger.LogWarning($"{diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})"); + logger.LogWarning("{Message} ({File}:{Line})", diagnostic.Message, diagnostic.File, diagnostic.Line); } } diff --git a/src/docs-builder/Http/DocumentationWebHost.cs b/src/docs-builder/Http/DocumentationWebHost.cs index 86d3f9977..5b97a0bd3 100644 --- a/src/docs-builder/Http/DocumentationWebHost.cs +++ b/src/docs-builder/Http/DocumentationWebHost.cs @@ -29,36 +29,39 @@ public DocumentationWebHost(string? path, int port, ILoggerFactory logger, IFile { var builder = WebApplication.CreateSlimBuilder(); - builder.Logging.ClearProviders(); - builder.Logging.SetMinimumLevel(LogLevel.Warning) + _ = builder.Logging + .ClearProviders() + .SetMinimumLevel(LogLevel.Warning) .AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Error) .AddFilter("Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware", LogLevel.Error) .AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Information) - .AddSimpleConsole(o => o.SingleLine = true); _context = new BuildContext(fileSystem, fileSystem, path, null) { Collector = new LiveModeDiagnosticsCollector(logger) }; - builder.Services.AddAotLiveReload(s => - { - s.FolderToMonitor = _context.SourcePath.FullName; - s.ClientFileExtensions = ".md,.yml"; - }); - builder.Services.AddSingleton(_ => new ReloadableGeneratorState(_context.SourcePath, null, _context, logger)); - builder.Services.AddHostedService(); + _ = builder.Services + .AddAotLiveReload(s => + { + s.FolderToMonitor = _context.SourcePath.FullName; + s.ClientFileExtensions = ".md,.yml"; + }) + .AddSingleton(_ => + new ReloadableGeneratorState(_context.SourcePath, null, _context, logger) + ) + .AddHostedService(); + if (IsDotNetWatchBuild()) - builder.Services.AddHostedService(); + _ = builder.Services.AddHostedService(); - builder.WebHost.UseUrls($"http://localhost:{port}"); + _ = builder.WebHost.UseUrls($"http://localhost:{port}"); _webApplication = builder.Build(); SetUpRoutes(); } - private bool IsDotNetWatchBuild() => - Environment.GetEnvironmentVariable("DOTNET_WATCH") is not null; + private static bool IsDotNetWatchBuild() => Environment.GetEnvironmentVariable("DOTNET_WATCH") is not null; public async Task RunAsync(Cancel ctx) { @@ -74,18 +77,20 @@ public async Task StopAsync(Cancel ctx) private void SetUpRoutes() { - _webApplication.UseLiveReload(); - _webApplication.UseStaticFiles(new StaticFileOptions - { - FileProvider = new EmbeddedOrPhysicalFileProvider(_context), - RequestPath = "/_static" - }); - _webApplication.UseRouting(); + _ = _webApplication + .UseLiveReload() + .UseStaticFiles( + new StaticFileOptions + { + FileProvider = new EmbeddedOrPhysicalFileProvider(_context), + RequestPath = "/_static" + }) + .UseRouting(); - _webApplication.MapGet("/", (ReloadableGeneratorState holder, Cancel ctx) => + _ = _webApplication.MapGet("/", (ReloadableGeneratorState holder, Cancel ctx) => ServeDocumentationFile(holder, "index.md", ctx)); - _webApplication.MapGet("{**slug}", (string slug, ReloadableGeneratorState holder, Cancel ctx) => + _ = _webApplication.MapGet("{**slug}", (string slug, ReloadableGeneratorState holder, Cancel ctx) => ServeDocumentationFile(holder, slug, ctx)); } @@ -122,7 +127,7 @@ private static async Task ServeDocumentationFile(ReloadableGeneratorSta } -public class EmbeddedOrPhysicalFileProvider : IFileProvider +public sealed class EmbeddedOrPhysicalFileProvider : IFileProvider, IDisposable { private readonly EmbeddedFileProvider _embeddedProvider = new(typeof(BuildContext).Assembly, "Elastic.Markdown._static"); private readonly PhysicalFileProvider? _staticFilesInDocsFolder; @@ -184,4 +189,10 @@ public IChangeToken Watch(string filter) changeToken = _embeddedProvider.Watch(filter); return changeToken; } + + public void Dispose() + { + _staticFilesInDocsFolder?.Dispose(); + _staticWebFilesDuringDebug?.Dispose(); + } } diff --git a/src/docs-builder/Http/LiveReload.cs b/src/docs-builder/Http/LiveReload.cs index 1650fb45d..d71e62c0b 100644 --- a/src/docs-builder/Http/LiveReload.cs +++ b/src/docs-builder/Http/LiveReload.cs @@ -8,7 +8,9 @@ using Microsoft.Extensions.DependencyInjection; // ReSharper disable once CheckNamespace +#pragma warning disable IDE0130 namespace Westwind.AspNetCore.LiveReload; +#pragma warning restore IDE0130 // This exists to disable AOT trimming error messages for the LiveReload middleware's own AddLiveReload() method. // longer term we should build our own LiveReload middleware that doesn't rely on this also to help reduce dependencies @@ -35,13 +37,13 @@ public static IServiceCollection AddAotLiveReload(this IServiceCollection servic var env = provider.GetService(); if (string.IsNullOrEmpty(config.FolderToMonitor)) config.FolderToMonitor = env!.ContentRootPath; - else if (config.FolderToMonitor.StartsWith("~")) + else if (config.FolderToMonitor.StartsWith('~')) { if (config.FolderToMonitor.Length > 1) { - var folder = config.FolderToMonitor.Substring(1); - if (folder.StartsWith('/') || folder.StartsWith("\\")) - folder = folder.Substring(1); + var folder = config.FolderToMonitor[1..]; + if (folder.StartsWith('/') || folder.StartsWith('\\')) + folder = folder[1..]; config.FolderToMonitor = Path.Combine(env!.ContentRootPath, folder); config.FolderToMonitor = Path.GetFullPath(config.FolderToMonitor); } diff --git a/src/docs-builder/Http/ParcelWatchService.cs b/src/docs-builder/Http/ParcelWatchService.cs index d942a50a8..84b84be0a 100644 --- a/src/docs-builder/Http/ParcelWatchService.cs +++ b/src/docs-builder/Http/ParcelWatchService.cs @@ -12,7 +12,7 @@ public class ParcelWatchService : IHostedService { private Process? _process; - public Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(Cancel cancellationToken) { _process = Process.Start(new ProcessStartInfo { @@ -35,7 +35,7 @@ public Task StartAsync(CancellationToken cancellationToken) return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken) + public Task StopAsync(Cancel cancellationToken) { _process?.Kill(entireProcessTree: true); _process?.Kill(); diff --git a/src/docs-builder/Http/ReloadGeneratorService.cs b/src/docs-builder/Http/ReloadGeneratorService.cs index d1fea4f36..8045e8fec 100644 --- a/src/docs-builder/Http/ReloadGeneratorService.cs +++ b/src/docs-builder/Http/ReloadGeneratorService.cs @@ -6,9 +6,10 @@ namespace Documentation.Builder.Http; -public class ReloadGeneratorService( +public sealed class ReloadGeneratorService( ReloadableGeneratorState reloadableGenerator, - ILogger logger) : IHostedService + ILogger logger) : IHostedService, + IDisposable { private FileSystemWatcher? _watcher; private ReloadableGeneratorState ReloadableGenerator { get; } = reloadableGenerator; @@ -17,9 +18,9 @@ public class ReloadGeneratorService( //debounce reload requests due to many file changes private readonly Debouncer _debouncer = new(TimeSpan.FromMilliseconds(200)); - public async Task StartAsync(Cancel ctx) + public async Task StartAsync(Cancel cancellationToken) { - await ReloadableGenerator.ReloadAsync(ctx); + await ReloadableGenerator.ReloadAsync(cancellationToken); var watcher = new FileSystemWatcher(ReloadableGenerator.Generator.DocumentationSet.SourcePath.FullName) { @@ -53,7 +54,7 @@ private void Reload() => Logger.LogInformation("Reload complete!"); }, default); - public Task StopAsync(CancellationToken cancellationToken) + public Task StopAsync(Cancel cancellationToken) { _watcher?.Dispose(); return Task.CompletedTask; @@ -69,29 +70,28 @@ private void OnChanged(object sender, FileSystemEventArgs e) if (e.FullPath.EndsWith(".md")) Reload(); - Logger.LogInformation($"Changed: {e.FullPath}"); + Logger.LogInformation("Changed: {FullPath}", e.FullPath); } private void OnCreated(object sender, FileSystemEventArgs e) { - var value = $"Created: {e.FullPath}"; if (e.FullPath.EndsWith(".md")) Reload(); - Logger.LogInformation(value); + Logger.LogInformation("Created: {FullPath}", e.FullPath); } private void OnDeleted(object sender, FileSystemEventArgs e) { if (e.FullPath.EndsWith(".md")) Reload(); - Logger.LogInformation($"Deleted: {e.FullPath}"); + Logger.LogInformation("Deleted: {FullPath}", e.FullPath); } private void OnRenamed(object sender, RenamedEventArgs e) { - Logger.LogInformation($"Renamed:"); - Logger.LogInformation($" Old: {e.OldFullPath}"); - Logger.LogInformation($" New: {e.FullPath}"); + Logger.LogInformation("Renamed:"); + Logger.LogInformation(" Old: {OldFullPath}", e.OldFullPath); + Logger.LogInformation(" New: {NewFullPath}", e.FullPath); if (e.FullPath.EndsWith(".md")) Reload(); } @@ -103,19 +103,25 @@ private void PrintException(Exception? ex) { if (ex == null) return; - Logger.LogError($"Message: {ex.Message}"); + Logger.LogError("Message: {Message}", ex.Message); Logger.LogError("Stacktrace:"); - Logger.LogError(ex.StackTrace); + Logger.LogError("{StackTrace}", ex.StackTrace ?? "No stack trace available"); PrintException(ex.InnerException); } - private class Debouncer(TimeSpan window) + public void Dispose() + { + _watcher?.Dispose(); + _debouncer.Dispose(); + } + + private sealed class Debouncer(TimeSpan window) : IDisposable { private readonly SemaphoreSlim _semaphore = new(1, 1); private readonly long _windowInTicks = window.Ticks; private long _nextRun; - public async Task ExecuteAsync(Func innerAction, CancellationToken cancellationToken) + public async Task ExecuteAsync(Func innerAction, Cancel cancellationToken) { var requestStart = DateTime.UtcNow.Ticks; @@ -132,8 +138,11 @@ public async Task ExecuteAsync(Func innerAction, Cancel } finally { - _semaphore.Release(); + _ = _semaphore.Release(); } } + + public void Dispose() => _semaphore.Dispose(); } + } diff --git a/src/docs-builder/Http/ReloadableGeneratorState.cs b/src/docs-builder/Http/ReloadableGeneratorState.cs index 0ae1cdb8c..7db472e31 100644 --- a/src/docs-builder/Http/ReloadableGeneratorState.cs +++ b/src/docs-builder/Http/ReloadableGeneratorState.cs @@ -30,6 +30,6 @@ public async Task ReloadAsync(Cancel ctx) var docSet = new DocumentationSet(context, logger); var generator = new DocumentationGenerator(docSet, logger); await generator.ResolveDirectoryTree(ctx); - Interlocked.Exchange(ref _generator, generator); + _ = Interlocked.Exchange(ref _generator, generator); } } diff --git a/src/docs-builder/Program.cs b/src/docs-builder/Program.cs index 5a74ce1e1..021239ec7 100644 --- a/src/docs-builder/Program.cs +++ b/src/docs-builder/Program.cs @@ -11,18 +11,17 @@ var services = new ServiceCollection(); services.AddGitHubActionsCore(); -services.AddLogging(x => -{ - x.ClearProviders(); - x.SetMinimumLevel(LogLevel.Information); - x.AddSimpleConsole(c => +services.AddLogging(x => x + .ClearProviders() + .SetMinimumLevel(LogLevel.Information) + .AddSimpleConsole(c => { c.SingleLine = true; c.IncludeScopes = true; c.UseUtcTimestamp = true; c.TimestampFormat = Environment.UserInteractive ? ":: " : "[yyyy-MM-ddTHH:mm:ss] "; - }); -}); + }) +); services.AddSingleton(); services.AddSingleton(); diff --git a/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs b/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs index 564879e95..d5e87e466 100644 --- a/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs +++ b/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs @@ -63,7 +63,7 @@ public class ClassicCallOutsRequiresContent(ITestOutputHelper output) : CodeBloc public void ParsesMagicCallOuts() => Block!.CallOuts .Should().NotBeNullOrEmpty() .And.HaveCount(2) - .And.OnlyContain(c => c.Text.StartsWith("<")); + .And.OnlyContain(c => c.Text.StartsWith('<')); [Fact] public void RequiresContentToFollow() => Collector.Diagnostics.Should().HaveCount(1) @@ -86,7 +86,7 @@ public class ClassicCallOutsNotFollowedByList(ITestOutputHelper output) : CodeBl public void ParsesMagicCallOuts() => Block!.CallOuts .Should().NotBeNullOrEmpty() .And.HaveCount(2) - .And.OnlyContain(c => c.Text.StartsWith("<")); + .And.OnlyContain(c => c.Text.StartsWith('<')); [Fact] public void RequiresContentToFollow() => Collector.Diagnostics.Should().HaveCount(1) @@ -114,7 +114,7 @@ 2. Marking the second callout public void ParsesMagicCallOuts() => Block!.CallOuts .Should().NotBeNullOrEmpty() .And.HaveCount(2) - .And.OnlyContain(c => c.Text.StartsWith("<")); + .And.OnlyContain(c => c.Text.StartsWith('<')); [Fact] public void AllowsAParagraphInBetween() => Collector.Diagnostics.Should().BeEmpty(); @@ -142,7 +142,7 @@ 2. Marking the second callout public void ParsesMagicCallOuts() => Block!.CallOuts .Should().NotBeNullOrEmpty() .And.HaveCount(2) - .And.OnlyContain(c => c.Text.StartsWith("<")); + .And.OnlyContain(c => c.Text.StartsWith('<')); [Fact] public void RequiresContentToFollow() => Collector.Diagnostics.Should().HaveCount(1) @@ -167,7 +167,7 @@ 1. Only marking the first callout public void ParsesMagicCallOuts() => Block!.CallOuts .Should().NotBeNullOrEmpty() .And.HaveCount(2) - .And.OnlyContain(c => c.Text.StartsWith("<")); + .And.OnlyContain(c => c.Text.StartsWith('<')); [Fact] public void RequiresContentToFollow() => Collector.Diagnostics.Should().HaveCount(1) @@ -191,13 +191,13 @@ 2. The second appears twice public void SeesTwoUniqueCallouts() => Block!.UniqueCallOuts .Should().NotBeNullOrEmpty() .And.HaveCount(2) - .And.OnlyContain(c => c.Text.StartsWith("<")); + .And.OnlyContain(c => c.Text.StartsWith('<')); [Fact] public void ParsesAllForLineInformation() => Block!.CallOuts .Should().NotBeNullOrEmpty() .And.HaveCount(3) - .And.OnlyContain(c => c.Text.StartsWith("<")); + .And.OnlyContain(c => c.Text.StartsWith('<')); [Fact] public void RequiresContentToFollow() => Collector.Diagnostics.Should().BeEmpty(); @@ -264,7 +264,7 @@ public void ParsesClassicCallouts() Block!.CallOuts .Should().NotBeNullOrEmpty() .And.HaveCount(9) - .And.OnlyContain(c => c.Text.StartsWith("<")); + .And.OnlyContain(c => c.Text.StartsWith('<')); Block!.UniqueCallOuts .Should().NotBeNullOrEmpty() @@ -291,7 +291,7 @@ 2. Second callout public void ParsesMagicCallOuts() => Block!.CallOuts .Should().NotBeNullOrEmpty() .And.HaveCount(3) - .And.OnlyContain(c => c.Text.StartsWith("<")); + .And.OnlyContain(c => c.Text.StartsWith('<')); [Fact] public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); @@ -316,7 +316,7 @@ 2. Second callout public void ParsesMagicCallOuts() => Block!.CallOuts .Should().NotBeNullOrEmpty() .And.HaveCount(5) - .And.OnlyContain(c => c.Text.StartsWith("<")); + .And.OnlyContain(c => c.Text.StartsWith('<')); [Fact] public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); diff --git a/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs b/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs index 4a49046b9..fa8c5e58b 100644 --- a/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs @@ -43,7 +43,7 @@ protected DirectiveTest(ITestOutputHelper output, [LanguageInjection("markdown") { var logger = new TestLoggerFactory(output); - TestingFullDocument = string.IsNullOrEmpty(content) || content.StartsWith("---"); + TestingFullDocument = string.IsNullOrEmpty(content) || content.StartsWith("---", StringComparison.OrdinalIgnoreCase); var documentContents = TestingFullDocument ? content : // language=markdown $""" @@ -86,7 +86,7 @@ public virtual async ValueTask InitializeAsync() await Set.LinkResolver.FetchLinks(); Document = await File.ParseFullAsync(default); - var html = File.CreateHtml(Document).AsSpan(); + var html = MarkdownFile.CreateHtml(Document).AsSpan(); var find = ""; var start = html.IndexOf(find, StringComparison.Ordinal); Html = start >= 0 @@ -97,6 +97,9 @@ public virtual async ValueTask InitializeAsync() await Collector.StopAsync(default); } - public ValueTask DisposeAsync() => ValueTask.CompletedTask; - + public ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; + } } diff --git a/tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs b/tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs index bff00ec6c..88316f4c4 100644 --- a/tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs +++ b/tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs @@ -50,5 +50,9 @@ public async ValueTask InitializeAsync() Configuration = Generator.DocumentationSet.Configuration; } - public ValueTask DisposeAsync() => ValueTask.CompletedTask; + public ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; + } } diff --git a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs index c93f0019f..708cb1b79 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs @@ -64,10 +64,7 @@ public void GeneratesHtml() => public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); [Fact] - public void EmitsCrossLink() - { - Collector.CrossLinks.Should().HaveCount(0); - } + public void EmitsCrossLink() => Collector.CrossLinks.Should().HaveCount(0); } public class InsertPageTitleTests(ITestOutputHelper output) : LinkTestBase(output, @@ -87,10 +84,7 @@ public void GeneratesHtml() => public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); [Fact] - public void EmitsCrossLink() - { - Collector.CrossLinks.Should().HaveCount(0); - } + public void EmitsCrossLink() => Collector.CrossLinks.Should().HaveCount(0); } public class LinkReferenceTest(ITestOutputHelper output) : LinkTestBase(output, @@ -112,10 +106,7 @@ public void GeneratesHtml() => public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); [Fact] - public void EmitsCrossLink() - { - Collector.CrossLinks.Should().HaveCount(0); - } + public void EmitsCrossLink() => Collector.CrossLinks.Should().HaveCount(0); } public class CrossLinkReferenceTest(ITestOutputHelper output) : LinkTestBase(output, diff --git a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs index 82fe321c6..d131aaab0 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs @@ -83,7 +83,7 @@ protected InlineTest( Dictionary? globalVariables = null) { var logger = new TestLoggerFactory(output); - TestingFullDocument = string.IsNullOrEmpty(content) || content.StartsWith("---"); + TestingFullDocument = string.IsNullOrEmpty(content) || content.StartsWith("---", StringComparison.OrdinalIgnoreCase); var documentContents = TestingFullDocument ? content : // language=markdown @@ -130,7 +130,7 @@ public virtual async ValueTask InitializeAsync() await Set.LinkResolver.FetchLinks(); Document = await File.ParseFullAsync(default); - var html = File.CreateHtml(Document).AsSpan(); + var html = MarkdownFile.CreateHtml(Document).AsSpan(); var find = "\n"; var start = html.IndexOf(find, StringComparison.Ordinal); Html = start >= 0 && !TestingFullDocument @@ -140,6 +140,9 @@ public virtual async ValueTask InitializeAsync() await Collector.StopAsync(default); } - public ValueTask DisposeAsync() => ValueTask.CompletedTask; - + public ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; + } } diff --git a/tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs b/tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs index 988a688da..1781901dd 100644 --- a/tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs +++ b/tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs @@ -11,11 +11,12 @@ namespace Elastic.Markdown.Tests; public class TestCrossLinkResolver : ICrossLinkResolver { - public Dictionary LinkReferences { get; } = new(); - public HashSet DeclaredRepositories { get; } = new(); + private Dictionary LinkReferences { get; } = []; + private HashSet DeclaredRepositories { get; } = []; public Task FetchLinks() { + // language=json var json = """ { "origin": { diff --git a/tests/Elastic.Markdown.Tests/TestDiagnosticsCollector.cs b/tests/Elastic.Markdown.Tests/TestDiagnosticsCollector.cs index 9af4bd32d..820808d23 100644 --- a/tests/Elastic.Markdown.Tests/TestDiagnosticsCollector.cs +++ b/tests/Elastic.Markdown.Tests/TestDiagnosticsCollector.cs @@ -20,7 +20,7 @@ public void Write(Diagnostic diagnostic) public class TestDiagnosticsCollector(ITestOutputHelper output) : DiagnosticsCollector([new TestDiagnosticsOutput(output)]) { - private readonly List _diagnostics = new(); + private readonly List _diagnostics = []; public IReadOnlyCollection Diagnostics => _diagnostics; diff --git a/tests/Elastic.Markdown.Tests/TestLogger.cs b/tests/Elastic.Markdown.Tests/TestLogger.cs index dd3419178..1e69d3565 100644 --- a/tests/Elastic.Markdown.Tests/TestLogger.cs +++ b/tests/Elastic.Markdown.Tests/TestLogger.cs @@ -8,7 +8,7 @@ namespace Elastic.Markdown.Tests; public class TestLogger(ITestOutputHelper output) : ILogger { - private class NullScope : IDisposable + private sealed class NullScope : IDisposable { public void Dispose() { } } @@ -23,18 +23,14 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except public class TestLoggerProvider(ITestOutputHelper output) : ILoggerProvider { - public void Dispose() - { - } + public void Dispose() => GC.SuppressFinalize(this); public ILogger CreateLogger(string categoryName) => new TestLogger(output); } public class TestLoggerFactory(ITestOutputHelper output) : ILoggerFactory { - public void Dispose() - { - } + public void Dispose() => GC.SuppressFinalize(this); public void AddProvider(ILoggerProvider provider) { } diff --git a/tests/authoring/Applicability/AppliesToDirective.fs b/tests/authoring/Applicability/AppliesToDirective.fs index 006c1c70c..2d4e76296 100644 --- a/tests/authoring/Applicability/AppliesToDirective.fs +++ b/tests/authoring/Applicability/AppliesToDirective.fs @@ -28,9 +28,9 @@ serverless: directives |> appliesToDirective (ApplicableTo( Serverless=ServerlessProjectApplicability( - Security=ApplicabilityOverTime.op_Explicit "ga 9.0.0", - Elasticsearch=ApplicabilityOverTime.op_Explicit "beta 9.1.0", - Observability=ApplicabilityOverTime.op_Explicit "discontinued 9.2.0" + Security=AppliesCollection.op_Explicit "ga 9.0.0", + Elasticsearch=AppliesCollection.op_Explicit "beta 9.1.0", + Observability=AppliesCollection.op_Explicit "discontinued 9.2.0" ) )) @@ -51,9 +51,9 @@ serverless: directives |> appliesToDirective (ApplicableTo( Serverless=ServerlessProjectApplicability( - Security=ApplicabilityOverTime.op_Explicit "ga 9.0.0", - Elasticsearch=ApplicabilityOverTime.op_Explicit "beta 9.1.0", - Observability=ApplicabilityOverTime.op_Explicit "discontinued 9.2.0" + Security=AppliesCollection.op_Explicit "ga 9.0.0", + Elasticsearch=AppliesCollection.op_Explicit "beta 9.1.0", + Observability=AppliesCollection.op_Explicit "discontinued 9.2.0" ) )) diff --git a/tests/authoring/Applicability/AppliesToFrontMatter.fs b/tests/authoring/Applicability/AppliesToFrontMatter.fs index 87fcb5426..fe7e1b311 100644 --- a/tests/authoring/Applicability/AppliesToFrontMatter.fs +++ b/tests/authoring/Applicability/AppliesToFrontMatter.fs @@ -45,7 +45,7 @@ applies_to: """ [] let ``apply matches expected`` () = - let expectedAvailability = ApplicabilityOverTime.op_Explicit "ga 9.0.0" + let expectedAvailability = AppliesCollection.op_Explicit "ga 9.0.0" markdown |> appliesTo (ApplicableTo( Serverless=ServerlessProjectApplicability( Elasticsearch=expectedAvailability, @@ -66,9 +66,9 @@ applies_to: let ``apply matches expected`` () = markdown |> appliesTo (ApplicableTo( Serverless=ServerlessProjectApplicability( - Security=ApplicabilityOverTime.op_Explicit "ga 9.0.0", - Elasticsearch=ApplicabilityOverTime.op_Explicit "beta 9.1.0", - Observability=ApplicabilityOverTime.op_Explicit "discontinued 9.2.0" + Security=AppliesCollection.op_Explicit "ga 9.0.0", + Elasticsearch=AppliesCollection.op_Explicit "beta 9.1.0", + Observability=AppliesCollection.op_Explicit "discontinued 9.2.0" ) )) @@ -80,7 +80,7 @@ applies_to: [] let ``apply matches expected`` () = markdown |> appliesTo (ApplicableTo( - Stack=ApplicabilityOverTime.op_Explicit "ga 9.1.0" + Stack=AppliesCollection.op_Explicit "ga 9.1.0" )) type ``parses deployment as string to set all deployment targets`` () = @@ -90,7 +90,7 @@ applies_to: """ [] let ``apply matches expected`` () = - let expectedAvailability = ApplicabilityOverTime.op_Explicit "ga 9.0.0" + let expectedAvailability = AppliesCollection.op_Explicit "ga 9.0.0" markdown |> appliesTo (ApplicableTo( Deployment=DeploymentApplicability( Eck=expectedAvailability, @@ -113,10 +113,10 @@ applies_to: let ``apply matches expected`` () = markdown |> appliesTo (ApplicableTo( Deployment=DeploymentApplicability( - Eck=ApplicabilityOverTime.op_Explicit "ga 9.0", - Ess=ApplicabilityOverTime.op_Explicit "beta 9.1", - Ece=ApplicabilityOverTime.op_Explicit "discontinued 9.2.0", - Self=ApplicabilityOverTime.op_Explicit "unavailable 9.3.0" + Eck=AppliesCollection.op_Explicit "ga 9.0", + Ess=AppliesCollection.op_Explicit "beta 9.1", + Ece=AppliesCollection.op_Explicit "discontinued 9.2.0", + Self=AppliesCollection.op_Explicit "unavailable 9.3.0" ) )) @@ -128,7 +128,7 @@ applies_to: [] let ``apply matches expected`` () = markdown |> appliesTo (ApplicableTo( - Product=ApplicabilityOverTime.op_Explicit "coming 9.5.0" + Product=AppliesCollection.op_Explicit "coming 9.5.0" )) type ``parses product multiple`` () = @@ -139,7 +139,7 @@ applies_to: [] let ``apply matches expected`` () = markdown |> appliesTo (ApplicableTo( - Product=ApplicabilityOverTime([ + Product=AppliesCollection([ Applicability.op_Explicit "coming 9.5"; Applicability.op_Explicit "discontinued 9.7" ] |> Array.ofList) @@ -162,16 +162,16 @@ applies_to: let ``apply matches expected`` () = markdown |> appliesTo (ApplicableTo( Deployment=DeploymentApplicability( - Eck=ApplicabilityOverTime.op_Explicit "ga 9.0", - Ess=ApplicabilityOverTime.op_Explicit "beta 9.1", - Ece=ApplicabilityOverTime.op_Explicit "discontinued 9.2.0", - Self=ApplicabilityOverTime.op_Explicit "unavailable 9.3.0" + Eck=AppliesCollection.op_Explicit "ga 9.0", + Ess=AppliesCollection.op_Explicit "beta 9.1", + Ece=AppliesCollection.op_Explicit "discontinued 9.2.0", + Self=AppliesCollection.op_Explicit "unavailable 9.3.0" ), Serverless=ServerlessProjectApplicability( - Security=ApplicabilityOverTime.op_Explicit "ga 9.0.0", - Elasticsearch=ApplicabilityOverTime.op_Explicit "beta 9.1.0", - Observability=ApplicabilityOverTime.op_Explicit "discontinued 9.2.0" + Security=AppliesCollection.op_Explicit "ga 9.0.0", + Elasticsearch=AppliesCollection.op_Explicit "beta 9.1.0", + Observability=AppliesCollection.op_Explicit "discontinued 9.2.0" ), - Stack=ApplicabilityOverTime.op_Explicit "ga 9.1.0", - Product=ApplicabilityOverTime.op_Explicit "coming 9.5, discontinued 9.7" + Stack=AppliesCollection.op_Explicit "ga 9.1.0", + Product=AppliesCollection.op_Explicit "coming 9.5, discontinued 9.7" )) diff --git a/tests/authoring/Framework/TestValues.fs b/tests/authoring/Framework/TestValues.fs index 6fbd89da9..8399a7f25 100644 --- a/tests/authoring/Framework/TestValues.fs +++ b/tests/authoring/Framework/TestValues.fs @@ -91,7 +91,7 @@ and MarkdownTestContext = // technically we do this work twice since generate all also does it let! document = f.ParseFullAsync(ctx) let! minimal = f.MinimalParseAsync(ctx) - let html = f.CreateHtml(document) + let html = MarkdownFile.CreateHtml(document) return { File = f; Document = document; MinimalParse = minimal; Html = html; Context = this } }) // this is not great code, refactor or depend on FSharp.Control.TaskSeq