From 98bcfc5daa42387b118b40696a83efc7b44c0b48 Mon Sep 17 00:00:00 2001 From: ancplua Date: Sat, 9 May 2026 12:22:33 +0200 Subject: [PATCH 01/27] fix: enforce generated analyzer config ordering --- .editorconfig | 2 +- Directory.Build.props | 14 +++- ...ANcpLua.NET.Sdk.SingleFileApp.editorconfig | 4 +- src/Config/ANcpLua.NET.Sdk.Web.editorconfig | 4 +- .../Analyzer.ANcpLua.Analyzers.editorconfig | 5 +- ...rosoft.CodeAnalysis.Analyzers.editorconfig | 6 +- ...deAnalysis.BannedApiAnalyzers.editorconfig | 6 +- ...oft.CodeAnalysis.NetAnalyzers.editorconfig | 31 +------- src/Config/CodingStyle.editorconfig | 23 ++---- src/Config/Compiler.editorconfig | 6 +- src/Config/Global.editorconfig | 2 +- src/Config/NamingConvention.editorconfig | 2 +- .../Helpers/SdkProjectBuilder.cs | 11 +-- tests/ANcpLua.Sdk.Tests/MtpDetectionTests.cs | 13 +++- tests/ANcpLua.Sdk.Tests/SdkTests.cs | 11 ++- .../SourceGeneratorDefaultsTests.cs | 5 ++ tests/ANcpLua.Sdk.Tests/TemplatesTests.cs | 32 ++++---- tools/ConfigFilesGenerator/Program.cs | 78 ++++++++++++++++--- 18 files changed, 153 insertions(+), 102 deletions(-) diff --git a/.editorconfig b/.editorconfig index fb21635..3937b59 100644 --- a/.editorconfig +++ b/.editorconfig @@ -42,7 +42,7 @@ insert_final_newline = true [*.cs] indent_size = 4 insert_final_newline = true -charset = utf-8-bom +charset = utf-8 # C# naming conventions live in src/Config/NamingConvention.editorconfig # (single source of truth, dotnet/runtime aligned, shipped with the SDK package). diff --git a/Directory.Build.props b/Directory.Build.props index 1c2f2cb..9d2ddab 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,14 @@ latest enable + enable true + true + latest-all + + true + true + true @@ -24,9 +31,10 @@ - + - \ No newline at end of file + diff --git a/src/Config/ANcpLua.NET.Sdk.SingleFileApp.editorconfig b/src/Config/ANcpLua.NET.Sdk.SingleFileApp.editorconfig index 3517c56..d186572 100644 --- a/src/Config/ANcpLua.NET.Sdk.SingleFileApp.editorconfig +++ b/src/Config/ANcpLua.NET.Sdk.SingleFileApp.editorconfig @@ -1,6 +1,6 @@ -# global_level must be higher than the base editorconfig files +# global_level must be higher than the base SDK-shipped globals is_global = true -global_level = 1 +global_level = 61 # MA0047: Declare types in namespaces dotnet_diagnostic.MA0047.severity = none diff --git a/src/Config/ANcpLua.NET.Sdk.Web.editorconfig b/src/Config/ANcpLua.NET.Sdk.Web.editorconfig index 28f52c7..4064d76 100644 --- a/src/Config/ANcpLua.NET.Sdk.Web.editorconfig +++ b/src/Config/ANcpLua.NET.Sdk.Web.editorconfig @@ -1,6 +1,6 @@ -# global_level must be higher than the base editorconfig files +# global_level must be higher than the base SDK-shipped globals is_global = true -global_level = 1 +global_level = 60 # CA1002: Do not expose generic lists dotnet_diagnostic.CA1002.severity = none diff --git a/src/Config/Analyzer.ANcpLua.Analyzers.editorconfig b/src/Config/Analyzer.ANcpLua.Analyzers.editorconfig index 8b15e36..d98ea36 100644 --- a/src/Config/Analyzer.ANcpLua.Analyzers.editorconfig +++ b/src/Config/Analyzer.ANcpLua.Analyzers.editorconfig @@ -1,6 +1,7 @@ -# global_level must be higher than the NET Analyzer files +# global_level controls precedence across multiple global AnalyzerConfig files. +# Keep distinct values to avoid equal-level conflicts (which are ignored). is_global = true -global_level = 0 +global_level = 13 # AL0001: Prohibit reassignment of primary constructor parameters # Help link: https://github.com/ANcpLua/ANcpLua.Analyzers/blob/main/docs/AL0001.md diff --git a/src/Config/Analyzer.Microsoft.CodeAnalysis.Analyzers.editorconfig b/src/Config/Analyzer.Microsoft.CodeAnalysis.Analyzers.editorconfig index 2ee6e53..8018850 100644 --- a/src/Config/Analyzer.Microsoft.CodeAnalysis.Analyzers.editorconfig +++ b/src/Config/Analyzer.Microsoft.CodeAnalysis.Analyzers.editorconfig @@ -1,6 +1,7 @@ -# global_level must be higher than the NET Analyzer files +# global_level controls precedence across multiple global AnalyzerConfig files. +# Keep distinct values to avoid equal-level conflicts (which are ignored). is_global = true -global_level = 0 +global_level = 10 # Microsoft.CodeAnalysis.Analyzers - no configurable rules @@ -224,4 +225,3 @@ dotnet_diagnostic.RS2007.severity = warning # Help link: https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md # Enabled: True, Severity: warning dotnet_diagnostic.RS2008.severity = warning - diff --git a/src/Config/Analyzer.Microsoft.CodeAnalysis.BannedApiAnalyzers.editorconfig b/src/Config/Analyzer.Microsoft.CodeAnalysis.BannedApiAnalyzers.editorconfig index bb6ac84..1d738bb 100644 --- a/src/Config/Analyzer.Microsoft.CodeAnalysis.BannedApiAnalyzers.editorconfig +++ b/src/Config/Analyzer.Microsoft.CodeAnalysis.BannedApiAnalyzers.editorconfig @@ -1,6 +1,7 @@ -# global_level must be higher than the NET Analyzer files +# global_level controls precedence across multiple global AnalyzerConfig files. +# Keep distinct values to avoid equal-level conflicts (which are ignored). is_global = true -global_level = 0 +global_level = 12 # RS0030: Do not use banned APIs # Help link: https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md @@ -15,4 +16,3 @@ dotnet_diagnostic.RS0031.severity = warning # RS0035: External access to internal symbols outside the restricted namespace(s) is prohibited # Enabled: True, Severity: error dotnet_diagnostic.RS0035.severity = error - diff --git a/src/Config/Analyzer.Microsoft.CodeAnalysis.NetAnalyzers.editorconfig b/src/Config/Analyzer.Microsoft.CodeAnalysis.NetAnalyzers.editorconfig index 19cdd83..d2f0b71 100644 --- a/src/Config/Analyzer.Microsoft.CodeAnalysis.NetAnalyzers.editorconfig +++ b/src/Config/Analyzer.Microsoft.CodeAnalysis.NetAnalyzers.editorconfig @@ -1,6 +1,7 @@ -# global_level must be higher than the NET Analyzer files +# global_level controls precedence across multiple global AnalyzerConfig files. +# Keep distinct values to avoid equal-level conflicts (which are ignored). is_global = true -global_level = 0 +global_level = 11 dotnet_diagnostic.CA2217.api_surface = all @@ -400,11 +401,6 @@ dotnet_diagnostic.CA1515.severity = none # Enabled: True, Severity: suggestion dotnet_diagnostic.CA1516.severity = suggestion -# CA1517: Use 'ReadOnlySpan' or 'ReadOnlyMemory' instead of 'Span' or 'Memory' -# Help link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1517 -# Enabled: True, Severity: suggestion -dotnet_diagnostic.CA1517.severity = suggestion - # CA1700: Do not name enum values 'Reserved' # Help link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1700 # Enabled: False, Severity: warning @@ -807,16 +803,6 @@ dotnet_diagnostic.CA1874.severity = suggestion # Enabled: True, Severity: suggestion dotnet_diagnostic.CA1875.severity = suggestion -# CA1876: Do not use 'AsParallel' in 'foreach' -# Help link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1876 -# Enabled: True, Severity: suggestion -dotnet_diagnostic.CA1876.severity = suggestion - -# CA1877: Collapse consecutive Path.Combine or Path.Join operations -# Help link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1877 -# Enabled: True, Severity: suggestion -dotnet_diagnostic.CA1877.severity = suggestion - # CA2000: Dispose objects before losing scope # Help link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2000 # Enabled: False, Severity: warning @@ -918,16 +904,6 @@ dotnet_diagnostic.CA2024.severity = warning # Enabled: False, Severity: warning dotnet_diagnostic.CA2025.severity = warning -# CA2026: Prefer JsonElement.Parse over JsonDocument.Parse().RootElement -# Help link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2026 -# Enabled: True, Severity: suggestion -dotnet_diagnostic.CA2026.severity = suggestion - -# CA2027: Cancel Task.Delay after Task.WhenAny completes -# Help link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2027 -# Enabled: True, Severity: suggestion -dotnet_diagnostic.CA2027.severity = suggestion - # CA2100: Review SQL queries for security vulnerabilities # Help link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2100 # Enabled: False, Severity: warning @@ -1627,4 +1603,3 @@ dotnet_diagnostic.CA5404.severity = warning # Help link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5405 # Enabled: False, Severity: warning dotnet_diagnostic.CA5405.severity = warning - diff --git a/src/Config/CodingStyle.editorconfig b/src/Config/CodingStyle.editorconfig index 142c3ed..29c409f 100644 --- a/src/Config/CodingStyle.editorconfig +++ b/src/Config/CodingStyle.editorconfig @@ -1,11 +1,9 @@ is_global = true -global_level = 0 +global_level = 30 -charset = utf-8-bom -indent_style = space -indent_size = 4 -insert_final_newline = true -trim_trailing_whitespace = true +# Note: global AnalyzerConfig files aren't intended for editor-only formatting +# settings (indent size, trimming whitespace, encoding, ...). Keep those in a +# regular repo-level `.editorconfig` instead. # New line preferences csharp_new_line_before_open_brace = all @@ -25,7 +23,7 @@ csharp_indent_switch_labels = true csharp_indent_labels = one_less_than_current # Modifier preferences -csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async # avoid this. unless absolutely necessary dotnet_style_qualification_for_field = false @@ -33,17 +31,13 @@ dotnet_style_qualification_for_property = false dotnet_style_qualification_for_method = false dotnet_style_qualification_for_event = false -# Types: use keywords instead of BCL types, and permit var only when the type is clear +# Types: use keywords instead of BCL types; prefer `var` consistently csharp_style_var_for_built_in_types = true csharp_style_var_when_type_is_apparent = true csharp_style_var_elsewhere = true dotnet_style_predefined_type_for_locals_parameters_members = true dotnet_style_predefined_type_for_member_access = true -# Use language keywords instead of framework type names for type references -dotnet_style_predefined_type_for_locals_parameters_members = true -dotnet_style_predefined_type_for_member_access = true - # File style csharp_using_directive_placement = outside_namespace dotnet_sort_system_directives_first = true @@ -52,6 +46,7 @@ csharp_style_namespace_declarations = file_scoped # Expression-level preferences dotnet_style_coalesce_expression = true dotnet_style_collection_initializer = true +dotnet_style_prefer_collection_expression = when_types_exactly_match dotnet_style_explicit_tuple_names = true dotnet_style_null_propagation = true dotnet_style_object_initializer = true @@ -69,7 +64,7 @@ csharp_prefer_inferred_anonymous_type_member_names = true csharp_prefer_inferred_tuple_names = true csharp_prefer_simple_default_expression = true csharp_style_deconstructed_variable_declaration = true -csharp_style_pattern_local_over_anonymous_function = true +csharp_style_prefer_local_over_anonymous_function = true csharp_style_prefer_switch_expression = true dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity @@ -98,7 +93,6 @@ csharp_style_throw_expression = true # Other features csharp_style_prefer_index_operator = true csharp_style_prefer_range_operator = true -csharp_style_pattern_local_over_anonymous_function = true # Code block preferences csharp_prefer_braces = when_multiline @@ -136,4 +130,3 @@ dotnet_code_quality_unused_parameters = all # IDE0130: Namespace does not match folder structure dotnet_diagnostic.IDE0130.severity = none - diff --git a/src/Config/Compiler.editorconfig b/src/Config/Compiler.editorconfig index 0c28236..22f3fd1 100644 --- a/src/Config/Compiler.editorconfig +++ b/src/Config/Compiler.editorconfig @@ -1,6 +1,7 @@ -# global_level must be higher than the NET Analyzer files +# global_level controls precedence across multiple global AnalyzerConfig files. +# Keep distinct values to avoid equal-level conflicts (which are ignored). is_global = true -global_level = 0 +global_level = 40 # EnableGenerateDocumentationFile: Set MSBuild property 'GenerateDocumentationFile' to 'true' # Help link: https://github.com/dotnet/roslyn/issues/41640 @@ -298,4 +299,3 @@ dotnet_diagnostic.IDE2006.severity = silent # RemoveUnnecessaryImportsFixable: # Enabled: True, Severity: silent dotnet_diagnostic.RemoveUnnecessaryImportsFixable.severity = silent - diff --git a/src/Config/Global.editorconfig b/src/Config/Global.editorconfig index 40d292a..68b25b8 100644 --- a/src/Config/Global.editorconfig +++ b/src/Config/Global.editorconfig @@ -1,5 +1,5 @@ is_global = true -global_level = 0 # higher than the NET Analyzer files +global_level = 50 # highest precedence among SDK-shipped globals dotnet_style_prefer_foreach_explicit_cast_in_source=when_strongly_typed diff --git a/src/Config/NamingConvention.editorconfig b/src/Config/NamingConvention.editorconfig index e8e2821..4ecdb56 100644 --- a/src/Config/NamingConvention.editorconfig +++ b/src/Config/NamingConvention.editorconfig @@ -1,5 +1,5 @@ is_global = true -global_level = 0 +global_level = 20 dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case diff --git a/tests/ANcpLua.Sdk.Tests/Helpers/SdkProjectBuilder.cs b/tests/ANcpLua.Sdk.Tests/Helpers/SdkProjectBuilder.cs index 1f9cc86..8297059 100644 --- a/tests/ANcpLua.Sdk.Tests/Helpers/SdkProjectBuilder.cs +++ b/tests/ANcpLua.Sdk.Tests/Helpers/SdkProjectBuilder.cs @@ -339,7 +339,8 @@ public static SdkProjectBuilder Create( true - """); + """, + StringComparison.Ordinal); base.WithDirectoryBuildProps(modifiedContent); return this; } @@ -579,14 +580,14 @@ public override async Task ExecuteDotnetCommandAsync( var result = await psi.RunAsTaskAsync(); // Retry on SDK resolution errors - const int maxRetries = 5; - for (var retry = 0; retry < maxRetries && result.ExitCode is not 0; retry++) + const int MaxRetries = 5; + for (var retry = 0; retry < MaxRetries && result.ExitCode is not 0; retry++) if (result.Output.Any(static line => line.Text.Contains("error MSB4236", StringComparison.Ordinal) || line.Text.Contains( "The project file may be invalid or missing targets required for restore", StringComparison.Ordinal))) { - Output?.WriteLine($"SDK resolution or restore error detected, retrying ({retry + 1}/{maxRetries})..."); + Output?.WriteLine($"SDK resolution or restore error detected, retrying ({retry + 1}/{MaxRetries})..."); await Task.Delay(100 * (1 << retry)); result = await psi.RunAsTaskAsync(); } @@ -643,7 +644,7 @@ public override async Task ExecuteDotnetCommandAsync( } // Fail fast on SDK resolution errors - if (result.Output.Any(static line => line.Text.Contains("Could not resolve SDK"))) + if (result.Output.Any(static line => line.Text.Contains("Could not resolve SDK", StringComparison.Ordinal))) Assert.Fail("The SDK cannot be found, expected version: " + _fixture.Version); return new BuildResult(result.ExitCode, result.Output, sarif, binlogContent) diff --git a/tests/ANcpLua.Sdk.Tests/MtpDetectionTests.cs b/tests/ANcpLua.Sdk.Tests/MtpDetectionTests.cs index cfa51f2..08ca3a5 100644 --- a/tests/ANcpLua.Sdk.Tests/MtpDetectionTests.cs +++ b/tests/ANcpLua.Sdk.Tests/MtpDetectionTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using static ANcpLua.Sdk.Tests.Helpers.PackageFixture; namespace ANcpLua.Sdk.Tests; @@ -42,6 +43,10 @@ public abstract class MtpDetectionTests( "TestingPlatformCommandLineArguments" ]; + [SuppressMessage( + "Reliability", + "CA2000:Dispose objects before losing scope", + Justification = "Returned builder is disposed by the test via await using.")] private SdkProjectBuilder CreateProject(string? sdkName = null) => SdkProjectBuilder.Create(fixture, SdkImportStyle.ProjectElement, sdkName ?? SdkTestName) .WithDotnetSdkVersion(dotnetSdkVersion) @@ -121,10 +126,10 @@ public class SampleTests .BuildAsync(); var cliArgs = result.GetRecordedProperty("TestingPlatformCommandLineArguments"); - Assert.Contains("--report-xunit-trx", cliArgs); - Assert.DoesNotContain("--report-trx ", cliArgs); - Assert.DoesNotContain("--crashdump", cliArgs); - Assert.DoesNotContain("--hangdump", cliArgs); + Assert.Contains("--report-xunit-trx", cliArgs, StringComparison.Ordinal); + Assert.DoesNotContain("--report-trx ", cliArgs, StringComparison.Ordinal); + Assert.DoesNotContain("--crashdump", cliArgs, StringComparison.Ordinal); + Assert.DoesNotContain("--hangdump", cliArgs, StringComparison.Ordinal); } [Fact] diff --git a/tests/ANcpLua.Sdk.Tests/SdkTests.cs b/tests/ANcpLua.Sdk.Tests/SdkTests.cs index 77168da..3f03fdc 100644 --- a/tests/ANcpLua.Sdk.Tests/SdkTests.cs +++ b/tests/ANcpLua.Sdk.Tests/SdkTests.cs @@ -1,4 +1,5 @@ using System.IO.Compression; +using System.Diagnostics.CodeAnalysis; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using System.Xml.Linq; @@ -37,6 +38,10 @@ public abstract class SdkTests( "_IsGitHubActions" ]; + [SuppressMessage( + "Reliability", + "CA2000:Dispose objects before losing scope", + Justification = "Returned builder is disposed by the test via await using.")] private SdkProjectBuilder CreateProject(string? sdkName = null) => SdkProjectBuilder.Create(fixture, sdkImportStyle, sdkName ?? SdkName) .WithDotnetSdkVersion(dotnetSdkVersion) @@ -71,7 +76,7 @@ static bool IsCpmItemGroup(XElement? parent) if (parent?.Name.LocalName != "ItemGroup") return false; var condition = parent.Attribute("Condition")?.Value; - return condition?.Contains("ManagePackageVersionsCentrally") == true; + return condition?.Contains("ManagePackageVersionsCentrally", StringComparison.Ordinal) == true; } } @@ -1040,7 +1045,7 @@ public async Task SetTargetFramework(string propName, string version) _ = blobReader.ReadSerializedString(); var key = blobReader.ReadSerializedString(); - Assert.Contains(expectedVersion.Replace("net", "v", StringComparison.Ordinal), key); + Assert.Contains(expectedVersion.Replace("net", "v", StringComparison.Ordinal), key, StringComparison.Ordinal); return; } } @@ -1332,7 +1337,7 @@ private static async Task AssertDebugInfoExists(IReadOnlyCollection outp { var dllPath = outputFiles.Single(static f => f.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)); await using var stream = File.OpenRead(dllPath); - var peReader = new PEReader(stream); + using var peReader = new PEReader(stream); var debug = peReader.ReadDebugDirectory(); if (isPackOutput) diff --git a/tests/ANcpLua.Sdk.Tests/SourceGeneratorDefaultsTests.cs b/tests/ANcpLua.Sdk.Tests/SourceGeneratorDefaultsTests.cs index 02a0356..3880c8c 100644 --- a/tests/ANcpLua.Sdk.Tests/SourceGeneratorDefaultsTests.cs +++ b/tests/ANcpLua.Sdk.Tests/SourceGeneratorDefaultsTests.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using static ANcpLua.Sdk.Tests.Helpers.PackageFixture; namespace ANcpLua.Sdk.Tests; @@ -29,6 +30,10 @@ public abstract class SourceGeneratorDefaultsTests( "RoslynVersion" ]; + [SuppressMessage( + "Reliability", + "CA2000:Dispose objects before losing scope", + Justification = "Returned builder is disposed by the test via await using.")] private SdkProjectBuilder CreateProject(string sdkName = SdkName) => SdkProjectBuilder.Create(fixture, SdkImportStyle.ProjectElement, sdkName) .WithDotnetSdkVersion(dotnetSdkVersion) diff --git a/tests/ANcpLua.Sdk.Tests/TemplatesTests.cs b/tests/ANcpLua.Sdk.Tests/TemplatesTests.cs index 496c084..af625bd 100644 --- a/tests/ANcpLua.Sdk.Tests/TemplatesTests.cs +++ b/tests/ANcpLua.Sdk.Tests/TemplatesTests.cs @@ -17,8 +17,6 @@ public sealed partial class TemplatesTests(PackageFixture fixture) { private const string TemplatesPackageId = "ANcpLua.NET.Sdk.Templates"; - private static readonly string[] s_shortNames = ["ancplua-app", "ancplua-lib", "ancplua-web"]; - // Concurrent dotnet-build invocations against scaffolded projects step on each other // via the shared MSBuild build server (node reuse) and the shared NuGet global packages // folder — manifests as flaky "file is being used by another process" errors in @@ -80,8 +78,8 @@ public async Task Pack_TemplateJson_HasNoUnsubstitutedPlaceholders(string shortN { var content = await ReadEntryAsync($"content/{shortName}/.template.config/template.json"); - Assert.DoesNotContain("__PACK_TIME_SDK_VERSION__", content); - Assert.DoesNotContain("__PACK_TIME_DOTNET_SDK_VERSION__", content); + Assert.DoesNotContain("__PACK_TIME_SDK_VERSION__", content, StringComparison.Ordinal); + Assert.DoesNotContain("__PACK_TIME_DOTNET_SDK_VERSION__", content, StringComparison.Ordinal); } [Theory] @@ -140,8 +138,8 @@ public async Task Pack_GlobalJson_HasPlaceholders_ForScaffoldTimeSubstitution(st // symbol values (which template.json's defaults provide). var content = await ReadEntryAsync($"content/{shortName}/global.json"); - Assert.Contains("ANCPLUA_SDK_VERSION_PLACEHOLDER", content); - Assert.Contains("ANCPLUA_DOTNET_SDK_VERSION_PLACEHOLDER", content); + Assert.Contains("ANCPLUA_SDK_VERSION_PLACEHOLDER", content, StringComparison.Ordinal); + Assert.Contains("ANCPLUA_DOTNET_SDK_VERSION_PLACEHOLDER", content, StringComparison.Ordinal); } [Fact] @@ -152,9 +150,9 @@ public async Task Pack_WebTemplate_GlobalJsonPinsAllThreeMsbuildSdks() // ANcpLua.NET.Sdk.Test for the tests project. var content = await ReadEntryAsync("content/ancplua-web/global.json"); - Assert.Contains("\"ANcpLua.NET.Sdk\":", content); - Assert.Contains("\"ANcpLua.NET.Sdk.Web\":", content); - Assert.Contains("\"ANcpLua.NET.Sdk.Test\":", content); + Assert.Contains("\"ANcpLua.NET.Sdk\":", content, StringComparison.Ordinal); + Assert.Contains("\"ANcpLua.NET.Sdk.Web\":", content, StringComparison.Ordinal); + Assert.Contains("\"ANcpLua.NET.Sdk.Test\":", content, StringComparison.Ordinal); } [Theory] @@ -184,9 +182,9 @@ public async Task Scaffold_ProducesNamedTreeWithVersionPins(string shortName, st // stamped at pack time). var globalJsonPath = output.FullPath / "global.json"; var globalJson = await File.ReadAllTextAsync(globalJsonPath, TestContext.Current.CancellationToken); - Assert.DoesNotContain("ANCPLUA_SDK_VERSION_PLACEHOLDER", globalJson); - Assert.DoesNotContain("ANCPLUA_DOTNET_SDK_VERSION_PLACEHOLDER", globalJson); - Assert.Contains($"\"ANcpLua.NET.Sdk\": \"{fixture.Version}\"", globalJson); + Assert.DoesNotContain("ANCPLUA_SDK_VERSION_PLACEHOLDER", globalJson, StringComparison.Ordinal); + Assert.DoesNotContain("ANCPLUA_DOTNET_SDK_VERSION_PLACEHOLDER", globalJson, StringComparison.Ordinal); + Assert.Contains($"\"ANcpLua.NET.Sdk\": \"{fixture.Version}\"", globalJson, StringComparison.Ordinal); // Verify the scaffolded sdk.version pins to the .NET SDK we stamped at pack time // (read from Build/Common/Version.props, propagated through DotNetSdkVersion symbol). @@ -210,7 +208,7 @@ public async Task Scaffold_ProducesNamedTreeWithVersionPins(string shortName, st var dirPackagesPath = output.FullPath / "Directory.Packages.props"; Assert.True(File.Exists(dirPackagesPath)); var dirPackages = await File.ReadAllTextAsync(dirPackagesPath, TestContext.Current.CancellationToken); - Assert.Contains("true", dirPackages); + Assert.Contains("true", dirPackages, StringComparison.Ordinal); } [Theory] @@ -243,10 +241,10 @@ await DotnetNewScaffoldAsync( var srcContent = await File.ReadAllTextAsync(srcCsproj, TestContext.Current.CancellationToken); var testsContent = await File.ReadAllTextAsync(testsCsproj, TestContext.Current.CancellationToken); - Assert.Contains("net9.0", srcContent); - Assert.Contains("net9.0", testsContent); - Assert.DoesNotContain("net10.0", srcContent); - Assert.DoesNotContain("net10.0", testsContent); + Assert.Contains("net9.0", srcContent, StringComparison.Ordinal); + Assert.Contains("net9.0", testsContent, StringComparison.Ordinal); + Assert.DoesNotContain("net10.0", srcContent, StringComparison.Ordinal); + Assert.DoesNotContain("net10.0", testsContent, StringComparison.Ordinal); } [Theory] diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index 19763df..6176ff5 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -4,6 +4,7 @@ using System.Runtime.Loader; using System.Text; using System.Text.RegularExpressions; +using System.Xml.Linq; using Basic.Reference.Assemblies; using Meziantou.Framework; using Meziantou.Framework.DependencyScanning; @@ -20,6 +21,7 @@ using NuGet.Versioning; var rootFolder = GetRootFolderPath(); +var dotNetSdkVersion = GetDotNetSdkVersionFromRepo(rootFolder); var writtenFiles = 0; await GenerateEditorConfigForCompilerAnalyzers().ConfigureAwait(false); @@ -54,9 +56,10 @@ async Task GenerateEditorConfigForCompilerAnalyzers() if (rules.Count > 0) { var sb = new StringBuilder(); - sb.AppendLine("# global_level must be higher than the NET Analyzer files"); + sb.AppendLine("# global_level controls precedence across multiple global AnalyzerConfig files."); + sb.AppendLine("# Keep distinct values to avoid equal-level conflicts (which are ignored)."); sb.AppendLine("is_global = true"); - sb.AppendLine("global_level = 0"); + sb.AppendLine($"global_level = {CompilerAnalyzerConfigGlobalLevel}"); var currentConfiguration = GetConfiguration(configurationFilePath); @@ -227,6 +230,7 @@ static Assembly[] LoadAssembliesFromDisk(string[] assemblyPaths, string[] probeF async Task GenerateEditorConfigForAnalyzers() { var packages = await GetAllReferencedNuGetPackages().ConfigureAwait(false); + var globalLevelsByPackageId = GetAnalyzerConfigGlobalLevels(packages.Select(static p => p.Id)); await Parallel.ForEachAsync(packages, async (item, cancellationToken) => { var (packageId, packageVersion) = item; @@ -257,9 +261,10 @@ await Parallel.ForEachAsync(packages, async (item, cancellationToken) => if (rules.Count > 0) { var sb = new StringBuilder(); - sb.AppendLine("# global_level must be higher than the NET Analyzer files"); + sb.AppendLine("# global_level controls precedence across multiple global AnalyzerConfig files."); + sb.AppendLine("# Keep distinct values to avoid equal-level conflicts (which are ignored)."); sb.AppendLine("is_global = true"); - sb.AppendLine("global_level = 0"); + sb.AppendLine($"global_level = {globalLevelsByPackageId[packageId]}"); var currentConfiguration = GetConfiguration(configurationFilePath); @@ -369,6 +374,55 @@ FullPath GetAnalyzerConfigurationFilePath(string packageId) return rootFolder / "src" / "Config" / ("Analyzer." + packageId + ".editorconfig"); } +const int CompilerAnalyzerConfigGlobalLevel = 40; + +static IReadOnlyDictionary GetAnalyzerConfigGlobalLevels(IEnumerable packageIds) +{ + var orderedIds = packageIds + .Distinct(StringComparer.Ordinal) + .OrderBy(static id => id, StringComparer.Ordinal) + .ToArray(); + + // Stable, deterministic, collision-free within the referenced package set. + // Low range is reserved for known "core" analyzer packs. + var result = new Dictionary(StringComparer.Ordinal); + for (var i = 0; i < orderedIds.Length; i++) + { + var id = orderedIds[i]; + result[id] = id switch + { + "Microsoft.CodeAnalysis.Analyzers" => 10, + "Microsoft.CodeAnalysis.NetAnalyzers" => 11, + "Microsoft.CodeAnalysis.BannedApiAnalyzers" => 12, + "ANcpLua.Analyzers" => 13, + _ => 1000 + i + }; + } + + return result; +} + +static string? GetDotNetSdkVersionFromRepo(FullPath rootFolder) +{ + var versionPropsPath = rootFolder / "src" / "Build" / "Common" / "Version.props"; + if (!File.Exists(versionPropsPath.Value)) + return null; + + try + { + var doc = XDocument.Load(versionPropsPath.Value); + var dotNetSdkVersion = doc.Descendants() + .FirstOrDefault(static e => string.Equals(e.Name.LocalName, "DotNetSdkVersion", StringComparison.Ordinal))?.Value; + + dotNetSdkVersion = dotNetSdkVersion?.Trim(); + return string.IsNullOrEmpty(dotNetSdkVersion) ? null : dotNetSdkVersion; + } + catch + { + return null; + } +} + async Task<(string Id, NuGetVersion Version)[]> GetAllReferencedNuGetPackages() { var foundPackages = new HashSet(); @@ -441,11 +495,17 @@ await ListAllPackageDependencies( (item.Version is null || !item.Version.Contains("$("))) yield return (item.Name, item.Version); - foreach (var package in new[] - { - "Microsoft.CodeAnalysis.NetAnalyzers" - }) - yield return (package, null); + // Pin analyzer package versions to the repo's declared .NET SDK version to keep + // config generation reproducible (avoid "latest stable" drift). + if (!string.IsNullOrWhiteSpace(dotNetSdkVersion)) + { + yield return ("Microsoft.CodeAnalysis.NetAnalyzers", dotNetSdkVersion); + } + else + { + // Fallback: keep prior behavior if we cannot read the repo pin. + yield return ("Microsoft.CodeAnalysis.NetAnalyzers", null); + } } static FullPath GetRootFolderPath() From e16db33a216a3523390f1f77664c06fc5a0e3004 Mon Sep 17 00:00:00 2001 From: ancplua Date: Sat, 9 May 2026 12:26:30 +0200 Subject: [PATCH 02/27] fix: satisfy config generator lint --- ...lyzer.Microsoft.CodeAnalysis.Analyzers.editorconfig | 1 + ...er.Microsoft.CodeAnalysis.NetAnalyzers.editorconfig | 1 + src/Config/Compiler.editorconfig | 1 + tools/ConfigFilesGenerator/Program.cs | 10 +++++----- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Config/Analyzer.Microsoft.CodeAnalysis.Analyzers.editorconfig b/src/Config/Analyzer.Microsoft.CodeAnalysis.Analyzers.editorconfig index 8018850..89037f3 100644 --- a/src/Config/Analyzer.Microsoft.CodeAnalysis.Analyzers.editorconfig +++ b/src/Config/Analyzer.Microsoft.CodeAnalysis.Analyzers.editorconfig @@ -225,3 +225,4 @@ dotnet_diagnostic.RS2007.severity = warning # Help link: https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md # Enabled: True, Severity: warning dotnet_diagnostic.RS2008.severity = warning + diff --git a/src/Config/Analyzer.Microsoft.CodeAnalysis.NetAnalyzers.editorconfig b/src/Config/Analyzer.Microsoft.CodeAnalysis.NetAnalyzers.editorconfig index d2f0b71..5d5eb12 100644 --- a/src/Config/Analyzer.Microsoft.CodeAnalysis.NetAnalyzers.editorconfig +++ b/src/Config/Analyzer.Microsoft.CodeAnalysis.NetAnalyzers.editorconfig @@ -1603,3 +1603,4 @@ dotnet_diagnostic.CA5404.severity = warning # Help link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5405 # Enabled: False, Severity: warning dotnet_diagnostic.CA5405.severity = warning + diff --git a/src/Config/Compiler.editorconfig b/src/Config/Compiler.editorconfig index 22f3fd1..2ba37a6 100644 --- a/src/Config/Compiler.editorconfig +++ b/src/Config/Compiler.editorconfig @@ -299,3 +299,4 @@ dotnet_diagnostic.IDE2006.severity = silent # RemoveUnnecessaryImportsFixable: # Enabled: True, Severity: silent dotnet_diagnostic.RemoveUnnecessaryImportsFixable.severity = silent + diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index 6176ff5..107eb44 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -20,6 +20,8 @@ using NuGet.Protocol.Core.Types; using NuGet.Versioning; +const int CompilerAnalyzerConfigGlobalLevel = 40; + var rootFolder = GetRootFolderPath(); var dotNetSdkVersion = GetDotNetSdkVersionFromRepo(rootFolder); @@ -104,7 +106,7 @@ async Task GenerateEditorConfigForCompilerAnalyzers() DiagnosticSeverity.Info => "suggestion", DiagnosticSeverity.Warning => "warning", DiagnosticSeverity.Error => "error", - _ => throw new Exception($"Severity '{severity}' is not supported") + _ => throw new InvalidOperationException($"Severity '{severity}' is not supported") }; } } @@ -312,7 +314,7 @@ static string GetSeverity(DiagnosticSeverity? severity) DiagnosticSeverity.Info => "suggestion", DiagnosticSeverity.Warning => "warning", DiagnosticSeverity.Error => "error", - _ => throw new Exception($"Severity '{severity}' is not supported") + _ => throw new InvalidOperationException($"Severity '{severity}' is not supported") }; } } @@ -374,8 +376,6 @@ FullPath GetAnalyzerConfigurationFilePath(string packageId) return rootFolder / "src" / "Config" / ("Analyzer." + packageId + ".editorconfig"); } -const int CompilerAnalyzerConfigGlobalLevel = 40; - static IReadOnlyDictionary GetAnalyzerConfigGlobalLevels(IEnumerable packageIds) { var orderedIds = packageIds @@ -492,7 +492,7 @@ await ListAllPackageDependencies( foreach (var item in result) if (item.Type is DependencyType.NuGet && item.Name is not null && - (item.Version is null || !item.Version.Contains("$("))) + (item.Version is null || !item.Version.Contains("$(", StringComparison.Ordinal))) yield return (item.Name, item.Version); // Pin analyzer package versions to the repo's declared .NET SDK version to keep From f7b6125b0bedea5b1801453d21b91a086f209ad1 Mon Sep 17 00:00:00 2001 From: ancplua Date: Sat, 9 May 2026 12:26:51 +0200 Subject: [PATCH 03/27] fix: normalize generated config file endings --- tools/ConfigFilesGenerator/Program.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index 107eb44..74b89f5 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -90,7 +90,7 @@ async Task GenerateEditorConfigForCompilerAnalyzers() sb.AppendLine(); } - var text = sb.ToString().ReplaceLineEndings("\n"); + var text = NormalizeGeneratedText(sb); if (File.Exists(configurationFilePath)) if (File.ReadAllText(configurationFilePath).ReplaceLineEndings("\n") == text) return; @@ -296,7 +296,7 @@ await Parallel.ForEachAsync(packages, async (item, cancellationToken) => sb.AppendLine(); } - var text = sb.ToString().ReplaceLineEndings("\n"); + var text = NormalizeGeneratedText(sb); if (File.Exists(configurationFilePath)) if (File.ReadAllText(configurationFilePath).ReplaceLineEndings("\n") == text) return; @@ -361,7 +361,7 @@ async Task GenerateBanSymbolsForNewtonsoftJson() sb.AppendLine("# Banned symbols from Newtonsoft.Json"); foreach (var symbol in bannedSymbols.OrderBy(static s => s, StringComparer.Ordinal)) sb.AppendLine(symbol); - var text = sb.ToString().ReplaceLineEndings("\n"); + var text = NormalizeGeneratedText(sb); if (File.Exists(bannedSymbolsFilePath)) if (File.ReadAllText(bannedSymbolsFilePath).ReplaceLineEndings("\n") == text) return; @@ -376,6 +376,11 @@ FullPath GetAnalyzerConfigurationFilePath(string packageId) return rootFolder / "src" / "Config" / ("Analyzer." + packageId + ".editorconfig"); } +static string NormalizeGeneratedText(StringBuilder sb) +{ + return sb.ToString().ReplaceLineEndings("\n").TrimEnd('\n') + "\n"; +} + static IReadOnlyDictionary GetAnalyzerConfigGlobalLevels(IEnumerable packageIds) { var orderedIds = packageIds From ada2f470b97080dc3afef619378ad808d8a5010d Mon Sep 17 00:00:00 2001 From: ancplua Date: Sat, 9 May 2026 12:27:04 +0200 Subject: [PATCH 04/27] chore: normalize generated analyzer configs --- .../Analyzer.Microsoft.CodeAnalysis.Analyzers.editorconfig | 1 - .../Analyzer.Microsoft.CodeAnalysis.NetAnalyzers.editorconfig | 1 - src/Config/Compiler.editorconfig | 1 - 3 files changed, 3 deletions(-) diff --git a/src/Config/Analyzer.Microsoft.CodeAnalysis.Analyzers.editorconfig b/src/Config/Analyzer.Microsoft.CodeAnalysis.Analyzers.editorconfig index 89037f3..8018850 100644 --- a/src/Config/Analyzer.Microsoft.CodeAnalysis.Analyzers.editorconfig +++ b/src/Config/Analyzer.Microsoft.CodeAnalysis.Analyzers.editorconfig @@ -225,4 +225,3 @@ dotnet_diagnostic.RS2007.severity = warning # Help link: https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md # Enabled: True, Severity: warning dotnet_diagnostic.RS2008.severity = warning - diff --git a/src/Config/Analyzer.Microsoft.CodeAnalysis.NetAnalyzers.editorconfig b/src/Config/Analyzer.Microsoft.CodeAnalysis.NetAnalyzers.editorconfig index 5d5eb12..d2f0b71 100644 --- a/src/Config/Analyzer.Microsoft.CodeAnalysis.NetAnalyzers.editorconfig +++ b/src/Config/Analyzer.Microsoft.CodeAnalysis.NetAnalyzers.editorconfig @@ -1603,4 +1603,3 @@ dotnet_diagnostic.CA5404.severity = warning # Help link: https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5405 # Enabled: False, Severity: warning dotnet_diagnostic.CA5405.severity = warning - diff --git a/src/Config/Compiler.editorconfig b/src/Config/Compiler.editorconfig index 2ba37a6..22f3fd1 100644 --- a/src/Config/Compiler.editorconfig +++ b/src/Config/Compiler.editorconfig @@ -299,4 +299,3 @@ dotnet_diagnostic.IDE2006.severity = silent # RemoveUnnecessaryImportsFixable: # Enabled: True, Severity: silent dotnet_diagnostic.RemoveUnnecessaryImportsFixable.severity = silent - From cf18da37733d75c2340ab62e6872ae0483e31a70 Mon Sep 17 00:00:00 2001 From: ancplua Date: Sat, 9 May 2026 12:35:59 +0200 Subject: [PATCH 05/27] fix: address analyzer config review feedback --- Directory.Build.props | 20 ++++++++------ src/Build/Common/Version.props | 1 + .../Helpers/SdkProjectBuilder.cs | 18 ++++++------- tests/ANcpLua.Sdk.Tests/MtpDetectionTests.cs | 7 +---- tools/ConfigFilesGenerator/Program.cs | 27 ++++++++++--------- 5 files changed, 37 insertions(+), 36 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 9d2ddab..6f75465 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,14 +3,14 @@ latest enable - enable - true - true - latest-all + enable + true + true + latest-all - true - true - true + true + true + true @@ -33,7 +33,11 @@ so the SDK's Common.props EditorConfigFiles injection doesn't run. Pull in the canonical SDK-shipped editorconfigs so this repo eats its own dog food. --> - + + true + + + diff --git a/src/Build/Common/Version.props b/src/Build/Common/Version.props index 0cdf282..f7d27f6 100644 --- a/src/Build/Common/Version.props +++ b/src/Build/Common/Version.props @@ -54,6 +54,7 @@ 1.29.1 + 10.0.203 4.1.5 4.14.0 diff --git a/tests/ANcpLua.Sdk.Tests/Helpers/SdkProjectBuilder.cs b/tests/ANcpLua.Sdk.Tests/Helpers/SdkProjectBuilder.cs index 8297059..8e54ccb 100644 --- a/tests/ANcpLua.Sdk.Tests/Helpers/SdkProjectBuilder.cs +++ b/tests/ANcpLua.Sdk.Tests/Helpers/SdkProjectBuilder.cs @@ -332,15 +332,15 @@ public static SdkProjectBuilder Create( public new SdkProjectBuilder WithDirectoryBuildProps(string content) { // Inject DisableVersionAnalyzer to prevent AL0017-AL0019 errors in test projects - var modifiedContent = content.Replace( - "", - """ - - - true - - """, - StringComparison.Ordinal); + var doc = XDocument.Parse(content, LoadOptions.PreserveWhitespace); + if (doc.Root is null || doc.Root.Name.LocalName != "Project") + throw new InvalidOperationException("Directory.Build.props must have a root element."); + + doc.Root.AddFirst( + new XElement("PropertyGroup", + new XElement("DisableVersionAnalyzer", "true"))); + + var modifiedContent = doc.ToString(SaveOptions.DisableFormatting); base.WithDirectoryBuildProps(modifiedContent); return this; } diff --git a/tests/ANcpLua.Sdk.Tests/MtpDetectionTests.cs b/tests/ANcpLua.Sdk.Tests/MtpDetectionTests.cs index 08ca3a5..bcadff1 100644 --- a/tests/ANcpLua.Sdk.Tests/MtpDetectionTests.cs +++ b/tests/ANcpLua.Sdk.Tests/MtpDetectionTests.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using static ANcpLua.Sdk.Tests.Helpers.PackageFixture; namespace ANcpLua.Sdk.Tests; @@ -43,10 +42,6 @@ public abstract class MtpDetectionTests( "TestingPlatformCommandLineArguments" ]; - [SuppressMessage( - "Reliability", - "CA2000:Dispose objects before losing scope", - Justification = "Returned builder is disposed by the test via await using.")] private SdkProjectBuilder CreateProject(string? sdkName = null) => SdkProjectBuilder.Create(fixture, SdkImportStyle.ProjectElement, sdkName ?? SdkTestName) .WithDotnetSdkVersion(dotnetSdkVersion) @@ -127,7 +122,7 @@ public class SampleTests var cliArgs = result.GetRecordedProperty("TestingPlatformCommandLineArguments"); Assert.Contains("--report-xunit-trx", cliArgs, StringComparison.Ordinal); - Assert.DoesNotContain("--report-trx ", cliArgs, StringComparison.Ordinal); + Assert.DoesNotContain("--report-trx", cliArgs, StringComparison.Ordinal); Assert.DoesNotContain("--crashdump", cliArgs, StringComparison.Ordinal); Assert.DoesNotContain("--hangdump", cliArgs, StringComparison.Ordinal); } diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index 74b89f5..115c350 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -23,7 +23,7 @@ const int CompilerAnalyzerConfigGlobalLevel = 40; var rootFolder = GetRootFolderPath(); -var dotNetSdkVersion = GetDotNetSdkVersionFromRepo(rootFolder); +var netAnalyzersVersion = GetVersionPropertyFromRepo(rootFolder, "NetAnalyzersVersion"); var writtenFiles = 0; await GenerateEditorConfigForCompilerAnalyzers().ConfigureAwait(false); @@ -384,13 +384,13 @@ static string NormalizeGeneratedText(StringBuilder sb) static IReadOnlyDictionary GetAnalyzerConfigGlobalLevels(IEnumerable packageIds) { var orderedIds = packageIds - .Distinct(StringComparer.Ordinal) - .OrderBy(static id => id, StringComparer.Ordinal) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(static id => id, StringComparer.OrdinalIgnoreCase) .ToArray(); // Stable, deterministic, collision-free within the referenced package set. // Low range is reserved for known "core" analyzer packs. - var result = new Dictionary(StringComparer.Ordinal); + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); for (var i = 0; i < orderedIds.Length; i++) { var id = orderedIds[i]; @@ -407,7 +407,7 @@ static IReadOnlyDictionary GetAnalyzerConfigGlobalLevels(IEnumerabl return result; } -static string? GetDotNetSdkVersionFromRepo(FullPath rootFolder) +static string? GetVersionPropertyFromRepo(FullPath rootFolder, string propertyName) { var versionPropsPath = rootFolder / "src" / "Build" / "Common" / "Version.props"; if (!File.Exists(versionPropsPath.Value)) @@ -416,14 +416,15 @@ static IReadOnlyDictionary GetAnalyzerConfigGlobalLevels(IEnumerabl try { var doc = XDocument.Load(versionPropsPath.Value); - var dotNetSdkVersion = doc.Descendants() - .FirstOrDefault(static e => string.Equals(e.Name.LocalName, "DotNetSdkVersion", StringComparison.Ordinal))?.Value; + var value = doc.Descendants() + .FirstOrDefault(e => string.Equals(e.Name.LocalName, propertyName, StringComparison.Ordinal))?.Value; - dotNetSdkVersion = dotNetSdkVersion?.Trim(); - return string.IsNullOrEmpty(dotNetSdkVersion) ? null : dotNetSdkVersion; + value = value?.Trim(); + return string.IsNullOrEmpty(value) ? null : value; } - catch + catch (Exception ex) { + Console.Error.WriteLine($"Warning: Failed to read {propertyName} from {versionPropsPath}: {ex.Message}"); return null; } } @@ -500,11 +501,11 @@ await ListAllPackageDependencies( (item.Version is null || !item.Version.Contains("$(", StringComparison.Ordinal))) yield return (item.Name, item.Version); - // Pin analyzer package versions to the repo's declared .NET SDK version to keep + // Pin analyzer package versions to a repo-declared package version to keep // config generation reproducible (avoid "latest stable" drift). - if (!string.IsNullOrWhiteSpace(dotNetSdkVersion)) + if (!string.IsNullOrWhiteSpace(netAnalyzersVersion)) { - yield return ("Microsoft.CodeAnalysis.NetAnalyzers", dotNetSdkVersion); + yield return ("Microsoft.CodeAnalysis.NetAnalyzers", netAnalyzersVersion); } else { From b6e83a87f270188774b491de380d670bf5e0485e Mon Sep 17 00:00:00 2001 From: ancplua Date: Sat, 9 May 2026 12:36:02 +0200 Subject: [PATCH 06/27] fix: address analyzer config review comments --- tests/ANcpLua.Sdk.Tests/SdkTests.cs | 5 ----- tests/ANcpLua.Sdk.Tests/SourceGeneratorDefaultsTests.cs | 5 ----- 2 files changed, 10 deletions(-) diff --git a/tests/ANcpLua.Sdk.Tests/SdkTests.cs b/tests/ANcpLua.Sdk.Tests/SdkTests.cs index 3f03fdc..d8756a3 100644 --- a/tests/ANcpLua.Sdk.Tests/SdkTests.cs +++ b/tests/ANcpLua.Sdk.Tests/SdkTests.cs @@ -1,5 +1,4 @@ using System.IO.Compression; -using System.Diagnostics.CodeAnalysis; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using System.Xml.Linq; @@ -38,10 +37,6 @@ public abstract class SdkTests( "_IsGitHubActions" ]; - [SuppressMessage( - "Reliability", - "CA2000:Dispose objects before losing scope", - Justification = "Returned builder is disposed by the test via await using.")] private SdkProjectBuilder CreateProject(string? sdkName = null) => SdkProjectBuilder.Create(fixture, sdkImportStyle, sdkName ?? SdkName) .WithDotnetSdkVersion(dotnetSdkVersion) diff --git a/tests/ANcpLua.Sdk.Tests/SourceGeneratorDefaultsTests.cs b/tests/ANcpLua.Sdk.Tests/SourceGeneratorDefaultsTests.cs index 3880c8c..02a0356 100644 --- a/tests/ANcpLua.Sdk.Tests/SourceGeneratorDefaultsTests.cs +++ b/tests/ANcpLua.Sdk.Tests/SourceGeneratorDefaultsTests.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using static ANcpLua.Sdk.Tests.Helpers.PackageFixture; namespace ANcpLua.Sdk.Tests; @@ -30,10 +29,6 @@ public abstract class SourceGeneratorDefaultsTests( "RoslynVersion" ]; - [SuppressMessage( - "Reliability", - "CA2000:Dispose objects before losing scope", - Justification = "Returned builder is disposed by the test via await using.")] private SdkProjectBuilder CreateProject(string sdkName = SdkName) => SdkProjectBuilder.Create(fixture, SdkImportStyle.ProjectElement, sdkName) .WithDotnetSdkVersion(dotnetSdkVersion) From da195e3524b2580702cd60c8ecb248ef1da165ee Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 04:34:30 +0000 Subject: [PATCH 07/27] fix: apply CodeRabbit auto-fixes Fixed 2 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit --- src/Build/Common/Version.props | 2 +- tools/ConfigFilesGenerator/Program.cs | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Build/Common/Version.props b/src/Build/Common/Version.props index f7d27f6..8eb8ce8 100644 --- a/src/Build/Common/Version.props +++ b/src/Build/Common/Version.props @@ -54,7 +54,7 @@ 1.29.1 - 10.0.203 + 10.0.203 4.1.5 4.14.0 diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index 115c350..12c6fb0 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -454,7 +454,15 @@ await ListAllPackageDependencies(packageIdentity, [repository], NuGetFramework.A NullLogger.Instance, foundPackages, CancellationToken.None).ConfigureAwait(false); } - return [.. foundPackages.Select(static p => (p.Id, p.Version))]; + // Deduplicate by package ID. If multiple versions exist (e.g., from dependency scanning + // and explicit pinning), prefer the highest version to ensure we get all analyzer rules. + var deduplicatedPackages = foundPackages + .GroupBy(static p => p.Id, StringComparer.OrdinalIgnoreCase) + .Select(static g => g.OrderByDescending(static p => p.Version).First()) + .Select(static p => (p.Id, p.Version)) + .ToArray(); + + return deduplicatedPackages; static async Task ListAllPackageDependencies( PackageIdentity package, From eb99db299e7f12fcbe7180cb6055a267ac41230f Mon Sep 17 00:00:00 2001 From: Alexander Nachtmann Date: Sun, 10 May 2026 06:46:36 +0200 Subject: [PATCH 08/27] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/ANcpLua.Sdk.Tests/Helpers/SdkProjectBuilder.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/ANcpLua.Sdk.Tests/Helpers/SdkProjectBuilder.cs b/tests/ANcpLua.Sdk.Tests/Helpers/SdkProjectBuilder.cs index 8e54ccb..5c38d79 100644 --- a/tests/ANcpLua.Sdk.Tests/Helpers/SdkProjectBuilder.cs +++ b/tests/ANcpLua.Sdk.Tests/Helpers/SdkProjectBuilder.cs @@ -336,9 +336,10 @@ public static SdkProjectBuilder Create( if (doc.Root is null || doc.Root.Name.LocalName != "Project") throw new InvalidOperationException("Directory.Build.props must have a root element."); + var ns = doc.Root.Name.Namespace; doc.Root.AddFirst( - new XElement("PropertyGroup", - new XElement("DisableVersionAnalyzer", "true"))); + new XElement(ns + "PropertyGroup", + new XElement(ns + "DisableVersionAnalyzer", "true"))); var modifiedContent = doc.ToString(SaveOptions.DisableFormatting); base.WithDirectoryBuildProps(modifiedContent); From 642a434550a77c94edae821fd95771de09193f64 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 04:51:56 +0000 Subject: [PATCH 09/27] fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit --- src/Build/Common/Version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/Common/Version.props b/src/Build/Common/Version.props index 8eb8ce8..85bb8ba 100644 --- a/src/Build/Common/Version.props +++ b/src/Build/Common/Version.props @@ -54,7 +54,7 @@ 1.29.1 - 10.0.203 + $(DotNetSdkVersion) 4.1.5 4.14.0 From 5b85e593f2ec95ec9c7028fa1b520c1c0ed40caf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 04:57:29 +0000 Subject: [PATCH 10/27] fix: resolve NetAnalyzersVersion from Version.props for generator pinning Agent-Logs-Url: https://github.com/ANcpLua/ANcpLua.NET.Sdk/sessions/185d4461-1909-404d-9103-572cec49dcda Co-authored-by: ANcpLua <124206820+ANcpLua@users.noreply.github.com> --- tools/ConfigFilesGenerator/Program.cs | 64 ++++++++++++++++----------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index 12c6fb0..2c43fce 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -23,7 +23,11 @@ const int CompilerAnalyzerConfigGlobalLevel = 40; var rootFolder = GetRootFolderPath(); -var netAnalyzersVersion = GetVersionPropertyFromRepo(rootFolder, "NetAnalyzersVersion"); +var msbuildProperties = LoadMsBuildProperties(rootFolder); +var netAnalyzersVersion = TryResolveMsBuildProperty("$(NetAnalyzersVersion)", msbuildProperties); +if (string.IsNullOrWhiteSpace(netAnalyzersVersion) || ContainsUnresolvedProperty(netAnalyzersVersion)) + throw new InvalidOperationException( + "Could not resolve NetAnalyzersVersion from src/Build/Common/Version.props."); var writtenFiles = 0; await GenerateEditorConfigForCompilerAnalyzers().ConfigureAwait(false); @@ -407,26 +411,27 @@ static IReadOnlyDictionary GetAnalyzerConfigGlobalLevels(IEnumerabl return result; } -static string? GetVersionPropertyFromRepo(FullPath rootFolder, string propertyName) +static IReadOnlyDictionary LoadMsBuildProperties(FullPath rootFolder) { var versionPropsPath = rootFolder / "src" / "Build" / "Common" / "Version.props"; - if (!File.Exists(versionPropsPath.Value)) - return null; + if (!File.Exists(versionPropsPath)) + return new Dictionary(StringComparer.OrdinalIgnoreCase); - try - { - var doc = XDocument.Load(versionPropsPath.Value); - var value = doc.Descendants() - .FirstOrDefault(e => string.Equals(e.Name.LocalName, propertyName, StringComparison.Ordinal))?.Value; + var doc = XDocument.Load(versionPropsPath); + var root = doc.Root; + if (root is null) + return new Dictionary(StringComparer.OrdinalIgnoreCase); - value = value?.Trim(); - return string.IsNullOrEmpty(value) ? null : value; - } - catch (Exception ex) + var properties = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var propertyGroup in root.Elements("PropertyGroup")) + foreach (var element in propertyGroup.Elements()) { - Console.Error.WriteLine($"Warning: Failed to read {propertyName} from {versionPropsPath}: {ex.Message}"); - return null; + var value = element.Value?.Trim(); + if (!string.IsNullOrEmpty(value)) + properties[element.Name.LocalName] = value; } + + return properties; } async Task<(string Id, NuGetVersion Version)[]> GetAllReferencedNuGetPackages() @@ -509,17 +514,26 @@ await ListAllPackageDependencies( (item.Version is null || !item.Version.Contains("$(", StringComparison.Ordinal))) yield return (item.Name, item.Version); - // Pin analyzer package versions to a repo-declared package version to keep + // Pin analyzer package versions to repo-declared versions to keep // config generation reproducible (avoid "latest stable" drift). - if (!string.IsNullOrWhiteSpace(netAnalyzersVersion)) - { - yield return ("Microsoft.CodeAnalysis.NetAnalyzers", netAnalyzersVersion); - } - else - { - // Fallback: keep prior behavior if we cannot read the repo pin. - yield return ("Microsoft.CodeAnalysis.NetAnalyzers", null); - } + yield return ("Microsoft.CodeAnalysis.NetAnalyzers", netAnalyzersVersion); +} + +static bool ContainsUnresolvedProperty(string? value) => + value is not null && value.Contains("$(", StringComparison.Ordinal); + +static string? TryResolveMsBuildProperty(string? value, IReadOnlyDictionary properties) +{ + if (string.IsNullOrEmpty(value)) + return value; + + // Resolve simple property references like $(PropertyName). + var match = Regex.Match(value, @"^\$\(([^)]+)\)$", RegexOptions.CultureInvariant); + if (!match.Success) + return value; + + var propertyName = match.Groups[1].Value; + return properties.TryGetValue(propertyName, out var resolved) ? resolved : value; } static FullPath GetRootFolderPath() From 23aa3207c079d9e9194b9003023248f4bb6a5ac3 Mon Sep 17 00:00:00 2001 From: ANcpLua Date: Sun, 10 May 2026 05:09:59 +0000 Subject: [PATCH 11/27] fix: harden config generator review feedback - recursively expand MSBuild property references in Version.props so NetAnalyzersVersion (=) actually resolves - restore SDK-injected analyzer packages (ANcpLua.Analyzers, BannedApiAnalyzers, AwesomeAssertions.Analyzers) that DependencyScanner drops because their PackageReference Versions are property-based - accept .git as either directory or file so git worktrees keep working - match reserved analyzer IDs case-insensitively (NuGet IDs are case-insensitive, dictionary already uses OrdinalIgnoreCase) - null-check + dispose Process.Start, await with WaitForExitAsync - move trailing comment off global_level in Global.editorconfig and reword to clarify scope (base globals; flavor overlays sit above) Co-authored-by: Codesmith --- src/Config/Global.editorconfig | 5 +- tools/ConfigFilesGenerator/Program.cs | 94 ++++++++++++++++++++------- 2 files changed, 75 insertions(+), 24 deletions(-) diff --git a/src/Config/Global.editorconfig b/src/Config/Global.editorconfig index 68b25b8..28d0e2c 100644 --- a/src/Config/Global.editorconfig +++ b/src/Config/Global.editorconfig @@ -1,5 +1,8 @@ +# Highest precedence among the base/non-flavor SDK globals. +# Per-flavor overlays (e.g. ANcpLua.NET.Sdk.Web=60, .SingleFileApp=61) sit above this. +# EditorConfig spec: comments must be on their own line, never trailing a value. is_global = true -global_level = 50 # highest precedence among SDK-shipped globals +global_level = 50 dotnet_style_prefer_foreach_explicit_cast_in_source=when_strongly_typed diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index 2c43fce..bd18271 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -24,10 +24,27 @@ var rootFolder = GetRootFolderPath(); var msbuildProperties = LoadMsBuildProperties(rootFolder); -var netAnalyzersVersion = TryResolveMsBuildProperty("$(NetAnalyzersVersion)", msbuildProperties); -if (string.IsNullOrWhiteSpace(netAnalyzersVersion) || ContainsUnresolvedProperty(netAnalyzersVersion)) - throw new InvalidOperationException( - "Could not resolve NetAnalyzersVersion from src/Build/Common/Version.props."); + +// Pin SDK-injected analyzers to the versions declared in Version.props so the +// generator always refreshes the configs the SDK actually ships, regardless of +// whether DependencyScanner sees the property-based PackageReference values. +var injectedAnalyzerPackages = new (string PackageId, string PropertyName)[] +{ + ("Microsoft.CodeAnalysis.NetAnalyzers", "NetAnalyzersVersion"), + ("ANcpLua.Analyzers", "ANcpLuaAnalyzersVersion"), + ("Microsoft.CodeAnalysis.BannedApiAnalyzers", "BannedApiAnalyzersVersion"), + ("AwesomeAssertions.Analyzers", "AwesomeAssertionsAnalyzersVersion"), +}; + +var injectedAnalyzerVersions = new Dictionary(StringComparer.OrdinalIgnoreCase); +foreach (var (packageId, propertyName) in injectedAnalyzerPackages) +{ + var resolved = ResolveMsBuildProperty($"$({propertyName})", msbuildProperties); + if (string.IsNullOrWhiteSpace(resolved) || ContainsUnresolvedProperty(resolved)) + throw new InvalidOperationException( + $"Could not resolve {propertyName} from src/Build/Common/Version.props."); + injectedAnalyzerVersions[packageId] = resolved; +} var writtenFiles = 0; await GenerateEditorConfigForCompilerAnalyzers().ConfigureAwait(false); @@ -35,7 +52,12 @@ await GenerateBanSymbolsForNewtonsoftJson().ConfigureAwait(false); Console.WriteLine($"{writtenFiles} configuration files written"); -if (writtenFiles > 0) Process.Start("git", "--no-pager diff --color").WaitForExit(); +if (writtenFiles > 0) +{ + using var diffProcess = Process.Start("git", "--no-pager diff --color"); + if (diffProcess is not null) + await diffProcess.WaitForExitAsync().ConfigureAwait(false); +} return 0; @@ -393,17 +415,19 @@ static IReadOnlyDictionary GetAnalyzerConfigGlobalLevels(IEnumerabl .ToArray(); // Stable, deterministic, collision-free within the referenced package set. - // Low range is reserved for known "core" analyzer packs. + // Low range is reserved for known "core" analyzer packs. NuGet IDs are + // case-insensitive, so the reserved-ID matches must be case-insensitive too. var result = new Dictionary(StringComparer.OrdinalIgnoreCase); for (var i = 0; i < orderedIds.Length; i++) { var id = orderedIds[i]; result[id] = id switch { - "Microsoft.CodeAnalysis.Analyzers" => 10, - "Microsoft.CodeAnalysis.NetAnalyzers" => 11, - "Microsoft.CodeAnalysis.BannedApiAnalyzers" => 12, - "ANcpLua.Analyzers" => 13, + _ when string.Equals(id, "Microsoft.CodeAnalysis.Analyzers", StringComparison.OrdinalIgnoreCase) => 10, + _ when string.Equals(id, "Microsoft.CodeAnalysis.NetAnalyzers", StringComparison.OrdinalIgnoreCase) => 11, + _ when string.Equals(id, "Microsoft.CodeAnalysis.BannedApiAnalyzers", StringComparison.OrdinalIgnoreCase) => 12, + _ when string.Equals(id, "ANcpLua.Analyzers", StringComparison.OrdinalIgnoreCase) => 13, + _ when string.Equals(id, "AwesomeAssertions.Analyzers", StringComparison.OrdinalIgnoreCase) => 14, _ => 1000 + i }; } @@ -507,33 +531,54 @@ await ListAllPackageDependencies( async IAsyncEnumerable<(string Id, string? Version)> GetReferencedNuGetPackages() { + var seenIds = new HashSet(StringComparer.OrdinalIgnoreCase); var result = await DependencyScanner.ScanDirectoryAsync(rootFolder / "src", null).ConfigureAwait(false); foreach (var item in result) if (item.Type is DependencyType.NuGet && item.Name is not null && (item.Version is null || !item.Version.Contains("$(", StringComparison.Ordinal))) + { + seenIds.Add(item.Name); yield return (item.Name, item.Version); + } - // Pin analyzer package versions to repo-declared versions to keep - // config generation reproducible (avoid "latest stable" drift). - yield return ("Microsoft.CodeAnalysis.NetAnalyzers", netAnalyzersVersion); + // Pin analyzer package versions to repo-declared versions so config + // generation reproducibly refreshes the analyzer set the SDK injects, + // even though DependencyScanner skips entries with property-based versions. + foreach (var (packageId, version) in injectedAnalyzerVersions) + if (seenIds.Add(packageId)) + yield return (packageId, version); } static bool ContainsUnresolvedProperty(string? value) => value is not null && value.Contains("$(", StringComparison.Ordinal); -static string? TryResolveMsBuildProperty(string? value, IReadOnlyDictionary properties) +static string ResolveMsBuildProperty(string value, IReadOnlyDictionary properties) { - if (string.IsNullOrEmpty(value)) - return value; + // Recursively expand $(...) references with a bounded depth and cycle guard. + const int maxDepth = 16; + var visited = new HashSet(StringComparer.OrdinalIgnoreCase); + var current = value; + for (var depth = 0; depth < maxDepth; depth++) + { + if (string.IsNullOrEmpty(current)) + return current; + + var match = Regex.Match(current, @"^\$\(([^)]+)\)$", RegexOptions.CultureInvariant); + if (!match.Success) + return current; - // Resolve simple property references like $(PropertyName). - var match = Regex.Match(value, @"^\$\(([^)]+)\)$", RegexOptions.CultureInvariant); - if (!match.Success) - return value; + var propertyName = match.Groups[1].Value; + if (!visited.Add(propertyName)) + return current; + + if (!properties.TryGetValue(propertyName, out var resolved)) + return current; + + current = resolved; + } - var propertyName = match.Groups[1].Value; - return properties.TryGetValue(propertyName, out var resolved) ? resolved : value; + return current; } static FullPath GetRootFolderPath() @@ -541,7 +586,10 @@ static FullPath GetRootFolderPath() var path = FullPath.CurrentDirectory(); while (!path.IsEmpty) { - if (Directory.Exists(path / ".git")) + // In a regular checkout `.git` is a directory; in a worktree it is a + // file pointing at the real gitdir. Accept both so worktree layouts work. + var gitMarker = path / ".git"; + if (Directory.Exists(gitMarker) || File.Exists(gitMarker)) return path; path = path.Parent; From 6b504e0214247cf2d139887ed08ed426fd921748 Mon Sep 17 00:00:00 2001 From: ANcpLua Date: Sun, 10 May 2026 05:14:48 +0000 Subject: [PATCH 12/27] fix: preserve pinned analyzer versions during package dedup When a transitive dependency pulls a newer version of an SDK-injected analyzer, the previous highest-version dedup overrode the pinned entry from injectedAnalyzerVersions, so the generated .editorconfig no longer matched the analyzer set the SDK actually ships. Honor pinned versions first; fall back to highest-version selection for non-pinned packages. Co-authored-by: Codesmith --- tools/ConfigFilesGenerator/Program.cs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index bd18271..6f5f2ce 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -483,11 +483,26 @@ await ListAllPackageDependencies(packageIdentity, [repository], NuGetFramework.A NullLogger.Instance, foundPackages, CancellationToken.None).ConfigureAwait(false); } - // Deduplicate by package ID. If multiple versions exist (e.g., from dependency scanning - // and explicit pinning), prefer the highest version to ensure we get all analyzer rules. + // Deduplicate by package ID. For SDK-injected analyzers, honor the version + // pinned via Version.props so the generated configs reflect the analyzer set + // the SDK actually injects, even if a transitive dep pulls a newer version. + // For everything else, prefer the highest version to capture all analyzer rules. + var pinnedVersions = injectedAnalyzerVersions.ToDictionary( + static kvp => kvp.Key, + static kvp => NuGetVersion.Parse(kvp.Value), + StringComparer.OrdinalIgnoreCase); + var deduplicatedPackages = foundPackages .GroupBy(static p => p.Id, StringComparer.OrdinalIgnoreCase) - .Select(static g => g.OrderByDescending(static p => p.Version).First()) + .Select(g => + { + if (pinnedVersions.TryGetValue(g.Key, out var pinnedVersion)) + return g.FirstOrDefault(p => p.Version == pinnedVersion) + ?? throw new InvalidOperationException( + $"Pinned analyzer package '{g.Key}' version '{pinnedVersion}' was not resolved."); + + return g.OrderByDescending(static p => p.Version).First(); + }) .Select(static p => (p.Id, p.Version)) .ToArray(); From 1d0f0712f5cef7ce8bd2ec339ef78430ea6de8b4 Mon Sep 17 00:00:00 2001 From: ANcpLua Date: Sun, 10 May 2026 05:19:38 +0000 Subject: [PATCH 13/27] fix: resolve property-based package versions instead of dropping Previously, references like xunit.v3.mtp-v2 with Version="$(XunitV3Version)" were filtered out wholesale, so their transitive analyzers (e.g. xunit.analyzers) never reached the scan and Analyzer.xunit.analyzers.editorconfig drifted from the analyzer version test projects actually pull. Resolve property-based versions through Version.props; drop only when a reference still has an unresolved property after expansion. Co-authored-by: Codesmith --- tools/ConfigFilesGenerator/Program.cs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index 6f5f2ce..4c12e5a 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -549,17 +549,29 @@ await ListAllPackageDependencies( var seenIds = new HashSet(StringComparer.OrdinalIgnoreCase); var result = await DependencyScanner.ScanDirectoryAsync(rootFolder / "src", null).ConfigureAwait(false); foreach (var item in result) + { + if (item.Type is not DependencyType.NuGet || item.Name is null) + continue; - if (item.Type is DependencyType.NuGet && item.Name is not null && - (item.Version is null || !item.Version.Contains("$(", StringComparison.Ordinal))) + var version = item.Version; + // Centrally-managed PackageReferences carry property-based versions + // (e.g. "$(XunitV3Version)"). Resolve them via Version.props so their + // transitive analyzers (xunit.analyzers, etc.) still get scanned. + // Drop only when resolution still leaves an unresolved property. + if (version is not null && version.Contains("$(", StringComparison.Ordinal)) { - seenIds.Add(item.Name); - yield return (item.Name, item.Version); + version = ResolveMsBuildProperty(version, msbuildProperties); + if (ContainsUnresolvedProperty(version)) + continue; } + seenIds.Add(item.Name); + yield return (item.Name, version); + } + // Pin analyzer package versions to repo-declared versions so config // generation reproducibly refreshes the analyzer set the SDK injects, - // even though DependencyScanner skips entries with property-based versions. + // even when DependencyScanner doesn't surface them at all. foreach (var (packageId, version) in injectedAnalyzerVersions) if (seenIds.Add(packageId)) yield return (packageId, version); From e6d76a475cf3650136eb1ba20fa64b336a4d2f34 Mon Sep 17 00:00:00 2001 From: ANcpLua Date: Sun, 10 May 2026 05:22:12 +0000 Subject: [PATCH 14/27] fix: give xunit.analyzers a reserved global_level slot xunit.analyzers ships as a transitive of xunit.v3.mtp-v2 (now visible to the scanner since 1d0f071 resolves property-based versions). Without a reserved slot it picks up an alphabetical 1000+i level that re-numbers whenever the package set shifts, churning the generated config diff. Pin it at 15 to match the other reserved core analyzers. Co-authored-by: Codesmith --- tools/ConfigFilesGenerator/Program.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index 4c12e5a..b8c8fdb 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -428,6 +428,9 @@ _ when string.Equals(id, "Microsoft.CodeAnalysis.NetAnalyzers", StringComparison _ when string.Equals(id, "Microsoft.CodeAnalysis.BannedApiAnalyzers", StringComparison.OrdinalIgnoreCase) => 12, _ when string.Equals(id, "ANcpLua.Analyzers", StringComparison.OrdinalIgnoreCase) => 13, _ when string.Equals(id, "AwesomeAssertions.Analyzers", StringComparison.OrdinalIgnoreCase) => 14, + // xunit.analyzers ships as a transitive of xunit.v3 (resolved via Version.props + // property expansion). Reserved so its config doesn't churn when the package set shifts. + _ when string.Equals(id, "xunit.analyzers", StringComparison.OrdinalIgnoreCase) => 15, _ => 1000 + i }; } From abfdd06d0f011fb4bd685ac06a6af75a9c01a8bd Mon Sep 17 00:00:00 2001 From: ANcpLua Date: Sun, 10 May 2026 05:36:25 +0000 Subject: [PATCH 15/27] chore: regenerate analyzer editorconfigs for current package versions Re-running tools/ConfigFilesGenerator with the now-resolved ANcpLuaAnalyzersVersion (1.29.1), pinned NetAnalyzersVersion, and the restored-via-property-resolution xunit/awesome-assertions packages produces an updated set of shipped Analyzer.*.editorconfig files. The lint_config job runs the generator and diffs against the committed output, so commit the regenerated artifacts: - Analyzer.ANcpLua.Analyzers.editorconfig: refresh rule titles, switch Help links to docs site, add AL0110-AL0138 rules - Analyzer.AwesomeAssertions.Analyzers.editorconfig: regenerated header and reserved global_level (14) - Analyzer.xunit.analyzers.editorconfig: regenerated header and reserved global_level (15) Co-authored-by: Codesmith --- .../Analyzer.ANcpLua.Analyzers.editorconfig | 444 ++++++++++++++---- ...r.AwesomeAssertions.Analyzers.editorconfig | 6 +- .../Analyzer.xunit.analyzers.editorconfig | 6 +- 3 files changed, 360 insertions(+), 96 deletions(-) diff --git a/src/Config/Analyzer.ANcpLua.Analyzers.editorconfig b/src/Config/Analyzer.ANcpLua.Analyzers.editorconfig index d98ea36..7dc1f30 100644 --- a/src/Config/Analyzer.ANcpLua.Analyzers.editorconfig +++ b/src/Config/Analyzer.ANcpLua.Analyzers.editorconfig @@ -4,408 +4,672 @@ is_global = true global_level = 13 # AL0001: Prohibit reassignment of primary constructor parameters -# Help link: https://github.com/ANcpLua/ANcpLua.Analyzers/blob/main/docs/AL0001.md +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0001 # Enabled: True, Severity: error dotnet_diagnostic.AL0001.severity = error # AL0002: Don't repeat negated patterns -# Help link: https://github.com/ANcpLua/ANcpLua.Analyzers/blob/main/docs/AL0002.md +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0002 # Enabled: True, Severity: warning dotnet_diagnostic.AL0002.severity = warning # AL0003: Don't divide by constant zero -# Help link: https://github.com/ANcpLua/ANcpLua.Analyzers/blob/main/docs/AL0003.md +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0003 # Enabled: True, Severity: error dotnet_diagnostic.AL0003.severity = error # AL0004: Use pattern matching when comparing Span with constants -# Help link: https://github.com/ANcpLua/ANcpLua.Analyzers/blob/main/docs/AL0004.md +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0004 # Enabled: True, Severity: warning dotnet_diagnostic.AL0004.severity = warning # AL0005: Use SequenceEqual when comparing Span with non-constants -# Help link: https://github.com/ANcpLua/ANcpLua.Analyzers/blob/main/docs/AL0005.md +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0005 # Enabled: True, Severity: warning dotnet_diagnostic.AL0005.severity = warning # AL0006: Field name conflicts with primary constructor parameter -# Help link: https://github.com/ANcpLua/ANcpLua.Analyzers/blob/main/docs/AL0006.md +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0006 # Enabled: True, Severity: warning dotnet_diagnostic.AL0006.severity = warning # AL0007: GetSchema should be explicitly implemented -# Help link: https://github.com/ANcpLua/ANcpLua.Analyzers/blob/main/docs/AL0007.md +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0007 # Enabled: True, Severity: error dotnet_diagnostic.AL0007.severity = error # AL0008: GetSchema must return null and not be abstract -# Help link: https://github.com/ANcpLua/ANcpLua.Analyzers/blob/main/docs/AL0008.md +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0008 # Enabled: True, Severity: error dotnet_diagnostic.AL0008.severity = error # AL0009: Don't call IXmlSerializable.GetSchema -# Help link: https://github.com/ANcpLua/ANcpLua.Analyzers/blob/main/docs/AL0009.md +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0009 # Enabled: True, Severity: error dotnet_diagnostic.AL0009.severity = error # AL0010: Type should be partial -# Help link: https://github.com/ANcpLua/ANcpLua.Analyzers/blob/main/docs/AL0010.md +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0010 # Enabled: False, Severity: suggestion dotnet_diagnostic.AL0010.severity = none # AL0011: Avoid lock keyword on non-Lock types -# Help link: https://github.com/ANcpLua/ANcpLua.Analyzers/blob/main/docs/AL0011.md +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0011 # Enabled: True, Severity: warning dotnet_diagnostic.AL0011.severity = warning # AL0012: Deprecated semantic convention attribute -# Help link: https://github.com/ANcpLua/ANcpLua.Analyzers/blob/main/docs/AL0012.md +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0012 # Enabled: True, Severity: warning dotnet_diagnostic.AL0012.severity = warning # AL0013: Missing telemetry schema URL -# Help link: https://github.com/ANcpLua/ANcpLua.Analyzers/blob/main/docs/AL0013.md +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0013 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0013.severity = suggestion -# AR0001: Convert SCREAMING_SNAKE_CASE to PascalCase -# Enabled: True, Severity: none (refactoring only) -dotnet_diagnostic.AR0001.severity = none - # AL0014: Prefer pattern matching for null and zero comparisons +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0014 # Enabled: True, Severity: warning dotnet_diagnostic.AL0014.severity = warning # AL0015: Normalize null-guard style +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0015 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0015.severity = suggestion # AL0016: Combine declaration with subsequent null-check +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0016 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0016.severity = suggestion -# AL0017: Hardcoded version in Directory.Packages.props +# AL0017: Hardcoded package version detected +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0017 # Enabled: True, Severity: warning dotnet_diagnostic.AL0017.severity = warning # AL0018: Version.props not imported +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0018 # Enabled: True, Severity: warning dotnet_diagnostic.AL0018.severity = warning # AL0019: Undefined version variable +# Help link: https://ancplua.mintlify.app/analyzers/rules/ # Enabled: True, Severity: warning dotnet_diagnostic.AL0019.severity = warning -# AL0020: IFormCollection requires explicit [FromForm] attribute +# AL0020: IFormCollection requires explicit attribute +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0020 # Enabled: True, Severity: error dotnet_diagnostic.AL0020.severity = error -# AL0021: Multiple structured form sources not allowed +# AL0021: Multiple structured form sources +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0021 # Enabled: True, Severity: error dotnet_diagnostic.AL0021.severity = error -# AL0022: Cannot mix IFormCollection with [FromForm] DTO +# AL0022: Mixed form collection and DTO +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0022 # Enabled: True, Severity: error dotnet_diagnostic.AL0022.severity = error -# AL0023: Unsupported [FromForm] type +# AL0023: Unsupported form type +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0023 # Enabled: True, Severity: error dotnet_diagnostic.AL0023.severity = error -# AL0024: [FromForm] and [FromBody] conflict +# AL0024: Form and body conflict +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0024 # Enabled: True, Severity: error dotnet_diagnostic.AL0024.severity = error # AL0025: Anonymous function can be made static +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0025 # Enabled: True, Severity: warning dotnet_diagnostic.AL0025.severity = warning -# AL0026: Avoid DateTime time accessors - use TimeProvider instead +# AL0026: Avoid DateTime/DateTimeOffset time accessors +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0026 # Enabled: True, Severity: warning dotnet_diagnostic.AL0026.severity = warning -# AL0027: Avoid legacy JSON library - use System.Text.Json instead +# AL0027: Avoid legacy JSON library +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0027 # Enabled: True, Severity: warning dotnet_diagnostic.AL0027.severity = warning -# AL0028: Use IsEqualTo instead of SymbolEqualityComparer.Equals +# AL0028: Use IsEqualTo extension +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0028 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0028.severity = suggestion -# AL0029: Use HasAttribute instead of GetAttributes() patterns +# AL0029: Use HasAttribute extension +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0029 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0029.severity = suggestion -# AL0030: Use Implements/InheritsFrom instead of type hierarchy loops +# AL0030: Use type hierarchy extension +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0030 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0030.severity = suggestion -# AL0031: Use IsMethodNamed/TryGetConstantValue instead of verbose patterns +# AL0031: Use operation extension +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0031 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0031.severity = suggestion -# AL0032: Use OrEmpty() instead of null-coalescing with empty collections +# AL0032: Use OrEmpty extension +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0032 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0032.severity = suggestion -# AL0033: Use ToImmutableArrayOrEmpty() instead of null-conditional with fallback +# AL0033: Use ToImmutableArrayOrEmpty extension +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0033 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0033.severity = suggestion -# AL0034: Use WhereNotNull() instead of Where with null check +# AL0034: Use WhereNotNull extension +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0034 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0034.severity = suggestion -# AL0035: Use GetFullyQualifiedName/GetMetadataName() instead of ToDisplayString with format +# AL0035: Use symbol display string extension +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0035 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0035.severity = suggestion -# AL0036: Use Guard.NotNull instead of ?? throw new ArgumentNullException +# AL0036: Use null-guard helper +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0036 # Enabled: True, Severity: warning dotnet_diagnostic.AL0036.severity = warning -# AL0037: Use TryParse extensions instead of verbose TryParse patterns +# AL0037: Use TryParse extension +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0037 # Enabled: True, Severity: warning dotnet_diagnostic.AL0037.severity = warning -# AL0038: Use GetOrNull instead of TryGetValue patterns -# Enabled: True, Severity: warning -dotnet_diagnostic.AL0038.severity = warning - -# AL0039: Use StringComparison extensions for clearer intent +# AL0039: Use StringComparison extension +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0039 # Enabled: True, Severity: warning dotnet_diagnostic.AL0039.severity = warning -# AL0040: Use attribute argument extraction extensions +# AL0040: Use attribute argument extraction extension +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0040 # Enabled: True, Severity: warning dotnet_diagnostic.AL0040.severity = warning # AL0041: Method with [AotTest] or [TrimTest] must return int +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0041 # Enabled: True, Severity: error dotnet_diagnostic.AL0041.severity = error # AL0042: [AotTest]/[TrimTest] method should return 100 on success +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0042 # Enabled: True, Severity: warning dotnet_diagnostic.AL0042.severity = warning # AL0043: [TrimSafe] code must not call methods with [RequiresUnreferencedCode] +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0043 # Enabled: True, Severity: warning dotnet_diagnostic.AL0043.severity = warning # AL0044: [AotSafe] code must not call methods with [RequiresDynamicCode] +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0044 # Enabled: True, Severity: warning dotnet_diagnostic.AL0044.severity = warning -# AL0045: Use Guard.NotNullOrEmpty instead of if (string.IsNullOrEmpty) throw +# AL0045: Use null-or-empty guard helper +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0045 # Enabled: True, Severity: warning dotnet_diagnostic.AL0045.severity = warning -# AL0046: Use Guard.NotNullOrWhiteSpace instead of if (string.IsNullOrWhiteSpace) throw +# AL0046: Use null-or-whitespace guard helper +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0046 # Enabled: True, Severity: warning dotnet_diagnostic.AL0046.severity = warning -# AL0047: Use Guard.NotZero instead of if (x == 0) throw ArgumentOutOfRangeException +# AL0047: Use zero-guard helper +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0047 # Enabled: True, Severity: warning dotnet_diagnostic.AL0047.severity = warning -# AL0048: Use Guard.NotNegative instead of if (x < 0) throw ArgumentOutOfRangeException +# AL0048: Use non-negative guard helper +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0048 # Enabled: True, Severity: warning dotnet_diagnostic.AL0048.severity = warning -# AL0049: Use Guard.Positive instead of if (x <= 0) throw ArgumentOutOfRangeException +# AL0049: Use positive-guard helper +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0049 # Enabled: True, Severity: warning dotnet_diagnostic.AL0049.severity = warning -# AL0050: Use Guard.NotEmpty instead of if (guid == Guid.Empty) throw +# AL0050: Use empty-guid guard helper +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0050 # Enabled: True, Severity: warning dotnet_diagnostic.AL0050.severity = warning -# AL0051: Use Guard.DefinedEnum instead of if (!Enum.IsDefined) throw patterns +# AL0051: Use defined-enum guard helper +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0051 # Enabled: True, Severity: warning dotnet_diagnostic.AL0051.severity = warning # AL0052: [AotSafe] code must not call [AotUnsafe] code +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0052 # Enabled: True, Severity: error dotnet_diagnostic.AL0052.severity = error -# AL0053: [AotUnsafe] attribute applied to code that doesn't use AOT-incompatible patterns +# AL0053: Unnecessary [AotUnsafe] attribute +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0053 # Enabled: True, Severity: warning dotnet_diagnostic.AL0053.severity = warning -# AL0054: Diagnostic defined in Descriptors.cs is missing from diagnostics.md +# AL0054: Diagnostic missing from documentation +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0054 # Enabled: True, Severity: warning dotnet_diagnostic.AL0054.severity = warning -# AL0055: Diagnostic defined in Descriptors.cs is missing from AnalyzerReleases.*.md +# AL0055: Diagnostic missing from release notes +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0055 # Enabled: True, Severity: warning dotnet_diagnostic.AL0055.severity = warning -# AL0056: Diagnostic title/severity/category mismatch between Descriptors.cs and documentation +# AL0056: Diagnostic documentation mismatch +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0056 # Enabled: True, Severity: warning dotnet_diagnostic.AL0056.severity = warning -# AL0057: Avoid async void methods except for event handlers +# AL0057: Avoid async void methods +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0057 # Enabled: True, Severity: warning dotnet_diagnostic.AL0057.severity = warning -# AL0058: Avoid lock on 'this' - external code can cause deadlocks +# AL0058: Avoid lock on 'this' +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0058 # Enabled: True, Severity: warning dotnet_diagnostic.AL0058.severity = warning -# AL0059: Avoid lock on typeof(T) - type objects are globally visible +# AL0059: Avoid lock on typeof(T) +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0059 # Enabled: True, Severity: warning dotnet_diagnostic.AL0059.severity = warning -# AL0060: Avoid lock on string literal - interned strings are globally visible +# AL0060: Avoid lock on string +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0060 # Enabled: True, Severity: warning dotnet_diagnostic.AL0060.severity = warning # AL0061: Activity/Span missing semantic convention attributes +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0061 # Enabled: True, Severity: warning dotnet_diagnostic.AL0061.severity = warning -# AL0062: Deprecated semantic convention attribute +# AL0062: Deprecated semantic convention +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0062 # Enabled: True, Severity: warning dotnet_diagnostic.AL0062.severity = warning -# AL0063: ActivitySource not registered with AddSource() -# Enabled: True, Severity: suggestion +# AL0063: ActivitySource should be registered with AddSource() +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0063 +# Enabled: True, Severity: warning # Note: May report false positives when AddSource() is in a different assembly dotnet_diagnostic.AL0063.severity = suggestion # AL0064: GenAI span missing required attributes +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0064 # Enabled: True, Severity: warning dotnet_diagnostic.AL0064.severity = warning # AL0065: Use gen_ai.client.token.usage histogram for token metrics +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0065 # Enabled: True, Severity: warning dotnet_diagnostic.AL0065.severity = warning # AL0066: GenAI operation name should follow semantic conventions +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0066 # Enabled: True, Severity: warning dotnet_diagnostic.AL0066.severity = warning -# AL0067: Meter not registered with AddMeter() -# Enabled: True, Severity: suggestion +# AL0067: Meter should be registered with AddMeter() +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0067 +# Enabled: True, Severity: warning # Note: May report false positives when AddMeter() is in a different assembly dotnet_diagnostic.AL0067.severity = suggestion # AL0068: Metric instrument name should follow naming conventions +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0068 # Enabled: True, Severity: warning dotnet_diagnostic.AL0068.severity = warning # AL0069: ServiceDefaults configuration incomplete -# Enabled: True, Severity: suggestion +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0069 +# Enabled: True, Severity: warning # Note: May report false positives when configuration is in a shared helper method dotnet_diagnostic.AL0069.severity = suggestion # AL0070: Collector endpoint should use OTLP protocol +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0070 # Enabled: True, Severity: warning dotnet_diagnostic.AL0070.severity = warning # AL0071: [Meter] class must be partial static +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0071 # Enabled: True, Severity: error dotnet_diagnostic.AL0071.severity = error -# AL0072: [Counter]/[Histogram] method must be partial +# AL0072: Metric method must be partial +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0072 # Enabled: True, Severity: error dotnet_diagnostic.AL0072.severity = error -# AL0073: [Traced] attribute must have non-empty ActivitySourceName +# AL0073: [Traced] attribute requires non-empty ActivitySourceName +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0073 # Enabled: True, Severity: error dotnet_diagnostic.AL0073.severity = error -# AL0074: Deprecated GenAI semantic convention attribute +# AL0074: Deprecated GenAI semantic convention +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0074 # Enabled: True, Severity: warning dotnet_diagnostic.AL0074.severity = warning -# AL0075: High-cardinality tag on metrics (user.id, request.id, etc.) +# AL0075: High-cardinality tag on metric +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0075 # Enabled: True, Severity: warning dotnet_diagnostic.AL0075.severity = warning -# AL0076: AddServiceDefaults called but AddOpenTelemetry missing -# Enabled: True, Severity: suggestion +# AL0076: Missing OpenTelemetry configuration +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0076 +# Enabled: True, Severity: warning # Note: May report false positives when OTel is configured in a different assembly dotnet_diagnostic.AL0076.severity = suggestion -# AL0077: Duplicate instrumentation - method has both auto and manual tracing +# AL0077: Duplicate instrumentation detected +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0077 # Enabled: True, Severity: warning dotnet_diagnostic.AL0077.severity = warning -# AL0078: ActivitySource name doesn't follow reverse-DNS naming convention +# AL0078: Invalid ActivitySource name +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0078 # Enabled: True, Severity: error dotnet_diagnostic.AL0078.severity = error -# AL0079: Complex async flow detected; manual span recommended +# AL0079: Manual span recommended +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0079 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0079.severity = suggestion -# AL0080: HTTP client registered without resilience policies +# AL0080: Missing resilience configuration +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0080 # Enabled: True, Severity: warning dotnet_diagnostic.AL0080.severity = warning -# AL0081: Service doesn't expose health check endpoint +# AL0081: Missing health checks +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0081 # Enabled: True, Severity: warning dotnet_diagnostic.AL0081.severity = warning -# AL0082: Hardcoded connection string detected +# AL0082: Consider using configuration for connection string +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0082 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0082.severity = suggestion -# AL0083: HTTP endpoint used where HTTPS is expected +# AL0083: Insecure endpoint +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0083 # Enabled: True, Severity: warning dotnet_diagnostic.AL0083.severity = warning -# AL0084: Direct URL used instead of service discovery +# AL0084: Missing service discovery +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0084 # Enabled: True, Severity: warning dotnet_diagnostic.AL0084.severity = warning -# AL0085: Attribute value violates OTel semantic convention spec +# AL0085: Invalid attribute value +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0085 # Enabled: True, Severity: error dotnet_diagnostic.AL0085.severity = error -# AL0086: Attribute set with wrong type (e.g., string instead of int for token counts) +# AL0086: Incorrect attribute type +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0086 # Enabled: True, Severity: warning dotnet_diagnostic.AL0086.severity = warning -# AL0087: Prefer constant attribute over string literal for semantic convention names +# AL0087: Prefer constant attribute +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0087 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0087.severity = suggestion -# AL0088: Potential PII or credential detected in span attribute +# AL0088: Sensitive data in span attribute +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0088 # Enabled: True, Severity: warning dotnet_diagnostic.AL0088.severity = warning -# AL0089: OTEL_EXPORTER_OTLP_ENDPOINT not configured -# Enabled: True, Severity: suggestion +# AL0089: Missing OTLP configuration +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0089 +# Enabled: True, Severity: warning # Note: May report false positives when endpoint is set via runtime environment variable dotnet_diagnostic.AL0089.severity = suggestion -# AL0090: OTLP exporter doesn't have compression enabled +# AL0090: Uncompressed OTLP export +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0090 # Enabled: True, Severity: warning dotnet_diagnostic.AL0090.severity = warning -# AL0091: Single-span export configured instead of batch export +# AL0091: Batch export disabled +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0091 # Enabled: True, Severity: warning dotnet_diagnostic.AL0091.severity = warning -# AL0092: High-volume service without sampling configured +# AL0092: Consider configuring sampling +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0092 # Enabled: True, Severity: suggestion dotnet_diagnostic.AL0092.severity = suggestion -# AL0093: Missing resource attributes (service.name, service.version) -# Enabled: True, Severity: suggestion +# AL0093: Missing resource attributes +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0093 +# Enabled: True, Severity: warning # Note: May report false positives when resource attributes are set by the consuming app dotnet_diagnostic.AL0093.severity = suggestion -# AL0094: Avoid dynamic keyword in AOT/trimmed apps +# AL0094: Avoid 'dynamic' keyword in AOT-published code +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0094 # Enabled: True, Severity: warning dotnet_diagnostic.AL0094.severity = warning -# AL0095: Avoid Expression.Compile() in AOT/trimmed apps +# AL0095: Avoid Expression.Compile() in AOT context +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0095 # Enabled: True, Severity: warning dotnet_diagnostic.AL0095.severity = warning -# AL0096: Enable EventSourceSupport when PublishAot is set +# AL0096: Enable EventSourceSupport for AOT with telemetry +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0096 # Enabled: True, Severity: warning dotnet_diagnostic.AL0096.severity = warning + +# AL0101: Activator.CreateInstance is not AOT-safe +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0101 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0101.severity = warning + +# AL0102: Type.GetType with dynamic name is not AOT-safe +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0102 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0102.severity = warning + +# AL0103: Closed hierarchy match is not exhaustive +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0103 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0103.severity = warning + +# AL0104: Prefer 'await using' for IAsyncDisposable +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0104 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0104.severity = warning + +# AL0105: Avoid blocking calls in async methods +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0105 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0105.severity = warning + +# AL0106: Avoid Task.Run in ASP.NET Core request handlers +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0106 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0106.severity = warning + +# AL0107: Orphaned [TracedTag] +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0107 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0107.severity = warning + +# AL0108: Redundant [NoTrace] +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0108 +# Enabled: True, Severity: suggestion +dotnet_diagnostic.AL0108.severity = suggestion + +# AL0109: Non-interceptable [Traced] +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0109 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0109.severity = warning + +# AL0110: [TracedTag] on out/ref parameter +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0110 +# Enabled: True, Severity: error +dotnet_diagnostic.AL0110.severity = error + +# AL0111: Avoid SQL string interpolation in CommandText +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0111 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0111.severity = warning + +# AL0112: Avoid fire-and-forget task discard +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0112 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0112.severity = warning + +# AL0113: Missing exception recording on Activity error status +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0113 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0113.severity = warning + +# AL0114: Prefer TryParse over Parse +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0114 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0114.severity = warning + +# AL0115: Empty catch block swallows exceptions +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0115 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0115.severity = warning + +# AL0116: Exception details leaked in HTTP response +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0116 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0116.severity = warning + +# AL0117: Unnecessary LINQ materialization +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0117 +# Enabled: True, Severity: suggestion +dotnet_diagnostic.AL0117.severity = suggestion + +# AL0118: Read-modify-write without transaction +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0118 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0118.severity = warning + +# AL0119: Avoid storing ISymbol in source generator models +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0119 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0119.severity = warning + +# AL0120: Use IIncrementalGenerator instead of ISourceGenerator +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0120 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0120.severity = warning + +# AL0121: Avoid NormalizeWhitespace in source generators +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0121 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0121.severity = warning + +# AL0122: [DuckDbTable] type must be partial +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0122 +# Enabled: True, Severity: error +dotnet_diagnostic.AL0122.severity = error + +# AL0123: Conflicting [DuckDbColumn] ordinal values +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0123 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0123.severity = warning + +# AL0124: Non-interceptable [AgentTraced] +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0124 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0124.severity = warning + +# AL0125: Use *Any* string comparison extension +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0125 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0125.severity = warning + +# AL0126: Forward CancellationToken to invocations that support it +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0126 +# Enabled: True, Severity: suggestion +dotnet_diagnostic.AL0126.severity = suggestion + +# AL0127: Outdated MAF ecosystem package version +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0127 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0127.severity = warning + +# AL0128: Destructive Loom tool must require approval +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0128 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0128.severity = warning + +# AL0129: Loom tool should declare its side effect +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0129 +# Enabled: True, Severity: suggestion +dotnet_diagnostic.AL0129.severity = suggestion + +# AL0130: Loom tool should declare required capabilities +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0130 +# Enabled: True, Severity: suggestion +dotnet_diagnostic.AL0130.severity = suggestion + +# AL0131: Direct GenAI SDK call bypasses automatic OTel instrumentation +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0131 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0131.severity = warning + +# AL0132: Deprecated semantic convention value +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0132 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0132.severity = warning + +# AL0133: Context-sensitive deprecated semantic convention +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0133 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0133.severity = warning + +# AL0134: Use OpenTelemetry semantic convention constant +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0134 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0134.severity = warning + +# AL0135: Replace legacy SemanticConventions accessor +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0135 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0135.severity = warning + +# AL0136: Incubating semantic convention referenced from library +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0136 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0136.severity = warning + +# AL0137: Use Guard.* helpers instead of throw helpers +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0137 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0137.severity = warning + +# AL0138: Use Math.Round/MathF.Round overload with explicit MidpointRounding +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0138 +# Enabled: True, Severity: warning +dotnet_diagnostic.AL0138.severity = warning diff --git a/src/Config/Analyzer.AwesomeAssertions.Analyzers.editorconfig b/src/Config/Analyzer.AwesomeAssertions.Analyzers.editorconfig index a9dfa05..80c3de0 100644 --- a/src/Config/Analyzer.AwesomeAssertions.Analyzers.editorconfig +++ b/src/Config/Analyzer.AwesomeAssertions.Analyzers.editorconfig @@ -1,6 +1,7 @@ -# global_level is set to 100 so SDK configuration overrides analyzer-provided defaults. +# global_level controls precedence across multiple global AnalyzerConfig files. +# Keep distinct values to avoid equal-level conflicts (which are ignored). is_global = true -global_level = 100 +global_level = 14 # AwesomeAssertions0801: Code Smell # Enabled: True, Severity: warning @@ -21,4 +22,3 @@ dotnet_diagnostic.FAA0003.severity = suggestion # FAA0004: Replace NUnit assertion with Fluent Assertions equivalent # Enabled: True, Severity: suggestion dotnet_diagnostic.FAA0004.severity = suggestion - diff --git a/src/Config/Analyzer.xunit.analyzers.editorconfig b/src/Config/Analyzer.xunit.analyzers.editorconfig index c7d8785..75f0241 100644 --- a/src/Config/Analyzer.xunit.analyzers.editorconfig +++ b/src/Config/Analyzer.xunit.analyzers.editorconfig @@ -1,6 +1,7 @@ -# global_level is set to 100 so SDK configuration overrides analyzer-provided defaults. +# global_level controls precedence across multiple global AnalyzerConfig files. +# Keep distinct values to avoid equal-level conflicts (which are ignored). is_global = true -global_level = 100 +global_level = 15 # xUnit1000: Test classes must be public # Help link: https://xunit.net/xunit.analyzers/rules/xUnit1000 @@ -451,4 +452,3 @@ dotnet_diagnostic.xUnit3002.severity = warning # Help link: https://xunit.net/xunit.analyzers/rules/xUnit3003 # Enabled: True, Severity: warning dotnet_diagnostic.xUnit3003.severity = warning - From 7e47350b9b42ec2c508cb14030aff31fbdda6d91 Mon Sep 17 00:00:00 2001 From: ANcpLua Date: Sun, 10 May 2026 05:41:35 +0000 Subject: [PATCH 16/27] fix: append rule id to analyzer help links missing the slug ANcpLua.Analyzers AL0019 reports a HelpLinkUri that ends at the rules index (.../analyzers/rules/) without the rule slug, so the generator emitted a useless index link. Normalize: when a HelpLinkUri ends with '/rules/' append the rule id so the generated comment points at the rule-specific page. Regenerate Analyzer.ANcpLua.Analyzers.editorconfig to pick up the fixed AL0019 link. Co-authored-by: Codesmith --- .../Analyzer.ANcpLua.Analyzers.editorconfig | 2 +- tools/ConfigFilesGenerator/Program.cs | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Config/Analyzer.ANcpLua.Analyzers.editorconfig b/src/Config/Analyzer.ANcpLua.Analyzers.editorconfig index 7dc1f30..543de2e 100644 --- a/src/Config/Analyzer.ANcpLua.Analyzers.editorconfig +++ b/src/Config/Analyzer.ANcpLua.Analyzers.editorconfig @@ -94,7 +94,7 @@ dotnet_diagnostic.AL0017.severity = warning dotnet_diagnostic.AL0018.severity = warning # AL0019: Undefined version variable -# Help link: https://ancplua.mintlify.app/analyzers/rules/ +# Help link: https://ancplua.mintlify.app/analyzers/rules/AL0019 # Enabled: True, Severity: warning dotnet_diagnostic.AL0019.severity = warning diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index b8c8fdb..c30b7bb 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -105,7 +105,8 @@ async Task GenerateEditorConfigForCompilerAnalyzers() : rule.DefaultEffectiveSeverity; sb.AppendLine($"# {rule.Id}: {rule.Title}"); - if (!string.IsNullOrEmpty(rule.Url)) sb.AppendLine($"# Help link: {rule.Url}"); + var helpLink = NormalizeHelpLink(rule.Url, rule.Id); + if (!string.IsNullOrEmpty(helpLink)) sb.AppendLine($"# Help link: {helpLink}"); sb.AppendLine($"# Enabled: {rule.Enabled}, Severity: {GetSeverity(rule.DefaultSeverity)}"); if (currentRuleConfiguration?.Comments.Length > 0) @@ -310,7 +311,8 @@ await Parallel.ForEachAsync(packages, async (item, cancellationToken) => : rule.DefaultEffectiveSeverity; sb.AppendLine($"# {rule.Id}: {rule.Title}"); - if (!string.IsNullOrEmpty(rule.Url)) sb.AppendLine($"# Help link: {rule.Url}"); + var helpLink = NormalizeHelpLink(rule.Url, rule.Id); + if (!string.IsNullOrEmpty(helpLink)) sb.AppendLine($"# Help link: {helpLink}"); sb.AppendLine($"# Enabled: {rule.Enabled}, Severity: {GetSeverity(rule.DefaultSeverity)}"); @@ -407,6 +409,19 @@ static string NormalizeGeneratedText(StringBuilder sb) return sb.ToString().ReplaceLineEndings("\n").TrimEnd('\n') + "\n"; } +static string? NormalizeHelpLink(string? helpLinkUri, string ruleId) +{ + // Some analyzers (e.g. ANcpLua.Analyzers AL0019) return the rules-index URL + // without the rule slug. Append the rule ID so the generated link points at + // the rule-specific page rather than the index. + if (string.IsNullOrEmpty(helpLinkUri)) + return helpLinkUri; + + return helpLinkUri.EndsWith("/rules/", StringComparison.Ordinal) + ? helpLinkUri + ruleId + : helpLinkUri; +} + static IReadOnlyDictionary GetAnalyzerConfigGlobalLevels(IEnumerable packageIds) { var orderedIds = packageIds From 062473c8ac22ddb6ca63c012aed10fc924809c08 Mon Sep 17 00:00:00 2001 From: ANcpLua Date: Sun, 10 May 2026 05:42:51 +0000 Subject: [PATCH 17/27] perf: skip duplicate top-level package IDs before NuGet work DependencyScanner walks every project under src/, so the same top-level package shows up multiple times. Previously each duplicate went through full version resolution and the recursive dependency walker before the later GroupBy dedup discarded the redundant entries. Move the seenIds.Add gate up so duplicates are skipped before any NuGet work happens. Co-authored-by: Codesmith --- tools/ConfigFilesGenerator/Program.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index c30b7bb..e31974b 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -571,6 +571,12 @@ await ListAllPackageDependencies( if (item.Type is not DependencyType.NuGet || item.Name is null) continue; + // Skip duplicate top-level package IDs before any version resolution + // or dependency walking; the same package showing up in multiple + // projects in src/ shouldn't fan out into repeated NuGet work. + if (!seenIds.Add(item.Name)) + continue; + var version = item.Version; // Centrally-managed PackageReferences carry property-based versions // (e.g. "$(XunitV3Version)"). Resolve them via Version.props so their @@ -583,7 +589,6 @@ await ListAllPackageDependencies( continue; } - seenIds.Add(item.Name); yield return (item.Name, version); } From da6468293cdfa2e203884e02a4cb726b3f7738e8 Mon Sep 17 00:00:00 2001 From: ANcpLua Date: Sun, 10 May 2026 05:51:12 +0000 Subject: [PATCH 18/27] fix: fail fast when a pinned analyzer can't be resolved on the feed ListAllPackageDependencies silently 'continue's when ResolvePackage returns null (e.g. the pinned version doesn't exist on the configured feed), so a missing pinned analyzer would drop out of foundPackages, the dedup-time pinned-version check would never run for it, and the shipped Analyzer..editorconfig would silently fail to refresh. After the dependency walk, verify every pinned ID landed in foundPackages and throw a clear error otherwise. Co-authored-by: Codesmith --- tools/ConfigFilesGenerator/Program.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index e31974b..4347c3e 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -510,6 +510,18 @@ await ListAllPackageDependencies(packageIdentity, [repository], NuGetFramework.A static kvp => NuGetVersion.Parse(kvp.Value), StringComparer.OrdinalIgnoreCase); + // ListAllPackageDependencies silently `continue`s when ResolvePackage returns + // null (e.g. the pinned version doesn't exist on the feed), so a missing + // pinned analyzer would otherwise silently drop out of the generated set. + // Fail loudly instead. + var foundIds = foundPackages + .Select(static p => p.Id) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (var pinnedId in pinnedVersions.Keys) + if (!foundIds.Contains(pinnedId)) + throw new InvalidOperationException( + $"Pinned analyzer package '{pinnedId}@{pinnedVersions[pinnedId]}' could not be resolved on the configured NuGet feeds."); + var deduplicatedPackages = foundPackages .GroupBy(static p => p.Id, StringComparer.OrdinalIgnoreCase) .Select(g => From bd356d18a8d2d426dc52018166efb108ee013390 Mon Sep 17 00:00:00 2001 From: ANcpLua Date: Sun, 10 May 2026 05:57:49 +0000 Subject: [PATCH 19/27] fix: honor Condition guards and delay seenIds add until after resolution LoadMsBuildProperties was unconditionally writing every element, ignoring Condition attributes. Properties guarded by the standard 'default if undefined' shape (Condition="'$(Self)' == ''") were being treated as unconditional, so any external override that relied on the guard would have been silently overwritten. Apply the self-default semantic when that exact guard is present, treat a missing condition as unconditional (last write wins), and skip properties carrying any other Condition shape rather than guess. In GetReferencedNuGetPackages, move the seenIds.Add to happen only after a package has been confirmed yieldable. Previously, a first sighting with an unresolved $(...) version got dropped after we'd already added the id to seenIds, which then blocked the pinned fallback from yielding the analyzer at all. Rename to emittedIds for clarity. Co-authored-by: Codesmith --- tools/ConfigFilesGenerator/Program.cs | 47 ++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index 4347c3e..8277e94 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -464,16 +464,44 @@ static IReadOnlyDictionary LoadMsBuildProperties(FullPath rootFo if (root is null) return new Dictionary(StringComparer.OrdinalIgnoreCase); + // Honor the only Condition pattern used in Version.props: the standard + // "default if undefined" guard, e.g. Condition="'$(NetAnalyzersVersion)' == ''". + // For that pattern, only assign when the property is not already set + // (mirrors MSBuild's evaluation of a self-referential empty check). + // Unconditional assignments behave normally (last write wins). + // Anything else with a Condition is skipped to avoid silently mishandling + // a guard the loader doesn't understand. var properties = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var propertyGroup in root.Elements("PropertyGroup")) foreach (var element in propertyGroup.Elements()) { var value = element.Value?.Trim(); - if (!string.IsNullOrEmpty(value)) - properties[element.Name.LocalName] = value; + if (string.IsNullOrEmpty(value)) + continue; + + var name = element.Name.LocalName; + var condition = element.Attribute("Condition")?.Value; + if (string.IsNullOrWhiteSpace(condition)) + { + properties[name] = value; + continue; + } + + var selfDefaultGuard = $"'$({name})' == ''"; + if (string.Equals(NormalizeCondition(condition), selfDefaultGuard, StringComparison.Ordinal)) + { + if (!properties.ContainsKey(name)) + properties[name] = value; + continue; + } + + // Unknown Condition shape: don't assume it's true. } return properties; + + static string NormalizeCondition(string condition) => + Regex.Replace(condition, @"\s+", " ", RegexOptions.CultureInvariant).Trim(); } async Task<(string Id, NuGetVersion Version)[]> GetAllReferencedNuGetPackages() @@ -576,17 +604,19 @@ await ListAllPackageDependencies( async IAsyncEnumerable<(string Id, string? Version)> GetReferencedNuGetPackages() { - var seenIds = new HashSet(StringComparer.OrdinalIgnoreCase); + var emittedIds = new HashSet(StringComparer.OrdinalIgnoreCase); var result = await DependencyScanner.ScanDirectoryAsync(rootFolder / "src", null).ConfigureAwait(false); foreach (var item in result) { if (item.Type is not DependencyType.NuGet || item.Name is null) continue; - // Skip duplicate top-level package IDs before any version resolution - // or dependency walking; the same package showing up in multiple - // projects in src/ shouldn't fan out into repeated NuGet work. - if (!seenIds.Add(item.Name)) + // Skip duplicate top-level package IDs we've already yielded; the same + // package showing up in multiple projects in src/ shouldn't fan out + // into repeated NuGet work. Don't mark the ID as seen yet — only after + // we've successfully resolved its version, so that a first sighting + // with an unresolved $(...) doesn't lock out the pinned fallback. + if (emittedIds.Contains(item.Name)) continue; var version = item.Version; @@ -601,6 +631,7 @@ await ListAllPackageDependencies( continue; } + emittedIds.Add(item.Name); yield return (item.Name, version); } @@ -608,7 +639,7 @@ await ListAllPackageDependencies( // generation reproducibly refreshes the analyzer set the SDK injects, // even when DependencyScanner doesn't surface them at all. foreach (var (packageId, version) in injectedAnalyzerVersions) - if (seenIds.Add(packageId)) + if (emittedIds.Add(packageId)) yield return (packageId, version); } From bf2269e82d096b52dab2e7de29cc28fd1eed682e Mon Sep 17 00:00:00 2001 From: ANcpLua Date: Sun, 10 May 2026 06:02:08 +0000 Subject: [PATCH 20/27] fix: correct pinned-package error message to reflect nuget.org The generator hard-codes Repository.Factory.GetCoreV3 and the package download source to https://api.nuget.org/v3/index.json, so the 'configured NuGet feeds' phrasing was misleading. Update the message to name nuget.org explicitly. Co-authored-by: Codesmith --- tools/ConfigFilesGenerator/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index 8277e94..88f40ec 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -548,7 +548,7 @@ await ListAllPackageDependencies(packageIdentity, [repository], NuGetFramework.A foreach (var pinnedId in pinnedVersions.Keys) if (!foundIds.Contains(pinnedId)) throw new InvalidOperationException( - $"Pinned analyzer package '{pinnedId}@{pinnedVersions[pinnedId]}' could not be resolved on the configured NuGet feeds."); + $"Pinned analyzer package '{pinnedId}@{pinnedVersions[pinnedId]}' could not be resolved on nuget.org."); var deduplicatedPackages = foundPackages .GroupBy(static p => p.Id, StringComparer.OrdinalIgnoreCase) From fde3187c67295b0ed58f9dee656461548d2d4097 Mon Sep 17 00:00:00 2001 From: ANcpLua Date: Sun, 10 May 2026 06:07:22 +0000 Subject: [PATCH 21/27] fix: set ContinuousIntegrationBuild before warnings-as-errors guards PropertyGroups evaluate in document order, so the previous layout had the warning-as-error conditions checking $(ContinuousIntegrationBuild) *before* the CI-only PropertyGroup that defined it. CI Debug builds (.github/workflows/nuget-publish.yml runs dotnet test without -c Release) were therefore bypassing the intended enforcement. Move the CI marker assignment ahead of the warning-as-error properties. Co-authored-by: Codesmith --- Directory.Build.props | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 6f75465..428f367 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,4 +1,14 @@ + + + true + + latest @@ -13,11 +23,6 @@ true - - - true - - - true + true From d517b7e0c70518a838f88959b393049255a87db1 Mon Sep 17 00:00:00 2001 From: ANcpLua Date: Sun, 10 May 2026 06:25:58 +0000 Subject: [PATCH 25/27] fix: stable-hash global_level fallback and tuple-keyed pkg dedup GetReferencedNuGetPackages was deduping by package id alone, so when the same id appeared with different versions (e.g. Microsoft.CodeAnalysis.CSharp without a version in src/*.csproj plus a VersionOverride in SourceGenerators.targets) the first sighting won and later occurrences got dropped before the downstream highest-version dedup could see them. Switch the early-skip to an (id, resolved-version) tuple via a small file-scoped comparer so identical (id, version) pairs are still pruned but distinct versions of the same id flow through. GetAnalyzerConfigGlobalLevels was assigning the non-reserved fallback as 1000+i, where i indexed into the full alphabetically-sorted package list. Adding or removing any unrelated package shifted i and re-numbered every later entry's level, churning the regenerated configs. Derive the fallback from a deterministic FNV-1a hash of the package id (uppercased UTF-8) reduced into [1000,9999], with linear-probe collision resolution that wraps inside the same range. Stable IDs keep their level when unrelated packages join or leave; collisions only renumber the colliding neighbor. Co-authored-by: Codesmith --- tools/ConfigFilesGenerator/Program.cs | 77 ++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index 16a1273..a72b407 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -433,10 +433,11 @@ static IReadOnlyDictionary GetAnalyzerConfigGlobalLevels(IEnumerabl // Low range is reserved for known "core" analyzer packs. NuGet IDs are // case-insensitive, so the reserved-ID matches must be case-insensitive too. var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - for (var i = 0; i < orderedIds.Length; i++) + var assigned = new HashSet(); + + foreach (var id in orderedIds) { - var id = orderedIds[i]; - result[id] = id switch + var reserved = id switch { _ when string.Equals(id, "Microsoft.CodeAnalysis.Analyzers", StringComparison.OrdinalIgnoreCase) => 10, _ when string.Equals(id, "Microsoft.CodeAnalysis.NetAnalyzers", StringComparison.OrdinalIgnoreCase) => 11, @@ -446,11 +447,43 @@ _ when string.Equals(id, "AwesomeAssertions.Analyzers", StringComparison.Ordinal // xunit.analyzers ships as a transitive of xunit.v3 (resolved via Version.props // property expansion). Reserved so its config doesn't churn when the package set shifts. _ when string.Equals(id, "xunit.analyzers", StringComparison.OrdinalIgnoreCase) => 15, - _ => 1000 + i + _ => -1 }; + + if (reserved >= 0) + { + result[id] = reserved; + assigned.Add(reserved); + continue; + } + + // Derive a stable level from the package ID itself (FNV-1a over the + // upper-cased UTF-8 bytes is process-deterministic, unlike the runtime's + // randomized GetHashCode). Linear-probe on collision so adding or + // removing unrelated packages doesn't renumber neighbors. + var level = (int)(StableFnv1a(id) % 9000UL) + 1000; + while (assigned.Contains(level)) + level = level == 9999 ? 1000 : level + 1; + + result[id] = level; + assigned.Add(level); } return result; + + static ulong StableFnv1a(string value) + { + const ulong FnvOffsetBasis = 14695981039346656037UL; + const ulong FnvPrime = 1099511628211UL; + var bytes = System.Text.Encoding.UTF8.GetBytes(value.ToUpperInvariant()); + var hash = FnvOffsetBasis; + foreach (var b in bytes) + { + hash ^= b; + hash *= FnvPrime; + } + return hash; + } } static IReadOnlyDictionary LoadMsBuildProperties(FullPath rootFolder) @@ -604,6 +637,14 @@ await ListAllPackageDependencies( async IAsyncEnumerable<(string Id, string? Version)> GetReferencedNuGetPackages() { + // Dedupe on (id, resolved-version) tuples — NOT id alone — so that the + // same package showing up with two different versions (e.g. + // Microsoft.CodeAnalysis.CSharp without a version in src/*.csproj plus a + // VersionOverride in SourceGenerators.targets) both reach the downstream + // dedup pass, which already prefers pinned versions and falls back to + // the highest. Identical (id, version) pairs are still skipped to avoid + // redundant NuGet dependency walks. + var emittedKeys = new HashSet<(string Id, string? Version)>(VersionedPackageKeyComparer.Instance); var emittedIds = new HashSet(StringComparer.OrdinalIgnoreCase); var result = await DependencyScanner.ScanDirectoryAsync(rootFolder / "src", null).ConfigureAwait(false); foreach (var item in result) @@ -611,14 +652,6 @@ await ListAllPackageDependencies( if (item.Type is not DependencyType.NuGet || item.Name is null) continue; - // Skip duplicate top-level package IDs we've already yielded; the same - // package showing up in multiple projects in src/ shouldn't fan out - // into repeated NuGet work. Don't mark the ID as seen yet — only after - // we've successfully resolved its version, so that a first sighting - // with an unresolved $(...) doesn't lock out the pinned fallback. - if (emittedIds.Contains(item.Name)) - continue; - var version = item.Version; // Centrally-managed PackageReferences carry property-based versions // (e.g. "$(XunitV3Version)"). Resolve them via Version.props so their @@ -631,6 +664,9 @@ await ListAllPackageDependencies( continue; } + if (!emittedKeys.Add((item.Name, version))) + continue; + emittedIds.Add(item.Name); yield return (item.Name, version); } @@ -918,6 +954,23 @@ file sealed record AnalyzerRule( file sealed record AnalyzerConfiguration(string Id, string[] Comments, DiagnosticSeverity? Severity); +// Equality comparer for (id, version) where id is case-insensitive and +// version is compared ordinally. Used to dedupe top-level package yields +// without collapsing distinct versions of the same id. +file sealed class VersionedPackageKeyComparer : IEqualityComparer<(string Id, string? Version)> +{ + public static readonly VersionedPackageKeyComparer Instance = new(); + + public bool Equals((string Id, string? Version) x, (string Id, string? Version) y) => + StringComparer.OrdinalIgnoreCase.Equals(x.Id, y.Id) && + StringComparer.Ordinal.Equals(x.Version, y.Version); + + public int GetHashCode((string Id, string? Version) obj) => + HashCode.Combine( + StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Id), + obj.Version is null ? 0 : StringComparer.Ordinal.GetHashCode(obj.Version)); +} + internal static partial class Patterns { public static readonly Regex DiagnosticSeverity = MyRegex(); From 7034f77e280cde7b5db619257e8406e25b35a4ef Mon Sep 17 00:00:00 2001 From: ANcpLua Date: Sun, 10 May 2026 06:31:19 +0000 Subject: [PATCH 26/27] fix: rename opt-out property to EnableEditorConfigDogfooding The earlier reviewer guidance asked for the public opt-out switch to be EnableEditorConfigDogfooding, but the PropertyGroup and ItemGroup shipped with EnableRepoDogfoodEditorConfigs, so a consumer setting the documented name to false still got the repo editorconfigs injected. Rename both the default-definition and the ItemGroup Condition to the contract name. Co-authored-by: Codesmith --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index f7acbbe..3fa7e27 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -39,10 +39,10 @@ the canonical SDK-shipped editorconfigs so this repo eats its own dog food. --> - true + true - + From cb4e6cb4c5a40a810126970fa65ff62dfd7b0168 Mon Sep 17 00:00:00 2001 From: ANcpLua Date: Sun, 10 May 2026 08:19:10 +0000 Subject: [PATCH 27/27] fix: track visited packages in a typed PackageIdentity set ListAllPackageDependencies's early-exit was 'dependencies.Contains(package)' where dependencies is HashSet and package is PackageIdentity. The call did compile (IEnumerable covariance lets the LINQ Contains extension bind with TSource=PackageIdentity), but it silently degraded to an O(n) linear scan because the set's hash bucket lookup is keyed on SourcePackageDependencyInfo rather than PackageIdentity, and intent was muddled. Add a separate HashSet with PackageIdentityComparer.Default; the early-exit is now an explicit O(1) hash lookup with the right comparer, and the existing HashSet stays focused on accumulating the resolved dependency infos for downstream use. Co-authored-by: Codesmith --- tools/ConfigFilesGenerator/Program.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tools/ConfigFilesGenerator/Program.cs b/tools/ConfigFilesGenerator/Program.cs index a72b407..286b675 100644 --- a/tools/ConfigFilesGenerator/Program.cs +++ b/tools/ConfigFilesGenerator/Program.cs @@ -540,6 +540,7 @@ static string NormalizeCondition(string condition) => async Task<(string Id, NuGetVersion Version)[]> GetAllReferencedNuGetPackages() { var foundPackages = new HashSet(); + var visitedPackages = new HashSet(PackageIdentityComparer.Default); using var cache = new SourceCacheContext(); var repository = Repository.Factory.GetCoreV3("https://api.nuget.org/v3/index.json"); @@ -559,7 +560,7 @@ static string NormalizeCondition(string condition) => var packageIdentity = new PackageIdentity(package.Id, version); await ListAllPackageDependencies(packageIdentity, [repository], NuGetFramework.AnyFramework, cache, - NullLogger.Instance, foundPackages, CancellationToken.None).ConfigureAwait(false); + NullLogger.Instance, foundPackages, visitedPackages, CancellationToken.None).ConfigureAwait(false); } // Deduplicate by package ID. For SDK-injected analyzers, honor the version @@ -606,10 +607,17 @@ static async Task ListAllPackageDependencies( SourceCacheContext cache, ILogger logger, ISet dependencies, + ISet visited, CancellationToken cancellationToken) { + // Track visited identities in a typed PackageIdentity set so the + // early-exit check is an O(1) hash lookup (using NuGet's canonical + // PackageIdentityComparer) rather than the O(n) LINQ Contains that + // covariance silently produced when querying a HashSet typed for + // SourcePackageDependencyInfo with a base PackageIdentity argument. + if (!visited.Add(package)) return; + var sourceRepositories = repositories as IReadOnlyList ?? repositories.ToList(); - if (dependencies.Contains(package)) return; foreach (var repository in sourceRepositories) { @@ -630,6 +638,7 @@ await ListAllPackageDependencies( cache, logger, dependencies, + visited, cancellationToken).ConfigureAwait(false); } }