Skip to content

Commit 95a47a1

Browse files
committed
Enforce Result usage during compilation and release 10.3.12
1 parent ecb1ec3 commit 95a47a1

123 files changed

Lines changed: 674 additions & 199 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ jobs:
3939
--coverage-output-format cobertura
4040
--output Normal
4141
42+
- name: Pack NuGet packages
43+
run: dotnet pack --configuration Release --no-build --output ./artifacts/packages
44+
45+
- name: Verify packaged compiler analyzers
46+
run: bash scripts/verify-analyzer-packages.sh ./artifacts/packages "$(sed -n 's/.*<Version>\(.*\)<\/Version>.*/\1/p' Directory.Build.props | tr -d '\r')"
47+
4248
- name: Upload coverage reports to Codecov
4349
uses: codecov/codecov-action@v7.1.1
4450
with:

‎.github/workflows/release.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ jobs:
4848
- name: Pack NuGet packages
4949
run: dotnet pack --configuration Release --no-build --output ./artifacts
5050

51+
- name: Verify packaged compiler analyzers
52+
run: bash scripts/verify-analyzer-packages.sh ./artifacts "${{ steps.version.outputs.version }}"
53+
5154
- name: Upload artifacts
5255
uses: actions/upload-artifact@v7.0.2
5356
with:
@@ -82,7 +85,6 @@ jobs:
8285

8386
- name: Publish to NuGet
8487
id: publish
85-
continue-on-error: true
8688
run: |
8789
set +e
8890
OUTPUT=""

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ If I tell you to remember something, you do the same, update
99

1010

1111
## Rules to follow
12+
- Ship Communication usage analyzers inside every published runtime NuGet package and run them during ordinary compilation with Error severity. Use `IsFailed` for failure checks and consume the proven non-null `Problem`/payload directly; reject redundant guards and suppressions only when semantic flow proves them redundant.
1213
- A successful `Result<T>` must always carry a non-null `Value`, matching its `MemberNotNullWhen` annotations. Reject null success payloads at the owning result setter/factory, including nullable type arguments and initializer overrides. Use non-generic `Result.Succeed()` for a success without a payload; never preserve a successful null value to accommodate a consumer null guard.
1314
- Communication Orleans integration uses native generated Orleans serialization and its typed surrogates only. Do not add Newtonsoft.Json, JSON grain-storage adapters/converters, or automatic JSON storage registrations. Applications own their storage-provider serialization.
1415
- For failure logging, use Warning with failure details when there is no exception, and Error with the original stack trace when an exception is supplied.

‎Directory.Build.props‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
<RepositoryUrl>https://github.com/managedcode/Communication</RepositoryUrl>
2828
<PackageProjectUrl>https://github.com/managedcode/Communication</PackageProjectUrl>
2929
<Product>Managed Code - Communication</Product>
30-
<Version>10.3.11</Version>
31-
<PackageVersion>10.3.11</PackageVersion>
30+
<Version>10.3.12</Version>
31+
<PackageVersion>10.3.12</PackageVersion>
3232

3333
</PropertyGroup>
3434
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
@@ -44,4 +44,12 @@
4444
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4545
</PackageReference>
4646
</ItemGroup>
47+
<ItemGroup Condition="'$(MSBuildProjectName)' != 'ManagedCode.Communication.Analyzers' and '$(MSBuildProjectName)' != 'ManagedCode.Communication.Tests'">
48+
<ProjectReference Include="$(MSBuildThisFileDirectory)ManagedCode.Communication.Analyzers/ManagedCode.Communication.Analyzers.csproj"
49+
OutputItemType="Analyzer" ReferenceOutputAssembly="false" PrivateAssets="all" />
50+
</ItemGroup>
51+
<ItemGroup Condition="'$(MSBuildProjectName)' == 'ManagedCode.Communication' or '$(MSBuildProjectName)' == 'ManagedCode.Communication.Extensions' or '$(MSBuildProjectName)' == 'ManagedCode.Communication.AspNetCore' or '$(MSBuildProjectName)' == 'ManagedCode.Communication.Orleans'">
52+
<None Include="$(MSBuildThisFileDirectory)ManagedCode.Communication.Analyzers/bin/$(Configuration)/netstandard2.0/ManagedCode.Communication.Analyzers.dll"
53+
Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
54+
</ItemGroup>
4755
</Project>

