diff --git a/CheckedExceptions/AnalyzerSettings.cs b/CheckedExceptions/AnalyzerSettings.cs index 23a6bac..d5ce867 100644 --- a/CheckedExceptions/AnalyzerSettings.cs +++ b/CheckedExceptions/AnalyzerSettings.cs @@ -2,13 +2,13 @@ namespace Sundstrom.CheckedExceptions; using System.Text.Json.Serialization; -public partial class AnalyzerSettings +public class AnalyzerSettings(IReadOnlyList ignoredExceptions, IReadOnlyDictionary informationalExceptions) { - [JsonPropertyName("ignoredExceptions")] - public IEnumerable IgnoredExceptions { get; set; } = new List(); + public IReadOnlyList IgnoredExceptions { get; } = ignoredExceptions; - [JsonPropertyName("informationalExceptions")] - public IDictionary InformationalExceptions { get; set; } = new Dictionary(); + public IReadOnlyDictionary InformationalExceptions { get; } = 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..a497d48 100644 --- a/CheckedExceptions/AttributeHelper.cs +++ b/CheckedExceptions/AttributeHelper.cs @@ -1,17 +1,20 @@ 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; // 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 358f8cb..43c56b5 100644 --- a/CheckedExceptions/CheckedExceptions.csproj +++ b/CheckedExceptions/CheckedExceptions.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 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 c35837c..487cb84 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; @@ -26,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)); } } } @@ -61,28 +59,30 @@ private void CheckForDuplicateThrowsDeclarations( SyntaxNodeAnalysisContext context) { var semanticModel = context.SemanticModel; - var seen = new HashSet(SymbolEqualityComparer.Default); + + HashSet? seen = null; foreach (var throwsAttribute in throwsAttributes) { - foreach (var arg in throwsAttribute.ArgumentList?.Arguments ?? []) + Debug.Assert(throwsAttribute is not null); + + seen ??= new HashSet(SymbolEqualityComparer.Default); + + foreach (var arg in throwsAttribute!.ArgumentList?.Arguments ?? []) { - 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 (!seen.Add(exceptionType)) { - var typeInfo = semanticModel.GetTypeInfo(typeOfExpr.Type, context.CancellationToken); - var exceptionType = typeInfo.Type as INamedTypeSymbol; - if (exceptionType == null) - continue; - - if (seen.Contains(exceptionType)) - { - context.ReportDiagnostic(Diagnostic.Create( - RuleDuplicateDeclarations, - typeOfExpr.GetLocation(), // ✅ precise location - exceptionType.Name)); - } - - seen.Add(exceptionType); + context.ReportDiagnostic(Diagnostic.Create( + RuleDuplicateDeclarations, + typeOfExpr.GetLocation(), // ✅ precise location + exceptionType.Name)); } } } diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.GeneralThrows.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.GeneralThrows.cs index 8c5aa99..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; - if (type.Name == exceptionName && type.ContainingNamespace?.ToDisplayString() == "System") - { - context.ReportDiagnostic(Diagnostic.Create( - RuleGeneralThrows, - typeOfExpr.GetLocation(), // ✅ precise location - type.Name)); - } + var typeInfo = semanticModel.GetTypeInfo(typeOfExpr.Type, context.CancellationToken); + if (typeInfo.Type is not INamedTypeSymbol type) + continue; + + 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)); } } } @@ -50,34 +47,29 @@ private static void CheckForGeneralExceptionThrows( #region Lambda expression and Local functions private void CheckForGeneralExceptionThrows( - SyntaxNodeAnalysisContext context, - List throwsAttributes) + IEnumerable throwsAttributes, + SyntaxNodeAnalysisContext context) { - const string generalExceptionName = "Exception"; - const string generalExceptionNamespace = "System"; - var semanticModel = context.SemanticModel; foreach (var attribute in throwsAttributes) { foreach (var arg in attribute.ArgumentList?.Arguments ?? []) { - if (arg.Expression is TypeOfExpressionSyntax typeOfExpr) - { - var typeInfo = semanticModel.GetTypeInfo(typeOfExpr.Type, context.CancellationToken); - var type = typeInfo.Type as INamedTypeSymbol; + if (arg.Expression is not TypeOfExpressionSyntax typeOfExpr) + continue; - if (type is null) - continue; + var typeInfo = semanticModel.GetTypeInfo(typeOfExpr.Type, context.CancellationToken); + if (typeInfo.Type is not INamedTypeSymbol type) + continue; - if (type.Name == generalExceptionName && - type.ContainingNamespace?.ToDisplayString() == generalExceptionNamespace) - { - context.ReportDiagnostic(Diagnostic.Create( - RuleGeneralThrows, - typeOfExpr.GetLocation(), // ✅ report precisely on typeof(Exception) - 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(), // ✅ report precisely on typeof(Exception) + type.Name)); } } } diff --git a/CheckedExceptions/CheckedExceptionsAnalyzer.Inheritance.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.Inheritance.cs index a50d397..736a710 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.Inheritance.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.Inheritance.cs @@ -1,16 +1,14 @@ using System.Collections.Immutable; +using System.Diagnostics; using System.Net.NetworkInformation; 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; @@ -23,7 +21,8 @@ MethodKind.EventAdd or MethodKind.EventRemove)) return; - var declaredExceptions = GetDistictExceptionTypes(throwsAttributes).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) return; @@ -42,7 +41,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()) { @@ -77,13 +76,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) { @@ -115,13 +114,11 @@ public static string FormatMethodSignature(IMethodSymbol methodSymbol) private bool IsTooGenericException(ITypeSymbol ex) { - var namedType = ex as INamedTypeSymbol; - if (namedType == null) - return false; + if (ex is not INamedTypeSymbol namedTypeSymbol) return false; - var fullName = namedType.ToDisplayString(); + 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) @@ -133,18 +130,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; @@ -162,6 +159,4 @@ private IEnumerable GetBaseOrInterfaceMethods(IMethodSymbol metho return results; } - - #endregion } \ No newline at end of file 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.XmlDocs.cs b/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs index 8b0063d..deaa13e 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.XmlDocs.cs @@ -32,7 +32,8 @@ partial class CheckedExceptionsAnalyzer } } - if (string.IsNullOrEmpty(assemblyPath)) + // 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 @@ -82,30 +83,74 @@ public record struct ParamInfo(string Name); public record struct ExceptionInfo(INamedTypeSymbol ExceptionType, string Description, IEnumerable Parameters); - private static IEnumerable GetExceptionTypesFromDocumentationCommentXml(Compilation compilation, XElement? xml) + 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; + string? cref = e.Attribute("cref")?.Value; + if (string.IsNullOrWhiteSpace(cref)) + { + return default; + } - var name = compilation.GetTypeByMetadataName(crefValue) ?? - compilation.GetTypeByMetadataName(crefValue.Split('.').Last()); + var exceptionTypeSymbol = GetExceptionTypeSymbolFromCref(cref!, compilation); + if (exceptionTypeSymbol is null) + { + return default; + } - var parameters = e.Elements("paramref").Select(x => new ParamInfo(x.Attribute("name").Value)); + string innerText = e.Value; - return new ExceptionInfo(name, innerText, parameters); - }); + 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; + } } /// @@ -121,7 +166,7 @@ private IEnumerable GetExceptionTypesFromDocumentationCommentXml( } // Attempt to get exceptions from XML documentation - return GetExceptionTypesFromDocumentationCommentXml(compilation, docCommentXml).ToList(); + return GetExceptionTypesFromDocumentationCommentXml(compilation, docCommentXml); } readonly bool loadFromProject = true; @@ -157,39 +202,37 @@ private IEnumerable GetExceptionTypesFromDocumentationCommentXml( public XElement? GetXmlDocumentation(Compilation compilation, ISymbol symbol) { var path = GetXmlDocumentationPath(compilation, symbol.ContainingAssembly); - if (path is not null) + if (path is null) { - if (!XmlDocPathsAndMembers.TryGetValue(path, out var lookup)) + return null; + } + + if (!XmlDocPathsAndMembers.TryGetValue(path, out var lookup) || lookup is null) + { + try { - try - { - using var reader = XmlReader.Create(path, new XmlReaderSettings - { - DtdProcessing = DtdProcessing.Ignore, - IgnoreComments = true, - IgnoreWhitespace = true, - CloseInput = true - }); - - var file = XmlDocumentationHelper.CreateMemberLookup(XDocument.Load(reader, LoadOptions.PreserveWhitespace)); - lookup = new ConcurrentDictionary(file); - XmlDocPathsAndMembers.TryAdd(path, lookup); - } - catch (Exception ex) when (ex is XmlException || ex is IOException || ex is UnauthorizedAccessException) + using var reader = XmlReader.Create(path, new XmlReaderSettings { - // Suppress AD0001-inducing exceptions in analyzers - XmlDocPathsAndMembers.TryAdd(path, new ConcurrentDictionary()); - return null; - } - } - var member = symbol.GetDocumentationCommentId(); + DtdProcessing = DtdProcessing.Ignore, + IgnoreComments = true, + IgnoreWhitespace = true, + CloseInput = true + }); - if (lookup.TryGetValue(member, out var xml)) + var file = XmlDocumentationHelper.CreateMemberLookup(XDocument.Load(reader, LoadOptions.PreserveWhitespace)); + lookup = new ConcurrentDictionary(file); + XmlDocPathsAndMembers[path] = lookup; + } + catch (Exception ex) when (ex is XmlException || ex is IOException || ex is UnauthorizedAccessException) { - return xml; + // Suppress AD0001-inducing exceptions in analyzers + XmlDocPathsAndMembers[path] = new ConcurrentDictionary(); + return null; } } - return null; + 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 b32c777..e09f6c0 100644 --- a/CheckedExceptions/CheckedExceptionsAnalyzer.cs +++ b/CheckedExceptions/CheckedExceptionsAnalyzer.cs @@ -7,7 +7,6 @@ using System.Net.Sockets; using System.Reflection; using System.Text.Json; -using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -31,7 +30,7 @@ public partial class CheckedExceptionsAnalyzer : DiagnosticAnalyzer public const string DiagnosticIdRedundantTypedCatchClause = "THROW009"; public const string DiagnosticIdThrowsDeclarationNotValidOnFullProperty = "THROW010"; - public static IEnumerable AllDiagnosticsIds = [DiagnosticIdUnhandled, DiagnosticIdGeneralThrows, DiagnosticIdGeneralThrow, DiagnosticIdDuplicateDeclarations]; + public static readonly IEnumerable AllDiagnosticsIds = [DiagnosticIdUnhandled, DiagnosticIdGeneralThrows, DiagnosticIdGeneralThrow, DiagnosticIdDuplicateDeclarations]; private static readonly DiagnosticDescriptor RuleUnhandledException = new( DiagnosticIdUnhandled, @@ -126,6 +125,14 @@ public partial class CheckedExceptionsAnalyzer : DiagnosticAnalyzer public override ImmutableArray SupportedDiagnostics => [RuleUnhandledException, RuleIgnoredException, RuleGeneralThrows, RuleGeneralThrow, RuleDuplicateDeclarations, RuleMissingThrowsOnBaseMember, RuleMissingThrowsFromBaseMember, RuleDuplicateThrowsByHierarchy, RuleRedundantTypedCatchClause, RuleThrowsDeclarationNotValidOnFullProperty]; + 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); @@ -222,32 +229,23 @@ private void AnalyzeTryStatement(SyntaxNodeAnalysisContext context) } } - private const string SettingsFileName = "CheckedExceptions.settings.json"; - private AnalyzerSettings GetAnalyzerSettings(AnalyzerOptions analyzerOptions) { - if (!configs.TryGetValue(analyzerOptions, out var config)) + return configs.GetOrAdd(analyzerOptions, _ => { - 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; - } - } - } + var configFileText = analyzerOptions.AdditionalFiles + .FirstOrDefault(f => SettingsFileName.Equals(Path.GetFileName(f.Path), StringComparison.OrdinalIgnoreCase)) + ?.GetText()?.ToString(); - config ??= new AnalyzerSettings(); // Return default options if config file is not found + AnalyzerSettings? val = null; - configs.TryAdd(analyzerOptions, config); - } + if (configFileText is not null) + { + val = JsonSerializer.Deserialize(configFileText, _settingsFileJsonOptions); + } - return config ?? new AnalyzerSettings(); + return val ?? AnalyzerSettings.CreateWithDefaults(); // Return default options if the config file is not found + }); } private void AnalyzeLambdaExpression(SyntaxNodeAnalysisContext context) @@ -271,13 +269,9 @@ private void AnalyzeFunctionAttributes(SyntaxNode node, IEnumerable @@ -297,9 +291,6 @@ private void AnalyzeMethodSymbol(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; - if (methodSymbol is null) - return; - var throwsAttributes = GetThrowsAttributes(methodSymbol).ToImmutableArray(); CheckForCompatibilityWithBaseOrInterface(context, throwsAttributes); @@ -323,7 +314,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); } @@ -351,11 +342,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(); } @@ -371,27 +363,25 @@ 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) { - // 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); + 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,8 +482,7 @@ private HashSet CollectUnhandledExceptions(SyntaxNodeAnalysisC // Remove exceptions that are caught by the inner catch clauses var caughtExceptions = GetCaughtExceptions(tryStatement.Catches, context.SemanticModel); - innerUnhandledExceptions.RemoveWhere(exceptionType => - IsExceptionCaught(exceptionType, caughtExceptions)); + innerUnhandledExceptions.RemoveWhere(exceptionType => IsExceptionCaught(exceptionType, caughtExceptions)); // Add any exceptions that are not handled in the inner try block unhandledExceptions.UnionWith(innerUnhandledExceptions); @@ -517,161 +506,93 @@ private HashSet CollectExceptionsFromStatement(SyntaxNodeAnaly var exceptions = new HashSet(SymbolEqualityComparer.Default); - // Collect exceptions from throw statements - var throwStatements = statement.DescendantNodesAndSelf().OfType(); - foreach (var throwStatement in throwStatements) + foreach (var s in statement.DescendantNodesAndSelf()) { - if (throwStatement.Expression is not null) + switch (s) { - 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 statements + case ThrowStatementSyntax throwStatement: + CollectExpressionsFromThrows(throwStatement, throwStatement.Expression); + break; - // 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 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; } } - // 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); + return exceptions; - xmlExceptionTypes = ProcessNullable(context, invocation, methodSymbol, xmlExceptionTypes); + void CollectExpressionsFromThrows(SyntaxNode throwExpression, ExpressionSyntax? subExpression) + { + if (subExpression is null) return; - if (xmlExceptionTypes.Any()) - { - exceptionTypes.AddRange(xmlExceptionTypes.Select(x => x.ExceptionType)); - } + if (semanticModel.GetTypeInfo(subExpression).Type is not INamedTypeSymbol exceptionType) return; - foreach (var exceptionType in exceptionTypes) - { - if (ShouldIncludeException(exceptionType, invocation, settings)) - { - exceptions.Add(exceptionType); - } - } + if (ShouldIncludeException(exceptionType, throwExpression, settings)) + { + exceptions.Add(exceptionType); } } - var objectCreations = statement.DescendantNodesAndSelf().OfType(); - foreach (var objectCreation in objectCreations) + void CollectExpressionsForMethodSymbols(ExpressionSyntax expression) { - var methodSymbol = semanticModel.GetSymbolInfo(objectCreation).Symbol as IMethodSymbol; - if (methodSymbol is not null) - { - var exceptionTypes = GetExceptionTypes(methodSymbol); + if (semanticModel.GetSymbolInfo(expression).Symbol is not IMethodSymbol methodSymbol) return; - // Get exceptions from XML documentation - var xmlExceptionTypes = GetExceptionTypesFromDocumentationCommentXml(semanticModel.Compilation, methodSymbol); + var exceptionTypes = GetExceptionTypes(methodSymbol); - xmlExceptionTypes = ProcessNullable(context, objectCreation, methodSymbol, xmlExceptionTypes); + // Get exceptions from XML documentation + var xmlExceptionTypes = GetExceptionTypesFromDocumentationCommentXml(semanticModel.Compilation, methodSymbol); - if (xmlExceptionTypes.Any()) - { - exceptionTypes.AddRange(xmlExceptionTypes.Select(x => x.ExceptionType)); - } + xmlExceptionTypes = ProcessNullable(context, expression, methodSymbol, xmlExceptionTypes); - foreach (var exceptionType in exceptionTypes) - { - if (ShouldIncludeException(exceptionType, objectCreation, settings)) - { - exceptions.Add(exceptionType); - } - } + if (xmlExceptionTypes.Any()) + { + exceptionTypes = exceptionTypes.Concat(xmlExceptionTypes.Select(x => x.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) + foreach (var exceptionType in exceptionTypes) { - HashSet exceptionTypes = GetPropertyExceptionTypes(context, memberAccess, propertySymbol); - - foreach (var exceptionType in exceptionTypes) + if (ShouldIncludeException(exceptionType, expression, settings)) { - if (ShouldIncludeException(exceptionType, memberAccess, settings)) - { - exceptions.Add(exceptionType); - } + exceptions.Add(exceptionType); } } } - var elementAccessExpressions = statement.DescendantNodesAndSelf().OfType(); - foreach (var elementAccess in elementAccessExpressions) + void CollectExpressionsForPropertySymbols(ExpressionSyntax expression) { - var propertySymbol = semanticModel.GetSymbolInfo(elementAccess).Symbol as IPropertySymbol; - if (propertySymbol is not null) - { - HashSet exceptionTypes = GetPropertyExceptionTypes(context, elementAccess, propertySymbol); + if (semanticModel.GetSymbolInfo(expression).Symbol is not IPropertySymbol propertySymbol) return; - foreach (var exceptionType in exceptionTypes) - { - if (ShouldIncludeException(exceptionType, elementAccess, settings)) - { - exceptions.Add(exceptionType); - } - } - } - } + HashSet exceptionTypes = GetPropertyExceptionTypes(context, expression, propertySymbol); - var identifierExpressions = statement.DescendantNodesAndSelf().OfType(); - foreach (var identifier in identifierExpressions) - { - var propertySymbol = semanticModel.GetSymbolInfo(identifier).Symbol as IPropertySymbol; - if (propertySymbol is not null) + foreach (var exceptionType in exceptionTypes) { - HashSet exceptionTypes = GetPropertyExceptionTypes(context, identifier, propertySymbol); - - foreach (var exceptionType in exceptionTypes) + if (ShouldIncludeException(exceptionType, expression, settings)) { - if (exceptionType is not null) - { - if (ShouldIncludeException(exceptionType, identifier, settings)) - { - exceptions.Add(exceptionType); - } - } + exceptions.Add(exceptionType); } } } - - return exceptions; } 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(); @@ -691,7 +612,7 @@ public bool ShouldIncludeException(INamedTypeSymbol exceptionType, SyntaxNode no return true; } - private HashSet GetCaughtExceptions(SyntaxList catchClauses, SemanticModel semanticModel) + private HashSet? GetCaughtExceptions(SyntaxList catchClauses, SemanticModel semanticModel) { var caughtExceptions = new HashSet(SymbolEqualityComparer.Default); @@ -716,7 +637,7 @@ private HashSet GetCaughtExceptions(SyntaxList caughtExceptions) + private bool IsExceptionCaught(INamedTypeSymbol exceptionType, HashSet? caughtExceptions) { if (caughtExceptions is null) { @@ -774,7 +695,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; @@ -839,7 +760,7 @@ private void AnalyzeMethodCall(SyntaxNodeAnalysisContext context) /// /// Resolves the target method symbol from a delegate, lambda, or method group. /// - private IMethodSymbol GetTargetMethodSymbol(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation) + private IMethodSymbol? GetTargetMethodSymbol(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocation) { var expression = invocation.Expression; @@ -963,14 +884,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); } } @@ -983,13 +899,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); } } @@ -1052,28 +964,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) { @@ -1104,18 +1005,29 @@ 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) return; - List exceptionTypes = GetExceptionTypes(methodSymbol); + IEnumerable exceptionTypes = GetExceptionTypes(methodSymbol); // Get exceptions from XML documentation var xmlExceptionTypes = GetExceptionTypesFromDocumentationCommentXml(context.Compilation, methodSymbol); @@ -1124,25 +1036,22 @@ private void AnalyzeMemberExceptions(SyntaxNodeAnalysisContext context, SyntaxNo if (xmlExceptionTypes.Any()) { - exceptionTypes.AddRange(xmlExceptionTypes.Select(x => x.ExceptionType)); + exceptionTypes = exceptionTypes.Concat(xmlExceptionTypes.Select(x => x.ExceptionType)); } - exceptionTypes = ProcessNullable(context, node, methodSymbol, exceptionTypes).ToList(); + exceptionTypes = ProcessNullable(context, node, methodSymbol, exceptionTypes) + .Distinct(SymbolEqualityComparer.Default) + .OfType(); - foreach (var exceptionType in exceptionTypes.Distinct(SymbolEqualityComparer.Default).OfType()) + foreach (var exceptionType in exceptionTypes) { 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 argumentNullExceptionTypeSymbol = context.Compilation.GetTypeByMetadataName("System.ArgumentNullException"); var isCompilationNullableEnabled = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Enable; @@ -1178,8 +1087,9 @@ private static IEnumerable ProcessNullable(SyntaxNodeAnalysisCont return false; } } + return true; - }).ToList(); + }); } } @@ -1188,10 +1098,7 @@ private static IEnumerable ProcessNullable(SyntaxNodeAnalysisCont private static IEnumerable ProcessNullable(SyntaxNodeAnalysisContext context, SyntaxNode node, IMethodSymbol methodSymbol, IEnumerable exceptions) { - if (argumentNullExceptionTypeSymbol is null) - { - argumentNullExceptionTypeSymbol = context.Compilation.GetTypeByMetadataName("System.ArgumentNullException"); - } + var argumentNullExceptionTypeSymbol = context.Compilation.GetTypeByMetadataName("System.ArgumentNullException"); var isCompilationNullableEnabled = context.Compilation.Options.NullableContextOptions is NullableContextOptions.Enable; @@ -1206,24 +1113,22 @@ private static IEnumerable ProcessNullable(SyntaxNodeAnalysisC return exceptions; } - private static List GetExceptionTypes(ISymbol symbol) + private static IEnumerable GetExceptionTypes(ISymbol symbol) { // Get exceptions from Throws attributes var exceptionAttributes = GetThrowsAttributes(symbol); - return GetDistictExceptionTypes(exceptionAttributes).ToList(); + return GetDistinctExceptionTypes(exceptionAttributes); } - private static List GetThrowsAttributes(ISymbol symbol) + private static IEnumerable GetThrowsAttributes(ISymbol symbol) { return GetThrowsAttributes(symbol.GetAttributes()); } - private static List GetThrowsAttributes(IEnumerable attributes) + private static IEnumerable GetThrowsAttributes(IEnumerable attributes) { - return attributes - .Where(attr => attr.AttributeClass?.Name is "ThrowsAttribute") - .ToList(); + return attributes.Where(attr => attr.AttributeClass?.Name is "ThrowsAttribute"); } /// @@ -1248,7 +1153,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) @@ -1264,7 +1169,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)) || @@ -1283,7 +1188,7 @@ private bool IsExceptionHandled(SyntaxNode node, INamedTypeSymbol exceptionType, } } - prevNode = current; + // prevNode = current; current = current.Parent; } @@ -1364,72 +1269,57 @@ private bool IsExceptionDeclaredInMember(SyntaxNodeAnalysisContext context, Synt { foreach (var ancestor in node.Ancestors()) { - ISymbol? symbol = null; - - switch (ancestor) + ISymbol? symbol; + if (ancestor is PropertyDeclarationSyntax propertyDeclaration) { - case BaseMethodDeclarationSyntax methodDeclaration: - symbol = context.SemanticModel.GetDeclaredSymbol(methodDeclaration); - break; + var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration); - case PropertyDeclarationSyntax propertyDeclaration: - var propertySymbol = context.SemanticModel.GetDeclaredSymbol(propertyDeclaration); + // Don't continue with the analysis if it's a full property with accessors + // In that case, the accessors are analyzed separately + if ((propertySymbol?.GetMethod is not null && propertySymbol?.SetMethod is not null) + || (propertySymbol?.GetMethod is null && propertySymbol?.SetMethod is not null)) + { + return false; + } - // Don't continue with the analysis if it's a full property with accessors - // In that case, the accessors are analyzed separately - if ((propertySymbol?.GetMethod is not null && propertySymbol?.SetMethod is not null) - || (propertySymbol?.GetMethod is null && propertySymbol?.SetMethod is not null)) + var propertySyntaxRef = propertySymbol?.DeclaringSyntaxReferences.FirstOrDefault(); + if (propertySyntaxRef is not null && propertySyntaxRef.GetSyntax() is PropertyDeclarationSyntax basePropertyDeclaration) + { + if (basePropertyDeclaration.ExpressionBody is null) { return false; } + } + + symbol = propertySymbol; + } + else symbol = ancestor switch + { + BaseMethodDeclarationSyntax methodDeclaration => context.SemanticModel.GetDeclaredSymbol(methodDeclaration), + AccessorDeclarationSyntax accessorDeclaration => context.SemanticModel.GetDeclaredSymbol(accessorDeclaration), + LocalFunctionStatementSyntax localFunction => context.SemanticModel.GetDeclaredSymbol(localFunction), + AnonymousFunctionExpressionSyntax anonymousFunction => context.SemanticModel.GetSymbolInfo(anonymousFunction).Symbol as IMethodSymbol, + _ => null, + }; - var propertySyntaxRef = propertySymbol?.DeclaringSyntaxReferences.FirstOrDefault(); - if (propertySyntaxRef is not null && propertySyntaxRef.GetSyntax() is PropertyDeclarationSyntax basePropertyDeclaration) - { - if (basePropertyDeclaration.ExpressionBody is null) - { - return false; - } - } - - symbol = propertySymbol; - break; - - case AccessorDeclarationSyntax accessorDeclaration: - symbol = context.SemanticModel.GetDeclaredSymbol(accessorDeclaration); - break; - - case LocalFunctionStatementSyntax localFunction: - symbol = context.SemanticModel.GetDeclaredSymbol(localFunction); - break; - - case AnonymousFunctionExpressionSyntax anonymousFunction: - symbol = context.SemanticModel.GetSymbolInfo(anonymousFunction).Symbol as IMethodSymbol; - break; + if (symbol is null) + continue; // Continue up to next node - default: - // Continue up to next node - continue; - } + if (IsExceptionDeclaredInSymbol(symbol, exceptionType)) + return true; - if (symbol is not null) + if (ancestor is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax) { - if (IsExceptionDeclaredInSymbol(symbol, 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; - } + // 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(ISymbol symbol, INamedTypeSymbol exceptionType) + private bool IsExceptionDeclaredInSymbol(ISymbol? symbol, INamedTypeSymbol exceptionType) { if (symbol 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 ); diff --git a/Directory.Packages.props b/Directory.Packages.props index a5943c0..736fdca 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,21 +1,21 @@ - - - true - false - - - - - - - - - - - - - - - - + + + true + false + + + + + + + + + + + + + + + + \ No newline at end of file