Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .claude/TASK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# TASK — two arcs: (A) hygiene: real warnings-as-errors + zero-warning build → 2.2.34; (B) features: GetFullyQualifiedMetadataName + cachability assertions → 2.2.35

Status: active

## Arc A — hygiene (this branch: `claude/warnings-as-errors`)
The `TreatWarningsAsErrors` gate keys on `ContinuousIntegrationBuild`, which CI never sets — so CI
builds green with ~236 unique analyzer warnings. Plan:
- [x] Map `CI=true` → `ContinuousIntegrationBuild=true` in `Directory.Build.props`.
- [x] Fixed in code: CA1305 (TypeCache invariant culture), CA1307 (Guard.Path ordinal Contains,
test Replace calls), CA2007 (Testing await-using ConfigureAwait scope pattern ×5),
CA1063 (SolutionRefactoringTest full Dispose pattern), CA1859 (3 private helpers → concrete
return types), CA1822 (dead Dispose on private NonGenericEnumerator removed), RS2008
(pragma, test-only descriptor), CA1849 (pragma ×2 — CancelAsync unavailable on netstandard2.0),
plus 6 xUnit1051 errors the armed gate caught in the 2.2.30–33 tests.
- [x] Configured off with written justification in `.editorconfig`: CA1062 (redundant with
enforced NRT), CA1815, CA1031, CA1034, CA1054/CA1055, CA1028, CA1024, CA1710/CA1711/CA1721,
CA2225, CA1819.
- [x] `CI=true` clean rebuild: **0 warnings, 0 errors**; 215 tests passed.
- [ ] PR → auto-merge → publish **2.2.34** (auto-bump), verify tag+index, bump qyl.

## Arc B — features (branch: `claude/metadata-name-caching-assertions`, after A merges)
- [ ] `TypeDeclarationInfo.GetFullyQualifiedMetadataName()` — reconstruct
``Deep.Outer`1+Middle+Inner`1`` from the snapshot (inverse of `From` w.r.t. lookup), for
`Compilation.GetTypeByMetadataName` in later pipeline stages. Tests incl. round-trip:
`GetTypeByMetadataName(info.GetFullyQualifiedMetadataName())` resolves the original symbol.
- [ ] Cachability assertions in `.Testing`: run-twice helper asserting all tracked incremental
steps report `IncrementalStepRunReason.Cached`/`Unchanged` on the second run (TrackingNames
pattern). Integrate with existing CachingHintBuilder/ModelEqualityClassifier if they overlap.
- [ ] Tests, README, build+test green.
- [ ] PR → merge → publish **2.2.35**, verify, bump qyl 2.2.34 → 2.2.35.
- [ ] Remove this file in a final `.claude/`-only PR.