‎Directory.Packages.props‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="2.0.5" />
99
<PackageVersion Include="ManagedCode.Orleans.RateLimiting.Core" Version="10.4.2" />
1010
<PackageVersion Include="ManagedCode.Orleans.RateLimiting.Server" Version="10.4.2" />
11+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
1112
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.12" />
1213
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="10.0.12" />
1314
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.12" />
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
; Shipped analyzer releases
2+
3+
## Release 10.3.12
4+
5+
### New Rules
6+
7+
Rule ID | Category | Severity | Notes
8+
--------|----------|----------|------
9+
MCC0001 | Usage | Error | Use IsFailed for failure checks
10+
MCC0002 | Usage | Error | Trust the result nullability contract
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
; Analyzer release tracking
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>netstandard2.0</TargetFramework>
4+
<IsPackable>false</IsPackable>
5+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
6+
<GenerateDocumentationFile>false</GenerateDocumentationFile>
7+
</PropertyGroup>
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
10+
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
11+
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
12+
</ItemGroup>
13+
</Project>
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using System.Linq;
2+
using System.Threading;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
7+
namespace ManagedCode.Communication.Analyzers;
8+
9+
internal static class ResultStatusFlow
10+
{
11+
private const string AnnotationType = "System.Diagnostics.CodeAnalysis.MemberNotNullWhenAttribute";
12+
13+
internal static bool ProvesMember(SyntaxNode node, MemberAccessExpressionSyntax member, SemanticModel model, CancellationToken token)
14+
{
15+
var receiver = model.GetSymbolInfo(member.Expression, token).Symbol;
16+
if (receiver is not (ILocalSymbol or IParameterSymbol))
17+
{
18+
return false;
19+
}
20+
for (var current = node; current.Parent is not null; current = current.Parent)
21+
{
22+
if (current is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax || Writes(current, receiver, model, token))
23+
{
24+
return false;
25+
}
26+
if (current.Parent is BinaryExpressionSyntax binary && current == binary.Right &&
27+
(binary.IsKind(SyntaxKind.LogicalAndExpression) && Guarantees(binary.Left, true, member, receiver, model, token) ||
28+
binary.IsKind(SyntaxKind.LogicalOrExpression) && Guarantees(binary.Left, false, member, receiver, model, token)))
29+
{
30+
return true;
31+
}
32+
if (current.Parent is ConditionalExpressionSyntax conditional &&
33+
(current == conditional.WhenTrue && Guarantees(conditional.Condition, true, member, receiver, model, token) ||
34+
current == conditional.WhenFalse && Guarantees(conditional.Condition, false, member, receiver, model, token)))
35+
{
36+
return true;
37+
}
38+
if (current.Parent is IfStatementSyntax parentIf && current == parentIf.Statement &&
39+
Guarantees(parentIf.Condition, true, member, receiver, model, token) ||
40+
current.Parent is ElseClauseSyntax { Parent: IfStatementSyntax elseIf } &&
41+
Guarantees(elseIf.Condition, false, member, receiver, model, token))
42+
{
43+
return true;
44+
}
45+
if (current is StatementSyntax statement && statement.Parent is BlockSyntax block)
46+
{
47+
for (int index = block.Statements.IndexOf(statement) - 1; index >= 0; index--)
48+
{
49+
var preceding = block.Statements[index];
50+
if (Writes(preceding, receiver, model, token))
51+
{
52+
return false;
53+
}
54+
if (preceding is IfStatementSyntax { Else: null } guard &&
55+
model.AnalyzeControlFlow(guard.Statement) is { Succeeded: true, EndPointIsReachable: false } &&
56+
Guarantees(guard.Condition, false, member, receiver, model, token))
57+
{
58+
return true;
59+
}
60+
}
61+
}
62+
}
63+
return false;
64+
}
65+
66+
private static bool Writes(SyntaxNode node, ISymbol receiver, SemanticModel model, CancellationToken token)
67+
{
68+
return node.DescendantNodesAndSelf().Any(part => part switch
69+
{
70+
AssignmentExpressionSyntax assignment => References(assignment.Left, receiver, model, token),
71+
ArgumentSyntax argument when argument.RefKindKeyword.IsKind(SyntaxKind.RefKeyword) || argument.RefKindKeyword.IsKind(SyntaxKind.OutKeyword) =>
72+
References(argument.Expression, receiver, model, token),
73+
RefExpressionSyntax reference => References(reference.Expression, receiver, model, token),
74+
PrefixUnaryExpressionSyntax prefix when prefix.IsKind(SyntaxKind.PreIncrementExpression) || prefix.IsKind(SyntaxKind.PreDecrementExpression) =>
75+
References(prefix.Operand, receiver, model, token),
76+
PostfixUnaryExpressionSyntax postfix when postfix.IsKind(SyntaxKind.PostIncrementExpression) || postfix.IsKind(SyntaxKind.PostDecrementExpression) =>
77+
References(postfix.Operand, receiver, model, token),
78+
VariableDeclaratorSyntax variable => SymbolEqualityComparer.Default.Equals(model.GetDeclaredSymbol(variable, token), receiver),
79+
_ => false
80+
});
81+
}
82+
83+
private static bool References(ExpressionSyntax expression, ISymbol receiver, SemanticModel model, CancellationToken token) =>
84+
expression.DescendantNodesAndSelf().OfType<ExpressionSyntax>()
85+
.Any(part => SymbolEqualityComparer.Default.Equals(model.GetSymbolInfo(part, token).Symbol, receiver));
86+
87+
private static bool Guarantees(ExpressionSyntax condition, bool outcome, MemberAccessExpressionSyntax member,
88+
ISymbol receiver, SemanticModel model, CancellationToken token)
89+
{
90+
if (Writes(condition, receiver, model, token))
91+
{
92+
return false;
93+
}
94+
if (condition is ParenthesizedExpressionSyntax parentheses)
95+
{
96+
return Guarantees(parentheses.Expression, outcome, member, receiver, model, token);
97+
}
98+
if (condition is PrefixUnaryExpressionSyntax negation && negation.IsKind(SyntaxKind.LogicalNotExpression))
99+
{
100+
return Guarantees(negation.Operand, !outcome, member, receiver, model, token);
101+
}
102+
if (condition is BinaryExpressionSyntax binary &&
103+
(outcome && binary.IsKind(SyntaxKind.LogicalAndExpression) || !outcome && binary.IsKind(SyntaxKind.LogicalOrExpression)))
104+
{
105+
return Guarantees(binary.Left, outcome, member, receiver, model, token) ||
106+
Guarantees(binary.Right, outcome, member, receiver, model, token);
107+
}
108+
if (condition is not MemberAccessExpressionSyntax status || !ResultUsageAnalyzer.IsResultProperty(status, model, token) ||
109+
!SymbolEqualityComparer.Default.Equals(model.GetSymbolInfo(status.Expression, token).Symbol, receiver) ||
110+
model.GetSymbolInfo(status, token).Symbol is not IPropertySymbol property)
111+
{
112+
return false;
113+
}
114+
return property.GetAttributes().Any(attribute => attribute.AttributeClass?.ToDisplayString() == AnnotationType &&
115+
attribute.ConstructorArguments.Length == 2 && attribute.ConstructorArguments[0].Value is bool expected && expected == outcome &&
116+
(attribute.ConstructorArguments[1].Value is string name && name == member.Name.Identifier.ValueText ||
117+
attribute.ConstructorArguments[1].Kind == TypedConstantKind.Array &&
118+
attribute.ConstructorArguments[1].Values.Any(value => value.Value is string item && item == member.Name.Identifier.ValueText)));
119+
}
120+
}

0 commit comments

Comments
 (0)