mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-04-28 11:39:48 +00:00
Added this usage rule & code fix
This commit is contained in:
parent
60b58648ca
commit
db9bb8f487
@ -3,8 +3,9 @@
|
||||
### New Rules
|
||||
|
||||
Rule ID | Category | Severity | Notes
|
||||
-----------|----------|----------|------------------------
|
||||
-----------|----------|----------|-----------------------------
|
||||
MWAIS0001 | Usage | Error | ProviderAccessAnalyzer
|
||||
MWAIS0002 | Naming | Error | ConstStaticAnalyzer
|
||||
MWAIS0003 | Naming | Error | UnderscorePrefixAnalyzer
|
||||
MWAIS0004 | Usage | Error | RandomInstantiationAnalyzer
|
||||
MWAIS0002 | Naming | Error | ConstStaticAnalyzer
|
||||
MWAIS0003 | Naming | Error | UnderscorePrefixAnalyzer
|
||||
MWAIS0004 | Usage | Error | RandomInstantiationAnalyzer
|
||||
MWAIS0005 | Usage | Error | ThisUsageAnalyzer
|
@ -6,4 +6,5 @@ public static class Identifier
|
||||
public const string CONST_STATIC_ANALYZER = $"{Tools.ID_PREFIX}0002";
|
||||
public const string UNDERSCORE_PREFIX_ANALYZER = $"{Tools.ID_PREFIX}0003";
|
||||
public const string RANDOM_INSTANTIATION_ANALYZER = $"{Tools.ID_PREFIX}0004";
|
||||
public const string THIS_USAGE_ANALYZER = $"{Tools.ID_PREFIX}0005";
|
||||
}
|
@ -0,0 +1,236 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace SourceCodeRules.UsageAnalyzers;
|
||||
#pragma warning disable RS1038
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
#pragma warning restore RS1038
|
||||
public sealed class ThisUsageAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string DIAGNOSTIC_ID = Identifier.THIS_USAGE_ANALYZER;
|
||||
|
||||
private static readonly string TITLE = "`this.` must be used";
|
||||
|
||||
private static readonly string MESSAGE_FORMAT = "`this.` must be used to access variables, methods, and properties";
|
||||
|
||||
private static readonly string DESCRIPTION = MESSAGE_FORMAT;
|
||||
|
||||
private const string CATEGORY = "Usage";
|
||||
|
||||
private static readonly DiagnosticDescriptor RULE = new(DIAGNOSTIC_ID, TITLE, MESSAGE_FORMAT, CATEGORY, DiagnosticSeverity.Error, isEnabledByDefault: true, description: DESCRIPTION);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [RULE];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(this.AnalyzeIdentifier, SyntaxKind.IdentifierName);
|
||||
context.RegisterSyntaxNodeAction(this.AnalyzeGenericName, SyntaxKind.GenericName);
|
||||
}
|
||||
|
||||
private void AnalyzeGenericName(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var genericNameSyntax = (GenericNameSyntax)context.Node;
|
||||
|
||||
// Skip if already part of a 'this' expression
|
||||
if (IsAccessedThroughThis(genericNameSyntax))
|
||||
return;
|
||||
|
||||
if (IsWithinInitializer(genericNameSyntax))
|
||||
return;
|
||||
|
||||
if (IsPartOfMemberAccess(genericNameSyntax))
|
||||
return;
|
||||
|
||||
// Get symbol info for the generic name
|
||||
var symbolInfo = context.SemanticModel.GetSymbolInfo(genericNameSyntax);
|
||||
var symbol = symbolInfo.Symbol;
|
||||
|
||||
if (symbol == null)
|
||||
return;
|
||||
|
||||
// Skip static methods
|
||||
if (symbol.IsStatic)
|
||||
return;
|
||||
|
||||
// Skip local functions
|
||||
if (symbol is IMethodSymbol methodSymbol && IsLocalFunction(methodSymbol))
|
||||
return;
|
||||
|
||||
// Get the containing type of the current context
|
||||
var containingSymbol = context.ContainingSymbol;
|
||||
var currentType = containingSymbol?.ContainingType;
|
||||
|
||||
// If we're in a static context, allow accessing members without this
|
||||
if (IsInStaticContext(containingSymbol))
|
||||
return;
|
||||
|
||||
if (symbol is IMethodSymbol)
|
||||
{
|
||||
var containingType = symbol.ContainingType;
|
||||
|
||||
// If the symbol is a member of the current type or a base type, then require this
|
||||
if (currentType != null && (SymbolEqualityComparer.Default.Equals(containingType, currentType) ||
|
||||
IsBaseTypeOf(containingType, currentType)))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, genericNameSyntax.Identifier.GetLocation());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var identifierNameSyntax = (IdentifierNameSyntax)context.Node;
|
||||
|
||||
// Skip if this identifier is part of a generic name - we'll handle that separately
|
||||
if (identifierNameSyntax.Parent is GenericNameSyntax)
|
||||
return;
|
||||
|
||||
// Skip if already part of a 'this' expression
|
||||
if (IsAccessedThroughThis(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
if (IsWithinInitializer(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
if (IsPartOfMemberAccess(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
// Also skip if it's part of static import statements
|
||||
if (IsPartOfUsingStaticDirective(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
// Skip if it's part of a namespace or type name
|
||||
if (IsPartOfNamespaceOrTypeName(identifierNameSyntax))
|
||||
return;
|
||||
|
||||
// Get symbol info
|
||||
var symbolInfo = context.SemanticModel.GetSymbolInfo(identifierNameSyntax);
|
||||
var symbol = symbolInfo.Symbol;
|
||||
|
||||
if (symbol == null)
|
||||
return;
|
||||
|
||||
// Skip local variables, parameters, and range variables
|
||||
if (symbol.Kind is SymbolKind.Local or SymbolKind.Parameter or SymbolKind.RangeVariable or SymbolKind.TypeParameter)
|
||||
return;
|
||||
|
||||
// Skip types and namespaces
|
||||
if (symbol.Kind is SymbolKind.NamedType or SymbolKind.Namespace)
|
||||
return;
|
||||
|
||||
// Explicitly check if this is a local function
|
||||
if (symbol is IMethodSymbol methodSymbol && IsLocalFunction(methodSymbol))
|
||||
return;
|
||||
|
||||
// Get the containing type of the current context
|
||||
var containingSymbol = context.ContainingSymbol;
|
||||
var currentType = containingSymbol?.ContainingType;
|
||||
|
||||
// If we're in a static context, allow accessing members without this
|
||||
if (IsInStaticContext(containingSymbol))
|
||||
return;
|
||||
|
||||
// Now check if the symbol is an instance member of the current class
|
||||
if (symbol is IFieldSymbol or IPropertySymbol or IMethodSymbol or IEventSymbol)
|
||||
{
|
||||
// Skip static members
|
||||
if (symbol.IsStatic)
|
||||
return;
|
||||
|
||||
// Skip constants
|
||||
if (symbol is IFieldSymbol { IsConst: true })
|
||||
return;
|
||||
|
||||
var containingType = symbol.ContainingType;
|
||||
|
||||
// If the symbol is a member of the current type or a base type, then require this
|
||||
if (currentType != null && (SymbolEqualityComparer.Default.Equals(containingType, currentType) ||
|
||||
IsBaseTypeOf(containingType, currentType)))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(RULE, identifierNameSyntax.GetLocation());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsLocalFunction(IMethodSymbol methodSymbol) => methodSymbol.MethodKind is MethodKind.LocalFunction;
|
||||
|
||||
private static bool IsBaseTypeOf(INamedTypeSymbol baseType, INamedTypeSymbol derivedType)
|
||||
{
|
||||
var currentType = derivedType.BaseType;
|
||||
while (currentType != null)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(currentType, baseType))
|
||||
return true;
|
||||
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsInStaticContext(ISymbol? containingSymbol) => containingSymbol?.IsStatic is true;
|
||||
|
||||
private static bool IsAccessedThroughThis(SyntaxNode node)
|
||||
{
|
||||
if (node.Parent is MemberAccessExpressionSyntax memberAccess)
|
||||
if (memberAccess.Expression is ThisExpressionSyntax && memberAccess.Name == node)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsWithinInitializer(SyntaxNode node)
|
||||
{
|
||||
for (var current = node.Parent; current != null; current = current.Parent)
|
||||
if (current is InitializerExpressionSyntax)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPartOfMemberAccess(SyntaxNode node)
|
||||
{
|
||||
// Check if the node is part of a member access expression where the expression is not 'this':
|
||||
if (node.Parent is MemberAccessExpressionSyntax memberAccess)
|
||||
{
|
||||
// If the member access expression is 'this', it's allowed:
|
||||
if (memberAccess.Expression is ThisExpressionSyntax)
|
||||
return false;
|
||||
|
||||
// If the member access expression is something else (e.g., instance.Member), skip:
|
||||
if (memberAccess.Name == node)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also check for conditional access expressions (e.g., instance?.Member):
|
||||
if (node.Parent is ConditionalAccessExpressionSyntax)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPartOfUsingStaticDirective(SyntaxNode node)
|
||||
{
|
||||
for (var current = node.Parent; current != null; current = current.Parent)
|
||||
if (current is UsingDirectiveSyntax)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPartOfNamespaceOrTypeName(SyntaxNode node)
|
||||
{
|
||||
// Check if a node is part of a namespace, class, or type declaration:
|
||||
if (node.Parent is NameSyntax && node.Parent is not MemberAccessExpressionSyntax)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Composition;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace SourceCodeRules.UsageCodeFixes;
|
||||
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ThisUsageCodeFixProvider)), Shared]
|
||||
public class ThisUsageCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
private const string TITLE = "Add 'this.' prefix";
|
||||
|
||||
public sealed override ImmutableArray<string> FixableDiagnosticIds => [Identifier.THIS_USAGE_ANALYZER];
|
||||
|
||||
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
|
||||
|
||||
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
|
||||
if (root == null)
|
||||
return;
|
||||
|
||||
var diagnostic = context.Diagnostics.First();
|
||||
var diagnosticSpan = diagnostic.Location.SourceSpan;
|
||||
var node = root.FindNode(diagnosticSpan);
|
||||
|
||||
if (node is IdentifierNameSyntax identifierNode)
|
||||
{
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: TITLE,
|
||||
createChangedDocument: c => AddThisPrefixAsync(context.Document, identifierNode, c),
|
||||
equivalenceKey: nameof(ThisUsageCodeFixProvider)),
|
||||
diagnostic);
|
||||
}
|
||||
else if (node is GenericNameSyntax genericNameNode)
|
||||
{
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: TITLE,
|
||||
createChangedDocument: c => AddThisPrefixAsync(context.Document, genericNameNode, c),
|
||||
equivalenceKey: nameof(ThisUsageCodeFixProvider)),
|
||||
diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Document> AddThisPrefixAsync(Document document, SyntaxNode node, CancellationToken cancellationToken)
|
||||
{
|
||||
var root = await document.GetSyntaxRootAsync(cancellationToken);
|
||||
if (root == null)
|
||||
return document;
|
||||
|
||||
var thisExpression = SyntaxFactory.ThisExpression();
|
||||
var leadingTrivia = node.GetLeadingTrivia();
|
||||
var memberAccessExpression = SyntaxFactory.MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
thisExpression.WithLeadingTrivia(leadingTrivia),
|
||||
((SimpleNameSyntax)node).WithLeadingTrivia(SyntaxTriviaList.Empty))
|
||||
.WithTrailingTrivia(node.GetTrailingTrivia());
|
||||
|
||||
var newRoot = root.ReplaceNode(node, memberAccessExpression);
|
||||
return document.WithSyntaxRoot(newRoot);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user