save progress
This commit is contained in:
@@ -20,6 +20,9 @@ public sealed class AocForbiddenFieldAnalyzer : DiagnosticAnalyzer
|
||||
public const string DiagnosticIdForbiddenField = "AOC0001";
|
||||
public const string DiagnosticIdDerivedField = "AOC0002";
|
||||
public const string DiagnosticIdUnguardedWrite = "AOC0003";
|
||||
private const string IngestionAllOption = "stellaops_aoc_ingestion";
|
||||
private const string IngestionAssemblyOption = "stellaops_aoc_ingestion_assemblies";
|
||||
private const string IngestionNamespaceOption = "stellaops_aoc_ingestion_namespace_prefixes";
|
||||
|
||||
private static readonly ImmutableHashSet<string> ForbiddenTopLevel = ImmutableHashSet.Create(
|
||||
StringComparer.OrdinalIgnoreCase,
|
||||
@@ -72,21 +75,25 @@ public sealed class AocForbiddenFieldAnalyzer : DiagnosticAnalyzer
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterOperationAction(AnalyzeAssignment, OperationKind.SimpleAssignment);
|
||||
context.RegisterOperationAction(AnalyzePropertyReference, OperationKind.PropertyReference);
|
||||
context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeObjectInitializer, SyntaxKind.ObjectInitializerExpression);
|
||||
context.RegisterSyntaxNodeAction(AnalyzeAnonymousObjectMember, SyntaxKind.AnonymousObjectMemberDeclarator);
|
||||
context.RegisterCompilationStartAction(startContext =>
|
||||
{
|
||||
var symbols = new AnalyzerTypeSymbols(startContext.Compilation);
|
||||
startContext.RegisterOperationAction(ctx => AnalyzeAssignment(ctx, symbols), OperationKind.SimpleAssignment);
|
||||
startContext.RegisterOperationAction(ctx => AnalyzePropertyReference(ctx, symbols), OperationKind.PropertyReference);
|
||||
startContext.RegisterOperationAction(ctx => AnalyzeInvocation(ctx, symbols), OperationKind.Invocation);
|
||||
startContext.RegisterSyntaxNodeAction(ctx => AnalyzeObjectInitializer(ctx, symbols), SyntaxKind.ObjectInitializerExpression);
|
||||
startContext.RegisterSyntaxNodeAction(ctx => AnalyzeAnonymousObjectMember(ctx, symbols), SyntaxKind.AnonymousObjectMemberDeclarator);
|
||||
});
|
||||
}
|
||||
|
||||
private static void AnalyzeAssignment(OperationAnalysisContext context)
|
||||
private static void AnalyzeAssignment(OperationAnalysisContext context, AnalyzerTypeSymbols symbols)
|
||||
{
|
||||
if (context.Operation is not ISimpleAssignmentOperation assignment)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsIngestionContext(context.ContainingSymbol))
|
||||
if (!IsIngestionContext(context.ContainingSymbol, context.Options, symbols))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -100,14 +107,14 @@ public sealed class AocForbiddenFieldAnalyzer : DiagnosticAnalyzer
|
||||
CheckForbiddenField(context, targetName!, assignment.Syntax.GetLocation());
|
||||
}
|
||||
|
||||
private static void AnalyzePropertyReference(OperationAnalysisContext context)
|
||||
private static void AnalyzePropertyReference(OperationAnalysisContext context, AnalyzerTypeSymbols symbols)
|
||||
{
|
||||
if (context.Operation is not IPropertyReferenceOperation propertyRef)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsIngestionContext(context.ContainingSymbol))
|
||||
if (!IsIngestionContext(context.ContainingSymbol, context.Options, symbols))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -121,14 +128,14 @@ public sealed class AocForbiddenFieldAnalyzer : DiagnosticAnalyzer
|
||||
CheckForbiddenField(context, propertyName, propertyRef.Syntax.GetLocation());
|
||||
}
|
||||
|
||||
private static void AnalyzeInvocation(OperationAnalysisContext context)
|
||||
private static void AnalyzeInvocation(OperationAnalysisContext context, AnalyzerTypeSymbols symbols)
|
||||
{
|
||||
if (context.Operation is not IInvocationOperation invocation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsIngestionContext(context.ContainingSymbol))
|
||||
if (!IsIngestionContext(context.ContainingSymbol, context.Options, symbols))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -144,9 +151,9 @@ public sealed class AocForbiddenFieldAnalyzer : DiagnosticAnalyzer
|
||||
}
|
||||
|
||||
// Check for unguarded database write operations
|
||||
if (IsDatabaseWriteOperation(method))
|
||||
if (IsDatabaseWriteOperation(method, symbols))
|
||||
{
|
||||
if (!IsWithinAocGuardScope(invocation))
|
||||
if (!IsWithinAocGuardScope(invocation, symbols))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(
|
||||
UnguardedWriteRule,
|
||||
@@ -157,11 +164,11 @@ public sealed class AocForbiddenFieldAnalyzer : DiagnosticAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
private static void AnalyzeObjectInitializer(SyntaxNodeAnalysisContext context)
|
||||
private static void AnalyzeObjectInitializer(SyntaxNodeAnalysisContext context, AnalyzerTypeSymbols symbols)
|
||||
{
|
||||
var initializer = (InitializerExpressionSyntax)context.Node;
|
||||
|
||||
if (!IsIngestionContext(context.ContainingSymbol))
|
||||
if (!IsIngestionContext(context.ContainingSymbol, context.Options, symbols))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -185,11 +192,11 @@ public sealed class AocForbiddenFieldAnalyzer : DiagnosticAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
private static void AnalyzeAnonymousObjectMember(SyntaxNodeAnalysisContext context)
|
||||
private static void AnalyzeAnonymousObjectMember(SyntaxNodeAnalysisContext context, AnalyzerTypeSymbols symbols)
|
||||
{
|
||||
var member = (AnonymousObjectMemberDeclaratorSyntax)context.Node;
|
||||
|
||||
if (!IsIngestionContext(context.ContainingSymbol))
|
||||
if (!IsIngestionContext(context.ContainingSymbol, context.Options, symbols))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -265,7 +272,7 @@ public sealed class AocForbiddenFieldAnalyzer : DiagnosticAnalyzer
|
||||
return parent is ISimpleAssignmentOperation assignment && assignment.Target == propertyRef;
|
||||
}
|
||||
|
||||
private static bool IsIngestionContext(ISymbol? containingSymbol)
|
||||
private static bool IsIngestionContext(ISymbol? containingSymbol, AnalyzerOptions options, AnalyzerTypeSymbols symbols)
|
||||
{
|
||||
if (containingSymbol is null)
|
||||
{
|
||||
@@ -280,11 +287,30 @@ public sealed class AocForbiddenFieldAnalyzer : DiagnosticAnalyzer
|
||||
|
||||
// Allow analyzer assemblies and tests
|
||||
if (assemblyName!.EndsWith(".Analyzers", StringComparison.Ordinal) ||
|
||||
assemblyName.EndsWith(".Tests", StringComparison.Ordinal))
|
||||
assemblyName.EndsWith(".Tests", StringComparison.Ordinal) ||
|
||||
assemblyName.EndsWith(".Test", StringComparison.Ordinal) ||
|
||||
assemblyName.EndsWith(".Testing", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (HasIngestionMarker(containingSymbol) ||
|
||||
HasIngestionMarker(containingSymbol.ContainingType) ||
|
||||
HasIngestionMarker(containingSymbol.ContainingAssembly))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsConfigIngestionAssembly(assemblyName, options))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsConfigIngestionNamespace(containingSymbol.ContainingNamespace?.ToDisplayString(), options))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for ingestion-related assemblies/namespaces
|
||||
if (assemblyName.Contains(".Connector.", StringComparison.Ordinal) ||
|
||||
assemblyName.Contains(".Ingestion", StringComparison.Ordinal) ||
|
||||
@@ -307,6 +333,112 @@ public sealed class AocForbiddenFieldAnalyzer : DiagnosticAnalyzer
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasIngestionMarker(ISymbol? symbol)
|
||||
{
|
||||
if (symbol is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var attribute in symbol.GetAttributes())
|
||||
{
|
||||
var attributeName = attribute.AttributeClass?.Name;
|
||||
if (string.Equals(attributeName, "AocIngestionAttribute", StringComparison.Ordinal) ||
|
||||
string.Equals(attributeName, "AocIngestionContextAttribute", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsConfigIngestionAssembly(string assemblyName, AnalyzerOptions options)
|
||||
{
|
||||
if (IsConfigIngestionEnabledForAll(options))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryGetOption(options, IngestionAssemblyOption, out var assemblies) ||
|
||||
TryGetOption(options, "build_property.StellaOpsAocIngestionAssemblies", out assemblies))
|
||||
{
|
||||
foreach (var name in SplitOptionValue(assemblies))
|
||||
{
|
||||
if (string.Equals(name, "*", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "all", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(name, assemblyName, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsConfigIngestionNamespace(string? ns, AnalyzerOptions options)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ns))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var namespaceValue = ns!;
|
||||
if (TryGetOption(options, IngestionNamespaceOption, out var namespaces) ||
|
||||
TryGetOption(options, "build_property.StellaOpsAocIngestionNamespacePrefixes", out namespaces))
|
||||
{
|
||||
foreach (var prefix in SplitOptionValue(namespaces))
|
||||
{
|
||||
if (namespaceValue.StartsWith(prefix, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsConfigIngestionEnabledForAll(AnalyzerOptions options)
|
||||
{
|
||||
if (TryGetOption(options, IngestionAllOption, out var value) ||
|
||||
TryGetOption(options, "build_property.StellaOpsAocIngestion", out value))
|
||||
{
|
||||
if (string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(value, "all", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetOption(AnalyzerOptions options, string key, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
if (!options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = raw.Trim();
|
||||
return value.Length > 0;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> SplitOptionValue(string value)
|
||||
{
|
||||
foreach (var entry in value.Split(new[] { ';', ',', ' ' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var trimmed = entry.Trim();
|
||||
if (!string.IsNullOrEmpty(trimmed))
|
||||
{
|
||||
yield return trimmed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsDictionarySetOperation(IMethodSymbol method)
|
||||
{
|
||||
var name = method.Name;
|
||||
@@ -331,35 +463,54 @@ public sealed class AocForbiddenFieldAnalyzer : DiagnosticAnalyzer
|
||||
typeName.Contains("JsonElement", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsDatabaseWriteOperation(IMethodSymbol method)
|
||||
private static bool IsDatabaseWriteOperation(IMethodSymbol method, AnalyzerTypeSymbols symbols)
|
||||
{
|
||||
var name = method.Name;
|
||||
var writeOps = new[]
|
||||
if ((string.Equals(name, "SaveChanges", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "SaveChangesAsync", StringComparison.Ordinal)) &&
|
||||
IsOnTypeOrDerived(method.ContainingType, symbols.DbContext))
|
||||
{
|
||||
"InsertOne", "InsertOneAsync",
|
||||
"InsertMany", "InsertManyAsync",
|
||||
"UpdateOne", "UpdateOneAsync",
|
||||
"UpdateMany", "UpdateManyAsync",
|
||||
"ReplaceOne", "ReplaceOneAsync",
|
||||
"BulkWrite", "BulkWriteAsync",
|
||||
"ExecuteNonQuery", "ExecuteNonQueryAsync",
|
||||
"SaveChanges", "SaveChangesAsync",
|
||||
"Add", "AddAsync",
|
||||
"Update", "UpdateAsync"
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var op in writeOps)
|
||||
if ((string.Equals(name, "Add", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "AddAsync", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "Update", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "UpdateAsync", StringComparison.Ordinal)) &&
|
||||
IsOnTypeOrDerived(method.ContainingType, symbols.DbSet))
|
||||
{
|
||||
if (string.Equals(name, op, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((string.Equals(name, "ExecuteNonQuery", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "ExecuteNonQueryAsync", StringComparison.Ordinal)) &&
|
||||
(IsOnTypeOrDerived(method.ContainingType, symbols.DbCommand) ||
|
||||
IsOnTypeOrDerived(method.ContainingType, symbols.NpgsqlCommand)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((string.Equals(name, "InsertOne", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "InsertOneAsync", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "InsertMany", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "InsertManyAsync", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "UpdateOne", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "UpdateOneAsync", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "UpdateMany", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "UpdateManyAsync", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "ReplaceOne", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "ReplaceOneAsync", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "BulkWrite", StringComparison.Ordinal) ||
|
||||
string.Equals(name, "BulkWriteAsync", StringComparison.Ordinal)) &&
|
||||
IsOnTypeOrDerived(method.ContainingType, symbols.MongoCollection))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsWithinAocGuardScope(IInvocationOperation invocation)
|
||||
private static bool IsWithinAocGuardScope(IInvocationOperation invocation, AnalyzerTypeSymbols symbols)
|
||||
{
|
||||
// Walk up the operation tree to find if we're within an AOC guard validation scope
|
||||
var current = invocation.Parent;
|
||||
@@ -371,8 +522,8 @@ public sealed class AocForbiddenFieldAnalyzer : DiagnosticAnalyzer
|
||||
if (current is IInvocationOperation parentInvocation)
|
||||
{
|
||||
var method = parentInvocation.TargetMethod;
|
||||
if (method.Name == "Validate" &&
|
||||
method.ContainingType?.Name.Contains("AocGuard", StringComparison.Ordinal) == true)
|
||||
if ((method.Name == "Validate" || method.Name == "ValidateOrThrow") &&
|
||||
IsAocGuardInvocation(method, symbols))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -387,7 +538,7 @@ public sealed class AocForbiddenFieldAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
foreach (var param in containingMethod.Parameters)
|
||||
{
|
||||
if (param.Type.Name.Contains("AocGuard", StringComparison.Ordinal))
|
||||
if (IsAocGuardType(param.Type, symbols))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -401,4 +552,80 @@ public sealed class AocForbiddenFieldAnalyzer : DiagnosticAnalyzer
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsOnTypeOrDerived(INamedTypeSymbol? type, INamedTypeSymbol? expected)
|
||||
{
|
||||
if (type is null || expected is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var current = type; current is not null; current = current.BaseType)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(current, expected))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var iface in type.AllInterfaces)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(iface, expected))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsAocGuardType(ITypeSymbol? type, AnalyzerTypeSymbols symbols)
|
||||
{
|
||||
if (type is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (symbols.AocGuard is not null && IsOnTypeOrDerived(type as INamedTypeSymbol, symbols.AocGuard))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return type.Name.Contains("AocGuard", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsAocGuardInvocation(IMethodSymbol method, AnalyzerTypeSymbols symbols)
|
||||
{
|
||||
if (IsAocGuardType(method.ContainingType, symbols))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (method.IsExtensionMethod && method.Parameters.Length > 0)
|
||||
{
|
||||
return IsAocGuardType(method.Parameters[0].Type, symbols);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private sealed class AnalyzerTypeSymbols
|
||||
{
|
||||
public AnalyzerTypeSymbols(Compilation compilation)
|
||||
{
|
||||
AocGuard = compilation.GetTypeByMetadataName("StellaOps.Aoc.IAocGuard");
|
||||
DbContext = compilation.GetTypeByMetadataName("Microsoft.EntityFrameworkCore.DbContext");
|
||||
DbSet = compilation.GetTypeByMetadataName("Microsoft.EntityFrameworkCore.DbSet`1");
|
||||
DbCommand = compilation.GetTypeByMetadataName("System.Data.Common.DbCommand");
|
||||
NpgsqlCommand = compilation.GetTypeByMetadataName("Npgsql.NpgsqlCommand");
|
||||
MongoCollection = compilation.GetTypeByMetadataName("MongoDB.Driver.IMongoCollection`1");
|
||||
}
|
||||
|
||||
public INamedTypeSymbol? AocGuard { get; }
|
||||
public INamedTypeSymbol? DbContext { get; }
|
||||
public INamedTypeSymbol? DbSet { get; }
|
||||
public INamedTypeSymbol? DbCommand { get; }
|
||||
public INamedTypeSymbol? NpgsqlCommand { get; }
|
||||
public INamedTypeSymbol? MongoCollection { get; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user