From 5b11688a8dc87b3cce15d321e6d73e04bd2088a8 Mon Sep 17 00:00:00 2001 From: Stuart Meeks Date: Fri, 21 Aug 2026 13:42:06 +0000 Subject: [PATCH] =?UTF-8?q?chore:=20adopt=20latest=20standards=20=E2=80=94?= =?UTF-8?q?=20gate=20house=20style=20in-build=20(=C2=A71.2.1,=20=C2=A75.2,?= =?UTF-8?q?=20=C2=A74.4,=20=C2=A72.7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Catches this repo up to the NextIteration.Standards changes landed 2026-08-21, after Splash's 2026-08-20 adoption reached 29/29. No release: stays 0.3.0 and the CHANGELOG entries sit under [Unreleased]. - §1.2.1 EnforceCodeStyleInBuild=true, via the canonical Directory.Build.props. - §5.2 canonical .editorconfig — a deliberate allow-list of named style gates (braces always, var throughout, System-first usings, full naming ruleset), no blanket severity. Byte-identical to the template. - §4.4 codeql.yml gains the query-filters block excluding cs/unmanaged-code and cs/call-to-unmanaged-code. This repo has no P/Invoke, so it matches nothing here; carried so the workflow stays identical to the estate template (§3.0.1). - §2.7 the test project (and the demo, which hits the same conflict) add IDE0005 to NoWarn — it would otherwise hard-error demanding GenerateDocumentationFile once the flag gates it as a warning. Library, test and demo code brought to green under the flag: braces, var, using order, single-line expression bodies in tests, and removal of gratuitous `_ =` discards per the estate house rule. Behaviour-preserving — no public API or rendered output changed; the single-AnsiConsole.Markup render path and its markup escaping are untouched. Build clean at zero warnings; 80 tests (40 × net8.0/net10.0) pass on all three platforms. Co-Authored-By: Claude Opus 4.8 --- .editorconfig | 232 +++++++++++------- .github/workflows/codeql.yml | 14 ++ CHANGELOG.md | 22 ++ Directory.Build.props | 10 +- ...teration.SpectreConsole.Splash.Demo.csproj | 5 + .../Program.cs | 2 + .../Internal/Gradient.cs | 15 +- .../Internal/Renderer.cs | 73 ++++-- .../SplashColors.cs | 2 +- .../SplashScreen.cs | 14 +- .../GradientTests.cs | 6 +- ...eration.SpectreConsole.Splash.Tests.csproj | 9 +- .../QuotesTests.cs | 15 +- .../RendererTests.cs | 2 + .../SplashColorsTests.cs | 16 +- .../SplashScreenTests.cs | 7 +- .../SplashTaglineTests.cs | 11 +- 17 files changed, 297 insertions(+), 158 deletions(-) diff --git a/.editorconfig b/.editorconfig index bd0de72..dcd945c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,19 @@ root = true -# ------------------------------- -# General -# ------------------------------- +# ============================================================ +# Canonical .editorconfig for the NextIteration estate. +# +# Copied verbatim into every governed repo (STANDARD.md §5.2). +# Posture: a DELIBERATE ALLOW-LIST of style gates — there is no +# blanket `dotnet_analyzer_diagnostic.severity`, so a style rule a +# future SDK ships never auto-gates the build. Every rule that IS +# gated below (`:warning`) is a hard build failure under +# `TreatWarningsAsErrors`; that is intentional — the build is what +# forces the code into the ordained style. Code-quality (CA) rules +# keep their `AnalysisLevel=latest` defaults. +# ============================================================ + +# ---------- All files ---------- [*] charset = utf-8 end_of_line = lf @@ -11,99 +22,136 @@ indent_style = space indent_size = 4 trim_trailing_whitespace = true -# ------------------------------- -# C# files -# ------------------------------- -[*.cs] - -indent_size = 4 - -# New lines & braces -csharp_new_line_before_open_brace = all -csharp_prefer_braces = true:warning - -# Using directives -dotnet_sort_system_directives_first = true -dotnet_separate_import_directive_groups = true - -# var usage (Spectre-style: pragmatic) -csharp_style_var_for_built_in_types = true:suggestion -csharp_style_var_when_type_is_apparent = true:suggestion -csharp_style_var_elsewhere = false:suggestion - -# Expression-bodied members (used where clean) -csharp_style_expression_bodied_methods = when_on_single_line:suggestion -csharp_style_expression_bodied_constructors = false:suggestion -csharp_style_expression_bodied_operators = when_on_single_line:suggestion -csharp_style_expression_bodied_properties = when_on_single_line:suggestion - -# Pattern matching / modern C# -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion - -# Nullability helpers -dotnet_style_null_propagation = true:suggestion -dotnet_style_coalesce_expression = true:suggestion - -# Readonly fields -dotnet_style_readonly_field = true:suggestion - -# ------------------------------- -# Naming -# ------------------------------- +[*.{csproj,props,targets}] +indent_size = 2 -# Private fields: _camelCase -dotnet_naming_rule.private_fields_should_be_camel_case.severity = suggestion -dotnet_naming_rule.private_fields_should_be_camel_case.symbols = private_fields -dotnet_naming_rule.private_fields_should_be_camel_case.style = camel_case_with_underscore +[*.{json,yml,yaml}] +indent_size = 2 -dotnet_naming_symbols.private_fields.applicable_kinds = field -dotnet_naming_symbols.private_fields.applicable_accessibilities = private -# A const IS a field, so without this the rule demands `_nonceSize` for -# `private const int NonceSize` — PascalCase constants are correct .NET style and -# the codebase uses them throughout. Restricting the rule to instance fields keeps -# it aimed at what it was written for. Found when EnforceCodeStyleInBuild surfaced -# 76 IDE1006 violations, every one of them a constant. -dotnet_naming_symbols.private_fields.required_modifiers = - -dotnet_naming_style.camel_case_with_underscore.capitalization = camel_case -dotnet_naming_style.camel_case_with_underscore.required_prefix = _ - -# Interfaces: IMyInterface -dotnet_naming_rule.interfaces_should_start_with_i.severity = suggestion -dotnet_naming_rule.interfaces_should_start_with_i.symbols = interfaces -dotnet_naming_rule.interfaces_should_start_with_i.style = interface_prefix +# Trailing whitespace is a hard line break in Markdown +[*.md] +trim_trailing_whitespace = false -dotnet_naming_symbols.interfaces.applicable_kinds = interface +# ---------- C# ---------- +[*.cs] +indent_size = 4 +# Braces & new lines +csharp_new_line_before_open_brace = all +csharp_prefer_braces = true:warning # braces always (IDE0011) + +# Namespaces — block-scoped estate-wide (IDE0160) +csharp_style_namespace_declarations = block_scoped:warning + +# using directives +csharp_using_directive_placement = outside_namespace:warning # IDE0065 +dotnet_sort_system_directives_first = true # feeds IDE0055 (gated below) +dotnet_separate_import_directive_groups = true # feeds IDE0055 — matches estate style + +# 'this.' qualification — never used in this estate +dotnet_style_qualification_for_field = false:warning # IDE0003 +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning + +# Accessibility — always explicit +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning # IDE0040 + +# var — ordained: var everywhere it is legal (IDE0007) +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning +csharp_style_var_elsewhere = true:warning + +# Expression-bodied members — single-line only; block-bodied constructors (IDE0021-0027) +csharp_style_expression_bodied_methods = when_on_single_line:warning +csharp_style_expression_bodied_constructors = false:warning +csharp_style_expression_bodied_operators = when_on_single_line:warning +csharp_style_expression_bodied_properties = when_on_single_line:warning + +# Pattern matching / null handling (IDE0019/0020/0029/0030/0031) +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_pattern_matching_over_as_with_null_check = true:warning +dotnet_style_null_propagation = true:warning +dotnet_style_coalesce_expression = true:warning + +# readonly fields (IDE0044) — only flags never-reassigned fields, so genuinely mutable state is safe +dotnet_style_readonly_field = true:warning + +# Modern syntax — gated toward the modern form +dotnet_style_prefer_collection_expression = when_types_loosely_match:warning # IDE0300+ +csharp_style_implicit_object_creation_when_type_is_apparent = true:warning # IDE0090 + +# Primary constructors — NOT forced. IDE0290 is one-directional (it can only push toward primary +# constructors), and forcing them onto service classes with real initialisation is a downgrade. +# Advisory only; existing class primary constructors are left as they are. +csharp_style_prefer_primary_constructors = false:suggestion # IDE0290 + +# Unused expression/assignment values — never nudge toward `_ =` discards (IDE0058/IDE0059). +# House rule: no discard solely to swallow a return value (see CLAUDE.md for the carve-outs). +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_style_unused_value_assignment_preference = discard_variable:silent + +# ---------- Naming (gated) ---------- +# Matches estate reality: _camelCase private fields (incl. static readonly), PascalCase const +# fields, PascalCase types/members, I-prefixed interfaces, T-prefixed type parameters, camelCase +# locals/parameters. `const_fields` is declared before `private_fields`: a const is a field, so +# both specs match it and precedence decides — const → PascalCase must win. + +# Styles +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_naming_style.camel_case_underscore.required_prefix = _ +dotnet_naming_style.camel_case_underscore.capitalization = camel_case +dotnet_naming_style.camel_case_plain.capitalization = camel_case dotnet_naming_style.interface_prefix.required_prefix = I dotnet_naming_style.interface_prefix.capitalization = pascal_case +dotnet_naming_style.type_param_prefix.required_prefix = T +dotnet_naming_style.type_param_prefix.capitalization = pascal_case -# ------------------------------- -# Analyzers -# ------------------------------- - -# Keep warnings visible but not painful -dotnet_analyzer_diagnostic.severity = warning - -# Unused usings -dotnet_diagnostic.IDE0005.severity = warning - -# Simplification -dotnet_diagnostic.IDE0007.severity = suggestion -dotnet_diagnostic.IDE0008.severity = suggestion - -# Documentation (Spectre.Console is pragmatic here) -dotnet_diagnostic.CS1591.severity = silent - -# ------------------------------- -# JSON / YAML -# ------------------------------- -[*.json] -indent_size = 2 - -[*.yml] -indent_size = 2 - -[*.yaml] -indent_size = 2 \ No newline at end of file +# Symbols +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.type_parameters.applicable_kinds = type_parameter +dotnet_naming_symbols.types.applicable_kinds = class, struct, enum, delegate +dotnet_naming_symbols.non_field_members.applicable_kinds = property, method, event +dotnet_naming_symbols.const_fields.applicable_kinds = field +dotnet_naming_symbols.const_fields.required_modifiers = const +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, private_protected, internal, protected_internal +dotnet_naming_symbols.locals_and_params.applicable_kinds = parameter, local + +# Rules (all warning = gated) +dotnet_naming_rule.interfaces_i.severity = warning +dotnet_naming_rule.interfaces_i.symbols = interfaces +dotnet_naming_rule.interfaces_i.style = interface_prefix + +dotnet_naming_rule.type_params_t.severity = warning +dotnet_naming_rule.type_params_t.symbols = type_parameters +dotnet_naming_rule.type_params_t.style = type_param_prefix + +dotnet_naming_rule.types_pascal.severity = warning +dotnet_naming_rule.types_pascal.symbols = types +dotnet_naming_rule.types_pascal.style = pascal_case + +dotnet_naming_rule.members_pascal.severity = warning +dotnet_naming_rule.members_pascal.symbols = non_field_members +dotnet_naming_rule.members_pascal.style = pascal_case + +dotnet_naming_rule.const_pascal.severity = warning +dotnet_naming_rule.const_pascal.symbols = const_fields +dotnet_naming_rule.const_pascal.style = pascal_case + +dotnet_naming_rule.private_underscore.severity = warning +dotnet_naming_rule.private_underscore.symbols = private_fields +dotnet_naming_rule.private_underscore.style = camel_case_underscore + +dotnet_naming_rule.locals_camel.severity = warning +dotnet_naming_rule.locals_camel.symbols = locals_and_params +dotnet_naming_rule.locals_camel.style = camel_case_plain + +# ---------- Option-less / compiler severities ---------- +dotnet_diagnostic.IDE0005.severity = warning # unnecessary usings (needs GenerateDocumentationFile — set by §1.6) +dotnet_diagnostic.IDE0055.severity = warning # formatting: whitespace, using sort/groups +dotnet_diagnostic.CS1591.severity = warning # public members MUST carry XML docs (STANDARD §1.6, CLAUDE.md non-negotiable) + +# ---------- Deliberate exemptions (advisory, never gate) ---------- +dotnet_diagnostic.IDE0046.severity = suggestion # convert to conditional expression — nested ternaries hurt readability +dotnet_diagnostic.IDE0058.severity = suggestion # unused expression value — see the discard house rule above diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7177a68..4e37bc7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -49,10 +49,24 @@ jobs: # Analyse source only. obj/ and bin/ hold generated and compiled # output — e.g. the xUnit auto-generated entry point — so findings # there are noise against code no human maintains. + # + # query-filters excludes the two audit queries that fire on every + # P/Invoke declaration and call site (cs/unmanaged-code, + # cs/call-to-unmanaged-code). Native-backend packages (Keychain, + # libsecret, DPAPI) exist to call unmanaged code, so these are pure + # noise there and non-native repos have no P/Invoke for them to hit. + # This excludes ONLY those two queries — every other + # security-and-quality query still runs on the interop files, so no + # real finding is lost (STANDARD.md 4.4). config: | paths-ignore: - "**/obj/**" - "**/bin/**" + query-filters: + - exclude: + id: cs/unmanaged-code + - exclude: + id: cs/call-to-unmanaged-code # Explicit build rather than autobuild: these repos multi-target, and # autobuild has picked a single TFM in the past, silently analysing half diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f701f7..c777f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **The house style is now gated in-build.** `EnforceCodeStyleInBuild` is `true` + (§1.2.1) and the canonical `.editorconfig` (§5.2) is a deliberate allow-list of named + style gates — braces always, `var` throughout, `System`-first using order, and the + full naming ruleset — each a hard build failure under `TreatWarningsAsErrors`. The + library, test and demo code was brought to green under the flag: the changes are + brace insertion on single-statement `if`s, explicit-type-to-`var`, `System`-first + using reordering, single-line expression bodies in the tests, and removal of + gratuitous `_ =` discards (the estate house rule — write the call plainly). + Behaviour-preserving; no public API or rendered output changed. This is the per-repo + rollout the standard tracks, not a code redesign — the single `AnsiConsole.Markup` + render path, its markup escaping, and the space fast-path are untouched. - **`Microsoft.SourceLink.GitHub` package reference restored** (§1.7), at the estate-wide version 10.0.400 and with `PrivateAssets="All"` so it is never a consumer dependency. It had been removed as redundant — the .NET 8+ SDK does emit @@ -36,6 +47,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **CodeQL config gained a `query-filters` block** (§4.4) excluding exactly the two + audit queries `cs/unmanaged-code` and `cs/call-to-unmanaged-code`, which fire on every + P/Invoke declaration and call site. This repo has no P/Invoke, so the filter matches + nothing here — it is carried harmlessly so the workflow stays byte-identical to the + estate template (§3.0.1). Every other `security-and-quality` query still runs. +- **Test and demo projects suppress `IDE0005`** (§2.7). `IDE0005` (remove unnecessary + usings) only runs in-build when `GenerateDocumentationFile` is `true`, which both + non-shipping projects set to `false`; once `EnforceCodeStyleInBuild` gates it as a + warning, the build would otherwise hard-error demanding the doc file be enabled. + Suppressing it there resolves the conflict while `IDE0005` still gates the shipping + project, where the doc file is on. - **Central Package Management completed and shared build properties centralised.** `Directory.Packages.props` gains `CentralPackageVersionOverrideEnabled=false`, so a stray inline `Version=` alongside CPM is now a hard build failure instead of being diff --git a/Directory.Build.props b/Directory.Build.props index 8447737..1480ced 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,16 +13,18 @@ no XML docs, and TreatWarningsAsErrors would otherwise fail the build over every missing one. - EnforceCodeStyleInBuild is deliberately absent. STANDARD.md 1.2.1 is blocked: - TreatWarningsAsErrors promotes every advisory .editorconfig preference to a - hard failure, which produced 490 build errors in Auth. It needs a per-rule - gate-versus-advisory decision before it can be added here. + EnforceCodeStyleInBuild is on (STANDARD.md 1.2.1). It runs the IDE analyzers + in-build, so the canonical .editorconfig (5.2) actually gates the house style + rather than merely documenting it. The blanket severity that once turned every + advisory preference into an error (490 in Auth) is gone; .editorconfig is now a + deliberate allow-list of named gates. See 1.2.1 for the per-rule decisions. --> enable enable en latest + true true true true diff --git a/demo/NextIteration.SpectreConsole.Splash.Demo/NextIteration.SpectreConsole.Splash.Demo.csproj b/demo/NextIteration.SpectreConsole.Splash.Demo/NextIteration.SpectreConsole.Splash.Demo.csproj index 0bc124c..57d386f 100644 --- a/demo/NextIteration.SpectreConsole.Splash.Demo/NextIteration.SpectreConsole.Splash.Demo.csproj +++ b/demo/NextIteration.SpectreConsole.Splash.Demo/NextIteration.SpectreConsole.Splash.Demo.csproj @@ -8,6 +8,11 @@ XML docs, and TreatWarningsAsErrors would fail the build over every missing one. Same reasoning as the test project (STANDARD.md 2.7). --> false + + $(NoWarn);IDE0005 diff --git a/demo/NextIteration.SpectreConsole.Splash.Demo/Program.cs b/demo/NextIteration.SpectreConsole.Splash.Demo/Program.cs index 768e781..7de4215 100644 --- a/demo/NextIteration.SpectreConsole.Splash.Demo/Program.cs +++ b/demo/NextIteration.SpectreConsole.Splash.Demo/Program.cs @@ -1,5 +1,7 @@ using Figgle.Fonts; + using NextIteration.SpectreConsole.Splash; + using Spectre.Console; // Demo 1 — all defaults (Roman font, neutral blue gradient, random built-in tagline). diff --git a/src/NextIteration.SpectreConsole.Splash/Internal/Gradient.cs b/src/NextIteration.SpectreConsole.Splash/Internal/Gradient.cs index c4fee40..10d0eb8 100644 --- a/src/NextIteration.SpectreConsole.Splash/Internal/Gradient.cs +++ b/src/NextIteration.SpectreConsole.Splash/Internal/Gradient.cs @@ -17,11 +17,14 @@ internal static class Gradient /// public static Color[] Generate(IReadOnlyList hexStops, int width) { - if (width <= 0) return []; + if (width <= 0) + { + return []; + } // Convert hex → RGB once. var stops = new (byte R, byte G, byte B)[hexStops.Count]; - for (int i = 0; i < hexStops.Count; i++) + for (var i = 0; i < hexStops.Count; i++) { stops[i] = HexToRgb(hexStops[i]); } @@ -50,11 +53,15 @@ public static Color[] Generate(IReadOnlyList hexStops, int width) var segments = stops.Length - 1; var step = (float)segments / (width - 1); - for (int i = 0; i < width; i++) + for (var i = 0; i < width; i++) { var t = i * step; var segment = (int)t; - if (segment >= segments) segment = segments - 1; + if (segment >= segments) + { + segment = segments - 1; + } + var fraction = t - segment; var a = stops[segment]; diff --git a/src/NextIteration.SpectreConsole.Splash/Internal/Renderer.cs b/src/NextIteration.SpectreConsole.Splash/Internal/Renderer.cs index c97cc5e..b3473f4 100644 --- a/src/NextIteration.SpectreConsole.Splash/Internal/Renderer.cs +++ b/src/NextIteration.SpectreConsole.Splash/Internal/Renderer.cs @@ -1,6 +1,7 @@ -using Spectre.Console; using System.Text; +using Spectre.Console; + namespace NextIteration.SpectreConsole.Splash.Internal { /// @@ -37,10 +38,13 @@ public static string Render(string logo, Color[] gradient, string? tagline) // dependent; splitting on '\n' and trimming '\r' handles both. var lines = logo.Split('\n'); var maxLineLength = 0; - for (int i = 0; i < lines.Length; i++) + for (var i = 0; i < lines.Length; i++) { lines[i] = lines[i].TrimEnd('\r'); - if (lines[i].Length > maxLineLength) maxLineLength = lines[i].Length; + if (lines[i].Length > maxLineLength) + { + maxLineLength = lines[i].Length; + } } // Rough upper-bound allocation: each char gets ~16 chars of @@ -48,7 +52,7 @@ public static string Render(string logo, Color[] gradient, string? tagline) var sb = new StringBuilder(capacity: logo.Length * 16); // Blank line before the logo so there's breathing room. - _ = sb.AppendLine(); + sb.AppendLine(); RenderLogo(sb, lines, gradient); @@ -64,19 +68,19 @@ private static void RenderLogo(StringBuilder sb, string[] lines, Color[] gradien { foreach (var line in lines) { - for (int i = 0; i < line.Length; i++) + for (var i = 0; i < line.Length; i++) { var ch = line[i]; if (ch == ' ') { // Spaces don't need a colour escape; saves ~14 chars // of markup per space character. - _ = sb.Append(' '); + sb.Append(' '); } else { var colour = gradient[Math.Min(i, gradient.Length - 1)]; - _ = sb.Append('[') + sb.Append('[') .Append('#').Append(colour.R.ToString("X2", System.Globalization.CultureInfo.InvariantCulture)) .Append(colour.G.ToString("X2", System.Globalization.CultureInfo.InvariantCulture)) .Append(colour.B.ToString("X2", System.Globalization.CultureInfo.InvariantCulture)) @@ -84,23 +88,38 @@ private static void RenderLogo(StringBuilder sb, string[] lines, Color[] gradien // Markup escape: characters `[` and `]` in the // visible logo would otherwise be parsed as markup. // Figgle glyphs can contain both, so escape always. - if (ch == '[') _ = sb.Append("[["); - else if (ch == ']') _ = sb.Append("]]"); - else _ = sb.Append(ch); - _ = sb.Append("[/]"); + if (ch == '[') + { + sb.Append("[["); + } + else if (ch == ']') + { + sb.Append("]]"); + } + else + { + sb.Append(ch); + } + sb.Append("[/]"); } } - _ = sb.AppendLine(); + sb.AppendLine(); } } private static void RenderTagline(StringBuilder sb, string tagline, int maxLineLength, Color[] gradient) { var words = tagline.Split(' ', StringSplitOptions.RemoveEmptyEntries); - if (words.Length == 0) return; + if (words.Length == 0) + { + return; + } var maxWidth = maxLineLength - MinTaglinePadding * 2; - if (maxWidth < 1) maxWidth = 1; + if (maxWidth < 1) + { + maxWidth = 1; + } // Word-wrap to maxWidth. Greedy: pack as many words per line // as fit, break before the word that wouldn't fit. @@ -112,13 +131,21 @@ private static void RenderTagline(StringBuilder sb, string tagline, int maxLineL if (current.Length > 0 && current.Length + spaceNeeded + word.Length > maxWidth) { taglineLines.Add(current.ToString()); - _ = current.Clear(); + current.Clear(); spaceNeeded = 0; } - if (spaceNeeded > 0) _ = current.Append(' '); - _ = current.Append(word); + if (spaceNeeded > 0) + { + current.Append(' '); + } + + current.Append(word); + } + + if (current.Length > 0) + { + taglineLines.Add(current.ToString()); } - if (current.Length > 0) taglineLines.Add(current.ToString()); // Render each line centred within the logo width, coloured // with the gradient's midpoint (visually anchored to the @@ -133,18 +160,22 @@ private static void RenderTagline(StringBuilder sb, string tagline, int maxLineL foreach (var line in taglineLines) { var padding = Math.Max(MinTaglinePadding, (maxLineLength - line.Length) / 2); - _ = sb.Append(colourHex) + sb.Append(colourHex) .Append(EscapeMarkup(line.PadLeft(line.Length + padding))) .AppendLine("[/]"); } - _ = sb.AppendLine(); + sb.AppendLine(); } // Spectre's markup parser treats `[` and `]` as control chars; doubled forms are literals. private static string EscapeMarkup(string text) { - if (text.IndexOfAny(['[', ']']) < 0) return text; + if (text.IndexOfAny(['[', ']']) < 0) + { + return text; + } + return text.Replace("[", "[[").Replace("]", "]]"); } } diff --git a/src/NextIteration.SpectreConsole.Splash/SplashColors.cs b/src/NextIteration.SpectreConsole.Splash/SplashColors.cs index 3f3cc2d..d6bfd4c 100644 --- a/src/NextIteration.SpectreConsole.Splash/SplashColors.cs +++ b/src/NextIteration.SpectreConsole.Splash/SplashColors.cs @@ -54,7 +54,7 @@ private static void ValidateHex(string hex) throw new ArgumentException( $"Hex colour must be in '#RRGGBB' form, got '{hex}'.", nameof(hex)); } - for (int i = 1; i < 7; i++) + for (var i = 1; i < 7; i++) { var c = hex[i]; var isHex = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); diff --git a/src/NextIteration.SpectreConsole.Splash/SplashScreen.cs b/src/NextIteration.SpectreConsole.Splash/SplashScreen.cs index bfd9e32..e44c4ff 100644 --- a/src/NextIteration.SpectreConsole.Splash/SplashScreen.cs +++ b/src/NextIteration.SpectreConsole.Splash/SplashScreen.cs @@ -1,4 +1,5 @@ using NextIteration.SpectreConsole.Splash.Internal; + using Spectre.Console; namespace NextIteration.SpectreConsole.Splash @@ -58,19 +59,26 @@ private static int MaxLineLength(string logo) { var max = 0; var start = 0; - for (int i = 0; i < logo.Length; i++) + for (var i = 0; i < logo.Length; i++) { if (logo[i] == '\n') { var end = i > start && logo[i - 1] == '\r' ? i - 1 : i; var len = end - start; - if (len > max) max = len; + if (len > max) + { + max = len; + } + start = i + 1; } } // Trailing line without newline. var tailLen = logo.Length - start; - if (tailLen > max) max = tailLen; + if (tailLen > max) + { + max = tailLen; + } return max; } } diff --git a/tests/NextIteration.SpectreConsole.Splash.Tests/GradientTests.cs b/tests/NextIteration.SpectreConsole.Splash.Tests/GradientTests.cs index c66ec06..f4f012f 100644 --- a/tests/NextIteration.SpectreConsole.Splash.Tests/GradientTests.cs +++ b/tests/NextIteration.SpectreConsole.Splash.Tests/GradientTests.cs @@ -1,4 +1,5 @@ using NextIteration.SpectreConsole.Splash.Internal; + using Xunit; namespace NextIteration.SpectreConsole.Splash.Tests @@ -90,7 +91,10 @@ public void Large_stop_count_still_linear_time() // Regression guard: 20 stops, 200 cols = 4000 ops, must still // produce coherent output (endpoints anchored). var stops = new string[20]; - for (int i = 0; i < 20; i++) stops[i] = i % 2 == 0 ? "#000000" : "#FFFFFF"; + for (var i = 0; i < 20; i++) + { + stops[i] = i % 2 == 0 ? "#000000" : "#FFFFFF"; + } var result = Gradient.Generate(stops, 200); diff --git a/tests/NextIteration.SpectreConsole.Splash.Tests/NextIteration.SpectreConsole.Splash.Tests.csproj b/tests/NextIteration.SpectreConsole.Splash.Tests/NextIteration.SpectreConsole.Splash.Tests.csproj index 8f03bba..fe08d0c 100644 --- a/tests/NextIteration.SpectreConsole.Splash.Tests/NextIteration.SpectreConsole.Splash.Tests.csproj +++ b/tests/NextIteration.SpectreConsole.Splash.Tests/NextIteration.SpectreConsole.Splash.Tests.csproj @@ -22,8 +22,15 @@ CA2007 (ConfigureAwait) doesn't apply in test contexts; there is no SynchronizationContext to recapture. + + IDE0005 (remove unnecessary usings) only runs in-build when + GenerateDocumentationFile is true, which is false here. Once + EnforceCodeStyleInBuild is on (§1.2.1) the canonical .editorconfig + gates IDE0005 as a warning, so a test project would hard-error + demanding the doc file be enabled. Suppressing it resolves the + conflict; IDE0005 still gates the shipping project. STANDARD.md 2.7. --> - $(NoWarn);CA1707;CA1515;CA2007 + $(NoWarn);CA1707;CA1515;CA2007;IDE0005 diff --git a/tests/NextIteration.SpectreConsole.Splash.Tests/QuotesTests.cs b/tests/NextIteration.SpectreConsole.Splash.Tests/QuotesTests.cs index 94c46ef..7bd1a07 100644 --- a/tests/NextIteration.SpectreConsole.Splash.Tests/QuotesTests.cs +++ b/tests/NextIteration.SpectreConsole.Splash.Tests/QuotesTests.cs @@ -1,17 +1,15 @@ using NextIteration.SpectreConsole.Splash.Internal; + using Xunit; namespace NextIteration.SpectreConsole.Splash.Tests { public class QuotesTests { + // Rough lower bound — the whole point of the built-in pool is + // that the same quote shouldn't appear twice in quick succession. [Fact] - public void Pool_has_at_least_100_entries() - { - // Rough lower bound — the whole point of the built-in pool is - // that the same quote shouldn't appear twice in quick succession. - Assert.True(Quotes.Count >= 100, $"Expected >= 100 quotes, got {Quotes.Count}"); - } + public void Pool_has_at_least_100_entries() => Assert.True(Quotes.Count >= 100, $"Expected >= 100 quotes, got {Quotes.Count}"); [Fact] public void No_null_or_whitespace_quotes() @@ -35,7 +33,10 @@ public void Random_eventually_returns_different_values() // Not strictly guaranteed, but 500 calls from a ~300-entry pool // with Random.Shared collisions only would be astronomically rare. var seen = new HashSet(); - for (int i = 0; i < 500; i++) seen.Add(Quotes.Random()); + for (var i = 0; i < 500; i++) + { + seen.Add(Quotes.Random()); + } Assert.True(seen.Count > 1, "Random() returned the same value 500 times in a row"); } diff --git a/tests/NextIteration.SpectreConsole.Splash.Tests/RendererTests.cs b/tests/NextIteration.SpectreConsole.Splash.Tests/RendererTests.cs index bc3c5d4..9faaacc 100644 --- a/tests/NextIteration.SpectreConsole.Splash.Tests/RendererTests.cs +++ b/tests/NextIteration.SpectreConsole.Splash.Tests/RendererTests.cs @@ -1,5 +1,7 @@ using NextIteration.SpectreConsole.Splash.Internal; + using Spectre.Console; + using Xunit; namespace NextIteration.SpectreConsole.Splash.Tests diff --git a/tests/NextIteration.SpectreConsole.Splash.Tests/SplashColorsTests.cs b/tests/NextIteration.SpectreConsole.Splash.Tests/SplashColorsTests.cs index 51bed95..41399b8 100644 --- a/tests/NextIteration.SpectreConsole.Splash.Tests/SplashColorsTests.cs +++ b/tests/NextIteration.SpectreConsole.Splash.Tests/SplashColorsTests.cs @@ -1,4 +1,5 @@ using NextIteration.SpectreConsole.Splash; + using Xunit; namespace NextIteration.SpectreConsole.Splash.Tests @@ -6,16 +7,10 @@ namespace NextIteration.SpectreConsole.Splash.Tests public class SplashColorsTests { [Fact] - public void Ctor_rejects_empty_stop_list() - { - Assert.Throws(() => new SplashColors()); - } + public void Ctor_rejects_empty_stop_list() => Assert.Throws(() => new SplashColors()); [Fact] - public void Ctor_rejects_null() - { - Assert.Throws(() => new SplashColors(null!)); - } + public void Ctor_rejects_null() => Assert.Throws(() => new SplashColors(null!)); [Theory] [InlineData("#GGGGGG")] // non-hex digit @@ -23,10 +18,7 @@ public void Ctor_rejects_null() [InlineData("#12345678")] // too long [InlineData("123456")] // missing '#' [InlineData("")] // empty - public void Ctor_rejects_malformed_hex(string bad) - { - Assert.Throws(() => new SplashColors(bad)); - } + public void Ctor_rejects_malformed_hex(string bad) => Assert.Throws(() => new SplashColors(bad)); [Theory] [InlineData("#000000")] diff --git a/tests/NextIteration.SpectreConsole.Splash.Tests/SplashScreenTests.cs b/tests/NextIteration.SpectreConsole.Splash.Tests/SplashScreenTests.cs index ba8083a..97e1219 100644 --- a/tests/NextIteration.SpectreConsole.Splash.Tests/SplashScreenTests.cs +++ b/tests/NextIteration.SpectreConsole.Splash.Tests/SplashScreenTests.cs @@ -1,6 +1,8 @@ using NextIteration.SpectreConsole.Splash; + using Spectre.Console; using Spectre.Console.Testing; + using Xunit; namespace NextIteration.SpectreConsole.Splash.Tests @@ -8,10 +10,7 @@ namespace NextIteration.SpectreConsole.Splash.Tests public class SplashScreenTests { [Fact] - public void Show_with_null_options_throws() - { - Assert.Throws(() => SplashScreen.Show((SplashOptions)null!)); - } + public void Show_with_null_options_throws() => Assert.Throws(() => SplashScreen.Show((SplashOptions)null!)); [Fact] public void Show_with_empty_appName_throws() diff --git a/tests/NextIteration.SpectreConsole.Splash.Tests/SplashTaglineTests.cs b/tests/NextIteration.SpectreConsole.Splash.Tests/SplashTaglineTests.cs index 38dcd58..647f820 100644 --- a/tests/NextIteration.SpectreConsole.Splash.Tests/SplashTaglineTests.cs +++ b/tests/NextIteration.SpectreConsole.Splash.Tests/SplashTaglineTests.cs @@ -1,4 +1,5 @@ using NextIteration.SpectreConsole.Splash; + using Xunit; namespace NextIteration.SpectreConsole.Splash.Tests @@ -6,10 +7,7 @@ namespace NextIteration.SpectreConsole.Splash.Tests public class SplashTaglineTests { [Fact] - public void None_resolves_to_null() - { - Assert.Null(SplashTagline.None.Resolve()); - } + public void None_resolves_to_null() => Assert.Null(SplashTagline.None.Resolve()); [Fact] public void RandomBuiltIn_resolves_to_a_non_empty_string() @@ -36,9 +34,6 @@ public void FromProvider_allows_null_result() } [Fact] - public void FromProvider_rejects_null_callback() - { - Assert.Throws(() => SplashTagline.FromProvider(null!)); - } + public void FromProvider_rejects_null_callback() => Assert.Throws(() => SplashTagline.FromProvider(null!)); } }