From a66f6dca8b15f97d9764306be9a4374dad0c4ab5 Mon Sep 17 00:00:00 2001 From: sibber5 Date: Thu, 10 Jul 2025 22:46:33 +0300 Subject: [PATCH 1/5] Fix null warnings clean up code --- CheckedExceptions/AnalyzerSettings.cs | 16 +- CheckedExceptions/AttributeHelper.cs | 9 +- CheckedExceptions/CheckedExceptions.csproj | 2 +- ...edExceptionsAnalyzer.DuplicateDetection.cs | 23 +- ...CheckedExceptionsAnalyzer.GeneralThrows.cs | 8 +- .../CheckedExceptionsAnalyzer.Inheritance.cs | 47 +- .../CheckedExceptionsAnalyzer.XmlDocs.cs | 390 +-- .../CheckedExceptionsAnalyzer.cs | 2675 ++++++++--------- Directory.Packages.props | 40 +- 9 files changed, 1582 insertions(+), 1628 deletions(-) diff --git a/CheckedExceptions/AnalyzerSettings.cs b/CheckedExceptions/AnalyzerSettings.cs index 23a6bac..c3265e1 100644 --- a/CheckedExceptions/AnalyzerSettings.cs +++ b/CheckedExceptions/AnalyzerSettings.cs @@ -2,13 +2,19 @@ namespace Sundstrom.CheckedExceptions; using System.Text.Json.Serialization; -public partial class AnalyzerSettings +public record AnalyzerSettings { - [JsonPropertyName("ignoredExceptions")] - public IEnumerable IgnoredExceptions { get; set; } = new List(); + public IReadOnlyList IgnoredExceptions { get; } - [JsonPropertyName("informationalExceptions")] - public IDictionary InformationalExceptions { get; set; } = new Dictionary(); + public IReadOnlyDictionary InformationalExceptions { get; } + + public AnalyzerSettings(IReadOnlyList ignoredExceptions, IReadOnlyDictionary informationalExceptions) + { + IgnoredExceptions = ignoredExceptions; + InformationalExceptions = informationalExceptions; + } + + public static AnalyzerSettings CreateWithDefaults() => new(new List(), new Dictionary()); } [JsonConverter(typeof(JsonStringEnumConverter))] diff --git a/CheckedExceptions/AttributeHelper.cs b/CheckedExceptions/AttributeHelper.cs index 4dd847c..7f3b873 100644 --- a/CheckedExceptions/AttributeHelper.cs +++ b/CheckedExceptions/AttributeHelper.cs @@ -1,5 +1,7 @@ -namespace Sundstrom.CheckedExceptions; - +namespace Sundstrom.CheckedExceptions; + +using System.Diagnostics; + using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -11,7 +13,8 @@ public static class AttributeHelper return null; // Get the symbol to which the attribute is applied - var declaredSymbol = semanticModel.GetDeclaredSymbol(attributeSyntax.Parent?.Parent); + Debug.Assert(attributeSyntax.Parent?.Parent is not null); + var declaredSymbol = semanticModel.GetDeclaredSymbol(attributeSyntax.Parent?.Parent!); if (declaredSymbol is null) return null; diff --git a/CheckedExceptions/CheckedExceptions.csproj b/CheckedExceptions/CheckedExceptions.csproj index f8223a2..7b2463c 100644 --- a/CheckedExceptions/CheckedExceptions.csproj +++ b/CheckedExceptions/CheckedExceptions.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.DuplicateDetection.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.DuplicateDetection.cs index 38f22a5..657e3d3 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.DuplicateDetection.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.DuplicateDetection.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Diagnostics; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -46,23 +47,23 @@ private void CheckForDuplicateThrowsAttributes(IEnumerable thro { var semanticModel = context.SemanticModel; - HashSet exceptionTypesList = new HashSet(SymbolEqualityComparer.Default); + HashSet? exceptionTypesSet = null; - foreach (var throwsAttribute in throwsAttributes) + foreach (AttributeSyntax throwsAttribute in throwsAttributes) { - var exceptionTypes = GetExceptionTypes(throwsAttribute, semanticModel); + Debug.Assert(throwsAttribute is not null); + + IEnumerable exceptionTypes = GetExceptionTypes(throwsAttribute!, semanticModel); + + exceptionTypesSet ??= new HashSet(SymbolEqualityComparer.Default); + foreach (var exceptionType in exceptionTypes) { - if (exceptionTypesList.FirstOrDefault(x => x.Equals(exceptionType, SymbolEqualityComparer.Default)) is not null) + if (!exceptionTypesSet.Add(exceptionType)) { - var duplicateAttributeSyntax = throwsAttribute; - if (duplicateAttributeSyntax is not null) - { - context.ReportDiagnostic(Diagnostic.Create(RuleDuplicateDeclarations, duplicateAttributeSyntax.GetLocation(), exceptionType.Name)); - } + AttributeSyntax duplicateAttributeSyntax = throwsAttribute!; + context.ReportDiagnostic(Diagnostic.Create(RuleDuplicateDeclarations, duplicateAttributeSyntax.GetLocation(), exceptionType.Name)); } - - exceptionTypesList.Add(exceptionType); } } } diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.GeneralThrows.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.GeneralThrows.cs index d6bd8eb..f5270b8 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.GeneralThrows.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.GeneralThrows.cs @@ -31,20 +31,18 @@ private static void CheckForGeneralExceptionThrows(ImmutableArray #region Lambda expression and Local functions - private void CheckForGeneralExceptionThrows(SyntaxNodeAnalysisContext context, List throwsAttributes) + private void CheckForGeneralExceptionThrows(IEnumerable throwsAttributes, SyntaxNodeAnalysisContext context) { - string generalExceptionName = "Exception"; - // Check for general Throws(typeof(Exception)) attributes foreach (var attribute in throwsAttributes) { var exceptionTypeName = GetExceptionTypeName(attribute, context.SemanticModel); - if (exceptionTypeName == generalExceptionName) + if (nameof(Exception).Equals(exceptionTypeName, StringComparison.Ordinal)) { context.ReportDiagnostic(Diagnostic.Create( RuleGeneralThrows, attribute.GetLocation(), - generalExceptionName)); + nameof(Exception))); } } } diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.Inheritance.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.Inheritance.cs index b27364b..538e570 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.Inheritance.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.Inheritance.cs @@ -1,15 +1,13 @@ using System.Collections.Immutable; +using System.Diagnostics; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; namespace Sundstrom.CheckedExceptions; partial class CheckedExceptionsAnalyzer { - #region Method - private void CheckForCompatibilityWithBaseOrInterface(SymbolAnalysisContext context, ImmutableArray throwsAttributes) { var method = (IMethodSymbol)context.Symbol; @@ -22,7 +20,8 @@ MethodKind.EventAdd or MethodKind.EventRemove)) return; - var declaredExceptions = GetDistictExceptionTypes(throwsAttributes).ToImmutableHashSet(SymbolEqualityComparer.Default); + ImmutableHashSet declaredExceptions = GetDistictExceptionTypes(throwsAttributes).Where(x => x is not null).ToImmutableHashSet(SymbolEqualityComparer.Default)!; + Debug.Assert(!declaredExceptions.Any(x => x is null)); if (declaredExceptions.Count == 0) return; @@ -41,7 +40,7 @@ MethodKind.EventAdd or } } - private void AnalyzeMissingThrowsFromBaseMember(SymbolAnalysisContext context, IMethodSymbol method, ImmutableHashSet declaredExceptions, IMethodSymbol baseMethod, ImmutableHashSet baseExceptions) + private void AnalyzeMissingThrowsFromBaseMember(SymbolAnalysisContext context, IMethodSymbol method, ImmutableHashSet declaredExceptions, IMethodSymbol baseMethod, ImmutableHashSet baseExceptions) { foreach (var baseException in baseExceptions.OfType()) { @@ -51,7 +50,7 @@ private void AnalyzeMissingThrowsFromBaseMember(SymbolAnalysisContext context, I var isCovered = declaredExceptions.Any(declared => { - if (declared.Equals(baseException, SymbolEqualityComparer.Default)) + if (baseException.Equals(declared, SymbolEqualityComparer.Default)) return true; var declaredNamed = declared as INamedTypeSymbol; @@ -76,13 +75,13 @@ private void AnalyzeMissingThrowsFromBaseMember(SymbolAnalysisContext context, I } } - private static void AnalyzeMissingThrowsOnBaseMember(SymbolAnalysisContext context, IMethodSymbol method, ImmutableHashSet declaredExceptions, IMethodSymbol baseMethod, ImmutableHashSet baseExceptions) + private static void AnalyzeMissingThrowsOnBaseMember(SymbolAnalysisContext context, IMethodSymbol method, ImmutableHashSet declaredExceptions, IMethodSymbol baseMethod, ImmutableHashSet baseExceptions) { foreach (var declared in declaredExceptions) { - var isCompatible = baseExceptions.Any(baseEx => - declared.Equals(baseEx, SymbolEqualityComparer.Default) || - ((INamedTypeSymbol)declared).InheritsFrom((INamedTypeSymbol)baseEx)); + var isCompatible = baseExceptions.Any(baseEx => baseEx is not null && + (declared.Equals(baseEx, SymbolEqualityComparer.Default) + || ((INamedTypeSymbol)declared).InheritsFrom((INamedTypeSymbol)baseEx))); if (!isCompatible) { @@ -102,13 +101,11 @@ private static void AnalyzeMissingThrowsOnBaseMember(SymbolAnalysisContext conte private bool IsTooGenericException(ITypeSymbol ex) { - var namedType = ex as INamedTypeSymbol; - if (namedType == null) - return false; - - var fullName = namedType.ToDisplayString(); + if (ex is not INamedTypeSymbol namedTypeSymbol) return false; + + var fullName = namedTypeSymbol.ToDisplayString(); - return fullName == "System.Exception" || fullName == "System.SystemException"; + return fullName.Equals(typeof(Exception).FullName, StringComparison.Ordinal) || fullName.Equals(typeof(SystemException).FullName, StringComparison.Ordinal); } private IEnumerable GetBaseOrInterfaceMethods(IMethodSymbol method) @@ -120,18 +117,18 @@ private IEnumerable GetBaseOrInterfaceMethods(IMethodSymbol metho if (method.AssociatedSymbol is IPropertySymbol prop && prop.OverriddenProperty is not null) { - if (SymbolEqualityComparer.Default.Equals(method, prop.GetMethod)) - results.Add(prop.OverriddenProperty.GetMethod); - else if (SymbolEqualityComparer.Default.Equals(method, prop.SetMethod)) - results.Add(prop.OverriddenProperty.SetMethod); + if (SymbolEqualityComparer.Default.Equals(method, prop.GetMethod) && prop.OverriddenProperty.GetMethod is { } getMethodSymbol) + results.Add(getMethodSymbol); + else if (SymbolEqualityComparer.Default.Equals(method, prop.SetMethod) && prop.OverriddenProperty.SetMethod is { } setMethodSymbol) + results.Add(setMethodSymbol); } if (method.AssociatedSymbol is IEventSymbol ev && ev.OverriddenEvent is not null) { - if (SymbolEqualityComparer.Default.Equals(method, ev.AddMethod)) - results.Add(ev.OverriddenEvent.AddMethod); - else if (SymbolEqualityComparer.Default.Equals(method, ev.RemoveMethod)) - results.Add(ev.OverriddenEvent.RemoveMethod); + if (SymbolEqualityComparer.Default.Equals(method, ev.AddMethod) && ev.OverriddenEvent.AddMethod is { } addMethodSymbol) + results.Add(addMethodSymbol); + else if (SymbolEqualityComparer.Default.Equals(method, ev.RemoveMethod) && ev.OverriddenEvent.RemoveMethod is { } removeMethodSymbol) + results.Add(removeMethodSymbol); } var type = method.ContainingType; @@ -149,6 +146,4 @@ private IEnumerable GetBaseOrInterfaceMethods(IMethodSymbol metho return results; } - - #endregion } \ No newline at end of file diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs index 9bec1e5..57f2a66 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs @@ -1,178 +1,214 @@ -using System.Collections.Concurrent; -using System.Xml; -using System.Xml.Linq; - -using Microsoft.CodeAnalysis; - -namespace Sundstrom.CheckedExceptions; - -partial class CheckedExceptionsAnalyzer -{ - // A thread-safe dictionary to cache XML documentation paths - private static readonly ConcurrentDictionary XmlDocPathsCache = new(); - private static readonly ConcurrentDictionary?> XmlDocPathsAndMembers = new(); - - private string? GetXmlDocumentationPath(Compilation compilation, IAssemblySymbol assemblySymbol) - { - var assemblyName = assemblySymbol.Name; - var assemblyPath = assemblySymbol.Locations.FirstOrDefault()?.SourceTree?.FilePath; - - if (string.IsNullOrEmpty(assemblyPath)) - { - // Fallback: Attempt to get the path from the MetadataReference - var metadataReference = compilation.References - .FirstOrDefault(r => compilation.GetAssemblyOrModuleSymbol(r)?.Name == assemblyName); - - if (metadataReference is not null) - { - if (metadataReference is PortableExecutableReference peReference) - { - assemblyPath = peReference.FilePath; - } - } - } - - if (string.IsNullOrEmpty(assemblyPath)) - return null; - - // Check cache first - if (XmlDocPathsCache.TryGetValue(assemblyPath, out var cachedPath)) - { - return cachedPath; - } - - // Assume XML doc is in the same directory with the same base name - var xmlDocPath = Path.ChangeExtension(assemblyPath, ".xml"); - -#pragma warning disable RS1035 // Do not use APIs banned for analyzers - if (File.Exists(xmlDocPath)) - { - XmlDocPathsCache[assemblyPath] = xmlDocPath; - return xmlDocPath; - } -#pragma warning restore RS1035 // Do not use APIs banned for analyzers - - // Handle .NET Core / .NET 5+ SDK paths - // Attempt to locate XML docs in SDK installation directories - // This requires knowledge of the SDK paths, which can vary - // A heuristic approach is necessary - - // Example heuristic (may need adjustments based on environment) - var sdkXmlDocPath = Path.Combine( - Path.GetDirectoryName(assemblyPath) ?? string.Empty, - "..", "xml", - $"{assemblyName}.xml"); - - sdkXmlDocPath = Path.GetFullPath(sdkXmlDocPath); - -#pragma warning disable RS1035 // Do not use APIs banned for analyzers - if (File.Exists(sdkXmlDocPath)) - { - XmlDocPathsCache[assemblyPath] = sdkXmlDocPath; - return sdkXmlDocPath; - } -#pragma warning restore RS1035 // Do not use APIs banned for analyzers - - // XML documentation not found - XmlDocPathsCache[assemblyPath] = null; - return null; - } - - public record struct ParamInfo(string Name); - - public record struct ExceptionInfo(INamedTypeSymbol ExceptionType, string Description, IEnumerable Parameters); - - private static IEnumerable GetExceptionTypesFromDocumentationCommentXml(Compilation compilation, XElement? xml) - { - try - { - return xml.Descendants("exception") - .Select(e => - { - var cref = e.Attribute("cref")?.Value; - var crefValue = cref.StartsWith("T:") ? cref.Substring(2) : cref; - var innerText = e.Value; - - var name = compilation.GetTypeByMetadataName(crefValue) ?? - compilation.GetTypeByMetadataName(crefValue.Split('.').Last()); - - var parameters = e.Elements("paramref").Select(x => new ParamInfo(x.Attribute("name").Value)); - - return new ExceptionInfo(name, innerText, parameters); - }); - } - catch - { - // Handle or log parsing errors - return Enumerable.Empty(); - } - } - - /// - /// Retrieves exception types declared in XML documentation. - /// - private IEnumerable GetExceptionTypesFromDocumentationCommentXml(Compilation compilation, ISymbol symbol) - { - XElement? docCommentXml = GetDocumentationCommentXmlForSymbol(compilation, symbol); - - if (docCommentXml is null) - { - return Enumerable.Empty(); - } - - // Attempt to get exceptions from XML documentation - return GetExceptionTypesFromDocumentationCommentXml(compilation, docCommentXml).ToList(); - } - - readonly bool loadFromProject = true; - - private XElement? GetDocumentationCommentXmlForSymbol(Compilation compilation, ISymbol symbol) - { - // Retrieve comment from project in solution that is being built - var docCommentXmlString = symbol.GetDocumentationCommentXml(); - - XElement? docCommentXml; - - if (!string.IsNullOrEmpty(docCommentXmlString) && loadFromProject) - { - try - { - docCommentXml = XElement.Parse(docCommentXmlString); - } - catch - { - // Badly formed XML - return null; - } - } - else - { - // Retrieve comment from referenced libraries (framework and DLLs in NuGet packages etc) - docCommentXml = GetXmlDocumentation(compilation, symbol); - } - - return docCommentXml; - } - - public XElement? GetXmlDocumentation(Compilation compilation, ISymbol symbol) - { - var path = GetXmlDocumentationPath(compilation, symbol.ContainingAssembly); - if (path is not null) - { - if (!XmlDocPathsAndMembers.TryGetValue(path, out var lookup)) - { - var file = XmlDocumentationHelper.CreateMemberLookup(XDocument.Load(path)); - lookup = new ConcurrentDictionary(file); - XmlDocPathsAndMembers.TryAdd(path, lookup); - } - var member = symbol.GetDocumentationCommentId(); - - if (lookup.TryGetValue(member, out var xml)) - { - return xml; - } - } - - return null; - } +using System.Collections.Concurrent; +using System.Xml.Linq; + +using Microsoft.CodeAnalysis; + +namespace Sundstrom.CheckedExceptions; + +partial class CheckedExceptionsAnalyzer +{ + // A thread-safe dictionary to cache XML documentation paths + private static readonly ConcurrentDictionary XmlDocPathsCache = new(); + private static readonly ConcurrentDictionary?> XmlDocPathsAndMembers = new(); + + private string? GetXmlDocumentationPath(Compilation compilation, IAssemblySymbol assemblySymbol) + { + var assemblyName = assemblySymbol.Name; + var assemblyPath = assemblySymbol.Locations.FirstOrDefault()?.SourceTree?.FilePath; + + if (string.IsNullOrEmpty(assemblyPath)) + { + // Fallback: Attempt to get the path from the MetadataReference + var metadataReference = compilation.References + .FirstOrDefault(r => compilation.GetAssemblyOrModuleSymbol(r)?.Name == assemblyName); + + if (metadataReference is not null) + { + if (metadataReference is PortableExecutableReference peReference) + { + assemblyPath = peReference.FilePath; + } + } + } + + // explicitly check instead of using string.IsNullOrEmpty because netstandard2.0 does not support NotNullWhenAttribute + if (assemblyPath is null || assemblyPath.Length == 0) + return null; + + // Check cache first + if (XmlDocPathsCache.TryGetValue(assemblyPath, out var cachedPath)) + { + return cachedPath; + } + + // Assume XML doc is in the same directory with the same base name + var xmlDocPath = Path.ChangeExtension(assemblyPath, ".xml"); + +#pragma warning disable RS1035 // Do not use APIs banned for analyzers + if (File.Exists(xmlDocPath)) + { + XmlDocPathsCache[assemblyPath] = xmlDocPath; + return xmlDocPath; + } +#pragma warning restore RS1035 // Do not use APIs banned for analyzers + + // Handle .NET Core / .NET 5+ SDK paths + // Attempt to locate XML docs in SDK installation directories + // This requires knowledge of the SDK paths, which can vary + // A heuristic approach is necessary + + // Example heuristic (may need adjustments based on environment) + var sdkXmlDocPath = Path.Combine( + Path.GetDirectoryName(assemblyPath) ?? string.Empty, + "..", "xml", + $"{assemblyName}.xml"); + + sdkXmlDocPath = Path.GetFullPath(sdkXmlDocPath); + +#pragma warning disable RS1035 // Do not use APIs banned for analyzers + if (File.Exists(sdkXmlDocPath)) + { + XmlDocPathsCache[assemblyPath] = sdkXmlDocPath; + return sdkXmlDocPath; + } +#pragma warning restore RS1035 // Do not use APIs banned for analyzers + + // XML documentation not found + XmlDocPathsCache[assemblyPath] = null; + return null; + } + + public record struct ParamInfo(string Name); + + public record struct ExceptionInfo(INamedTypeSymbol ExceptionType, string Description, IEnumerable Parameters); + + private static IEnumerable GetExceptionTypesFromDocumentationCommentXml(Compilation compilation, XElement xml) + { + try + { + return xml.Descendants("exception") + .Select(e => + { + string? cref = e.Attribute("cref")?.Value; + if (string.IsNullOrWhiteSpace(cref)) + { + return default; + } + + string exceptionTypeName = cref!.StartsWith("T:", StringComparison.Ordinal) ? cref.Substring(2) : cref; + string cleanExceptionTypeName = RemoveGenericParameters(exceptionTypeName); + + INamedTypeSymbol? typeSymbol = compilation.GetTypeByMetadataName(cleanExceptionTypeName); + if (typeSymbol is null && !cleanExceptionTypeName.Contains('.')) + { + typeSymbol = compilation.GetTypeByMetadataName($"System.{cleanExceptionTypeName}"); + } + + if (typeSymbol is null) + { + return default; + } + + string innerText = e.Value; + + IEnumerable parameters = e.Elements("paramref") + .Select(x => new ParamInfo(x.Attribute("name")?.Value!)) + .Where(p => !string.IsNullOrWhiteSpace(p.Name)); + + return new ExceptionInfo(typeSymbol, innerText, parameters); + }) + .Where(x => x != default) + .ToList(); // Materialize to catch parsing errors + } + catch + { + // Handle or log parsing errors + return Enumerable.Empty(); + } + + static string RemoveGenericParameters(string typeName) + { + // Handle generic types like "System.Collections.Generic.List`1" + var backtickIndex = typeName.IndexOf('`'); + if (backtickIndex >= 0) + { + return typeName.Substring(0, backtickIndex); + } + + // Handle generic syntax like "List" + var angleIndex = typeName.IndexOf('<'); + if (angleIndex >= 0) + { + return typeName.Substring(0, angleIndex); + } + + return typeName; + } + } + + /// + /// Retrieves exception types declared in XML documentation. + /// + private IEnumerable GetExceptionTypesFromDocumentationCommentXml(Compilation compilation, ISymbol symbol) + { + XElement? docCommentXml = GetDocumentationCommentXmlForSymbol(compilation, symbol); + + if (docCommentXml is null) + { + return Enumerable.Empty(); + } + + // Attempt to get exceptions from XML documentation + return GetExceptionTypesFromDocumentationCommentXml(compilation, docCommentXml); + } + + readonly bool loadFromProject = true; + + private XElement? GetDocumentationCommentXmlForSymbol(Compilation compilation, ISymbol symbol) + { + // Retrieve comment from project in solution that is being built + var docCommentXmlString = symbol.GetDocumentationCommentXml(); + + XElement? docCommentXml; + + if (!string.IsNullOrEmpty(docCommentXmlString) && loadFromProject) + { + try + { + docCommentXml = XElement.Parse(docCommentXmlString); + } + catch + { + // Badly formed XML + return null; + } + } + else + { + // Retrieve comment from referenced libraries (framework and DLLs in NuGet packages etc) + docCommentXml = GetXmlDocumentation(compilation, symbol); + } + + return docCommentXml; + } + + public XElement? GetXmlDocumentation(Compilation compilation, ISymbol symbol) + { + var path = GetXmlDocumentationPath(compilation, symbol.ContainingAssembly); + if (path is null) + { + return null; + } + + if (!XmlDocPathsAndMembers.TryGetValue(path, out var lookup) || lookup is null) + { + var file = XmlDocumentationHelper.CreateMemberLookup(XDocument.Load(path)); + lookup = new ConcurrentDictionary(file); + XmlDocPathsAndMembers[path] = lookup; + } + + var member = symbol.GetDocumentationCommentId(); + + return member is not null && lookup.TryGetValue(member, out var xml) ? xml : null; + } } \ No newline at end of file diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.cs index 441cc18..1af33a8 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.cs @@ -1,1381 +1,1296 @@ -namespace Sundstrom.CheckedExceptions; - -using System.Collections.Concurrent; -using System.Collections.Immutable; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text.Json; -using System.Xml.Linq; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public partial class CheckedExceptionsAnalyzer : DiagnosticAnalyzer -{ - static readonly ConcurrentDictionary configs = new ConcurrentDictionary(); - - // Diagnostic IDs - public const string DiagnosticIdUnhandled = "THROW001"; - public const string DiagnosticIdIgnoredException = "THROW002"; - public const string DiagnosticIdGeneralThrows = "THROW003"; - public const string DiagnosticIdGeneralThrow = "THROW004"; - public const string DiagnosticIdDuplicateDeclarations = "THROW005"; - public const string DiagnosticIdMissingThrowsOnBaseMember = "THROW006"; - public const string DiagnosticIdMissingThrowsFromBaseMember = "THROW007"; - - public static IEnumerable AllDiagnosticsIds = [DiagnosticIdUnhandled, DiagnosticIdGeneralThrows, DiagnosticIdGeneralThrow, DiagnosticIdDuplicateDeclarations]; - - private static readonly DiagnosticDescriptor RuleUnhandledException = new( - DiagnosticIdUnhandled, - "Unhandled exception", - "Exception '{0}' {1} thrown but not handled", - "Usage", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Reports exceptions that are thrown but not caught or declared with [Throws], potentially violating exception safety."); - - private static readonly DiagnosticDescriptor RuleIgnoredException = new DiagnosticDescriptor( - DiagnosticIdIgnoredException, - "Ignored exception may cause runtime issues", - "Exception '{0}' is ignored by configuration but may cause runtime issues if unhandled", - "Usage", - DiagnosticSeverity.Info, - isEnabledByDefault: true, - description: "Informs about exceptions excluded from analysis but which may still propagate at runtime if not properly handled."); - - private static readonly DiagnosticDescriptor RuleGeneralThrow = new( - DiagnosticIdGeneralThrow, - "Avoid throwing 'Exception'", - "Throwing 'Exception' is too general; use a more specific exception type instead", - "Usage", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Discourages throwing the base System.Exception type directly, encouraging clearer and more actionable error semantics."); - - private static readonly DiagnosticDescriptor RuleGeneralThrows = new DiagnosticDescriptor( - DiagnosticIdGeneralThrows, - "Avoid declaring exception type 'Exception'", - "Declaring 'Exception' is too general; use a more specific exception type instead", - "Usage", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Discourages the use of System.Exception in [Throws] attributes. Prefer declaring more specific exception types."); - - private static readonly DiagnosticDescriptor RuleDuplicateDeclarations = new DiagnosticDescriptor( - DiagnosticIdDuplicateDeclarations, - "Avoid duplicate declarations of the same exception type", - "Duplicate declarations of the exception type '{0}' found. Remove them to avoid redundancy.", - "Usage", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Detects multiple [Throws] declarations for the same exception type on a single member, which is redundant."); - - private static readonly DiagnosticDescriptor RuleMissingThrowsOnBaseMember = new DiagnosticDescriptor( - DiagnosticIdMissingThrowsOnBaseMember, - "Missing Throws declaration", - "Exception '{1}' is not declared on base member '{0}'", - "Usage", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Base or interface members should declare compatible exceptions when overridden or implemented members declare them using [Throws]."); - - private static readonly DiagnosticDescriptor RuleMissingThrowsFromBaseMember = new( - DiagnosticIdMissingThrowsFromBaseMember, - "Missing Throws declaration for exception declared on base member", - "Base member '{0}' declares exception '{1}' which is not declared here", - "Usage", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Ensures that overridden or implemented members declare exceptions required by their base or interface definitions."); - - public override ImmutableArray SupportedDiagnostics => - [RuleUnhandledException, RuleIgnoredException, RuleGeneralThrows, RuleGeneralThrow, RuleDuplicateDeclarations, RuleMissingThrowsOnBaseMember, RuleMissingThrowsFromBaseMember]; - - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - - // Register actions for throw statements and expressions - context.RegisterSyntaxNodeAction(AnalyzeThrowStatement, SyntaxKind.ThrowStatement); - context.RegisterSyntaxNodeAction(AnalyzeThrowExpression, SyntaxKind.ThrowExpression); - - context.RegisterSymbolAction(AnalyzeMethodSymbol, SymbolKind.Method); - - context.RegisterSyntaxNodeAction(AnalyzeLambdaExpression, SyntaxKind.SimpleLambdaExpression); - context.RegisterSyntaxNodeAction(AnalyzeLambdaExpression, SyntaxKind.ParenthesizedLambdaExpression); - context.RegisterSyntaxNodeAction(AnalyzeLocalFunctionStatement, SyntaxKind.LocalFunctionStatement); - - // Register additional actions for method calls, object creations, etc. - context.RegisterSyntaxNodeAction(AnalyzeMethodCall, SyntaxKind.InvocationExpression); - context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression); - context.RegisterSyntaxNodeAction(AnalyzeImplicitObjectCreation, SyntaxKind.ImplicitObjectCreationExpression); - context.RegisterSyntaxNodeAction(AnalyzeIdentifier, SyntaxKind.IdentifierName); - context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); - context.RegisterSyntaxNodeAction(AnalyzeAwait, SyntaxKind.AwaitExpression); - context.RegisterSyntaxNodeAction(AnalyzeElementAccess, SyntaxKind.ElementAccessExpression); - context.RegisterSyntaxNodeAction(AnalyzeEventAssignment, SyntaxKind.AddAssignmentExpression); - context.RegisterSyntaxNodeAction(AnalyzeEventAssignment, SyntaxKind.SubtractAssignmentExpression); - } - - private const string SettingsFileName = "CheckedExceptions.settings.json"; - - private AnalyzerSettings GetAnalyzerSettings(AnalyzerOptions analyzerOptions) - { - if (!configs.TryGetValue(analyzerOptions, out var config)) - { - foreach (var additionalFile in analyzerOptions.AdditionalFiles) - { - if (Path.GetFileName(additionalFile.Path).Equals(SettingsFileName, StringComparison.OrdinalIgnoreCase)) - { - var text = additionalFile.GetText(); - if (text is not null) - { - var json = text.ToString(); - config = JsonSerializer.Deserialize(json); - break; - } - } - } - - config ??= new AnalyzerSettings(); // Return default options if config file is not found - - configs.TryAdd(analyzerOptions, config); - } - - return config ?? new AnalyzerSettings(); - } - - private void AnalyzeLambdaExpression(SyntaxNodeAnalysisContext context) - { - var lambdaExpression = (LambdaExpressionSyntax)context.Node; - AnalyzeFunctionAttributes(lambdaExpression, lambdaExpression.AttributeLists.SelectMany(a => a.Attributes), context.SemanticModel, context); - } - - private void AnalyzeLocalFunctionStatement(SyntaxNodeAnalysisContext context) - { - var localFunction = (LocalFunctionStatementSyntax)context.Node; - AnalyzeFunctionAttributes(localFunction, localFunction.AttributeLists.SelectMany(a => a.Attributes), context.SemanticModel, context); - } - - private void AnalyzeFunctionAttributes(SyntaxNode node, IEnumerable attributes, SemanticModel semanticModel, SyntaxNodeAnalysisContext context) - { - var throwsAttributes = attributes - .Where(attr => IsThrowsAttribute(attr, semanticModel)) - .ToList(); - - if (throwsAttributes.Count is 0) - return; - - CheckForGeneralExceptionThrows(context, throwsAttributes); - - if (throwsAttributes.Any()) - { - CheckForDuplicateThrowsAttributes(throwsAttributes, context); - } - } - - /// - /// Determines whether the given attribute is a ThrowsAttribute. - /// - private bool IsThrowsAttribute(AttributeSyntax attributeSyntax, SemanticModel semanticModel) - { - var attributeSymbol = semanticModel.GetSymbolInfo(attributeSyntax).Symbol as IMethodSymbol; - if (attributeSymbol is null) - return false; - - var attributeType = attributeSymbol.ContainingType; - return attributeType.Name is "ThrowsAttribute"; - } - - private void AnalyzeMethodSymbol(SymbolAnalysisContext context) - { - var methodSymbol = (IMethodSymbol)context.Symbol; - - if (methodSymbol is null) - return; - - var throwsAttributes = GetThrowsAttributes(methodSymbol).ToImmutableArray(); - - CheckForCompatibilityWithBaseOrInterface(context, throwsAttributes); - - if (throwsAttributes.Length == 0) - return; - - CheckForGeneralExceptionThrows(throwsAttributes, context); - CheckForDuplicateThrowsAttributes(context, throwsAttributes); - } - - private static IEnumerable FilterThrowsAttributesByException(ImmutableArray exceptionAttributes, string exceptionTypeName) - { - return exceptionAttributes - .Where(attribute => IsThrowsAttributeForException(attribute, exceptionTypeName)); - } - - public static bool IsThrowsAttributeForException(AttributeData attribute, string exceptionTypeName) - { - if (!attribute.ConstructorArguments.Any()) - return false; - - var exceptionTypes = GetDistictExceptionTypes(attribute); - return exceptionTypes.Any(exceptionType => exceptionType?.Name == exceptionTypeName); - } - - public static IEnumerable GetExceptionTypes(params IEnumerable exceptionAttributes) - { - var constructorArguments = exceptionAttributes - .SelectMany(attr => attr.ConstructorArguments); - - foreach (var arg in constructorArguments) - { - if (arg.Kind is TypedConstantKind.Array) - { - foreach (var t in arg.Values) - { - if (t.Kind is TypedConstantKind.Type) - { - yield return (INamedTypeSymbol)t.Value!; - } - } - } - else if (arg.Kind is TypedConstantKind.Type) - { - yield return (INamedTypeSymbol)arg.Value!; - } - } - } - - public static IEnumerable GetDistictExceptionTypes(params IEnumerable exceptionAttributes) - { - var exceptionTypes = GetExceptionTypes(exceptionAttributes); - - return exceptionTypes.Distinct(SymbolEqualityComparer.Default) - .OfType(); - } - - /// - /// Analyzes throw statements to determine if exceptions are handled or declared. - /// - private void AnalyzeThrowStatement(SyntaxNodeAnalysisContext context) - { - var settings = GetAnalyzerSettings(context.Options); - - var throwStatement = (ThrowStatementSyntax)context.Node; - - // Handle rethrows (throw;) - if (throwStatement.Expression is null) - { - if (IsWithinCatchBlock(throwStatement, out var catchClause)) - { - if (catchClause is not null) - { - if (catchClause.Declaration is null) - { - // General catch block with 'throw;' - // Analyze exceptions thrown in the try block - var tryStatement = catchClause.Ancestors().OfType().FirstOrDefault(); - if (tryStatement is not null) - { - AnalyzeExceptionsInTryBlock(context, tryStatement, catchClause, throwStatement, settings); - } - } - else - { - var exceptionType = context.SemanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; - AnalyzeExceptionThrowingNode(context, throwStatement, exceptionType, settings); - } - } - } - return; // No further analysis for rethrows - } - - // Handle throw new ExceptionType() - if (throwStatement.Expression is ObjectCreationExpressionSyntax creationExpression) - { - var exceptionType = context.SemanticModel.GetTypeInfo(creationExpression).Type as INamedTypeSymbol; - AnalyzeExceptionThrowingNode(context, throwStatement, exceptionType, settings); - } - } - - private void AnalyzeExceptionsInTryBlock(SyntaxNodeAnalysisContext context, TryStatementSyntax tryStatement, CatchClauseSyntax generalCatchClause, ThrowStatementSyntax throwStatement, AnalyzerSettings settings) - { - var semanticModel = context.SemanticModel; - - // Collect exceptions that can be thrown in the try block - var thrownExceptions = CollectUnhandledExceptions(context, tryStatement.Block, settings); - - // Collect exception types handled by preceding catch clauses - var handledExceptions = new HashSet(SymbolEqualityComparer.Default); - foreach (var catchClause in tryStatement.Catches) - { - if (catchClause == generalCatchClause) - break; // Stop at the general catch clause - - if (catchClause.Declaration is not null) - { - var catchType = semanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; - if (catchType is not null) - { - handledExceptions.Add(catchType); - } - } - else - { - // General catch clause before our general catch; handles all exceptions - handledExceptions = null; - break; - } - } - - if (handledExceptions is null) - { - // All exceptions are handled by a previous general catch - return; - } - - // For each thrown exception, check if it is handled - foreach (var exceptionType in thrownExceptions.Distinct(SymbolEqualityComparer.Default).OfType()) - { - var exceptionName = exceptionType.ToDisplayString(); - - if (settings.IgnoredExceptions.Contains(exceptionName)) - { - // Completely ignore this exception - continue; - } - else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) - { - if (ShouldIgnore(throwStatement, mode)) - { - // Report as THROW002 (Info level) - var diagnostic = Diagnostic.Create(RuleIgnoredException, GetSignificantLocation(throwStatement), exceptionType.Name); - context.ReportDiagnostic(diagnostic); - continue; - } - } - - bool isHandled = handledExceptions.Any(handledException => - exceptionType.Equals(handledException, SymbolEqualityComparer.Default) || - exceptionType.InheritsFrom(handledException)); - - bool isDeclared = IsExceptionDeclaredInMethod(context, tryStatement, exceptionType); - - if (!isHandled && !isDeclared) - { - // Report diagnostic for unhandled exception - var diagnostic = Diagnostic.Create( - RuleUnhandledException, - GetSignificantLocation(throwStatement), - exceptionType.Name, - THROW001Verbs.MightBe); - - context.ReportDiagnostic(diagnostic); - } - } - } - - private HashSet CollectUnhandledExceptions(SyntaxNodeAnalysisContext context, BlockSyntax block, AnalyzerSettings settings) - { - var unhandledExceptions = new HashSet(SymbolEqualityComparer.Default); - - foreach (var statement in block.Statements) - { - if (statement is TryStatementSyntax tryStatement) - { - // Recursively collect exceptions from the inner try block - var innerUnhandledExceptions = CollectUnhandledExceptions(context, tryStatement.Block, settings); - - // Remove exceptions that are caught by the inner catch clauses - var caughtExceptions = GetCaughtExceptions(tryStatement.Catches, context.SemanticModel); - innerUnhandledExceptions.RemoveWhere(exceptionType => - IsExceptionCaught(exceptionType, caughtExceptions)); - - // Add any exceptions that are not handled in the inner try block - unhandledExceptions.UnionWith(innerUnhandledExceptions); - } - else - { - // Collect exceptions thrown in this statement - var statementExceptions = CollectExceptionsFromStatement(context, statement, settings); - - // Add them to the unhandled exceptions - unhandledExceptions.UnionWith(statementExceptions); - } - } - - return unhandledExceptions; - } - - private HashSet CollectExceptionsFromStatement(SyntaxNodeAnalysisContext context, StatementSyntax statement, AnalyzerSettings settings) - { - SemanticModel semanticModel = context.SemanticModel; - - var exceptions = new HashSet(SymbolEqualityComparer.Default); - - // Collect exceptions from throw statements - var throwStatements = statement.DescendantNodesAndSelf().OfType(); - foreach (var throwStatement in throwStatements) - { - if (throwStatement.Expression is not null) - { - var exceptionType = semanticModel.GetTypeInfo(throwStatement.Expression).Type as INamedTypeSymbol; - if (exceptionType is not null) - { - if (ShouldIncludeException(exceptionType, throwStatement, settings)) - { - exceptions.Add(exceptionType); - } - } - } - } - - // Collect exceptions from throw expressions - var throwExpressions = statement.DescendantNodesAndSelf().OfType(); - foreach (var throwExpression in throwExpressions) - { - if (throwExpression.Expression is not null) - { - var exceptionType = semanticModel.GetTypeInfo(throwExpression.Expression).Type as INamedTypeSymbol; - if (exceptionType is not null) - { - if (ShouldIncludeException(exceptionType, throwExpression, settings)) - { - exceptions.Add(exceptionType); - } - } - } - } - - // Collect exceptions from method calls and other expressions - var invocationExpressions = statement.DescendantNodesAndSelf().OfType(); - foreach (var invocation in invocationExpressions) - { - var methodSymbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol; - if (methodSymbol is not null) - { - var exceptionTypes = GetExceptionTypes(methodSymbol); - - // Get exceptions from XML documentation - var xmlExceptionTypes = GetExceptionTypesFromDocumentationCommentXml(semanticModel.Compilation, methodSymbol); - - xmlExceptionTypes = ProcessNullable(context, invocation, methodSymbol, xmlExceptionTypes); - - if (xmlExceptionTypes.Any()) - { - exceptionTypes.AddRange(xmlExceptionTypes.Select(x => x.ExceptionType)); - } - - foreach (var exceptionType in exceptionTypes) - { - if (ShouldIncludeException(exceptionType, invocation, settings)) - { - exceptions.Add(exceptionType); - } - } - } - } - - var objectCreations = statement.DescendantNodesAndSelf().OfType(); - foreach (var objectCreation in objectCreations) - { - var methodSymbol = semanticModel.GetSymbolInfo(objectCreation).Symbol as IMethodSymbol; - if (methodSymbol is not null) - { - var exceptionTypes = GetExceptionTypes(methodSymbol); - - // Get exceptions from XML documentation - var xmlExceptionTypes = GetExceptionTypesFromDocumentationCommentXml(semanticModel.Compilation, methodSymbol); - - xmlExceptionTypes = ProcessNullable(context, objectCreation, methodSymbol, xmlExceptionTypes); - - if (xmlExceptionTypes.Any()) - { - exceptionTypes.AddRange(xmlExceptionTypes.Select(x => x.ExceptionType)); - } - - foreach (var exceptionType in exceptionTypes) - { - if (ShouldIncludeException(exceptionType, objectCreation, settings)) - { - exceptions.Add(exceptionType); - } - } - } - } - - // Collect from MemberAccess and Identifier - var memberAccessExpressions = statement.DescendantNodesAndSelf().OfType(); - foreach (var memberAccess in memberAccessExpressions) - { - var propertySymbol = semanticModel.GetSymbolInfo(memberAccess).Symbol as IPropertySymbol; - if (propertySymbol is not null) - { - HashSet exceptionTypes = GetPropertyExceptionTypes(context, memberAccess, propertySymbol); - - foreach (var exceptionType in exceptionTypes) - { - if (ShouldIncludeException(exceptionType, memberAccess, settings)) - { - exceptions.Add(exceptionType); - } - } - } - } - - var elementAccessExpressions = statement.DescendantNodesAndSelf().OfType(); - foreach (var elementAccess in elementAccessExpressions) - { - var propertySymbol = semanticModel.GetSymbolInfo(elementAccess).Symbol as IPropertySymbol; - if (propertySymbol is not null) - { - HashSet exceptionTypes = GetPropertyExceptionTypes(context, elementAccess, propertySymbol); - - foreach (var exceptionType in exceptionTypes) - { - if (ShouldIncludeException(exceptionType, elementAccess, settings)) - { - exceptions.Add(exceptionType); - } - } - } - } - - var identifierExpressions = statement.DescendantNodesAndSelf().OfType(); - foreach (var identifier in identifierExpressions) - { - var propertySymbol = semanticModel.GetSymbolInfo(identifier).Symbol as IPropertySymbol; - if (propertySymbol is not null) - { - HashSet exceptionTypes = GetPropertyExceptionTypes(context, identifier, propertySymbol); - - foreach (var exceptionType in exceptionTypes) - { - if (exceptionType is not null) - { - if (ShouldIncludeException(exceptionType, identifier, settings)) - { - exceptions.Add(exceptionType); - } - } - } - } - } - - return exceptions; - } - - public bool ShouldIncludeException(INamedTypeSymbol exceptionType, SyntaxNode node, AnalyzerSettings settings) - { - var exceptions = new HashSet(SymbolEqualityComparer.Default); - - var exceptionName = exceptionType.ToDisplayString(); - - if (settings.IgnoredExceptions.Contains(exceptionName)) - { - // Completely ignore this exception - return false; - } - else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) - { - if (ShouldIgnore(node, mode)) - { - return false; - } - } - - return true; - } - - private HashSet GetCaughtExceptions(SyntaxList catchClauses, SemanticModel semanticModel) - { - var caughtExceptions = new HashSet(SymbolEqualityComparer.Default); - - foreach (var catchClause in catchClauses) - { - if (catchClause.Declaration is not null) - { - var catchType = semanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; - if (catchType is not null) - { - caughtExceptions.Add(catchType); - } - } - else - { - // General catch clause catches all exceptions - caughtExceptions = null; - break; - } - } - - return caughtExceptions; - } - - private bool IsExceptionCaught(INamedTypeSymbol exceptionType, HashSet caughtExceptions) - { - if (caughtExceptions is null) - { - // General catch clause catches all exceptions - return true; - } - - return caughtExceptions.Any(catchType => - exceptionType.Equals(catchType, SymbolEqualityComparer.Default) || - exceptionType.InheritsFrom(catchType)); - } - - private void AnalyzeAwait(SyntaxNodeAnalysisContext context) - { - var settings = GetAnalyzerSettings(context.Options); - - var awaitExpression = (AwaitExpressionSyntax)context.Node; - - if (awaitExpression.Expression is InvocationExpressionSyntax invocation) - { - // Get the invoked symbol - var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation); - var methodSymbol = symbolInfo.Symbol as IMethodSymbol; - - if (methodSymbol is null) - return; - - // Handle delegate invokes by getting the target method symbol - if (methodSymbol.MethodKind == MethodKind.DelegateInvoke) - { - var targetMethodSymbol = GetTargetMethodSymbol(context, invocation); - if (targetMethodSymbol is not null) - { - methodSymbol = targetMethodSymbol; - } - else - { - // Could not find the target method symbol - return; - } - } - - AnalyzeMemberExceptions(context, invocation, methodSymbol, settings); - } - else if (awaitExpression.Expression is MemberAccessExpressionSyntax memberAccess) - { - AnalyzeIdentifierAndMemberAccess(context, memberAccess, settings); - } - else if (awaitExpression.Expression is IdentifierNameSyntax identifier) - { - AnalyzeIdentifierAndMemberAccess(context, identifier, settings); - } - } - - /// - /// Determines if a node is within a catch block. - /// - private bool IsWithinCatchBlock(SyntaxNode node, out CatchClauseSyntax catchClause) - { - catchClause = node.Ancestors().OfType().FirstOrDefault(); - return catchClause is not null; - } - - /// - /// Analyzes throw expressions to determine if exceptions are handled or declared. - /// - private void AnalyzeThrowExpression(SyntaxNodeAnalysisContext context) - { - var settings = GetAnalyzerSettings(context.Options); - - var throwExpression = (ThrowExpressionSyntax)context.Node; - - if (throwExpression.Expression is ObjectCreationExpressionSyntax creationExpression) - { - var exceptionType = context.SemanticModel.GetTypeInfo(creationExpression).Type as INamedTypeSymbol; - AnalyzeExceptionThrowingNode(context, throwExpression, exceptionType, settings); - } - } - - /// - /// Analyzes method calls to determine if exceptions are handled or declared. - /// - private void AnalyzeMethodCall(SyntaxNodeAnalysisContext context) - { - var settings = GetAnalyzerSettings(context.Options); - - var invocation = (InvocationExpressionSyntax)context.Node; - - if (invocation.Parent is AwaitExpressionSyntax) - { - // Handled in other method. - return; - } - - // Get the invoked symbol - var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation); - var methodSymbol = symbolInfo.Symbol as IMethodSymbol; - - if (methodSymbol is null) - return; - - // Handle delegate invokes by getting the target method symbol - if (methodSymbol.MethodKind == MethodKind.DelegateInvoke) - { - var targetMethodSymbol = GetTargetMethodSymbol(context, invocation); - if (targetMethodSymbol is not null) - { - methodSymbol = targetMethodSymbol; - } - else - { - // Could not find the target method symbol - return; - } - } - - AnalyzeMemberExceptions(context, invocation, methodSymbol, settings); - } - - /// - /// Resolves the target method symbol from a delegate, lambda, or method group. - /// - private IMethodSymbol GetTargetMethodSymbol(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation) - { - var expression = invocation.Expression; - - // Get the symbol of the expression being invoked - var symbolInfo = context.SemanticModel.GetSymbolInfo(expression); - var symbol = symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.FirstOrDefault(); - - if (symbol is null) - return null; - - if (symbol is ILocalSymbol localSymbol) - { - // Get the syntax node where the local variable is declared - var declaringSyntaxReference = localSymbol.DeclaringSyntaxReferences.FirstOrDefault(); - if (declaringSyntaxReference is not null) - { - var syntaxNode = declaringSyntaxReference.GetSyntax(); - - if (syntaxNode is VariableDeclaratorSyntax variableDeclarator) - { - var initializer = variableDeclarator.Initializer?.Value; - - if (initializer is not null) - { - // Handle lambdas - if (initializer is AnonymousFunctionExpressionSyntax anonymousFunction) - { - var lambdaSymbol = context.SemanticModel.GetSymbolInfo(anonymousFunction).Symbol as IMethodSymbol; - if (lambdaSymbol is not null) - return lambdaSymbol; - } - - // Handle method groups - if (initializer is IdentifierNameSyntax || initializer is MemberAccessExpressionSyntax) - { - var methodGroupSymbol = context.SemanticModel.GetSymbolInfo(initializer).Symbol as IMethodSymbol; - if (methodGroupSymbol is not null) - return methodGroupSymbol; - } - - // Get the method symbol of the initializer (lambda or method group) - var initializerSymbolInfo = context.SemanticModel.GetSymbolInfo(initializer); - var initializerSymbol = initializerSymbolInfo.Symbol ?? initializerSymbolInfo.CandidateSymbols.FirstOrDefault(); - - if (initializerSymbol is IMethodSymbol targetMethodSymbol) - { - return targetMethodSymbol; - } - } - } - } - } - - return null; - } - - /// - /// Analyzes object creation expressions to determine if exceptions are handled or declared. - /// - private void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context) - { - var settings = GetAnalyzerSettings(context.Options); - - var objectCreation = (ObjectCreationExpressionSyntax)context.Node; - - var constructorSymbol = context.SemanticModel.GetSymbolInfo(objectCreation).Symbol as IMethodSymbol; - if (constructorSymbol is null) - return; - - AnalyzeMemberExceptions(context, objectCreation, constructorSymbol, settings); - } - - - /// - /// Analyzes implicit object creation expressions to determine if exceptions are handled or declared. - /// - private void AnalyzeImplicitObjectCreation(SyntaxNodeAnalysisContext context) - { - var settings = GetAnalyzerSettings(context.Options); - - var objectCreation = (ImplicitObjectCreationExpressionSyntax)context.Node; - - var constructorSymbol = context.SemanticModel.GetSymbolInfo(objectCreation).Symbol as IMethodSymbol; - if (constructorSymbol is null) - return; - - AnalyzeMemberExceptions(context, objectCreation, constructorSymbol, settings); - } - - /// - /// Analyzes member access expressions (e.g., property accessors) for exception handling. - /// - private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) - { - var settings = GetAnalyzerSettings(context.Options); - - var memberAccess = (MemberAccessExpressionSyntax)context.Node; - - AnalyzeIdentifierAndMemberAccess(context, memberAccess, settings); - } - - /// - /// Analyzes identifier names (e.g. local variables or property accessors in context) for exception handling. - /// - private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context) - { - var settings = GetAnalyzerSettings(context.Options); - - var identifierName = (IdentifierNameSyntax)context.Node; - - // Ignore identifiers that are part of await expression - if (identifierName.Parent is AwaitExpressionSyntax) - return; - - // Ignore identifiers that are part of member access - if (identifierName.Parent is MemberAccessExpressionSyntax) - return; - - AnalyzeIdentifierAndMemberAccess(context, identifierName, settings); - } - - private void AnalyzeIdentifierAndMemberAccess(SyntaxNodeAnalysisContext context, ExpressionSyntax expression, AnalyzerSettings settings) - { - var s = context.SemanticModel.GetSymbolInfo(expression).Symbol; - var symbol = s as IPropertySymbol; - if (symbol is null) - return; - - if (symbol is IPropertySymbol propertySymbol) - { - AnalyzePropertyExceptions(context, expression, symbol, settings); - } - } - - /// - /// Analyzes element access expressions (e.g., indexers) for exception handling. - /// - private void AnalyzeElementAccess(SyntaxNodeAnalysisContext context) - { - var settings = GetAnalyzerSettings(context.Options); - - var elementAccess = (ElementAccessExpressionSyntax)context.Node; - - var symbol = context.SemanticModel.GetSymbolInfo(elementAccess).Symbol as IPropertySymbol; - if (symbol is null) - return; - - if (symbol is IPropertySymbol propertySymbol) - { - AnalyzePropertyExceptions(context, elementAccess, symbol, settings); - } - } - - /// - /// Analyzes event assignments (e.g., += or -=) for exception handling. - /// - private void AnalyzeEventAssignment(SyntaxNodeAnalysisContext context) - { - var settings = GetAnalyzerSettings(context.Options); - - var assignment = (AssignmentExpressionSyntax)context.Node; - - var eventSymbol = context.SemanticModel.GetSymbolInfo(assignment.Left).Symbol as IEventSymbol; - if (eventSymbol is null) - return; - - // Get the method symbol for the add or remove accessor - IMethodSymbol? methodSymbol = null; - - if (assignment.IsKind(SyntaxKind.AddAssignmentExpression) && eventSymbol.AddMethod is not null) - { - methodSymbol = eventSymbol.AddMethod; - } - else if (assignment.IsKind(SyntaxKind.SubtractAssignmentExpression) && eventSymbol.RemoveMethod is not null) - { - methodSymbol = eventSymbol.RemoveMethod; - } - - if (methodSymbol is not null) - { - AnalyzeMemberExceptions(context, assignment, methodSymbol, settings); - } - } - - /// - /// Analyzes exceptions thrown by a property, specifically its getters and setters. - /// - private void AnalyzePropertyExceptions(SyntaxNodeAnalysisContext context, ExpressionSyntax expression, IPropertySymbol propertySymbol, - AnalyzerSettings settings) - { - HashSet exceptionTypes = GetPropertyExceptionTypes(context, expression, propertySymbol); - - // Deduplicate and analyze each distinct exception type - foreach (var exceptionType in exceptionTypes.Distinct(SymbolEqualityComparer.Default).OfType()) - { - AnalyzeExceptionThrowingNode(context, expression, exceptionType, settings); - } - } - - private HashSet GetPropertyExceptionTypes(SyntaxNodeAnalysisContext context, ExpressionSyntax expression, IPropertySymbol propertySymbol) - { - // Determine if the analyzed expression is for a getter or setter - bool isGetter = IsPropertyGetter(expression); - bool isSetter = IsPropertySetter(expression); - - // List to collect all relevant exception types - var exceptionTypes = new HashSet(SymbolEqualityComparer.Default); - - // Retrieve exception types documented in XML comments for the property - var xmlDocumentedExceptions = GetExceptionTypesFromDocumentationCommentXml(context.Compilation, propertySymbol); - - // Filter exceptions documented specifically for the getter and setter - var getterExceptions = xmlDocumentedExceptions.Where(x => - x.Description.Contains(" get ") || - x.Description.Contains(" gets ") || - x.Description.Contains(" getting ") || - x.Description.Contains(" retrieved ")); - - var setterExceptions = xmlDocumentedExceptions.Where(x => - x.Description.Contains(" set ") || - x.Description.Contains(" sets ") || - x.Description.Contains(" setting ")); - - if (isSetter && propertySymbol.SetMethod is not null) - { - // Will filter away - setterExceptions = ProcessNullable(context, expression, propertySymbol.SetMethod, setterExceptions); - } - - // Handle exceptions that don't explicitly belong to getters or setters - var allOtherExceptions = xmlDocumentedExceptions - .Except(getterExceptions); - allOtherExceptions = allOtherExceptions - .Except(setterExceptions); - - if (isSetter && propertySymbol.SetMethod is not null) - { - allOtherExceptions = ProcessNullable(context, expression, propertySymbol.SetMethod, allOtherExceptions); - } - - // Analyze exceptions thrown by the getter if applicable - if (isGetter && propertySymbol.GetMethod is not null) - { - var getterMethodExceptions = GetExceptionTypes(propertySymbol.GetMethod); - exceptionTypes.AddRange(getterExceptions.Select(x => x.ExceptionType)); - exceptionTypes.AddRange(getterMethodExceptions); - } - - // Analyze exceptions thrown by the setter if applicable - if (isSetter && propertySymbol.SetMethod is not null) - { - var setterMethodExceptions = GetExceptionTypes(propertySymbol.SetMethod); - exceptionTypes.AddRange(setterExceptions.Select(x => x.ExceptionType)); - exceptionTypes.AddRange(setterMethodExceptions); - } - - if (propertySymbol.GetMethod is not null) - { - allOtherExceptions = ProcessNullable(context, expression, propertySymbol.GetMethod, allOtherExceptions); - } - - // Add other exceptions not specific to getters or setters - exceptionTypes.AddRange(allOtherExceptions.Select(x => x.ExceptionType)); - return exceptionTypes; - } - - /// - /// Analyzes exceptions thrown by a method, constructor, or other member. - /// - private void AnalyzeMemberExceptions(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol methodSymbol, - AnalyzerSettings settings) - { - if (methodSymbol is null) - return; - - List exceptionTypes = GetExceptionTypes(methodSymbol); - - // Get exceptions from XML documentation - var xmlExceptionTypes = GetExceptionTypesFromDocumentationCommentXml(context.Compilation, methodSymbol); - - xmlExceptionTypes = ProcessNullable(context, node, methodSymbol, xmlExceptionTypes); - - if (xmlExceptionTypes.Any()) - { - exceptionTypes.AddRange(xmlExceptionTypes.Select(x => x.ExceptionType)); - } - - exceptionTypes = ProcessNullable(context, node, methodSymbol, exceptionTypes).ToList(); - - foreach (var exceptionType in exceptionTypes.Distinct(SymbolEqualityComparer.Default).OfType()) - { - AnalyzeExceptionThrowingNode(context, node, exceptionType, settings); - } - } - - static INamedTypeSymbol? argumentNullExceptionTypeSymbol; - - private static IEnumerable ProcessNullable(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol methodSymbol, IEnumerable exceptionInfos) - { - if (argumentNullExceptionTypeSymbol is null) - { - argumentNullExceptionTypeSymbol = context.Compilation.GetTypeByMetadataName("System.ArgumentNullException"); - } - - var isCompilationNullableEnabled = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Enable; - - var nullableContext = context.SemanticModel.GetNullableContext(node.SpanStart); - var isNodeInNullableContext = nullableContext is NullableContext.Enabled; - - if (isNodeInNullableContext || isCompilationNullableEnabled) - { - if (methodSymbol.IsExtensionMethod) - { - return exceptionInfos.Where(x => !x.ExceptionType.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default)); - } - - if (methodSymbol.Parameters.Count() is 1) - { - var p = methodSymbol.Parameters.First(); - - if (p.NullableAnnotation is NullableAnnotation.NotAnnotated) - { - return exceptionInfos.Where(x => !x.ExceptionType.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default)); - } - } - else - { - exceptionInfos = exceptionInfos.Where(x => - { - var p = methodSymbol.Parameters.FirstOrDefault(p => x.Parameters.Any(p2 => p.Name == p2.Name)); - if (p is not null) - { - if (x.ExceptionType.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default) - && p.NullableAnnotation is NullableAnnotation.NotAnnotated) - { - return false; - } - } - return true; - }).ToList(); - } - } - - return exceptionInfos; - } - - private static IEnumerable ProcessNullable(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol methodSymbol, IEnumerable exceptions) - { - if (argumentNullExceptionTypeSymbol is null) - { - argumentNullExceptionTypeSymbol = context.Compilation.GetTypeByMetadataName("System.ArgumentNullException"); - } - - var isCompilationNullableEnabled = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Enable; - - var nullableContext = context.SemanticModel.GetNullableContext(node.SpanStart); - var isNodeInNullableContext = nullableContext is NullableContext.Enabled; - - if (isNodeInNullableContext || isCompilationNullableEnabled) - { - return exceptions.Where(x => !x.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default)); - } - - return exceptions; - } - - private static List GetExceptionTypes(IMethodSymbol methodSymbol) - { - // Get exceptions from Throws attributes - var exceptionAttributes = GetThrowsAttributes(methodSymbol); - - return GetDistictExceptionTypes(exceptionAttributes).ToList(); - } - - private static List GetThrowsAttributes(ISymbol symbol) - { - return GetThrowsAttributes(symbol.GetAttributes()); - } - - private static List GetThrowsAttributes(IEnumerable attributes) - { - return attributes - .Where(attr => attr.AttributeClass?.Name is "ThrowsAttribute") - .ToList(); - } - - /// - /// Determines if a catch clause handles the specified exception type. - /// - private bool CatchClauseHandlesException(CatchClauseSyntax catchClause, SemanticModel semanticModel, INamedTypeSymbol exceptionType) - { - if (catchClause.Declaration is null) - return true; // Catch-all handles all exceptions - - var catchType = semanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; - if (catchType is null) - return false; - - // Check if the exceptionType matches or inherits from the catchType - return exceptionType.Equals(catchType, SymbolEqualityComparer.Default) || - exceptionType.InheritsFrom(catchType); - } - - /// - /// Determines if an exception is handled by any enclosing try-catch blocks. - /// - private bool IsExceptionHandled(SyntaxNode node, INamedTypeSymbol exceptionType, SemanticModel semanticModel) - { - SyntaxNode? prevNode = null; - - var current = node.Parent; - while (current is not null) - { - // Stop here since the throwing node is within a lambda or a local function - // and the boundary has been reached. - if (current is AnonymousFunctionExpressionSyntax - or LocalFunctionStatementSyntax) - { - return false; - } - - if (current is TryStatementSyntax tryStatement) - { - // Prevents analysis within the first try-catch, - // when coming from either a catch clause or a finally clause. - - // Skip if the node is within a catch or finally block of the current try statement - bool isInCatchOrFinally = tryStatement.Catches.Any(c => c.Contains(node)) || - (tryStatement.Finally is not null && tryStatement.Finally.Contains(node)); - - - if (!isInCatchOrFinally) - { - foreach (var catchClause in tryStatement.Catches) - { - if (CatchClauseHandlesException(catchClause, semanticModel, exceptionType)) - { - return true; - } - } - } - } - - prevNode = current; - current = current.Parent; - } - - return false; // Exception is not handled by any enclosing try-catch - } - - /// - /// Analyzes a node that throws or propagates exceptions to check for handling or declaration. - /// - private void AnalyzeExceptionThrowingNode( - SyntaxNodeAnalysisContext context, - SyntaxNode node, - INamedTypeSymbol? exceptionType, - AnalyzerSettings settings) - { - if (exceptionType is null) - return; - - var exceptionName = exceptionType.ToDisplayString(); - - if (settings.IgnoredExceptions.Contains(exceptionName)) - { - // Completely ignore this exception - return; - } - else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) - { - if (ShouldIgnore(node, mode)) - { - // Report as THROW002 (Info level) - var diagnostic = Diagnostic.Create(RuleIgnoredException, GetSignificantLocation(node), exceptionType.Name); - context.ReportDiagnostic(diagnostic); - return; - } - } - - // Check for general exceptions - if (context.Node is not InvocationExpressionSyntax && IsGeneralException(exceptionType)) - { - context.ReportDiagnostic(Diagnostic.Create(RuleGeneralThrow, GetSignificantLocation(node))); - } - - // Check if the exception is declared via [Throws] - var isDeclared = IsExceptionDeclaredInMethod(context, node, exceptionType); - - // Determine if the exception is handled by any enclosing try-catch - var isHandled = IsExceptionHandled(node, exceptionType, context.SemanticModel); - - // Report diagnostic if neither handled nor declared - if (!isHandled && !isDeclared) - { - var properties = ImmutableDictionary.Create() - .Add("ExceptionType", exceptionType.Name); - - var isThrowingConstruct = node is ThrowStatementSyntax or ThrowExpressionSyntax; - - var verb = isThrowingConstruct ? THROW001Verbs.Is : THROW001Verbs.MightBe; - - var diagnostic = Diagnostic.Create(RuleUnhandledException, GetSignificantLocation(node), properties, exceptionType.Name, verb); - context.ReportDiagnostic(diagnostic); - } - } - - private bool ShouldIgnore(SyntaxNode node, ExceptionMode mode) - { - if (mode is ExceptionMode.Always) - return true; - - if (mode is ExceptionMode.Throw && node is ThrowStatementSyntax or ThrowExpressionSyntax) - return true; - - if (mode is ExceptionMode.Propagation && node - is MemberAccessExpressionSyntax - or IdentifierNameSyntax - or InvocationExpressionSyntax) - return true; - - return false; - } - - private bool IsExceptionDeclaredInMethod(SyntaxNodeAnalysisContext context, SyntaxNode node, INamedTypeSymbol exceptionType) - { - foreach (var ancestor in node.Ancestors()) - { - IMethodSymbol? methodSymbol = null; - - switch (ancestor) - { - case MethodDeclarationSyntax methodDeclaration: - methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDeclaration); - break; - case ConstructorDeclarationSyntax constructorDeclaration: - methodSymbol = context.SemanticModel.GetDeclaredSymbol(constructorDeclaration); - break; - case AccessorDeclarationSyntax accessorDeclaration: - methodSymbol = context.SemanticModel.GetDeclaredSymbol(accessorDeclaration); - break; - case LocalFunctionStatementSyntax localFunction: - methodSymbol = context.SemanticModel.GetDeclaredSymbol(localFunction); - break; - case AnonymousFunctionExpressionSyntax anonymousFunction: - methodSymbol = context.SemanticModel.GetSymbolInfo(anonymousFunction).Symbol as IMethodSymbol; - break; - default: - // Continue up to next node - continue; - } - - if (methodSymbol is not null) - { - if (IsExceptionDeclaredInSymbol(methodSymbol, exceptionType)) - return true; - - if (ancestor is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax) - { - // Break because you are analyzing a local function or anonymous function (lambda) - // If you don't then it will got to the method, and it will affect analysis of this inline function. - break; - } - } - } - - return false; - } - - private bool IsExceptionDeclaredInSymbol(IMethodSymbol methodSymbol, INamedTypeSymbol exceptionType) - { - if (methodSymbol is null) - return false; - - var declaredExceptionTypes = GetExceptionTypes(methodSymbol); - - foreach (var declaredExceptionType in declaredExceptionTypes) - { - if (exceptionType.Equals(declaredExceptionType, SymbolEqualityComparer.Default)) - return true; - - // Check if the declared exception is a base type of the thrown exception - if (exceptionType.InheritsFrom(declaredExceptionType)) - return true; - } - - return false; - } - - private bool IsGeneralException(INamedTypeSymbol exceptionType) - { - return exceptionType.ToDisplayString() is "System.Exception"; - } - - private bool IsPropertyGetter(ExpressionSyntax expression) - { - var parent = expression.Parent; - - if (parent is AssignmentExpressionSyntax assignment) - { - if (assignment.Left == expression) - return false; // It's a setter - } - else if (parent is PrefixUnaryExpressionSyntax prefixUnary) - { - if (prefixUnary.IsKind(SyntaxKind.PreIncrementExpression) || prefixUnary.IsKind(SyntaxKind.PreDecrementExpression)) - return false; // It's a setter - } - else if (parent is PostfixUnaryExpressionSyntax postfixUnary) - { - if (postfixUnary.IsKind(SyntaxKind.PostIncrementExpression) || postfixUnary.IsKind(SyntaxKind.PostDecrementExpression)) - return false; // It's a setter - } - - return true; // Assume getter in other cases - } - - private bool IsPropertySetter(ExpressionSyntax expression) - { - var parent = expression.Parent; - - if (parent is AssignmentExpressionSyntax assignment) - { - if (assignment.Left == expression) - return true; // It's a setter - } - else if (parent is PrefixUnaryExpressionSyntax prefixUnary) - { - if (prefixUnary.IsKind(SyntaxKind.PreIncrementExpression) || prefixUnary.IsKind(SyntaxKind.PreDecrementExpression)) - return true; // It's a setter - } - else if (parent is PostfixUnaryExpressionSyntax postfixUnary) - { - if (postfixUnary.IsKind(SyntaxKind.PostIncrementExpression) || postfixUnary.IsKind(SyntaxKind.PostDecrementExpression)) - return true; // It's a setter - } - - return false; // Assume getter in other cases - } +namespace Sundstrom.CheckedExceptions; + +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Text.Json; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public partial class CheckedExceptionsAnalyzer : DiagnosticAnalyzer +{ + static readonly ConcurrentDictionary configs = new ConcurrentDictionary(); + + // Diagnostic IDs + public const string DiagnosticIdUnhandled = "THROW001"; + public const string DiagnosticIdIgnoredException = "THROW002"; + public const string DiagnosticIdGeneralThrows = "THROW003"; + public const string DiagnosticIdGeneralThrow = "THROW004"; + public const string DiagnosticIdDuplicateDeclarations = "THROW005"; + public const string DiagnosticIdMissingThrowsOnBaseMember = "THROW006"; + public const string DiagnosticIdMissingThrowsFromBaseMember = "THROW007"; + + public static IEnumerable AllDiagnosticsIds = [DiagnosticIdUnhandled, DiagnosticIdGeneralThrows, DiagnosticIdGeneralThrow, DiagnosticIdDuplicateDeclarations]; + + private static readonly DiagnosticDescriptor RuleUnhandledException = new( + DiagnosticIdUnhandled, + "Unhandled exception", + "Exception '{0}' {1} thrown but not handled", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Reports exceptions that are thrown but not caught or declared with [Throws], potentially violating exception safety."); + + private static readonly DiagnosticDescriptor RuleIgnoredException = new DiagnosticDescriptor( + DiagnosticIdIgnoredException, + "Ignored exception may cause runtime issues", + "Exception '{0}' is ignored by configuration but may cause runtime issues if unhandled", + "Usage", + DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Informs about exceptions excluded from analysis but which may still propagate at runtime if not properly handled."); + + private static readonly DiagnosticDescriptor RuleGeneralThrow = new( + DiagnosticIdGeneralThrow, + "Avoid throwing 'Exception'", + "Throwing 'Exception' is too general; use a more specific exception type instead", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Discourages throwing the base System.Exception type directly, encouraging clearer and more actionable error semantics."); + + private static readonly DiagnosticDescriptor RuleGeneralThrows = new DiagnosticDescriptor( + DiagnosticIdGeneralThrows, + "Avoid declaring exception type 'Exception'", + "Declaring 'Exception' is too general; use a more specific exception type instead", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Discourages the use of System.Exception in [Throws] attributes. Prefer declaring more specific exception types."); + + private static readonly DiagnosticDescriptor RuleDuplicateDeclarations = new DiagnosticDescriptor( + DiagnosticIdDuplicateDeclarations, + "Avoid duplicate declarations of the same exception type", + "Duplicate declarations of the exception type '{0}' found. Remove them to avoid redundancy.", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Detects multiple [Throws] declarations for the same exception type on a single member, which is redundant."); + + private static readonly DiagnosticDescriptor RuleMissingThrowsOnBaseMember = new DiagnosticDescriptor( + DiagnosticIdMissingThrowsOnBaseMember, + "Missing Throws declaration", + "Exception '{1}' is not declared on base member '{0}'", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Base or interface members should declare compatible exceptions when overridden or implemented members declare them using [Throws]."); + + private static readonly DiagnosticDescriptor RuleMissingThrowsFromBaseMember = new( + DiagnosticIdMissingThrowsFromBaseMember, + "Missing Throws declaration for exception declared on base member", + "Base member '{0}' declares exception '{1}' which is not declared here", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Ensures that overridden or implemented members declare exceptions required by their base or interface definitions."); + + public override ImmutableArray SupportedDiagnostics => + [RuleUnhandledException, RuleIgnoredException, RuleGeneralThrows, RuleGeneralThrow, RuleDuplicateDeclarations, RuleMissingThrowsOnBaseMember, RuleMissingThrowsFromBaseMember]; + + private const string SettingsFileName = "CheckedExceptions.settings.json"; + private static readonly JsonSerializerOptions _settingsFileJsonOptions = new() + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + }; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + // Register actions for throw statements and expressions + context.RegisterSyntaxNodeAction(AnalyzeThrowStatement, SyntaxKind.ThrowStatement); + context.RegisterSyntaxNodeAction(AnalyzeThrowExpression, SyntaxKind.ThrowExpression); + + context.RegisterSymbolAction(AnalyzeMethodSymbol, SymbolKind.Method); + + context.RegisterSyntaxNodeAction(AnalyzeLambdaExpression, SyntaxKind.SimpleLambdaExpression); + context.RegisterSyntaxNodeAction(AnalyzeLambdaExpression, SyntaxKind.ParenthesizedLambdaExpression); + context.RegisterSyntaxNodeAction(AnalyzeLocalFunctionStatement, SyntaxKind.LocalFunctionStatement); + + // Register additional actions for method calls, object creations, etc. + context.RegisterSyntaxNodeAction(AnalyzeMethodCall, SyntaxKind.InvocationExpression); + context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression); + context.RegisterSyntaxNodeAction(AnalyzeImplicitObjectCreation, SyntaxKind.ImplicitObjectCreationExpression); + context.RegisterSyntaxNodeAction(AnalyzeIdentifier, SyntaxKind.IdentifierName); + context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); + context.RegisterSyntaxNodeAction(AnalyzeAwait, SyntaxKind.AwaitExpression); + context.RegisterSyntaxNodeAction(AnalyzeElementAccess, SyntaxKind.ElementAccessExpression); + context.RegisterSyntaxNodeAction(AnalyzeEventAssignment, SyntaxKind.AddAssignmentExpression); + context.RegisterSyntaxNodeAction(AnalyzeEventAssignment, SyntaxKind.SubtractAssignmentExpression); + } + + private AnalyzerSettings LoadAnalyzerSettings(AnalyzerOptions analyzerOptions) + { + return configs.GetOrAdd(analyzerOptions, key => + { + var configFileText = analyzerOptions.AdditionalFiles + .FirstOrDefault(f => SettingsFileName.Equals(Path.GetFileName(f.Path), StringComparison.OrdinalIgnoreCase)) + ?.GetText()?.ToString(); + + AnalyzerSettings? val = null; + + if (configFileText is not null) + { + val = JsonSerializer.Deserialize(configFileText, _settingsFileJsonOptions); + } + + return val ?? AnalyzerSettings.CreateWithDefaults(); // Return default options if config file is not found + }); + } + + private void AnalyzeLambdaExpression(SyntaxNodeAnalysisContext context) + { + var lambdaExpression = (LambdaExpressionSyntax)context.Node; + AnalyzeFunctionAttributes(lambdaExpression, lambdaExpression.AttributeLists.SelectMany(a => a.Attributes), context.SemanticModel, context); + } + + private void AnalyzeLocalFunctionStatement(SyntaxNodeAnalysisContext context) + { + var localFunction = (LocalFunctionStatementSyntax)context.Node; + AnalyzeFunctionAttributes(localFunction, localFunction.AttributeLists.SelectMany(a => a.Attributes), context.SemanticModel, context); + } + + private void AnalyzeFunctionAttributes(SyntaxNode node, IEnumerable attributes, SemanticModel semanticModel, SyntaxNodeAnalysisContext context) + { + var throwsAttributes = attributes + .Where(attr => IsThrowsAttribute(attr, semanticModel)) + .ToList(); + + CheckForGeneralExceptionThrows(throwsAttributes, context); + CheckForDuplicateThrowsAttributes(throwsAttributes, context); + } + + /// + /// Determines whether the given attribute is a ThrowsAttribute. + /// + private bool IsThrowsAttribute(AttributeSyntax attributeSyntax, SemanticModel semanticModel) + { + var attributeSymbol = semanticModel.GetSymbolInfo(attributeSyntax).Symbol as IMethodSymbol; + if (attributeSymbol is null) + return false; + + var attributeType = attributeSymbol.ContainingType; + return attributeType.Name is "ThrowsAttribute"; + } + + private void AnalyzeMethodSymbol(SymbolAnalysisContext context) + { + var methodSymbol = (IMethodSymbol)context.Symbol; + + if (methodSymbol is null) + return; + + var throwsAttributes = GetThrowsAttributes(methodSymbol).ToImmutableArray(); + + CheckForCompatibilityWithBaseOrInterface(context, throwsAttributes); + + if (throwsAttributes.Length == 0) + return; + + CheckForGeneralExceptionThrows(throwsAttributes, context); + CheckForDuplicateThrowsAttributes(context, throwsAttributes); + } + + private static IEnumerable FilterThrowsAttributesByException(ImmutableArray exceptionAttributes, string exceptionTypeName) + { + return exceptionAttributes + .Where(attribute => IsThrowsAttributeForException(attribute, exceptionTypeName)); + } + + public static bool IsThrowsAttributeForException(AttributeData attribute, string exceptionTypeName) + { + if (!attribute.ConstructorArguments.Any()) + return false; + + var exceptionTypes = GetDistictExceptionTypes(attribute); + return exceptionTypes.Any(exceptionType => exceptionType?.Name == exceptionTypeName); + } + + public static IEnumerable GetExceptionTypes(params IEnumerable exceptionAttributes) + { + var constructorArguments = exceptionAttributes + .SelectMany(attr => attr.ConstructorArguments); + + foreach (var arg in constructorArguments) + { + if (arg.Kind is TypedConstantKind.Array) + { + foreach (var t in arg.Values) + { + if (t.Kind is TypedConstantKind.Type) + { + yield return (INamedTypeSymbol)t.Value!; + } + } + } + else if (arg.Kind is TypedConstantKind.Type) + { + yield return (INamedTypeSymbol)arg.Value!; + } + } + } + + public static IEnumerable GetDistictExceptionTypes(params IEnumerable exceptionAttributes) + { + var exceptionTypes = GetExceptionTypes(exceptionAttributes); + + return exceptionTypes.Distinct(SymbolEqualityComparer.Default) + .OfType(); + } + + /// + /// Analyzes throw statements to determine if exceptions are handled or declared. + /// + private void AnalyzeThrowStatement(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var throwStatement = (ThrowStatementSyntax)context.Node; + + // Handle rethrows (throw;) + if (throwStatement.Expression is null) + { + if (IsWithinCatchBlock(throwStatement, out var catchClause)) + { + if (catchClause is not null) + { + if (catchClause.Declaration is null) + { + // General catch block with 'throw;' + // Analyze exceptions thrown in the try block + var tryStatement = catchClause.Ancestors().OfType().FirstOrDefault(); + if (tryStatement is not null) + { + AnalyzeExceptionsInTryBlock(context, tryStatement, catchClause, throwStatement, settings); + } + } + else + { + var exceptionType = context.SemanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; + AnalyzeExceptionThrowingNode(context, throwStatement, exceptionType, settings); + } + } + } + + return; // No further analysis for rethrows + } + + // Handle throw new ExceptionType() + if (throwStatement.Expression is ObjectCreationExpressionSyntax creationExpression) + { + var exceptionType = context.SemanticModel.GetTypeInfo(creationExpression).Type as INamedTypeSymbol; + AnalyzeExceptionThrowingNode(context, throwStatement, exceptionType, settings); + } + } + + private void AnalyzeExceptionsInTryBlock(SyntaxNodeAnalysisContext context, TryStatementSyntax tryStatement, CatchClauseSyntax generalCatchClause, ThrowStatementSyntax throwStatement, AnalyzerSettings settings) + { + var semanticModel = context.SemanticModel; + + // Collect exceptions that can be thrown in the try block + var thrownExceptions = CollectUnhandledExceptions(context, tryStatement.Block, settings); + + // Collect exception types handled by preceding catch clauses + var handledExceptions = new HashSet(SymbolEqualityComparer.Default); + foreach (var catchClause in tryStatement.Catches) + { + if (catchClause == generalCatchClause) + break; // Stop at the general catch clause + + if (catchClause.Declaration is not null) + { + var catchType = semanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; + if (catchType is not null) + { + handledExceptions.Add(catchType); + } + } + else + { + // General catch clause before our general catch; handles all exceptions + handledExceptions = null; + break; + } + } + + if (handledExceptions is null) + { + // All exceptions are handled by a previous general catch + return; + } + + // For each thrown exception, check if it is handled + foreach (var exceptionType in thrownExceptions.Distinct(SymbolEqualityComparer.Default).OfType()) + { + var exceptionName = exceptionType.ToDisplayString(); + + if (settings.IgnoredExceptions.Contains(exceptionName)) + { + // Completely ignore this exception + continue; + } + else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) + { + if (ShouldIgnore(throwStatement, mode)) + { + // Report as THROW002 (Info level) + var diagnostic = Diagnostic.Create(RuleIgnoredException, GetSignificantLocation(throwStatement), exceptionType.Name); + context.ReportDiagnostic(diagnostic); + continue; + } + } + + bool isHandled = handledExceptions.Any(handledException => + exceptionType.Equals(handledException, SymbolEqualityComparer.Default) || + exceptionType.InheritsFrom(handledException)); + + bool isDeclared = IsExceptionDeclaredInMethod(context, tryStatement, exceptionType); + + if (!isHandled && !isDeclared) + { + // Report diagnostic for unhandled exception + var diagnostic = Diagnostic.Create( + RuleUnhandledException, + GetSignificantLocation(throwStatement), + exceptionType.Name, + THROW001Verbs.MightBe); + + context.ReportDiagnostic(diagnostic); + } + } + } + + private HashSet CollectUnhandledExceptions(SyntaxNodeAnalysisContext context, BlockSyntax block, AnalyzerSettings settings) + { + var unhandledExceptions = new HashSet(SymbolEqualityComparer.Default); + + foreach (var statement in block.Statements) + { + if (statement is TryStatementSyntax tryStatement) + { + // Recursively collect exceptions from the inner try block + var innerUnhandledExceptions = CollectUnhandledExceptions(context, tryStatement.Block, settings); + + // Remove exceptions that are caught by the inner catch clauses + var caughtExceptions = GetCaughtExceptions(tryStatement.Catches, context.SemanticModel); + innerUnhandledExceptions.RemoveWhere(exceptionType => IsExceptionCaught(exceptionType, caughtExceptions)); + + // Add any exceptions that are not handled in the inner try block + unhandledExceptions.UnionWith(innerUnhandledExceptions); + } + else + { + // Collect exceptions thrown in this statement + var statementExceptions = CollectExceptionsFromStatement(context, statement, settings); + + // Add them to the unhandled exceptions + unhandledExceptions.UnionWith(statementExceptions); + } + } + + return unhandledExceptions; + } + + private HashSet CollectExceptionsFromStatement(SyntaxNodeAnalysisContext context, StatementSyntax statement, AnalyzerSettings settings) + { + SemanticModel semanticModel = context.SemanticModel; + + var exceptions = new HashSet(SymbolEqualityComparer.Default); + + foreach (var s in statement.DescendantNodesAndSelf()) + { + switch (s) + { + // Collect exceptions from throw statements + case ThrowStatementSyntax throwStatement: + CollectExpressionsFromThrows(throwStatement, throwStatement.Expression); + break; + + // Collect exceptions from throw expressions + case ThrowExpressionSyntax throwExpression: + CollectExpressionsFromThrows(throwExpression, throwExpression.Expression); + break; + + // Collect exceptions from method calls and object creations + case InvocationExpressionSyntax: + case ObjectCreationExpressionSyntax: + CollectExpressionsForMethodSymbols((ExpressionSyntax)s); + break; + + // Collect exceptions from property accessors and identifiers + case MemberAccessExpressionSyntax: + case ElementAccessExpressionSyntax: + case IdentifierNameSyntax: + CollectExpressionsForPropertySymbols((ExpressionSyntax)s); + break; + } + } + + return exceptions; + + void CollectExpressionsFromThrows(SyntaxNode throwExpression, ExpressionSyntax? subExpression) + { + if (subExpression is null) return; + + if (semanticModel.GetTypeInfo(subExpression).Type is not INamedTypeSymbol exceptionType) return; + + if (ShouldIncludeException(exceptionType, throwExpression, settings)) + { + exceptions.Add(exceptionType); + } + } + + void CollectExpressionsForMethodSymbols(ExpressionSyntax expression) + { + if (semanticModel.GetSymbolInfo(expression).Symbol is not IMethodSymbol methodSymbol) return; + + var exceptionTypes = GetExceptionTypes(methodSymbol); + + // Get exceptions from XML documentation + var xmlExceptionTypes = GetExceptionTypesFromDocumentationCommentXml(semanticModel.Compilation, methodSymbol); + + xmlExceptionTypes = ProcessNullable(context, expression, methodSymbol, xmlExceptionTypes); + + if (xmlExceptionTypes.Any()) + { + exceptionTypes = exceptionTypes.Concat(xmlExceptionTypes.Select(x => x.ExceptionType)); + } + + foreach (var exceptionType in exceptionTypes) + { + if (ShouldIncludeException(exceptionType, expression, settings)) + { + exceptions.Add(exceptionType); + } + } + } + + void CollectExpressionsForPropertySymbols(ExpressionSyntax expression) + { + if (semanticModel.GetSymbolInfo(expression).Symbol is not IPropertySymbol propertySymbol) return; + + HashSet exceptionTypes = GetPropertyExceptionTypes(context, expression, propertySymbol); + + foreach (var exceptionType in exceptionTypes) + { + if (ShouldIncludeException(exceptionType, expression, settings)) + { + exceptions.Add(exceptionType); + } + } + } + } + + public bool ShouldIncludeException(INamedTypeSymbol exceptionType, SyntaxNode node, AnalyzerSettings settings) + { + var exceptions = new HashSet(SymbolEqualityComparer.Default); + + var exceptionName = exceptionType.ToDisplayString(); + + if (settings.IgnoredExceptions.Contains(exceptionName)) + { + // Completely ignore this exception + return false; + } + else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) + { + if (ShouldIgnore(node, mode)) + { + return false; + } + } + + return true; + } + + private HashSet? GetCaughtExceptions(SyntaxList catchClauses, SemanticModel semanticModel) + { + var caughtExceptions = new HashSet(SymbolEqualityComparer.Default); + + foreach (var catchClause in catchClauses) + { + if (catchClause.Declaration is not null) + { + var catchType = semanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; + if (catchType is not null) + { + caughtExceptions.Add(catchType); + } + } + else + { + // General catch clause catches all exceptions + caughtExceptions = null; + break; + } + } + + return caughtExceptions; + } + + private bool IsExceptionCaught(INamedTypeSymbol exceptionType, HashSet? caughtExceptions) + { + if (caughtExceptions is null) + { + // General catch clause catches all exceptions + return true; + } + + return caughtExceptions.Any(catchType => + exceptionType.Equals(catchType, SymbolEqualityComparer.Default) || + exceptionType.InheritsFrom(catchType)); + } + + private void AnalyzeAwait(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var awaitExpression = (AwaitExpressionSyntax)context.Node; + + if (awaitExpression.Expression is InvocationExpressionSyntax invocation) + { + // Get the invoked symbol + var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation); + var methodSymbol = symbolInfo.Symbol as IMethodSymbol; + + if (methodSymbol is null) + return; + + // Handle delegate invokes by getting the target method symbol + if (methodSymbol.MethodKind == MethodKind.DelegateInvoke) + { + var targetMethodSymbol = GetTargetMethodSymbol(context, invocation); + if (targetMethodSymbol is not null) + { + methodSymbol = targetMethodSymbol; + } + else + { + // Could not find the target method symbol + return; + } + } + + AnalyzeMemberExceptions(context, invocation, methodSymbol, settings); + } + else if (awaitExpression.Expression is MemberAccessExpressionSyntax memberAccess) + { + AnalyzeIdentifierAndMemberAccess(context, memberAccess, settings); + } + else if (awaitExpression.Expression is IdentifierNameSyntax identifier) + { + AnalyzeIdentifierAndMemberAccess(context, identifier, settings); + } + } + + /// + /// Determines if a node is within a catch block. + /// + private bool IsWithinCatchBlock(SyntaxNode node, out CatchClauseSyntax catchClause) + { + catchClause = node.Ancestors().OfType().FirstOrDefault(); + return catchClause is not null; + } + + /// + /// Analyzes throw expressions to determine if exceptions are handled or declared. + /// + private void AnalyzeThrowExpression(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var throwExpression = (ThrowExpressionSyntax)context.Node; + + if (throwExpression.Expression is ObjectCreationExpressionSyntax creationExpression) + { + var exceptionType = context.SemanticModel.GetTypeInfo(creationExpression).Type as INamedTypeSymbol; + AnalyzeExceptionThrowingNode(context, throwExpression, exceptionType, settings); + } + } + + /// + /// Analyzes method calls to determine if exceptions are handled or declared. + /// + private void AnalyzeMethodCall(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var invocation = (InvocationExpressionSyntax)context.Node; + + if (invocation.Parent is AwaitExpressionSyntax) + { + // Handled in other method. + return; + } + + // Get the invoked symbol + var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation); + var methodSymbol = symbolInfo.Symbol as IMethodSymbol; + + if (methodSymbol is null) + return; + + // Handle delegate invokes by getting the target method symbol + if (methodSymbol.MethodKind == MethodKind.DelegateInvoke) + { + var targetMethodSymbol = GetTargetMethodSymbol(context, invocation); + if (targetMethodSymbol is not null) + { + methodSymbol = targetMethodSymbol; + } + else + { + // Could not find the target method symbol + return; + } + } + + AnalyzeMemberExceptions(context, invocation, methodSymbol, settings); + } + + /// + /// Resolves the target method symbol from a delegate, lambda, or method group. + /// + private IMethodSymbol? GetTargetMethodSymbol(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation) + { + var expression = invocation.Expression; + + // Get the symbol of the expression being invoked + var symbolInfo = context.SemanticModel.GetSymbolInfo(expression); + var symbol = symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.FirstOrDefault(); + + if (symbol is null) + return null; + + if (symbol is ILocalSymbol localSymbol) + { + // Get the syntax node where the local variable is declared + var declaringSyntaxReference = localSymbol.DeclaringSyntaxReferences.FirstOrDefault(); + if (declaringSyntaxReference is not null) + { + var syntaxNode = declaringSyntaxReference.GetSyntax(); + + if (syntaxNode is VariableDeclaratorSyntax variableDeclarator) + { + var initializer = variableDeclarator.Initializer?.Value; + + if (initializer is not null) + { + // Handle lambdas + if (initializer is AnonymousFunctionExpressionSyntax anonymousFunction) + { + var lambdaSymbol = context.SemanticModel.GetSymbolInfo(anonymousFunction).Symbol as IMethodSymbol; + if (lambdaSymbol is not null) + return lambdaSymbol; + } + + // Handle method groups + if (initializer is IdentifierNameSyntax || initializer is MemberAccessExpressionSyntax) + { + var methodGroupSymbol = context.SemanticModel.GetSymbolInfo(initializer).Symbol as IMethodSymbol; + if (methodGroupSymbol is not null) + return methodGroupSymbol; + } + + // Get the method symbol of the initializer (lambda or method group) + var initializerSymbolInfo = context.SemanticModel.GetSymbolInfo(initializer); + var initializerSymbol = initializerSymbolInfo.Symbol ?? initializerSymbolInfo.CandidateSymbols.FirstOrDefault(); + + if (initializerSymbol is IMethodSymbol targetMethodSymbol) + { + return targetMethodSymbol; + } + } + } + } + } + + return null; + } + + /// + /// Analyzes object creation expressions to determine if exceptions are handled or declared. + /// + private void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var objectCreation = (ObjectCreationExpressionSyntax)context.Node; + + var constructorSymbol = context.SemanticModel.GetSymbolInfo(objectCreation).Symbol as IMethodSymbol; + if (constructorSymbol is null) + return; + + AnalyzeMemberExceptions(context, objectCreation, constructorSymbol, settings); + } + + + /// + /// Analyzes implicit object creation expressions to determine if exceptions are handled or declared. + /// + private void AnalyzeImplicitObjectCreation(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var objectCreation = (ImplicitObjectCreationExpressionSyntax)context.Node; + + var constructorSymbol = context.SemanticModel.GetSymbolInfo(objectCreation).Symbol as IMethodSymbol; + if (constructorSymbol is null) + return; + + AnalyzeMemberExceptions(context, objectCreation, constructorSymbol, settings); + } + + /// + /// Analyzes member access expressions (e.g., property accessors) for exception handling. + /// + private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var memberAccess = (MemberAccessExpressionSyntax)context.Node; + + AnalyzeIdentifierAndMemberAccess(context, memberAccess, settings); + } + + /// + /// Analyzes identifier names (e.g. local variables or property accessors in context) for exception handling. + /// + private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var identifierName = (IdentifierNameSyntax)context.Node; + + // Ignore identifiers that are part of await expression + if (identifierName.Parent is AwaitExpressionSyntax) + return; + + // Ignore identifiers that are part of member access + if (identifierName.Parent is MemberAccessExpressionSyntax) + return; + + AnalyzeIdentifierAndMemberAccess(context, identifierName, settings); + } + + private void AnalyzeIdentifierAndMemberAccess(SyntaxNodeAnalysisContext context, ExpressionSyntax expression, AnalyzerSettings settings) + { + var s = context.SemanticModel.GetSymbolInfo(expression).Symbol; + var symbol = s as IPropertySymbol; + if (symbol is null) + return; + + if (symbol is IPropertySymbol propertySymbol) + { + AnalyzePropertyExceptions(context, expression, symbol, settings); + } + } + + /// + /// Analyzes element access expressions (e.g., indexers) for exception handling. + /// + private void AnalyzeElementAccess(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var elementAccess = (ElementAccessExpressionSyntax)context.Node; + + var symbol = context.SemanticModel.GetSymbolInfo(elementAccess).Symbol as IPropertySymbol; + if (symbol is null) + return; + + if (symbol is IPropertySymbol propertySymbol) + { + AnalyzePropertyExceptions(context, elementAccess, symbol, settings); + } + } + + /// + /// Analyzes event assignments (e.g., += or -=) for exception handling. + /// + private void AnalyzeEventAssignment(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var assignment = (AssignmentExpressionSyntax)context.Node; + + var eventSymbol = context.SemanticModel.GetSymbolInfo(assignment.Left).Symbol as IEventSymbol; + if (eventSymbol is null) + return; + + // Get the method symbol for the add or remove accessor + IMethodSymbol? methodSymbol = null; + + if (assignment.IsKind(SyntaxKind.AddAssignmentExpression) && eventSymbol.AddMethod is not null) + { + methodSymbol = eventSymbol.AddMethod; + } + else if (assignment.IsKind(SyntaxKind.SubtractAssignmentExpression) && eventSymbol.RemoveMethod is not null) + { + methodSymbol = eventSymbol.RemoveMethod; + } + + if (methodSymbol is not null) + { + AnalyzeMemberExceptions(context, assignment, methodSymbol, settings); + } + } + + /// + /// Analyzes exceptions thrown by a property, specifically its getters and setters. + /// + private void AnalyzePropertyExceptions(SyntaxNodeAnalysisContext context, ExpressionSyntax expression, IPropertySymbol propertySymbol, + AnalyzerSettings settings) + { + HashSet exceptionTypes = GetPropertyExceptionTypes(context, expression, propertySymbol); + + // Deduplicate and analyze each distinct exception type + foreach (var exceptionType in exceptionTypes.Distinct(SymbolEqualityComparer.Default).OfType()) + { + AnalyzeExceptionThrowingNode(context, expression, exceptionType, settings); + } + } + + private HashSet GetPropertyExceptionTypes(SyntaxNodeAnalysisContext context, ExpressionSyntax expression, IPropertySymbol propertySymbol) + { + // Determine if the analyzed expression is for a getter or setter + bool isGetter = IsPropertyGetter(expression); + bool isSetter = IsPropertySetter(expression); + + // List to collect all relevant exception types + var exceptionTypes = new HashSet(SymbolEqualityComparer.Default); + + // Retrieve exception types documented in XML comments for the property + var xmlDocumentedExceptions = GetExceptionTypesFromDocumentationCommentXml(context.Compilation, propertySymbol); + + // Filter exceptions documented specifically for the getter and setter + var getterExceptions = xmlDocumentedExceptions.Where(x => + x.Description.Contains(" get ") || + x.Description.Contains(" gets ") || + x.Description.Contains(" getting ") || + x.Description.Contains(" retrieved ")); + + var setterExceptions = xmlDocumentedExceptions.Where(x => + x.Description.Contains(" set ") || + x.Description.Contains(" sets ") || + x.Description.Contains(" setting ")); + + if (isSetter && propertySymbol.SetMethod is not null) + { + // Will filter away + setterExceptions = ProcessNullable(context, expression, propertySymbol.SetMethod, setterExceptions); + } + + // Handle exceptions that don't explicitly belong to getters or setters + var allOtherExceptions = xmlDocumentedExceptions + .Except(getterExceptions); + allOtherExceptions = allOtherExceptions + .Except(setterExceptions); + + if (isSetter && propertySymbol.SetMethod is not null) + { + allOtherExceptions = ProcessNullable(context, expression, propertySymbol.SetMethod, allOtherExceptions); + } + + // Analyze exceptions thrown by the getter if applicable + if (isGetter && propertySymbol.GetMethod is not null) + { + var getterMethodExceptions = GetExceptionTypes(propertySymbol.GetMethod); + exceptionTypes.AddRange(getterExceptions.Select(x => x.ExceptionType)); + exceptionTypes.AddRange(getterMethodExceptions); + } + + // Analyze exceptions thrown by the setter if applicable + if (isSetter && propertySymbol.SetMethod is not null) + { + var setterMethodExceptions = GetExceptionTypes(propertySymbol.SetMethod); + exceptionTypes.AddRange(setterExceptions.Select(x => x.ExceptionType)); + exceptionTypes.AddRange(setterMethodExceptions); + } + + if (propertySymbol.GetMethod is not null) + { + allOtherExceptions = ProcessNullable(context, expression, propertySymbol.GetMethod, allOtherExceptions); + } + + // Add other exceptions not specific to getters or setters + exceptionTypes.AddRange(allOtherExceptions.Select(x => x.ExceptionType)); + return exceptionTypes; + } + + /// + /// Analyzes exceptions thrown by a method, constructor, or other member. + /// + private void AnalyzeMemberExceptions(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol methodSymbol, + AnalyzerSettings settings) + { + if (methodSymbol is null) + return; + + IEnumerable exceptionTypes = GetExceptionTypes(methodSymbol); + + // Get exceptions from XML documentation + var xmlExceptionTypes = GetExceptionTypesFromDocumentationCommentXml(context.Compilation, methodSymbol); + + xmlExceptionTypes = ProcessNullable(context, node, methodSymbol, xmlExceptionTypes); + + if (xmlExceptionTypes.Any()) + { + exceptionTypes = exceptionTypes.Concat(xmlExceptionTypes.Select(x => x.ExceptionType)); + } + + exceptionTypes = ProcessNullable(context, node, methodSymbol, exceptionTypes) + .Distinct(SymbolEqualityComparer.Default) + .OfType(); + + foreach (var exceptionType in exceptionTypes) + { + AnalyzeExceptionThrowingNode(context, node, exceptionType, settings); + } + } + + private static IEnumerable ProcessNullable(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol methodSymbol, IEnumerable exceptionInfos) + { + var argumentNullExceptionTypeSymbol = context.Compilation.GetTypeByMetadataName("System.ArgumentNullException"); + + var isCompilationNullableEnabled = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Enable; + + var nullableContext = context.SemanticModel.GetNullableContext(node.SpanStart); + var isNodeInNullableContext = nullableContext is NullableContext.Enabled; + + if (isNodeInNullableContext || isCompilationNullableEnabled) + { + if (methodSymbol.IsExtensionMethod) + { + return exceptionInfos.Where(x => !x.ExceptionType.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default)); + } + + if (methodSymbol.Parameters.Count() is 1) + { + var p = methodSymbol.Parameters.First(); + + if (p.NullableAnnotation is NullableAnnotation.NotAnnotated) + { + return exceptionInfos.Where(x => !x.ExceptionType.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default)); + } + } + else + { + exceptionInfos = exceptionInfos.Where(x => + { + var p = methodSymbol.Parameters.FirstOrDefault(p => x.Parameters.Any(p2 => p.Name == p2.Name)); + if (p is not null) + { + if (x.ExceptionType.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default) + && p.NullableAnnotation is NullableAnnotation.NotAnnotated) + { + return false; + } + } + + return true; + }); + } + } + + return exceptionInfos; + } + + private static IEnumerable ProcessNullable(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol methodSymbol, IEnumerable exceptions) + { + var argumentNullExceptionTypeSymbol = context.Compilation.GetTypeByMetadataName("System.ArgumentNullException"); + + var isCompilationNullableEnabled = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Enable; + + var nullableContext = context.SemanticModel.GetNullableContext(node.SpanStart); + var isNodeInNullableContext = nullableContext is NullableContext.Enabled; + + if (isNodeInNullableContext || isCompilationNullableEnabled) + { + return exceptions.Where(x => !x.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default)); + } + + return exceptions; + } + + private static IEnumerable GetExceptionTypes(IMethodSymbol methodSymbol) + { + // Get exceptions from Throws attributes + var exceptionAttributes = GetThrowsAttributes(methodSymbol); + + return GetDistictExceptionTypes(exceptionAttributes); + } + + private static IEnumerable GetThrowsAttributes(ISymbol symbol) + { + return GetThrowsAttributes(symbol.GetAttributes()); + } + + private static IEnumerable GetThrowsAttributes(IEnumerable attributes) + { + return attributes.Where(attr => attr.AttributeClass?.Name is "ThrowsAttribute"); + } + + /// + /// Determines if a catch clause handles the specified exception type. + /// + private bool CatchClauseHandlesException(CatchClauseSyntax catchClause, SemanticModel semanticModel, INamedTypeSymbol exceptionType) + { + if (catchClause.Declaration is null) + return true; // Catch-all handles all exceptions + + var catchType = semanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; + if (catchType is null) + return false; + + // Check if the exceptionType matches or inherits from the catchType + return exceptionType.Equals(catchType, SymbolEqualityComparer.Default) || + exceptionType.InheritsFrom(catchType); + } + + /// + /// Determines if an exception is handled by any enclosing try-catch blocks. + /// + private bool IsExceptionHandled(SyntaxNode node, INamedTypeSymbol exceptionType, SemanticModel semanticModel) + { + SyntaxNode? prevNode = null; + + var current = node.Parent; + while (current is not null) + { + // Stop here since the throwing node is within a lambda or a local function + // and the boundary has been reached. + if (current is AnonymousFunctionExpressionSyntax + or LocalFunctionStatementSyntax) + { + return false; + } + + if (current is TryStatementSyntax tryStatement) + { + // Prevents analysis within the first try-catch, + // when coming from either a catch clause or a finally clause. + + // Skip if the node is within a catch or finally block of the current try statement + bool isInCatchOrFinally = tryStatement.Catches.Any(c => c.Contains(node)) || + (tryStatement.Finally is not null && tryStatement.Finally.Contains(node)); + + + if (!isInCatchOrFinally) + { + foreach (var catchClause in tryStatement.Catches) + { + if (CatchClauseHandlesException(catchClause, semanticModel, exceptionType)) + { + return true; + } + } + } + } + + prevNode = current; + current = current.Parent; + } + + return false; // Exception is not handled by any enclosing try-catch + } + + /// + /// Analyzes a node that throws or propagates exceptions to check for handling or declaration. + /// + private void AnalyzeExceptionThrowingNode( + SyntaxNodeAnalysisContext context, + SyntaxNode node, + INamedTypeSymbol? exceptionType, + AnalyzerSettings settings) + { + if (exceptionType is null) + return; + + var exceptionName = exceptionType.ToDisplayString(); + + if (settings.IgnoredExceptions.Contains(exceptionName)) + { + // Completely ignore this exception + return; + } + else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) + { + if (ShouldIgnore(node, mode)) + { + // Report as THROW002 (Info level) + var diagnostic = Diagnostic.Create(RuleIgnoredException, GetSignificantLocation(node), exceptionType.Name); + context.ReportDiagnostic(diagnostic); + return; + } + } + + // Check for general exceptions + if (context.Node is not InvocationExpressionSyntax && IsGeneralException(exceptionType)) + { + context.ReportDiagnostic(Diagnostic.Create(RuleGeneralThrow, GetSignificantLocation(node))); + } + + // Check if the exception is declared via [Throws] + var isDeclared = IsExceptionDeclaredInMethod(context, node, exceptionType); + + // Determine if the exception is handled by any enclosing try-catch + var isHandled = IsExceptionHandled(node, exceptionType, context.SemanticModel); + + // Report diagnostic if neither handled nor declared + if (!isHandled && !isDeclared) + { + var properties = ImmutableDictionary.Create() + .Add("ExceptionType", exceptionType.Name); + + var isThrowingConstruct = node is ThrowStatementSyntax or ThrowExpressionSyntax; + + var verb = isThrowingConstruct ? THROW001Verbs.Is : THROW001Verbs.MightBe; + + var diagnostic = Diagnostic.Create(RuleUnhandledException, GetSignificantLocation(node), properties, exceptionType.Name, verb); + context.ReportDiagnostic(diagnostic); + } + } + + private bool ShouldIgnore(SyntaxNode node, ExceptionMode mode) + { + if (mode is ExceptionMode.Always) + return true; + + if (mode is ExceptionMode.Throw && node is ThrowStatementSyntax or ThrowExpressionSyntax) + return true; + + if (mode is ExceptionMode.Propagation && node + is MemberAccessExpressionSyntax + or IdentifierNameSyntax + or InvocationExpressionSyntax) + return true; + + return false; + } + + private bool IsExceptionDeclaredInMethod(SyntaxNodeAnalysisContext context, SyntaxNode node, INamedTypeSymbol exceptionType) + { + foreach (var ancestor in node.Ancestors()) + { + IMethodSymbol? methodSymbol = null; + + switch (ancestor) + { + case MethodDeclarationSyntax methodDeclaration: + methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDeclaration); + break; + case ConstructorDeclarationSyntax constructorDeclaration: + methodSymbol = context.SemanticModel.GetDeclaredSymbol(constructorDeclaration); + break; + case AccessorDeclarationSyntax accessorDeclaration: + methodSymbol = context.SemanticModel.GetDeclaredSymbol(accessorDeclaration); + break; + case LocalFunctionStatementSyntax localFunction: + methodSymbol = context.SemanticModel.GetDeclaredSymbol(localFunction); + break; + case AnonymousFunctionExpressionSyntax anonymousFunction: + methodSymbol = context.SemanticModel.GetSymbolInfo(anonymousFunction).Symbol as IMethodSymbol; + break; + default: + // Continue up to next node + continue; + } + + if (methodSymbol is not null) + { + if (IsExceptionDeclaredInSymbol(methodSymbol, exceptionType)) + return true; + + if (ancestor is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax) + { + // Break because you are analyzing a local function or anonymous function (lambda) + // If you don't then it will got to the method, and it will affect analysis of this inline function. + break; + } + } + } + + return false; + } + + private bool IsExceptionDeclaredInSymbol(IMethodSymbol methodSymbol, INamedTypeSymbol exceptionType) + { + if (methodSymbol is null) + return false; + + var declaredExceptionTypes = GetExceptionTypes(methodSymbol); + + foreach (var declaredExceptionType in declaredExceptionTypes) + { + if (exceptionType.Equals(declaredExceptionType, SymbolEqualityComparer.Default)) + return true; + + // Check if the declared exception is a base type of the thrown exception + if (exceptionType.InheritsFrom(declaredExceptionType)) + return true; + } + + return false; + } + + private bool IsGeneralException(INamedTypeSymbol exceptionType) + { + return exceptionType.ToDisplayString() is "System.Exception"; + } + + private bool IsPropertyGetter(ExpressionSyntax expression) + { + var parent = expression.Parent; + + if (parent is AssignmentExpressionSyntax assignment) + { + if (assignment.Left == expression) + return false; // It's a setter + } + else if (parent is PrefixUnaryExpressionSyntax prefixUnary) + { + if (prefixUnary.IsKind(SyntaxKind.PreIncrementExpression) || prefixUnary.IsKind(SyntaxKind.PreDecrementExpression)) + return false; // It's a setter + } + else if (parent is PostfixUnaryExpressionSyntax postfixUnary) + { + if (postfixUnary.IsKind(SyntaxKind.PostIncrementExpression) || postfixUnary.IsKind(SyntaxKind.PostDecrementExpression)) + return false; // It's a setter + } + + return true; // Assume getter in other cases + } + + private bool IsPropertySetter(ExpressionSyntax expression) + { + var parent = expression.Parent; + + if (parent is AssignmentExpressionSyntax assignment) + { + if (assignment.Left == expression) + return true; // It's a setter + } + else if (parent is PrefixUnaryExpressionSyntax prefixUnary) + { + if (prefixUnary.IsKind(SyntaxKind.PreIncrementExpression) || prefixUnary.IsKind(SyntaxKind.PreDecrementExpression)) + return true; // It's a setter + } + else if (parent is PostfixUnaryExpressionSyntax postfixUnary) + { + if (postfixUnary.IsKind(SyntaxKind.PostIncrementExpression) || postfixUnary.IsKind(SyntaxKind.PostDecrementExpression)) + return true; // It's a setter + } + + return false; // Assume getter in other cases + } } \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index ce02fa6..6496259 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,21 +1,21 @@ - - - true - false - - - - - - - - - - - - - - - - + + + true + false + + + + + + + + + + + + + + + + \ No newline at end of file From e4390f10146a98ca5ffcd2296eb9824ccab3dcb0 Mon Sep 17 00:00:00 2001 From: sibber5 Date: Fri, 11 Jul 2025 03:50:17 +0300 Subject: [PATCH 2/5] Fix rest of nullable warnings in CheckedExceptions proj --- CheckedExceptions/AttributeHelper.cs | 10 +- .../CheckedExceptionsAnalyzer.Inheritance.cs | 6 +- .../CheckedExceptionsAnalyzer.Shared.cs | 2 +- .../CheckedExceptionsAnalyzer.cs | 157 +++++++----------- CheckedExceptions/TypeSymbolExtensions.cs | 2 +- CheckedExceptions/XmlDocumentationHelper.cs | 5 +- 6 files changed, 75 insertions(+), 107 deletions(-) diff --git a/CheckedExceptions/AttributeHelper.cs b/CheckedExceptions/AttributeHelper.cs index 7f3b873..a497d48 100644 --- a/CheckedExceptions/AttributeHelper.cs +++ b/CheckedExceptions/AttributeHelper.cs @@ -1,13 +1,13 @@ -namespace Sundstrom.CheckedExceptions; - -using System.Diagnostics; - +namespace Sundstrom.CheckedExceptions; + +using System.Diagnostics; + using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; public static class AttributeHelper { - public static AttributeData? GetSpecificAttributeData(AttributeSyntax attributeSyntax, SemanticModel semanticModel) + public static AttributeData? GetSpecificAttributeData(AttributeSyntax? attributeSyntax, SemanticModel? semanticModel) { if (attributeSyntax is null || semanticModel is null) return null; diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.Inheritance.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.Inheritance.cs index 538e570..5a39013 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.Inheritance.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.Inheritance.cs @@ -20,7 +20,7 @@ MethodKind.EventAdd or MethodKind.EventRemove)) return; - ImmutableHashSet declaredExceptions = GetDistictExceptionTypes(throwsAttributes).Where(x => x is not null).ToImmutableHashSet(SymbolEqualityComparer.Default)!; + ImmutableHashSet declaredExceptions = GetDistinctExceptionTypes(throwsAttributes).Where(x => x is not null).ToImmutableHashSet(SymbolEqualityComparer.Default)!; Debug.Assert(!declaredExceptions.Any(x => x is null)); if (declaredExceptions.Count == 0) @@ -50,7 +50,7 @@ private void AnalyzeMissingThrowsFromBaseMember(SymbolAnalysisContext context, I var isCovered = declaredExceptions.Any(declared => { - if (baseException.Equals(declared, SymbolEqualityComparer.Default)) + if (declared.Equals(baseException, SymbolEqualityComparer.Default)) return true; var declaredNamed = declared as INamedTypeSymbol; @@ -102,7 +102,7 @@ private static void AnalyzeMissingThrowsOnBaseMember(SymbolAnalysisContext conte private bool IsTooGenericException(ITypeSymbol ex) { if (ex is not INamedTypeSymbol namedTypeSymbol) return false; - + var fullName = namedTypeSymbol.ToDisplayString(); return fullName.Equals(typeof(Exception).FullName, StringComparison.Ordinal) || fullName.Equals(typeof(SystemException).FullName, StringComparison.Ordinal); diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.Shared.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.Shared.cs index 7070925..c892b5e 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.Shared.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.Shared.cs @@ -9,7 +9,7 @@ partial class CheckedExceptionsAnalyzer /// /// Retrieves the name of the exception type from a ThrowsAttribute's AttributeData. /// - private string GetExceptionTypeName(AttributeData attributeData) + private string GetExceptionTypeName(AttributeData? attributeData) { if (attributeData is null) return string.Empty; diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.cs index 1af33a8..b20bda0 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Collections.Immutable; using System.Linq; -using System.Reflection; using System.Text.Json; using Microsoft.CodeAnalysis; @@ -25,7 +24,7 @@ public partial class CheckedExceptionsAnalyzer : DiagnosticAnalyzer public const string DiagnosticIdMissingThrowsOnBaseMember = "THROW006"; public const string DiagnosticIdMissingThrowsFromBaseMember = "THROW007"; - public static IEnumerable AllDiagnosticsIds = [DiagnosticIdUnhandled, DiagnosticIdGeneralThrows, DiagnosticIdGeneralThrow, DiagnosticIdDuplicateDeclarations]; + public static readonly IEnumerable AllDiagnosticsIds = [DiagnosticIdUnhandled, DiagnosticIdGeneralThrows, DiagnosticIdGeneralThrow, DiagnosticIdDuplicateDeclarations]; private static readonly DiagnosticDescriptor RuleUnhandledException = new( DiagnosticIdUnhandled, @@ -130,7 +129,7 @@ public override void Initialize(AnalysisContext context) private AnalyzerSettings LoadAnalyzerSettings(AnalyzerOptions analyzerOptions) { - return configs.GetOrAdd(analyzerOptions, key => + return configs.GetOrAdd(analyzerOptions, _ => { var configFileText = analyzerOptions.AdditionalFiles .FirstOrDefault(f => SettingsFileName.Equals(Path.GetFileName(f.Path), StringComparison.OrdinalIgnoreCase)) @@ -143,7 +142,7 @@ private AnalyzerSettings LoadAnalyzerSettings(AnalyzerOptions analyzerOptions) val = JsonSerializer.Deserialize(configFileText, _settingsFileJsonOptions); } - return val ?? AnalyzerSettings.CreateWithDefaults(); // Return default options if config file is not found + return val ?? AnalyzerSettings.CreateWithDefaults(); // Return default options if the config file is not found }); } @@ -186,9 +185,6 @@ private void AnalyzeMethodSymbol(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; - if (methodSymbol is null) - return; - var throwsAttributes = GetThrowsAttributes(methodSymbol).ToImmutableArray(); CheckForCompatibilityWithBaseOrInterface(context, throwsAttributes); @@ -211,7 +207,7 @@ public static bool IsThrowsAttributeForException(AttributeData attribute, string if (!attribute.ConstructorArguments.Any()) return false; - var exceptionTypes = GetDistictExceptionTypes(attribute); + var exceptionTypes = GetDistinctExceptionTypes(attribute); return exceptionTypes.Any(exceptionType => exceptionType?.Name == exceptionTypeName); } @@ -239,11 +235,12 @@ public static IEnumerable GetExceptionTypes(params IEnumerable } } - public static IEnumerable GetDistictExceptionTypes(params IEnumerable exceptionAttributes) + public static IEnumerable GetDistinctExceptionTypes(params IEnumerable exceptionAttributes) { var exceptionTypes = GetExceptionTypes(exceptionAttributes); - return exceptionTypes.Distinct(SymbolEqualityComparer.Default) + return exceptionTypes + .Distinct(SymbolEqualityComparer.Default) .OfType(); } @@ -259,26 +256,23 @@ private void AnalyzeThrowStatement(SyntaxNodeAnalysisContext context) // Handle rethrows (throw;) if (throwStatement.Expression is null) { - if (IsWithinCatchBlock(throwStatement, out var catchClause)) + if (IsWithinCatchBlock(throwStatement, out var catchClause) && catchClause is not null) { - if (catchClause is not null) + if (catchClause.Declaration is null) { - if (catchClause.Declaration is null) - { - // General catch block with 'throw;' - // Analyze exceptions thrown in the try block - var tryStatement = catchClause.Ancestors().OfType().FirstOrDefault(); - if (tryStatement is not null) - { - AnalyzeExceptionsInTryBlock(context, tryStatement, catchClause, throwStatement, settings); - } - } - else + // General catch block with 'throw;' + // Analyze exceptions thrown in the try block + var tryStatement = catchClause.Ancestors().OfType().FirstOrDefault(); + if (tryStatement is not null) { - var exceptionType = context.SemanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; - AnalyzeExceptionThrowingNode(context, throwStatement, exceptionType, settings); + AnalyzeExceptionsInTryBlock(context, tryStatement, catchClause, throwStatement, settings); } } + else + { + var exceptionType = context.SemanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; + AnalyzeExceptionThrowingNode(context, throwStatement, exceptionType, settings); + } } return; // No further analysis for rethrows @@ -492,7 +486,7 @@ void CollectExpressionsForPropertySymbols(ExpressionSyntax expression) public bool ShouldIncludeException(INamedTypeSymbol exceptionType, SyntaxNode node, AnalyzerSettings settings) { - var exceptions = new HashSet(SymbolEqualityComparer.Default); + // var exceptions = new HashSet(SymbolEqualityComparer.Default); var exceptionName = exceptionType.ToDisplayString(); @@ -595,7 +589,7 @@ private void AnalyzeAwait(SyntaxNodeAnalysisContext context) /// /// Determines if a node is within a catch block. /// - private bool IsWithinCatchBlock(SyntaxNode node, out CatchClauseSyntax catchClause) + private bool IsWithinCatchBlock(SyntaxNode node, out CatchClauseSyntax? catchClause) { catchClause = node.Ancestors().OfType().FirstOrDefault(); return catchClause is not null; @@ -784,14 +778,9 @@ private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context) private void AnalyzeIdentifierAndMemberAccess(SyntaxNodeAnalysisContext context, ExpressionSyntax expression, AnalyzerSettings settings) { - var s = context.SemanticModel.GetSymbolInfo(expression).Symbol; - var symbol = s as IPropertySymbol; - if (symbol is null) - return; - - if (symbol is IPropertySymbol propertySymbol) + if (context.SemanticModel.GetSymbolInfo(expression).Symbol is IPropertySymbol propertySymbol) { - AnalyzePropertyExceptions(context, expression, symbol, settings); + AnalyzePropertyExceptions(context, expression, propertySymbol, settings); } } @@ -804,13 +793,9 @@ private void AnalyzeElementAccess(SyntaxNodeAnalysisContext context) var elementAccess = (ElementAccessExpressionSyntax)context.Node; - var symbol = context.SemanticModel.GetSymbolInfo(elementAccess).Symbol as IPropertySymbol; - if (symbol is null) - return; - - if (symbol is IPropertySymbol propertySymbol) + if (context.SemanticModel.GetSymbolInfo(elementAccess).Symbol is IPropertySymbol propertySymbol) { - AnalyzePropertyExceptions(context, elementAccess, symbol, settings); + AnalyzePropertyExceptions(context, elementAccess, propertySymbol, settings); } } @@ -873,28 +858,17 @@ private HashSet GetPropertyExceptionTypes(SyntaxNodeAnalysisCo var xmlDocumentedExceptions = GetExceptionTypesFromDocumentationCommentXml(context.Compilation, propertySymbol); // Filter exceptions documented specifically for the getter and setter - var getterExceptions = xmlDocumentedExceptions.Where(x => - x.Description.Contains(" get ") || - x.Description.Contains(" gets ") || - x.Description.Contains(" getting ") || - x.Description.Contains(" retrieved ")); - - var setterExceptions = xmlDocumentedExceptions.Where(x => - x.Description.Contains(" set ") || - x.Description.Contains(" sets ") || - x.Description.Contains(" setting ")); + var getterExceptions = xmlDocumentedExceptions.Where(IsGetterException); + var setterExceptions = xmlDocumentedExceptions.Where(IsSetterException); if (isSetter && propertySymbol.SetMethod is not null) { - // Will filter away + // Will filter away setterExceptions = ProcessNullable(context, expression, propertySymbol.SetMethod, setterExceptions); } // Handle exceptions that don't explicitly belong to getters or setters - var allOtherExceptions = xmlDocumentedExceptions - .Except(getterExceptions); - allOtherExceptions = allOtherExceptions - .Except(setterExceptions); + var allOtherExceptions = xmlDocumentedExceptions.Where(x => !IsGetterException(x) && !IsSetterException(x)); if (isSetter && propertySymbol.SetMethod is not null) { @@ -925,12 +899,23 @@ private HashSet GetPropertyExceptionTypes(SyntaxNodeAnalysisCo // Add other exceptions not specific to getters or setters exceptionTypes.AddRange(allOtherExceptions.Select(x => x.ExceptionType)); return exceptionTypes; + + static bool IsGetterException(ExceptionInfo ei) => + ei.Description.Contains(" get ") || + ei.Description.Contains(" gets ") || + ei.Description.Contains(" getting ") || + ei.Description.Contains(" retrieved "); + + static bool IsSetterException(ExceptionInfo ei) => + ei.Description.Contains(" set ") || + ei.Description.Contains(" sets ") || + ei.Description.Contains(" setting "); } /// /// Analyzes exceptions thrown by a method, constructor, or other member. /// - private void AnalyzeMemberExceptions(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol methodSymbol, + private void AnalyzeMemberExceptions(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol? methodSymbol, AnalyzerSettings settings) { if (methodSymbol is null) @@ -1027,7 +1012,7 @@ private static IEnumerable GetExceptionTypes(IMethodSymbol met // Get exceptions from Throws attributes var exceptionAttributes = GetThrowsAttributes(methodSymbol); - return GetDistictExceptionTypes(exceptionAttributes); + return GetDistinctExceptionTypes(exceptionAttributes); } private static IEnumerable GetThrowsAttributes(ISymbol symbol) @@ -1062,7 +1047,7 @@ private bool CatchClauseHandlesException(CatchClauseSyntax catchClause, Semantic /// private bool IsExceptionHandled(SyntaxNode node, INamedTypeSymbol exceptionType, SemanticModel semanticModel) { - SyntaxNode? prevNode = null; + // SyntaxNode? prevNode = null; var current = node.Parent; while (current is not null) @@ -1078,7 +1063,7 @@ private bool IsExceptionHandled(SyntaxNode node, INamedTypeSymbol exceptionType, if (current is TryStatementSyntax tryStatement) { // Prevents analysis within the first try-catch, - // when coming from either a catch clause or a finally clause. + // when coming from either a catch clause or a finally clause. // Skip if the node is within a catch or finally block of the current try statement bool isInCatchOrFinally = tryStatement.Catches.Any(c => c.Contains(node)) || @@ -1097,7 +1082,7 @@ private bool IsExceptionHandled(SyntaxNode node, INamedTypeSymbol exceptionType, } } - prevNode = current; + // prevNode = current; current = current.Parent; } @@ -1182,48 +1167,34 @@ private bool IsExceptionDeclaredInMethod(SyntaxNodeAnalysisContext context, Synt { foreach (var ancestor in node.Ancestors()) { - IMethodSymbol? methodSymbol = null; - - switch (ancestor) + IMethodSymbol? methodSymbol = ancestor switch { - case MethodDeclarationSyntax methodDeclaration: - methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDeclaration); - break; - case ConstructorDeclarationSyntax constructorDeclaration: - methodSymbol = context.SemanticModel.GetDeclaredSymbol(constructorDeclaration); - break; - case AccessorDeclarationSyntax accessorDeclaration: - methodSymbol = context.SemanticModel.GetDeclaredSymbol(accessorDeclaration); - break; - case LocalFunctionStatementSyntax localFunction: - methodSymbol = context.SemanticModel.GetDeclaredSymbol(localFunction); - break; - case AnonymousFunctionExpressionSyntax anonymousFunction: - methodSymbol = context.SemanticModel.GetSymbolInfo(anonymousFunction).Symbol as IMethodSymbol; - break; - default: - // Continue up to next node - continue; - } + MethodDeclarationSyntax methodDeclaration => context.SemanticModel.GetDeclaredSymbol(methodDeclaration), + ConstructorDeclarationSyntax constructorDeclaration => context.SemanticModel.GetDeclaredSymbol(constructorDeclaration), + AccessorDeclarationSyntax accessorDeclaration => context.SemanticModel.GetDeclaredSymbol(accessorDeclaration), + LocalFunctionStatementSyntax localFunction => context.SemanticModel.GetDeclaredSymbol(localFunction), + AnonymousFunctionExpressionSyntax anonymousFunction => context.SemanticModel.GetSymbolInfo(anonymousFunction).Symbol as IMethodSymbol, + _ => null, + }; - if (methodSymbol is not null) - { - if (IsExceptionDeclaredInSymbol(methodSymbol, exceptionType)) - return true; + if (methodSymbol is null) + continue; // Continue up to next node - if (ancestor is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax) - { - // Break because you are analyzing a local function or anonymous function (lambda) - // If you don't then it will got to the method, and it will affect analysis of this inline function. - break; - } + if (IsExceptionDeclaredInSymbol(methodSymbol, exceptionType)) + return true; + + if (ancestor is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax) + { + // Break because you are analyzing a local function or anonymous function (lambda) + // If you don't then it will got to the method, and it will affect analysis of this inline function. + break; } } return false; } - private bool IsExceptionDeclaredInSymbol(IMethodSymbol methodSymbol, INamedTypeSymbol exceptionType) + private bool IsExceptionDeclaredInSymbol(IMethodSymbol? methodSymbol, INamedTypeSymbol exceptionType) { if (methodSymbol is null) return false; diff --git a/CheckedExceptions/TypeSymbolExtensions.cs b/CheckedExceptions/TypeSymbolExtensions.cs index eca97af..985ae54 100644 --- a/CheckedExceptions/TypeSymbolExtensions.cs +++ b/CheckedExceptions/TypeSymbolExtensions.cs @@ -10,7 +10,7 @@ public static class TypeSymbolExtensions /// /// Determines if a type inherits from a base type. /// - public static bool InheritsFrom(this INamedTypeSymbol type, INamedTypeSymbol baseType) + public static bool InheritsFrom(this INamedTypeSymbol? type, INamedTypeSymbol? baseType) { if (type is null || baseType is null) return false; diff --git a/CheckedExceptions/XmlDocumentationHelper.cs b/CheckedExceptions/XmlDocumentationHelper.cs index 0462a38..7234d66 100644 --- a/CheckedExceptions/XmlDocumentationHelper.cs +++ b/CheckedExceptions/XmlDocumentationHelper.cs @@ -1,8 +1,5 @@ -using System.Text; using System.Xml.Linq; -using Microsoft.CodeAnalysis; - namespace Sundstrom.CheckedExceptions; public static class XmlDocumentationHelper @@ -16,7 +13,7 @@ public static Dictionary CreateMemberLookup(XDocument xmlDoc) var lookup = members .Where(m => m.Attribute("name") is not null) // Ensure the member has a 'name' attribute .ToDictionary( - m => m.Attribute("name").Value, // Key: the member's name attribute + m => m.Attribute("name")!.Value, // Key: the member's name attribute m => m // Value: the inner XML or text content ); From 93c89bff54e1c3895526e3da95736e3dcf86005d Mon Sep 17 00:00:00 2001 From: sibber5 Date: Fri, 11 Jul 2025 04:04:29 +0300 Subject: [PATCH 3/5] Reuse original line endings for cleaner diff should normalize line endings in the future, with .gitattributes --- .../CheckedExceptionsAnalyzer.XmlDocs.cs | 432 +-- .../CheckedExceptionsAnalyzer.cs | 2532 ++++++++--------- 2 files changed, 1485 insertions(+), 1479 deletions(-) diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs index 57f2a66..6f7f980 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs @@ -1,214 +1,220 @@ -using System.Collections.Concurrent; -using System.Xml.Linq; - -using Microsoft.CodeAnalysis; - -namespace Sundstrom.CheckedExceptions; - -partial class CheckedExceptionsAnalyzer -{ - // A thread-safe dictionary to cache XML documentation paths - private static readonly ConcurrentDictionary XmlDocPathsCache = new(); - private static readonly ConcurrentDictionary?> XmlDocPathsAndMembers = new(); - - private string? GetXmlDocumentationPath(Compilation compilation, IAssemblySymbol assemblySymbol) - { - var assemblyName = assemblySymbol.Name; - var assemblyPath = assemblySymbol.Locations.FirstOrDefault()?.SourceTree?.FilePath; - - if (string.IsNullOrEmpty(assemblyPath)) - { - // Fallback: Attempt to get the path from the MetadataReference - var metadataReference = compilation.References - .FirstOrDefault(r => compilation.GetAssemblyOrModuleSymbol(r)?.Name == assemblyName); - - if (metadataReference is not null) - { - if (metadataReference is PortableExecutableReference peReference) - { - assemblyPath = peReference.FilePath; - } - } - } - - // explicitly check instead of using string.IsNullOrEmpty because netstandard2.0 does not support NotNullWhenAttribute - if (assemblyPath is null || assemblyPath.Length == 0) - return null; - - // Check cache first - if (XmlDocPathsCache.TryGetValue(assemblyPath, out var cachedPath)) - { - return cachedPath; - } - - // Assume XML doc is in the same directory with the same base name - var xmlDocPath = Path.ChangeExtension(assemblyPath, ".xml"); - -#pragma warning disable RS1035 // Do not use APIs banned for analyzers - if (File.Exists(xmlDocPath)) - { - XmlDocPathsCache[assemblyPath] = xmlDocPath; - return xmlDocPath; - } -#pragma warning restore RS1035 // Do not use APIs banned for analyzers - - // Handle .NET Core / .NET 5+ SDK paths - // Attempt to locate XML docs in SDK installation directories - // This requires knowledge of the SDK paths, which can vary - // A heuristic approach is necessary - - // Example heuristic (may need adjustments based on environment) - var sdkXmlDocPath = Path.Combine( - Path.GetDirectoryName(assemblyPath) ?? string.Empty, - "..", "xml", - $"{assemblyName}.xml"); - - sdkXmlDocPath = Path.GetFullPath(sdkXmlDocPath); - -#pragma warning disable RS1035 // Do not use APIs banned for analyzers - if (File.Exists(sdkXmlDocPath)) - { - XmlDocPathsCache[assemblyPath] = sdkXmlDocPath; - return sdkXmlDocPath; - } -#pragma warning restore RS1035 // Do not use APIs banned for analyzers - - // XML documentation not found - XmlDocPathsCache[assemblyPath] = null; - return null; - } - - public record struct ParamInfo(string Name); - - public record struct ExceptionInfo(INamedTypeSymbol ExceptionType, string Description, IEnumerable Parameters); - - private static IEnumerable GetExceptionTypesFromDocumentationCommentXml(Compilation compilation, XElement xml) - { - try - { - return xml.Descendants("exception") - .Select(e => - { - string? cref = e.Attribute("cref")?.Value; - if (string.IsNullOrWhiteSpace(cref)) - { - return default; - } - - string exceptionTypeName = cref!.StartsWith("T:", StringComparison.Ordinal) ? cref.Substring(2) : cref; - string cleanExceptionTypeName = RemoveGenericParameters(exceptionTypeName); - - INamedTypeSymbol? typeSymbol = compilation.GetTypeByMetadataName(cleanExceptionTypeName); - if (typeSymbol is null && !cleanExceptionTypeName.Contains('.')) - { - typeSymbol = compilation.GetTypeByMetadataName($"System.{cleanExceptionTypeName}"); - } - - if (typeSymbol is null) - { - return default; - } - - string innerText = e.Value; - - IEnumerable parameters = e.Elements("paramref") - .Select(x => new ParamInfo(x.Attribute("name")?.Value!)) - .Where(p => !string.IsNullOrWhiteSpace(p.Name)); - - return new ExceptionInfo(typeSymbol, innerText, parameters); - }) - .Where(x => x != default) - .ToList(); // Materialize to catch parsing errors - } - catch - { - // Handle or log parsing errors - return Enumerable.Empty(); - } - - static string RemoveGenericParameters(string typeName) - { - // Handle generic types like "System.Collections.Generic.List`1" - var backtickIndex = typeName.IndexOf('`'); - if (backtickIndex >= 0) - { - return typeName.Substring(0, backtickIndex); - } - - // Handle generic syntax like "List" - var angleIndex = typeName.IndexOf('<'); - if (angleIndex >= 0) - { - return typeName.Substring(0, angleIndex); - } - - return typeName; - } - } - - /// - /// Retrieves exception types declared in XML documentation. - /// - private IEnumerable GetExceptionTypesFromDocumentationCommentXml(Compilation compilation, ISymbol symbol) - { - XElement? docCommentXml = GetDocumentationCommentXmlForSymbol(compilation, symbol); - - if (docCommentXml is null) - { - return Enumerable.Empty(); - } - - // Attempt to get exceptions from XML documentation - return GetExceptionTypesFromDocumentationCommentXml(compilation, docCommentXml); - } - - readonly bool loadFromProject = true; - - private XElement? GetDocumentationCommentXmlForSymbol(Compilation compilation, ISymbol symbol) - { - // Retrieve comment from project in solution that is being built - var docCommentXmlString = symbol.GetDocumentationCommentXml(); - - XElement? docCommentXml; - - if (!string.IsNullOrEmpty(docCommentXmlString) && loadFromProject) - { - try - { - docCommentXml = XElement.Parse(docCommentXmlString); - } - catch - { - // Badly formed XML - return null; - } - } - else - { - // Retrieve comment from referenced libraries (framework and DLLs in NuGet packages etc) - docCommentXml = GetXmlDocumentation(compilation, symbol); - } - - return docCommentXml; - } - - public XElement? GetXmlDocumentation(Compilation compilation, ISymbol symbol) - { - var path = GetXmlDocumentationPath(compilation, symbol.ContainingAssembly); - if (path is null) - { - return null; - } - - if (!XmlDocPathsAndMembers.TryGetValue(path, out var lookup) || lookup is null) - { - var file = XmlDocumentationHelper.CreateMemberLookup(XDocument.Load(path)); - lookup = new ConcurrentDictionary(file); - XmlDocPathsAndMembers[path] = lookup; - } - - var member = symbol.GetDocumentationCommentId(); - - return member is not null && lookup.TryGetValue(member, out var xml) ? xml : null; - } +using System.Collections.Concurrent; +using System.Xml.Linq; + +using Microsoft.CodeAnalysis; + +namespace Sundstrom.CheckedExceptions; + +partial class CheckedExceptionsAnalyzer +{ + // A thread-safe dictionary to cache XML documentation paths + private static readonly ConcurrentDictionary XmlDocPathsCache = new(); + private static readonly ConcurrentDictionary?> XmlDocPathsAndMembers = new(); + + private string? GetXmlDocumentationPath(Compilation compilation, IAssemblySymbol assemblySymbol) + { + var assemblyName = assemblySymbol.Name; + var assemblyPath = assemblySymbol.Locations.FirstOrDefault()?.SourceTree?.FilePath; + + if (string.IsNullOrEmpty(assemblyPath)) + { + // Fallback: Attempt to get the path from the MetadataReference + var metadataReference = compilation.References + .FirstOrDefault(r => compilation.GetAssemblyOrModuleSymbol(r)?.Name == assemblyName); + + if (metadataReference is not null) + { + if (metadataReference is PortableExecutableReference peReference) + { + assemblyPath = peReference.FilePath; + } + } + } + + // explicitly check instead of using string.IsNullOrEmpty because netstandard2.0 does not support NotNullWhenAttribute + if (assemblyPath is null || assemblyPath.Length == 0) + return null; + + // Check cache first + if (XmlDocPathsCache.TryGetValue(assemblyPath, out var cachedPath)) + { + return cachedPath; + } + + // Assume XML doc is in the same directory with the same base name + var xmlDocPath = Path.ChangeExtension(assemblyPath, ".xml"); + +#pragma warning disable RS1035 // Do not use APIs banned for analyzers + if (File.Exists(xmlDocPath)) + { + XmlDocPathsCache[assemblyPath] = xmlDocPath; + return xmlDocPath; + } +#pragma warning restore RS1035 // Do not use APIs banned for analyzers + + // Handle .NET Core / .NET 5+ SDK paths + // Attempt to locate XML docs in SDK installation directories + // This requires knowledge of the SDK paths, which can vary + // A heuristic approach is necessary + + // Example heuristic (may need adjustments based on environment) + var sdkXmlDocPath = Path.Combine( + Path.GetDirectoryName(assemblyPath) ?? string.Empty, + "..", "xml", + $"{assemblyName}.xml"); + + sdkXmlDocPath = Path.GetFullPath(sdkXmlDocPath); + +#pragma warning disable RS1035 // Do not use APIs banned for analyzers + if (File.Exists(sdkXmlDocPath)) + { + XmlDocPathsCache[assemblyPath] = sdkXmlDocPath; + return sdkXmlDocPath; + } +#pragma warning restore RS1035 // Do not use APIs banned for analyzers + + // XML documentation not found + XmlDocPathsCache[assemblyPath] = null; + return null; + } + + public record struct ParamInfo(string Name); + + public record struct ExceptionInfo(INamedTypeSymbol ExceptionType, string Description, IEnumerable Parameters); + + private static IEnumerable GetExceptionTypesFromDocumentationCommentXml(Compilation compilation, XElement xml) + { + try + { + return xml.Descendants("exception") + .Select(e => + { + string? cref = e.Attribute("cref")?.Value; + if (string.IsNullOrWhiteSpace(cref)) + { + return default; + } + + var exceptionTypeSymbol = GetExceptionTypeSymbolFromCref(cref!, compilation); + if (exceptionTypeSymbol is null) + { + return default; + } + + string innerText = e.Value; + + IEnumerable parameters = e.Elements("paramref") + .Select(x => new ParamInfo(x.Attribute("name")?.Value!)) + .Where(p => !string.IsNullOrWhiteSpace(p.Name)); + + return new ExceptionInfo(exceptionTypeSymbol, innerText, parameters); + }) + .Where(x => x != default) + .ToList(); // Materialize to catch parsing errors + } + catch + { + // Handle or log parsing errors + return Enumerable.Empty(); + } + + static INamedTypeSymbol? GetExceptionTypeSymbolFromCref(string cref, Compilation compilation) + { + string exceptionTypeName = cref.StartsWith("T:", StringComparison.Ordinal) ? cref.Substring(2) : cref; + string cleanExceptionTypeName = RemoveGenericParameters(exceptionTypeName); + + INamedTypeSymbol? typeSymbol = compilation.GetTypeByMetadataName(cleanExceptionTypeName); + if (typeSymbol is null && !cleanExceptionTypeName.Contains('.')) + { + typeSymbol = compilation.GetTypeByMetadataName($"System.{cleanExceptionTypeName}"); + } + + return typeSymbol; + } + + static string RemoveGenericParameters(string typeName) + { + // Handle generic types like "System.Collections.Generic.List`1" + var backtickIndex = typeName.IndexOf('`'); + if (backtickIndex >= 0) + { + return typeName.Substring(0, backtickIndex); + } + + // Handle generic syntax like "List" + var angleIndex = typeName.IndexOf('<'); + if (angleIndex >= 0) + { + return typeName.Substring(0, angleIndex); + } + + return typeName; + } + } + + /// + /// Retrieves exception types declared in XML documentation. + /// + private IEnumerable GetExceptionTypesFromDocumentationCommentXml(Compilation compilation, ISymbol symbol) + { + XElement? docCommentXml = GetDocumentationCommentXmlForSymbol(compilation, symbol); + + if (docCommentXml is null) + { + return Enumerable.Empty(); + } + + // Attempt to get exceptions from XML documentation + return GetExceptionTypesFromDocumentationCommentXml(compilation, docCommentXml); + } + + readonly bool loadFromProject = true; + + private XElement? GetDocumentationCommentXmlForSymbol(Compilation compilation, ISymbol symbol) + { + // Retrieve comment from project in solution that is being built + var docCommentXmlString = symbol.GetDocumentationCommentXml(); + + XElement? docCommentXml; + + if (!string.IsNullOrEmpty(docCommentXmlString) && loadFromProject) + { + try + { + docCommentXml = XElement.Parse(docCommentXmlString); + } + catch + { + // Badly formed XML + return null; + } + } + else + { + // Retrieve comment from referenced libraries (framework and DLLs in NuGet packages etc) + docCommentXml = GetXmlDocumentation(compilation, symbol); + } + + return docCommentXml; + } + + public XElement? GetXmlDocumentation(Compilation compilation, ISymbol symbol) + { + var path = GetXmlDocumentationPath(compilation, symbol.ContainingAssembly); + if (path is null) + { + return null; + } + + if (!XmlDocPathsAndMembers.TryGetValue(path, out var lookup) || lookup is null) + { + var file = XmlDocumentationHelper.CreateMemberLookup(XDocument.Load(path)); + lookup = new ConcurrentDictionary(file); + XmlDocPathsAndMembers[path] = lookup; + } + + var member = symbol.GetDocumentationCommentId(); + + return member is not null && lookup.TryGetValue(member, out var xml) ? xml : null; + } } \ No newline at end of file diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.cs index b20bda0..c4262ea 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.cs @@ -1,1267 +1,1267 @@ -namespace Sundstrom.CheckedExceptions; - -using System.Collections.Concurrent; -using System.Collections.Immutable; -using System.Linq; -using System.Text.Json; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public partial class CheckedExceptionsAnalyzer : DiagnosticAnalyzer -{ - static readonly ConcurrentDictionary configs = new ConcurrentDictionary(); - - // Diagnostic IDs - public const string DiagnosticIdUnhandled = "THROW001"; - public const string DiagnosticIdIgnoredException = "THROW002"; - public const string DiagnosticIdGeneralThrows = "THROW003"; - public const string DiagnosticIdGeneralThrow = "THROW004"; - public const string DiagnosticIdDuplicateDeclarations = "THROW005"; - public const string DiagnosticIdMissingThrowsOnBaseMember = "THROW006"; - public const string DiagnosticIdMissingThrowsFromBaseMember = "THROW007"; - - public static readonly IEnumerable AllDiagnosticsIds = [DiagnosticIdUnhandled, DiagnosticIdGeneralThrows, DiagnosticIdGeneralThrow, DiagnosticIdDuplicateDeclarations]; - - private static readonly DiagnosticDescriptor RuleUnhandledException = new( - DiagnosticIdUnhandled, - "Unhandled exception", - "Exception '{0}' {1} thrown but not handled", - "Usage", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Reports exceptions that are thrown but not caught or declared with [Throws], potentially violating exception safety."); - - private static readonly DiagnosticDescriptor RuleIgnoredException = new DiagnosticDescriptor( - DiagnosticIdIgnoredException, - "Ignored exception may cause runtime issues", - "Exception '{0}' is ignored by configuration but may cause runtime issues if unhandled", - "Usage", - DiagnosticSeverity.Info, - isEnabledByDefault: true, - description: "Informs about exceptions excluded from analysis but which may still propagate at runtime if not properly handled."); - - private static readonly DiagnosticDescriptor RuleGeneralThrow = new( - DiagnosticIdGeneralThrow, - "Avoid throwing 'Exception'", - "Throwing 'Exception' is too general; use a more specific exception type instead", - "Usage", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Discourages throwing the base System.Exception type directly, encouraging clearer and more actionable error semantics."); - - private static readonly DiagnosticDescriptor RuleGeneralThrows = new DiagnosticDescriptor( - DiagnosticIdGeneralThrows, - "Avoid declaring exception type 'Exception'", - "Declaring 'Exception' is too general; use a more specific exception type instead", - "Usage", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Discourages the use of System.Exception in [Throws] attributes. Prefer declaring more specific exception types."); - - private static readonly DiagnosticDescriptor RuleDuplicateDeclarations = new DiagnosticDescriptor( - DiagnosticIdDuplicateDeclarations, - "Avoid duplicate declarations of the same exception type", - "Duplicate declarations of the exception type '{0}' found. Remove them to avoid redundancy.", - "Usage", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Detects multiple [Throws] declarations for the same exception type on a single member, which is redundant."); - - private static readonly DiagnosticDescriptor RuleMissingThrowsOnBaseMember = new DiagnosticDescriptor( - DiagnosticIdMissingThrowsOnBaseMember, - "Missing Throws declaration", - "Exception '{1}' is not declared on base member '{0}'", - "Usage", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Base or interface members should declare compatible exceptions when overridden or implemented members declare them using [Throws]."); - - private static readonly DiagnosticDescriptor RuleMissingThrowsFromBaseMember = new( - DiagnosticIdMissingThrowsFromBaseMember, - "Missing Throws declaration for exception declared on base member", - "Base member '{0}' declares exception '{1}' which is not declared here", - "Usage", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Ensures that overridden or implemented members declare exceptions required by their base or interface definitions."); - - public override ImmutableArray SupportedDiagnostics => - [RuleUnhandledException, RuleIgnoredException, RuleGeneralThrows, RuleGeneralThrow, RuleDuplicateDeclarations, RuleMissingThrowsOnBaseMember, RuleMissingThrowsFromBaseMember]; - - private const string SettingsFileName = "CheckedExceptions.settings.json"; - private static readonly JsonSerializerOptions _settingsFileJsonOptions = new() - { - PropertyNameCaseInsensitive = true, - ReadCommentHandling = JsonCommentHandling.Skip, - AllowTrailingCommas = true, - }; - - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - - // Register actions for throw statements and expressions - context.RegisterSyntaxNodeAction(AnalyzeThrowStatement, SyntaxKind.ThrowStatement); - context.RegisterSyntaxNodeAction(AnalyzeThrowExpression, SyntaxKind.ThrowExpression); - - context.RegisterSymbolAction(AnalyzeMethodSymbol, SymbolKind.Method); - - context.RegisterSyntaxNodeAction(AnalyzeLambdaExpression, SyntaxKind.SimpleLambdaExpression); - context.RegisterSyntaxNodeAction(AnalyzeLambdaExpression, SyntaxKind.ParenthesizedLambdaExpression); - context.RegisterSyntaxNodeAction(AnalyzeLocalFunctionStatement, SyntaxKind.LocalFunctionStatement); - - // Register additional actions for method calls, object creations, etc. - context.RegisterSyntaxNodeAction(AnalyzeMethodCall, SyntaxKind.InvocationExpression); - context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression); - context.RegisterSyntaxNodeAction(AnalyzeImplicitObjectCreation, SyntaxKind.ImplicitObjectCreationExpression); - context.RegisterSyntaxNodeAction(AnalyzeIdentifier, SyntaxKind.IdentifierName); - context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); - context.RegisterSyntaxNodeAction(AnalyzeAwait, SyntaxKind.AwaitExpression); - context.RegisterSyntaxNodeAction(AnalyzeElementAccess, SyntaxKind.ElementAccessExpression); - context.RegisterSyntaxNodeAction(AnalyzeEventAssignment, SyntaxKind.AddAssignmentExpression); - context.RegisterSyntaxNodeAction(AnalyzeEventAssignment, SyntaxKind.SubtractAssignmentExpression); - } - - private AnalyzerSettings LoadAnalyzerSettings(AnalyzerOptions analyzerOptions) - { - return configs.GetOrAdd(analyzerOptions, _ => - { - var configFileText = analyzerOptions.AdditionalFiles - .FirstOrDefault(f => SettingsFileName.Equals(Path.GetFileName(f.Path), StringComparison.OrdinalIgnoreCase)) - ?.GetText()?.ToString(); - - AnalyzerSettings? val = null; - - if (configFileText is not null) - { - val = JsonSerializer.Deserialize(configFileText, _settingsFileJsonOptions); - } - - return val ?? AnalyzerSettings.CreateWithDefaults(); // Return default options if the config file is not found - }); - } - - private void AnalyzeLambdaExpression(SyntaxNodeAnalysisContext context) - { - var lambdaExpression = (LambdaExpressionSyntax)context.Node; - AnalyzeFunctionAttributes(lambdaExpression, lambdaExpression.AttributeLists.SelectMany(a => a.Attributes), context.SemanticModel, context); - } - - private void AnalyzeLocalFunctionStatement(SyntaxNodeAnalysisContext context) - { - var localFunction = (LocalFunctionStatementSyntax)context.Node; - AnalyzeFunctionAttributes(localFunction, localFunction.AttributeLists.SelectMany(a => a.Attributes), context.SemanticModel, context); - } - - private void AnalyzeFunctionAttributes(SyntaxNode node, IEnumerable attributes, SemanticModel semanticModel, SyntaxNodeAnalysisContext context) - { - var throwsAttributes = attributes - .Where(attr => IsThrowsAttribute(attr, semanticModel)) - .ToList(); - - CheckForGeneralExceptionThrows(throwsAttributes, context); - CheckForDuplicateThrowsAttributes(throwsAttributes, context); - } - - /// - /// Determines whether the given attribute is a ThrowsAttribute. - /// - private bool IsThrowsAttribute(AttributeSyntax attributeSyntax, SemanticModel semanticModel) - { - var attributeSymbol = semanticModel.GetSymbolInfo(attributeSyntax).Symbol as IMethodSymbol; - if (attributeSymbol is null) - return false; - - var attributeType = attributeSymbol.ContainingType; - return attributeType.Name is "ThrowsAttribute"; - } - - private void AnalyzeMethodSymbol(SymbolAnalysisContext context) - { - var methodSymbol = (IMethodSymbol)context.Symbol; - - var throwsAttributes = GetThrowsAttributes(methodSymbol).ToImmutableArray(); - - CheckForCompatibilityWithBaseOrInterface(context, throwsAttributes); - - if (throwsAttributes.Length == 0) - return; - - CheckForGeneralExceptionThrows(throwsAttributes, context); - CheckForDuplicateThrowsAttributes(context, throwsAttributes); - } - - private static IEnumerable FilterThrowsAttributesByException(ImmutableArray exceptionAttributes, string exceptionTypeName) - { - return exceptionAttributes - .Where(attribute => IsThrowsAttributeForException(attribute, exceptionTypeName)); - } - - public static bool IsThrowsAttributeForException(AttributeData attribute, string exceptionTypeName) - { - if (!attribute.ConstructorArguments.Any()) - return false; - - var exceptionTypes = GetDistinctExceptionTypes(attribute); - return exceptionTypes.Any(exceptionType => exceptionType?.Name == exceptionTypeName); - } - - public static IEnumerable GetExceptionTypes(params IEnumerable exceptionAttributes) - { - var constructorArguments = exceptionAttributes - .SelectMany(attr => attr.ConstructorArguments); - - foreach (var arg in constructorArguments) - { - if (arg.Kind is TypedConstantKind.Array) - { - foreach (var t in arg.Values) - { - if (t.Kind is TypedConstantKind.Type) - { - yield return (INamedTypeSymbol)t.Value!; - } - } - } - else if (arg.Kind is TypedConstantKind.Type) - { - yield return (INamedTypeSymbol)arg.Value!; - } - } - } - - public static IEnumerable GetDistinctExceptionTypes(params IEnumerable exceptionAttributes) - { - var exceptionTypes = GetExceptionTypes(exceptionAttributes); - - return exceptionTypes - .Distinct(SymbolEqualityComparer.Default) - .OfType(); - } - - /// - /// Analyzes throw statements to determine if exceptions are handled or declared. - /// - private void AnalyzeThrowStatement(SyntaxNodeAnalysisContext context) - { - var settings = LoadAnalyzerSettings(context.Options); - - var throwStatement = (ThrowStatementSyntax)context.Node; - - // Handle rethrows (throw;) - if (throwStatement.Expression is null) - { - if (IsWithinCatchBlock(throwStatement, out var catchClause) && catchClause is not null) - { - if (catchClause.Declaration is null) - { - // General catch block with 'throw;' - // Analyze exceptions thrown in the try block - var tryStatement = catchClause.Ancestors().OfType().FirstOrDefault(); - if (tryStatement is not null) - { - AnalyzeExceptionsInTryBlock(context, tryStatement, catchClause, throwStatement, settings); - } - } - else - { - var exceptionType = context.SemanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; - AnalyzeExceptionThrowingNode(context, throwStatement, exceptionType, settings); - } - } - - return; // No further analysis for rethrows - } - - // Handle throw new ExceptionType() - if (throwStatement.Expression is ObjectCreationExpressionSyntax creationExpression) - { - var exceptionType = context.SemanticModel.GetTypeInfo(creationExpression).Type as INamedTypeSymbol; - AnalyzeExceptionThrowingNode(context, throwStatement, exceptionType, settings); - } - } - - private void AnalyzeExceptionsInTryBlock(SyntaxNodeAnalysisContext context, TryStatementSyntax tryStatement, CatchClauseSyntax generalCatchClause, ThrowStatementSyntax throwStatement, AnalyzerSettings settings) - { - var semanticModel = context.SemanticModel; - - // Collect exceptions that can be thrown in the try block - var thrownExceptions = CollectUnhandledExceptions(context, tryStatement.Block, settings); - - // Collect exception types handled by preceding catch clauses - var handledExceptions = new HashSet(SymbolEqualityComparer.Default); - foreach (var catchClause in tryStatement.Catches) - { - if (catchClause == generalCatchClause) - break; // Stop at the general catch clause - - if (catchClause.Declaration is not null) - { - var catchType = semanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; - if (catchType is not null) - { - handledExceptions.Add(catchType); - } - } - else - { - // General catch clause before our general catch; handles all exceptions - handledExceptions = null; - break; - } - } - - if (handledExceptions is null) - { - // All exceptions are handled by a previous general catch - return; - } - - // For each thrown exception, check if it is handled - foreach (var exceptionType in thrownExceptions.Distinct(SymbolEqualityComparer.Default).OfType()) - { - var exceptionName = exceptionType.ToDisplayString(); - - if (settings.IgnoredExceptions.Contains(exceptionName)) - { - // Completely ignore this exception - continue; - } - else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) - { - if (ShouldIgnore(throwStatement, mode)) - { - // Report as THROW002 (Info level) - var diagnostic = Diagnostic.Create(RuleIgnoredException, GetSignificantLocation(throwStatement), exceptionType.Name); - context.ReportDiagnostic(diagnostic); - continue; - } - } - - bool isHandled = handledExceptions.Any(handledException => - exceptionType.Equals(handledException, SymbolEqualityComparer.Default) || - exceptionType.InheritsFrom(handledException)); - - bool isDeclared = IsExceptionDeclaredInMethod(context, tryStatement, exceptionType); - - if (!isHandled && !isDeclared) - { - // Report diagnostic for unhandled exception - var diagnostic = Diagnostic.Create( - RuleUnhandledException, - GetSignificantLocation(throwStatement), - exceptionType.Name, - THROW001Verbs.MightBe); - - context.ReportDiagnostic(diagnostic); - } - } - } - - private HashSet CollectUnhandledExceptions(SyntaxNodeAnalysisContext context, BlockSyntax block, AnalyzerSettings settings) - { - var unhandledExceptions = new HashSet(SymbolEqualityComparer.Default); - - foreach (var statement in block.Statements) - { - if (statement is TryStatementSyntax tryStatement) - { - // Recursively collect exceptions from the inner try block - var innerUnhandledExceptions = CollectUnhandledExceptions(context, tryStatement.Block, settings); - - // Remove exceptions that are caught by the inner catch clauses - var caughtExceptions = GetCaughtExceptions(tryStatement.Catches, context.SemanticModel); - innerUnhandledExceptions.RemoveWhere(exceptionType => IsExceptionCaught(exceptionType, caughtExceptions)); - - // Add any exceptions that are not handled in the inner try block - unhandledExceptions.UnionWith(innerUnhandledExceptions); - } - else - { - // Collect exceptions thrown in this statement - var statementExceptions = CollectExceptionsFromStatement(context, statement, settings); - - // Add them to the unhandled exceptions - unhandledExceptions.UnionWith(statementExceptions); - } - } - - return unhandledExceptions; - } - - private HashSet CollectExceptionsFromStatement(SyntaxNodeAnalysisContext context, StatementSyntax statement, AnalyzerSettings settings) - { - SemanticModel semanticModel = context.SemanticModel; - - var exceptions = new HashSet(SymbolEqualityComparer.Default); - - foreach (var s in statement.DescendantNodesAndSelf()) - { - switch (s) - { - // Collect exceptions from throw statements - case ThrowStatementSyntax throwStatement: - CollectExpressionsFromThrows(throwStatement, throwStatement.Expression); - break; - - // Collect exceptions from throw expressions - case ThrowExpressionSyntax throwExpression: - CollectExpressionsFromThrows(throwExpression, throwExpression.Expression); - break; - - // Collect exceptions from method calls and object creations - case InvocationExpressionSyntax: - case ObjectCreationExpressionSyntax: - CollectExpressionsForMethodSymbols((ExpressionSyntax)s); - break; - - // Collect exceptions from property accessors and identifiers - case MemberAccessExpressionSyntax: - case ElementAccessExpressionSyntax: - case IdentifierNameSyntax: - CollectExpressionsForPropertySymbols((ExpressionSyntax)s); - break; - } - } - - return exceptions; - - void CollectExpressionsFromThrows(SyntaxNode throwExpression, ExpressionSyntax? subExpression) - { - if (subExpression is null) return; - - if (semanticModel.GetTypeInfo(subExpression).Type is not INamedTypeSymbol exceptionType) return; - - if (ShouldIncludeException(exceptionType, throwExpression, settings)) - { - exceptions.Add(exceptionType); - } - } - - void CollectExpressionsForMethodSymbols(ExpressionSyntax expression) - { - if (semanticModel.GetSymbolInfo(expression).Symbol is not IMethodSymbol methodSymbol) return; - - var exceptionTypes = GetExceptionTypes(methodSymbol); - - // Get exceptions from XML documentation - var xmlExceptionTypes = GetExceptionTypesFromDocumentationCommentXml(semanticModel.Compilation, methodSymbol); - - xmlExceptionTypes = ProcessNullable(context, expression, methodSymbol, xmlExceptionTypes); - - if (xmlExceptionTypes.Any()) - { - exceptionTypes = exceptionTypes.Concat(xmlExceptionTypes.Select(x => x.ExceptionType)); - } - - foreach (var exceptionType in exceptionTypes) - { - if (ShouldIncludeException(exceptionType, expression, settings)) - { - exceptions.Add(exceptionType); - } - } - } - - void CollectExpressionsForPropertySymbols(ExpressionSyntax expression) - { - if (semanticModel.GetSymbolInfo(expression).Symbol is not IPropertySymbol propertySymbol) return; - - HashSet exceptionTypes = GetPropertyExceptionTypes(context, expression, propertySymbol); - - foreach (var exceptionType in exceptionTypes) - { - if (ShouldIncludeException(exceptionType, expression, settings)) - { - exceptions.Add(exceptionType); - } - } - } - } - - public bool ShouldIncludeException(INamedTypeSymbol exceptionType, SyntaxNode node, AnalyzerSettings settings) - { - // var exceptions = new HashSet(SymbolEqualityComparer.Default); - - var exceptionName = exceptionType.ToDisplayString(); - - if (settings.IgnoredExceptions.Contains(exceptionName)) - { - // Completely ignore this exception - return false; - } - else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) - { - if (ShouldIgnore(node, mode)) - { - return false; - } - } - - return true; - } - - private HashSet? GetCaughtExceptions(SyntaxList catchClauses, SemanticModel semanticModel) - { - var caughtExceptions = new HashSet(SymbolEqualityComparer.Default); - - foreach (var catchClause in catchClauses) - { - if (catchClause.Declaration is not null) - { - var catchType = semanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; - if (catchType is not null) - { - caughtExceptions.Add(catchType); - } - } - else - { - // General catch clause catches all exceptions - caughtExceptions = null; - break; - } - } - - return caughtExceptions; - } - - private bool IsExceptionCaught(INamedTypeSymbol exceptionType, HashSet? caughtExceptions) - { - if (caughtExceptions is null) - { - // General catch clause catches all exceptions - return true; - } - - return caughtExceptions.Any(catchType => - exceptionType.Equals(catchType, SymbolEqualityComparer.Default) || - exceptionType.InheritsFrom(catchType)); - } - - private void AnalyzeAwait(SyntaxNodeAnalysisContext context) - { - var settings = LoadAnalyzerSettings(context.Options); - - var awaitExpression = (AwaitExpressionSyntax)context.Node; - - if (awaitExpression.Expression is InvocationExpressionSyntax invocation) - { - // Get the invoked symbol - var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation); - var methodSymbol = symbolInfo.Symbol as IMethodSymbol; - - if (methodSymbol is null) - return; - - // Handle delegate invokes by getting the target method symbol - if (methodSymbol.MethodKind == MethodKind.DelegateInvoke) - { - var targetMethodSymbol = GetTargetMethodSymbol(context, invocation); - if (targetMethodSymbol is not null) - { - methodSymbol = targetMethodSymbol; - } - else - { - // Could not find the target method symbol - return; - } - } - - AnalyzeMemberExceptions(context, invocation, methodSymbol, settings); - } - else if (awaitExpression.Expression is MemberAccessExpressionSyntax memberAccess) - { - AnalyzeIdentifierAndMemberAccess(context, memberAccess, settings); - } - else if (awaitExpression.Expression is IdentifierNameSyntax identifier) - { - AnalyzeIdentifierAndMemberAccess(context, identifier, settings); - } - } - - /// - /// Determines if a node is within a catch block. - /// - private bool IsWithinCatchBlock(SyntaxNode node, out CatchClauseSyntax? catchClause) - { - catchClause = node.Ancestors().OfType().FirstOrDefault(); - return catchClause is not null; - } - - /// - /// Analyzes throw expressions to determine if exceptions are handled or declared. - /// - private void AnalyzeThrowExpression(SyntaxNodeAnalysisContext context) - { - var settings = LoadAnalyzerSettings(context.Options); - - var throwExpression = (ThrowExpressionSyntax)context.Node; - - if (throwExpression.Expression is ObjectCreationExpressionSyntax creationExpression) - { - var exceptionType = context.SemanticModel.GetTypeInfo(creationExpression).Type as INamedTypeSymbol; - AnalyzeExceptionThrowingNode(context, throwExpression, exceptionType, settings); - } - } - - /// - /// Analyzes method calls to determine if exceptions are handled or declared. - /// - private void AnalyzeMethodCall(SyntaxNodeAnalysisContext context) - { - var settings = LoadAnalyzerSettings(context.Options); - - var invocation = (InvocationExpressionSyntax)context.Node; - - if (invocation.Parent is AwaitExpressionSyntax) - { - // Handled in other method. - return; - } - - // Get the invoked symbol - var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation); - var methodSymbol = symbolInfo.Symbol as IMethodSymbol; - - if (methodSymbol is null) - return; - - // Handle delegate invokes by getting the target method symbol - if (methodSymbol.MethodKind == MethodKind.DelegateInvoke) - { - var targetMethodSymbol = GetTargetMethodSymbol(context, invocation); - if (targetMethodSymbol is not null) - { - methodSymbol = targetMethodSymbol; - } - else - { - // Could not find the target method symbol - return; - } - } - - AnalyzeMemberExceptions(context, invocation, methodSymbol, settings); - } - - /// - /// Resolves the target method symbol from a delegate, lambda, or method group. - /// - private IMethodSymbol? GetTargetMethodSymbol(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation) - { - var expression = invocation.Expression; - - // Get the symbol of the expression being invoked - var symbolInfo = context.SemanticModel.GetSymbolInfo(expression); - var symbol = symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.FirstOrDefault(); - - if (symbol is null) - return null; - - if (symbol is ILocalSymbol localSymbol) - { - // Get the syntax node where the local variable is declared - var declaringSyntaxReference = localSymbol.DeclaringSyntaxReferences.FirstOrDefault(); - if (declaringSyntaxReference is not null) - { - var syntaxNode = declaringSyntaxReference.GetSyntax(); - - if (syntaxNode is VariableDeclaratorSyntax variableDeclarator) - { - var initializer = variableDeclarator.Initializer?.Value; - - if (initializer is not null) - { - // Handle lambdas - if (initializer is AnonymousFunctionExpressionSyntax anonymousFunction) - { - var lambdaSymbol = context.SemanticModel.GetSymbolInfo(anonymousFunction).Symbol as IMethodSymbol; - if (lambdaSymbol is not null) - return lambdaSymbol; - } - - // Handle method groups - if (initializer is IdentifierNameSyntax || initializer is MemberAccessExpressionSyntax) - { - var methodGroupSymbol = context.SemanticModel.GetSymbolInfo(initializer).Symbol as IMethodSymbol; - if (methodGroupSymbol is not null) - return methodGroupSymbol; - } - - // Get the method symbol of the initializer (lambda or method group) - var initializerSymbolInfo = context.SemanticModel.GetSymbolInfo(initializer); - var initializerSymbol = initializerSymbolInfo.Symbol ?? initializerSymbolInfo.CandidateSymbols.FirstOrDefault(); - - if (initializerSymbol is IMethodSymbol targetMethodSymbol) - { - return targetMethodSymbol; - } - } - } - } - } - - return null; - } - - /// - /// Analyzes object creation expressions to determine if exceptions are handled or declared. - /// - private void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context) - { - var settings = LoadAnalyzerSettings(context.Options); - - var objectCreation = (ObjectCreationExpressionSyntax)context.Node; - - var constructorSymbol = context.SemanticModel.GetSymbolInfo(objectCreation).Symbol as IMethodSymbol; - if (constructorSymbol is null) - return; - - AnalyzeMemberExceptions(context, objectCreation, constructorSymbol, settings); - } - - - /// - /// Analyzes implicit object creation expressions to determine if exceptions are handled or declared. - /// - private void AnalyzeImplicitObjectCreation(SyntaxNodeAnalysisContext context) - { - var settings = LoadAnalyzerSettings(context.Options); - - var objectCreation = (ImplicitObjectCreationExpressionSyntax)context.Node; - - var constructorSymbol = context.SemanticModel.GetSymbolInfo(objectCreation).Symbol as IMethodSymbol; - if (constructorSymbol is null) - return; - - AnalyzeMemberExceptions(context, objectCreation, constructorSymbol, settings); - } - - /// - /// Analyzes member access expressions (e.g., property accessors) for exception handling. - /// - private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) - { - var settings = LoadAnalyzerSettings(context.Options); - - var memberAccess = (MemberAccessExpressionSyntax)context.Node; - - AnalyzeIdentifierAndMemberAccess(context, memberAccess, settings); - } - - /// - /// Analyzes identifier names (e.g. local variables or property accessors in context) for exception handling. - /// - private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context) - { - var settings = LoadAnalyzerSettings(context.Options); - - var identifierName = (IdentifierNameSyntax)context.Node; - - // Ignore identifiers that are part of await expression - if (identifierName.Parent is AwaitExpressionSyntax) - return; - - // Ignore identifiers that are part of member access - if (identifierName.Parent is MemberAccessExpressionSyntax) - return; - - AnalyzeIdentifierAndMemberAccess(context, identifierName, settings); - } - - private void AnalyzeIdentifierAndMemberAccess(SyntaxNodeAnalysisContext context, ExpressionSyntax expression, AnalyzerSettings settings) - { - if (context.SemanticModel.GetSymbolInfo(expression).Symbol is IPropertySymbol propertySymbol) - { - AnalyzePropertyExceptions(context, expression, propertySymbol, settings); - } - } - - /// - /// Analyzes element access expressions (e.g., indexers) for exception handling. - /// - private void AnalyzeElementAccess(SyntaxNodeAnalysisContext context) - { - var settings = LoadAnalyzerSettings(context.Options); - - var elementAccess = (ElementAccessExpressionSyntax)context.Node; - - if (context.SemanticModel.GetSymbolInfo(elementAccess).Symbol is IPropertySymbol propertySymbol) - { - AnalyzePropertyExceptions(context, elementAccess, propertySymbol, settings); - } - } - - /// - /// Analyzes event assignments (e.g., += or -=) for exception handling. - /// - private void AnalyzeEventAssignment(SyntaxNodeAnalysisContext context) - { - var settings = LoadAnalyzerSettings(context.Options); - - var assignment = (AssignmentExpressionSyntax)context.Node; - - var eventSymbol = context.SemanticModel.GetSymbolInfo(assignment.Left).Symbol as IEventSymbol; - if (eventSymbol is null) - return; - - // Get the method symbol for the add or remove accessor - IMethodSymbol? methodSymbol = null; - - if (assignment.IsKind(SyntaxKind.AddAssignmentExpression) && eventSymbol.AddMethod is not null) - { - methodSymbol = eventSymbol.AddMethod; - } - else if (assignment.IsKind(SyntaxKind.SubtractAssignmentExpression) && eventSymbol.RemoveMethod is not null) - { - methodSymbol = eventSymbol.RemoveMethod; - } - - if (methodSymbol is not null) - { - AnalyzeMemberExceptions(context, assignment, methodSymbol, settings); - } - } - - /// - /// Analyzes exceptions thrown by a property, specifically its getters and setters. - /// - private void AnalyzePropertyExceptions(SyntaxNodeAnalysisContext context, ExpressionSyntax expression, IPropertySymbol propertySymbol, - AnalyzerSettings settings) - { - HashSet exceptionTypes = GetPropertyExceptionTypes(context, expression, propertySymbol); - - // Deduplicate and analyze each distinct exception type - foreach (var exceptionType in exceptionTypes.Distinct(SymbolEqualityComparer.Default).OfType()) - { - AnalyzeExceptionThrowingNode(context, expression, exceptionType, settings); - } - } - - private HashSet GetPropertyExceptionTypes(SyntaxNodeAnalysisContext context, ExpressionSyntax expression, IPropertySymbol propertySymbol) - { - // Determine if the analyzed expression is for a getter or setter - bool isGetter = IsPropertyGetter(expression); - bool isSetter = IsPropertySetter(expression); - - // List to collect all relevant exception types - var exceptionTypes = new HashSet(SymbolEqualityComparer.Default); - - // Retrieve exception types documented in XML comments for the property - var xmlDocumentedExceptions = GetExceptionTypesFromDocumentationCommentXml(context.Compilation, propertySymbol); - - // Filter exceptions documented specifically for the getter and setter - var getterExceptions = xmlDocumentedExceptions.Where(IsGetterException); - var setterExceptions = xmlDocumentedExceptions.Where(IsSetterException); - - if (isSetter && propertySymbol.SetMethod is not null) - { - // Will filter away - setterExceptions = ProcessNullable(context, expression, propertySymbol.SetMethod, setterExceptions); - } - - // Handle exceptions that don't explicitly belong to getters or setters - var allOtherExceptions = xmlDocumentedExceptions.Where(x => !IsGetterException(x) && !IsSetterException(x)); - - if (isSetter && propertySymbol.SetMethod is not null) - { - allOtherExceptions = ProcessNullable(context, expression, propertySymbol.SetMethod, allOtherExceptions); - } - - // Analyze exceptions thrown by the getter if applicable - if (isGetter && propertySymbol.GetMethod is not null) - { - var getterMethodExceptions = GetExceptionTypes(propertySymbol.GetMethod); - exceptionTypes.AddRange(getterExceptions.Select(x => x.ExceptionType)); - exceptionTypes.AddRange(getterMethodExceptions); - } - - // Analyze exceptions thrown by the setter if applicable - if (isSetter && propertySymbol.SetMethod is not null) - { - var setterMethodExceptions = GetExceptionTypes(propertySymbol.SetMethod); - exceptionTypes.AddRange(setterExceptions.Select(x => x.ExceptionType)); - exceptionTypes.AddRange(setterMethodExceptions); - } - - if (propertySymbol.GetMethod is not null) - { - allOtherExceptions = ProcessNullable(context, expression, propertySymbol.GetMethod, allOtherExceptions); - } - - // Add other exceptions not specific to getters or setters - exceptionTypes.AddRange(allOtherExceptions.Select(x => x.ExceptionType)); - return exceptionTypes; - - static bool IsGetterException(ExceptionInfo ei) => - ei.Description.Contains(" get ") || - ei.Description.Contains(" gets ") || - ei.Description.Contains(" getting ") || - ei.Description.Contains(" retrieved "); - - static bool IsSetterException(ExceptionInfo ei) => - ei.Description.Contains(" set ") || - ei.Description.Contains(" sets ") || - ei.Description.Contains(" setting "); - } - - /// - /// Analyzes exceptions thrown by a method, constructor, or other member. - /// - private void AnalyzeMemberExceptions(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol? methodSymbol, - AnalyzerSettings settings) - { - if (methodSymbol is null) - return; - - IEnumerable exceptionTypes = GetExceptionTypes(methodSymbol); - - // Get exceptions from XML documentation - var xmlExceptionTypes = GetExceptionTypesFromDocumentationCommentXml(context.Compilation, methodSymbol); - - xmlExceptionTypes = ProcessNullable(context, node, methodSymbol, xmlExceptionTypes); - - if (xmlExceptionTypes.Any()) - { - exceptionTypes = exceptionTypes.Concat(xmlExceptionTypes.Select(x => x.ExceptionType)); - } - - exceptionTypes = ProcessNullable(context, node, methodSymbol, exceptionTypes) - .Distinct(SymbolEqualityComparer.Default) - .OfType(); - - foreach (var exceptionType in exceptionTypes) - { - AnalyzeExceptionThrowingNode(context, node, exceptionType, settings); - } - } - - private static IEnumerable ProcessNullable(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol methodSymbol, IEnumerable exceptionInfos) - { - var argumentNullExceptionTypeSymbol = context.Compilation.GetTypeByMetadataName("System.ArgumentNullException"); - - var isCompilationNullableEnabled = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Enable; - - var nullableContext = context.SemanticModel.GetNullableContext(node.SpanStart); - var isNodeInNullableContext = nullableContext is NullableContext.Enabled; - - if (isNodeInNullableContext || isCompilationNullableEnabled) - { - if (methodSymbol.IsExtensionMethod) - { - return exceptionInfos.Where(x => !x.ExceptionType.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default)); - } - - if (methodSymbol.Parameters.Count() is 1) - { - var p = methodSymbol.Parameters.First(); - - if (p.NullableAnnotation is NullableAnnotation.NotAnnotated) - { - return exceptionInfos.Where(x => !x.ExceptionType.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default)); - } - } - else - { - exceptionInfos = exceptionInfos.Where(x => - { - var p = methodSymbol.Parameters.FirstOrDefault(p => x.Parameters.Any(p2 => p.Name == p2.Name)); - if (p is not null) - { - if (x.ExceptionType.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default) - && p.NullableAnnotation is NullableAnnotation.NotAnnotated) - { - return false; - } - } - - return true; - }); - } - } - - return exceptionInfos; - } - - private static IEnumerable ProcessNullable(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol methodSymbol, IEnumerable exceptions) - { - var argumentNullExceptionTypeSymbol = context.Compilation.GetTypeByMetadataName("System.ArgumentNullException"); - - var isCompilationNullableEnabled = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Enable; - - var nullableContext = context.SemanticModel.GetNullableContext(node.SpanStart); - var isNodeInNullableContext = nullableContext is NullableContext.Enabled; - - if (isNodeInNullableContext || isCompilationNullableEnabled) - { - return exceptions.Where(x => !x.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default)); - } - - return exceptions; - } - - private static IEnumerable GetExceptionTypes(IMethodSymbol methodSymbol) - { - // Get exceptions from Throws attributes - var exceptionAttributes = GetThrowsAttributes(methodSymbol); - - return GetDistinctExceptionTypes(exceptionAttributes); - } - - private static IEnumerable GetThrowsAttributes(ISymbol symbol) - { - return GetThrowsAttributes(symbol.GetAttributes()); - } - - private static IEnumerable GetThrowsAttributes(IEnumerable attributes) - { - return attributes.Where(attr => attr.AttributeClass?.Name is "ThrowsAttribute"); - } - - /// - /// Determines if a catch clause handles the specified exception type. - /// - private bool CatchClauseHandlesException(CatchClauseSyntax catchClause, SemanticModel semanticModel, INamedTypeSymbol exceptionType) - { - if (catchClause.Declaration is null) - return true; // Catch-all handles all exceptions - - var catchType = semanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; - if (catchType is null) - return false; - - // Check if the exceptionType matches or inherits from the catchType - return exceptionType.Equals(catchType, SymbolEqualityComparer.Default) || - exceptionType.InheritsFrom(catchType); - } - - /// - /// Determines if an exception is handled by any enclosing try-catch blocks. - /// - private bool IsExceptionHandled(SyntaxNode node, INamedTypeSymbol exceptionType, SemanticModel semanticModel) - { - // SyntaxNode? prevNode = null; - - var current = node.Parent; - while (current is not null) - { - // Stop here since the throwing node is within a lambda or a local function - // and the boundary has been reached. - if (current is AnonymousFunctionExpressionSyntax - or LocalFunctionStatementSyntax) - { - return false; - } - - if (current is TryStatementSyntax tryStatement) - { - // Prevents analysis within the first try-catch, - // when coming from either a catch clause or a finally clause. - - // Skip if the node is within a catch or finally block of the current try statement - bool isInCatchOrFinally = tryStatement.Catches.Any(c => c.Contains(node)) || - (tryStatement.Finally is not null && tryStatement.Finally.Contains(node)); - - - if (!isInCatchOrFinally) - { - foreach (var catchClause in tryStatement.Catches) - { - if (CatchClauseHandlesException(catchClause, semanticModel, exceptionType)) - { - return true; - } - } - } - } - - // prevNode = current; - current = current.Parent; - } - - return false; // Exception is not handled by any enclosing try-catch - } - - /// - /// Analyzes a node that throws or propagates exceptions to check for handling or declaration. - /// - private void AnalyzeExceptionThrowingNode( - SyntaxNodeAnalysisContext context, - SyntaxNode node, - INamedTypeSymbol? exceptionType, - AnalyzerSettings settings) - { - if (exceptionType is null) - return; - - var exceptionName = exceptionType.ToDisplayString(); - - if (settings.IgnoredExceptions.Contains(exceptionName)) - { - // Completely ignore this exception - return; - } - else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) - { - if (ShouldIgnore(node, mode)) - { - // Report as THROW002 (Info level) - var diagnostic = Diagnostic.Create(RuleIgnoredException, GetSignificantLocation(node), exceptionType.Name); - context.ReportDiagnostic(diagnostic); - return; - } - } - - // Check for general exceptions - if (context.Node is not InvocationExpressionSyntax && IsGeneralException(exceptionType)) - { - context.ReportDiagnostic(Diagnostic.Create(RuleGeneralThrow, GetSignificantLocation(node))); - } - - // Check if the exception is declared via [Throws] - var isDeclared = IsExceptionDeclaredInMethod(context, node, exceptionType); - - // Determine if the exception is handled by any enclosing try-catch - var isHandled = IsExceptionHandled(node, exceptionType, context.SemanticModel); - - // Report diagnostic if neither handled nor declared - if (!isHandled && !isDeclared) - { - var properties = ImmutableDictionary.Create() - .Add("ExceptionType", exceptionType.Name); - - var isThrowingConstruct = node is ThrowStatementSyntax or ThrowExpressionSyntax; - - var verb = isThrowingConstruct ? THROW001Verbs.Is : THROW001Verbs.MightBe; - - var diagnostic = Diagnostic.Create(RuleUnhandledException, GetSignificantLocation(node), properties, exceptionType.Name, verb); - context.ReportDiagnostic(diagnostic); - } - } - - private bool ShouldIgnore(SyntaxNode node, ExceptionMode mode) - { - if (mode is ExceptionMode.Always) - return true; - - if (mode is ExceptionMode.Throw && node is ThrowStatementSyntax or ThrowExpressionSyntax) - return true; - - if (mode is ExceptionMode.Propagation && node - is MemberAccessExpressionSyntax - or IdentifierNameSyntax - or InvocationExpressionSyntax) - return true; - - return false; - } - - private bool IsExceptionDeclaredInMethod(SyntaxNodeAnalysisContext context, SyntaxNode node, INamedTypeSymbol exceptionType) - { - foreach (var ancestor in node.Ancestors()) - { - IMethodSymbol? methodSymbol = ancestor switch - { - MethodDeclarationSyntax methodDeclaration => context.SemanticModel.GetDeclaredSymbol(methodDeclaration), - ConstructorDeclarationSyntax constructorDeclaration => context.SemanticModel.GetDeclaredSymbol(constructorDeclaration), - AccessorDeclarationSyntax accessorDeclaration => context.SemanticModel.GetDeclaredSymbol(accessorDeclaration), - LocalFunctionStatementSyntax localFunction => context.SemanticModel.GetDeclaredSymbol(localFunction), - AnonymousFunctionExpressionSyntax anonymousFunction => context.SemanticModel.GetSymbolInfo(anonymousFunction).Symbol as IMethodSymbol, - _ => null, - }; - - if (methodSymbol is null) - continue; // Continue up to next node - - if (IsExceptionDeclaredInSymbol(methodSymbol, exceptionType)) - return true; - - if (ancestor is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax) - { - // Break because you are analyzing a local function or anonymous function (lambda) - // If you don't then it will got to the method, and it will affect analysis of this inline function. - break; - } - } - - return false; - } - - private bool IsExceptionDeclaredInSymbol(IMethodSymbol? methodSymbol, INamedTypeSymbol exceptionType) - { - if (methodSymbol is null) - return false; - - var declaredExceptionTypes = GetExceptionTypes(methodSymbol); - - foreach (var declaredExceptionType in declaredExceptionTypes) - { - if (exceptionType.Equals(declaredExceptionType, SymbolEqualityComparer.Default)) - return true; - - // Check if the declared exception is a base type of the thrown exception - if (exceptionType.InheritsFrom(declaredExceptionType)) - return true; - } - - return false; - } - - private bool IsGeneralException(INamedTypeSymbol exceptionType) - { - return exceptionType.ToDisplayString() is "System.Exception"; - } - - private bool IsPropertyGetter(ExpressionSyntax expression) - { - var parent = expression.Parent; - - if (parent is AssignmentExpressionSyntax assignment) - { - if (assignment.Left == expression) - return false; // It's a setter - } - else if (parent is PrefixUnaryExpressionSyntax prefixUnary) - { - if (prefixUnary.IsKind(SyntaxKind.PreIncrementExpression) || prefixUnary.IsKind(SyntaxKind.PreDecrementExpression)) - return false; // It's a setter - } - else if (parent is PostfixUnaryExpressionSyntax postfixUnary) - { - if (postfixUnary.IsKind(SyntaxKind.PostIncrementExpression) || postfixUnary.IsKind(SyntaxKind.PostDecrementExpression)) - return false; // It's a setter - } - - return true; // Assume getter in other cases - } - - private bool IsPropertySetter(ExpressionSyntax expression) - { - var parent = expression.Parent; - - if (parent is AssignmentExpressionSyntax assignment) - { - if (assignment.Left == expression) - return true; // It's a setter - } - else if (parent is PrefixUnaryExpressionSyntax prefixUnary) - { - if (prefixUnary.IsKind(SyntaxKind.PreIncrementExpression) || prefixUnary.IsKind(SyntaxKind.PreDecrementExpression)) - return true; // It's a setter - } - else if (parent is PostfixUnaryExpressionSyntax postfixUnary) - { - if (postfixUnary.IsKind(SyntaxKind.PostIncrementExpression) || postfixUnary.IsKind(SyntaxKind.PostDecrementExpression)) - return true; // It's a setter - } - - return false; // Assume getter in other cases - } +namespace Sundstrom.CheckedExceptions; + +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Linq; +using System.Text.Json; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public partial class CheckedExceptionsAnalyzer : DiagnosticAnalyzer +{ + static readonly ConcurrentDictionary configs = new ConcurrentDictionary(); + + // Diagnostic IDs + public const string DiagnosticIdUnhandled = "THROW001"; + public const string DiagnosticIdIgnoredException = "THROW002"; + public const string DiagnosticIdGeneralThrows = "THROW003"; + public const string DiagnosticIdGeneralThrow = "THROW004"; + public const string DiagnosticIdDuplicateDeclarations = "THROW005"; + public const string DiagnosticIdMissingThrowsOnBaseMember = "THROW006"; + public const string DiagnosticIdMissingThrowsFromBaseMember = "THROW007"; + + public static readonly IEnumerable AllDiagnosticsIds = [DiagnosticIdUnhandled, DiagnosticIdGeneralThrows, DiagnosticIdGeneralThrow, DiagnosticIdDuplicateDeclarations]; + + private static readonly DiagnosticDescriptor RuleUnhandledException = new( + DiagnosticIdUnhandled, + "Unhandled exception", + "Exception '{0}' {1} thrown but not handled", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Reports exceptions that are thrown but not caught or declared with [Throws], potentially violating exception safety."); + + private static readonly DiagnosticDescriptor RuleIgnoredException = new DiagnosticDescriptor( + DiagnosticIdIgnoredException, + "Ignored exception may cause runtime issues", + "Exception '{0}' is ignored by configuration but may cause runtime issues if unhandled", + "Usage", + DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Informs about exceptions excluded from analysis but which may still propagate at runtime if not properly handled."); + + private static readonly DiagnosticDescriptor RuleGeneralThrow = new( + DiagnosticIdGeneralThrow, + "Avoid throwing 'Exception'", + "Throwing 'Exception' is too general; use a more specific exception type instead", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Discourages throwing the base System.Exception type directly, encouraging clearer and more actionable error semantics."); + + private static readonly DiagnosticDescriptor RuleGeneralThrows = new DiagnosticDescriptor( + DiagnosticIdGeneralThrows, + "Avoid declaring exception type 'Exception'", + "Declaring 'Exception' is too general; use a more specific exception type instead", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Discourages the use of System.Exception in [Throws] attributes. Prefer declaring more specific exception types."); + + private static readonly DiagnosticDescriptor RuleDuplicateDeclarations = new DiagnosticDescriptor( + DiagnosticIdDuplicateDeclarations, + "Avoid duplicate declarations of the same exception type", + "Duplicate declarations of the exception type '{0}' found. Remove them to avoid redundancy.", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Detects multiple [Throws] declarations for the same exception type on a single member, which is redundant."); + + private static readonly DiagnosticDescriptor RuleMissingThrowsOnBaseMember = new DiagnosticDescriptor( + DiagnosticIdMissingThrowsOnBaseMember, + "Missing Throws declaration", + "Exception '{1}' is not declared on base member '{0}'", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Base or interface members should declare compatible exceptions when overridden or implemented members declare them using [Throws]."); + + private static readonly DiagnosticDescriptor RuleMissingThrowsFromBaseMember = new( + DiagnosticIdMissingThrowsFromBaseMember, + "Missing Throws declaration for exception declared on base member", + "Base member '{0}' declares exception '{1}' which is not declared here", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Ensures that overridden or implemented members declare exceptions required by their base or interface definitions."); + + public override ImmutableArray SupportedDiagnostics => + [RuleUnhandledException, RuleIgnoredException, RuleGeneralThrows, RuleGeneralThrow, RuleDuplicateDeclarations, RuleMissingThrowsOnBaseMember, RuleMissingThrowsFromBaseMember]; + + private const string SettingsFileName = "CheckedExceptions.settings.json"; + private static readonly JsonSerializerOptions _settingsFileJsonOptions = new() + { + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + }; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + // Register actions for throw statements and expressions + context.RegisterSyntaxNodeAction(AnalyzeThrowStatement, SyntaxKind.ThrowStatement); + context.RegisterSyntaxNodeAction(AnalyzeThrowExpression, SyntaxKind.ThrowExpression); + + context.RegisterSymbolAction(AnalyzeMethodSymbol, SymbolKind.Method); + + context.RegisterSyntaxNodeAction(AnalyzeLambdaExpression, SyntaxKind.SimpleLambdaExpression); + context.RegisterSyntaxNodeAction(AnalyzeLambdaExpression, SyntaxKind.ParenthesizedLambdaExpression); + context.RegisterSyntaxNodeAction(AnalyzeLocalFunctionStatement, SyntaxKind.LocalFunctionStatement); + + // Register additional actions for method calls, object creations, etc. + context.RegisterSyntaxNodeAction(AnalyzeMethodCall, SyntaxKind.InvocationExpression); + context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression); + context.RegisterSyntaxNodeAction(AnalyzeImplicitObjectCreation, SyntaxKind.ImplicitObjectCreationExpression); + context.RegisterSyntaxNodeAction(AnalyzeIdentifier, SyntaxKind.IdentifierName); + context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); + context.RegisterSyntaxNodeAction(AnalyzeAwait, SyntaxKind.AwaitExpression); + context.RegisterSyntaxNodeAction(AnalyzeElementAccess, SyntaxKind.ElementAccessExpression); + context.RegisterSyntaxNodeAction(AnalyzeEventAssignment, SyntaxKind.AddAssignmentExpression); + context.RegisterSyntaxNodeAction(AnalyzeEventAssignment, SyntaxKind.SubtractAssignmentExpression); + } + + private AnalyzerSettings LoadAnalyzerSettings(AnalyzerOptions analyzerOptions) + { + return configs.GetOrAdd(analyzerOptions, _ => + { + var configFileText = analyzerOptions.AdditionalFiles + .FirstOrDefault(f => SettingsFileName.Equals(Path.GetFileName(f.Path), StringComparison.OrdinalIgnoreCase)) + ?.GetText()?.ToString(); + + AnalyzerSettings? val = null; + + if (configFileText is not null) + { + val = JsonSerializer.Deserialize(configFileText, _settingsFileJsonOptions); + } + + return val ?? AnalyzerSettings.CreateWithDefaults(); // Return default options if the config file is not found + }); + } + + private void AnalyzeLambdaExpression(SyntaxNodeAnalysisContext context) + { + var lambdaExpression = (LambdaExpressionSyntax)context.Node; + AnalyzeFunctionAttributes(lambdaExpression, lambdaExpression.AttributeLists.SelectMany(a => a.Attributes), context.SemanticModel, context); + } + + private void AnalyzeLocalFunctionStatement(SyntaxNodeAnalysisContext context) + { + var localFunction = (LocalFunctionStatementSyntax)context.Node; + AnalyzeFunctionAttributes(localFunction, localFunction.AttributeLists.SelectMany(a => a.Attributes), context.SemanticModel, context); + } + + private void AnalyzeFunctionAttributes(SyntaxNode node, IEnumerable attributes, SemanticModel semanticModel, SyntaxNodeAnalysisContext context) + { + var throwsAttributes = attributes + .Where(attr => IsThrowsAttribute(attr, semanticModel)) + .ToList(); + + CheckForGeneralExceptionThrows(throwsAttributes, context); + CheckForDuplicateThrowsAttributes(throwsAttributes, context); + } + + /// + /// Determines whether the given attribute is a ThrowsAttribute. + /// + private bool IsThrowsAttribute(AttributeSyntax attributeSyntax, SemanticModel semanticModel) + { + var attributeSymbol = semanticModel.GetSymbolInfo(attributeSyntax).Symbol as IMethodSymbol; + if (attributeSymbol is null) + return false; + + var attributeType = attributeSymbol.ContainingType; + return attributeType.Name is "ThrowsAttribute"; + } + + private void AnalyzeMethodSymbol(SymbolAnalysisContext context) + { + var methodSymbol = (IMethodSymbol)context.Symbol; + + var throwsAttributes = GetThrowsAttributes(methodSymbol).ToImmutableArray(); + + CheckForCompatibilityWithBaseOrInterface(context, throwsAttributes); + + if (throwsAttributes.Length == 0) + return; + + CheckForGeneralExceptionThrows(throwsAttributes, context); + CheckForDuplicateThrowsAttributes(context, throwsAttributes); + } + + private static IEnumerable FilterThrowsAttributesByException(ImmutableArray exceptionAttributes, string exceptionTypeName) + { + return exceptionAttributes + .Where(attribute => IsThrowsAttributeForException(attribute, exceptionTypeName)); + } + + public static bool IsThrowsAttributeForException(AttributeData attribute, string exceptionTypeName) + { + if (!attribute.ConstructorArguments.Any()) + return false; + + var exceptionTypes = GetDistinctExceptionTypes(attribute); + return exceptionTypes.Any(exceptionType => exceptionType?.Name == exceptionTypeName); + } + + public static IEnumerable GetExceptionTypes(params IEnumerable exceptionAttributes) + { + var constructorArguments = exceptionAttributes + .SelectMany(attr => attr.ConstructorArguments); + + foreach (var arg in constructorArguments) + { + if (arg.Kind is TypedConstantKind.Array) + { + foreach (var t in arg.Values) + { + if (t.Kind is TypedConstantKind.Type) + { + yield return (INamedTypeSymbol)t.Value!; + } + } + } + else if (arg.Kind is TypedConstantKind.Type) + { + yield return (INamedTypeSymbol)arg.Value!; + } + } + } + + public static IEnumerable GetDistinctExceptionTypes(params IEnumerable exceptionAttributes) + { + var exceptionTypes = GetExceptionTypes(exceptionAttributes); + + return exceptionTypes + .Distinct(SymbolEqualityComparer.Default) + .OfType(); + } + + /// + /// Analyzes throw statements to determine if exceptions are handled or declared. + /// + private void AnalyzeThrowStatement(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var throwStatement = (ThrowStatementSyntax)context.Node; + + // Handle rethrows (throw;) + if (throwStatement.Expression is null) + { + if (IsWithinCatchBlock(throwStatement, out var catchClause) && catchClause is not null) + { + if (catchClause.Declaration is null) + { + // General catch block with 'throw;' + // Analyze exceptions thrown in the try block + var tryStatement = catchClause.Ancestors().OfType().FirstOrDefault(); + if (tryStatement is not null) + { + AnalyzeExceptionsInTryBlock(context, tryStatement, catchClause, throwStatement, settings); + } + } + else + { + var exceptionType = context.SemanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; + AnalyzeExceptionThrowingNode(context, throwStatement, exceptionType, settings); + } + } + + return; // No further analysis for rethrows + } + + // Handle throw new ExceptionType() + if (throwStatement.Expression is ObjectCreationExpressionSyntax creationExpression) + { + var exceptionType = context.SemanticModel.GetTypeInfo(creationExpression).Type as INamedTypeSymbol; + AnalyzeExceptionThrowingNode(context, throwStatement, exceptionType, settings); + } + } + + private void AnalyzeExceptionsInTryBlock(SyntaxNodeAnalysisContext context, TryStatementSyntax tryStatement, CatchClauseSyntax generalCatchClause, ThrowStatementSyntax throwStatement, AnalyzerSettings settings) + { + var semanticModel = context.SemanticModel; + + // Collect exceptions that can be thrown in the try block + var thrownExceptions = CollectUnhandledExceptions(context, tryStatement.Block, settings); + + // Collect exception types handled by preceding catch clauses + var handledExceptions = new HashSet(SymbolEqualityComparer.Default); + foreach (var catchClause in tryStatement.Catches) + { + if (catchClause == generalCatchClause) + break; // Stop at the general catch clause + + if (catchClause.Declaration is not null) + { + var catchType = semanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; + if (catchType is not null) + { + handledExceptions.Add(catchType); + } + } + else + { + // General catch clause before our general catch; handles all exceptions + handledExceptions = null; + break; + } + } + + if (handledExceptions is null) + { + // All exceptions are handled by a previous general catch + return; + } + + // For each thrown exception, check if it is handled + foreach (var exceptionType in thrownExceptions.Distinct(SymbolEqualityComparer.Default).OfType()) + { + var exceptionName = exceptionType.ToDisplayString(); + + if (settings.IgnoredExceptions.Contains(exceptionName)) + { + // Completely ignore this exception + continue; + } + else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) + { + if (ShouldIgnore(throwStatement, mode)) + { + // Report as THROW002 (Info level) + var diagnostic = Diagnostic.Create(RuleIgnoredException, GetSignificantLocation(throwStatement), exceptionType.Name); + context.ReportDiagnostic(diagnostic); + continue; + } + } + + bool isHandled = handledExceptions.Any(handledException => + exceptionType.Equals(handledException, SymbolEqualityComparer.Default) || + exceptionType.InheritsFrom(handledException)); + + bool isDeclared = IsExceptionDeclaredInMethod(context, tryStatement, exceptionType); + + if (!isHandled && !isDeclared) + { + // Report diagnostic for unhandled exception + var diagnostic = Diagnostic.Create( + RuleUnhandledException, + GetSignificantLocation(throwStatement), + exceptionType.Name, + THROW001Verbs.MightBe); + + context.ReportDiagnostic(diagnostic); + } + } + } + + private HashSet CollectUnhandledExceptions(SyntaxNodeAnalysisContext context, BlockSyntax block, AnalyzerSettings settings) + { + var unhandledExceptions = new HashSet(SymbolEqualityComparer.Default); + + foreach (var statement in block.Statements) + { + if (statement is TryStatementSyntax tryStatement) + { + // Recursively collect exceptions from the inner try block + var innerUnhandledExceptions = CollectUnhandledExceptions(context, tryStatement.Block, settings); + + // Remove exceptions that are caught by the inner catch clauses + var caughtExceptions = GetCaughtExceptions(tryStatement.Catches, context.SemanticModel); + innerUnhandledExceptions.RemoveWhere(exceptionType => IsExceptionCaught(exceptionType, caughtExceptions)); + + // Add any exceptions that are not handled in the inner try block + unhandledExceptions.UnionWith(innerUnhandledExceptions); + } + else + { + // Collect exceptions thrown in this statement + var statementExceptions = CollectExceptionsFromStatement(context, statement, settings); + + // Add them to the unhandled exceptions + unhandledExceptions.UnionWith(statementExceptions); + } + } + + return unhandledExceptions; + } + + private HashSet CollectExceptionsFromStatement(SyntaxNodeAnalysisContext context, StatementSyntax statement, AnalyzerSettings settings) + { + SemanticModel semanticModel = context.SemanticModel; + + var exceptions = new HashSet(SymbolEqualityComparer.Default); + + foreach (var s in statement.DescendantNodesAndSelf()) + { + switch (s) + { + // Collect exceptions from throw statements + case ThrowStatementSyntax throwStatement: + CollectExpressionsFromThrows(throwStatement, throwStatement.Expression); + break; + + // Collect exceptions from throw expressions + case ThrowExpressionSyntax throwExpression: + CollectExpressionsFromThrows(throwExpression, throwExpression.Expression); + break; + + // Collect exceptions from method calls and object creations + case InvocationExpressionSyntax: + case ObjectCreationExpressionSyntax: + CollectExpressionsForMethodSymbols((ExpressionSyntax)s); + break; + + // Collect exceptions from property accessors and identifiers + case MemberAccessExpressionSyntax: + case ElementAccessExpressionSyntax: + case IdentifierNameSyntax: + CollectExpressionsForPropertySymbols((ExpressionSyntax)s); + break; + } + } + + return exceptions; + + void CollectExpressionsFromThrows(SyntaxNode throwExpression, ExpressionSyntax? subExpression) + { + if (subExpression is null) return; + + if (semanticModel.GetTypeInfo(subExpression).Type is not INamedTypeSymbol exceptionType) return; + + if (ShouldIncludeException(exceptionType, throwExpression, settings)) + { + exceptions.Add(exceptionType); + } + } + + void CollectExpressionsForMethodSymbols(ExpressionSyntax expression) + { + if (semanticModel.GetSymbolInfo(expression).Symbol is not IMethodSymbol methodSymbol) return; + + var exceptionTypes = GetExceptionTypes(methodSymbol); + + // Get exceptions from XML documentation + var xmlExceptionTypes = GetExceptionTypesFromDocumentationCommentXml(semanticModel.Compilation, methodSymbol); + + xmlExceptionTypes = ProcessNullable(context, expression, methodSymbol, xmlExceptionTypes); + + if (xmlExceptionTypes.Any()) + { + exceptionTypes = exceptionTypes.Concat(xmlExceptionTypes.Select(x => x.ExceptionType)); + } + + foreach (var exceptionType in exceptionTypes) + { + if (ShouldIncludeException(exceptionType, expression, settings)) + { + exceptions.Add(exceptionType); + } + } + } + + void CollectExpressionsForPropertySymbols(ExpressionSyntax expression) + { + if (semanticModel.GetSymbolInfo(expression).Symbol is not IPropertySymbol propertySymbol) return; + + HashSet exceptionTypes = GetPropertyExceptionTypes(context, expression, propertySymbol); + + foreach (var exceptionType in exceptionTypes) + { + if (ShouldIncludeException(exceptionType, expression, settings)) + { + exceptions.Add(exceptionType); + } + } + } + } + + public bool ShouldIncludeException(INamedTypeSymbol exceptionType, SyntaxNode node, AnalyzerSettings settings) + { + // var exceptions = new HashSet(SymbolEqualityComparer.Default); + + var exceptionName = exceptionType.ToDisplayString(); + + if (settings.IgnoredExceptions.Contains(exceptionName)) + { + // Completely ignore this exception + return false; + } + else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) + { + if (ShouldIgnore(node, mode)) + { + return false; + } + } + + return true; + } + + private HashSet? GetCaughtExceptions(SyntaxList catchClauses, SemanticModel semanticModel) + { + var caughtExceptions = new HashSet(SymbolEqualityComparer.Default); + + foreach (var catchClause in catchClauses) + { + if (catchClause.Declaration is not null) + { + var catchType = semanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; + if (catchType is not null) + { + caughtExceptions.Add(catchType); + } + } + else + { + // General catch clause catches all exceptions + caughtExceptions = null; + break; + } + } + + return caughtExceptions; + } + + private bool IsExceptionCaught(INamedTypeSymbol exceptionType, HashSet? caughtExceptions) + { + if (caughtExceptions is null) + { + // General catch clause catches all exceptions + return true; + } + + return caughtExceptions.Any(catchType => + exceptionType.Equals(catchType, SymbolEqualityComparer.Default) || + exceptionType.InheritsFrom(catchType)); + } + + private void AnalyzeAwait(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var awaitExpression = (AwaitExpressionSyntax)context.Node; + + if (awaitExpression.Expression is InvocationExpressionSyntax invocation) + { + // Get the invoked symbol + var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation); + var methodSymbol = symbolInfo.Symbol as IMethodSymbol; + + if (methodSymbol is null) + return; + + // Handle delegate invokes by getting the target method symbol + if (methodSymbol.MethodKind == MethodKind.DelegateInvoke) + { + var targetMethodSymbol = GetTargetMethodSymbol(context, invocation); + if (targetMethodSymbol is not null) + { + methodSymbol = targetMethodSymbol; + } + else + { + // Could not find the target method symbol + return; + } + } + + AnalyzeMemberExceptions(context, invocation, methodSymbol, settings); + } + else if (awaitExpression.Expression is MemberAccessExpressionSyntax memberAccess) + { + AnalyzeIdentifierAndMemberAccess(context, memberAccess, settings); + } + else if (awaitExpression.Expression is IdentifierNameSyntax identifier) + { + AnalyzeIdentifierAndMemberAccess(context, identifier, settings); + } + } + + /// + /// Determines if a node is within a catch block. + /// + private bool IsWithinCatchBlock(SyntaxNode node, out CatchClauseSyntax? catchClause) + { + catchClause = node.Ancestors().OfType().FirstOrDefault(); + return catchClause is not null; + } + + /// + /// Analyzes throw expressions to determine if exceptions are handled or declared. + /// + private void AnalyzeThrowExpression(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var throwExpression = (ThrowExpressionSyntax)context.Node; + + if (throwExpression.Expression is ObjectCreationExpressionSyntax creationExpression) + { + var exceptionType = context.SemanticModel.GetTypeInfo(creationExpression).Type as INamedTypeSymbol; + AnalyzeExceptionThrowingNode(context, throwExpression, exceptionType, settings); + } + } + + /// + /// Analyzes method calls to determine if exceptions are handled or declared. + /// + private void AnalyzeMethodCall(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var invocation = (InvocationExpressionSyntax)context.Node; + + if (invocation.Parent is AwaitExpressionSyntax) + { + // Handled in other method. + return; + } + + // Get the invoked symbol + var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation); + var methodSymbol = symbolInfo.Symbol as IMethodSymbol; + + if (methodSymbol is null) + return; + + // Handle delegate invokes by getting the target method symbol + if (methodSymbol.MethodKind == MethodKind.DelegateInvoke) + { + var targetMethodSymbol = GetTargetMethodSymbol(context, invocation); + if (targetMethodSymbol is not null) + { + methodSymbol = targetMethodSymbol; + } + else + { + // Could not find the target method symbol + return; + } + } + + AnalyzeMemberExceptions(context, invocation, methodSymbol, settings); + } + + /// + /// Resolves the target method symbol from a delegate, lambda, or method group. + /// + private IMethodSymbol? GetTargetMethodSymbol(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation) + { + var expression = invocation.Expression; + + // Get the symbol of the expression being invoked + var symbolInfo = context.SemanticModel.GetSymbolInfo(expression); + var symbol = symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.FirstOrDefault(); + + if (symbol is null) + return null; + + if (symbol is ILocalSymbol localSymbol) + { + // Get the syntax node where the local variable is declared + var declaringSyntaxReference = localSymbol.DeclaringSyntaxReferences.FirstOrDefault(); + if (declaringSyntaxReference is not null) + { + var syntaxNode = declaringSyntaxReference.GetSyntax(); + + if (syntaxNode is VariableDeclaratorSyntax variableDeclarator) + { + var initializer = variableDeclarator.Initializer?.Value; + + if (initializer is not null) + { + // Handle lambdas + if (initializer is AnonymousFunctionExpressionSyntax anonymousFunction) + { + var lambdaSymbol = context.SemanticModel.GetSymbolInfo(anonymousFunction).Symbol as IMethodSymbol; + if (lambdaSymbol is not null) + return lambdaSymbol; + } + + // Handle method groups + if (initializer is IdentifierNameSyntax || initializer is MemberAccessExpressionSyntax) + { + var methodGroupSymbol = context.SemanticModel.GetSymbolInfo(initializer).Symbol as IMethodSymbol; + if (methodGroupSymbol is not null) + return methodGroupSymbol; + } + + // Get the method symbol of the initializer (lambda or method group) + var initializerSymbolInfo = context.SemanticModel.GetSymbolInfo(initializer); + var initializerSymbol = initializerSymbolInfo.Symbol ?? initializerSymbolInfo.CandidateSymbols.FirstOrDefault(); + + if (initializerSymbol is IMethodSymbol targetMethodSymbol) + { + return targetMethodSymbol; + } + } + } + } + } + + return null; + } + + /// + /// Analyzes object creation expressions to determine if exceptions are handled or declared. + /// + private void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var objectCreation = (ObjectCreationExpressionSyntax)context.Node; + + var constructorSymbol = context.SemanticModel.GetSymbolInfo(objectCreation).Symbol as IMethodSymbol; + if (constructorSymbol is null) + return; + + AnalyzeMemberExceptions(context, objectCreation, constructorSymbol, settings); + } + + + /// + /// Analyzes implicit object creation expressions to determine if exceptions are handled or declared. + /// + private void AnalyzeImplicitObjectCreation(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var objectCreation = (ImplicitObjectCreationExpressionSyntax)context.Node; + + var constructorSymbol = context.SemanticModel.GetSymbolInfo(objectCreation).Symbol as IMethodSymbol; + if (constructorSymbol is null) + return; + + AnalyzeMemberExceptions(context, objectCreation, constructorSymbol, settings); + } + + /// + /// Analyzes member access expressions (e.g., property accessors) for exception handling. + /// + private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var memberAccess = (MemberAccessExpressionSyntax)context.Node; + + AnalyzeIdentifierAndMemberAccess(context, memberAccess, settings); + } + + /// + /// Analyzes identifier names (e.g. local variables or property accessors in context) for exception handling. + /// + private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var identifierName = (IdentifierNameSyntax)context.Node; + + // Ignore identifiers that are part of await expression + if (identifierName.Parent is AwaitExpressionSyntax) + return; + + // Ignore identifiers that are part of member access + if (identifierName.Parent is MemberAccessExpressionSyntax) + return; + + AnalyzeIdentifierAndMemberAccess(context, identifierName, settings); + } + + private void AnalyzeIdentifierAndMemberAccess(SyntaxNodeAnalysisContext context, ExpressionSyntax expression, AnalyzerSettings settings) + { + if (context.SemanticModel.GetSymbolInfo(expression).Symbol is IPropertySymbol propertySymbol) + { + AnalyzePropertyExceptions(context, expression, propertySymbol, settings); + } + } + + /// + /// Analyzes element access expressions (e.g., indexers) for exception handling. + /// + private void AnalyzeElementAccess(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var elementAccess = (ElementAccessExpressionSyntax)context.Node; + + if (context.SemanticModel.GetSymbolInfo(elementAccess).Symbol is IPropertySymbol propertySymbol) + { + AnalyzePropertyExceptions(context, elementAccess, propertySymbol, settings); + } + } + + /// + /// Analyzes event assignments (e.g., += or -=) for exception handling. + /// + private void AnalyzeEventAssignment(SyntaxNodeAnalysisContext context) + { + var settings = LoadAnalyzerSettings(context.Options); + + var assignment = (AssignmentExpressionSyntax)context.Node; + + var eventSymbol = context.SemanticModel.GetSymbolInfo(assignment.Left).Symbol as IEventSymbol; + if (eventSymbol is null) + return; + + // Get the method symbol for the add or remove accessor + IMethodSymbol? methodSymbol = null; + + if (assignment.IsKind(SyntaxKind.AddAssignmentExpression) && eventSymbol.AddMethod is not null) + { + methodSymbol = eventSymbol.AddMethod; + } + else if (assignment.IsKind(SyntaxKind.SubtractAssignmentExpression) && eventSymbol.RemoveMethod is not null) + { + methodSymbol = eventSymbol.RemoveMethod; + } + + if (methodSymbol is not null) + { + AnalyzeMemberExceptions(context, assignment, methodSymbol, settings); + } + } + + /// + /// Analyzes exceptions thrown by a property, specifically its getters and setters. + /// + private void AnalyzePropertyExceptions(SyntaxNodeAnalysisContext context, ExpressionSyntax expression, IPropertySymbol propertySymbol, + AnalyzerSettings settings) + { + HashSet exceptionTypes = GetPropertyExceptionTypes(context, expression, propertySymbol); + + // Deduplicate and analyze each distinct exception type + foreach (var exceptionType in exceptionTypes.Distinct(SymbolEqualityComparer.Default).OfType()) + { + AnalyzeExceptionThrowingNode(context, expression, exceptionType, settings); + } + } + + private HashSet GetPropertyExceptionTypes(SyntaxNodeAnalysisContext context, ExpressionSyntax expression, IPropertySymbol propertySymbol) + { + // Determine if the analyzed expression is for a getter or setter + bool isGetter = IsPropertyGetter(expression); + bool isSetter = IsPropertySetter(expression); + + // List to collect all relevant exception types + var exceptionTypes = new HashSet(SymbolEqualityComparer.Default); + + // Retrieve exception types documented in XML comments for the property + var xmlDocumentedExceptions = GetExceptionTypesFromDocumentationCommentXml(context.Compilation, propertySymbol); + + // Filter exceptions documented specifically for the getter and setter + var getterExceptions = xmlDocumentedExceptions.Where(IsGetterException); + var setterExceptions = xmlDocumentedExceptions.Where(IsSetterException); + + if (isSetter && propertySymbol.SetMethod is not null) + { + // Will filter away + setterExceptions = ProcessNullable(context, expression, propertySymbol.SetMethod, setterExceptions); + } + + // Handle exceptions that don't explicitly belong to getters or setters + var allOtherExceptions = xmlDocumentedExceptions.Where(x => !IsGetterException(x) && !IsSetterException(x)); + + if (isSetter && propertySymbol.SetMethod is not null) + { + allOtherExceptions = ProcessNullable(context, expression, propertySymbol.SetMethod, allOtherExceptions); + } + + // Analyze exceptions thrown by the getter if applicable + if (isGetter && propertySymbol.GetMethod is not null) + { + var getterMethodExceptions = GetExceptionTypes(propertySymbol.GetMethod); + exceptionTypes.AddRange(getterExceptions.Select(x => x.ExceptionType)); + exceptionTypes.AddRange(getterMethodExceptions); + } + + // Analyze exceptions thrown by the setter if applicable + if (isSetter && propertySymbol.SetMethod is not null) + { + var setterMethodExceptions = GetExceptionTypes(propertySymbol.SetMethod); + exceptionTypes.AddRange(setterExceptions.Select(x => x.ExceptionType)); + exceptionTypes.AddRange(setterMethodExceptions); + } + + if (propertySymbol.GetMethod is not null) + { + allOtherExceptions = ProcessNullable(context, expression, propertySymbol.GetMethod, allOtherExceptions); + } + + // Add other exceptions not specific to getters or setters + exceptionTypes.AddRange(allOtherExceptions.Select(x => x.ExceptionType)); + return exceptionTypes; + + static bool IsGetterException(ExceptionInfo ei) => + ei.Description.Contains(" get ") || + ei.Description.Contains(" gets ") || + ei.Description.Contains(" getting ") || + ei.Description.Contains(" retrieved "); + + static bool IsSetterException(ExceptionInfo ei) => + ei.Description.Contains(" set ") || + ei.Description.Contains(" sets ") || + ei.Description.Contains(" setting "); + } + + /// + /// Analyzes exceptions thrown by a method, constructor, or other member. + /// + private void AnalyzeMemberExceptions(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol? methodSymbol, + AnalyzerSettings settings) + { + if (methodSymbol is null) + return; + + IEnumerable exceptionTypes = GetExceptionTypes(methodSymbol); + + // Get exceptions from XML documentation + var xmlExceptionTypes = GetExceptionTypesFromDocumentationCommentXml(context.Compilation, methodSymbol); + + xmlExceptionTypes = ProcessNullable(context, node, methodSymbol, xmlExceptionTypes); + + if (xmlExceptionTypes.Any()) + { + exceptionTypes = exceptionTypes.Concat(xmlExceptionTypes.Select(x => x.ExceptionType)); + } + + exceptionTypes = ProcessNullable(context, node, methodSymbol, exceptionTypes) + .Distinct(SymbolEqualityComparer.Default) + .OfType(); + + foreach (var exceptionType in exceptionTypes) + { + AnalyzeExceptionThrowingNode(context, node, exceptionType, settings); + } + } + + private static IEnumerable ProcessNullable(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol methodSymbol, IEnumerable exceptionInfos) + { + var argumentNullExceptionTypeSymbol = context.Compilation.GetTypeByMetadataName("System.ArgumentNullException"); + + var isCompilationNullableEnabled = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Enable; + + var nullableContext = context.SemanticModel.GetNullableContext(node.SpanStart); + var isNodeInNullableContext = nullableContext is NullableContext.Enabled; + + if (isNodeInNullableContext || isCompilationNullableEnabled) + { + if (methodSymbol.IsExtensionMethod) + { + return exceptionInfos.Where(x => !x.ExceptionType.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default)); + } + + if (methodSymbol.Parameters.Count() is 1) + { + var p = methodSymbol.Parameters.First(); + + if (p.NullableAnnotation is NullableAnnotation.NotAnnotated) + { + return exceptionInfos.Where(x => !x.ExceptionType.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default)); + } + } + else + { + exceptionInfos = exceptionInfos.Where(x => + { + var p = methodSymbol.Parameters.FirstOrDefault(p => x.Parameters.Any(p2 => p.Name == p2.Name)); + if (p is not null) + { + if (x.ExceptionType.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default) + && p.NullableAnnotation is NullableAnnotation.NotAnnotated) + { + return false; + } + } + + return true; + }); + } + } + + return exceptionInfos; + } + + private static IEnumerable ProcessNullable(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol methodSymbol, IEnumerable exceptions) + { + var argumentNullExceptionTypeSymbol = context.Compilation.GetTypeByMetadataName("System.ArgumentNullException"); + + var isCompilationNullableEnabled = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Enable; + + var nullableContext = context.SemanticModel.GetNullableContext(node.SpanStart); + var isNodeInNullableContext = nullableContext is NullableContext.Enabled; + + if (isNodeInNullableContext || isCompilationNullableEnabled) + { + return exceptions.Where(x => !x.Equals(argumentNullExceptionTypeSymbol, SymbolEqualityComparer.Default)); + } + + return exceptions; + } + + private static IEnumerable GetExceptionTypes(IMethodSymbol methodSymbol) + { + // Get exceptions from Throws attributes + var exceptionAttributes = GetThrowsAttributes(methodSymbol); + + return GetDistinctExceptionTypes(exceptionAttributes); + } + + private static IEnumerable GetThrowsAttributes(ISymbol symbol) + { + return GetThrowsAttributes(symbol.GetAttributes()); + } + + private static IEnumerable GetThrowsAttributes(IEnumerable attributes) + { + return attributes.Where(attr => attr.AttributeClass?.Name is "ThrowsAttribute"); + } + + /// + /// Determines if a catch clause handles the specified exception type. + /// + private bool CatchClauseHandlesException(CatchClauseSyntax catchClause, SemanticModel semanticModel, INamedTypeSymbol exceptionType) + { + if (catchClause.Declaration is null) + return true; // Catch-all handles all exceptions + + var catchType = semanticModel.GetTypeInfo(catchClause.Declaration.Type).Type as INamedTypeSymbol; + if (catchType is null) + return false; + + // Check if the exceptionType matches or inherits from the catchType + return exceptionType.Equals(catchType, SymbolEqualityComparer.Default) || + exceptionType.InheritsFrom(catchType); + } + + /// + /// Determines if an exception is handled by any enclosing try-catch blocks. + /// + private bool IsExceptionHandled(SyntaxNode node, INamedTypeSymbol exceptionType, SemanticModel semanticModel) + { + // SyntaxNode? prevNode = null; + + var current = node.Parent; + while (current is not null) + { + // Stop here since the throwing node is within a lambda or a local function + // and the boundary has been reached. + if (current is AnonymousFunctionExpressionSyntax + or LocalFunctionStatementSyntax) + { + return false; + } + + if (current is TryStatementSyntax tryStatement) + { + // Prevents analysis within the first try-catch, + // when coming from either a catch clause or a finally clause. + + // Skip if the node is within a catch or finally block of the current try statement + bool isInCatchOrFinally = tryStatement.Catches.Any(c => c.Contains(node)) || + (tryStatement.Finally is not null && tryStatement.Finally.Contains(node)); + + + if (!isInCatchOrFinally) + { + foreach (var catchClause in tryStatement.Catches) + { + if (CatchClauseHandlesException(catchClause, semanticModel, exceptionType)) + { + return true; + } + } + } + } + + // prevNode = current; + current = current.Parent; + } + + return false; // Exception is not handled by any enclosing try-catch + } + + /// + /// Analyzes a node that throws or propagates exceptions to check for handling or declaration. + /// + private void AnalyzeExceptionThrowingNode( + SyntaxNodeAnalysisContext context, + SyntaxNode node, + INamedTypeSymbol? exceptionType, + AnalyzerSettings settings) + { + if (exceptionType is null) + return; + + var exceptionName = exceptionType.ToDisplayString(); + + if (settings.IgnoredExceptions.Contains(exceptionName)) + { + // Completely ignore this exception + return; + } + else if (settings.InformationalExceptions.TryGetValue(exceptionName, out var mode)) + { + if (ShouldIgnore(node, mode)) + { + // Report as THROW002 (Info level) + var diagnostic = Diagnostic.Create(RuleIgnoredException, GetSignificantLocation(node), exceptionType.Name); + context.ReportDiagnostic(diagnostic); + return; + } + } + + // Check for general exceptions + if (context.Node is not InvocationExpressionSyntax && IsGeneralException(exceptionType)) + { + context.ReportDiagnostic(Diagnostic.Create(RuleGeneralThrow, GetSignificantLocation(node))); + } + + // Check if the exception is declared via [Throws] + var isDeclared = IsExceptionDeclaredInMethod(context, node, exceptionType); + + // Determine if the exception is handled by any enclosing try-catch + var isHandled = IsExceptionHandled(node, exceptionType, context.SemanticModel); + + // Report diagnostic if neither handled nor declared + if (!isHandled && !isDeclared) + { + var properties = ImmutableDictionary.Create() + .Add("ExceptionType", exceptionType.Name); + + var isThrowingConstruct = node is ThrowStatementSyntax or ThrowExpressionSyntax; + + var verb = isThrowingConstruct ? THROW001Verbs.Is : THROW001Verbs.MightBe; + + var diagnostic = Diagnostic.Create(RuleUnhandledException, GetSignificantLocation(node), properties, exceptionType.Name, verb); + context.ReportDiagnostic(diagnostic); + } + } + + private bool ShouldIgnore(SyntaxNode node, ExceptionMode mode) + { + if (mode is ExceptionMode.Always) + return true; + + if (mode is ExceptionMode.Throw && node is ThrowStatementSyntax or ThrowExpressionSyntax) + return true; + + if (mode is ExceptionMode.Propagation && node + is MemberAccessExpressionSyntax + or IdentifierNameSyntax + or InvocationExpressionSyntax) + return true; + + return false; + } + + private bool IsExceptionDeclaredInMethod(SyntaxNodeAnalysisContext context, SyntaxNode node, INamedTypeSymbol exceptionType) + { + foreach (var ancestor in node.Ancestors()) + { + IMethodSymbol? methodSymbol = ancestor switch + { + MethodDeclarationSyntax methodDeclaration => context.SemanticModel.GetDeclaredSymbol(methodDeclaration), + ConstructorDeclarationSyntax constructorDeclaration => context.SemanticModel.GetDeclaredSymbol(constructorDeclaration), + AccessorDeclarationSyntax accessorDeclaration => context.SemanticModel.GetDeclaredSymbol(accessorDeclaration), + LocalFunctionStatementSyntax localFunction => context.SemanticModel.GetDeclaredSymbol(localFunction), + AnonymousFunctionExpressionSyntax anonymousFunction => context.SemanticModel.GetSymbolInfo(anonymousFunction).Symbol as IMethodSymbol, + _ => null, + }; + + if (methodSymbol is null) + continue; // Continue up to next node + + if (IsExceptionDeclaredInSymbol(methodSymbol, exceptionType)) + return true; + + if (ancestor is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax) + { + // Break because you are analyzing a local function or anonymous function (lambda) + // If you don't then it will got to the method, and it will affect analysis of this inline function. + break; + } + } + + return false; + } + + private bool IsExceptionDeclaredInSymbol(IMethodSymbol? methodSymbol, INamedTypeSymbol exceptionType) + { + if (methodSymbol is null) + return false; + + var declaredExceptionTypes = GetExceptionTypes(methodSymbol); + + foreach (var declaredExceptionType in declaredExceptionTypes) + { + if (exceptionType.Equals(declaredExceptionType, SymbolEqualityComparer.Default)) + return true; + + // Check if the declared exception is a base type of the thrown exception + if (exceptionType.InheritsFrom(declaredExceptionType)) + return true; + } + + return false; + } + + private bool IsGeneralException(INamedTypeSymbol exceptionType) + { + return exceptionType.ToDisplayString() is "System.Exception"; + } + + private bool IsPropertyGetter(ExpressionSyntax expression) + { + var parent = expression.Parent; + + if (parent is AssignmentExpressionSyntax assignment) + { + if (assignment.Left == expression) + return false; // It's a setter + } + else if (parent is PrefixUnaryExpressionSyntax prefixUnary) + { + if (prefixUnary.IsKind(SyntaxKind.PreIncrementExpression) || prefixUnary.IsKind(SyntaxKind.PreDecrementExpression)) + return false; // It's a setter + } + else if (parent is PostfixUnaryExpressionSyntax postfixUnary) + { + if (postfixUnary.IsKind(SyntaxKind.PostIncrementExpression) || postfixUnary.IsKind(SyntaxKind.PostDecrementExpression)) + return false; // It's a setter + } + + return true; // Assume getter in other cases + } + + private bool IsPropertySetter(ExpressionSyntax expression) + { + var parent = expression.Parent; + + if (parent is AssignmentExpressionSyntax assignment) + { + if (assignment.Left == expression) + return true; // It's a setter + } + else if (parent is PrefixUnaryExpressionSyntax prefixUnary) + { + if (prefixUnary.IsKind(SyntaxKind.PreIncrementExpression) || prefixUnary.IsKind(SyntaxKind.PreDecrementExpression)) + return true; // It's a setter + } + else if (parent is PostfixUnaryExpressionSyntax postfixUnary) + { + if (postfixUnary.IsKind(SyntaxKind.PostIncrementExpression) || postfixUnary.IsKind(SyntaxKind.PostDecrementExpression)) + return true; // It's a setter + } + + return false; // Assume getter in other cases + } } \ No newline at end of file From fd1a591fc9a02a752181ce3ead7e1c93d96d6a0f Mon Sep 17 00:00:00 2001 From: sibber5 Date: Fri, 11 Jul 2025 20:20:27 +0300 Subject: [PATCH 4/5] Minor change to analyzer settings --- CheckedExceptions/AnalyzerSettings.cs | 12 +++------- .../CheckedExceptionsAnalyzer.cs | 22 +++++++++---------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/CheckedExceptions/AnalyzerSettings.cs b/CheckedExceptions/AnalyzerSettings.cs index c3265e1..d5ce867 100644 --- a/CheckedExceptions/AnalyzerSettings.cs +++ b/CheckedExceptions/AnalyzerSettings.cs @@ -2,17 +2,11 @@ namespace Sundstrom.CheckedExceptions; using System.Text.Json.Serialization; -public record AnalyzerSettings +public class AnalyzerSettings(IReadOnlyList ignoredExceptions, IReadOnlyDictionary informationalExceptions) { - public IReadOnlyList IgnoredExceptions { get; } + public IReadOnlyList IgnoredExceptions { get; } = ignoredExceptions; - public IReadOnlyDictionary InformationalExceptions { get; } - - public AnalyzerSettings(IReadOnlyList ignoredExceptions, IReadOnlyDictionary informationalExceptions) - { - IgnoredExceptions = ignoredExceptions; - InformationalExceptions = informationalExceptions; - } + public IReadOnlyDictionary InformationalExceptions { get; } = informationalExceptions; public static AnalyzerSettings CreateWithDefaults() => new(new List(), new Dictionary()); } diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.cs index c4262ea..68532b2 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.cs @@ -127,7 +127,7 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(AnalyzeEventAssignment, SyntaxKind.SubtractAssignmentExpression); } - private AnalyzerSettings LoadAnalyzerSettings(AnalyzerOptions analyzerOptions) + private AnalyzerSettings GetAnalyzerSettings(AnalyzerOptions analyzerOptions) { return configs.GetOrAdd(analyzerOptions, _ => { @@ -249,7 +249,7 @@ public static IEnumerable GetDistinctExceptionTypes(params IEn /// private void AnalyzeThrowStatement(SyntaxNodeAnalysisContext context) { - var settings = LoadAnalyzerSettings(context.Options); + var settings = GetAnalyzerSettings(context.Options); var throwStatement = (ThrowStatementSyntax)context.Node; @@ -546,7 +546,7 @@ private bool IsExceptionCaught(INamedTypeSymbol exceptionType, HashSet private void AnalyzeThrowExpression(SyntaxNodeAnalysisContext context) { - var settings = LoadAnalyzerSettings(context.Options); + var settings = GetAnalyzerSettings(context.Options); var throwExpression = (ThrowExpressionSyntax)context.Node; @@ -616,7 +616,7 @@ private void AnalyzeThrowExpression(SyntaxNodeAnalysisContext context) /// private void AnalyzeMethodCall(SyntaxNodeAnalysisContext context) { - var settings = LoadAnalyzerSettings(context.Options); + var settings = GetAnalyzerSettings(context.Options); var invocation = (InvocationExpressionSyntax)context.Node; @@ -716,7 +716,7 @@ private void AnalyzeMethodCall(SyntaxNodeAnalysisContext context) /// private void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context) { - var settings = LoadAnalyzerSettings(context.Options); + var settings = GetAnalyzerSettings(context.Options); var objectCreation = (ObjectCreationExpressionSyntax)context.Node; @@ -733,7 +733,7 @@ private void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context) /// private void AnalyzeImplicitObjectCreation(SyntaxNodeAnalysisContext context) { - var settings = LoadAnalyzerSettings(context.Options); + var settings = GetAnalyzerSettings(context.Options); var objectCreation = (ImplicitObjectCreationExpressionSyntax)context.Node; @@ -749,7 +749,7 @@ private void AnalyzeImplicitObjectCreation(SyntaxNodeAnalysisContext context) /// private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) { - var settings = LoadAnalyzerSettings(context.Options); + var settings = GetAnalyzerSettings(context.Options); var memberAccess = (MemberAccessExpressionSyntax)context.Node; @@ -761,7 +761,7 @@ private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) /// private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context) { - var settings = LoadAnalyzerSettings(context.Options); + var settings = GetAnalyzerSettings(context.Options); var identifierName = (IdentifierNameSyntax)context.Node; @@ -789,7 +789,7 @@ private void AnalyzeIdentifierAndMemberAccess(SyntaxNodeAnalysisContext context, /// private void AnalyzeElementAccess(SyntaxNodeAnalysisContext context) { - var settings = LoadAnalyzerSettings(context.Options); + var settings = GetAnalyzerSettings(context.Options); var elementAccess = (ElementAccessExpressionSyntax)context.Node; @@ -804,7 +804,7 @@ private void AnalyzeElementAccess(SyntaxNodeAnalysisContext context) /// private void AnalyzeEventAssignment(SyntaxNodeAnalysisContext context) { - var settings = LoadAnalyzerSettings(context.Options); + var settings = GetAnalyzerSettings(context.Options); var assignment = (AssignmentExpressionSyntax)context.Node; From 0e36eb6ad158e836f2682dd2b0ca2cf7aeaacae2 Mon Sep 17 00:00:00 2001 From: sibber5 Date: Mon, 28 Jul 2025 18:01:42 +0300 Subject: [PATCH 5/5] Fix merge conflict --- ...onsAnalyzer.DeclaredSuperClassDetection.cs | 3 +- ...edExceptionsAnalyzer.DuplicateDetection.cs | 35 +++++++++---------- ...CheckedExceptionsAnalyzer.GeneralThrows.cs | 27 +++++++------- .../CheckedExceptionsAnalyzer.XmlDocs.cs | 1 + 4 files changed, 30 insertions(+), 36 deletions(-) diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.DeclaredSuperClassDetection.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.DeclaredSuperClassDetection.cs index f9ef0be..b989a40 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.DeclaredSuperClassDetection.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.DeclaredSuperClassDetection.cs @@ -29,8 +29,7 @@ private void CheckForRedundantThrowsHandledByDeclaredSuperClass( if (arg.Expression is TypeOfExpressionSyntax typeOfExpr) { var typeInfo = semanticModel.GetTypeInfo(typeOfExpr.Type, context.CancellationToken); - var typeSymbol = typeInfo.Type as INamedTypeSymbol; - if (typeSymbol == null) + if (typeInfo.Type is not INamedTypeSymbol typeSymbol) continue; declaredTypes.Add(typeSymbol); diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.DuplicateDetection.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.DuplicateDetection.cs index ef4b1ff..487cb84 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.DuplicateDetection.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.DuplicateDetection.cs @@ -27,22 +27,19 @@ private void CheckForDuplicateThrowsAttributes( foreach (var arg in attrSyntax.ArgumentList?.Arguments ?? default) { - if (arg.Expression is TypeOfExpressionSyntax typeOfExpr) + if (arg.Expression is not TypeOfExpressionSyntax typeOfExpr) + continue; + + var typeInfo = semanticModel.GetTypeInfo(typeOfExpr.Type, context.CancellationToken); + if (typeInfo.Type is not INamedTypeSymbol exceptionType) + continue; + + if (!reportedTypes.Add(exceptionType)) { - var typeInfo = semanticModel.GetTypeInfo(typeOfExpr.Type, context.CancellationToken); - var exceptionType = typeInfo.Type as INamedTypeSymbol; - if (exceptionType is null) - continue; - - if (reportedTypes.Contains(exceptionType)) - { - context.ReportDiagnostic(Diagnostic.Create( - RuleDuplicateDeclarations, - typeOfExpr.GetLocation(), // ✅ precise location - exceptionType.Name)); - } - - reportedTypes.Add(exceptionType); + context.ReportDiagnostic(Diagnostic.Create( + RuleDuplicateDeclarations, + typeOfExpr.GetLocation(), // ✅ precise location + exceptionType.Name)); } } } @@ -65,19 +62,19 @@ private void CheckForDuplicateThrowsDeclarations( HashSet? seen = null; - foreach (AttributeSyntax throwsAttribute in throwsAttributes) + foreach (var throwsAttribute in throwsAttributes) { Debug.Assert(throwsAttribute is not null); seen ??= new HashSet(SymbolEqualityComparer.Default); - - foreach (var arg in throwsAttribute.ArgumentList?.Arguments ?? []) + + foreach (var arg in throwsAttribute!.ArgumentList?.Arguments ?? []) { if (arg.Expression is not TypeOfExpressionSyntax typeOfExpr) continue; var typeInfo = semanticModel.GetTypeInfo(typeOfExpr.Type, context.CancellationToken); - if (typeInfo is not INamedTypeSymbol exceptionType) + if (typeInfo.Type is not INamedTypeSymbol exceptionType) continue; if (!seen.Add(exceptionType)) diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.GeneralThrows.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.GeneralThrows.cs index 9a36eb9..3110b6f 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.GeneralThrows.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.GeneralThrows.cs @@ -14,8 +14,6 @@ private static void CheckForGeneralExceptionThrows( ImmutableArray throwsAttributes, SymbolAnalysisContext context) { - const string exceptionName = "Exception"; - foreach (var attribute in throwsAttributes) { var syntaxRef = attribute.ApplicationSyntaxReference; @@ -26,20 +24,19 @@ private static void CheckForGeneralExceptionThrows( foreach (var arg in attrSyntax.ArgumentList?.Arguments ?? []) { - if (arg.Expression is TypeOfExpressionSyntax typeOfExpr) - { - var typeInfo = semanticModel.GetTypeInfo(typeOfExpr.Type, context.CancellationToken); - var type = typeInfo.Type as INamedTypeSymbol; - if (type is null) - continue; + if (arg.Expression is not TypeOfExpressionSyntax typeOfExpr) + continue; + + var typeInfo = semanticModel.GetTypeInfo(typeOfExpr.Type, context.CancellationToken); + if (typeInfo.Type is not INamedTypeSymbol type) + continue; - if (type.Name == exceptionName && type.ContainingNamespace?.ToDisplayString() == "System") - { - context.ReportDiagnostic(Diagnostic.Create( - RuleGeneralThrows, - typeOfExpr.GetLocation(), // ✅ precise location - type.Name)); - } + if (nameof(Exception).Equals(type.Name, StringComparison.Ordinal) && nameof(System).Equals(type.ContainingNamespace?.ToDisplayString(), StringComparison.Ordinal)) + { + context.ReportDiagnostic(Diagnostic.Create( + RuleGeneralThrows, + typeOfExpr.GetLocation(), // ✅ precise location + type.Name)); } } } diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs index d85f96c..deaa13e 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Xml; using System.Xml.Linq; using Microsoft.CodeAnalysis;