mirror of
				https://github.com/MindWorkAI/AI-Studio.git
				synced 2025-11-04 06:40:20 +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 
 | 
			
		||||
 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