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/CHANGELOG.md b/CHANGELOG.md
index 9636530..61689e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
+- **Adopted the revised canonical `.editorconfig` and enabled `EnforceCodeStyleInBuild`** (NextIteration.Standards §5.2, §1.2.1 — the latter now a `MUST`). The canonical file is a deliberate allow-list of gated style rules rather than a blanket `dotnet_analyzer_diagnostic.severity`, so a style rule a future SDK ships never auto-gates the build. With the flag on, the gated rules fail the build under `TreatWarningsAsErrors` instead of merely showing in the IDE. Bringing the code green was a mechanical, behaviour-preserving reformat of 92 sites — braces on all single-statement `if`s (IDE0011, 64 of them), collection expressions (IDE0300/IDE0301/IDE0028), `var` usage, two expression-bodied members, one simplified null check, and five unnecessary usings — applied with `dotnet format` plus the collection-expression sites it cannot fix automatically. All 392 tests (196 × `net8.0`/`net10.0`) pass unchanged, and the build stays at zero warnings.
+- **`IDE0005` suppressed in the non-shipping projects.** It only runs in-build when `GenerateDocumentationFile` is `true`, which both the test and demo projects set to `false` — sample and fixture code carries no XML docs, and `TreatWarningsAsErrors` would fail the build over every missing one. With `EnforceCodeStyleInBuild` on it hard-errors demanding the doc file be enabled instead. NextIteration.Standards §2.7 mandates this for the test project; the demo is the same shape (not shipped, so not public surface) and needs the same opt-out. `IDE0005` still gates the shipping project, where the doc file is on — and it caught five real unnecessary usings there.
+
- **`release.yml` folded into `ci.yml`.** Publishing now happens in the same workflow run as the build, so the `publish` job pushes the artifact this run's `build` job produced — the exact bytes the gate tested. The old tag-triggered `release.yml` rebuilt from the tag and published an artifact no gate had ever seen. It also globbed `*.nupkg` when uploading, so the `.snupkg` was built and then silently never published; the glob is now `*nupkg` and symbols ship. Repointing the nuget.org Trusted Publishing policy from `release.yml` to `ci.yml` was part of the same change, because the policy is bound to a workflow filename.
- **CI now has a single aggregating gate job, `ci`, and it is the only required status check.** `build` and `test` were required directly before, which couples the branch ruleset to the matrix: `test`'s check names carry the matrix values, so adding or dropping a platform broke protection. The gate declares `needs: [build, test]` with `if: always()` and fails on any upstream result that is not success — including `skipped`, which branch protection would otherwise read as satisfied.
- **Every workflow declares `concurrency`, explicit `permissions`, and per-job `timeout-minutes`.** Superseded pushes cancel instead of stacking up, except on tags — a half-cancelled release can leave an incomplete package set on nuget.org. NuGet restore is cached on `~/.nuget/packages`.
diff --git a/Directory.Build.props b/Directory.Build.props
index 65a7154..1480ced 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,15 +1,30 @@
+
enable
enable
en
latest
+ true
true
true
true
@@ -24,4 +39,5 @@
Stuart Meeks
Next Iteration
+
diff --git a/demo/NextIteration.SpectreConsole.SelfUpdate.Demo/NextIteration.SpectreConsole.SelfUpdate.Demo.csproj b/demo/NextIteration.SpectreConsole.SelfUpdate.Demo/NextIteration.SpectreConsole.SelfUpdate.Demo.csproj
index 25222a7..11f042b 100644
--- a/demo/NextIteration.SpectreConsole.SelfUpdate.Demo/NextIteration.SpectreConsole.SelfUpdate.Demo.csproj
+++ b/demo/NextIteration.SpectreConsole.SelfUpdate.Demo/NextIteration.SpectreConsole.SelfUpdate.Demo.csproj
@@ -12,6 +12,16 @@
the test project (STANDARD.md 2.7).
-->
false
+
+ $(NoWarn);IDE0005
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/CommandConfiguratorExtensions.cs b/src/NextIteration.SpectreConsole.SelfUpdate/CommandConfiguratorExtensions.cs
index e35c059..da52b75 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/CommandConfiguratorExtensions.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/CommandConfiguratorExtensions.cs
@@ -1,4 +1,5 @@
using NextIteration.SpectreConsole.SelfUpdate.Commands;
+
using Spectre.Console.Cli;
namespace NextIteration.SpectreConsole.SelfUpdate
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/Commands/UpdateCommand.cs b/src/NextIteration.SpectreConsole.SelfUpdate/Commands/UpdateCommand.cs
index bdea55c..c8db644 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/Commands/UpdateCommand.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/Commands/UpdateCommand.cs
@@ -194,12 +194,10 @@ private Task PromptForConflictAsync(UpdateConflict con
return Task.FromResult(keep ? UpdateConflictResolution.KeepExisting : UpdateConflictResolution.UseNew);
}
- private static bool IsUpdateAvailable(string current, string latestTag)
- {
- // Defer to the same comparator the checker uses so behaviour is
- // identical between the cached probe and this fresh one.
- return Pipeline.UpdateChecker.IsNewer(current, latestTag);
- }
+ // Defer to the same comparator the checker uses so behaviour is
+ // identical between the cached probe and this fresh one.
+ private static bool IsUpdateAvailable(string current, string latestTag) =>
+ Pipeline.UpdateChecker.IsNewer(current, latestTag);
private static string StageLabel(UpdateStage stage) => stage switch
{
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/SelfUpdater.cs b/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/SelfUpdater.cs
index cfcb59c..68e74cb 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/SelfUpdater.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/SelfUpdater.cs
@@ -51,12 +51,9 @@ public Task InstallAsync(
public async Task InstallAsync(IProgress? progress = null, CancellationToken ct = default)
{
- var release = await _source.GetLatestAsync(_options.Channel, ct).ConfigureAwait(false);
- if (release is null)
- {
- throw new UpdateException(
+ var release = await _source.GetLatestAsync(_options.Channel, ct).ConfigureAwait(false)
+ ?? throw new UpdateException(
"No release is available from the configured update source. The source either returned null or is currently unreachable.");
- }
await _installer.InstallAsync(release, progress, onConflict: null, ct).ConfigureAwait(false);
}
}
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateCacheFile.cs b/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateCacheFile.cs
index 209e7d4..2263e09 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateCacheFile.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateCacheFile.cs
@@ -22,7 +22,11 @@ internal static class UpdateCacheFile
{
try
{
- if (!File.Exists(path)) return null;
+ if (!File.Exists(path))
+ {
+ return null;
+ }
+
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize(json, JsonOpts);
}
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateChecker.cs b/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateChecker.cs
index 64c2234..7491f0c 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateChecker.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateChecker.cs
@@ -50,11 +50,21 @@ internal UpdateChecker(
public async Task CheckAsync(bool? includePrereleasesOverride, CancellationToken ct = default)
{
- if (IsOptOutSet()) return null;
+ if (IsOptOutSet())
+ {
+ return null;
+ }
var current = GetCurrentVersion();
- if (current is null) return null;
- if (IsVersionSkipped(current)) return null;
+ if (current is null)
+ {
+ return null;
+ }
+
+ if (IsVersionSkipped(current))
+ {
+ return null;
+ }
var effectivePrerelease = includePrereleasesOverride ?? _options.IncludePrereleases;
var cachePath = ResolveCacheFilePath();
@@ -70,7 +80,10 @@ internal UpdateChecker(
linked.CancelAfter(_options.CheckTimeout);
var release = await _source.GetLatestAsync(_options.Channel, includePrereleasesOverride, linked.Token).ConfigureAwait(false);
- if (release is null) return null;
+ if (release is null)
+ {
+ return null;
+ }
UpdateCacheFile.TryWrite(cachePath, new UpdateCacheEntry(
CheckedAt: _utcNow(),
@@ -94,7 +107,11 @@ internal UpdateChecker(
public string? GetCurrentVersion()
{
var v = _currentVersionResolver();
- if (string.IsNullOrWhiteSpace(v)) return null;
+ if (string.IsNullOrWhiteSpace(v))
+ {
+ return null;
+ }
+
return StripBuildMetadata(v);
}
@@ -106,7 +123,11 @@ internal bool IsVersionSkipped(string version) =>
internal bool IsOptOutSet()
{
var name = ResolveSkipEnvVarName();
- if (string.IsNullOrWhiteSpace(name)) return false;
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ return false;
+ }
+
return string.Equals(_envResolver(name), "1", StringComparison.Ordinal);
}
@@ -132,12 +153,23 @@ internal bool IsCacheFresh(UpdateCacheEntry? entry) =>
internal bool IsCacheFresh(UpdateCacheEntry? entry, bool effectivePrerelease)
{
- if (entry is null) return false;
- if (!string.Equals(entry.Channel, _options.Channel, StringComparison.Ordinal)) return false;
+ if (entry is null)
+ {
+ return false;
+ }
+
+ if (!string.Equals(entry.Channel, _options.Channel, StringComparison.Ordinal))
+ {
+ return false;
+ }
// Pre-0.1.4 cache entries have no IncludePrereleases field — they
// were written before the override existed, so they always
// reflect a non-prerelease answer.
- if ((entry.IncludePrereleases ?? false) != effectivePrerelease) return false;
+ if ((entry.IncludePrereleases ?? false) != effectivePrerelease)
+ {
+ return false;
+ }
+
return (_utcNow() - entry.CheckedAt) < _options.CacheTtl;
}
@@ -159,8 +191,15 @@ internal static bool IsNewer(string current, string latestTag)
return false;
}
- if (lv > cv) return true;
- if (lv < cv) return false;
+ if (lv > cv)
+ {
+ return true;
+ }
+
+ if (lv < cv)
+ {
+ return false;
+ }
// Numeric versions equal — compare prereleases. Semver: a release
// without a prerelease is newer than one with a prerelease at the
@@ -172,9 +211,20 @@ internal static bool IsNewer(string current, string latestTag)
internal static int ComparePrerelease(string? current, string? latest)
{
- if (current is null && latest is null) return 0;
- if (current is null) return 1; // no-prerelease > prerelease, so current is newer
- if (latest is null) return -1; // current has prerelease, latest does not → current is older
+ if (current is null && latest is null)
+ {
+ return 0;
+ }
+
+ if (current is null)
+ {
+ return 1; // no-prerelease > prerelease, so current is newer
+ }
+
+ if (latest is null)
+ {
+ return -1; // current has prerelease, latest does not → current is older
+ }
// Semver §11: compare dot-separated identifiers left to right.
// Numeric identifiers compare numerically; numeric is always lower
@@ -187,7 +237,10 @@ internal static int ComparePrerelease(string? current, string? latest)
for (var i = 0; i < shared; i++)
{
var cmp = ComparePrereleaseIdentifier(cParts[i], lParts[i]);
- if (cmp != 0) return cmp;
+ if (cmp != 0)
+ {
+ return cmp;
+ }
}
return cParts.Length.CompareTo(lParts.Length);
}
@@ -197,22 +250,42 @@ private static int ComparePrereleaseIdentifier(string current, string latest)
var cNumeric = long.TryParse(current, out var cn);
var lNumeric = long.TryParse(latest, out var ln);
- if (cNumeric && lNumeric) return cn.CompareTo(ln);
- if (cNumeric) return -1; // numeric identifiers rank lower than alphanumeric
- if (lNumeric) return 1;
+ if (cNumeric && lNumeric)
+ {
+ return cn.CompareTo(ln);
+ }
+
+ if (cNumeric)
+ {
+ return -1; // numeric identifiers rank lower than alphanumeric
+ }
+
+ if (lNumeric)
+ {
+ return 1;
+ }
+
return string.CompareOrdinal(current, latest);
}
internal static (string Numeric, string? Prerelease) SplitNumericPrerelease(string version)
{
var dash = version.IndexOf('-', StringComparison.Ordinal);
- if (dash < 0) return (version, null);
+ if (dash < 0)
+ {
+ return (version, null);
+ }
+
return (version[..dash], version[(dash + 1)..]);
}
internal static string StripLeadingV(string version)
{
- if (string.IsNullOrEmpty(version)) return string.Empty;
+ if (string.IsNullOrEmpty(version))
+ {
+ return string.Empty;
+ }
+
return version[0] is 'v' or 'V' ? version[1..] : version;
}
@@ -224,7 +297,11 @@ internal static string StripBuildMetadata(string version)
internal static string ComputeDefaultSkipEnvVarName(string appName)
{
- if (string.IsNullOrWhiteSpace(appName)) return string.Empty;
+ if (string.IsNullOrWhiteSpace(appName))
+ {
+ return string.Empty;
+ }
+
var sb = new StringBuilder(appName.Length + "_SKIP_UPDATE_CHECK".Length);
foreach (var c in appName)
{
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateInstaller.cs b/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateInstaller.cs
index 1c7cced..ab6fc51 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateInstaller.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/Pipeline/UpdateInstaller.cs
@@ -1,5 +1,3 @@
-using System.Linq;
-
namespace NextIteration.SpectreConsole.SelfUpdate.Pipeline
{
///
@@ -165,7 +163,11 @@ internal async Task DownloadAssetAsync(ReleaseAsset asset, string destinationPat
await using var fileStream = File.Create(destinationPath);
var downloadProgress = new Progress(dp =>
{
- if (progress is null) return;
+ if (progress is null)
+ {
+ return;
+ }
+
if (dp.TotalBytes is { } total && total > 0)
{
progress.Report(new UpdateProgressEvent(
@@ -193,7 +195,7 @@ internal static string ResolveSourceDirectory(string extractedDirectory)
// don't need preserve-path or conflict-resolution semantics.
// Equivalent to SwapAsync(..., preservePaths: empty, onConflict: null).
internal static void Swap(string sourceDirectory, string installDirectory, string oldDirectory) =>
- SwapAsync(sourceDirectory, installDirectory, oldDirectory, Array.Empty(), onConflict: null, CancellationToken.None)
+ SwapAsync(sourceDirectory, installDirectory, oldDirectory, [], onConflict: null, CancellationToken.None)
.GetAwaiter().GetResult();
internal static async Task SwapAsync(
@@ -220,8 +222,15 @@ internal static async Task SwapAsync(
foreach (var entry in Directory.EnumerateFileSystemEntries(installDirectory))
{
var name = Path.GetFileName(entry);
- if (IsMaintenanceEntry(name)) continue;
- if (IsPreserved(name, preservePaths)) continue;
+ if (IsMaintenanceEntry(name))
+ {
+ continue;
+ }
+
+ if (IsPreserved(name, preservePaths))
+ {
+ continue;
+ }
var dest = Path.Combine(oldDirectory, name);
if (File.Exists(entry))
@@ -274,8 +283,15 @@ internal static async Task SwapAsync(
// rollback restores the right thing on failure.
var oldDest = Path.Combine(oldDirectory, name);
TryDeleteEntry(oldDest);
- if (File.Exists(dest)) File.Move(dest, oldDest);
- else if (Directory.Exists(dest)) Directory.Move(dest, oldDest);
+ if (File.Exists(dest))
+ {
+ File.Move(dest, oldDest);
+ }
+ else if (Directory.Exists(dest))
+ {
+ Directory.Move(dest, oldDest);
+ }
+
movedNames.Add(name);
}
// else: a new release introduces a path that the
@@ -325,17 +341,28 @@ private static async Task ResolveConflictAsync(
internal static bool IsPreserved(string name, IReadOnlyList preservePaths)
{
- if (preservePaths.Count == 0) return false;
+ if (preservePaths.Count == 0)
+ {
+ return false;
+ }
+
foreach (var pattern in preservePaths)
{
- if (string.IsNullOrWhiteSpace(pattern)) continue;
+ if (string.IsNullOrWhiteSpace(pattern))
+ {
+ continue;
+ }
// Take the part of the pattern before the first slash —
// this lets `data/**`, `data/seed.json`, and bare `data`
// all match the top-level entry `data`. Nested-only
// preservation (e.g. preserve only `data/seed.json` but
// not the rest of `data/`) is out of scope for v0.1.x.
var head = TopLevelSegment(pattern);
- if (head.Length == 0) continue;
+ if (head.Length == 0)
+ {
+ continue;
+ }
+
if (System.IO.Enumeration.FileSystemName.MatchesSimpleExpression(head, name, ignoreCase: true))
{
return true;
@@ -349,7 +376,10 @@ private static ReadOnlySpan TopLevelSegment(string pattern)
ReadOnlySpan span = pattern;
for (var i = 0; i < span.Length; i++)
{
- if (span[i] == '/' || span[i] == '\\') return span[..i];
+ if (span[i] == '/' || span[i] == '\\')
+ {
+ return span[..i];
+ }
}
return span;
}
@@ -389,8 +419,14 @@ private static void TryDeleteEntry(string path)
{
try
{
- if (File.Exists(path)) File.Delete(path);
- else if (Directory.Exists(path)) DeleteDirectoryRobustly(path);
+ if (File.Exists(path))
+ {
+ File.Delete(path);
+ }
+ else if (Directory.Exists(path))
+ {
+ DeleteDirectoryRobustly(path);
+ }
}
catch
{
@@ -417,7 +453,10 @@ internal static void DeleteDirectoryRobustly(
Action? deleter = null,
Action? sleeper = null)
{
- if (!Directory.Exists(path)) return;
+ if (!Directory.Exists(path))
+ {
+ return;
+ }
deleter ??= Directory.Delete;
sleeper ??= Thread.Sleep;
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/Resolution/DefaultAssetResolver.cs b/src/NextIteration.SpectreConsole.SelfUpdate/Resolution/DefaultAssetResolver.cs
index 66fe80f..ca7eb5c 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/Resolution/DefaultAssetResolver.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/Resolution/DefaultAssetResolver.cs
@@ -23,7 +23,7 @@ namespace NextIteration.SpectreConsole.SelfUpdate.Resolution
///
public sealed class DefaultAssetResolver : IAssetResolver
{
- private static readonly string[] ArchiveExtensions = { ".tar.gz", ".tgz", ".zip" };
+ private static readonly string[] ArchiveExtensions = [".tar.gz", ".tgz", ".zip"];
private readonly string _appName;
@@ -46,7 +46,10 @@ public DefaultAssetResolver(string appName)
foreach (var rid in CandidateRids(runtimeIdentifier))
{
var match = ResolveForRid(release, rid);
- if (match is not null) return match;
+ if (match is not null)
+ {
+ return match;
+ }
}
return null;
}
@@ -58,19 +61,31 @@ public DefaultAssetResolver(string appName)
// 1. {app}-v{ver}-{rid}.ext
var match = MatchExact(release, $"{_appName}-{versionWithV}-{rid}");
- if (match is not null) return match;
+ if (match is not null)
+ {
+ return match;
+ }
// 2. {app}-{ver}-{rid}.ext (in case tag is unprefixed)
match = MatchExact(release, $"{_appName}-{versionNumeric}-{rid}");
- if (match is not null) return match;
+ if (match is not null)
+ {
+ return match;
+ }
// 3. {app}-{rid}.ext (no version segment)
match = MatchExact(release, $"{_appName}-{rid}");
- if (match is not null) return match;
+ if (match is not null)
+ {
+ return match;
+ }
// 4. {app}…-{rid}.ext (loose: starts with app, ends with -rid+ext)
match = MatchPrefixSuffix(release, $"{_appName}-", $"-{rid}");
- if (match is not null) return match;
+ if (match is not null)
+ {
+ return match;
+ }
// 5. *…-{rid}.ext (RID-only — last resort, may be ambiguous)
match = MatchSuffixOnly(release, $"-{rid}");
@@ -97,7 +112,10 @@ private static IEnumerable CandidateRids(string runtimeIdentifier)
{
foreach (var ext in ArchiveExtensions)
{
- if (NameEquals(asset.Name, stem + ext)) return asset;
+ if (NameEquals(asset.Name, stem + ext))
+ {
+ return asset;
+ }
}
}
return null;
@@ -107,8 +125,16 @@ private static IEnumerable CandidateRids(string runtimeIdentifier)
{
foreach (var asset in release.Assets)
{
- if (!asset.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) continue;
- if (!EndsWithRidAndArchive(asset.Name, ridSuffix)) continue;
+ if (!asset.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ if (!EndsWithRidAndArchive(asset.Name, ridSuffix))
+ {
+ continue;
+ }
+
return asset;
}
return null;
@@ -118,7 +144,10 @@ private static IEnumerable CandidateRids(string runtimeIdentifier)
{
foreach (var asset in release.Assets)
{
- if (EndsWithRidAndArchive(asset.Name, ridSuffix)) return asset;
+ if (EndsWithRidAndArchive(asset.Name, ridSuffix))
+ {
+ return asset;
+ }
}
return null;
}
@@ -127,7 +156,10 @@ private static bool EndsWithRidAndArchive(string name, string ridSuffix)
{
foreach (var ext in ArchiveExtensions)
{
- if (name.EndsWith(ridSuffix + ext, StringComparison.OrdinalIgnoreCase)) return true;
+ if (name.EndsWith(ridSuffix + ext, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
}
return false;
}
@@ -137,7 +169,11 @@ private static bool NameEquals(string actual, string expected) =>
private static string StripLeadingV(string tag)
{
- if (string.IsNullOrEmpty(tag)) return string.Empty;
+ if (string.IsNullOrEmpty(tag))
+ {
+ return string.Empty;
+ }
+
return tag[0] is 'v' or 'V' ? tag[1..] : tag;
}
}
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/RuntimeIdentifier.cs b/src/NextIteration.SpectreConsole.SelfUpdate/RuntimeIdentifier.cs
index a01f0f0..ca338fa 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/RuntimeIdentifier.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/RuntimeIdentifier.cs
@@ -33,9 +33,21 @@ public static string Detect()
private static string OsToken()
{
- if (OperatingSystem.IsWindows()) return "win";
- if (OperatingSystem.IsLinux()) return "linux";
- if (OperatingSystem.IsMacOS()) return "osx";
+ if (OperatingSystem.IsWindows())
+ {
+ return "win";
+ }
+
+ if (OperatingSystem.IsLinux())
+ {
+ return "linux";
+ }
+
+ if (OperatingSystem.IsMacOS())
+ {
+ return "osx";
+ }
+
throw new PlatformNotSupportedException(
$"Unsupported operating system: {RuntimeInformation.OSDescription}");
}
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/SelfUpdaterOptions.cs b/src/NextIteration.SpectreConsole.SelfUpdate/SelfUpdaterOptions.cs
index 423a3cf..df0104b 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/SelfUpdaterOptions.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/SelfUpdaterOptions.cs
@@ -138,7 +138,7 @@ public sealed class SelfUpdaterOptions
/// the consumer wouldn't want to lose across an upgrade.
///
///
- public IReadOnlyList PreservePaths { get; set; } = Array.Empty();
+ public IReadOnlyList PreservePaths { get; set; } = [];
// ---------- Source registration ----------
@@ -258,10 +258,7 @@ public void UseAssetResolver(Func resolver
/// built-in SHA-256 check entirely.
///
public void AddVerifier()
- where TVerifier : class, IPackageVerifier
- {
- ExtraVerifierTypes.Add(typeof(TVerifier));
- }
+ where TVerifier : class, IPackageVerifier => ExtraVerifierTypes.Add(typeof(TVerifier));
///
/// Add an additional built by the
@@ -289,8 +286,8 @@ public void AddVerifier(Func factory)
internal Func? AssetResolverFactory { get; private set; }
internal Func? AssetResolverFunc { get; private set; }
- internal List ExtraVerifierTypes { get; } = new();
- internal List> ExtraVerifierFactories { get; } = new();
+ internal List ExtraVerifierTypes { get; } = [];
+ internal List> ExtraVerifierFactories { get; } = [];
}
internal enum UpdateSourceKind
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/Sources/GhCliReleaseSource.cs b/src/NextIteration.SpectreConsole.SelfUpdate/Sources/GhCliReleaseSource.cs
index 4b2f0f9..bd12edd 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/Sources/GhCliReleaseSource.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/Sources/GhCliReleaseSource.cs
@@ -1,4 +1,3 @@
-using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -86,12 +85,11 @@ internal GhCliReleaseSource(
if (channel is null && !includePrereleases)
{
var stdout = await _runner(
- new[]
- {
+ [
"release", "view",
"--json", "tagName,name,url,publishedAt,isDraft,isPrerelease,assets",
"--repo", _repository,
- },
+ ],
DefaultViewTimeout,
ct).ConfigureAwait(false);
var dto = JsonSerializer.Deserialize(stdout, JsonOpts);
@@ -104,16 +102,15 @@ internal GhCliReleaseSource(
// status. Only request fields used for filtering / sort here;
// the full detail (incl. url, assets) is fetched per-tag below.
var listJson = await _runner(
- new[]
- {
+ [
"release", "list",
"--json", "tagName,publishedAt,isDraft,isPrerelease",
"--limit", "30",
"--repo", _repository,
- },
+ ],
DefaultListTimeout,
ct).ConfigureAwait(false);
- var releases = JsonSerializer.Deserialize(listJson, JsonOpts) ?? Array.Empty();
+ var releases = JsonSerializer.Deserialize(listJson, JsonOpts) ?? [];
var match = releases
.Where(r => !r.IsDraft)
.Where(r => includePrereleases || !r.IsPrerelease)
@@ -121,17 +118,19 @@ internal GhCliReleaseSource(
|| (r.TagName ?? string.Empty).Contains($"-{channel}", StringComparison.OrdinalIgnoreCase))
.OrderByDescending(r => r.PublishedAt)
.FirstOrDefault();
- if (match is null) return null;
+ if (match is null)
+ {
+ return null;
+ }
// `release list` doesn't return assets — fetch the matched
// release in detail so DownloadAssetAsync has something to act on.
var detailJson = await _runner(
- new[]
- {
+ [
"release", "view", match.TagName!,
"--json", "tagName,name,url,publishedAt,isDraft,isPrerelease,assets",
"--repo", _repository,
- },
+ ],
DefaultViewTimeout,
ct).ConfigureAwait(false);
var detail = JsonSerializer.Deserialize(detailJson, JsonOpts);
@@ -166,14 +165,13 @@ public async Task DownloadAssetAsync(ReleaseAsset asset, Stream destination, IPr
progress?.Report(new DownloadProgress(0, asset.SizeBytes));
await _runner(
- new[]
- {
+ [
"release", "download", tag,
"--repo", _repository,
"--pattern", asset.Name,
"--output", tempFile,
"--clobber",
- },
+ ],
DefaultDownloadTimeout,
ct).ConfigureAwait(false);
@@ -192,7 +190,13 @@ await _runner(
private static void TryDelete(string path)
{
- try { if (File.Exists(path)) File.Delete(path); }
+ try
+ {
+ if (File.Exists(path))
+ {
+ File.Delete(path);
+ }
+ }
catch
{
// Best effort.
@@ -201,10 +205,13 @@ private static void TryDelete(string path)
private static RemoteRelease? Convert(GhReleaseDto dto, string? channel)
{
- if (string.IsNullOrWhiteSpace(dto.TagName)) return null;
+ if (string.IsNullOrWhiteSpace(dto.TagName))
+ {
+ return null;
+ }
var tag = dto.TagName!;
- var assets = (dto.Assets ?? Array.Empty())
+ var assets = (dto.Assets ?? [])
.Where(a => !string.IsNullOrWhiteSpace(a.Name))
.Select(a => new ReleaseAsset(
Name: a.Name!,
@@ -214,7 +221,7 @@ private static void TryDelete(string path)
Metadata: BuildAssetMetadata(tag)))
.ToArray();
- Uri? notes = TryParseUri(dto.Url);
+ var notes = TryParseUri(dto.Url);
var resolvedChannel = dto.IsPrerelease ? (channel ?? "prerelease") : channel;
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/Sources/HttpGitHubReleaseSource.cs b/src/NextIteration.SpectreConsole.SelfUpdate/Sources/HttpGitHubReleaseSource.cs
index 2d81fc1..1a30a0c 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/Sources/HttpGitHubReleaseSource.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/Sources/HttpGitHubReleaseSource.cs
@@ -1,4 +1,3 @@
-using System.Linq;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -102,7 +101,10 @@ internal HttpGitHubReleaseSource(
}
var releases = await GetJsonAsync(http, $"repos/{_repository}/releases?per_page=30", ct).ConfigureAwait(false);
- if (releases is null || releases.Length == 0) return null;
+ if (releases is null || releases.Length == 0)
+ {
+ return null;
+ }
var match = releases
.Where(r => !r.Draft)
@@ -171,7 +173,11 @@ private void ConfigureRequestHeaders(HttpClient http)
private string? ResolveToken()
{
- if (!string.IsNullOrWhiteSpace(_explicitToken)) return _explicitToken;
+ if (!string.IsNullOrWhiteSpace(_explicitToken))
+ {
+ return _explicitToken;
+ }
+
return Environment.GetEnvironmentVariable("GITHUB_TOKEN")
?? Environment.GetEnvironmentVariable("GH_TOKEN");
}
@@ -180,7 +186,11 @@ private void ConfigureRequestHeaders(HttpClient http)
{
var url = new Uri(_apiBase, relativePath);
using var resp = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
- if (resp.StatusCode == System.Net.HttpStatusCode.NotFound) return default;
+ if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
+ {
+ return default;
+ }
+
resp.EnsureSuccessStatusCode();
await using var stream = await resp.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync(stream, JsonOpts, ct).ConfigureAwait(false);
@@ -188,9 +198,12 @@ private void ConfigureRequestHeaders(HttpClient http)
private static RemoteRelease? Convert(GitHubReleaseDto dto, string? channel)
{
- if (string.IsNullOrWhiteSpace(dto.TagName)) return null;
+ if (string.IsNullOrWhiteSpace(dto.TagName))
+ {
+ return null;
+ }
- var assets = (dto.Assets ?? Array.Empty())
+ var assets = (dto.Assets ?? [])
.Where(a => !string.IsNullOrWhiteSpace(a.Name) && !string.IsNullOrWhiteSpace(a.Url))
.Select(a => new ReleaseAsset(
Name: a.Name!,
@@ -200,7 +213,7 @@ private void ConfigureRequestHeaders(HttpClient http)
Metadata: BuildAssetMetadata(a)))
.ToArray();
- Uri? notes = string.IsNullOrWhiteSpace(dto.HtmlUrl) ? null : new Uri(dto.HtmlUrl);
+ var notes = string.IsNullOrWhiteSpace(dto.HtmlUrl) ? null : new Uri(dto.HtmlUrl);
// Sources should report channel based on what they observed —
// forward the caller's channel filter as the resolved channel
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/Sources/HttpManifestSource.cs b/src/NextIteration.SpectreConsole.SelfUpdate/Sources/HttpManifestSource.cs
index 5a19a8e..36a02af 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/Sources/HttpManifestSource.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/Sources/HttpManifestSource.cs
@@ -1,4 +1,3 @@
-using System.Linq;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -111,11 +110,17 @@ private static bool IsHttps(Uri uri) =>
{
using var http = _httpClientFactory.CreateClient();
using var resp = await http.GetAsync(_manifestUrl, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
- if (!resp.IsSuccessStatusCode) return null;
+ if (!resp.IsSuccessStatusCode)
+ {
+ return null;
+ }
await using var stream = await resp.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
var dto = await JsonSerializer.DeserializeAsync(stream, JsonOpts, ct).ConfigureAwait(false);
- if (dto is null || string.IsNullOrWhiteSpace(dto.Tag)) return null;
+ if (dto is null || string.IsNullOrWhiteSpace(dto.Tag))
+ {
+ return null;
+ }
if (channel is not null
&& !string.IsNullOrWhiteSpace(dto.Channel)
@@ -170,7 +175,7 @@ public async Task DownloadAssetAsync(ReleaseAsset asset, Stream destination, IPr
private static RemoteRelease Convert(ManifestDto dto, string? channel)
{
- var assets = (dto.Assets ?? Array.Empty())
+ var assets = (dto.Assets ?? [])
.Where(a => !string.IsNullOrWhiteSpace(a.Name) && !string.IsNullOrWhiteSpace(a.Url))
.Select(a => new ReleaseAsset(
Name: a.Name!,
@@ -180,7 +185,7 @@ private static RemoteRelease Convert(ManifestDto dto, string? channel)
Metadata: BuildMetadata(a)))
.ToArray();
- Uri? notes = string.IsNullOrWhiteSpace(dto.ReleaseNotesUrl)
+ var notes = string.IsNullOrWhiteSpace(dto.ReleaseNotesUrl)
? null
: new Uri(dto.ReleaseNotesUrl!, UriKind.Absolute);
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/UpdateBanner.cs b/src/NextIteration.SpectreConsole.SelfUpdate/UpdateBanner.cs
index 13190b2..e9dc02d 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/UpdateBanner.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/UpdateBanner.cs
@@ -68,7 +68,10 @@ public static void RenderIfAvailable(
{
return;
}
- if (info is null || !info.IsUpdateAvailable) return;
+ if (info is null || !info.IsUpdateAvailable)
+ {
+ return;
+ }
var ansi = console ?? AnsiConsole.Console;
if (info.ReleaseUrl is not null)
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/Verification/Sha256ChecksumVerifier.cs b/src/NextIteration.SpectreConsole.SelfUpdate/Verification/Sha256ChecksumVerifier.cs
index 907c2c7..b2e7a89 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/Verification/Sha256ChecksumVerifier.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/Verification/Sha256ChecksumVerifier.cs
@@ -1,4 +1,3 @@
-using System.Linq;
using System.Security.Cryptography;
using System.Text;
@@ -27,13 +26,13 @@ namespace NextIteration.SpectreConsole.SelfUpdate.Verification
public sealed class Sha256ChecksumVerifier : IPackageVerifier
{
private static readonly string[] ManifestNames =
- {
+ [
"SHA256SUMS.txt",
"SHA256SUMS",
"sha256sums.txt",
"sha256sums",
"checksums.txt",
- };
+ ];
private readonly IUpdateSource _source;
@@ -57,15 +56,9 @@ public async Task VerifyAsync(string downloadedFilePath, RemoteRelease release,
ArgumentNullException.ThrowIfNull(release);
ArgumentNullException.ThrowIfNull(asset);
- var expected = TryReadMetadataSha256(asset)
- ?? await TryFetchManifestSha256Async(release, asset, ct).ConfigureAwait(false);
-
- if (expected is null)
- {
- throw new UpdateException(
+ var expected = (TryReadMetadataSha256(asset)
+ ?? await TryFetchManifestSha256Async(release, asset, ct).ConfigureAwait(false)) ?? throw new UpdateException(
$"SHA-256 hash for '{asset.Name}' is not available. The asset's metadata does not include a 'sha256' entry, and no SHA256SUMS.txt asset was found on release '{release.Tag}'.");
- }
-
var actual = await ComputeSha256Async(downloadedFilePath, ct).ConfigureAwait(false);
if (!string.Equals(expected, actual, StringComparison.OrdinalIgnoreCase))
{
@@ -89,7 +82,10 @@ public async Task VerifyAsync(string downloadedFilePath, RemoteRelease release,
{
var manifest = release.Assets.FirstOrDefault(a =>
ManifestNames.Contains(a.Name, StringComparer.OrdinalIgnoreCase));
- if (manifest is null) return null;
+ if (manifest is null)
+ {
+ return null;
+ }
using var ms = new MemoryStream();
await _source.DownloadAssetAsync(manifest, ms, progress: null, ct).ConfigureAwait(false);
diff --git a/src/NextIteration.SpectreConsole.SelfUpdate/Verification/Sha256SumsManifest.cs b/src/NextIteration.SpectreConsole.SelfUpdate/Verification/Sha256SumsManifest.cs
index 9638f97..c691da6 100644
--- a/src/NextIteration.SpectreConsole.SelfUpdate/Verification/Sha256SumsManifest.cs
+++ b/src/NextIteration.SpectreConsole.SelfUpdate/Verification/Sha256SumsManifest.cs
@@ -9,7 +9,7 @@ namespace NextIteration.SpectreConsole.SelfUpdate.Verification
///
internal static class Sha256SumsManifest
{
- private static readonly char[] FieldSeparators = { ' ', '\t' };
+ private static readonly char[] FieldSeparators = [' ', '\t'];
public static IReadOnlyDictionary Parse(string content)
{
@@ -19,16 +19,29 @@ public static IReadOnlyDictionary Parse(string content)
foreach (var rawLine in content.Split('\n'))
{
var line = rawLine.Trim();
- if (line.Length == 0 || line.StartsWith('#')) continue;
+ if (line.Length == 0 || line.StartsWith('#'))
+ {
+ continue;
+ }
var split = line.IndexOfAny(FieldSeparators);
- if (split <= 0) continue;
+ if (split <= 0)
+ {
+ continue;
+ }
var hex = line[..split].Trim();
var name = line[(split + 1)..].Trim().TrimStart('*');
- if (hex.Length != 64 || name.Length == 0) continue;
- if (!IsHex(hex)) continue;
+ if (hex.Length != 64 || name.Length == 0)
+ {
+ continue;
+ }
+
+ if (!IsHex(hex))
+ {
+ continue;
+ }
result[name] = hex.ToLowerInvariant();
}
@@ -40,7 +53,10 @@ private static bool IsHex(string s)
foreach (var c in s)
{
var ok = c is >= '0' and <= '9' || c is >= 'a' and <= 'f' || c is >= 'A' and <= 'F';
- if (!ok) return false;
+ if (!ok)
+ {
+ return false;
+ }
}
return true;
}
diff --git a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/CommandConfiguratorExtensionsTests.cs b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/CommandConfiguratorExtensionsTests.cs
index c9a2ec5..9afddfa 100644
--- a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/CommandConfiguratorExtensionsTests.cs
+++ b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/CommandConfiguratorExtensionsTests.cs
@@ -13,9 +13,9 @@ namespace NextIteration.SpectreConsole.SelfUpdate.Tests
{
public sealed class CommandConfiguratorExtensionsTests
{
- private static readonly string[] HelpArgs = { "--help" };
- private static readonly string[] UpdateHelpArgs = { "update", "--help" };
- private static readonly string[] OtaHelpArgs = { "ota", "--help" };
+ private static readonly string[] HelpArgs = ["--help"];
+ private static readonly string[] UpdateHelpArgs = ["update", "--help"];
+ private static readonly string[] OtaHelpArgs = ["ota", "--help"];
[Fact]
public async Task AddUpdateCommand_with_default_name_registers_update_command()
diff --git a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Commands/UpdateCommandTests.cs b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Commands/UpdateCommandTests.cs
index c256761..458bbd4 100644
--- a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Commands/UpdateCommandTests.cs
+++ b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Commands/UpdateCommandTests.cs
@@ -19,14 +19,14 @@ public sealed class UpdateCommandTests
Tag: "v1.4.2",
Channel: null,
ReleaseNotesUrl: new Uri("https://example.com/r/v1.4.2"),
- Assets: Array.Empty(),
+ Assets: [],
PublishedAt: DateTimeOffset.UtcNow);
private static readonly RemoteRelease ReleaseV100 = new(
Tag: "v1.0.0",
Channel: null,
ReleaseNotesUrl: null,
- Assets: Array.Empty(),
+ Assets: [],
PublishedAt: DateTimeOffset.UtcNow);
[Fact]
diff --git a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/FakeHttpHandler.cs b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/FakeHttpHandler.cs
index 85b3da2..573fe42 100644
--- a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/FakeHttpHandler.cs
+++ b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/FakeHttpHandler.cs
@@ -10,7 +10,7 @@ namespace NextIteration.SpectreConsole.SelfUpdate.Tests.Infrastructure
internal sealed class FakeHttpHandler : HttpMessageHandler
{
public Func? Responder { get; set; }
- public List Requests { get; } = new();
+ public List Requests { get; } = [];
protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
@@ -38,7 +38,11 @@ public static HttpResponseMessage Bytes(byte[] payload, HttpStatusCode status =
internal sealed class FakeHttpClientFactory : IHttpClientFactory
{
private readonly HttpMessageHandler _handler;
- public FakeHttpClientFactory(HttpMessageHandler handler) => _handler = handler;
+ public FakeHttpClientFactory(HttpMessageHandler handler)
+ {
+ _handler = handler;
+ }
+
public HttpClient CreateClient(string name) => new(_handler, disposeHandler: false);
}
}
diff --git a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/RecordingProgress.cs b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/RecordingProgress.cs
index af0aa6a..cd093d0 100644
--- a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/RecordingProgress.cs
+++ b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/RecordingProgress.cs
@@ -19,7 +19,7 @@ namespace NextIteration.SpectreConsole.SelfUpdate.Tests.Infrastructure
///
internal sealed class RecordingProgress : IProgress
{
- private readonly List _reports = new();
+ private readonly List _reports = [];
/// Everything reported so far, as a point-in-time copy.
public IReadOnlyList Snapshot
@@ -28,7 +28,7 @@ public IReadOnlyList Snapshot
{
lock (_reports)
{
- return _reports.ToArray();
+ return [.. _reports];
}
}
}
diff --git a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/TempDir.cs b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/TempDir.cs
index 98730bb..ada1106 100644
--- a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/TempDir.cs
+++ b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/TempDir.cs
@@ -19,7 +19,7 @@ public TempDir(string? prefix = null)
Directory.CreateDirectory(Path);
}
- public string Combine(params string[] parts) => System.IO.Path.Combine(new[] { Path }.Concat(parts).ToArray());
+ public string Combine(params string[] parts) => System.IO.Path.Combine([Path, .. parts]);
public void Dispose()
{
diff --git a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/TestRegistrar.cs b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/TestRegistrar.cs
index 5e315df..94c8eb5 100644
--- a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/TestRegistrar.cs
+++ b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/Infrastructure/TestRegistrar.cs
@@ -37,7 +37,10 @@ private sealed class TestResolver : ITypeResolver
{
private readonly IServiceProvider _provider;
- public TestResolver(IServiceProvider provider) => _provider = provider;
+ public TestResolver(IServiceProvider provider)
+ {
+ _provider = provider;
+ }
public object? Resolve(Type? type) => type is null ? null : _provider.GetService(type);
}
diff --git a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/NextIteration.SpectreConsole.SelfUpdate.Tests.csproj b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/NextIteration.SpectreConsole.SelfUpdate.Tests.csproj
index e0263f8..282884e 100644
--- a/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/NextIteration.SpectreConsole.SelfUpdate.Tests.csproj
+++ b/tests/NextIteration.SpectreConsole.SelfUpdate.Tests/NextIteration.SpectreConsole.SelfUpdate.Tests.csproj
@@ -21,8 +21,14 @@
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 this project sets to
+ false. With EnforceCodeStyleInBuild on it would hard-error
+ demanding the doc file be enabled. It still gates the shipping
+ project, where the doc file is on. STANDARD.md 2.7.
-->
- $(NoWarn);CA1707;CA1515;CA2007
+ $(NoWarn);CA1707;CA1515;CA2007;IDE0005