## Notes
- Latest: 2.2.33 (qyl on 2.2.33 via qyl#492).
- cref discipline: overloaded members always get explicit parameter lists (memory: cref-overloads-explicit).
- Known ambient: none — ambiguous crefs were cleared in 2.2.33.
46 changes: 46 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1 +1,47 @@
root = true

# Analyzer tuning for a Roslyn utility library (AnalysisLevel=latest-all baseline).
# Every disabled rule below is a deliberate, categorical decision — new warning IDs
# not listed here fail the CI build (warnings-as-errors when ContinuousIntegrationBuild=true).

[*.cs]
# Nullable reference types are enabled and enforced repo-wide; inputs are Roslyn pipeline
# objects that are non-null by contract. Null guards on ~390 public members would add
# hot-path cost and churn without catching real defects NRT doesn't already catch.
dotnet_diagnostic.CA1062.severity = none

# Value types here are scopes (IndentScope, TypeDeclarationScope, ScopedActivity), builders
# (HashCombiner), and enumerator/entry structs — equality on them is meaningless by design.
dotnet_diagnostic.CA1815.severity = none

# Deliberate catch-alls: Try*/SelectAndCaptureExceptions/DiagnosticFlow.Try exist to convert
# arbitrary user-code exceptions into data (diagnostics/results). OperationCanceledException
# is rethrown where it matters.
dotnet_diagnostic.CA1031.severity = none

# Nested public types are the established zero-allocation enumerator pattern
# (LineSplitEnumerator/LineSplitEntry) and intentional grouping (TimeConversions.Iso8601).
dotnet_diagnostic.CA1034.severity = none

# String-typed URLs are intentional: OTel schema URLs are opaque identifiers, and the
# QueryString/ShortId helpers are string-in/string-out by design.
dotnet_diagnostic.CA1054.severity = none
dotnet_diagnostic.CA1055.severity = none

# OTel enums are byte-backed to match the wire format.
dotnet_diagnostic.CA1028.severity = none

# Method-vs-property and naming rules would break shipped public API
# (EquatableArray<T>, ReadOnlyCollection, ReadOnlyDictionary, HashCombiner.HashCode).
dotnet_diagnostic.CA1024.severity = none
dotnet_diagnostic.CA1710.severity = none
dotnet_diagnostic.CA1711.severity = none
dotnet_diagnostic.CA1721.severity = none

# Implicit operators ship documented named alternates using the As* naming family
# (AsImmutableArray, AsEquatableArray); CA2225 only recognizes To*/From* names.
dotnet_diagnostic.CA2225.severity = none

# Array-returning properties on result/report types (BuildResult, DataReaderExtensions)
# expose materialized snapshots by design.
dotnet_diagnostic.CA1819.severity = none
3 changes: 3 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
<EnableNETAnalyzers Condition="'$(EnableNETAnalyzers)' == ''">true</EnableNETAnalyzers>
<AnalysisLevel Condition="'$(AnalysisLevel)' == ''">latest-all</AnalysisLevel>

<!-- GitHub Actions only sets CI=true; without this mapping the warnings-as-errors gate below never fires in CI. -->
<ContinuousIntegrationBuild Condition="'$(ContinuousIntegrationBuild)' == '' AND '$(CI)' == 'true'">true</ContinuousIntegrationBuild>

<MSBuildTreatWarningsAsErrors Condition="'$(MSBuildTreatWarningsAsErrors)' == '' AND '$(ContinuousIntegrationBuild)' == 'true'">true</MSBuildTreatWarningsAsErrors>
<TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == '' AND '$(ContinuousIntegrationBuild)' == 'true'">true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild Condition="'$(EnforceCodeStyleInBuild)' == ''">true</EnforceCodeStyleInBuild>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,10 @@ public static async Task<FullPath> Get(NetSdkVersion version)
else
{
using var ms = new MemoryStream(bytes);
await using var gz = new GZipStream(ms, CompressionMode.Decompress);
await using var tar = new TarReader(gz);
var gz = new GZipStream(ms, CompressionMode.Decompress);
await using var gzScope = gz.ConfigureAwait(false);
var tar = new TarReader(gz);
await using var tarScope = tar.ConfigureAwait(false);
while ((await tar.GetNextEntryAsync().ConfigureAwait(false)) is { } entry)
{
var destinationPath = tempFolder / entry.Name;
Expand All @@ -160,7 +162,8 @@ public static async Task<FullPath> Get(NetSdkVersion version)
if (Path.GetDirectoryName(destinationPath) is { } parentDir)
Directory.CreateDirectory(parentDir);
var entryStream = entry.DataStream;
await using var outputStream = File.Create(destinationPath);
var outputStream = File.Create(destinationPath);
await using var outputScope = outputStream.ConfigureAwait(false);
if (entryStream is not null) await entryStream.CopyToAsync(outputStream).ConfigureAwait(false);
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,11 +290,12 @@ protected override async Task<BuildResult> QuickBuild(
string tfm = Tfm.Net100,
params (string Key, string Value)[] extraProps)
{
await using var project = CreateProjectBuilder(
var project = CreateProjectBuilder(
TestOutputHelper,
Fixture.PackageDirectory,
"TestPackage",
Fixture.Version);
await using var projectScope = project.ConfigureAwait(false);

return await project
.WithTargetFramework(tfm)
Expand Down Expand Up @@ -341,11 +342,12 @@ protected override async Task<BuildResult> BuildExe(
string tfm = Tfm.Net100,
params (string Key, string Value)[] extraProps)
{
await using var project = CreateProjectBuilder(
var project = CreateProjectBuilder(
TestOutputHelper,
Fixture.PackageDirectory,
"TestPackage",
Fixture.Version);
await using var projectScope = project.ConfigureAwait(false);

return await project
.WithTargetFramework(tfm)
Expand Down
14 changes: 13 additions & 1 deletion src/ANcpLua.Roslyn.Utilities.Testing/SolutionRefactoringTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,22 @@ public abstract class SolutionRefactoringTest<TRefactoring> : IDisposable
/// </summary>
public void Dispose()
{
_workspace.Dispose();
Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Disposes the underlying workspace when <paramref name="disposing" /> is <c>true</c>.
/// </summary>
/// <param name="disposing">
/// <c>true</c> when called from <see cref="Dispose()" />; <c>false</c> when called from a finalizer.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
_workspace.Dispose();
}

/// <summary>
/// Verifies a refactoring across multiple documents in a single project.
/// </summary>
Expand Down
4 changes: 4 additions & 0 deletions src/ANcpLua.Roslyn.Utilities/Async/ParallelAsyncExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,9 @@ void RecordFailure(Exception ex)
}
finally
{
#pragma warning disable CA1849 // CancelAsync is unavailable on netstandard2.0
cts.Cancel();
#pragma warning restore CA1849
try
{
await completion.ConfigureAwait(false);
Expand Down Expand Up @@ -317,7 +319,9 @@ void RecordFailure(Exception ex)
}
finally
{
#pragma warning disable CA1849 // CancelAsync is unavailable on netstandard2.0
cts.Cancel();
#pragma warning restore CA1849
try
{
await completion.ConfigureAwait(false);
Expand Down
2 changes: 1 addition & 1 deletion src/ANcpLua.Roslyn.Utilities/CodeGeneration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public static string RestoreWarnings(params string[] warningIds)
return warningIds.Length is 0 ? string.Empty : $"#pragma warning restore {string.Join(", ", ValidateWarningIds(warningIds))}";
}

private static IReadOnlyList<string> ValidateWarningIds(string[] warningIds)
private static string[] ValidateWarningIds(string[] warningIds)
{
foreach (var warningId in warningIds)
if (!IsValidWarningId(warningId))
Expand Down
2 changes: 1 addition & 1 deletion src/ANcpLua.Roslyn.Utilities/DocumentationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ static bool IsEligibleForAutomaticInheritdoc(ISymbol symbol)
}
}

private static IEnumerable<XNode> RewriteInheritdocElements(
private static XNode[] RewriteInheritdocElements(
ISymbol symbol,
HashSet<ISymbol>? visitedSymbols,
Compilation compilation,
Expand Down
4 changes: 0 additions & 4 deletions src/ANcpLua.Roslyn.Utilities/Enumerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ public bool MoveNext()
public void Reset()
{
}

public void Dispose()
{
}
}

private static class EmptyEnumeratorCache<T>
Expand Down
4 changes: 2 additions & 2 deletions src/ANcpLua.Roslyn.Utilities/Guard.Path.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public static string ValidExtension(
if (value.StartsWith(".", StringComparison.Ordinal))
throw new ArgumentException("Extension must not start with a period ('.').", paramName);

if (value.Contains('\\') || value.Contains('/'))
if (value.Contains('\\', StringComparison.Ordinal) || value.Contains('/', StringComparison.Ordinal))
throw new ArgumentException("Extension must not contain path separators.", paramName);

return value;
Expand Down Expand Up @@ -232,7 +232,7 @@ public static string NormalizedExtension(
{
NotNullOrEmpty(value, paramName);

if (value.Contains('\\') || value.Contains('/'))
if (value.Contains('\\', StringComparison.Ordinal) || value.Contains('/', StringComparison.Ordinal))
throw new ArgumentException("Extension must not contain path separators.", paramName);

return value.StartsWith(".", StringComparison.Ordinal) ? value : $".{value}";
Expand Down
2 changes: 1 addition & 1 deletion src/ANcpLua.Roslyn.Utilities/SyntaxExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ public static bool IsPrimitiveKeyword(this string typeName)
/// from <c>"Dictionary&lt;string, List&lt;int&gt;&gt;"</c>).
/// </param>
/// <returns>A list of individual type argument strings, trimmed of whitespace.</returns>
private static IEnumerable<string> ParseGenericArguments(string argsContent)
private static List<string> ParseGenericArguments(string argsContent)
{
var args = new List<string>();
var depth = 0;
Expand Down
4 changes: 2 additions & 2 deletions src/ANcpLua.Roslyn.Utilities/TypeCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public TypeCache(Func<TEnum, INamedTypeSymbol?> resolver)
var maxValue = 0;
foreach (TEnum value in enumValues)
{
var intValue = Convert.ToInt32(value);
var intValue = Convert.ToInt32(value, CultureInfo.InvariantCulture);
if (intValue > maxValue)
maxValue = intValue;
}
Expand All @@ -59,7 +59,7 @@ public TypeCache(Func<TEnum, INamedTypeSymbol?> resolver)
/// </remarks>
public INamedTypeSymbol? Get(TEnum type)
{
var index = Convert.ToInt32(type);
var index = Convert.ToInt32(type, CultureInfo.InvariantCulture);
if (index < 0 || index >= _cache.Length)
return null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public void Method() { }
}
""";

var tree = CSharpSyntaxTree.ParseText(source.Replace("ARG", argumentExpression));
var tree = CSharpSyntaxTree.ParseText(source.Replace("ARG", argumentExpression, StringComparison.Ordinal));
var semanticModel = GetSemanticModel(tree);
var method = tree.GetRoot().DescendantNodes().OfType<MethodDeclarationSyntax>().Single();

Expand Down Expand Up @@ -89,7 +89,7 @@ public void Method() { }
}
""";

var tree = CSharpSyntaxTree.ParseText(source.Replace("ARG", argumentExpression));
var tree = CSharpSyntaxTree.ParseText(source.Replace("ARG", argumentExpression, StringComparison.Ordinal));
var semanticModel = GetSemanticModel(tree);
var method = tree.GetRoot().DescendantNodes().OfType<MethodDeclarationSyntax>().Single();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Threading;
using ANcpLua.Roslyn.Utilities;
Expand Down Expand Up @@ -97,7 +98,7 @@ public void SelectAndReportExceptions_SingleValueOverload_ReportsDiagnosticWitho
result.Exception.Should().BeNull();
result.Diagnostics.Should().ContainSingle(diagnostic =>
diagnostic.Id == "SINGLE001" &&
diagnostic.GetMessage().Contains("single boom", StringComparison.Ordinal));
diagnostic.GetMessage(CultureInfo.InvariantCulture).Contains("single boom", StringComparison.Ordinal));
}

private static InvalidOperationException CaptureThrow()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Globalization;
using System.Linq;
using ANcpLua.Roslyn.Utilities.Models;
using AwesomeAssertions;
Expand All @@ -18,13 +19,15 @@ public class Widget
}
""";

#pragma warning disable RS2008 // release tracking does not apply to a test-only descriptor
private static readonly DiagnosticDescriptor Descriptor = new(
"TST001",
"Test title",
"{0}",
"Tests",
DiagnosticSeverity.Warning,
true);
#pragma warning restore RS2008

[Fact]
public void ToLocation_WithOriginTree_ReturnsTreeBoundLocation()
Expand Down Expand Up @@ -57,7 +60,7 @@ public void ToLocation_WithTreeTooShortForSpan_FallsBackToPathBasedLocation()
{
var (_, node) = ParseWidget();
var info = LocationInfo.From(node);
var unrelatedShortTree = CSharpSyntaxTree.ParseText("//");
var unrelatedShortTree = CSharpSyntaxTree.ParseText("//", cancellationToken: TestContext.Current.CancellationToken);

var location = info.ToLocation(unrelatedShortTree);

Expand All @@ -75,7 +78,7 @@ public void ToDiagnostic_WithTree_ReportsTreeBoundLocation()

diagnostic.Location.IsInSource.Should().BeTrue();
diagnostic.Location.SourceTree.Should().BeSameAs(tree);
diagnostic.GetMessage().Should().Be("message");
diagnostic.GetMessage(CultureInfo.InvariantCulture).Should().Be("message");
}

[Fact]
Expand All @@ -89,7 +92,7 @@ public void ToDiagnostic_WithNullTree_MatchesParameterlessBehavior()

withNull.Location.IsInSource.Should().BeFalse();
withNull.Location.GetLineSpan().Should().Be(parameterless.Location.GetLineSpan());
withNull.GetMessage().Should().Be(parameterless.GetMessage());
withNull.GetMessage(CultureInfo.InvariantCulture).Should().Be(parameterless.GetMessage(CultureInfo.InvariantCulture));
}

private static (SyntaxTree Tree, ClassDeclarationSyntax Node) ParseWidget()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,15 @@ public void BeginDeclaration_Output_CompilesTogetherWithOriginalSource()

var compilation = CSharpCompilation.Create(
"PartialMerge",
[CSharpSyntaxTree.ParseText(NestedSource), CSharpSyntaxTree.ParseText(builder.ToString())],
[
CSharpSyntaxTree.ParseText(NestedSource, cancellationToken: TestContext.Current.CancellationToken),
CSharpSyntaxTree.ParseText(builder.ToString(), cancellationToken: TestContext.Current.CancellationToken)
],
[MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

var errors = compilation.GetDiagnostics().Where(d => d.Severity is DiagnosticSeverity.Error).ToArray();
var errors = compilation.GetDiagnostics(TestContext.Current.CancellationToken)
.Where(d => d.Severity is DiagnosticSeverity.Error).ToArray();
errors.Should().BeEmpty();
}

Expand Down Expand Up @@ -318,7 +322,8 @@ public void GetHintName_IsAcceptedByRoslynHintNameValidation()
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

var driver = CSharpGeneratorDriver.Create(new AddSourceProbeGenerator(hintName).AsSourceGenerator());
var result = driver.RunGenerators(compilation).GetRunResult().Results.Single();
var result = driver.RunGenerators(compilation, TestContext.Current.CancellationToken)
.GetRunResult().Results.Single();

result.Exception.Should().BeNull();
result.GeneratedSources.Length.Should().Be(1);
Expand All @@ -337,7 +342,7 @@ private static INamedTypeSymbol GetType(string source, string metadataName)
{
var compilation = CSharpCompilation.Create(
"TypeDeclarationShapes",
[CSharpSyntaxTree.ParseText(source)],
[CSharpSyntaxTree.ParseText(source, cancellationToken: TestContext.Current.CancellationToken)],
[MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

Expand All @@ -347,6 +352,6 @@ private static INamedTypeSymbol GetType(string source, string metadataName)

private static string Normalize(string text)
{
return text.Replace("\r\n", "\n");
return text.Replace("\r\n", "\n", StringComparison.Ordinal);
}
}