Skip to content

Commit 1585f0c

Browse files
authored
Merge pull request #31 from Jean-Fischer/feature/csharp-13
Feature/csharp 13
2 parents f1a4ee0 + 5e9e5a2 commit 1585f0c

File tree

65 files changed

+3834
-0
lines changed

Some content is hidden

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

65 files changed

+3834
-0
lines changed

Readme.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ Rules are grouped by the C# language version they target (when applicable).
143143
| SHARPEN053 | Use default lambda parameters | Use default values in explicitly-typed lambda parameter lists when applicable. | Yes |
144144
| SHARPEN054 | Use InlineArray | Convert fixed-size buffer-like structs to `[InlineArray(N)]` when safe. | Yes |
145145

146+
### C# 13
147+
148+
| Rule ID | Title | Description | Code fix |
149+
|---|---|---|---|
150+
| SHARPEN058 | Prefer params collections | Suggest migrating non-public `params T[]` to collection-based `params` when safe. | Yes |
151+
| SHARPEN059 | Use from-end index in object initializers | Suggest using `^` indices in object/collection initializers when provably equivalent. | Yes |
152+
| SHARPEN060 | Use \e escape sequence | Suggest replacing `\u001b` / `\x1b` with `\e` when unambiguous. | Yes |
153+
| SHARPEN061 | Use System.Threading.Lock | Suggest migrating dedicated private lock objects to `System.Threading.Lock` when available and safe. | Yes |
154+
| SHARPEN062 | Partial properties/indexers refactoring | Suggest/refactor eligible members to C# 13 partial properties/indexers when safe. | No (refactoring) |
155+
| SHARPEN063 | Suggest allows ref struct constraint | Guidance-only: suggest `allows ref struct` for eligible generic APIs. | No |
156+
| SHARPEN064 | Suggest OverloadResolutionPriorityAttribute | Guidance-only: suggest `OverloadResolutionPriorityAttribute` for eligible overload sets. | No |
157+
146158
## Development
147159

148160
Open [Sharpen.Analyzer/Sharpen.Analyzer.sln](Sharpen.Analyzer/Sharpen.Analyzer.sln:1) and run the test project (`Sharpen.Analyzer.Tests`).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CodeActions;
8+
using Microsoft.CodeAnalysis.CodeFixes;
9+
using Microsoft.CodeAnalysis.CSharp;
10+
using Microsoft.CodeAnalysis.CSharp.Syntax;
11+
using Microsoft.CodeAnalysis.Editing;
12+
using Sharpen.Analyzer.Rules;
13+
using Sharpen.Analyzer.Safety.FixProviderSafety;
14+
15+
namespace Sharpen.Analyzer;
16+
17+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PartialPropertiesIndexersRefactoringCodeFixProvider)), Shared]
18+
public sealed class PartialPropertiesIndexersRefactoringCodeFixProvider : CodeFixProvider
19+
{
20+
public override ImmutableArray<string> FixableDiagnosticIds =>
21+
ImmutableArray.Create(CSharp13Rules.PartialPropertiesIndexersRefactoringRule.Id);
22+
23+
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
24+
25+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
26+
{
27+
var diagnostic = context.Diagnostics.FirstOrDefault();
28+
if (diagnostic is null)
29+
return;
30+
31+
var document = context.Document;
32+
var cancellationToken = context.CancellationToken;
33+
34+
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
35+
if (root is null)
36+
return;
37+
38+
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
39+
if (semanticModel is null)
40+
return;
41+
42+
// Safety gate.
43+
var safetyEvaluation = FixProviderSafetyRunner.EvaluateOrMatchFailed(
44+
checker: new PartialPropertiesIndexersRefactoringSafetyChecker(),
45+
syntaxTree: root.SyntaxTree,
46+
semanticModel: semanticModel,
47+
diagnostic: diagnostic,
48+
matchSucceeded: true,
49+
cancellationToken: cancellationToken);
50+
51+
if (safetyEvaluation.Outcome != FixProviderSafetyOutcome.Safe)
52+
return;
53+
54+
context.RegisterCodeFix(
55+
CodeAction.Create(
56+
title: "Refactor to partial property/indexer",
57+
createChangedDocument: ct => RefactorAsync(document, root, diagnostic, ct),
58+
equivalenceKey: nameof(PartialPropertiesIndexersRefactoringCodeFixProvider)),
59+
diagnostic);
60+
}
61+
62+
private static async Task<Document> RefactorAsync(
63+
Document document,
64+
SyntaxNode root,
65+
Diagnostic diagnostic,
66+
CancellationToken cancellationToken)
67+
{
68+
var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true);
69+
var propertyOrIndexer = node.FirstAncestorOrSelf<BasePropertyDeclarationSyntax>();
70+
if (propertyOrIndexer is null)
71+
return document;
72+
73+
// Avoid SyntaxEditor tracking issues by doing a pure syntax rewrite.
74+
var declaring = AddPartialModifier(propertyOrIndexer);
75+
var implementing = CreateImplementingDeclaration(propertyOrIndexer);
76+
77+
var newRoot = root.ReplaceNode(propertyOrIndexer, new SyntaxNode[] { declaring, implementing });
78+
return document.WithSyntaxRoot(newRoot);
79+
}
80+
81+
private static BasePropertyDeclarationSyntax AddPartialModifier(BasePropertyDeclarationSyntax member)
82+
{
83+
if (member.Modifiers.Any(SyntaxKind.PartialKeyword))
84+
return member;
85+
86+
// Keep modifier ordering: insert after accessibility modifiers if present, otherwise at start.
87+
var modifiers = member.Modifiers;
88+
var insertIndex = 0;
89+
for (var i = 0; i < modifiers.Count; i++)
90+
{
91+
if (modifiers[i].IsKind(SyntaxKind.PublicKeyword)
92+
|| modifiers[i].IsKind(SyntaxKind.PrivateKeyword)
93+
|| modifiers[i].IsKind(SyntaxKind.InternalKeyword)
94+
|| modifiers[i].IsKind(SyntaxKind.ProtectedKeyword))
95+
{
96+
insertIndex = i + 1;
97+
}
98+
}
99+
100+
modifiers = modifiers.Insert(insertIndex, SyntaxFactory.Token(SyntaxKind.PartialKeyword));
101+
return member.WithModifiers(modifiers);
102+
}
103+
104+
private static BasePropertyDeclarationSyntax CreateImplementingDeclaration(BasePropertyDeclarationSyntax original)
105+
{
106+
var implementing = AddPartialModifier(original);
107+
108+
// Ensure we have an accessor list.
109+
if (implementing.AccessorList is null)
110+
return implementing;
111+
112+
var newAccessors = implementing.AccessorList.Accessors
113+
.Select(a => a.WithBody(CreateTrivialBody(a.Kind())).WithSemicolonToken(default).WithExpressionBody(null))
114+
.ToList();
115+
116+
var accessorList = implementing.AccessorList.WithAccessors(SyntaxFactory.List(newAccessors));
117+
implementing = implementing.WithAccessorList(accessorList);
118+
119+
// Ensure the implementing part is a full declaration (not a semicolon-only property).
120+
if (implementing is PropertyDeclarationSyntax prop)
121+
{
122+
implementing = prop
123+
.WithInitializer(null)
124+
.WithSemicolonToken(default)
125+
.WithExpressionBody(null);
126+
}
127+
128+
return implementing;
129+
}
130+
131+
private static BlockSyntax CreateTrivialBody(SyntaxKind accessorKind)
132+
{
133+
// This is intentionally conservative and behavior-preserving only for auto-properties/indexers.
134+
// We generate a body that throws to force user review if they apply it in unexpected contexts.
135+
// However, analyzer/safety checker only allow auto accessors, so this should not be reachable.
136+
//
137+
// Note: We still need a syntactically valid body.
138+
var throwStatement = SyntaxFactory.ThrowStatement(
139+
SyntaxFactory.ObjectCreationExpression(
140+
SyntaxFactory.ParseTypeName("System.NotImplementedException"))
141+
.WithArgumentList(SyntaxFactory.ArgumentList()));
142+
143+
return SyntaxFactory.Block(throwStatement);
144+
}
145+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CodeFixes;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Editing;
11+
using Microsoft.CodeAnalysis.FindSymbols;
12+
using Sharpen.Analyzer.FixProvider.Common;
13+
using Sharpen.Analyzer.Rules;
14+
using Sharpen.Analyzer.Safety.FixProviderSafety;
15+
16+
namespace Sharpen.Analyzer;
17+
18+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PreferParamsCollectionsCodeFixProvider))]
19+
[Shared]
20+
public sealed class PreferParamsCollectionsCodeFixProvider : CSharp13OrAboveSharpenCodeFixProvider
21+
{
22+
public override ImmutableArray<string> FixableDiagnosticIds =>
23+
ImmutableArray.Create(CSharp13Rules.PreferParamsCollectionsRule.Id);
24+
25+
protected override async Task RegisterCodeFixesAsync(CodeFixContext context, SyntaxNode root, Diagnostic diagnostic)
26+
{
27+
var parameter = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true)
28+
.FirstAncestorOrSelf<ParameterSyntax>();
29+
if (parameter is null)
30+
return;
31+
32+
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
33+
if (semanticModel is null)
34+
return;
35+
36+
// Fix-provider-side safety gate: only offer code actions when the mapped safety checker says it's safe.
37+
var safetyEvaluation = FixProviderSafetyRunner.EvaluateOrMatchFailed(
38+
checker: new PreferParamsCollectionsSafetyChecker(),
39+
syntaxTree: root.SyntaxTree,
40+
semanticModel: semanticModel,
41+
diagnostic: diagnostic,
42+
matchSucceeded: true,
43+
cancellationToken: context.CancellationToken);
44+
45+
if (safetyEvaluation.Outcome != FixProviderSafetyOutcome.Safe)
46+
return;
47+
48+
RegisterCodeFix(
49+
context: context,
50+
diagnostic: diagnostic,
51+
title: "Prefer collection-based params",
52+
equivalenceKey: nameof(PreferParamsCollectionsCodeFixProvider),
53+
createChangedDocument: ct => ApplyAsync(context.Document, parameter, ct));
54+
}
55+
56+
private static async Task<Document> ApplyAsync(Document document, ParameterSyntax parameter, CancellationToken ct)
57+
{
58+
var semanticModel = await document.GetSemanticModelAsync(ct).ConfigureAwait(false);
59+
if (semanticModel is null)
60+
return document;
61+
62+
var method = parameter.FirstAncestorOrSelf<BaseMethodDeclarationSyntax>();
63+
if (method is null)
64+
return document;
65+
66+
var methodSymbol = semanticModel.GetDeclaredSymbol(method, ct) as IMethodSymbol;
67+
if (methodSymbol is null)
68+
return document;
69+
70+
var parameterSymbol = semanticModel.GetDeclaredSymbol(parameter, ct) as IParameterSymbol;
71+
if (parameterSymbol is null)
72+
return document;
73+
74+
// Target type: ReadOnlySpan<T>
75+
var elementType = (parameterSymbol.Type as IArrayTypeSymbol)?.ElementType;
76+
if (elementType is null)
77+
return document;
78+
79+
var readOnlySpanType = semanticModel.Compilation.GetTypeByMetadataName("System.ReadOnlySpan`1");
80+
if (readOnlySpanType is null)
81+
return document;
82+
83+
var newParamType = readOnlySpanType.Construct(elementType);
84+
85+
// 1) Update declaration
86+
var editor = await DocumentEditor.CreateAsync(document, ct).ConfigureAwait(false);
87+
var newTypeSyntax = SyntaxFactory.ParseTypeName(newParamType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))
88+
.WithTriviaFrom(parameter.Type!);
89+
90+
editor.ReplaceNode(parameter.Type!, newTypeSyntax);
91+
92+
var updatedDocument = editor.GetChangedDocument();
93+
var updatedSolution = updatedDocument.Project.Solution;
94+
95+
// 2) Update in-solution call sites
96+
var updatedCompilation = await updatedDocument.Project.GetCompilationAsync(ct).ConfigureAwait(false);
97+
if (updatedCompilation is null)
98+
return updatedDocument;
99+
100+
// We keep using the original symbol for reference search; SymbolFinder works across the solution.
101+
var references = await SymbolFinder.FindReferencesAsync(methodSymbol, updatedSolution, ct).ConfigureAwait(false);
102+
103+
foreach (var reference in references)
104+
{
105+
foreach (var location in reference.Locations)
106+
{
107+
var refDocument = updatedSolution.GetDocument(location.Document.Id);
108+
if (refDocument is null)
109+
continue;
110+
111+
var refRoot = await refDocument.GetSyntaxRootAsync(ct).ConfigureAwait(false);
112+
if (refRoot is null)
113+
continue;
114+
115+
var node = refRoot.FindNode(location.Location.SourceSpan, getInnermostNodeForTie: true);
116+
var invocation = node.FirstAncestorOrSelf<InvocationExpressionSyntax>();
117+
if (invocation is null)
118+
continue;
119+
120+
var refSemanticModel = await refDocument.GetSemanticModelAsync(ct).ConfigureAwait(false);
121+
if (refSemanticModel is null)
122+
continue;
123+
124+
var invokedSymbol = refSemanticModel.GetSymbolInfo(invocation, ct).Symbol as IMethodSymbol;
125+
if (invokedSymbol is null)
126+
continue;
127+
128+
// Only update invocations that bind to the same method.
129+
if (!SymbolEqualityComparer.Default.Equals(invokedSymbol.OriginalDefinition, methodSymbol.OriginalDefinition))
130+
continue;
131+
132+
// If the call already passes an array explicitly, leave it (ReadOnlySpan<T> can be created from array implicitly).
133+
// If the call uses expanded params arguments, wrap them into an array creation.
134+
var args = invocation.ArgumentList.Arguments;
135+
var paramIndex = parameterSymbol.Ordinal;
136+
if (args.Count <= paramIndex)
137+
continue;
138+
139+
// Determine if this is an expanded params call: more args than parameters.
140+
// (If the call passes a single argument at the params position, we assume it's already an array-like value.)
141+
var isExpanded = args.Count > methodSymbol.Parameters.Length;
142+
if (!isExpanded)
143+
continue;
144+
145+
var expandedArgs = args.Skip(paramIndex).ToImmutableArray();
146+
var arrayCreation = SyntaxFactory.ArrayCreationExpression(
147+
SyntaxFactory.ArrayType(
148+
SyntaxFactory.ParseTypeName(elementType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)),
149+
SyntaxFactory.SingletonList(SyntaxFactory.ArrayRankSpecifier(
150+
SyntaxFactory.SingletonSeparatedList<ExpressionSyntax>(SyntaxFactory.OmittedArraySizeExpression())))))
151+
.WithInitializer(SyntaxFactory.InitializerExpression(
152+
SyntaxKind.ArrayInitializerExpression,
153+
SyntaxFactory.SeparatedList(expandedArgs.Select(a => a.Expression))));
154+
155+
var newArgs = args.Take(paramIndex)
156+
.Concat(new[] { SyntaxFactory.Argument(arrayCreation).WithTriviaFrom(args[paramIndex]) })
157+
.ToImmutableArray();
158+
159+
var newInvocation = invocation.WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(newArgs)));
160+
161+
var refEditor = await DocumentEditor.CreateAsync(refDocument, ct).ConfigureAwait(false);
162+
refEditor.ReplaceNode(invocation, newInvocation);
163+
updatedSolution = refEditor.GetChangedDocument().Project.Solution;
164+
}
165+
}
166+
167+
return updatedSolution.GetDocument(document.Id) ?? updatedDocument;
168+
}
169+
}

0 commit comments

Comments
 (0)